I have a set and I want to convert it to map in order to use it later in guava's Maps.difference(). I only care about the keys in the difference.
Came up with this version:
private <T> Map<T, T> toMap(Set<T> set) {
return set.stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
}
However, I know that usually, a set has a backing field of map. This is the method I use to create the map:
public static <E> Set<E> newConcurrentHashSet() {
return Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
}
Since I only need the keys I thought maybe I can get a view of this field somehow. any idea?
I ended up with a fairly straight-forward one line solution with Java 8 as follows:
Map<String, Foo> map = fooSet.stream().collect(Collectors.toMap(Foo::getKey, e -> e));
fooSet is a set of objects of type Foo, i.e. Set<Foo> fooSet
Foo has a getter called getKey which returns a String
You can convert a Set to a Map (same key and value taken from elements of Set) as shown below:
private <T> Map<T, T> toMap(Set<T> set) {
Map<T, T> map = new ConcurrentHashMap<>();
set.forEach(t -> map.put(t, t));//contains same key and value pair
return map;
}
Improvement of developer's answer:
Map<String, Foo> map = fooSet.stream().collect(Collectors.toMap(Foo::getKey, Function.identity()));
or if you statically import Collectors.toMap and Function.identity:
Map<String, Foo> map = fooSet.stream().collect(toMap(Foo::getKey, identity()));
From comment:
I would like to know which items only on left, which only on right, which in common (similar to map difference)
Use removeAll() and [retainAll()][3].
Example:
Set<Integer> set1 = new HashSet<>(Arrays.asList(1,3,5,7,9));
Set<Integer> set2 = new HashSet<>(Arrays.asList(3,4,5,6,7));
Set<Integer> onlyIn1 = new HashSet<>(set1);
onlyIn1.removeAll(set2);
Set<Integer> onlyIn2 = new HashSet<>(set2);
onlyIn2.removeAll(set1);
Set<Integer> inBoth = new HashSet<>(set1);
inBoth.retainAll(set2);
System.out.println("set1: " + set1);
System.out.println("set2: " + set2);
System.out.println("onlyIn1: " + onlyIn1);
System.out.println("onlyIn2: " + onlyIn2);
System.out.println("inBoth : " + inBoth);
Output
set1: [1, 3, 5, 7, 9]
set2: [3, 4, 5, 6, 7]
onlyIn1: [1, 9]
onlyIn2: [4, 6]
inBoth : [3, 5, 7]
Now, if you want to know all values and where they were found, you can do this (Java 8):
Set<Integer> setA = new HashSet<>(Arrays.asList(1,3,5,7,9));
Set<Integer> setB = new HashSet<>(Arrays.asList(3,4,5,6,7));
Map<Integer, String> map = new HashMap<>();
for (Integer i : setA)
map.put(i, "In A");
for (Integer i : setB)
map.compute(i, (k, v) -> (v == null ? "In B" : "In Both"));
System.out.println("setA: " + setA);
System.out.println("setB: " + setB);
map.entrySet().stream().forEach(System.out::println);
Output
setA: [1, 3, 5, 7, 9]
setB: [3, 4, 5, 6, 7]
1=In A
3=In Both
4=In B
5=In Both
6=In B
7=In Both
9=In A
See similar answer here.
Assuming that your original set is a set of values (no keys in original data!), you will need to specify keys for the newly created map. Guava's Maps.uniqueIndex might be helpful (see here)
Otherwise, if your original set is a set of keys (no values in original data!) that you want to retain, you will need to specify default or specific values for the newly created map. Guava's Maps.toMap might be helpful here. (See more here)
package com.example;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Set<Foo> s = new HashSet<>();
s.add(new Foo("cccc"));
s.add(new Foo("aaaa"));
s.add(new Foo("bbb"));
Map<String, Foo> m = s.stream().collect(Collectors.toMap(Foo::getKey, Function.identity()));
System.out.println(m);
}
}
class Foo {
String name;
Foo(String name){this.name = name;}
String getKey() {return name;}
}
Important note from the reference:
The returned Collector is not concurrent. For parallel stream
pipelines, the combiner function operates by merging the keys from one
map into another, which can be an expensive operation. If it is not
required that results are inserted into the Map in encounter order,
using toConcurrentMap(Function, Function) may offer better parallel
performance.
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.
As I couldn't find anything related to this, I am wondering if streams even allow this.
In my answer to another question, I have following code to add elements to a result list, only if the result list doesn't already contain it:
List<Entry<List<Integer>, Integer>> list = new ArrayList<>(diffMap.entrySet());
list.sort(Entry.comparingByValue());
List<List<Integer>> resultList = new ArrayList<>();
for (Entry<List<Integer>, Integer> entry2 : list) {
if (!checkResultContainsElement(resultList, entry2.getKey()))
resultList.add(entry2.getKey());
}
checkResultContainsElement method:
private static boolean checkResultContainsElement(List<List<Integer>> resultList, List<Integer> key) {
List<Integer> vals = resultList.stream().flatMap(e -> e.stream().map(e2 -> e2))
.collect(Collectors.toList());
return key.stream().map(e -> e).anyMatch(e -> vals.contains(e));
}
Now I am wondering, if this for-loop:
for (Entry<List<Integer>, Integer> entry2 : list) {
if (!checkResultContainsElement(resultList, entry2.getKey()))
resultList.add(entry2.getKey());
}
can be realized using streams. I don't think that .filter() method would work, as it would remove data from List<Entry<List<Integer>, Integer>> list while I don't even know if an element should be considered. I guess that a custom collector could work, but I also wouldn't know how to implement one, as the result is constantly changing with each newly added element.
I am looking for something like this (can be different if something else is better):
list.stream().sorted(Entry.comparingByValue()).collect(???);
where ??? would filter the data and return it as a list.
The values of one result list may not be contained in another one. So these lists are valid:
[1, 2, 3, 4]
[5, 6, 7, 8]
[12, 12, 12, 12]
but of these, only the first is valid:
[1, 2, 3, 4] <-- valid
[5, 3, 7, 8] <-- invalid: 3 already exists
[12, 12, 2, 12] <-- invalid: 2 already exists
If we put aside for a moment the details on whether implementation will be stream-based or not, the existing implementation of how uniqueness of the values of incoming lists is being checked can be improved.
We can gain a significant performance improvement by maintaining a Set of previously encountered values.
I.e. values from each list that was added to the resulting list would be stored in a set. And in order to ensure uniqueness of every incoming list, its values would be checked against the set.
Since operations of a stream pipeline should be stateless, as well as collector shouldn't hold a state (i.e. changes should happen only inside its mutable container). We can approach this problem by defining a container that will encompass a resulting list of lists of Foo and a set of foo-values.
I've implemented this container as a Java 16 record:
public record FooContainer(Set<Integer> fooValues, List<List<Foo>> foosList) {
public void tryAdd(List<Foo> foos) {
if (!hasValue(foos)) {
foos.forEach(foo -> fooValues.add(foo.getValue()));
foosList.add(foos);
}
}
public boolean hasValue(List<Foo> foos) {
return foos.stream().map(Foo::getValue).anyMatch(fooValues::contains);
}
}
The record shown above would is used as a mutable container of a custom collector created with Colloctors.of(). Collector's accumulator make's use of tryAdd() method defined by the container. And the finisher extracts the resulting list from the container.
Note that this operation is not parallelizable, hence combiner of the collector throws an AssertionError.
public static void main(String[] args) {
Map<List<Foo>, Integer> diffMap =
Map.of(List.of(new Foo(1), new Foo(2), new Foo(3)), 1,
List.of(new Foo(1), new Foo(4), new Foo(5)), 2,
List.of(new Foo(7), new Foo(8), new Foo(9)), 3);
List<List<Foo>> result = diffMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.collect(Collector.of(
() -> new FooContainer(new HashSet<>(), new ArrayList<>()),
FooContainer::tryAdd,
(left, right) -> {throw new AssertionError("The operation isn't parallelizable");},
FooContainer::foosList
));
System.out.println(result);
}
Output:
[[Foo{1}, Foo{2}, Foo{3}], [Foo{7}, Foo{8}, Foo{9}]]
May be something like this:-
list.stream().
sorted(Entry.comparingByValue()).
collect(ArrayList<List<Foo>>::new,(x,y)->!checkResultContainsElement(x, y.getKey()),(x,y)->x.add(y.getKey()));
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));
Is there any ready implementation of super MultiValue map?
I need a map
SuperMap<List<String>, List<String>> superMap
where key List is set of keys, and value List is set of values.
keys: {a, b, c} and values for any of these keys are values: {1, 2, 3, 4, 5}
It means that key a should have a values {1, 2, 3, 4, 5} as well as the key b and c.
Updated with requirements from comments
I need to get all keys of one group? For example I need to get collection of keys for one similar value. In that case I cannot use your approach map.put("a", value); because I need to group (a, b it is first group, and c is related to second group).
UPD2
I'm looking for a simple and much concise solution to replace this code
Map<List<String>, List<String>> ops = new HashMap<List<String>, List<String>>() {{
put(asList("a", "aaa"), asList("1", "2"));
put(asList("b", "bbb"), asList("3", "4"));
}};
public static List<String> values(String optionKey) {
for (List<String> key : ops.keySet()) {
for (String k : key) {
if (optionKey.equals(k)) {
return ops.get(key);
}
}
}
return Collections.emptyList();
}
with some smart impl form any well known lib (Google Guava or something).
I think you're overcomplicating your data structure. You can simply use HashMap or similar implementation.
Map<String, List<String>> map = new LinkedHashMap<>();
List<String> value = new ArrayList<>();
// Fill the value list here...
map.put("a", value);
map.put("b", value);
map.put("c", value);