Java 8属性不同

Ric*_*chK 406 java collections java-8 java-stream

在Java 8中,如何Stream通过检查每个对象的属性的清晰度来使用API 过滤集合?

例如,我有一个Person对象列表,我想删除具有相同名称的人,

persons.stream().distinct();
Run Code Online (Sandbox Code Playgroud)

将使用Person对象的默认相等检查,所以我需要像,

persons.stream().distinct(p -> p.getName());
Run Code Online (Sandbox Code Playgroud)

不幸的是,该distinct()方法没有这种过载.如果不修改类中的相等性检查,Person是否可以简洁地执行此操作?

Stu*_*rks 486

考虑distinct成为一个有状态的过滤器.这是一个函数,它返回一个谓词,该谓词维护前面所看到的状态,并返回给定元素是否第一次被看到:

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}
Run Code Online (Sandbox Code Playgroud)

然后你可以写:

persons.stream().filter(distinctByKey(Person::getName))
Run Code Online (Sandbox Code Playgroud)

请注意,如果流是有序的并且并行运行,这将保留重复项中的任意元素,而不是第一个元素distinct().

(这与对这个问题的回答基本相同:Java Lambda Stream Distinct()在任意键上?)

  • 我想为了更好的兼容性,参数应该是`Function <?超级T,?>`,不是`功能<?super T,Object>`.还应该注意的是,对于有序并行流,这个解决方案并不能保证提取哪个对象(不像普通的`distinct()`).对于顺序流,使用CHM还有额外的开销(在@nosid解决方案中不存在).最后,这个解决方案违反了`filter`方法的契约,谓词必须是无状态的,如JavaDoc中所述.然而,投了赞成票. (26认同)
  • @holandaGo如果保存并重用`distinctByKey`返回的Predicate实例,它将失败.但是如果你每次都调用`distinctByKey`就行了,所以每次都会创建一个新的Predicate实例. (4认同)
  • 这太聪明了,完全不明显.*通常*这是一个有状态的lambda,底层的`CallSite`将链接到`get $ Lambda`方法 - 它将一直返回一个新的`Predicate`实例,但这些实例将共享相同的`map据我所知,`和`函数`.非常好! (3认同)
  • 有人可以解释一下如何每次创建一个新集合并保留以前的值吗? (3认同)
  • @java_newbie"distinctByKey"返回的Predicate实例不知道它是否在并行流中使用.它使用CHM以防并行使用,但这增加了顺序情况下的开销,如Tagir Valeev所述. (2认同)
  • 这不会在第二次运行时失败吗? (2认同)
  • @Chinmay不,不应该。如果使用`.filter(distinctByKey(...))`。它将执行一次该方法并返回谓词。因此,如果您在流中正确使用该地图,则基本上该地图已经在重复使用。如果将地图设为静态,则该地图将共享所有用途。因此,如果您有两个使用此`distinctByKey()`的流,则两者都将使用同一张地图,这不是您想要的。 (2认同)
  • 这个答案很棒。非常感谢。使用`ConcurrentHashMap.newKeySet()`代替Map本身可以稍微提高可读性:`Set&lt;Object&gt; seen = ConcurrentHashMap.newKeySet();return t -&gt; seen.add(t);` (2认同)
  • @AnyulRivas 在第一段代码中,`keyExtractor.apply(t)`可以替换为`person.getName()`。那么代码应该很容易阅读。 (2认同)
  • @Pr0pagate 它是静态的,因为它不依赖于声明它的类中的任何内容。因此,它可以位于实用程序类或其他类中。我不认为它是一个非静态方法,但它确实看起来有点奇怪,因为调用 `foo.distinctByKey()` 和 `bar.distinctByKey()` 没有任何东西可以做。处理 `foo` 或 `bar` 实例。 (2认同)
  • @javaAndBeyond:“persons.stream().filter(distinctByKey(Person::getName))”仅被调用一次。`distinctByKey(Person::getName)` 也是如此。`ConcurrentHashMap.newKeySet()` 也是如此。这里的关键概念是“distinctByKey”返回一个 lambda 作为谓词,并且 lambda *捕获*返回之前*存储在“seen”变量中的引用。这样,lambad 实例就可以访问其自己的底层 Map 捕获的实例。有关此捕获 Macanism 根据您的 JRE 版本的行为方式的更多详细信息,请参阅 https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html。 (2认同)

小智 122

另一种方法是使用名称作为关键字将人员放在地图中:

persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values();
Run Code Online (Sandbox Code Playgroud)

请注意,如果名称重复,则保留的Person将是第一个被限制的人.

  • @skiwi:你认为有一种方法可以实现`distinct()`而没有这种开销吗?任何实现如何知道它之前是否已经看过一个对象而没有真正记住它已经看到的所有不同的值?所以`toMap`和`distinct`的开销很可能是一样的. (21认同)
  • @Philipp:可以修改为`persons.collect(toMap(Person :: getName,p - > p,(p,q) - > p,LinkedHashMap :: new)).values();` (7认同)
  • 显然,它弄乱了列表的原始顺序 (2认同)
  • @DanielEarwicker 这个问题是关于“按属性区分”的。它需要流按*相同的属性*进行排序,以便能够利用它。首先,OP 根本没有声明流已排序。其次,流无法检测它们是否*按某个属性*排序。第三,没有真正的“按属性区分”流操作来执行您的建议。第四,在实践中,只有两种方法可以获得这样的排序流。一个已排序的源(`TreeSet`),它无论如何已经是不同的,或者流上的`sorted` 也缓冲了所有元素。 (2认同)

nos*_*sid 99

您可以将person对象包装到另一个类中,该类仅比较人员的名称.然后,您打开包装的对象以再次获取人流.流操作可能如下所示:

persons.stream()
    .map(Wrapper::new)
    .distinct()
    .map(Wrapper::unwrap)
    ...;
Run Code Online (Sandbox Code Playgroud)

该类Wrapper可能如下所示:

class Wrapper {
    private final Person person;
    public Wrapper(Person person) {
        this.person = person;
    }
    public Person unwrap() {
        return person;
    }
    public boolean equals(Object other) {
        if (other instanceof Wrapper) {
            return ((Wrapper) other).person.getName().equals(person.getName());
        } else {
            return false;
        }
    }
    public int hashCode() {
        return person.getName().hashCode();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这被称为[Schwartzian变换](https://en.wikipedia.org/wiki/Schwartzian_transform) (13认同)
  • 为了扩展@bjmi的建议,这里有一个示例用法: `persons.stream().map(Equivalence.equals().onResultOf(Person::getName)::wrap).distinct().map(Equivalence.Wrapper::得到)....` (7认同)
  • com.google.common.base.Equivalence.wrap(S)和com.google.common.base.Equivalence.Wrapper.get()也可以提供帮助. (6认同)
  • @StuartCaie不是真的......没有记忆,重点不是性能,而是适应现有的API. (5认同)
  • 您可以使包装类变得通用并通过键提取函数进行参数化。 (2认同)

San*_*osh 42

使用另一个解决方案Set.可能不是理想的解决方案,但它确实有效

Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

或者,如果您可以修改原始列表,则可以使用removeIf方法

persons.removeIf(p -> !set.add(p.getName()));
Run Code Online (Sandbox Code Playgroud)

  • 如果您不使用任何第三方库,这是最好的答案! (3认同)
  • 使用精巧的想法,如果此set尚未包含指定的元素,则Set.add返回true。+1 (2认同)

jos*_*res 27

使用带有自定义比较器的TreeSet有一种更简单的方法.

persons.stream()
    .collect(Collectors.toCollection(
      () -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName())) 
));
Run Code Online (Sandbox Code Playgroud)

  • Comparator.comparing(人::的getName) (11认同)
  • 我认为你的答案有助于订购而不是唯一性.然而,它帮助我设定了如何做到这一点的想法.点击此处:http://stackoverflow.com/questions/1019854/java-distinct-list-of-objects/1019870#1019870 (4认同)

frh*_*ack 24

我们也可以使用RxJava(非常强大的反应式扩展库)

Observable.from(persons).distinct(Person::getName)
Run Code Online (Sandbox Code Playgroud)

要么

Observable.from(persons).distinct(p -> p.getName())
Run Code Online (Sandbox Code Playgroud)

  • 问题要求java8解决方案不一定使用流.我的回答表明java8 stream api比rx api的功能要弱 (4认同)
  • 使用 [reactor](https://projectreactor.io/),它将是 `Flux.fromIterable(persons).distinct(p -&gt; p.getName())` (3认同)
  • Rx 很棒,但这是一个糟糕的答案。`Observable` 是基于推送的,而 `Stream` 是基于拉取的。/sf/ask/2115188561/ (2认同)

Cra*_*lin 10

您可以distinct(HashingStrategy)Eclipse Collections中使用该方法.

List<Person> persons = ...;
MutableList<Person> distinct =
    ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
Run Code Online (Sandbox Code Playgroud)

如果您可以重构persons实现Eclipse Collections接口,则可以直接在列表中调用该方法.

MutableList<Person> persons = ...;
MutableList<Person> distinct =
    persons.distinct(HashingStrategies.fromFunction(Person::getName));
Run Code Online (Sandbox Code Playgroud)

HashingStrategy只是一个策略接口,允许您定义equals和hashcode的自定义实现.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}
Run Code Online (Sandbox Code Playgroud)

注意:我是Eclipse Collections的提交者.


Sae*_*fam 10

你可以使用groupingBy收藏家:

persons.collect(Collectors.groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
Run Code Online (Sandbox Code Playgroud)

如果你想要另一个流,你可以使用它:

persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
Run Code Online (Sandbox Code Playgroud)


Mat*_*ski 9

如果可以,我建议使用Vavr.使用此库,您可以执行以下操作:

io.vavr.collection.List.ofAll(persons)
                       .distinctBy(Person::getName)
                       .toJavaSet() // or any another Java 8 Collection
Run Code Online (Sandbox Code Playgroud)


Sll*_*ort 9

您可以使用StreamEx库:

StreamEx.of(persons)
        .distinct(Person::getName)
        .toList()
Run Code Online (Sandbox Code Playgroud)

  • 不幸的是,原本很棒的 StreamEx 库的该方法设计得很糟糕 - 它比较对象相等性而不是使用 equals。由于字符串驻留,这可能对“String”有效,但也可能无效。 (2认同)

小智 8

虽然获得最高票数的答案绝对是 Java 8 的最佳答案,但它同时在性能方面绝对是最差的。如果您确实想要一个糟糕的低性能应用程序,那么请继续使用它。提取一组唯一的人名的简单要求应仅通过“For-Each”和“Set”来实现。如果列表大小超过 10,情况会变得更糟。

假设您有 20 个对象的集合,如下所示:

public static final List<SimpleEvent> testList = Arrays.asList(
            new SimpleEvent("Tom"), new SimpleEvent("Dick"),new SimpleEvent("Harry"),new SimpleEvent("Tom"),
            new SimpleEvent("Dick"),new SimpleEvent("Huckle"),new SimpleEvent("Berry"),new SimpleEvent("Tom"),
            new SimpleEvent("Dick"),new SimpleEvent("Moses"),new SimpleEvent("Chiku"),new SimpleEvent("Cherry"),
            new SimpleEvent("Roses"),new SimpleEvent("Moses"),new SimpleEvent("Chiku"),new SimpleEvent("gotya"),
            new SimpleEvent("Gotye"),new SimpleEvent("Nibble"),new SimpleEvent("Berry"),new SimpleEvent("Jibble"));
Run Code Online (Sandbox Code Playgroud)

你反对的地方SimpleEvent看起来像这样:

public class SimpleEvent {

private String name;
private String type;

public SimpleEvent(String name) {
    this.name = name;
    this.type = "type_"+name;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getType() {
    return type;
}

public void setType(String type) {
    this.type = type;
}
}
Run Code Online (Sandbox Code Playgroud)

为了测试,您有这样的JMH代码,(请注意,我使用接受的答案中提到的相同的uniqueByKey谓词):

@Benchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public void aStreamBasedUniqueSet(Blackhole blackhole) throws Exception{

    Set<String> uniqueNames = testList
            .stream()
            .filter(distinctByKey(SimpleEvent::getName))
            .map(SimpleEvent::getName)
            .collect(Collectors.toSet());
    blackhole.consume(uniqueNames);
}

@Benchmark
@OutputTimeUnit(TimeUnit.SECONDS)
public void aForEachBasedUniqueSet(Blackhole blackhole) throws Exception{
    Set<String> uniqueNames = new HashSet<>();

    for (SimpleEvent event : testList) {
        uniqueNames.add(event.getName());
    }
    blackhole.consume(uniqueNames);
}

public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
            .include(MyBenchmark.class.getSimpleName())
            .forks(1)
            .mode(Mode.Throughput)
            .warmupBatchSize(3)
            .warmupIterations(3)
            .measurementIterations(3)
            .build();

    new Runner(opt).run();
}
Run Code Online (Sandbox Code Playgroud)

然后你会得到如下的基准测试结果:

Benchmark                                  Mode  Samples        Score  Score error  Units
c.s.MyBenchmark.aForEachBasedUniqueSet    thrpt        3  2635199.952  1663320.718  ops/s
c.s.MyBenchmark.aStreamBasedUniqueSet     thrpt        3   729134.695   895825.697  ops/s
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,与 Java 8 Stream 相比,简单的For-Each 的吞吐量提高了 3 倍,错误分数降低了 3 倍。

吞吐量越高,性能越好

  • 谢谢,但问题是在 Stream API 的背景下非常具体的 (2认同)

小智 8

我想改进斯图尔特·马克斯的答案。如果 key 为 null 的话会通过NullPointerException. 在这里,我通过添加一个检查来忽略 null 键 keyExtractor.apply(t)!=null

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> keyExtractor.apply(t)!=null && seen.add(keyExtractor.apply(t));
Run Code Online (Sandbox Code Playgroud)

}


小智 7

可以使用以下方法找到不同的对象列表:

 List distinctPersons = persons.stream()
                    .collect(Collectors.collectingAndThen(
                            Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
                            ArrayList::new));
Run Code Online (Sandbox Code Playgroud)


sar*_*n3h 7

这就像一个魅力:

  1. 按唯一键对数据进行分组以形成地图。
  2. 从地图的每个值返回第一个对象(可能有多个具有相同名称的人)。
persons.stream()
    .collect(groupingBy(Person::getName))
    .values()
    .stream()
    .flatMap(values -> values.stream().limit(1))
    .collect(toList());
Run Code Online (Sandbox Code Playgroud)


Woj*_*ski 6

扩展Stuart Marks的答案,这可以用更短的方式完成,没有并发映射(如果你不需要并行流):

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    final Set<Object> seen = new HashSet<>();
    return t -> seen.add(keyExtractor.apply(t));
}
Run Code Online (Sandbox Code Playgroud)

然后打电话:

persons.stream().filter(distinctByKey(p -> p.getName());
Run Code Online (Sandbox Code Playgroud)

  • 这个没有考虑到流可能是并行的。 (2认同)

小智 6

我做了一个通用版本:

private <T, R> Collector<T, ?, Stream<T>> distinctByKey(Function<T, R> keyExtractor) {
    return Collectors.collectingAndThen(
            toMap(
                    keyExtractor,
                    t -> t,
                    (t1, t2) -> t1
            ),
            (Map<R, T> map) -> map.values().stream()
    );
}
Run Code Online (Sandbox Code Playgroud)

一个例子:

Stream.of(new Person("Jean"), 
          new Person("Jean"),
          new Person("Paul")
)
    .filter(...)
    .collect(distinctByKey(Person::getName)) // return a stream of Person with 2 elements, jean and Paul
    .map(...)
    .collect(toList())
Run Code Online (Sandbox Code Playgroud)


Tom*_*ski 6

另一个支持这个的库是jOO? ,及其Seq.distinct(Function<T,U>)方法:

Seq.seq(persons).distinct(Person::getName).toList();
Run Code Online (Sandbox Code Playgroud)

不过,在幕后,它实际上与公认的答案相同。


小智 6

Set<YourPropertyType> set = new HashSet<>();
list
        .stream()
        .filter(it -> set.add(it.getYourProperty()))
        .forEach(it -> ...);
Run Code Online (Sandbox Code Playgroud)

  • 一个好的答案有一个更好的解释[如何写一个好的答案?](https://stackoverflow.com/help/how-to-answer) (2认同)

une*_*q95 6

我的方法是将所有具有相同属性的对象组合在一起,然后将这些组缩短为 1 的大小,最后将它们收集为List.

  List<YourPersonClass> listWithDistinctPersons =   persons.stream()
            //operators to remove duplicates based on person name
            .collect(Collectors.groupingBy(p -> p.getName()))
            .values()
            .stream()
            //cut short the groups to size of 1
            .flatMap(group -> group.stream().limit(1))
            //collect distinct users as list
            .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)


Ale*_*lex 5

Saeed Zarinfam使用了类似的方法,但是使用了更多的Java 8样式:)

persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
 .map(plans -> plans.stream().findFirst().get())
 .collect(toList());
Run Code Online (Sandbox Code Playgroud)

  • 我将用“flatMap(plans -&gt;plans.stream().findFirst().stream())”替换地图行,它避免使用 get onOptional (2认同)