The groupingBy operation (or something similar) is unavoidable, the Map created by the operation is also used during the operation for looking up the grouping keys and finding the duplicates. But you can combine it with the reduction of the group elements:
Map<String, Friend> uniqueFriendMap = friends.stream()
.collect(Collectors.groupingBy(Friend::uniqueFunction,
Collectors.collectingAndThen(
Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)));
The values of the map are already the resulting distinct friends. If you really need a List, you can create it with a plain Collection operation:
List<Friend> mergedFriends = new ArrayList<>(uniqueFriendMap.values());
If this second operation still annoys you, you can hide it within the collect operation:
List<Friend> mergedFriends = friends.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Friend::uniqueFunction, Collectors.collectingAndThen(
Collectors.reducing((a,b) -> friendMergeFunction(a,b)), Optional::get)),
m -> new ArrayList<>(m.values())));
Since the nested collector represents a Reduction (see also this answer), we can use toMap instead:
List<Friend> mergedFriends = friends.stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(Friend::uniqueFunction, Function.identity(),
(a,b) -> friendMergeFunction(a,b)),
m -> new ArrayList<>(m.values())));
Depending on whether friendMergeFunction is a static method or instance method, you may replace (a,b) -> friendMergeFunction(a,b) with DeclaringClass::friendMergeFunction or this::friendMergeFunction.
But note that even within your original approach, several simplifications are possible. When you only process the values of a Map, you don’t need to use the entrySet(), which requires you to call getValue() on each entry. You can process the values() in the first place. Then, you don’t need the verbose input -> { return expression; } syntax, as input -> expression is sufficient. Since the groups of the preceding grouping operation can not be empty, the filter step is obsolete. So your original approach would look like:
Map<String, List<Friend>> uniqueFriendMap
= friends.stream().collect(Collectors.groupingBy(Friend::uniqueFunction));
List<Friend> mergedFriends = uniqueFriendMap.values().stream()
.map(group -> group.stream().reduce((a,b) -> friendMergeFunction(a,b)).get())
.collect(Collectors.toList());
which is not so bad. As said, the fused operation doesn’t skip the Map creation as that’s unavoidable. It only skips the creations of the Lists representing each group, as it will reduce them to a single Friend in-place.
Java 8 stream - merge collections of objects sharing the same Id - Stack Overflow
Combine some fields of two objects by common id using stream
java - Lambda to obtain a new merged object - Code Review Stack Exchange
Merging lists under same objects in a list using Java streams - Stack Overflow
If you are OK returning a Collection it would look like this:
Collection<Invoice> invoices = list.collect(Collectors.toMap(Invoice::getMonth, Function.identity(), (left, right) -> {
left.setAmount(left.getAmount().add(right.getAmount()));
return left;
})).values();
If you really need a List:
list.stream().collect(Collectors.collectingAndThen(Collectors.toMap(Invoice::getMonth, Function.identity(), (left, right) -> {
left.setAmount(left.getAmount().add(right.getAmount()));
return left;
}), m -> new ArrayList<>(m.values())));
Both obviously assume that Invoice is mutable...
If you could add the following copy constructor and merge method to your Invoice class:
public Invoice(Invoice another) {
this.month = another.month;
this.amount = another.amount;
}
public Invoice merge(Invoice another) {
amount = amount.add(another.amount); // BigDecimal is immutable
return this;
}
You could reduce as you want, as follows:
Collection<Invoice> result = list.stream()
.collect(Collectors.toMap(
Invoice::getMonth, // use month as key
Invoice::new, // use copy constructor => don't mutate original invoices
Invoice::merge)) // merge invoices with same month
.values();
I'm using Collectors.toMap to do the job, which has three arguments: a function that maps elements of the stream to keys, a function that maps elements of the stream to values and a merge function that is used to combine values when there are collisions on the keys.
I have list of Objects A, list of Objects B;
Object A:
private String id;
private String fieldToMerge;
Object b:
private String id;
private String haveToMergeInThisField;
I need to merge all fields from Objects A to field of Objects B, both object are tied up in the "id" field. Which is the most elegant way to do it?
Assuming class A has a copy constructor that effectively copies the List<B> list attribute and a method that merges two instances of A:
public A(A another) {
this.id = another.id;
this.name = another.name;
this.list = new ArrayList<>(another.list);
}
public A merge(A another) {
list.addAll(another.list):
return this;
}
You could achieve what you want as follows:
Map<Integer, A> result = listOfA.stream()
.collect(Collectors.toMap(A::getId, A::new, A::merge));
Collection<A> result = map.values();
This uses Collectors.toMap, which expects a function that extracts the key of the map from the elements of the stream (here this would be A::getId, which extracts the id of A), a function that transforms each element of the stream to the values of the map (here it would be A::new, which references the copy constructor) and a merge function that combines two values of the map that have the same key (here this would be A::merge, which is only called when the map already contains an entry for the same key).
If you need a List<A> instead of a Collection<A>, simply do:
List<A> result = new ArrayList<>(map.values());
If you don't want to use extra functions you can do the following, it's readable and easy to understand, first group by id, create a new object with the first element in the list and then join all the B's classes to finally collect the A's.
List<A> result = list.stream()
.collect(Collectors.groupingBy(A::getId))
.values().stream()
.map(grouped -> new A(grouped.get(0).getId(), grouped.get(0).getName(),
grouped.stream().map(A::getList).flatMap(List::stream)
.collect(Collectors.toList())))
.collect(Collectors.toList());
Another way is to use a binary operator and the Collectors.groupingBy method. Here you use the java 8 optional class to create the new A the first time when fst is null.
BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
.map(cur -> { cur.getList().addAll(snd.getList()); return cur; })
.orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));
Collection<A> result = list.stream()
.collect(Collectors.groupingBy(A::getId, Collectors.reducing(null, joiner)))
.values();
If you don't like to use return in short lambdas (doesn't look that well) the only option is a filter because java does not provide another method like stream's peek (note: some IDEs highlight to 'simplify' the expression and mutations shouldn't be made in filter [but i think in maps neither]).
BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
.filter(cur -> cur.getList().addAll(snd.getList()) || true)
.orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));
You can also use this joiner as a generic method and create a left to right reducer with a consumer that allows to join the new mutable object created with the initializer function.
public class Reducer {
public static <A> Collector<A, ?, A> reduce(Function<A, A> initializer,
BiConsumer<A, A> combiner) {
return Collectors.reducing(null, (fst, snd) -> Optional.ofNullable(fst)
.map(cur -> { combiner.accept(cur, snd); return cur; })
.orElseGet(() -> initializer.apply(snd)));
}
public static <A> Collector<A, ?, A> reduce(Supplier<A> supplier,
BiConsumer<A, A> combiner) {
return reduce((ign) -> supplier.get(), combiner);
}
}
And use it like
Collection<A> result = list.stream()
.collect(Collectors.groupingBy(A::getId, Reducer.reduce(
(cur) -> new A(cur.getId(), cur.getName(), new ArrayList<>(cur.getList())),
(fst, snd) -> fst.getList().addAll(snd.getList())
))).values();
Or like if you have an empty constructor that initializes the collections
Collection<A> result = list.stream()
.collect(Collectors.groupingBy(A::getId, Reducer.reduce(A::new,
(fst, snd) -> {
fst.getList().addAll(snd.getList());
fst.setId(snd.getId());
fst.setName(snd.getName());
}
))).values();
Finally, if you already have the copy constructor or the merge method mentioned in the other answers you can simplify the code even more or use the Collectors.toMap method.
@Jigar Joshi has answered the first part of your question which is "how to merge two IntStream's into one".
Your other question of "how to merge two Stream<T> without overwriting the equals() and hashCode() method?" can be done using the toMap collector, i.e.
assuming you don't want the result as a Stream<T>.
Example:
Stream.concat(stream1, stream2)
.collect(Collectors.toMap(Student::getNo,
Function.identity(),
(l, r) -> l,
LinkedHashMap::new)
).values();
if you want the result as a Stream<T> then one could do:
Stream.concat(stream1, stream2)
.collect(Collectors.collectingAndThen(
Collectors.toMap(Student::getNo,
Function.identity(),
(l, r) -> l,
LinkedHashMap::new),
f -> f.values().stream()));
This is possibly not as efficient as it can be but it's another way to return a Stream<T> where the T items are all distinct but without using overriding equals and hashcode as you've mentioned.
You can use concat()
IntStream.concat(stream1, stream2)
Finite sequence
Supposed you want to create a finite sequence of characters containing Y or R, randomly, just create some next() method and call it repeatedly:
public final class SequenceGenerator {
private final String[] elements;
private final Random rnd;
public SequenceGenerator(Collection<String> elements) {
this.elements = elements.toArray(new String[elements.size()]);
rnd = new Random();
}
public String next() {
return elements[rnd.nextInt(elements.length)];
}
public String generate(int amountOfElements) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < amountOfElements; i++) {
sb.append(next());
}
return sb.toString();
}
}
The class takes the collection of possible elements to draw from and its next methods chooses one randomly. The generate method calls it repeatedly and appends the drawn elements.
Usage is simple:
SequenceGenerator gen = new SequenceGenerator(List.of("Y", "R"));
System.out.println(gen.generate(11));
May print something like:
YYRYYRYYRYY
Endless stream
You can also nicely use the Stream API here to create an endless stream:
SequenceGenerator gen = new SequenceGenerator(List.of("Y", "R"));
Stream<String> sequence = Stream.generate(gen::next);
Edit: YY and R
As you said you want the stream to start and end with YY, as well as have a R at every third position, we need to modify next() a bit. First of all we add a field int amount to keep track of calls. Then next() is modified to account for YY at the start and R. Likewise generate must be modified to account for YY at the end. The Stream-variant can remain unchanged since it uses the modified next() and doesn't need to account for YY at the end since it is endless.
public final class SequenceGenerator {
// ...
private int amount;
public SequenceGenerator(Collection<String> elements) {
// ...
amount = 0;
}
public String next() {
// Start must be 'YY'
if (amount == 0 || amount == 1) {
amount++;
return "Y";
}
// Every third must be 'R'
if (amount % 3 == 0) {
amount++;
return "R";
}
// Pick a random element
amount++;
return elements[rnd.nextInt(elements.length)];
}
public String generate(int amountOfElements) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < amountOfElements - 2; i++) {
sb.append(next());
}
// End must be 'YY'
sb.append("YY");
return sb.toString();
}
}
Actually there is another requirement we need to account for. The concatenation of YY at the end must be valid in regards to the rule that forces a R at each third position. So for generate the amountOfElements must be two bigger than a value which is dividable by 3, else the resulting value is invalid.
And we need to account for edge cases, amountOfElements must be at least 5, every smaller sequence is not valid. YYRYY is the first valid sequence. The modified method:
public String generate(int amountOfElements) {
if (amountOfElements < 5 || (amountOfElements - 2) % 3 != 0) {
throw new IllegalArgumentException("Invalid sequence length");
}
// ...
}
If yor want to use a stream, you have to do something like this:
final long iterateLimit = 20;
final String sequence = "YYR";
final String suffix = "YY";
final String yyrString = Stream.generate(() -> sequence)
.limit(iterateLimit)
.collect(Collectors.joining())
.concat(suffix);
I offer generate sequence of "YYR" and add suffix "YY" after creating the string.
If they don't follow the same order, you'll need to create an ID map first:
Map<Integer, User> usersById = users.stream()
.collect(Collectors.toMap(User::getUserId, u -> u));
Now you can stream the other list and map each element to its matching User by ID:
List<UserCompanyView> views = userCompanies.stream()
.map(uc -> new UserCompanyView(usersById.get(uc.getUserId()), uc))
.collect(Collectors.toList())
If there are UserCompanys without matching Users, you can filter them out by adding this before map():
.filter(uc -> usersById.containsKey(uc.getUserId()))
In order for it to perform, you start by building a Map of user_id to User object.
Using streams, you'd do it like this:
List<User> users = // built elsewhere
Map<Integer, User> userById = users.stream()
.collect(Collectors.toMap(User::getUserId, u -> u));
Then you iterate and UserCompany objects, lookup the User object, and create the UserCompanyView object, adding them to a List.
Using streams, you'd do it like this:
List<UserCompany> userCompanies = // built elsewhere
List<UserCompanyView> views = userCompanies.stream()
.map(uc -> new UserCompanyView(userById.get(uc.getUserId()), uc))
.collect(Collectors.toList());