How to convert Array to HashMap in Java with Stream API - java

I was trying to something pretty simple, but it fails on compilation, and I can't understand who
I have a list of headers, I need to convert it to
Map<Index, String> meaning the key (index) and the value is the header name
I know how to make it with for each, but I want to have it in Collectors.to map
any help would be appreciated
final String[] headerDisplayName = getHeaderDisplayName(harmonizationComponentDataFixRequest);
IntStream.of(0, headerDisplayName.length).collect(Collectors.toMap(Function.identity(), index-> headerDisplayName[index]));

You can use range method in combination with boxed method of IntStream.
(When you use the of method like in your example, only 0 and the size of the array are in this stream. In addition this would lead to an ArrayIndexOutOfBoundsException)
A possible solution would look like this (first parameter of the range method is included, the second parameter is excluded)
Map<Integer, String> map = IntStream.range(0, headerDisplayName.length)
.boxed()
.collect(Collectors.toMap(
Function.identity(),
i -> headerDisplayName[i])
);

Adding to the #csalmhof's answer, I think it's to explain here why using boxed is working.
If you don't use boxed() method and simply write the following:
Map<Integer, String> map = IntStream.range(0, headerDisplayName.length)
.collect(Collectors.toMap(
Function.identity(),
index -> headerDisplayName[index])
);
Java will have to take index as of type Object and there's no implicit conversion that can happen and so you'll get error.
But if you put boxed() like in the following code, you won't get error:
Map<Integer, String> map = IntStream.range(0, headerDisplayName.length)
.boxed()
.collect(Collectors.toMap(
Function.identity(),
index -> headerDisplayName[index])
);
you're not getting error here because java interprets index as an Integer and there's implicit casting happening from Integer to int.
A good IDE can help you with this type of explanation. In IntelliJ if you press ctrl + space after keeping your cursor on index (with Eclipse key-map enabled) you will get the following without boxed().
And this is what you get when you've boxed() placed.
I hope this clarifies why using boxed() is saving you from compilation error. Also, you can use this thing in future to find actual type of parameter in lambda which can be helpful in case cases (one of the case is the one that OP pointed out)

Related

How to fix Duplicate Key IllegalStateException while using Collectors.toMap()

I have a stream that processes some strings and collects them in a map.
But getting the following exception:
java.lang.IllegalStateException:
Duplicate key test#yahoo.com
(attempted merging values [test#yahoo.com] and [test#yahoo.com])
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:133)
I'm using the following code:
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email)
));
The flavor of toMap() you're using in your code (which expects only keyMapper and valueMapper) disallow duplicates merely because it's not capable to handle them. And exception message explicitly tells you that.
Judging by the resulting type Map<String, List<String>> and by the exception message which shows strings enclosed in square brackets, it is possible to make the conclusion that processEmails(email) produces a List<String> (although it's not obvious from your description and IMO worth specifying).
There are multiple ways to solve this problem, you can either:
Use this another version of toMap(keyMapper,valueMapper,mergeFunction) which requires the third argument, mergeFunction - a function responsible for resolving duplicates.
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email),
(list1, list2) -> list1 // or { list1.addAll(list2); return list1} depending on the your logic of resolving duplicates you need
));
Make use of the collector groupingBy(classifier,downstream) to preserve all the emails retrieved by processEmails() that are associated with the same key by storing them into a List. As a downstream collector we could utilize a combination of collectors flatMapping() and toList().
Map<String, List<String>> map = emails.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.flatMapping(email -> processEmails(email).stream(),
Collectors.toList())
));
Note that the later option would make sense only if processEmails() somehow generates different results for the same key, otherwise you would end up with a list of repeated values which doesn't seem to be useful.
But what you definitely shouldn't do in this case is to use distinct(). It'll unnecessarily increase the memory consumption because it eliminates the duplicates by maintaining a LinkedHashSet under the hood. It would be wasteful because you're already using Map which is capable to deal with duplicated keys.
You have duplicate emails. The toMap version you're using explicitly doesn't allow duplicate keys. Use the toMap that takes a merge function. How to merge those processEmails results depends on your business logic.
Alternatively, use distinct() before collecting, because otherwise you'll probably end up sending some people multiple emails.
try using
Collectors.toMap(Function keyFuntion, Function valueFunction, BinaryOperator mergeFunction)
You obviously have to write your own merge logic, a simple mergeFunction could be
(x1, x2) -> x1

Using Comparator.thenComparing for passed string

I'm using streams to try and create an arraylist of the keys in a map sorted first by the values (integers) then sort the keys alphabetically. I have them sorted by the values, but I get an error when trying to compare them alphabetically:
return map.keySet()
.stream()
.sorted(Comparator.comparing( (k1) -> map.get(k1)).thenComparing(String::compareTo)) //ErrorHere
.toArray(String[]::new);
Coc-java gives me a The method thenComparing(Comparator<? super Object>) in the type Comparator<Object> is not applicable for the arguments (String::compareTo) error. I have used thenComparing before, but the .sorted method looked like this:
.sorted(Comparator.comparing(String::length).thenComparing(String::compareTo))
This produced no errors and worked fine. I'm supposing that it might have something to do with what the lamda returns?
You probably just need to explicitly specify the type, e.g. Comparator.comparing((String k1) -> map.get(k1)), or Comparator.<String, WhateverTheValueTypeIs>comparing(map::get).

How to operate on value from collecting an IntStream inside a lambda function

Let's say I have an IntStream and I would like to get e.g. a list of items from some collection things whose index is in that stream.
I thought it would be as simple as
IntStream.range(0, 10).collect(Collectors.toList(
_i -> things[_i]
))
but a problem of incompatible types (<lambda parameter> vs. int) appears.
<lambda parameter> can't be cast to Integer. This surprised me because a similar method toMap seems to work fine, if I add boxed() to the stream – then the type of the lambda parameter is java.lang.Object instead of <lambda parameter>:
IntStream.range(0, 10).boxed().collect(Collectors.toMap(
_i -> things[_i], _i -> 1
))
I am using Java 12.
How can I achieve similar behaviour in the toList method, or if what I am attempting is a bad idea altogether – why, and what would be a good alternative approach?
1) I would like to get e.g. a list of items from some collection things whose index is in that stream
For first question you have to use mapToObj intermediate operation, To get the elements from another list using index and collecting them into List
List<ObjectType> res = IntStream.range(0, 10)
.mapToObj(i-> things.get(i)) // or mapToObj(things::get)
.collect(Collectors.toList());
2) This surprised me because a similar method toMap seems to work fine, if I add boxed() to the stream
For second question boxed() will create a Stream of Integer (like Stream<Integer>), were Stream has collect to collect the result into Map by using Collectors.toMap. Here is the article with more information
Stream
<R,A> R collect(Collector<? super T,A,R> collector)
Collectors
public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper)
This surprised me because a similar method toMap seems to work fine
You can make it with the same way of a Map by using mapToObj like so
List<Thing> collect = IntStream.range(0, 10)
.mapToObj(_i -> things[_i])
.collect(Collectors.toList());

Using collectingAndThen() with Collectors.toMap() requires explicit type cast

I have a Map<String,BigDecimal> (say amountMap) which I want to convert to an ImmutableMap<String,Double>, code for which is:
return amountMap.entrySet().stream()
.collect(collectingAndThen(toMap(e->e.getKey(),e->e.getValue().doubleValue()),ImmutableMap::copyOf));
However Eclipse shows an error which says that e.getKey() and e.getValue() require explicit type casting since they are of type Object.
The same code works when I split it like so:
Map<String,Double> tempMap = amountMap.entrySet().stream()
.collect(toMap(e->e.getKey(),e->e.getValue().doubleValue());
return ImmutableMap.copyOf(tempMap);
I am assuming the former error is because of Type Erasure, but if not, is there a way to return the Map as an ImmutableMap without the intermediate step of creating a temporary map to hold the results ?
This was because I was using an old version of Eclipse(Luna) was fixed on upgrading

Convert CSV string value to Hashmap using Stream lambda

I am trying to get a HashMap<String,String> from a CSV String value using Java 8 Streams API. I am able to get the values etc, but how do I add the index of the List as the key in my HashMap.
(HashMap<String, String>) Arrays.asList(sContent.split(",")).stream()
.collect(Collectors.toMap(??????,i->i );
So my map will contain like Key ,Value as below.
0->Value1
1->Value2
2->Value3
...
Using Normal Java I can do it easily but I wanted to use the JAVA 8 stream API.
That’s a strange requirement. When you call Arrays.asList(sContent.split(",")), you already have a data structure which maps int numbers to their Strings. The result is a List<String>, something on which you can invoke .get(intNumber) to get the desired value as you can with a Map<Integer,String>…
However, if it really has to be a Map and you want to use the stream API, you may use
Map<Integer,String> map=new HashMap<>();
Pattern.compile(",").splitAsStream(sContent).forEachOrdered(s->map.put(map.size(), s));
To explain it, Pattern.compile(separator).splitAsStream(string) does the same as Arrays.stream(string.split(separator)) but doesn’t create an intermediate array, so it’s preferable. And you don’t need a separate counter as the map intrinsically maintains such a counter, its size.
The code above in the simplest code for creating such a map ad-hoc whereas a clean solution would avoid mutable state outside of the stream operation itself and return a new map on completion. But the clean solution is not always the most concise:
Map<Integer,String> map=Pattern.compile(",").splitAsStream(sContent)
.collect(HashMap::new, (m,s)->m.put(m.size(), s),
(m1,m2)->{ int off=m1.size(); m2.forEach((k,v)->m1.put(k+off, v)); }
);
While the first two arguments to collect define an operation similar to the previous solution, the biggest obstacle is the third argument, a function only used when requesting parallel processing though a single csv line is unlikely to ever benefit from parallel processing. But omitting it is not supported. If used, it will merge two maps which are the result of two parallel operations. Since both used their own counter, the indices of the second map have to be adapted by adding the size of the first map.
You can use below approach to get you the required output
private Map<Integer, String> getMapFromCSVString(String csvString) {
AtomicInteger integer = new AtomicInteger();
return Arrays.stream(csvString.split(","))
.collect(Collectors.toMap(splittedStr -> integer.getAndAdd(1), splittedStr -> splittedStr));
}
I have written below test to verify the output.
#Test
public void getCsvValuesIntoMap(){
String csvString ="shirish,vilas,Nikhil";
Map<Integer,String> expected = new HashMap<Integer,String>(){{
put(0,"shirish");
put(1,"vilas");
put(2,"Nikhil");
}};
Map<Integer,String> result = getMapFromCSVString(csvString);
System.out.println(result);
assertEquals(expected,result);
}
You can do it creating a range of indices like this:
String[] values = sContent.split(",");
Map<Integer, String> result = IntStream.range(0, values.length)
.boxed()
.collect(toMap(Function.identity(), i -> values[i]));

Categories

Resources