I have a list of DTO objects with the nested list field.
The aim is to group them by id field and merge, and then sort the list using Streams API.
class DTO {
private Long id;
private List<ItemDTO> items;
}
class ItemDTO {
private Long priority;
private Long value;
}
// input
List<DTO> dtoList = List.of(
DTO(1, List.of(ItemDTO(1, 1), ItemDTO(7, 2))),
DTO(2, List.of(ItemDTO(1, 1), ItemDTO(2, 2))),
DTO(1, List.of(ItemDTO(10, 3), ItemDTO(1, 4)))
);
I need to group these nested objects with the same id field and merge all items in descending order by field priority.
The final result for this dtoList will be something like this:
// output
List<DTO> resultList = [
DTO(1, List.of(ItemDTO(10,3), ItemDTO(7,2), ItemDTO(1,1), ItemDTO(1,4)),
DTO(2, List.of(ItemDTO(2,2), ItemDTO(1,1),
];
Can we achieve this with Streams API?
I would start by a simple grouping by to get a map Map<Long,List<DTO>> and stream over the entries of that map and map each to a new DTO. You can extract a method / function to get the ItemDTOs sorted:
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
....
Function<List<DTO>, List<ItemDTO>> func =
list -> list.stream()
.map(DTO::getItems)
.flatMap(List::stream)
.sorted(Comparator.comparing(ItemDTO::getPriority,Comparator.reverseOrder()))
.collect(Collectors.toList());
List<DTO> result =
dtoList.stream()
.collect(Collectors.groupingBy(DTO::getId))
.entrySet().stream()
.map(entry -> new DTO(entry.getKey(), func.apply(entry.getValue())))
//.sorted(Comparator.comparingLong(DTO::getId)) if the resulting list need to be sorted by id
.collect(Collectors.toList());
You can create an intermediate map by grouping the data by id and then transform each entry into a new DTO object.
For that, you can use a combination of built-in collectors groupingBy() and flatMapping() to create an intermediate map.
In order to sort the items mapped each id, flatMapping() is being used in conjunction with collectionAndThen().
public static void main(String[] args) {
// input
List<DTO> dtoList = List.of(
new DTO(1L, List.of(new ItemDTO(1L, 1L), new ItemDTO(7L, 2L))),
new DTO(2L, List.of(new ItemDTO(1L, 1L), new ItemDTO(2L, 2L))),
new DTO(1L, List.of(new ItemDTO(10L, 3L), new ItemDTO(1L, 4L)))
);
List<DTO> result = dtoList.stream()
.collect(Collectors.groupingBy(DTO::getId,
Collectors.collectingAndThen(
Collectors.flatMapping(dto -> dto.getItems().stream(), Collectors.toList()),
(List<ItemDTO> items) -> {
items.sort(Comparator.comparing(ItemDTO::getPriority).reversed());
return items;
})))
.entrySet().stream()
.map(entry -> new DTO(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
result.forEach(System.out::println);
}
Output
DTO{id = 1, items = [ItemDTO{10, 3}, ItemDTO{7, 2}, ItemDTO{1, 1}, ItemDTO{1, 4}]}
DTO{id = 2, items = [ItemDTO{2, 2}, ItemDTO{1, 1}]}
As #shmosel has pointed out, flatMapping() is one of the boons of Java 9. You may also think of it as a reminder, maybe it's time to move to the modular system provided by Java 9 and other useful features.
The version that is fully compliant with Java 8 will look like this:
List<DTO> result = dtoList.stream()
.collect(Collectors.groupingBy(DTO::getId,
Collectors.collectingAndThen(
Collectors.mapping(DTO::getItems, Collectors.toList()),
(List<List<ItemDTO>> items) ->
items.stream().flatMap(List::stream)
.sorted(Comparator.comparing(ItemDTO::getPriority).reversed())
.collect(Collectors.toList())
)))
.entrySet().stream()
.map(entry -> new DTO(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
Imo, this is the easiest way. I am presuming you had the appropriate getters defined for you classes.
simply covert to a map keyed on the id.
merge the appropriate lists
and return the values and convert to an ArrayList.
List<DTO> results = new ArrayList<>(dtoList.stream().collect(
Collectors.toMap(DTO::getId, dto -> dto, (a, b) -> {
a.getItems().addAll(b.getItems());
return a;
})).values());
Then simply sort them in place based on your requirements. This takes no more time that doing it in the stream construct but in my opinion is less cluttered.
for (DTO d: results) {
d.getItems().sort(Comparator.comparing(ItemDTO::getPriority)
.reversed());
}
results.forEach(System.out::println);
prints (using a simple toString for the two classes)
DTO[1, [ItemDTO[10, 3], ItemDTO[7, 2], ItemDTO[1, 1], ItemDTO[1, 4]]]
DTO[2, [ItemDTO[2, 2], ItemDTO[1, 1]]]
Note: List.of is immutable so you can't change them. I would use new ArrayList<>(List.of(...)) in your list construct.
Related
I have a List of ProductTransactions. I want to find the Final (largest) productTransactionId sale for each product in List<ProductTransaction> . So I partition this by ProductId, and order by ProductTransactionId.
Final List in example below List<Integer> (2, 5, 9)
How can this be done? I am trying to use stream and filter.
#Data
public class ProductTransaction {
private int productTransactionId;
private int productId;
private Date saleDate;
private BigDecimal amount;
}
ProductTransactionId
ProductId
SaleDate
Amount
1
1
3/2/2019
5
2
1
4/1/2019
9
3
2
4/1/2019
2
4
2
8/21/2019
3
5
2
8/21/2019
4
6
3
10/1/2019
2
7
3
10/3/2019
5
8
3
10/3/2019
7
9
3
10/3/2019
8
(please ignore the SaleDate, only sort by ProductTransactionId;
The table input data, may not be necessarily sorted
currently using Java 8
Attempt:
current Long Solution (want to make cleaner short hand, or perhaps faster performance)
Set<Long> finalProductTransactionIds = new HashSet<>();
Set<Long> distinctProductIds = productTransactions.stream()
.map(ProductTransaction::getProductid)
.collect(Collectors.toSet());
for (Long productId: distinctProductIds) {
Long productTransactionId = productTransactions.stream()
.filter(x -> x.getProductId() == productId])
.sorted(Comparator.comparing(ProductTransaction::getProductTransactionId)
.reversed())
.collect(Collectors.toList()).get(0).getProductTransactionId();
finalProductTransactionIds.add(productTransactionId);
}
If you don't mind unwrapping Optionals, you can group by your product id and then use a mapping + maxBy downstream collector. This avoids having to collect to a temporary list, as only the last item will be kept (but adds minimal overhead for the optional instances).
final Map<Integer, Optional<Integer>> map = transactions.stream()
.collect(
Collectors.groupingBy(
ProductTransaction::getProductId,
Collectors.mapping(
ProductTransaction::getProductTransactionId,
Collectors.maxBy(Comparator.naturalOrder()))));
final Collection<Optional<Integer>> optionalMax = map.values();
final List<Optional<Integer>> max = optionalMax.stream()
.filter(Optional::isPresent)
.collect(Collectors.toList());
It is also possible to use the special overload of the toMap collector to avoid the Optional type:
final Collection<Integer> maxTransactionIds = transactions.stream()
.collect(
Collectors.toMap(
ProductTransaction::getProductId,
ProductTransaction::getProductTransactionId,
BinaryOperator.maxBy(Comparator.naturalOrder())))
.values();
Thanks to Eritrean for pointing out that getProductId returns an int, so we can replace the generally applicable BinaryOperator.maxBy(Comparator.naturalOrder) with the shorter Math::max (Math#max(int,int)) method reference, which will return the larger value of two integers:
final Collection<Integer> maxTransactionIds = transactions.stream()
.collect(
Collectors.toMap(
ProductTransaction::getProductId,
ProductTransaction::getProductTransactionId,
Math::max))
.values();
And maybe you don't like the Stream API. You can use a regular loop and the Map#merge function to achieve the same end result. If you squint, the merge call even looks like the toMap collector (why that is, is left as an exercise to the reader :)).
final Map<Integer, Integer> maxTxPerProduct = new HashMap<>();
for (final ProductTransaction transaction : transactions) {
maxTxPerProduct.merge(
transaction.getProductId(),
transaction.getProductTransactionId(),
Math::max);
}
final Collection<Integer> max = maxTxPerProduct.values();
It definitely avoids creating stream and collector objects (which is rarely a problem anyway).
Stream over your list and collect to map using productId as key and productTransactionId as value. If one or more objects share the same productId, take the one with the highest productTransactionId using Math::max and get the values of the map:
List<Integer> result = new ArrayList<>(
productTransactions.stream()
.collect(Collectors.toMap(ProductTransaction::getProductId,
ProductTransaction::getProductTransactionId,
Math::max))
.values());
To get Partition by First item in sort, just change to min
List<Integer> result = new ArrayList<>(
productTransactions.stream()
.collect(Collectors.toMap(ProductTransaction::getProductId,
ProductTransaction::getProductTransactionId,
Math::min))
.values());
You can achieve it with a little bit of collectors and grouping by. You can follow this helpful article for reference
Map<Integer, List<Integer>> productTransactionIdsByProductId = transactionList.stream()
.collect(Collectors.groupingBy(
ProductTransaction::getProductId,
Collectors.mapping(ProductTransaction::getProductTransactionId, Collectors.toList())));
final List<Integer> latestTransactionIds = new ArrayList<>();
productTransactionIdsByProductId.forEach( (k,v)-> {
if(!v.isEmpty())
latestTransactionIds.add(v.get(v.size()-1));
});
System.out.println(latestTransactionIds);
Using stream
record A(int tId, int pId, double amount) {
}
List<A> list = List.of(
new A(6, 3, 2),
new A(7, 3, 5),
new A(3, 2, 2),
new A(4, 2, 3),
new A(5, 2, 4),
new A(1, 1, 5),
new A(2, 1, 9),
new A(8, 3, 7),
new A(9, 3, 8)
);
Map<Integer, List<A>> grouped = list.stream()
.collect(Collectors.groupingBy(A::pId));
grouped.forEach((integer, as) -> as.sort(Comparator.comparing(A::tId).reversed()));
List<Integer> integers = grouped.values().stream()
.map(as -> as.stream().map(A::tId).findFirst().orElse(0))
.collect(Collectors.toList());
System.out.println(grouped);
System.out.println(integers);
[2, 5, 9]
BE SIMPLE !!!
Remember, that support of the code is much more complicated that implemnting. It is better to write smth. with a bit more lines, but much more clear.
E.g. Streams are quitre efficient, but sometime much more complicated to realise how it does it's work. In case you can write smth without it, do think about. Probably it can be more clear than streams.
public static List<Integer> getLargest(List<ProductTransaction> transactions) {
Map<Integer, Integer> map = new HashMap<>();
for (ProductTransaction transaction : transactions) {
int productId = transaction.getProductId();
map.put(productId, Math.max(map.getOrDefault(productId, 0),
transaction.getProductTransactionId()));
}
return new ArrayList<>(new TreeMap<>(map).values());
}
If you're open to third party libraries, StreamEx offers some nice helpers for more advanced transformations:
List<Integer> result = StreamEx.of(productTransactions)
.mapToEntry(
ProductTransaction::getProductId,
ProductTransaction::getProductTransactionId)
.collapseKeys(Math::max)
.values()
.toList();
Stream into a map accumulating on the desired key (in your case, productId) but resolving by max amount on map merge when you run into multiple values for the same key - BinaryOperator.maxBy below.
List<ProductTransaction> list = List.of(
new ProductTransaction(1, 1, "3/2/2019", 5),
new ProductTransaction(2, 1, "4/1/2019", 9),
new ProductTransaction(3, 2, "4/1/2019", 2),
new ProductTransaction(4, 2, "8/21/2019", 3),
new ProductTransaction(5, 2, "8/21/2019", 4),
new ProductTransaction(6, 3, "10/1/2019", 2),
new ProductTransaction(7, 3, "10/3/2019", 5),
new ProductTransaction(8, 3, "10/3/2019", 7),
new ProductTransaction(9, 3, "10/3/2019", 8));
Map<Integer, ProductTransaction> result = list.stream()
.collect(Collectors.toMap(tx -> tx.productId, Function.identity(),
BinaryOperator.maxBy(Comparator.comparingDouble(tx -> tx.amount.doubleValue()))));
System.out.println(result.values().stream().map(tx -> tx.productTransactionId).collect(Collectors.toList()));
prints: [2, 5, 9]
This question is about Java Streams' groupingBy capability.
Suppose I have a class, WorldCup:
public class WorldCup {
int year;
Country champion;
// all-arg constructor, getter/setters, etc
}
and an enum, Country:
public enum Country {
Brazil, France, USA
}
and the following code snippet:
WorldCup wc94 = new WorldCup(1994, Country.Brazil);
WorldCup wc98 = new WorldCup(1998, Country.France);
List<WorldCup> wcList = new ArrayList<WorldCup>();
wcList.add(wc94);
wcList.add(wc98);
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.groupingBy(WorldCup::getCountry, Collectors.mapping(WorldCup::getYear));
After running this code, championsMap will contain:
Brazil: [1994]
France: [1998]
Is there a succinct way to have this list include an entry for all of the values of the enum? What I'm looking for is:
Brazil: [1994]
France: [1998]
USA: []
There are several approaches you can take.
The map which would be used for accumulating the stream data can be prepopulated with entries corresponding to every enum-member. To access all existing enum-members you can use values() method or EnumSet.allOf().
It can be achieved using three-args version of collect() or through a custom collector created via Collector.of().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(
() -> EnumSet.allOf(Country.class).stream() // supplier
.collect(Collectors.toMap(
Function.identity(),
c -> new ArrayList<>()
)),
(Map<Country, List<Integer>> map, WorldCup next) -> // accumulator
map.get(next.getCountry()).add(next.getYear()),
(left, right) -> // combiner
right.forEach((k, v) -> left.get(k).addAll(v))
);
Another option is to add missing entries to the map after reduction of the stream has been finished.
For that we can use built-in collector collectingAndThen().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(WorldCup::getCountry,
Collectors.mapping(WorldCup::getYear,
Collectors.toList())),
map -> {
EnumSet.allOf(Country.class)
.forEach(country -> map.computeIfAbsent(country, k -> new ArrayList<>())); // if you're not going to mutate these lists - use Collections.emptyList()
return map;
}
));
I'm having a hard time converting a Map containing some integers as keys and a list of random strings as values.
E.g. :
1 = ["a", "b", "c"]
2 = ["a", "b", "z"]
3 = ["z"]
I want to transform it into a Map of distinct strings as keys and lists the integers as values.
E.g. :
a = [1, 2]
b = [1, 2]
c = [1]
z = [2,3]
Here's what I have so far:
Map<Integer, List<String>> integerListMap; // initial list is already populated
List<String> distinctStrings = new ArrayList<>();
SortedMap<String, List<Integer>> stringListSortedMap = new TreeMap<>();
for(Integer i: integers) {
integerListMap.put(i, strings);
distinctStrings.addAll(strings);
}
distinctStrings = distinctStrings.stream().distinct().collect(Collectors.toList());
for(String s : distinctStrings) {
distinctStrings.put(s, ???);
}
Iterate over your source map's value and put each value into the target map.
final Map<String, List<Integer>> target = new HashMap<>();
for (final Map.Entry<Integer, List<String>> entry = source.entrySet()) {
for (final String s : entry.getValue()) {
target.computeIfAbsent(s, k -> new ArrayList<>()).add(entry.getKey());
}
}
Or with the Stream API by abusing Map.Entry to build the inverse:
final Map<String, List<Integer>> target = source.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(s -> Map.entry(s, e.getKey()))
.collect(Collectors.groupingBy(e::getKey, Collectors.mapping(e::getValue, Collectors.toList())));
Although this might not be as clear as introducing a new custom type to hold the inverted mapping.
Another alternative would be using a bidirectial map. Many libraries come implementations of these, such as Guava.
There's no need to apply distinct() since you're storing the data into the Map and keys are guaranteed to be unique.
You can flatten the entries of the source map, so that only one string (let's call it name) and a single integer (let's call it number) would correspond to a stream element, and then group the data by string.
To implement this problem using streams, we can utilize flatMap() operation to perform one-to-many transformation. And it's a good practice to define a custom type for that purpose as a Java 16 record, or a class (you can also use a Map.Entry, but note that approach of using a custom type is more advantages because it allows writing self-documenting code).
In order to collect the data into a TreeMap you can make use of the three-args version of groupingBy() which allows to specify mapFactory.
record NameNumber(String name, Integer number) {}
Map<Integer, List<String>> dataByProvider = Map.of(
1, List.of("a", "b", "c"),
2, List.of("a", "b", "z"),
3, List.of("z")
);
NavigableMap<String, List<Integer>> numbersByName = dataByProvider.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(name -> new NameNumber(name, entry.getKey()))
)
.collect(Collectors.groupingBy(
NameNumber::name,
TreeMap::new,
Collectors.mapping(NameNumber::number, Collectors.toList())
));
numbersByName.forEach((name, numbers) -> System.out.println(name + " -> " + numbers));
Output:
a -> [2, 1]
b -> [2, 1]
c -> [1]
z -> [3, 2]
Sidenote: while using TreeMap it's more beneficial to use NavigableMap as an abstract type because it allows to access methods like higherKey(), lowerKey(), firstEntry(), lastEntry(), etc. which are declared in the SortedMap interface.
I have a Java bean, like
class EmployeeContract {
Long id;
Date date;
getter/setter
}
If a have a long list of these, in which we have duplicates by id but with different date, such as:
1, 2015/07/07
1, 2018/07/08
2, 2015/07/08
2, 2018/07/09
How can I reduce such a list keeping only the entries with the most recent date, such as:
1, 2018/07/08
2, 2018/07/09
?
Preferably using Java 8...
I've started with something like:
contract.stream()
.collect(Collectors.groupingBy(EmployeeContract::getId, Collectors.mapping(EmployeeContract::getId, Collectors.toList())))
.entrySet().stream().findFirst();
That gives me the mapping within individual groups, but I'm stuck as to how to collect that into a result list - my streams are not too strong I'm afraid...
Well, I am just going to put my comment here in the shape of an answer:
yourList.stream()
.collect(Collectors.toMap(
EmployeeContract::getId,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(EmployeeContract::getDate)))
)
.values();
This will give you a Collection instead of a List, if you really care about this.
You can do it in two steps as follows :
List<EmployeeContract> finalContract = contract.stream() // Stream<EmployeeContract>
.collect(Collectors.toMap(EmployeeContract::getId,
EmployeeContract::getDate, (a, b) -> a.after(b) ? a : b)) // Map<Long, Date> (Step 1)
.entrySet().stream() // Stream<Entry<Long, Date>>
.map(a -> new EmployeeContract(a.getKey(), a.getValue())) // Stream<EmployeeContract>
.collect(Collectors.toList()); // Step 2
First step: ensures the comparison of dates with the most recent one mapped to an id.
Second step: maps these key, value pairs to a final List<EmployeeContract> as a result.
Just to complement the existing answers, as you're asking:
how to collect that into a result list
Here are some options:
Wrap the values() into an ArrayList:
List<EmployeeContract> list1 =
new ArrayList<>(list.stream()
.collect(toMap(EmployeeContract::getId,
identity(),
maxBy(comparing(EmployeeContract::getDate))))
.values());
Wrap the toMap collector into collectingAndThen:
List<EmployeeContract> list2 =
list.stream()
.collect(collectingAndThen(toMap(EmployeeContract::getId,
identity(),
maxBy(comparing(EmployeeContract::getDate))),
c -> new ArrayList<>(c.values())));
Collect the values to a new List using another stream:
List<EmployeeContract> list3 =
list.stream()
.collect(toMap(EmployeeContract::getId,
identity(),
maxBy(comparing(EmployeeContract::getDate))))
.values()
.stream()
.collect(toList());
With vavr.io you can do it like this:
var finalContract = Stream.ofAll(contract) //create io.vavr.collection.Stream
.groupBy(EmployeeContract::getId)
.map(tuple -> tuple._2.maxBy(EmployeeContract::getDate))
.collect(Collectors.toList()); //result is list from java.util package
I would like to put the frequencies of the numbers in a TreeMap with the frequencies as the keys and the numbers that have that frequency in an ArrayList.
I have two problems:
1) I'm getting a "non-static methods cannot be referenced from a static context" error in the first parameter (AFAIK the stream references an object - what's going on?)
2) There are 4 parameters for Collectors.toMap() - it seems like parameter 4 requires initialization with a new TreeMap>, parameter 2 could be an ArrayList add() function and parameter 3 could be null (maybe). How is this done?
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> array = Arrays.asList(1, 2, 4, 5, 6, 4, 8, 4, 2, 3);
Map<Integer, Long> m = array.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
System.out.println(m);
TreeMap<Long, List<Integer>> tm = m.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, ...));
At the moment I can't use see how to get from
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html to where I need to be.
I think Collectors.groupingBy makes more sense than Collectors.toMap to achieve what you are looking for:
Map<Long, List<Integer>> tm =
m.entrySet()
.stream()
.collect(Collectors.groupingBy(Map.Entry::getValue, // group the entries by the
// value (the frequency)
TreeMap::new, // generate a TreeMap
Collectors.mapping (Map.Entry::getKey,
Collectors.toList()))); // the
// value of the output TreeMap
// should be a List of the
// original keys
You can replace Collectors.toList() with Collectors.toCollection(ArrayList::new) to make sure that the values of the output Map are ArrayLists (though the current implementation of toList() already results in java.util.ArrayList instances).
For your sample input, this produces the following TreeMap:
{1=[1, 3, 5, 6, 8], 2=[2], 3=[4]}
I wouldn't use streams to create the inverted map. Instead, I would just do:
Map<Long, List<Integer>> tm = new TreeMap<>();
m.forEach((num, freq) -> tm.computeIfAbsent(freq, k -> new ArrayList<>()).add(num));
System.out.println(tm); // {1=[1, 3, 5, 6, 8], 2=[2], 3=[4]}
As the code to create the inverted map is short, you could use Collectors.collectingAndThen to create both the frequencies and inverted map in one step:
TreeMap<Long, List<Integer>> invertedFrequenciesMap = array.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Function.identity(), Collectors.counting()),
map -> {
TreeMap<Long, List<Integer>> tm = new TreeMap<>();
map.forEach((num, freq) ->
tm.computeIfAbsent(freq, k -> new ArrayList<>()).add(num));
return tm;
}));
You are almost correct in your reasoning... Just that the 3-rd argument is where you merge your values for the same key - so you can't omit it.
TreeMap<Long, List<Integer>> tm = m.entrySet().stream()
.collect(Collectors.toMap(
Entry::getValue,
x -> {
List<Integer> list = new ArrayList<>();
list.add(x.getKey());
return list;
},
(left, right) -> {
left.addAll(right);
return left;
},
TreeMap::new));