Java 8 Collect two Lists to Map by condition - java

I have an object:
public class CurrencyItem {
private CurrencyName name;
private BigDecimal buy;
private BigDecimal sale;
private Date date;
//...
}
where CurrencyName is one of: EUR, USD, RUR etc.
And two lists
List<CurrencyItem> currenciesByCommercialBank = ...
List<CurrencyItem> currenciesByCentralBank = ...
How can I merge this lists to the Map<CurrencyItem, CurrencyItem> where keys are currenciesByCommercialBank and values are currenciesByCentralBank with condition such as
currenciesByCommercialBank.CurrencyName == currenciesByCentralBank.CurrencyName

This should be optimal. You first build a map from the currencies to their commercial banks. Then you run through your centrals building a map from commercial to central (looked up in the first map).
List<CurrencyItem> currenciesByCommercialBank = new ArrayList<>();
List<CurrencyItem> currenciesByCentralBank = new ArrayList<>();
// Build my lookup from CurrencyName to CommercialBank.
Map<CurrencyName, CurrencyItem> commercials = currenciesByCommercialBank
.stream()
.collect(
Collectors.toMap(
// Map from currency name.
ci -> ci.getName(),
// To the commercial bank itself.
ci -> ci));
Map<CurrencyItem, CurrencyItem> commercialToCentral = currenciesByCentralBank
.stream()
.collect(
Collectors.toMap(
// Map from the equivalent commercial
ci -> commercials.get(ci.getName()),
// To this central.
ci -> ci
));

The following code is O(n2), but it should be OK for small collections (which your lists probably are):
return currenciesByCommercialBank
.stream()
.map(c ->
new AbstractMap.SimpleImmutableEntry<>(
c, currenciesByCentralBank.stream()
.filter(c2 -> c.currencyName == c2.currencyName)
.findFirst()
.get()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
The above is appropriate if you want to assert that currenciesByCentralBank contains a match for each item in currenciesByCommercialBank. If the two lists can have mismatches, then the following would be appropriate:
currenciesByCommercialBank
.stream()
.flatMap(c ->
currenciesByCentralBank.stream()
.filter(c2 -> c.currencyName == c2.currencyName)
.map(c2 -> new AbstractMap.SimpleImmutableEntry<>(c, c2)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
In this case the map will contain all the matches and won't complain about missing entries.

Related

Have Java Streams GroupingBy result Map include a key for each value of an enum, even if value is an empty List

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;
}
));

How to group Objects by property using using collector groupingBy() on top of flatMap() in Java 8

How to create Map<String,List<Product>> of below. Here, String (key of the Map) is the category of a Product.
One product can belong to multiple categories, like in the example below.
I am trying with below code, however not able to get next operation:
products.stream()
.flatMap(product -> product.getCategories().stream())
. // how should I progress from here?
Result should be like below:
{electonics=[p1,p3,p4], fashion=[p1,p2,p4], kitchen=[p1,p2,p3],
abc1=[p2], xyz1=[p3],pqr1=[p4]}
Product p1 = new Product(123, Arrays.asList("electonics,fashion,kitchen".split(",")));
Product p2 = new Product(123, Arrays.asList("abc1,fashion,kitchen".split(",")));
Product p3 = new Product(123, Arrays.asList("electonics,xyz1,kitchen".split(",")));
Product p4 = new Product(123, Arrays.asList("electonics,fashion,pqr1".split(",")));
List<Product> products = Arrays.asList(p1, p2, p3, p4);
class Product {
int price;
List<String> categories;
public Product(int price) {
this.price = price;
}
public Product(int price, List<String> categories) {
this.price = price;
this.categories = categories;
}
public int getPrice() {
return price;
}
public List<String> getCategories() {
return categories;
}
}
If you want to use collector groupingBy() for some reason, then you can define a wrapper class (with Java 16+ a record would be more handy for that purpose) which would hold a reference to a category and a product to represent every combination category/product which exist in the given list.
public record ProductCategory(String category, Product product) {}
Pre-Java 16 alternative:
public class ProductCategory {
private String category;
private Product product;
// constructor and getters
}
And then in the make use of the combination of collectors mapping() and toList() as the downstream collector of groupingBy().
List<Product> products = // initializing the list of products
Map<String, List<Product>> productsByCategory = products.stream()
.flatMap(product -> product.getCategories().stream()
.map(category -> new ProductCategory(category, product)))
.collect(Collectors.groupingBy(
ProductCategory::category, // ProductCategory::getCategory if you used a class instead of record
Collectors.mapping(ProductCategory::product, // ProductCategory::getProduct if you used a class instead of record
Collectors.toList())
));
A link to Online-Demo
But instead of creating intermediate objects and generating nested streams, the more performant option would be to describe the accumulation strategy within the three-args version of collect() (or define a custom collector).
That's how it might be implemented:
Map<String, List<Product>> productsByCategory = products.stream()
.collect(
HashMap::new,
(Map<String, List<Product>> map, Product next) -> next.getCategories()
.forEach(category -> map.computeIfAbsent(category, k -> new ArrayList<>())
.add(next)),
(left, right) -> right.forEach((k, v) ->
left.merge(k, v,(oldProd, newProd) -> { oldProd.addAll(newProd); return oldProd; }))
);
A link to Online-Demo
I tried a few things and came up with the following solution:
Map<Object, List<Product>> result =
products.stream()
.flatMap(product -> product.getCategories().stream().map(p -> Map.entry(p, product)))
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
System.out.println(result);
Output:
xyz1=[org.example.Product#15db9742], electonics=[org.example.Product#6d06d69c, org.example.Product#15db9742, org.example.Product#7852e922], abc1=[org.ex ...
Edit: I have seen that my solution is pretty similar to the other answer. However, my solution uses a Map.Entry instead of a user-defined object to bring the data into the correct shape.
This can also be done with a combination of flatMapping and toMap:
Map<String, List<Product>> obj = products.stream()
.collect(
Collectors.flatMapping(
product -> product.categories().stream()
.map(category -> Map.entry(category, List.of(product))),
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> Stream.concat(v1.stream(), v2.stream()).toList()
)
));
What happens here is that first, each Product is converted to a Map.Entry<String, List<Product>>, where the key is the category and the value is the Product itself, or, more precisely, a List<Product>, where this list initially only contains the current product.
Then you could "unpack" the map entries by using toMap. Of course, for those cases where the key (=category) is the same, the values (that is, the List with the Products) must be merged.
Note: I used a Map.Entry here, but you can also write a custom class which is semantically more desirable (something like CategoryProductsMapping(String category, List<Product> products).
I am aware that the owner inquired about groupby and flatmap, but just in case, I'll mention reduce.
I believe this is kind simple; it feels like .collect( method with 3 args that #Alexander Ivanchenko metioned.
in order to use parallelstream, you must merges two hashmaps, I don’t think it’s a good idea, there are extra iteration, I don’t think it’s useful in this case.
HashMap<String, List<Product>> reduce = products.stream().reduce(
new HashMap<>(),
(result, product) -> {
product.getCategories().stream().distinct().forEach(
category -> result.computeIfAbsent(category, k -> new ArrayList<>())
.add(product)
);
return result;
},
(x, y) -> {
throw new RuntimeException("does not support parallel!");
}
);

Java Stream fill map from two lists by the first list items as keys

I have two lists that I need to check that every product (from products) has a code (from productCodes)
List<String> productCodes = List.of("X_14_AA_85", "X_14_BB_85", "X_14_ZZ_85");
List<String> products = List.of("AA", "BB", "CC", "ZZ");
// I want to achieve a collection of (product code, product)
// according if product name exists in productCode name
// key - product code, value - product
/*
Map<String, String> map = Map.of(
"AA", "X_14_AA_85",
"BB", "X_14_BB_85",
"CC", null, // null if code doesn't exist
"ZZ", "X_14_ZZ_85"
);
*/
// after a filter with null keys I could return a message something like this
// List<String> nullableProducts = List.of("CC");
// return "I could prompt that there's no code for product/s: " + nullableProducts;
Is there a way with streams to filter by list item values?
You can stream the keySet and filter null values:
Java 16+:
List<String> list = map.keySet().stream()
.filter(key -> map.get(key) == null).toList();
Java 15 and older:
List<String> list = map.keySet().stream()
.filter(key -> map.get(key) == null)
.collect(Collectors.toList());
Note: You can't instantiate an unmodifiable Map using Map.of() with null keys or values. Instead, you can do:
Map<String, String> map = new HashMap<>();
map.put("AA", "X_14_AA_85");
map.put("BB", "X_14_BB_85");
map.put("CC", null);
map.put("ZZ", "X_14_ZZ_85");
If the purpose is to get a map containing null value, this has to be implemented using a custom collector, because existing implementation throws NullPointerException when putting null:
List<String> productCodes = List.of("X_14_AA_85", "X_14_BB_85", "X_14_ZZ_85");
List<String> products = List.of("AA", "BB", "CC", "ZZ");
Map<String, String> mapCodes = products.stream()
.distinct()
.collect(
HashMap::new,
(m, p) -> m.put(p, productCodes
.stream()
.filter(pc -> pc.contains(p))
.findFirst()
.orElse(null)
),
HashMap::putAll
);
// -> {AA=X_14_AA_85, BB=X_14_BB_85, CC=null, ZZ=X_14_ZZ_85}
Then the list of non-matched products may be retrieved as follows:
List<String> nonMatchedProducts = mapCodes.entrySet()
.stream()
.filter(e -> e.getValue() == null)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
// -> [CC]
However, as the result of findFirst is returned as Optional it may be used along with Collectors::toMap, and then the non-matched values can be filtered out using Optional::isEmpty:
Map<String, Optional<String>> mapCodes2 = products.stream()
.distinct()
.collect(Collectors.toMap(
p -> p,
p -> productCodes.stream().filter(pc -> pc.contains(p)).findFirst()
));
// -> {AA=Optional[X_14_AA_85], BB=Optional[X_14_BB_85], CC=Optional.empty, ZZ=Optional[X_14_ZZ_85]}
List<String> nonMatchedProducts2 = mapCodes2.entrySet()
.stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
// -> [CC]
Also, the null/empty values may not be stored at all, then non-matched products can be found after removing all the matched ones:
Map<String, String> map3 = new HashMap<>();
for (String p : products) {
productCodes.stream()
.filter(pc -> pc.contains(p))
.findFirst()
.ifPresent(pc -> map3.put(p, pc)); // only matched pairs
}
// -> {AA=X_14_AA_85, BB=X_14_BB_85, ZZ=X_14_ZZ_85}
List<String> nonMatchedProducts3 = new ArrayList<>(products);
nonMatchedProducts3.removeAll(map3.keySet());
// -> [CC]
Given your two lists, I would do something like this. I added two products that contain non-existent codes.
List<String> products =
List.of("X_14_AA_85", "X_14_SS_88", "X_14_BB_85", "X_14_ZZ_85", "X_16_RR_85");
List<String> productCodes = List.of("AA", "BB", "CC", "ZZ");
Declare a lambda to extract the code and copy the codes to a set for efficient lookup. In fact, since duplicates codes aren't necessary, a set would be the preferred data structure from the start.
Assuming the product code is the same place and length, you can do it like this using substring. Otherwise you may need to use a regular expression to parse the product string.
Function<String, String> extractCode =
code -> code.substring(5,7);
Set<String> productCodeSet = new HashSet<>(productCodes);
And run it like this.
List<String> missingCodes = products.stream()
.filter(product -> !productCodeSet
.contains(extractCode.apply(product)))
.toList();
System.out.println("There are no codes for the following products: " + missingCodes);
Prints
There are no codes for the following products: [X_14_SS_88, X_16_RR_85]

Java Stream - Combine Two Streams

Is there a way I can combine these two streams into one?
Here's the first stream
Map<String, String> rawMapping = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("first_family"))
.findAny()
.map(columns -> columns.getColumns().stream()).get()
.collect(Collectors.toMap(
Column::getPrefix,
Column::getValue
));
Second stream
List<Token> tokenValues = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("second_family"))
.findAny()
.map(columns -> columns.getColumns().stream()).get()
.map(token -> {
return Token.builder()
.qualifier(token.getPrefix())
.raw(rawMapping.get(token.getPrefix()))
.token(token.getValue())
.build();
})
.collect(Collectors.toList());
Basically tokens is a list which has two column family, my goal is to create a list which will combine the value of the two-column family based on their qualifier. The first stream is storing the first column family into a map. The second stream is traversing the second family and getting the value thru the map using the qualifier and storing it into a new list.
you can use double filtering and then later you might use a flat map then to get a list:
Map<String, String> tokenvalues = tokens.getColumnFamilies().stream()
.filter(family -> family.getName().equals("first_family"))
.filter(family -> family.getName().equals("second_family"))
.map(columns -> columns.getColumns().stream())
//etc..
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()));
you can remake a stream out of it inline
https://www.baeldung.com/java-difference-map-and-flatmap

More efficient solution on coding task using Stream API?

I recently had a technical interview and got small coding task on Stream API.
Let's consider next input:
public class Student {
private String name;
private List<String> subjects;
//getters and setters
}
Student stud1 = new Student("John", Arrays.asList("Math", "Chemistry"));
Student stud2 = new Student("Peter", Arrays.asList("Math", "History"));
Student stud3 = new Student("Antony", Arrays.asList("Music", "History", "English"));
Stream<Student> studentStream = Stream.of(stud1, stud2, stud3);
The task is to find Students with unique subjects using Stream API.
So for the provided input expected result (ignoring order) is [John, Anthony].
I presented the solution using custom Collector:
Collector<Student, Map<String, Set<String>>, List<String>> studentsCollector = Collector.of(
HashMap::new,
(container, student) -> student.getSubjects().forEach(
subject -> container
.computeIfAbsent(subject, s -> new HashSet<>())
.add(student.getName())),
(c1, c2) -> c1,
container -> container.entrySet().stream()
.filter(e -> e.getValue().size() == 1)
.map(e -> e.getValue().iterator().next())
.distinct()
.collect(Collectors.toList())
);
List<String> studentNames = studentStream.collect(studentsCollector);
But the solution was considered as not optimal/efficient.
Could you please share your ideas on more efficient solution for this task?
UPDATE: I got another opinion from one guy that he would use reducer (Stream.reduce() method).
But I cannot understand how this could increase efficiency. What do you think?
Here is another one.
// using SimpleEntry from java.util.AbstractMap
Set<Student> list = new HashSet<>(studentStream
.flatMap(student -> student.getSubjects().stream()
.map(subject -> new SimpleEntry<>(subject, student)))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)
.values());
list.remove(Student.SENTINEL_VALUE);
(Intentionally using a sentinel value, more about that below.)
The steps:
Set<Student> list = new HashSet<>(studentStream
We're creating a HashSet from the Collection we're going to collect. That's because we want to get rid of the duplicate students (students with multiple unique subjects, in your case Antony).
.flatMap(student -> student.subjects()
.map(subject -> new SimpleEntry(subject, student)))
We are flatmapping each student's subjects into a stream, but first we map each element to a pair with as key the subject and as value the student. This is because we need to retain the association between the subject and the student. I'm using AbstractMap.SimpleEntry, but of course, you can use any implementation of a pair.
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (l, r) -> Student.SENTINEL_VALUE)
We are collecting the values into a map, setting the subject as key and the student as value for the resulting map. We pass in a third argument (a BinaryOperator) to define what should happen if a key collision takes place. We cannot pass in null, so we use a sentinel value1.
At this point, we have inverted the relation student ↔ subject by mapping each subject to a student (or the SENTINEL_VALUE if a subject has multiple students).
.values());
We take the values of the map, yielding the list of all students with a unique subject, plus the sentinel value.
list.remove(Student.SENTINEL_VALUE);
The only thing left to do is getting rid of the sentinel value.
1 We cannot use null in this situation. Most implementations of a Map make no distinction between a key mapped to null or the absence of that particular key. Or, more accurately, the merge method of HashMap actively removes a node when the remapping function returns null. If we want to avoid a sentinel value, then we must implement or own merge method, which could be implemented like something like this: return (!containsKey(key) ? super.merge(key, value, remappingFunction) : put(key, null));.
Another solution. Looks kind of similar to Eugene.
Stream.of(stud1, stud2, stud3, stud4)
.flatMap( s -> s.getSubjects().stream().map( subj -> new AbstractMap.SimpleEntry<>( subj, s ) ) )
.collect( Collectors.groupingBy(Map.Entry::getKey) )
.entrySet().stream()
.filter( e -> e.getValue().size() == 1 )
.map( e -> e.getValue().get(0).getValue().getName() )
.collect( Collectors.toSet() );
Not the most readable solution, but here you go:
studentStream.flatMap(st -> st.getSubjects().stream().map(subj -> new SimpleEntry<>(st.getName(), subj)))
.collect(Collectors.toMap(
Entry::getValue,
x -> {
List<String> list = new ArrayList<>();
list.add(x.getKey());
return list;
},
(left, right) -> {
left.addAll(right);
return left;
}
))
.entrySet()
.stream()
.filter(x -> x.getValue().size() == 1)
.map(Entry::getValue)
.flatMap(List::stream)
.distinct()
.forEachOrdered(System.out::println);
You can probably do it in a simpler way as :
Stream<Student> studentStream = Stream.of(stud1, stud2, stud3);
// collect all the unique subjects into a Set
Set<String> uniqueSubjects = studentStream
.flatMap(st -> st.getSubjects().stream()
.map(subj -> new AbstractMap.SimpleEntry<>(st.getName(), subj)))
// subject to occurence count map
.collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.counting()))
.entrySet()
.stream()
.filter(x -> x.getValue() == 1) // occurs only once
.map(Map.Entry::getKey) // Q -> map keys are anyway unique
.collect(Collectors.toSet()); // ^^ ... any way to optimise this?(keySet)
// amongst the students, filter those which have any unique subject in their subject list
List<String> studentsStudyingUniqueSubjects = studentStream
.filter(stud -> stud.getSubjects().stream()
.anyMatch(uniqueSubjects::contains))
.map(Student::getName)
.collect(Collectors.toList());

Categories