Java 8按属性区分

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

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

persons.stream().distinct();

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

persons.stream().distinct(p -> p.getName());

不幸的是,distinct()方法没有这样的重载。不修改相等检查在Person类是可能做到这一点简洁?

493678 次浏览

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

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

另一种方法是将人名作为键放在地图中:

persons.collect(Collectors.toMap(Person::getName, p -> p, (p, q) -> p)).values();

注意,如果有重复的名称,则保留的Person将是第一个遇到的名称。

实现这一点最简单的方法是跳到排序特性上,因为它已经提供了一个可选的Comparator,可以使用元素的属性创建。然后你必须过滤掉重复的元素,这可以使用一个完整的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不是线程安全的,但是如果你需要,你可以把这个逻辑移动到Collector中,让流在使用Collector时照顾线程安全。这取决于你想如何处理你在问题中没有告诉我们的不同元素流。

考虑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流独特()对任意键?)

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

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

基于@josketres的回答,我创建了一个通用的实用方法:

您可以通过创建收集器使其更适合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;
}
}

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

Observable.from(persons).distinct(Person::getName)

Observable.from(persons).distinct(p -> p.getName())

您可以在Eclipse集合中使用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());

如果可以的话,我建议使用Vavr。有了这个库,你可以做以下事情:

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

你可以使用groupingBy收集器:

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

如果你想有另一个流,你可以使用这个:

persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));

类似于Saeed Zarinfam使用的方法,但更像Java 8风格:)

persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());

我做了一个通用版本:

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

你可以使用StreamEx库:

StreamEx.of(persons)
.distinct(Person::getName)
.toList()

另一个解决方案,使用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()));

你能写的最简单的代码:

    persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());

也许会对某人有用。我还有一个要求。拥有来自第三方的对象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)); }

如果你想要名单,下面是最简单的方法

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

此外,如果你想找到不同的或唯一的名单不是人,你也可以使用以下两种方法。

方法一:使用distinct

persons.stream().map(x->x.getName()).distinct.collect(Collectors.toList());

方法二:使用HashSet

Set<E> set = new HashSet<>();
set.addAll(person.stream().map(x->x.getName()).collect(Collectors.toList()));

另一个支持此功能的库是jOOλ,以及它的Seq.distinct(Function<T,U>)方法:

Seq.seq(persons).distinct(Person::getName).toList();

在引擎盖下,它实际上做了与接受的答案相同的事情。

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

不同的对象列表可以使用:

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

我的方法是将所有具有相同属性的对象分组在一起,然后将组缩短为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());

在我的情况下,我需要控制什么是前一个元素。然后,我创建了一个有状态 Predicate,我控制前一个元素是否与当前元素不同,在这种情况下,我保留了它。

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

在我的情况下,我想找到不同的值,并把它们放在列表。

我遇到了一种情况,我应该根据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());
虽然最高赞的答案绝对是Java 8的最佳答案,但它同时在性能方面绝对是最差的。如果您真的想要一个糟糕的低性能应用程序,那么就使用它吧。提取一组唯一人名的简单要求,只需“For-Each”和“set”即可实现。 如果list的大小大于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倍,并且错误评分更低。

< em >高表示吞吐量,更好的表示性能

Here is the example
public class PayRoll {


private int payRollId;
private int id;
private String name;
private String dept;
private int salary;




public PayRoll(int payRollId, int id, String name, String dept, int salary) {
super();
this.payRollId = payRollId;
this.id = id;
this.name = name;
this.dept = dept;
this.salary = salary;
}
}


import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collector;
import java.util.stream.Collectors;


public class Prac {
public static void main(String[] args) {


int salary=70000;
PayRoll payRoll=new PayRoll(1311, 1, "A", "HR", salary);
PayRoll payRoll2=new PayRoll(1411, 2    , "B", "Technical", salary);
PayRoll payRoll3=new PayRoll(1511, 1, "C", "HR", salary);
PayRoll payRoll4=new PayRoll(1611, 1, "D", "Technical", salary);
PayRoll payRoll5=new PayRoll(711, 3,"E", "Technical", salary);
PayRoll payRoll6=new PayRoll(1811, 3, "F", "Technical", salary);
List<PayRoll>list=new ArrayList<PayRoll>();
list.add(payRoll);
list.add(payRoll2);
list.add(payRoll3);
list.add(payRoll4);
list.add(payRoll5);
list.add(payRoll6);




Map<Object, Optional<PayRoll>> k = list.stream().collect(Collectors.groupingBy(p->p.getId()+"|"+p.getDept(),Collectors.maxBy(Comparator.comparingInt(PayRoll::getPayRollId))));




k.entrySet().forEach(p->
{
if(p.getValue().isPresent())
{
System.out.println(p.getValue().get());
}
});






}
}


Output:


PayRoll [payRollId=1611, id=1, name=D, dept=Technical, salary=70000]
PayRoll [payRollId=1811, id=3, name=F, dept=Technical, salary=70000]
PayRoll [payRollId=1411, id=2, name=B, dept=Technical, salary=70000]
PayRoll [payRollId=1511, id=1, name=C, dept=HR, salary=70000]

处理null上面的答案的变体:

    public static <T, K> Predicate<T> distinctBy(final Function<? super T, K> getKey) {
val seen = ConcurrentHashMap.<Optional<K>>newKeySet();
return obj -> seen.add(Optional.ofNullable(getKey.apply(obj)));
}

在我的测试中:

        assertEquals(
asList("a", "bb"),
Stream.of("a", "b", "bb", "aa").filter(distinctBy(String::length)).collect(toList()));


assertEquals(
asList(5, null, 2, 3),
Stream.of(5, null, 2, null, 3, 3, 2).filter(distinctBy(x -> x)).collect(toList()));


val maps = asList(
hashMapWith(0, 2),
hashMapWith(1, 2),
hashMapWith(2, null),
hashMapWith(3, 1),
hashMapWith(4, null),
hashMapWith(5, 2));


assertEquals(
asList(0, 2, 3),
maps.stream()
.filter(distinctBy(m -> m.get("val")))
.map(m -> m.get("i"))
.collect(toList()));

虽然迟到了,但我有时会用这句俏皮话作为等效:

((Function<Value, Key>) Value::getKey).andThen(new HashSet<>()::add)::apply

表达式是Predicate<Value>,但由于映射是内联的,所以它作为过滤器工作。这当然可读性较差,但有时避免使用这种方法是有帮助的。

因为每个人都在分享他们自己的想法和实施方法,我也有一个,它不是一个有效的方法,但它是有效的:

Set<String> personNameList = personList.stream().
map(tempPerson->tempPerson.getName()).collect(Collectors.toSet());


personList.stream().
collect(()->new ArrayList<Person>(),
(l1,p)->{
if(!personNameList.contains(p.getName())) {
l1.add(p);
}
}, ArrayList::addAll);

这个解是什么呢?

只有当你的键实现了Equal时它才会起作用大多数基类型都是这样,但它更简单一点。

# EYZ0

我想改进Stuart Marks 回答。如果键是空的,它将通过NullPointerException。在这里,我通过添加一个检查keyExtractor.apply(t)!=null来忽略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));

这就像一个魅力:

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

有很多方法,这一个也会有帮助-简单,干净和清晰

    List<Employee> employees = new ArrayList<>();


employees.add(new Employee(11, "Ravi"));
employees.add(new Employee(12, "Stalin"));
employees.add(new Employee(23, "Anbu"));
employees.add(new Employee(24, "Yuvaraj"));
employees.add(new Employee(35, "Sena"));
employees.add(new Employee(36, "Antony"));
employees.add(new Employee(47, "Sena"));
employees.add(new Employee(48, "Ravi"));


List<Employee> empList = new ArrayList<>(employees.stream().collect(
Collectors.toMap(Employee::getName, obj -> obj,
(existingValue, newValue) -> existingValue))
.values());


empList.forEach(System.out::println);




//  Collectors.toMap(
//  Employee::getName, - key (the value by which you want to eliminate duplicate)
//  obj -> obj,  - value (entire employee object)
//  (existingValue, newValue) -> existingValue) - to avoid illegalstateexception: duplicate key

Output - toString()重载

Employee{id=35, name='Sena'}
Employee{id=12, name='Stalin'}
Employee{id=11, name='Ravi'}
Employee{id=24, name='Yuvaraj'}
Employee{id=36, name='Antony'}
Employee{id=23, name='Anbu'}