Answers:
考虑distinct
是一个有状态过滤器。以下是一个函数,该函数返回一个谓词,该谓词保持先前状态的状态,并返回是否第一次看到给定的元素:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
然后您可以编写:
persons.stream().filter(distinctByKey(Person::getName))
请注意,如果流是有序的并并行运行,则它将保留重复项中的任意元素,而不是第一个重复元素distinct()
。
(这基本上与我对以下问题的回答相同:Java Lambda Stream Distinct()在任意键上?)
distinctByKey
不知道是否在并行流中使用它。它在并行使用的情况下会使用CHM,尽管如上文Tagir Valeev所述,在顺序情况下这会增加开销。
distinctByKey
。但是,如果您distinctByKey
每次都调用它就可以工作,因此每次都可以创建一个新的Predicate实例。
.filter(distinctByKey(...))
。它将执行一次该方法并返回谓词。因此,如果您在流中正确使用该地图,则基本上该地图已经在重复使用。如果将地图设为静态,则该地图将共享所有用途。因此,如果您有两个使用this的流distinctByKey()
,那么两个都将使用同一张地图,这不是您想要的。
CallSite
将被链接到的get$Lambda
方法-它返回的一个新的实例Predicate
所有的时间,但这些实例将共享相同的map
,并function
尽可能我明白了。非常好!
一种替代方法是使用姓名作为关键字将人员放置在地图中:
persons.collect(Collectors.toMap(Person::getName, p -> p, (p, q) -> p)).values();
请注意,如果姓名重复,则被保住的人将是第一个进入的人。
distinct()
没有这种开销的实现方法吗?任何实现如何在不实际记住它已经看到的所有不同值的情况下知道它是否曾经看到过一个对象?所以的开销toMap
,并distinct
极有可能是相同的。
distinct()
本身会带来麻烦。
persons.collect(toMap(Person::getName, p -> p, (p, q) -> p, LinkedHashMap::new)).values();
TreeSet
)无论如何还是sorted
在流上都已经是不同的,该源还缓冲所有元素。
您可以将人员对象包装到另一个类中,该类仅比较人员的名称。然后,您解开包装的对象以再次获得人流。流操作可能如下所示:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
该类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();
}
}
equals
方法可以简化为return other instanceof Wrapper && ((Wrapper) other).person.getName().equals(person.getName());
另一种解决方案,使用Set
。可能不是理想的解决方案,但可以
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
或者,如果您可以修改原始列表,则可以使用removeIf方法
persons.removeIf(p -> !set.add(p.getName()));
将TreeSet与自定义比较器一起使用是一种更简单的方法。
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
您可以使用Eclipse Collections中的distinct(HashingStrategy)
方法。
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
如果可以重构persons
以实现Eclipse Collections接口,则可以直接在列表上调用该方法。
MutableList<Person> persons = ...;
MutableList<Person> distinct =
persons.distinct(HashingStrategies.fromFunction(Person::getName));
HashingStrategy只是一个策略接口,允许您定义equals和hashcode的自定义实现。
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
注意:我是Eclipse Collections的提交者。
扩展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));
}
然后致电:
persons.stream().filter(distinctByKey(p -> p.getName());
Collections.synchronizedSet(new HashSet<>())
。但这可能会比使用慢ConcurrentHashMap
。
Saeed Zarinfam使用了类似的方法,但是使用了更多的Java 8样式:)
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
flatMap(plans -> plans.stream().findFirst().stream())
它避免了可选使用get
我做了一个通用版本:
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()
);
}
例子:
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())
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);
另一个支持此功能的库是jOOλ及其Seq.distinct(Function<T,U>)
方法:
Seq.seq(persons).distinct(Person::getName).toList();
我的方法是将具有相同属性的所有对象组合在一起,然后将组切成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());
可以使用以下命令找到不同的对象列表:
List distinctPersons = persons.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
ArrayList::new));
实现此功能最简单的方法是跳到sort功能,因为它已经提供了Comparator
可以使用元素的属性创建的可选功能。然后,您必须过滤掉重复项,这可以使用statefull来完成Predicate
,其中使用以下事实:对于排序的流,所有相等的元素都是相邻的:
Comparator<Person> c=Comparator.comparing(Person::getName);
stream.sorted(c).filter(new Predicate<Person>() {
Person previous;
public boolean test(Person p) {
if(previous!=null && c.compare(previous, p)==0)
return false;
previous=p;
return true;
}
})./* more stream operations here */;
当然,有状态机Predicate
不是线程安全的,但是如果您需要,可以将此逻辑移到a中,Collector
并在使用your时让流负责线程安全Collector
。这取决于您要如何处理不同元素的流,而您没有在问题中告诉我们。
以@josketres的答案为基础,我创建了一个通用实用程序方法:
您可以通过创建Collector使它对Java 8更友好。
public static <T> Set<T> removeDuplicates(Collection<T> input, Comparator<T> comparer) {
return input.stream()
.collect(toCollection(() -> new TreeSet<>(comparer)));
}
@Test
public void removeDuplicatesWithDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(7), new C(42), new C(42));
Collection<C> result = removeDuplicates(input, (c1, c2) -> Integer.compare(c1.value, c2.value));
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 7));
assertTrue(result.stream().anyMatch(c -> c.value == 42));
}
@Test
public void removeDuplicatesWithoutDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(1), new C(2), new C(3));
Collection<C> result = removeDuplicates(input, (t1, t2) -> Integer.compare(t1.value, t2.value));
assertEquals(3, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 1));
assertTrue(result.stream().anyMatch(c -> c.value == 2));
assertTrue(result.stream().anyMatch(c -> c.value == 3));
}
private class C {
public final int value;
private C(int value) {
this.value = value;
}
}
也许对某人有用。我还有一点其他要求。具有A
来自第三方的对象列表,将所有具有相同A.b
字段的对象都删除A.id
(列表中A
具有相同对象的多个对象A.id
)。Tagir Valeev对流分区的回答启发了我使用返回的自定义。简单将完成其余的工作。Collector
Map<A.id, List<A>>
flatMap
public static <T, K, K2> Collector<T, ?, Map<K, List<T>>> groupingDistinctBy(Function<T, K> keyFunction, Function<T, K2> distinctFunction) {
return groupingBy(keyFunction, Collector.of((Supplier<Map<K2, T>>) HashMap::new,
(map, error) -> map.putIfAbsent(distinctFunction.apply(error), error),
(left, right) -> {
left.putAll(right);
return left;
}, map -> new ArrayList<>(map.values()),
Collector.Characteristics.UNORDERED)); }
我遇到了一种情况,当时我想根据2个键从列表中获取不同的元素。如果您想基于两个键来区分或可能使用复合键,请尝试以下操作
class Person{
int rollno;
String name;
}
List<Person> personList;
Function<Person, List<Object>> compositeKey = personList->
Arrays.<Object>asList(personList.getName(), personList.getRollno());
Map<Object, List<Person>> map = personList.stream().collect(Collectors.groupingBy(compositeKey, Collectors.toList()));
List<Object> duplicateEntrys = map.entrySet().stream()`enter code here`
.filter(settingMap ->
settingMap.getValue().size() > 1)
.collect(Collectors.toList());
就我而言,我需要控制先前的内容。然后,我创建了一个有状态的谓词,在其中控制上一个元素与当前元素是否不同,在这种情况下,我将其保留。
public List<Log> fetchLogById(Long id) {
return this.findLogById(id).stream()
.filter(new LogPredicate())
.collect(Collectors.toList());
}
public class LogPredicate implements Predicate<Log> {
private Log previous;
public boolean test(Log atual) {
boolean isDifferent = previouws == null || verifyIfDifferentLog(current, previous);
if (isDifferent) {
previous = current;
}
return isDifferent;
}
private boolean verifyIfDifferentLog(Log current, Log previous) {
return !current.getId().equals(previous.getId());
}
}
我在此清单中的解决方案:
List<HolderEntry> result ....
List<HolderEntry> dto3s = new ArrayList<>(result.stream().collect(toMap(
HolderEntry::getId,
holder -> holder, //or Function.identity() if you want
(holder1, holder2) -> holder1
)).values());
在我的情况下,我想找到不同的值并将其放在列表中。
尽管获得最高支持的答案绝对是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"));
您反对的地方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;
}
}
为了进行测试,您具有这样的JMH代码,(请注意,即时消息使用的是接受的答案中提到的相同的distinctByKey谓词):
@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();
}
然后,您将获得如下基准测试结果:
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
如您所见,与Java 8 Stream相比,一个简单的For-Each的吞吐量提高了3倍,错误分数降低了。
更高的吞吐量,更好的性能
如果您想列出人员列表,这将是简单的方法
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
另外,如果要查找名称的唯一列表或唯一列表,而不是Person,则也可以使用以下两种方法。
方法1:使用 distinct
persons.stream().map(x->x.getName()).distinct.collect(Collectors.toList());
方法2:使用 HashSet
Set<E> set = new HashSet<>();
set.addAll(person.stream().map(x->x.getName()).collect(Collectors.toList()));
Person
s。
您可以编写的最简单的代码:
persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());
Function<? super T, ?>
不是Function<? super T, Object>
。还应注意,对于有序并行流,此解决方案不能保证将提取哪个对象(与normal不同distinct()
)。同样对于顺序流,使用CHM会有额外的开销(@nosid解决方案中不存在)。最后,该解决方案违反了filter
JavaDoc中所述谓词必须为无状态的方法的约定。然而,赞成。