Grouping elements that each group contains only one object with specified field

889 Views Asked by At

I have a problem with grouping java objects. Let's look at example object:

public class MyObject {

    private String field1;

    public MyObject(String field1) {
        this.field1 = field1;
    }
}

What i want to achieve is grouping MyObject's in such a way that each group contains only one object with specified field1 value. For example, for such list of elements:

  public static void main(String[] args) {
    
    MyObject o1 = new MyObject("1");
    MyObject o2 = new MyObject("1");
    MyObject o3 = new MyObject("1");

    MyObject o4 = new MyObject("2");
    MyObject o5 = new MyObject("2");

    MyObject o6 = new MyObject("3");

    List<MyObject> list = Arrays.asList(o1, o2, o3, o4, o5, o6);
    List<List<MyObject>> listsWithUniqueField1Values = new ArrayList<>();

I want to get listsWithUniqueField1Values looks like that:

[
    [
        MyObject{field1='1'}, 
        MyObject{field1='2'}, 
        MyObject{field1='3'}
    ], 
    [   
        MyObject{field1='1'}, 
        MyObject{field1='2'}
    ], 
    [
        MyObject{field1='1'}
    ]
]

I've tried to acheive it in effective way with using java.util.stream.Collectors.groupingBy method, but i faild.

4

There are 4 best solutions below

2
On

I don't think you can do with it with groupingBy. Here is my solution - I also added an autogenerated equals, hashCode, and toString

public class SO67140234 {

    public static void main(String[] args) {

        MyObject o1 = new MyObject("1");
        MyObject o2 = new MyObject("1");
        MyObject o3 = new MyObject("1");

        MyObject o4 = new MyObject("2");
        MyObject o5 = new MyObject("2");

        MyObject o6 = new MyObject("3");

        List<MyObject> list = Arrays.asList(o1, o2, o3, o4, o5, o6);
        List<Set<MyObject>> listsWithUniqueField1Values = new ArrayList<>();

        outer:
        for (MyObject obj : list) {
            for (Set<MyObject> bucket : listsWithUniqueField1Values) {
                if (bucket.add(obj)) {
                    continue outer;
                }
            }
            listsWithUniqueField1Values.add(new HashSet<>(Collections.singleton(obj)));
        }

        System.out.println(listsWithUniqueField1Values);
    }

}

class MyObject {

    private final String field1;

    public MyObject(String field1) {
        this.field1 = field1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyObject myObject = (MyObject) o;
        return Objects.equals(field1, myObject.field1);
    }

    @Override
    public int hashCode() {
        return Objects.hash(field1);
    }

    @Override
    public String toString() {
        return "MyObject{" +
            "field1='" + field1 + '\'' +
            '}';
    }
}
2
On

In order to group by instances of MyObject, this class needs to implement equals and hashCode methods, also field1 should be final to avoid corruption of hashCode upon changing its value.

public class MyObject {

    private final String field1;

    public MyObject(String field1) {
        this.field1 = field1;
    }

    public String getField1() {return this.field1;}

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (null == o || !(o instanceof MyObject)) return false;
        MyObject that = (MyObject) o;
        return Objects.equals(this.field1, that.field1);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.field1);
    }

    @Override
    public String toString() {
        return "field1=" + this.field1;
    }
}

Collectors.groupingBy cannot be used to obtain the required result, but a custom Stream::collect operation may be applied to create a list of sets of unique MyObject instances (somewhat reminding @Rubydesic's solution but without nested loop).

List<MyObject> list = Arrays.asList(o1, o4, o5, o2, o6, o3);

List<Set<MyObject>> result = list.stream()
    .collect(
        ArrayList::new, //  `Supplier<ArrayList<Set<>>>`
        (lst, x) -> {   // accumulator
            for (Set<MyObject> set : lst) { 
                if (set.add(x)) {
                    return; // found a bucket to place MyObject instance
                }
            }
            // create new bucket 
            Set<MyObject> newSet = new HashSet<>(); 
            newSet.add(x); 
            lst.add(newSet);
        },
        (lst1, lst2) -> {} // empty combiner
    );

    System.out.println(result);

Output :

[[field1=1, field1=2, field1=3], [field1=1, field1=2], [field1=1]]
0
On

You could do this using a groupingBy itself. (Without the need of equals or hashCode)

  1. First group using field1. This would give a map as:
{ 1 : [1,1,1], 2 : [2,2], 3 : [3] }
  1. Now for each of these keys, iterate their respective lists and add each MyObject to a different list in listsWithUniqueField1Values.

a. First processing for key 1, the list becomes [[1]] -> [[1], [1]] -> [[1], [1], [1]].

b. Then key 2, the list becomes [[1,2], [1], [1]] -> [[1,2], [1,2], [1]].

c. The for key 3, the list becomes [[1,2,3], [1,2], [1]].

Code :

List<List<MyObject>> uniqueList = new ArrayList<>();
list.stream()
    .collect(Collectors.groupingBy(MyObject::getField1))
    .values()
    .stream()
    .forEach(values -> addToList(uniqueList, values));
    
return uniqueList;

The below method addToList is where the unique list is populated. ListIterator is used over Iterator in this case, as add method is available in ListIterator.

private static void addToList(List<List<MyObject>> uniqueList, List<MyObject> values) {
    ListIterator<List<MyObject>> iterator = uniqueList.listIterator();
    for (MyObject o : values) {
        List<MyObject> list;
        if (!iterator.hasNext()) {
            // the object needs to be added to a new list.
            list = new ArrayList<>();
            iterator.add(list);
        } else {
            list = iterator.next();
        }
        list.add(o);
    }
}
0
On

Assuming MyObject has a getter, one of the easiest way I can think of is to combine

  • Collectors.collectingAndThen
  • Collectors.groupingBy
  • A LinkedList
  • A method popping items from the LinkedList and inserting them inside of the result
List<List<MyObject>> finalResult = list.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.groupingBy(MyObject::getField1, Collectors.toCollection(LinkedList::new)),
                map -> {
                    List<List<MyObject>> result = new ArrayList<>();
                    Collection<LinkedList<MyObject>> values = map.values();
                    while (!values.isEmpty()) {
                        List<MyObject> subList = values.stream()
                                .map(LinkedList::pop)
                                .toList();
                        result.add(subList);
                        values.removeIf(LinkedList::isEmpty);
                    }
                    return result;
                }));

The result is

[
  [
    MyObject{field1='1'}, 
    MyObject{field1='2'}, 
    MyObject{field1='3'}
  ], 
  [
    MyObject{field1='1'}, 
    MyObject{field1='2'}
  ], 
  [
    MyObject{field1='1'}
  ]
]