2016-01-13 24 views
6

Próbuję grupować listę obiektów według atrybutów mulitple, używając Java 8 Collection-Stream.Dynamiczne grupowanie według określonych atrybutów z Collection.stream

Działa to całkiem dobrze:

public class MyClass 
{ 
    public String title; 
    public String type; 
    public String module; 
    public MyClass(String title, String type, String module) 
    { 
     this.type = type; 
     this.title = title; 
     this.module= module; 
    } 
} 

List<MyClass> data = new ArrayList(); 
data.add(new MyClass("1","A","B")); 
data.add(new MyClass("2","A","B")); 
data.add(new MyClass("3","A","C")); 
data.add(new MyClass("4","B","A")); 

Object result = data.stream().collect(Collectors.groupingBy((MyClass m) 
-> m.type, Collectors.groupingBy((MyClass m) -> m.module))); 

Ale chciałbym zrobić to trochę bardziej dynamiczny. Chcę tylko określić String-Array (lub List), które powinny być używane do GroupBy.

Coś jak:

Object groupListBy(List data, String[] groupByFieldNames) 
{ 
    //magic code 
} 

i chcę zadzwonić:

groupListBy(data, new String[]{"type","module"}); 

Jak mogę sprawić, że GroupBy-Metoda bardziej dynamiczny, jak w moim przykładzie?

Odpowiedz

10

Głównym problemem związanym z większą dynamiką tego kodu jest to, że nie wiesz z góry, ile elementów będzie grupować według grup. W takim przypadku najlepiej jest grupować według wszystkich elementów. Działa to, ponieważ dwie listy są równe, jeśli wszystkie ich elementy są równe i znajdują się w tej samej kolejności.

W tym przypadku zamiast grupowania według typu, a następnie modułu, będziemy grupować według listy składającej się z każdego typu danych i modułu.

private static Map<List<String>, List<MyClass>> groupListBy(List<MyClass> data, String[] groupByFieldNames) { 
    final MethodHandles.Lookup lookup = MethodHandles.lookup(); 
    List<MethodHandle> handles = 
     Arrays.stream(groupByFieldNames) 
       .map(field -> { 
        try { 
         return lookup.findGetter(MyClass.class, field, String.class); 
        } catch (Exception e) { 
         throw new RuntimeException(e); 
        } 
       }).collect(toList()); 
    return data.stream().collect(groupingBy(
      d -> handles.stream() 
         .map(handle -> { 
          try { 
           return (String) handle.invokeExact(d); 
          } catch (Throwable e) { 
           throw new RuntimeException(e); 
          } 
         }).collect(toList()) 
     )); 
} 

Pierwsza część kodu przekształca tablicy nazw pole, na List z MethodHandle. Dla każdego pola, MethodHandle są pobierane z tego pola: odbywa się to poprzez uzyskanie odnośnika od MethodHandles.lookup() i patrząc w górę uchwyt dla danej nazwy pola z findGetter:

Produkuje dawanie metoda uchwyt odczytu do niebędącego -statyczne pole.

Reszta kodu tworzy klasyfikator grupowania według. Wszystkie uchwyty są wywoływane w instancji danych, aby zwrócić listę wartości String. Ta Stream jest zbierana do List, aby służyć jako klasyfikator.

Przykładowy kod:

public static void main(String[] args) { 
    List<MyClass> data = new ArrayList<>(); 
    data.add(new MyClass("1", "A", "B")); 
    data.add(new MyClass("2", "A", "B")); 
    data.add(new MyClass("3", "A", "C")); 
    data.add(new MyClass("4", "B", "A")); 

    System.out.println(groupListBy(data, new String[] { "type", "module" })); 
} 

wyjściowa:

{[B, A]=[4], [A, B]=[1, 2], [A, C]=[3]} 

gdy MyClass.toString() jest nadpisane zwrócić title tylko.

5

Zamiast listy nazw można również rozważyć dostarczenie listy funkcji (z jednym obowiązkowym) do grupowania elementów.

Te funkcje powinny odwzorować element MyClass na obiekt, aby można było użyć Function<MyClass, ?>.

private static Map<List<Object>, List<MyClass>> groupListBy(List<MyClass> data, Function<MyClass, ?> mandatory, Function<MyClass, ?>... others) { 
    return data.stream() 
       .collect(groupingBy(cl -> Stream.concat(Stream.of(mandatory), Stream.of(others)).map(f -> f.apply(cl)).collect(toList()))); 
} 

A niektóre z przykładów połączeń:

groupListBy(data, m -> m.type); //group only by type 
groupListBy(data, m -> m.type, m -> m.module); //group by type and by module 

Oczywiście można zrobić to metoda rodzajowa tak, że zwraca Map<List<Object>, List<U>> z funkcjami typu U -> Object.