2017-02-13 224 views
4

我爲多個集合的統一以下功能(包括重複的元素)多個集合的路口:Java中使用流+ lambda表達式

public static <T> List<T> unify(Collection<T>... collections) { 
     return Arrays.stream(collections) 
       .flatMap(Collection::stream) 
       .collect(Collectors.toList()); 
} 

這將是很好有一個函數與一個類似的簽名交集(使用類型相等)。例如:

public static <T> List<T> intersect(Collection<T>... collections) { 
    //Here is where the magic happens 
} 

我發現交叉功能的實現,但它不使用流:

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) { 
    Set<T> common = new LinkedHashSet<T>(); 
    if (!collections.isEmpty()) { 
     Iterator<? extends Collection<T>> iterator = collections.iterator(); 
     common.addAll(iterator.next()); 
     while (iterator.hasNext()) { 
      common.retainAll(iterator.next()); 
     } 
    } 
    return common; 
} 

有什麼辦法來實現類似功能的統一利用流的東西嗎?我在java8/stream api中沒有那麼經驗,因爲有些建議會非常有用。

+3

爲什麼你認爲你需要流? –

+0

僅僅是好奇心!我同意提及,我真的是新的Java 8 /流API,所以我目前正試圖學習更多的使用api :) –

+0

正確。就我個人而言,我覺得學習這些API的最好方法就是嘗試自己解決這樣的問題。試試看,如果您遇到困難,請回來一個**特定**問題,概述您的問題。 –

回答

6

你可以寫你自己收集的一些實用工具類,並使用它:

public static <T, S extends Collection<T>> Collector<S, ?, Set<T>> intersecting() { 
    class Acc { 
     Set<T> result; 

     void accept(S s) { 
      if(result == null) result = new HashSet<>(s); 
      else result.retainAll(s); 
     } 

     Acc combine(Acc other) { 
      if(result == null) return other; 
      if(other.result != null) result.retainAll(other.result); 
      return this; 
     } 
    } 
    return Collector.of(Acc::new, Acc::accept, Acc::combine, 
         acc -> acc.result == null ? Collections.emptySet() : acc.result, 
         Collector.Characteristics.UNORDERED); 
} 

的使用是非常簡單的:

Set<T> result = Arrays.stream(collections).collect(MyCollectors.intersecting()); 

然而要注意收集不能短路:即使中間結果是空集合,它仍然會處理流的其餘部分。

這樣的收集器很容易在我的免費StreamEx庫中獲得(請參閱MoreCollectors.intersecting())。它可以像上面那樣處理普通流,但是如果使用StreamEx(它擴展了普通流),它就會變成短路:處理可能實際上會提前停止。

+0

它看起來非常有趣。我稍後會仔細研究一下。你的圖書館看起來非常酷,我看到了!它提供了我在C#中從Linq中錯過的一些功能特性。不幸的是,對於我將使用intersect/unify函數的項目,Im只允許使用java.util和其他一些基本庫。 –

+1

@ Jota.Toledo,那麼你可以將收集器從答案複製到一些實用程序類並使用它。 –

1

我想也許會更有意義,使用SET而不是列表(也許這是你的問題一個錯字):

public static <T> Set<T> intersect(Collection<T>... collections) { 
    //Here is where the magic happens 
    return (Set<T>) Arrays.stream(collections).reduce(
      (a,b) -> { 
       Set<T> c = new HashSet<>(a); 
       c.retainAll(b); 
       return c; 
      }).orElseGet(HashSet::new); 
} 
+2

請注意,您的解決方案可能會產生很多垃圾:每個輸入集合都會創建一個額外的集合。當我們有很多短集合時,這會顯着減慢處理速度。 –

+0

感謝您的評論@Tagir。你是對的,有更高效的解決方案,比如你的優秀StreamEx庫。我將這裏的答案留作教育目的,因爲它很短,它證明了使用減少,並且不需要任何外部庫。 –

+0

@TagirValeev我也注意到了。我想知道這種方法的複雜性是什麼。 –

0

,這裏是一個集的實現。 retainAll()是一個Collection方法,所以它適用於所有這些方法。

public static <T> Set<T> intersect(Collection<T>... collections) 
{ 
    return new HashSet<T>(Arrays.stream(collections).reduce(
      ((a, b) -> { 
       a.retainAll(b); 
       return a; 
      }) 
    ).orElse(new HashSet<T>()); 
} 

並與列表<>如果訂單是重要的。

public static <T> List<T> intersect2(Collection<T>... collections) 
{ 
    return new ArrayList<T>(Arrays.stream(collections).reduce(
      ((a, b) -> { 
       a.retainAll(b); 
       return a; 
      }) 
    ).orElse(new ArrayList<T>())); 
} 

Java集合讓他們看起來幾乎相同。如果需要,您可以過濾清單,因爲它可能包含重複。

public static <T> List<T> intersect2(Collection<T>... collections) 
{ 
    return new ArrayList<T>(Arrays.stream(collections).reduce(
      ((a, b) -> { 
       a.retainAll(b); 
       return a; 
      }) 
    ).orElse(new ArrayList<T>())).stream().distinct()); 
} 
+0

我不明白這是什麼增加了我的答案,除了你現在將某些輸入集合改爲副作用,這似乎是不可取的。 –

0

可以按如下方式與流寫:

return collections.stream() 
     .findFirst()  // find the first collection 
     .map(HashSet::new) // make a set out of it 
     .map(first -> collections.stream() 
       .skip(1) // don't need to process the first one 
       .collect(() -> first, Set::retainAll, Set::retainAll) 
     ) 
     .orElseGet(HashSet::new); // if the input collection was empty, return empty set 

的3個參數的collect複製你的retainAll邏輯

的流實現爲您提供了靈活性,更容易調整的邏輯。例如,如果所有集合都是集合,則可能需要從最小集合開始,而不是第一集合(爲了性能)。要做到這一點,你可以用min(comparing(Collection::size))替換findFirst(),擺脫skip(1)。或者,您可以通過並行運行第二個數據流來了解是否通過使用的數據類型獲得更好的性能,並且您只需將stream更改爲parallelStream即可。

+2

這假定迭代次序是可重複的,所以'skip(1)'總是會跳過第一次迭代中遇到的第一個元素。但如果輸入是非特定的「集合」,則無法保證。即使如果集合的'Iterator'每次都以相同的順序報告元素,如果源沒有報告ORDERED特徵,'skip(1)'就不需要遵守這個順序(這可以用一個'HashSet'和一個並行流)。 – Holger

3

雖然很容易將retainAll想象成一個黑盒批量操作,它必須是實現交集操作的最有效方式,但它暗示了迭代每個元素的整個收集和測試,無論它是否包含在收集作爲參數傳遞。您在Set上調用它的事實並不意味着任何優勢,因爲它是集合,其方法將決定整體性能。

這意味着線性掃描一個集合並測試每個元素在所有其他集合中的包含將與每個集合執行retainAll相同。在首先遍歷集合最小的獎勵積分:

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) { 
    if(collections.isEmpty()) return Collections.emptySet(); 
    Collection<T> smallest 
     = Collections.min(collections, Comparator.comparingInt(Collection::size)); 
    return smallest.stream().distinct() 
     .filter(t -> collections.stream().allMatch(c -> c==smallest || c.contains(t))) 
     .collect(Collectors.toSet()); 
} 

,或者

public static <T> Set<T> intersect(Collection<? extends Collection<T>> collections) { 
    if(collections.isEmpty()) return Collections.emptySet(); 
    Collection<T> smallest 
     = Collections.min(collections, Comparator.comparingInt(Collection::size)); 
    HashSet<T> result=new HashSet<>(smallest); 
    result.removeIf(t -> collections.stream().anyMatch(c -> c!=smallest&& !c.contains(t))); 
    return result; 
} 
+0

它是一個有趣的方法!我想過把最小的收藏作爲一個起點,但我並沒有進一步發展我的想法。 –