This works as long as you have POJOs with their own getters and setters. The method updates obj with non-null values from update. It calls setParameter() on obj with the return value of getParameter() on update:
public void merge(Object obj, Object update){
if(!obj.getClass().isAssignableFrom(update.getClass())){
return;
}
Method[] methods = obj.getClass().getMethods();
for(Method fromMethod: methods){
if(fromMethod.getDeclaringClass().equals(obj.getClass())
&& fromMethod.getName().startsWith("get")){
String fromName = fromMethod.getName();
String toName = fromName.replace("get", "set");
try {
Method toMetod = obj.getClass().getMethod(toName, fromMethod.getReturnType());
Object value = fromMethod.invoke(update, (Object[])null);
if(value != null){
toMetod.invoke(obj, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Answer from Alex Martín Jiménez on Stack OverflowThis works as long as you have POJOs with their own getters and setters. The method updates obj with non-null values from update. It calls setParameter() on obj with the return value of getParameter() on update:
public void merge(Object obj, Object update){
if(!obj.getClass().isAssignableFrom(update.getClass())){
return;
}
Method[] methods = obj.getClass().getMethods();
for(Method fromMethod: methods){
if(fromMethod.getDeclaringClass().equals(obj.getClass())
&& fromMethod.getName().startsWith("get")){
String fromName = fromMethod.getName();
String toName = fromName.replace("get", "set");
try {
Method toMetod = obj.getClass().getMethod(toName, fromMethod.getReturnType());
Object value = fromMethod.invoke(update, (Object[])null);
if(value != null){
toMetod.invoke(obj, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
I am using Spring Framework. I was facing the same issue on a project.
To solve it i used the class BeanUtils and the above method,
public static void copyProperties(Object source, Object target)
This is an example,
public class Model1 {
private String propertyA;
private String propertyB;
public Model1() {
this.propertyA = "";
this.propertyB = "";
}
public String getPropertyA() {
return this.propertyA;
}
public void setPropertyA(String propertyA) {
this.propertyA = propertyA;
}
public String getPropertyB() {
return this.propertyB;
}
public void setPropertyB(String propertyB) {
this.propertyB = propertyB;
}
}
public class Model2 {
private String propertyA;
public Model2() {
this.propertyA = "";
}
public String getPropertyA() {
return this.propertyA;
}
public void setPropertyA(String propertyA) {
this.propertyA = propertyA;
}
}
public class JustATest {
public void makeATest() {
// Initalize one model per class.
Model1 model1 = new Model1();
model1.setPropertyA("1a");
model1.setPropertyB("1b");
Model2 model2 = new Model2();
model2.setPropertyA("2a");
// Merge properties using BeanUtils class.
BeanUtils.copyProperties(model2, model1);
// The output.
System.out.println("Model1.propertyA:" + model1.getPropertyA(); //=> 2a
System.out.println("Model1.propertyB:" + model1.getPropertyB(); //=> 1b
}
}
Edited to address the issue of 4 specific fields allowed to mismatch.
@Hans-Martin Mosner is right that the remaining fields form a unique key. It's best to address this with the database, but it sounds like you can't in your circumstances. In this case, Option 1 seems to be the superior choice.
Option 3 is obviously undesirable because of its O(N^2) complexity, but why not Option 2? The main reason is that once we add an object to a set, we cannot retrieve it without iterating through the collection at O(N). So there's no way to "merge" two equal objects in O(1) (the main reason for preferring 2 to 3), other than simply replacing the existing object. This doesn't sound like what you wanted. You can use a modified form of Option 2 by using a HashMap<CustomObject, CustomObject>, but this is basically Option 1 except you're not keeping a list.
So what does this key look like?
If you want the key to be a CustomObject (for either Option 1 or modified Option 2), then we'll need to override hashCode() to use all and only the fields that we care about (e.g. like this). This is an especially nice option if you've already overridden equals(obj).
If you want a String key, then you can concatenate all the field values that we care about (including a separator to prevent values from one field flowing into another). E.g. assuming that field1, ... fieldN have a suitable toString():
String getKey(CustomObject obj) {
return obj.getField1() + "|" + ... + "|" obj.getFieldN();
}
Original answer assuming any 4 fields were allowed to mismatch.
As ugly as it seems, I think the most viable algorithm proposed is Option 3*. Here's why.
The Problem of Option 1
Let's suppose we have our list and we're going through elements one by one. We encounter the problem on the first element. How do we even know what the "concatenated string of all similar attributes" is for this element? We have nothing to compare against. In order to determine this, we need to scan through the rest of the elements and compare them to this particular element. It's up to you if we break once we find a match, or look for a better candidate / all candidates.
Okay, on to the next element. If it was marked as a duplicate already, then we might be able to skip this. Otherwise, then we are in exactly the same situation as before: we have to run through each of the subsequent elements looking for a potential match.
(You see where this is going, don't you?)
The Problem of Option 2
So you've decided to stick our objects in a Set. Great, we've got 2 popular choices: a HashSet and a TreeSet.
If we choose a HashSet, then we need to define a hashcode, particularly one such that equal objects have the same hashcode. Because of our fuzzy version of equality, defining a suitable hashcode is going to be really difficult. Given what I know of the problem, the only consistent hash seems to be a constant. (You may be able to do better, but it's not an easy task.) Practically this means that set.contains(myObj) will have linear lookup time, so we're back to O(N^2) for overall complexity.
If we choose a TreeSet, then we need to define an ordering. Again, good luck with finding one that works with our fuzzy equality. Worst case for this will also give us linear lookup time.
Aside
The problem of detecting duplicates is hard. For example, what potential duplicates do we have among this data?
A = { a: 1, b: 1 }, B = { a: 1, b: 2 }, C = { a: 2, b: 1 }, D = { a: 2, b: 2 }
Pairwise, A and D are each similar to both B and C. But B and C are not similar to each other; nor are A and D similar.
*Technically, option 3 takes N(N+1)/2 steps and not N^2, but I'm assuming that you are referring to the big-O.
This sort of problem is much easier solved if you know the meaning of those records and the process by which duplicates happen. Look at the actual fields and decide which ones should be "keys" and which ones should be "values", then you have basically solved it (you need to decide on a proper database representation but that's an implementation issue).
So if you already know the 4 fields that are variable, all others constitute the primary key. You could either simply concatenate them to form a hash key, or build a hierarchical map structure, or let a relational database engine figure out the details. All of these might be viable solutions depending on your problem details which you omitted from the question.
Just tested using reflection. The desired output is
merged person:Person{name=John, lastName=Snow}
public static void testReflection() {
Person p1 = new Person("John", null);
Person p2 = new Person(null, "Snow");
Person merged = (Person) mergePersons(p1, p2);
System.out.println("merged person:" + merged);
}
public static Object mergePersons(Object obj1, Object obj2) throws Exception {
Field[] allFields = obj1.getClass().getDeclaredFields();
for (Field field : allFields) {
if (Modifier.isPublic(field.getModifiers()) && field.isAccessible() && field.get(obj1) == null && field.get(obj2) != null) {
field.set(obj1, field.get(obj2));
}
}
return obj1;
}
mergePersons accepts two Objects.
Then it go through all fields and validate if the first object has a null value. If yes, then it verify if the second object is not nulled.
If this is true it assigns the value to the first Object.
Providing this solution you only access public data. If you want to access private data aswell, you need to remove the Modifier verification and set if accessible before like:
public static Object mergePersons(Object obj1, Object obj2) throws Exception {
Field[] allFields = obj1.getClass().getDeclaredFields();
for (Field field : allFields) {
if (!field.isAccessible() && Modifier.isPrivate(field.getModifiers()))
field.setAccessible(true);
if (field.get(obj1) == null && field.get(obj2) != null) {
field.set(obj1, field.get(obj2));
}
}
return obj1;
}
This is a quick (and presumptuous) approach that is basically the same as using reflection on the fields but instead uses:
- Groovy's built-in
getProperties()method on java.lang.Object, which provides us with a Map of its property names and values - Groovy's default Map constructor, which allows use to create instances of an Object given a Map of properties.
Given these two features, you can describe each object you want to merge as a Map of their properties, strip out the null-valued entries, combine the Maps together (and remove the pesky 'class' entry which is readonly), and use the merged Map to construct your merged instance.
class Person {
String first, last, middle
}
def p1 = new Person(first: 'bob')
def p2 = new Person(last: 'barker')
Person merged = (p1.properties.findAll { k, v -> v } // p1's non-null properties
+ p2.properties.findAll { k, v -> v }) // plus p2's non-null properties
.findAll { k, v -> k != 'class' } // excluding the 'class' property
assert merged.first == 'bob'
assert merged.last == 'barker'
assert merged.middle == null
You're going to have to go the reflection route. I'm assuming you have a default constructor, otherwise the following won't work. Also, it needs two same types. It won't copy inherited fields, for that, you also need to add some code from here.
@SuppressWarnings("unchecked")
public static <T> T mergeObjects(T first, T second) throws IllegalAccessException, InstantiationException {
Class<?> clazz = first.getClass();
Field[] fields = clazz.getDeclaredFields();
Object returnValue = clazz.newInstance();
for (Field field : fields) {
field.setAccessible(true);
Object value1 = field.get(first);
Object value2 = field.get(second);
Object value = (value1 != null) ? value1 : value2;
field.set(returnValue, value);
}
return (T) returnValue;
}
Here's an example
public static class ABC {
private int id;
private String name;
private int[] numbers;
public ABC() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int[] getNumbers() {
return numbers;
}
public void setNumbers(int[] numbers) {
this.numbers = numbers;
}
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
ABC abc = new ABC();
abc.setId(1);
abc.setName("Hello");
int[] newnumbers = new int[5];
for(int i = 0; i < newnumbers.length; i++) {
newnumbers[i] = i;
}
abc.setNumbers(newnumbers);
ABC abc2 = new ABC();
abc2.setName("World");
ABC abcFinal = mergeObjects(abc, abc2);
System.out.println("Properties of ABC Final:");
System.out.println("ID: " + abcFinal.getId());
System.out.println("Name: " + abcFinal.getName());
System.out.println("Numbers: " + Arrays.toString(abcFinal.getNumbers()));
}
Output:
Properties of ABC Final:
ID: 1
Name: Hello
Numbers: [0, 1, 2, 3, 4]
In case you want to create a new Object use:
public ABC mergeABC(ABC obj1, ABC obj2){
ABC retVal = new ABC();
retVal.name = ((obj1.name != null) ? obj1.name : obj2.name );
retVal.id = ((obj1.id != null) ? obj1.id : obj2.id );
retVal.salary = ((obj1.salary != null) ? obj1.salary : obj2.salary );
retVal.status = ((obj1.status != null) ? obj1.status : obj2.status );
return retVal;
}
If you want to "reuse" obj1 or obj2, simply use this as return value.
Its pretty easy to do using the org.springframework.beans.BeanUtils class provided by spring. Or the Apache Commons BeanUtils library which I believe Springs version is either based on or is the same as.
public static <T> T combine2Objects(T a, T b) throws InstantiationException, IllegalAccessException {
// would require a noargs constructor for the class, maybe you have a different way to create the result.
T result = (T) a.getClass().newInstance();
BeanUtils.copyProperties(a, result);
BeanUtils.copyProperties(b, result);
return result;
}
if you cant or dont have a noargs constructor maybe you just pass in the result
public static <T> T combine2Objects(T a, T b, T destination) {
BeanUtils.copyProperties(a, destination);
BeanUtils.copyProperties(b, destination);
return destination;
}
If you dont want null properties being copied you can use something like this:
public static void nullAwareBeanCopy(Object dest, Object source) throws IllegalAccessException, InvocationTargetException {
new BeanUtilsBean() {
@Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
if(value != null) {
super.copyProperty(dest, name, value);
}
}
}.copyProperties(dest, source);
}
Nested object solution
Heres a bit more robust solution. It supports nested object copying, objects 1+ level deep will no longer be copied by reference, instead Nested objects will be cloned or their properties be copied individually.
/**
* Copies all properties from sources to destination, does not copy null values and any nested objects will attempted to be
* either cloned or copied into the existing object. This is recursive. Should not cause any infinite recursion.
* @param dest object to copy props into (will mutate)
* @param sources
* @param <T> dest
* @return
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public static <T> T copyProperties(T dest, Object... sources) throws IllegalAccessException, InvocationTargetException {
// to keep from any chance infinite recursion lets limit each object to 1 instance at a time in the stack
final List<Object> lookingAt = new ArrayList<>();
BeanUtilsBean recursiveBeanUtils = new BeanUtilsBean() {
/**
* Check if the class name is an internal one
* @param name
* @return
*/
private boolean isInternal(String name) {
return name.startsWith("java.") || name.startsWith("javax.")
|| name.startsWith("com.sun.") || name.startsWith("javax.")
|| name.startsWith("oracle.");
}
/**
* Override to ensure that we dont end up in infinite recursion
* @param dest
* @param orig
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
@Override
public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
try {
// if we have an object in our list, that means we hit some sort of recursion, stop here.
if(lookingAt.stream().anyMatch(o->o == dest)) {
return; // recursion detected
}
lookingAt.add(dest);
super.copyProperties(dest, orig);
} finally {
lookingAt.remove(dest);
}
}
@Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
// dont copy over null values
if (value != null) {
// attempt to check if the value is a pojo we can clone using nested calls
if(!value.getClass().isPrimitive() && !value.getClass().isSynthetic() && !isInternal(value.getClass().getName())) {
try {
Object prop = super.getPropertyUtils().getProperty(dest, name);
// get current value, if its null then clone the value and set that to the value
if(prop == null) {
super.setProperty(dest, name, super.cloneBean(value));
} else {
// get the destination value and then recursively call
copyProperties(prop, value);
}
} catch (NoSuchMethodException e) {
return;
} catch (InstantiationException e) {
throw new RuntimeException("Nested property could not be cloned.", e);
}
} else {
super.copyProperty(dest, name, value);
}
}
}
};
for(Object source : sources) {
recursiveBeanUtils.copyProperties(dest, source);
}
return dest;
}
Its kinda quick and dirty but works well. Since it does use recursion and the potential is there for infinite recursion I did place in a safety against.
The below method will ignore the serialVersionUID, iterate through all the fields and copy the non-null values from object a --> object b if they are null in b. In other words, if any field is null in b, take it from a if there its not null.
public static <T> T combine2Objects(T a, T b) throws InstantiationException,IllegalAccessException{
T result = (T) a.getClass().newInstance();
Object[] fields = Arrays.stream(a.getClass().getDeclaredFields()).filter(f -> !f.getName().equals("serialVersionUID")).collect(Collectors.toList()).toArray();
for (Object fieldobj : fields) {
Field field = (Field) fieldobj;
field.set(result, field.get(b) != null ? field.get(b) : field.get(a));
}
return result;
}
There is a BeanUtils class in spring beans library.
BeanUtils.copyProperties(source, target);
As long as your classes contain the same property names the appropriate setter will be called in the target. It will ignore any properties which are not present in the target.
For your case you can do it using Apache or Spring bean utils.
org.apache.commons.beanutils.BeanUtils.copyProperties(Object destination, Object source)
org.springframework.beans.BeanUtils.copyProperties(Object source, Object dest)
Position of parameters is different in both cases.
java.util.Properties implements the java.util.Map interface, and so you can just treat it as such, and use methods like putAll to add the contents of another Map.
However, if you treat it like a Map, you need to be very careful with this:
new Properties(defaultProperties);
This often catches people out, because it looks like a copy constructor, but it isn't. If you use that constructor, and then call something like keySet() (inherited from its Hashtable superclass), you'll get an empty set, because the Map methods of Properties do not take account of the default Properties object that you passed into the constructor. The defaults are only recognised if you use the methods defined in Properties itself, such as getProperty and propertyNames, among others.
So if you need to merge two Properties objects, it is safer to do this:
Properties merged = new Properties();
merged.putAll(properties1);
merged.putAll(properties2);
This will give you more predictable results, rather than arbitrarily labelling one of them as the "default" property set.
Normally, I would recommend not treating Properties as a Map, because that was (in my opinion) an implementation mistake from the early days of Java (Properties should have contained a Hashtable, not extended it - that was lazy design), but the anemic interface defined in Properties itself doesn't give us many options.
Assuming you eventually would like to read the properties from a file, I'd go for loading both files in the same properties object like:
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream("default.properties"));
properties.load(getClass().getResourceAsStream("custom.properties"));