2016-01-13 10 views
6

Java 8 Collection-Streamを使用して、複数の属性でオブジェクトのリストをグループ化しようとしています。Collection.streamの特定の属性による動的グループ化

これはかなりうまく機能:

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))); 

しかし、私はそれはもう少し動的にしたいと思います。 GroupByに使用するString-Array(またはList)を指定したいだけです。

ような何か:

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

と私は呼びたい:

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

がどのように私は私の例のように、GROUPBY-方法がよりダイナミックにすることができますか?

答えて

10

このコードをより動的にする上での主な問題は、グループ化する要素の数が事前にわからないことです。そのような場合は、すべての要素のうちListでグループ化することをお勧めします。これは、2つのリストが等しく、同じ順序であれば等しくなるためです。

この場合、タイプ別にグループ化するのではなく、各データタイプとモジュールからなるリストをグループ化します。

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()) 
     )); 
} 

コードの最初の部分はMethodHandleListにフィールド名の配列を変換します。各フィールドについて、MethodHandleは、そのフィールドの取得されます。これはMethodHandles.lookup()から、ルックアップを取得し、findGetterで指定されたフィールド名のハンドルを調べることにより行われます。

以外への読み取りアクセス権を与える方法ハンドルを生成します-staticフィールド。

残りのコードは、から分類する分類子を作成します。すべてのハンドルがデータインスタンス上で呼び出され、String値のリストが返されます。このStreamは、分類器として機能するためにListに集められます。

サンプルコード:

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" })); 
} 

出力:

MyClass.toString()のみ titleを返すようにオーバーライドされ
{[B, A]=[4], [A, B]=[1, 2], [A, C]=[3]} 

5

名前のリストの代わりに、要素をグループ化するための関数リスト(必須のもの)を指定することもできます。

これらの関数は、MyClassの要素をオブジェクトにマップする必要があるため、Function<MyClass, ?>を使用できます。それはタイプU -> Objectの機能をMap<List<Object>, List<U>>を返すようにしますが、この方法は一般的なことができます。もちろん、

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

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()))); 
} 

そして呼び出しのいくつかの例。

関連する問題