4

Is there an easy way to copy an object's property's onto another object of a different class which has the same field names using direct field access - i.e. when one of the classes does not have getters or setters for the fields? I can use org.springframework.beans.BeanUtils#copyProperties(Object source, Object target) when they both have getter and setter methods, but what can I do when they don't?

It may also be relevant that the fields are public.

I know that I can write my own code to do this using reflection, but I'm hoping that there's some library that provides a one-liner.

2
  • It is unclear to me how Commons Beanutils works, but you may give it a shot : commons.apache.org/proper/commons-beanutils/index.html Commented Dec 14, 2015 at 9:18
  • 1
    I do not even like the question, because it ends in unstable code design, which might/will break at runtime and not during compile time as it should be. Commented Dec 14, 2015 at 9:22

3 Answers 3

4

I didn't find a 3rd-party library to do this quite how I wanted. I'll paste my code here in case it is useful to anyone:

import java.lang.reflect.Field; import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An alternative to Spring's BeanUtils#copyProperties for classes that don't have getters and setters. */ public class FieldCopier { private static final Logger log = LoggerFactory.getLogger(FieldCopier.class); /** Always use the same instance, so that we can cache the fields. */ private static final FieldCopier instance = new FieldCopier(); /** Caching the paired fields cuts the time taken by about 25% */ private final Map<Map.Entry<Class<?>, Class<?>>, Map<Field, Field>> PAIRED_FIELDS = new ConcurrentHashMap<>(); /** Caching the fields cuts the time taken by about 50% */ private final Map<Class<?>, Field[]> FIELDS = new ConcurrentHashMap<>(); public static FieldCopier instance() { return instance; } private FieldCopier() { // do not instantiate } public <S, T> T copyFields(S source, T target) { Map<Field, Field> pairedFields = getPairedFields(source, target); for (Field sourceField : pairedFields.keySet()) { Field targetField = pairedFields.get(sourceField); try { Object value = getValue(source, sourceField); setValue(target, targetField, value); } catch(Throwable t) { throw new RuntimeException("Failed to copy field value", t); } } return target; } private <S, T> Map<Field, Field> getPairedFields(S source, T target) { Class<?> sourceClass = source.getClass(); Class<?> targetClass = target.getClass(); Map.Entry<Class<?>, Class<?>> sourceToTarget = new AbstractMap.SimpleImmutableEntry<>(sourceClass, targetClass); PAIRED_FIELDS.computeIfAbsent(sourceToTarget, st -> mapSourceFieldsToTargetFields(sourceClass, targetClass)); Map<Field, Field> pairedFields = PAIRED_FIELDS.get(sourceToTarget); return pairedFields; } private Map<Field, Field> mapSourceFieldsToTargetFields(Class<?> sourceClass, Class<?> targetClass) { Map<Field, Field> sourceFieldsToTargetFields = new HashMap<>(); Field[] sourceFields = getDeclaredFields(sourceClass); Field[] targetFields = getDeclaredFields(targetClass); for (Field sourceField : sourceFields) { if (sourceField.getName().equals("serialVersionUID")) { continue; } Field targetField = findCorrespondingField(targetFields, sourceField); if (targetField == null) { log.warn("No target field found for " + sourceField.getName()); continue; } if (Modifier.isFinal(targetField.getModifiers())) { log.warn("The target field " + targetField.getName() + " is final, and so cannot be written to"); continue; } sourceFieldsToTargetFields.put(sourceField, targetField); } return Collections.unmodifiableMap(sourceFieldsToTargetFields); } private Field[] getDeclaredFields(Class<?> clazz) { FIELDS.computeIfAbsent(clazz, Class::getDeclaredFields); return FIELDS.get(clazz); } private <S> Object getValue(S source, Field sourceField) throws IllegalArgumentException, IllegalAccessException { sourceField.setAccessible(true); return sourceField.get(source); } private <T> void setValue(T target, Field targetField, Object value) throws IllegalArgumentException, IllegalAccessException { targetField.setAccessible(true); targetField.set(target, value); } private Field findCorrespondingField(Field[] targetFields, Field sourceField) { for (Field targetField : targetFields) { if (sourceField.getName().equals(targetField.getName())) { if (sourceField.getType().equals(targetField.getType())) { return targetField; } else { log.warn("Different types for field " + sourceField.getName() + " source " + sourceField.getType() + " and target " + targetField.getType()); return null; } } } return null; } } 
Sign up to request clarification or add additional context in comments.

4 Comments

It really seems that all library methods use getter/setter methods instead of direct field access. I would recommand to check if a field is final in the target class because you can't overwrite final fields using reflection. this could replace the "serialVersionUID" check.
Good idea, @Casey. I've updated the code. I had already written a unit test case proving that final fields would remain unchanged. Interestingly, when I debug it in eclipse, it looks like it does write to final fields of the target object, but the test asserting that they don't change still passes. I guess that proves that I don't understand how eclipse's debugger works.
Appreciate you posting your code as we have a similar need where the destination objects are immutable, and have been able to customize for our purposes. Do you happen to have any unit tests to go along with this?
Why are you returning unmodifiableMap of sourceFieldsToTargetFields? I mean it's not something global it's a method scoped value and any referenced copy change won't be reflected on the next method call because every call to mapSourceFieldsToTargetFields will re-initialize sourceFieldsToTargetFields anyway.
1

Write a simple utility class for that and you got your one liner... this task is IMHO to easy to use a library for it.

Just keep in mind to make your fields accessible if they aren't by default. Here are two functions you could adapt from our codebase:

 public void injectIntoObject(Object o, Object value) { try { getField().set(o, value); } catch (IllegalArgumentException e) { throw new RuntimeException("Illegal argument while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'. Got one of type "+value.getClass().getCanonicalName()+" but needed one of "+type.getCanonicalName()+"!",e); } catch (IllegalAccessException e) { getField().setAccessible(true); try { getField().set(o, value); } catch (IllegalArgumentException e1) { throw new RuntimeException("Illegal argument while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'. Got one of type "+value.getClass().getCanonicalName()+" but needed one of "+type.getCanonicalName()+"!",e); } catch (IllegalAccessException e1) { throw new RuntimeException("Access exception while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'!",e); } } catch (Exception e) { throw new RuntimeException("Exception while setting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'!",e); } } public Object extractFromObject(Object o) { try { return getField().get(o); } catch (IllegalArgumentException e) { throw new RuntimeException("Illegal argument while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' but needed one of "+type.getCanonicalName()+"!",e); } catch (IllegalAccessException e) { getField().setAccessible(true); try { return getField().get(o); } catch (IllegalArgumentException e1) { throw new RuntimeException("Illegal argument while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' but needed one of "+type.getCanonicalName()+"!",e); } catch (IllegalAccessException e1) { throw new RuntimeException("Access exception while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"'!",e); } } catch (Exception e) { throw new RuntimeException("Exception while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"'!",e); } } 

getField() returns a java.lang.Field, should be easy to implement.

Comments

0

I would strongly suggest that you avoid using reflection for this, as it leads to code that is difficult to understand and maintain. (Reflection is ok for testing and when creating frameworks, other than this it probably creates more problems than it solves.)

Also, if a property of an object needs to be accessed by something other than the object, it needs a scope that is not private (or an accessor/getter that is not private). That is the whole point of variable scopes. Keeping a variable private without accessors, and then using it anyways through reflection is just wrong, and will just lead to problems, as you are creating code that lies to the reader.

public class MyClass { private Integer someInt; private String someString; private List<Double> someList; //... } public class MyOtherClass { private Integer someInt; private String someString; private List<Double> someList; private boolean somethingElse; public copyPropertiesFromMyClass(final MyClass myClass) { this.someInt = myClass.getSomeInt(); this.someString = myClass.getSomeString(); this.someList = new ArrayList<>(myClass.getSomeList()); } } 

2 Comments

The objects to copy stuff from and to are not of the same class!
If a different class needs to use properties of a class, then that's what getters are for. The properties are then by definition not private, keeping them private and then accessing them through reflection is just lying to yourself.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.