Related
I have a simple nested structure as such:
public static class A {
private List<B> classBList;
// constructor, getters, etc.
}
public static class B {
private int id;
private String name;
// constructor, getters, etc.
}
I want to create a map of <Integer,List<A>> where the integer field in class B id will be the key, and the A objects in the input that contain the matching id will be rolled up into a list as the value. The input would be a list of class A.
So for example:
Input:
[classBList=[B{id:1, name:"Hello"}, B{id:2, name:"Hi"}],
classBList=[B{id:3, name:"No"}, B{id:3, name:"Go"}],
classBList=[B{id:1, name:"Yes"}]]
Output:
{Key=1, Value=[ A{classBList=[B{id:1, name:"Hello"}, B{id:1, name:"Yes"}]} ]
{Key=2, Value=[ A{classBList=[B{id:2, name:"Hi"}]} ]
{Key=3, Value=[ A{classBList=[B{id:3, name:"No"}, B{id:3, name:"Go"}]} ]
I'm having trouble, however, writing the lambdas that allow this to happen. What I tried:
Map<Integer, List<A>> heyThere = classAListInput.stream()
.collect(Collectors.toMap(
A::getClass,
element -> element.getClassBList().stream()
.map(B::getId)
.collect(Collectors.toList())
));
But this doesn't compile, so really not sure of how the syntax should look.
If you're wondering why not just alter the map so it's <Integer, List< B >>, there are other fields in class A that I didn't note but would be needed in the output, so that's why a list of A objects would be the value in the map.
It seems that you need to rebuild instances of A class with the new list of B.
However, expected output shows that there is only one A entry in the list, and all B's are added to the same A instance:
{Key=2, Value=[ A{classBList=[B{id:2, name:"No"}, B{id:2, name: "Go"}, B{id:2, name:"Yellow"}, B{id:2, name:"Joe"}, B{id:2, name:"Blow"}]} ]
So, the following implementation may be offered assuming there's an all-args constructor in A class accepting List<B>:
Map<Integer, List<A>> result = classAListInput
.stream() // Stream<A>
.flatMap(a -> a.getClassBList().stream()) // Stream<B>
.collect(Collectors.groupingBy(
B::getId,
Collectors.collectingAndThen(
Collectors.toList(), // List<B> flattening all B instances by id
lst -> List.of(new A(lst)) // or Arrays.asList or Collections.singletonList
)
));
Update
As the copies of A class may need to be created with some additional fields in A class and the filtered list of B instances, the following change may be offered using Map.entry (available since Java 9 which has been around for a while).
Also, to address an issue of duplicated entries for the same bKey inside the same A instance, Stream::distinct operation should be applied after mapping to the key field:
Map<Integer, List<A>> aByBKey = classAListInput.stream()
.flatMap(a -> a.getClassBList()
.stream()
.map(B::getBKey)
.distinct()
.map(bk -> Map.entry(bk, getCopy(a, bk)) )
)
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
aByBKey.forEach((bKey, a) -> System.out.println(bKey + " -> " + a));
where getCopy is a static method constructing a copy instance of A keeping the code of class A intact:
public static A getCopy(A a, int bKey) {
return new A(
a.aKey,
a.classBList
.stream()
.filter(b -> b.getBKey() == bKey)
.collect(Collectors.toList())
);
}
JDoodle example
If I understood the problem correctly, judging by the sample data, you have a List<List<B>> as an input.
And based on the sample output you've provided, you need to obtain a map of type Map<Integer,A> as a result (not a Map<Integer,List<A>>).
This can be done in the following steps:
flatten the data using flatMap(), i.e. transform a Stream<List<B>> into a Stream<B>;
group the elements by id by the means of collector groupingBy();
collect the elements mapped to the same key into a list and transform them into an object A, which can be done by applying a combination of collectors collectingAndThen() and toList() as the downstream of groupingBy().
That's how it might be implemented:
public static void main(String[] args) {
List<List<B>> classAListInput = List.of(
List.of(new B(1, "Hello"), new B(1, "Hi"), new B(1, "Bye"), new B(1, "Yes")),
List.of(new B(2, "No"), new B(2, "Go"), new B(2, "Yellow")),
List.of(new B(2, "Joe"), new B(2, "Blow"))
);
Map<Integer, A> aById = classAListInput.stream()
.flatMap(Collection::stream) // flattening the data
.collect(Collectors.groupingBy(
B::getId, // grouping by id
Collectors.collectingAndThen(
Collectors.toList(), // accumulating elements into a list
A::new) // instantiating object A based on the List<B>
));
aById.forEach((id, a) -> System.out.println(id + " -> " + a));
}
Output:
1 -> A{classBList=[B{id=1, name='Hello'}, B{id=1, name='Hi'}, B{id=1, name='Bye'}, B{id=1, name='Yes'}]}
2 -> A{classBList=[B{id=2, name='No'}, B{id=2, name='Go'}, B{id=2, name='Yellow'}, B{id=2, name='Joe'}, B{id=2, name='Blow'}]}
A link to Online Demo
Using Java 8 groupingBy:
You can use groupingBy feature as shown below:
Here,
I have used flatMap() to merge all the list of data into a single list.
I have used groupingBy() using id as a grouping element and then before collecting the data i have converted the data into List<A> using Collectors.mapping()
B.java
public class B {
private int id;
private String name;
public B(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Override
public String toString() {
return "B{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
A.java
public class A {
private List<B> classBList;
public A(List<B> classBList) {
this.classBList = classBList;
}
public List<B> getClassBList() {
return classBList;
}
public void setClassBList(List<B> classBList) {
this.classBList = classBList;
}
#Override
public String toString() {
return "A{" +
"classBList=" + classBList +
'}';
}
}
Test.java
public class Test {
public static void main(String[] args) {
List<List<B>> listInput = List.of(
List.of(new B(1, "Hello"), new B(1, "Hi"), new B(1, "Bye"), new B(1, "Yes")),
List.of(new B(2, "No"), new B(2, "Go"), new B(2, "Yellow")),
List.of(new B(2, "Joe"), new B(2, "Blow"))
);
Map<Integer,List<A>> output =
listInput.stream().flatMap(Collection::stream)
.collect(Collectors.groupingBy(B::getId,
Collectors.mapping(x ->
new A(List.of(new B(x.getId(), x.getName())))
,Collectors.toList())));
System.out.println(output);
}
}
Output:
{1=[A{classBList=[B{id=1, name='Hello'}]}, A{classBList=[B{id=1, name='Hi'}]}, A{classBList=[B{id=1, name='Bye'}]}, A{classBList=[B{id=1, name='Yes'}]}],
2=[A{classBList=[B{id=2, name='No'}]}, A{classBList=[B{id=2, name='Go'}]}, A{classBList=[B{id=2, name='Yellow'}]}, A{classBList=[B{id=2, name='Joe'}]}, A{classBList=[B{id=2, name='Blow'}]}]}
You'll need to flat map to some sort of tuple class, like AbstractMap.SimpleEntry, so you can stream A and B in parallel and then invert the grouping:
classAListInput.stream()
.flatMap(a -> a.getClassBList()
.stream()
.map(b -> new SimpleEntry<>(b.getId(), a)))
.collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())))
In Java 8 how can I filter a collection using the Stream API by checking the distinctness of a property of each object?
For example I have a list of Person object and I want to remove people with the same name,
persons.stream().distinct();
Will use the default equality check for a Person object, so I need something like,
persons.stream().distinct(p -> p.getName());
Unfortunately the distinct() method has no such overload. Without modifying the equality check inside the Person class is it possible to do this succinctly?
Consider distinct to be a stateful filter. Here is a function that returns a predicate that maintains state about what it's seen previously, and that returns whether the given element was seen for the first time:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
Then you can write:
persons.stream().filter(distinctByKey(Person::getName))
Note that if the stream is ordered and is run in parallel, this will preserve an arbitrary element from among the duplicates, instead of the first one, as distinct() does.
(This is essentially the same as my answer to this question: Java Lambda Stream Distinct() on arbitrary key?)
An alternative would be to place the persons in a map using the name as a key:
persons.collect(Collectors.toMap(Person::getName, p -> p, (p, q) -> p)).values();
Note that the Person that is kept, in case of a duplicate name, will be the first encontered.
You can wrap the person objects into another class, that only compares the names of the persons. Afterward, you unwrap the wrapped objects to get a person stream again. The stream operations might look as follows:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
The class Wrapper might look as follows:
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();
}
}
Another solution, using Set. May not be the ideal solution, but it works
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
Or if you can modify the original list, you can use removeIf method
persons.removeIf(p -> !set.add(p.getName()));
There's a simpler approach using a TreeSet with a custom comparator.
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
We can also use RxJava (very powerful reactive extension library)
Observable.from(persons).distinct(Person::getName)
or
Observable.from(persons).distinct(p -> p.getName())
You can use groupingBy collector:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
If you want to have another stream you can use this:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
You can use the distinct(HashingStrategy) method in Eclipse Collections.
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
If you can refactor persons to implement an Eclipse Collections interface, you can call the method directly on the list.
MutableList<Person> persons = ...;
MutableList<Person> distinct =
persons.distinct(HashingStrategies.fromFunction(Person::getName));
HashingStrategy is simply a strategy interface that allows you to define custom implementations of equals and hashcode.
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
Note: I am a committer for Eclipse Collections.
Similar approach which Saeed Zarinfam used but more Java 8 style:)
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
You can use StreamEx library:
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
I recommend using Vavr, if you can. With this library you can do the following:
io.vavr.collection.List.ofAll(persons)
.distinctBy(Person::getName)
.toJavaSet() // or any another Java 8 Collection
Extending Stuart Marks's answer, this can be done in a shorter way and without a concurrent map (if you don't need parallel streams):
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return t -> seen.add(keyExtractor.apply(t));
}
Then call:
persons.stream().filter(distinctByKey(p -> p.getName());
My approach to this is to group all the objects with same property together, then cut short the groups to size of 1 and then finally collect them as a 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());
Distinct objects list can be found using:
List distinctPersons = persons.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
ArrayList::new));
I made a generic version:
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()
);
}
An exemple:
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())
Another library that supports this is jOOλ, and its Seq.distinct(Function<T,U>) method:
Seq.seq(persons).distinct(Person::getName).toList();
Under the hood, it does practically the same thing as the accepted answer, though.
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);
While the highest upvoted answer is absolutely best answer wrt Java 8, it is at the same time absolutely worst in terms of performance. If you really want a bad low performant application, then go ahead and use it. Simple requirement of extracting a unique set of Person Names shall be achieved by mere "For-Each" and a "Set".
Things get even worse if list is above size of 10.
Consider you have a collection of 20 Objects, like this:
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"));
Where you object SimpleEvent looks like this:
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;
}
}
And to test, you have JMH code like this,(Please note, im using the same distinctByKey Predicate mentioned in accepted answer) :
#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();
}
Then you'll have Benchmark results like this:
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
And as you can see, a simple For-Each is 3 times better in throughput and less in error score as compared to Java 8 Stream.
Higher the throughput, better the performance
I would like to improve Stuart Marks answer. What if the key is null, it will through NullPointerException. Here I ignore the null key by adding one more check as 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));
}
This works like a charm:
Grouping the data by unique key to form a map.
Returning the first object from every value of the map (There could be multiple person having same name).
persons.stream()
.collect(groupingBy(Person::getName))
.values()
.stream()
.flatMap(values -> values.stream().limit(1))
.collect(toList());
The easiest way to implement this is to jump on the sort feature as it already provides an optional Comparator which can be created using an element’s property. Then you have to filter duplicates out which can be done using a statefull Predicate which uses the fact that for a sorted stream all equal elements are adjacent:
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 */;
Of course, a statefull Predicate is not thread-safe, however if that’s your need you can move this logic into a Collector and let the stream take care of the thread-safety when using your Collector. This depends on what you want to do with the stream of distinct elements which you didn’t tell us in your question.
There are lot of approaches, this one will also help - Simple, Clean and Clear
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() overloaded
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'}
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]
Late to the party but I sometimes use this one-liner as an equivalent:
((Function<Value, Key>) Value::getKey).andThen(new HashSet<>()::add)::apply
The expression is a Predicate<Value> but since the map is inline, it works as a filter. This is of course less readable but sometimes it can be helpful to avoid the method.
Building on #josketres's answer, I created a generic utility method:
You could make this more Java 8-friendly by creating a Collector.
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;
}
}
Maybe will be useful for somebody. I had a little bit another requirement. Having list of objects A from 3rd party remove all which have same A.b field for same A.id (multiple A object with same A.id in list). Stream partition answer by Tagir Valeev inspired me to use custom Collector which returns Map<A.id, List<A>>. Simple flatMap will do the rest.
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)); }
I had a situation, where I was suppose to get distinct elements from list based on 2 keys.
If you want distinct based on two keys or may composite key, try this
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());
A variation of the top answer that handles 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)));
}
In my tests:
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()));
In my case I needed to control what was the previous element. I then created a stateful Predicate where I controled if the previous element was different from the current element, in that case I kept it.
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());
}
}
My solution in this listing:
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());
In my situation i want to find distinct values and put their in List.
I have such collection: Map<Integer, List<MyObject>> collection
I would like to map the whole list of MyObject to MyObjectDTO and return the whole map with the mapped list.
So return will be:
Map<Integer, List<MyObjectDto>> collectionWithDtos
What is the easiest and quickest way? I've checked a couple of ways with streams but none of that produced results as I expected.
Thanks
This is a way to go with the following simple call:
Map<Integer, List<MyObjectDto>> mappedCollection = collection.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue()
.stream()
.map(myObject -> new MyObjectDto()) // mapping here
.collect(Collectors.toList())));
Basically, you want to collect it into the same-structured map with the same key. Stream the set of entries Set<Map.Entry<Integer, List<MyObject>>> and map it into a new map using Collectors.toMap(Function, Function) where:
Key is the same key: entry -> entry.getKey()
Value is the same value (List), except all MyObject objects are mapped into MyObjectDto, which can be performed with another stream.
As long as we don't know the structures of the objects to be mapped, you have to add it by yourself to the line with a comment.
With a for-loop and streams:
private Map<Integer, List<MyObjectDto>> mapNestedListToDto(Map<Integer, List<MyObject>> collection) {
Map<Integer, List<Dto>> collectionWithDtos = new HashMap<>();
for (Integer i : collection.keySet()) {
List<Dto> dtos = collection.get(i).stream().map(myObject -> mappingFunction(myObject)).collect(Collectors.toList());
mapped.put(i, dtos);
}
return collectionWithDtos;
}
Where mappingFunction() is the method that will actually convert an instance of MyObject to an instance of MyObjectDTO.
You can create a new HashMap where you will put the old keys with a new values ( values will be the Dto created based on the original objects )
class Person {
String name ;
public Person(String s) {
this.name=s;
}
#Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
class PersonDto {
String name;
public PersonDto(Person p) {
this.name=p.name+"Dto";
}
#Override
public String toString() {
// TODO Auto-generated method stub
return name;
}
}
public class Main {
public static void main(String[] args) {
// your initial HashMap
Map<Integer, List<Person>> map = new HashMap<>();
Person x =new Person("X");
Person y =new Person("Y");
List<Person> lst1 = new ArrayList<>();
lst1.add(x);
List<Person> lst2 = new ArrayList<>();
lst2.add(y);
map.put(1, lst1);
map.put(2, lst2);
// create a new HashMap<Integer, List<MyObjectDto>>
Map<Integer, List<PersonDto>> mapDto = new HashMap<>();
// Simply use forEach
// in the next line instead of "new PersonDto(e)" you will put your mapping method
map.forEach((k,v)->{
mapDto.put(k, v.stream().map(e-> new PersonDto(e)).collect(Collectors.toList()));
});
System.out.println(map);
System.out.println(mapDto);
}
}
Output :
{1=[X], 2=[Y]}
{1=[XDto], 2=[YDto]}
I have been searching around different groupBy and stream threads but cannot find the answer to my problem. Basically I have this object:
public class Object {
string name;
string type;
}
And I return a list of these from the database. What I would like to then do is iterate through the list of objects and remove duplicate names and save a list of the second properties under one object, in a new object that looks like this:
public class NewObject {
String name;
List<String> types;
}
You can do it like this.
use groupingBy to create a map of name, List of type
use the entryset of the map to create the new object.
I added the appropriate constructors and getters in the classes.
List<OldObject> list = ...
List<NewObject> newList = list.stream()
.collect(Collectors.groupingBy(OldObject::getName,
Collectors.mapping(OldObject::getType,
Collectors.toList())))
.entrySet().stream()
.map(e -> new NewObject(e.getKey(), e.getValue()))
.toList();
}
class OldObject {
String name;
String type;
public String getName() {
return name;
}
public String getType() {
return type;
}
}
class NewObject {
String name;
List<String> types = new ArrayList<>();
public NewObject(String name, List<String> types) {
this.types = types;
this.name = name;
}
}
For your added enjoyment, I thought I would also offer the following:
create the map
conditionally create a new object if the name key is not present
in either case, add the type to the list instance of the NewObject instance in the map which is returned by the computeIfAbsent method.
When finished, just assign the values to your Collection.
Map<String, NewObject> map = new HashMap<>();
for (OldObject ob : list) {
map.computeIfAbsent(ob.getName(),
v -> new NewObject(ob.getName(),
new ArrayList<>()))
.add(ob.getType());
}
Collection<NewObject> newLista = map.values();
Caveats:
values returns a Collection, not a list so you would need to use that or pass the Collection to a list constructor of some sort (e.g. ArrayList).
the requires the addition of a add method in the NewObject class.
you could also have a getter that returns the type list directly and do.
map.computeIfAbsent(ob.getName(),
v -> new NewObject(ob.getName(),
new ArrayList<>()))
.getTypeList().add(ob.getType());
Check out these additions to the Map interface
I understand you don't want to remove duplicates but actually merge them.
I replaced your Object class name with OldObject to avoid confusion with the actual Java Object class.
Collecting to Map<String, List<String>> and then converting to List<NewObject>
You could write your own Collector (see later in the answer), but the easiest way of writing what you need would be to use the current groupingBy and toList Collectors and then using the resulting Map and create your newObject instance based on it to then again collect to a new List:
oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collectors.mapping(OldObject::getType, Collectors.toList())))
.entrySet().stream()
// Assuming a NewObject constructor that receives a String name and a List<String> types
.map(e -> new NewObject(e.getKey(), e.getValue()))
.collect(Collectors.toList());
Custom Collectors
If you are interested in learning how to make your own Collector, I made an example using a custom Collector as well:
oldObjectList.stream().collect(Collector.of(
// Supplier
() -> new ConcurrentHashMap<String, NewObject>(),
// Accumulator
(map, oldObject) -> {
// Assuming a NewObject constructor that gets a name and creates a new empty List as types.
map.computeIfAbsent(oldObject.getName(), name -> new NewObject(name)).getTypes().add(oldObject.getType());
},
// Combiner
(map1, map2) -> {
map2.forEach((k, v) -> map1.merge(k, v, (v1, v2) -> {
v1.getTypes().addAll(v2.getTypes());
return v1;
}));
return map1;
},
// Finisher
map -> new ArrayList<>(map.values())
));
You can even combine groupingBy and a custom Collector as a merge function:
new ArrayList<>(oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collector.of(
// Supplier
// Assuming a NewObject constructor with no arguments that creates a new empty List as types.
NewObject::new,
// Accumulator
(newObject, oldObject) -> {
newObject.setName(oldObject.getName());
newObject.getTypes().add(oldObject.getType());
},
// Combiner
(newObject1, newObject2) -> {
newObject1.getTypes().addAll(newObject2.getTypes());
return newObject1;
}
))).values());
It may be possible to use Collectors.toMap to create NewObject instances immediately and append types as necessary within the merge function.
Assuming that there NewObject has custom constructor and overridden toString method:
class NewObject {
String name;
List<String> types = new ArrayList<>();
public NewObject(String name, String type) {
this.name = name;
this.types.add(type);
}
#Override
public String toString() {
return "{ name: " + name + "; types: " + types + "}";
}
}
and that there are helper methods to create an instance of NewObject from OldObject and merge types of two NewObject instances:
// MyClass
public static NewObject createNewObject(OldObject oo) {
return new NewObject(oo.getName(), oo.getType());
}
public static NewObject mergeTypes(NewObject no1, NewObject no2) {
no1.getTypes().addAll(no2.getTypes());
return no1;
}
The transformation may be implemented as follows:
List<OldObject> input = Arrays.asList(
new OldObject("n1", "type1"), new OldObject("n1", "type2"),
new OldObject("n2", "type1"), new OldObject("n3", "type2"),
new OldObject("n1", "type3"), new OldObject("n2", "type2")
);
List<NewObject> result = new ArrayList<>(
input
.stream()
.collect(Collectors.toMap(
OldObject::getName,
MyClass::createNewObject,
MyClass::mergeTypes,
LinkedHashMap::new // keep insertion order
))
.values());
result.forEach(System.out::println);
Output:
{ name: n1; types: [type1, type2, type3]}
{ name: n2; types: [type1, type2]}
{ name: n3; types: [type2]}
I was trying to filter a list based on multiple conditions, sorting.
class Student{
private int Age;
private String className;
private String Name;
public Student(int age, String className, String name) {
Age = age;
this.className = className;
Name = name;
}
public int getAge() {
return Age;
}
public void setAge(int age) {
Age = age;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
}
Now if I have a list of that, say
List<Student> students = new ArrayList<>();
students.add(new Student(24, "A", "Smith"));
students.add(new Student(24, "A", "John"));
students.add(new Student(30, "A", "John"));
students.add(new Student(20, "B", "John"));
students.add(new Student(24, "B", "Prince"));
How would I be able to get a list of the oldest students with a distinct name?
In C# this would be quite simple by using System.Linq GroupBy then comparing and then flattening with select, I'm not too sure how I could achieve the same in Java.
Use the toMap collector:
Collection<Student> values = students.stream()
.collect(toMap(Student::getName,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparingInt(Student::getAge))))
.values();
Explanation
We're using this overload of toMap:
toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
Student::getName above is the keyMapper function used to extract the values for the map keys.
Function.identity() above is the valueMapper function used to extract the values for the map values where Function.identity() simply returns the elements in the source them selves i.e. the Student objects.
BinaryOperator.maxBy(Comparator.comparingInt(Student::getAge)) above is the merge function used to "decide which Student object to return in the case of a key collission i.e. when two given students have the same name" in this case taking the oldest Student .
Finally, invoking values() returns us a collection of students.
The equivalent C# code being:
var values = students.GroupBy(s => s.Name, v => v,
(a, b) => b.OrderByDescending(e => e.Age).Take(1))
.SelectMany(x => x);
Explanation (for those unfamiliar with .NET)
We're using this extension method of GroupBy:
System.Collections.Generic.IEnumerable<TResult> GroupBy<TSource,TKey,TElement,TResult>
(this System.Collections.Generic.IEnumerable<TSource> source,
Func<TSource,TKey> keySelector,
Func<TSource,TElement> elementSelector,
Func<TKey,System.Collections.Generic.IEnumerable<TElement>,TResult> resultSelector);
s => s.Name above is the keySelector function used to extract the value to group by.
v => v above is the elementSelector function used to extract the values i.e. the Student objects them selves.
b.OrderByDescending(e => e.Age).Take(1) above is the resultSelector which given an IEnumerable<Student> represented as b takes the oldest student.
Finally, we apply .SelectMany(x => x); to collapse the resulting IEnumerable<IEnumerable<Student>> into a IEnumerable<Student>.
Or without streams:
Map<String, Student> map = new HashMap<>();
students.forEach(x -> map.merge(x.getName(), x, (oldV, newV) -> oldV.getAge() > newV.getAge() ? oldV : newV));
Collection<Student> max = map.values();
If you need a grouping only sorted, it is quite simple:
Map<String, List<Student>> collect = students.stream() // stream capabilities
.sorted(Comparator.comparingInt(Student::getAge).reversed()) // sort by age, descending
.collect(Collectors.groupingBy(Student::getName)); // group by name.
Output in collect:
Prince=[Student [Age=24, className=B, Name=Prince]],
Smith=[Student [Age=24, className=A, Name=Smith]],
John=[Student [Age=30, className=A, Name=John], Student [Age=24, className=A, Name=John], Student [Age=20, className=B, Name=John]]
Just to mix and merge the other solutions, you could alternatively do :
Map<String, Student> nameToStudentMap = new HashMap<>();
Set<Student> finalListOfStudents = students.stream()
.map(x -> nameToStudentMap.merge(x.getName(), x, (a, b) -> a.getAge() > b.getAge() ? a : b))
.collect(Collectors.toSet());