2017-02-13 79 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

它是一个有趣的方法!我想过把最小的收藏作为一个起点,但我并没有进一步发展我的想法。 –