Home > OS >  Merge list of Objects by property
Merge list of Objects by property

Time:10-07

I have been searching around different groupBy and stream threads but cannot find the answer to my problem. Basically I have this object:

public class Object {
    string name;
    string type; 
}

And I return a list of these from the database. What I would like to then do is iterate through the list of objects and remove duplicate names and save a list of the second properties under one object, in a new object that looks like this:

public class NewObject {
    String name;
    List<String> types;
}

CodePudding user response:

You can do it like this.

  • use groupingBy to create a map of name, List of type
  • use the entryset of the map to create the new object.

I added the appropriate constructors and getters in the classes.

List<OldObject> list = ...
    
List<NewObject> newList = list.stream()
        .collect(Collectors.groupingBy(OldObject::getName,
                Collectors.mapping(OldObject::getType,
                        Collectors.toList())))
        .entrySet().stream()
        .map(e -> new NewObject(e.getKey(), e.getValue()))
        .toList();
    
}


class OldObject {
    String name;
    String type;
    
    public String getName() {
        return name;
    }
    
    public String getType() {
        return type;
    }
}


class NewObject {
    String name;
    List<String> types = new ArrayList<>();
    
    public NewObject(String name, List<String> types) {
        this.types = types;
        this.name = name;
    }
}

For your added enjoyment, I thought I would also offer the following:

  • create the map
  • conditionally create a new object if the name key is not present
  • in either case, add the type to the list instance of the NewObject instance in the map which is returned by the computeIfAbsent method.

When finished, just assign the values to your Collection.

Map<String, NewObject> map = new HashMap<>();
for (OldObject ob : list) {
    map.computeIfAbsent(ob.getName(),
            v -> new NewObject(ob.getName(),
                    new ArrayList<>()))
            .add(ob.getType());
}
Collection<NewObject> newLista = map.values();

Caveats:

  • values returns a Collection, not a list so you would need to use that or pass the Collection to a list constructor of some sort (e.g. ArrayList).
  • the requires the addition of a add method in the NewObject class.
  • you could also have a getter that returns the type list directly and do.
map.computeIfAbsent(ob.getName(),
            v -> new NewObject(ob.getName(),
                    new ArrayList<>()))
            .getTypeList().add(ob.getType());

Check out these additions to the Map interface

CodePudding user response:

I understand you don't want to remove duplicates but actually merge them.

I replaced your Object class name with OldObject to avoid confusion with the actual Java Object class.

Collecting to Map<String, List<String>> and then converting to List<NewObject>

You could write your own Collector (see later in the answer), but the easiest way of writing what you need would be to use the current groupingBy and toList Collectors and then using the resulting Map and create your newObject instance based on it to then again collect to a new List:

oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collectors.mapping(OldObject::getType, Collectors.toList())))
        .entrySet().stream()
        // Assuming a NewObject constructor that receives a String name and a List<String> types
        .map(e -> new NewObject(e.getKey(), e.getValue()))
        .collect(Collectors.toList());

Custom Collectors

If you are interested in learning how to make your own Collector, I made an example using a custom Collector as well:

oldObjectList.stream().collect(Collector.of(
        // Supplier
        () -> new ConcurrentHashMap<String, NewObject>(),
        // Accumulator
        (map, oldObject) -> {
            // Assuming a NewObject constructor that gets a name and creates a new empty List as types.
            map.computeIfAbsent(oldObject.getName(), name -> new NewObject(name)).getTypes().add(oldObject.getType());
        },
        // Combiner
        (map1, map2) -> {
            map2.forEach((k, v) -> map1.merge(k, v, (v1, v2) -> {
                v1.getTypes().addAll(v2.getTypes());
                return v1;
            }));
            return map1;
        },
        // Finisher
        map -> new ArrayList<>(map.values())
));

You can even combine groupingBy and a custom Collector as a merge function:

new ArrayList<>(oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collector.of(
        // Supplier
        // Assuming a NewObject constructor with no arguments that creates a new empty List as types.
        NewObject::new,
        // Accumulator
        (newObject, oldObject) -> {
            newObject.setName(oldObject.getName());
            newObject.getTypes().add(oldObject.getType());
        },
        // Combiner
        (newObject1, newObject2) -> {
            newObject1.getTypes().addAll(newObject2.getTypes());
            return newObject1;
        }
))).values());
  • Related