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));
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]
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 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.
I have the following for loop which I would like to replace by a simple Java 8 stream statement:
List<String> words = new ArrayList<>("a", "b", "c");
Map<String, Long> wordToNumber = new LinkedHashMap<>();
Long index = 1L;
for (String word : words) {
wordToNumber.put(word, index++);
}
I basically want a sorted map (by insertion order) of each word to its number (which is incremented at each for loop by 1), but done simpler, if possible with Java 8 streams.
Map<String, Long> wordToNumber =
IntStream.range(0, words.size())
.boxed()
.collect(Collectors.toMap(
words::get,
x -> Long.valueOf(x) + 1,
(left, right) -> { throw new RuntimeException();},
LinkedHashMap::new
));
You can replace that (left, right) -> { throw new RuntimeException();} depending on how you want to merge two elements.
The following should work (though it's not clear why Long is needed because the size of List is int)
Map<String, Long> map = IntStream.range(0, words.size())
.boxed().collect(Collectors.toMap(words::get, Long::valueOf));
The code above works if there's no duplicate in the words list.
If duplicate words are possible, a merge function needs to be provided to select which index should be stored in the map (first or last)
Map<String, Long> map = IntStream.range(0, words.size())
.boxed().collect(
Collectors.toMap(words::get, Long::valueOf,
(w1, w2) -> w2, // keep the index of the last word as in the initial code
LinkedHashMap::new // keep insertion order
));
Similarly, the map can be built by streaming words and using external variable to increment the index (AtomicLong and getAndIncrement() may be used instead of long[]):
long[] index = {1L};
Map<String, Long> map = words.stream()
.collect(
Collectors.toMap(word -> word, word -> index[0]++,
(w1, w2) -> w2, // keep the index of the last word
LinkedHashMap::new // keep insertion order
));
A slightly different solution. The Integer::max is the merge function which gets called if the same word appears twice. In this case it picks the last position since that effectively what the code sample in the question does.
#Test
public void testWordPosition() {
List<String> words = Arrays.asList("a", "b", "c", "b");
AtomicInteger index = new AtomicInteger();
Map<String, Integer> map = words.stream()
.map(w -> new AbstractMap.SimpleEntry<>(w, index.incrementAndGet()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, Integer::max));
System.out.println(map);
}
Output:
{a=1, b=4, c=3}
Edit:
Incorporating Alex's suggestions in the comments, it becomes:
#Test
public void testWordPosition() {
List<String> words = Arrays.asList("a", "b", "c", "b");
AtomicLong index = new AtomicLong();
Map<String, Long> map = words.stream()
.collect(Collectors.toMap(w -> w, w -> index.incrementAndGet(), Long::max));
System.out.println(map);
}
I basically want a sorted map (by insertion order) of each word to its
number (which is incremented at each for loop by 1), but done simpler,
if possible with Java 8 streams.
You can do it concisely using the following Stream:
AtomicLong index = new AtomicLong(1);
words.stream().forEach(word -> wordToNumber.put(word, index.getAndIncrement()));
Personally, I think that either
Map<String, Long> wordToNumber = new LinkedHashMap<>();
for(int i = 0; i < words.size(); i++){
wordToNumber.put(words.get(i), (long) (i + 1));
}
or
Map<String, Long> wordToNumber = new LinkedHashMap<>();
for (String word : words) {
wordToNumber.put(word, index++);
}
is simpler enough.
As for example, there are two lists:
List<Double> list1 = Arrays.asList(1.0, 2.0);
List<String> list2 = Arrays.asList("one_point_zero", "two_point_zero");
Using Stream, I want to create a map composed of these lists, where list1 is for keys and list2 is for values. To do it, I need to create an auxiliary list:
List<Integer> list0 = Arrays.asList(0, 1);
Here is the map:
Map<Double, String> map2 = list0.stream()
.collect(Collectors.toMap(list1::get, list2::get));
list0 is used in order list1::get and list2::get to work. Is there a simpler way without creation of list0? I tried the following code, but it didn't work:
Map<Double, String> map2 = IntStream
.iterate(0, e -> e + 1)
.limit(list1.size())
.collect(Collectors.toMap(list1::get, list2::get));
Instead of using an auxiliary list to hold the indices, you can have them generated by an IntStream.
Map<Double, String> map = IntStream.range(0, list1.size())
.boxed()
.collect(Collectors.toMap(i -> list1.get(i), i -> list2.get(i)));
Indeed the best approach is to use IntStream.range(startInclusive, endExclusive) in order to access to each element of both lists with get(index) and finally use Math.min(a, b) to avoid getting IndexOutOfBoundsException if the lists are not of the exact same size, so the final code would be:
Map<Double, String> map2 = IntStream.range(0, Math.min(list1.size(), list2.size()))
.boxed()
.collect(Collectors.toMap(list1::get, list2::get));
This works for me but is O(n^2):
Map<Double, String> collect =
list1.stream()
.collect(
toMap(Double::doubleValue,
item -> list2.get(list1.indexOf(item))));