0

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.

Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
Paoloeno
  • 11
  • 1
  • what type of grouping is this? looks more like structuring data, could you share the specific use case or your attempted pseudo code? – Naman Apr 17 '21 at 19:24

4 Answers4

0

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 + '\'' +
            '}';
    }
}
Rubydesic
  • 3,386
  • 12
  • 27
0

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]]
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
  • This doesn't actually answer the question though, which requests grouping by UNIQUE field1 and not field1 which equals each other. – Rubydesic Apr 17 '21 at 16:33
  • @Rubydesic, you were right, I did not pay attention to the expected output; now the solution is updated to use Stream API. – Nowhere Man Apr 17 '21 at 18:05
0

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);
    }
}
Gautham M
  • 4,816
  • 3
  • 15
  • 37
0

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'}
  ]
]
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89