Streams use for map computation from list with counter - java

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.

Related

Create a map of maps with counts from list

Given a List<Integer> l and a factor int f, I would like to use a stream to create a Map<Integer, Map<Integer, Long>> m such that the parent map has keys that are the index within l divided by f, and the value is a map of values to counts.
If the list is {1,1,1,4} and the factor is f=2 I would like to get:
0 ->
{
1 -> 2
}
1 ->
{
1 -> 1
4 -> 1
}
Basically, I'm hoping for a stream version of:
Map<Integer, Map<Integer, Long>> m = new HashMap<>();
for (int i = 0; i < l.size(); i++) {
m.computeIfAbsent(i/f, k -> new HashMap<>())
.compute(l.get(i), (k, v) -> v==null?1:v+1);
}
I realize it is fairly similar to this question about collecting a map of maps and I understand how to do a much simpler groupingBy with a count:
Map<Integer, Long> m = l.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
But I do not understand how to put those two ideas together without iterating.
Because I am working with indexes as one of the keys, I imagine that rather than starting with l.stream() I will start with IntStream.range(0, l.size()).boxed() which lets me get the first key (i -> i/f) and the second key(i -> l.get(i)), but I still don't know how to properly collect the counts.
Here is a solution.
public static void main(String[] args) {
final List<Integer> l = List.of(1,1,1,4);
final int f = 2;
final var value = IntStream.range(0,l.size())
.boxed()
.collect(Collectors.groupingBy(i -> i/f, Collectors.groupingBy(l::get, Collectors.counting())));
System.out.println(value);
}
Not sure if this is a personal requirement, but sometime using standard loops over streams is not necessarily a bad thing.
You can wrap your grouping collector in CollectingAndThen collector which takes a downstream collector and a finisher function. In the finisher you can modify the values (sublists) to a map:
List<Integer> list = List.of(1, 1, 1, 4);
int fac = 2;
AtomicInteger ai = new AtomicInteger();
Map<Integer,Map<Integer,Long>> result =
list.stream()
.collect(Collectors.groupingBy(
i -> ai.getAndIncrement() / fac,
Collectors.collectingAndThen(
Collectors.toList(), val -> val.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.counting())))));
System.out.println(result);

Collect to map the order/position value of a sorted stream

I am sorting a populated set of MyObject (the object has a getName() getter) in a stream using a predefined myComparator.
Then once sorted, is there a way to collect into a map the name of the MyObject and the order/position of the object from the sort?
Here is what I think it should look like:
Set<MyObject> mySet; // Already populated mySet
Map<String, Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(Collectors.toMap(MyObject::getName, //HowToGetThePositionOfTheObjectInTheStream));
For example, if the set contain three objects (object1 with name name1, object2 with name name2, object3 with name name3) and during the stream they get sorted, how do I get a resulting map that looks like this:
name1, 1
name2, 2
name3, 3
Thanks.
A Java Stream doesn't expose any index or positioning of elements, so I know no way of replacing /*HowToGetThePositionOfTheObjectInTheStream*/ with streams magic to obtain the desired number.
Instead, one simple way is to collect to a List instead, which gives every element an index. It's zero-based, so when converting to a map, add 1.
List<String> inOrder = mySet.stream()
.sorted(myComparator)
.map(MyObject::getName)
.collect(Collectors.toList());
Map<String, Integer> nameMap = new HashMap<>();
for (int i = 0; i < inOrder.size(); i++) {
nameMap.put(inOrder.get(i), i + 1);
}
Try this one. you could use AtomicInteger for value of each entry of map. and also to guarantee order of map use LinkedHashMap.
AtomicInteger index = new AtomicInteger(1);
Map<String, Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(Collectors
.toMap(MyObject::getName, value -> index.getAndIncrement(),
(e1, e2) -> e1, LinkedHashMap::new));
The simplest solution would be a loop, as a formally correct stream solution that would also work in parallel requires a nontrivial (compared to the rest) merge functions:
Map<String,Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(HashMap::new, (m, s) -> m.put(s.getName(), m.size()),
(m1, m2) -> {
int offset = m1.size();
m2.forEach((k, v) -> m1.put(k, v + offset));
});
Compare with a loop/collection operations:
List<MyObject> ordered = new ArrayList<>(mySet);
ordered.sort(myComparator);
Map<String, Integer> result = new HashMap<>();
for(MyObject o: ordered) result.put(o.getName(), result.size());
Both solutions assume unique elements (as there can be only one position). It’s easy to change the loop to detect violations:
for(MyObject o: ordered)
if(result.putIfAbsent(o.getName(), result.size()) != null)
throw new IllegalStateException("duplicate " + o.getName());
Dont use a stream:
List<MyObject> list = new ArrayList<>(mySet);
list.sort(myComparator);
Map<String, Integer> nameMap = new HashMap<>();
for (int i = 0; i < list.size(); i++) {
nameMap.put(list.get(i).getName(), i);
}
Not only will this execute faster than a stream based approach, everyone knows what's going on.
Streams have their place, but pre-Java 8 code does too.

Count occurrences by stream

LinkedList<Double> list = new LinkedList<Double>();
list.add(9.5);
list.add(4.9);
list.add(3.2);
list.add(4.9);
I want to count the duplicate element in the list through a stream and put them into a HashMap which represent the occurrence of each number in the list:
e.g: (9.5=1, 4.9=2, 3.2=1)
Does anybody know how this works?
Using Collections.frequency
Make a list of all the distinct values, and for each of them, count their occurrences using the Collections.frequency method. Then collect into a Map
Map<Double, Integer> result = list.stream()
.distinct()
.collect(Collectors.toMap(
Function.identity(),
v -> Collections.frequency(list, v))
);
Using Collectors.groupingBy
I think it is not as nice as the example above.
Map<Double, Integer> result2 = list.stream()
.collect(Collectors.groupingBy(Function.identity())) // this makes {3.2=[3.2], 9.5=[9.5], 4.9=[4.9, 4.9]}
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().size())
);
Plain for loop
A plain for loop is quite short, you might not need streams and lambdas
Map<Double, Integer> map = new HashMap<>();
for(Double d : list)
map.put(d, map.containsKey(d) ? map.get(d)+1 : 1);
Using forEach
Even shorter with forEach
Map<Double, Integer> map = new HashMap<>();
list.forEach(d -> map.put(d, map.containsKey(d) ? map.get(d)+1 : 1));
Another way, using Collectors.counting which doesn't need the distinct.
Map<Double, Long> frequencies = list.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

Overwriting values in a HashMap that are in an ArrayList<String>

Let's say I have a HashMap with String keys and Integer values:
map = {cat=1, kid=3, girl=3, adult=2, human=5, dog=2, boy=2}
I want to switch the keys and values by putting this information into another HashMap. I know that a HashMap cannot have duplicate keys, so I tried to put the information into a HashMap with the Integer for the keys that would map to a String ArrayList so that I could potentially have one Integer mapping to multiple Strings:
swap = {1=[cat], 2=[adult, dog, boy], 3=[kid, girl], 5=[human]}
I tried the following code:
HashMap<Integer, ArrayList<String>> swap = new HashMap<Integer, ArrayList<String>>();
for (String x : map.keySet()) {
for (int i = 0; i <= 5; i++) {
ArrayList<String> list = new ArrayList<String>();
if (i == map.get(x)) {
list.add(x);
swap.put(i, list);
}
}
}
The only difference in my code is that I didn't hard code the number 5 into my index; I have a method that finds the highest integer value in the original HashMap and used that. I know it works correctly because I get the same output even if I hard code the 5 in there, I just didn't include it to save space.
My goal here is to be able to do this 'reversal' with any set of data, otherwise I could just hard code the value. The output I get from the above code is this:
swap = {1=[cat], 2=[boy], 3=[girl], 5=[human]}
As you can see, my problem is that the value ArrayList is only keeping the last String that was put into it, instead of collecting all of them. How can I make the ArrayList store each String, rather than just the last String?
With Java 8, you can do the following:
Map<String, Integer> map = new HashMap<>();
map.put("cat", 1);
map.put("kid", 3);
map.put("girl", 3);
map.put("adult", 2);
map.put("human", 5);
map.put("dog", 2);
map.put("boy", 2);
Map<Integer, List<String>> newMap = map.keySet()
.stream()
.collect(Collectors.groupingBy(map::get));
System.out.println(newMap);
The output will be:
{1=[cat], 2=[adult, dog, boy], 3=[kid, girl], 5=[human]}
you are recreating the arrayList for every iteration and i can't figure out a way to do it with that logic, here is a good way though and without the need to check for the max integer:
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
List<String> get = swap.get(value);
if (get == null) {
get = new ArrayList<>();
swap.put(value, get);
}
get.add(key);
}
Best way is to iterate over the key set of the original map.
Also you have to asure that the List is present for any key in the target map:
for (Map.Entry<String,Integer> inputEntry : map.entrySet())
swap.computeIfAbsent(inputEntry.getValue(),()->new ArrayList<>()).add(inputEntry.getKey());
This is obviously not the best solution, but approaches the problem the same way you did by interchanging inner and outer loops as shown below.
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("cat", 1);
map.put("kid", 3);
map.put("girl", 3);
map.put("adult", 2);
map.put("human", 5);
map.put("dog", 2);
map.put("boy", 2);
HashMap<Integer, ArrayList<String>> swap = new HashMap<Integer, ArrayList<String>>();
for (Integer value = 0; value <= 5; value++) {
ArrayList<String> list = new ArrayList<String>();
for (String key : map.keySet()) {
if (map.get(key) == value) {
list.add(key);
}
}
if (map.containsValue(value)) {
swap.put(value, list);
}
}
Output
{1=[cat], 2=[adult, dog, boy], 3=[kid, girl], 5=[human]}
Best way I can think of is using Map.forEach method on existing map and Map.computeIfAbsent method on new map:
Map<Integer, List<String>> swap = new HashMap<>();
map.forEach((k, v) -> swap.computeIfAbsent(v, k -> new ArrayList<>()).add(k));
As a side note, you can use the diamond operator <> to create your new map (there's no need to repeat the type of the key and value when invoking the map's constructor, as the compiler will infer them).
As a second side note, it's good practice to use interface types instead of concrete types, both for generic parameter types and for actual types. This is why I've used List and Map instead of ArrayList and HashMap, respectively.
Using groupingBy like in Jacob's answer but with Map.entrySet for better performance, as suggested by Boris:
// import static java.util.stream.Collectors.*
Map<Integer, List<String>> swap = map.entrySet()
.stream()
.collect(groupingBy(Entry::getValue, mapping(Entry::getKey, toList())));
This uses two more methods of Collectors: mapping and toList.
If it wasn't for these two helper functions, the solution could look like this:
Map<Integer, List<String>> swap = map.entrySet()
.stream()
.collect(
groupingBy(
Entry::getValue,
Collector.of(
ArrayList::new,
(list, e) -> {
list.add(e.getKey());
},
(left, right) -> { // only needed for parallel streams
left.addAll(right);
return left;
}
)
)
);
Or, using toMap instead of groupingBy:
Map<Integer, List<String>> swap = map.entrySet()
.stream()
.collect(
toMap(
Entry::getValue,
(e) -> new ArrayList<>(Arrays.asList(e.getKey())),
(left, right) -> {
left.addAll(right);
return left;
}
)
);
It seams you override the values instrad of adding them to the already creared arraylist. Try this:
HashMap<Integer, ArrayList<String>> swapedMap = new HashMap<Integer, ArrayList<String>>();
for (String key : map.keySet()) {
Integer swappedKey = map.get(key);
ArrayList<String> a = swapedMap.get(swappedKey);
if (a == null) {
a = new ArrayList<String>();
swapedMap.put(swappedKey, a)
}
a.add(key);
}
I didn't have time to run it (sorry, don't have Java compiler now), but should be almost ok :)
You could use the new merge method in java-8 from Map:
Map<Integer, List<String>> newMap = new HashMap<>();
map.forEach((key, value) -> {
List<String> values = new ArrayList<>();
values.add(key);
newMap.merge(value, values, (left, right) -> {
left.addAll(right);
return left;
});
});

java8 convert string array to map(odd index is key, even index is value)

Now I have a String array,
String[] a= {"from","a#a.com","to","b#b.com","subject","hello b"};
from command line arguments.
I want to convert it to Map,
{"from":"a#a.com","to":"b#b.com","subject":"hello b"}
Does exist convenient manner in java8 to achieve this?
Now my way is
Map<String,String> map = new HashMap<>();
for (int i = 0; i < args.length; i+=2) {
String key = args[i].replaceFirst("-+", ""); //-from --> from
map.put(key, args[i+1]);
}
You can use an IntStream to iterate on the indices of the array (which is required in order to process two elements of the array each time) and use the Collectors.toMap collector.
The IntStream will contain a corresponding index for each pair of elements of the input array. If the length of the array is odd, the last element will be ignored.
Map<String,String> map =
IntStream.range(0,a.length/2)
.boxed()
.collect(Collectors.toMap(i->a[2*i].replaceFirst("-+", ""),
i->a[2*i+1]));
You can do this quite elegant with Javaslang:
String[] a= {"from","a#a.com","to","b#b.com","subject","hello b"};
Map<String, String> map = Stream.of(a).grouped(2) // use javaslang.collection.Stream here
.map(group -> group.toJavaArray(String.class))
.toJavaStream() // this is the plain old java.util.Stream
.collect(toMap(tuple -> tuple[0], tuple -> tuple[1]));
The grouped function groups your stream in groups of 2 elements. These can be transformed to string arrays and those can be the base of a Map. Probably Javaslang allows you to do even more elegant.
In Java 8 you can use Stream.iterate to divide list to sublists of 2 elements
String[] a= {"from","a#a.com","to","b#b.com","subject","hello b"};
Map<String, String> map = Stream.iterate(
Arrays.asList(a), l -> l.subList(2, l.size()))
.limit(a.length / 2)
.collect(Collectors.toMap(
l -> l.get(0).replaceFirst("-+", ""),
l -> l.get(1))
);
Another recursive solution using simple Iterator
Map<String, String> map = buildMap(new HashMap<>(), Arrays.asList(a).iterator());
private static Map<String, String> buildMap(
Map<String, String> map, Iterator<String> iterator) {
if (iterator.hasNext()) {
map.put(iterator.next().replaceFirst("-+", ""), iterator.next());
createMap(map, iterator);
}
return map;
}
The trick is to first join the string, then split it up into key/value pairs, then the task is simple:
Map<String,String> map = Arrays.stream(
String.join(",", a)
.split(",(?!(([^,]*,){2})*[^,]*$)"))
.map(s -> s.split(","))
.collect(Collectors.toMap(s -> s[0], s -> s[1]))
;

Categories