Java 8では、各オブジェクトのプロパティの明確さをチェックすることによってStream
APIを使用してコレクションをフィルターすることができますか?
たとえば、Person
オブジェクトのリストがあり、同じ名前の人を削除したいとします。
persons.stream().distinct();
Person
オブジェクトに対してデフォルトの等価性チェックを使用するので、以下のようなものが必要です。
persons.stream().distinct(p -> p.getName());
残念ながら、distinct()
メソッドにはそのようなオーバーロードはありません。 Person
クラス内の等価性チェックを修正しなくても、これを簡潔に行うことは可能ですか?
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()? )
別の方法としては、名前をキーとして使用して人を地図に配置します。
persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values();
名前が重複している場合は、保持されている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();
}
}
カスタムコンパレータを使用してTreeSetを使用するより簡単な方法があります。
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
Set
を使ったもう一つの解決策。理想的な解決策ではないかもしれませんが、うまくいきます。
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
または元のリストを変更できる場合は、 removeIf methodを使用できます。
persons.removeIf(p -> !set.add(p.getName()));
distinct(HashingStrategy)
メソッドは Eclipse Collections で使用できます。
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
Eclipse Collectionsインターフェースを実装するためにpersons
をリファクタリングできる場合は、リスト上で直接メソッドを呼び出すことができます。
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コレクションのコミッターです。
StreamEx libraryを使用できます。
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
できれば Vavr の使用をお勧めします。このライブラリを使用すると、次のことができます。
io.vavr.collection.List.ofAll(persons)
.distinctBy(Person::getName)
.toJavaSet() // or any another Java 8 Collection
groupingBy
コレクターを使うことができます:
persons.collect(groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
別のストリームが欲しいなら、これを使うことができます:
persons.collect(groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
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());
私は一般的なバージョンを作りました:
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())
Saeed Zarinfamが使用したのと同様のアプローチですが、よりJava 8スタイル:)
persons.collect(groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.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();
フードの下で 、それは 受け入れられた答えと実質的に同じことをします 、しかし。
これを実装する最も簡単な方法は、要素のプロパティを使用して作成できるオプションの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
を使用するときにストリームにスレッドセーフを任せることができます。これはあなたがあなたの質問で私達に言わなかった異なった要素の流れをどうしたいかによって異なります。
@ 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;
}
}
異なるオブジェクトのリストは、次のものを使って見つけることができます -
List distnictPersons = 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());
たぶん誰かに役立つでしょう。もう1つ要件がありました。サードパーティからのオブジェクトA
のリストを持つことは、同じA.b
に対して同じA.id
フィールドを持つものをすべて削除します(リスト内の同じA.id
を持つ複数のA
オブジェクト)。 ストリームパーティション answer by Tagir ValeevMap<A.id, List<A>>
を返すカスタムCollector
を使うよう促しました。単純な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)); }
私の場合、前の要素を制御する必要がありました。次に、前の要素が現在の要素と異なる場合に制御するstateful 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());
}
}