How can I do the following using Java 8 Lambdas - java

I have a set of Strings as follows
Set<String> ids;
Each id is of the form #userId:#sessionId so for e.g. 1:2 where 1 is the userId and 2 is the sessionId.
I want to split these into userId which would be the key in a HashMap and each userId is unique. But each userId can have multiple sessions. So how do I get the values from Set<String> to Map<String, List<String>>
For e.g.
If the set contains the following values {1:2, 2:2, 1:3}
The map should contain
key=1 value=<2,3>
key=2 value=<2>

By "lambdas" I'm assuming you mean streams, because a straightforward loop to build a map wouldn't really require lambdas. If so, you can get close, but not quite there, with some of the built-in Collectors.
Map<String, List<String>> map = ids.stream()
.collect(Collectors.groupingBy(id -> id.split(":")[0]));
// result: {"1": ["1:2", "1:3"], "2": ["2:2"]}
This will group by the left number, but will store the full strings in the map values rather than just the right-hand portion.
Map<String, List<String>> map = ids.stream()
.collect(Collectors.toMap(
id -> id.split(":")[0],
id -> new ArrayList<>(Arrays.asList(id.split(":")[1])),
(l1, l2) -> {
List<String> l3 = new ArrayList<>(l1);
l3.addAll(l2);
return l3;
}
);
// result: {"1": ["2", "3"], "2": ["2"]}
This will return exactly what you want, but suffers from severe inefficiency. Rather than adding all equal elements to a single list, it will create many temporary lists and join them together. That turns what should be an O(n) operation into an O(n2) one.

You could use HashMap<Integer,Set<T>> or HashMap<Integer,List<T>>, where T is the type of value1, value2, etc..
I think you're asking for something similar to this question.
Also, here is a link for several solutions on how to proceed with this problem.

Related

Groupby counts in java

I am pretty new to java moving from c#. I have the following class.
class Resource {
String name;
String category;
String component;
String group;
}
I want to know the following numbers:
1. Count of resources in the category.
2. Distinct count of components in each category. (component names can be duplicate)
3. Count of resources grouped by category and group.
I was able to achieve a little bit of success using Collectors.groupingBy. However, the result is always like this.
Map<String, List<Resource>>
To get the counts I have to parse the keyset and compute the sizes.
Using c# linq, I can easily compute all the above metrics.
I am assuming there is definitely a better way to do this in java as well. Please advise.
For #1, I'd use Collectors.groupingBy along with Collectors.counting:
Map<String, Long> resourcesByCategoryCount = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.counting()));
This groups Resource elements by category, counting how many of them belong to each category.
For #2, I wouldn't use streams. Instead, I'd use the Map.computeIfAbsent operation (introduced in Java 8):
Map<String, Set<String>> distinctComponentsByCategory = new LinkedHashMap<>();
resources.forEach(r -> distinctComponentsByCategory.computeIfAbsent(
r.getCategory(),
k -> new HashSet<>())
.add(r.getGroup()));
This first creates a LinkedHashMap (which preserves insertion order). Then, Resource elements are iterated and put into this map in such a way that they are grouped by category and each group is added to a HashSet that is mapped to each category. As sets don't allow duplicates, there won't be duplicated groups for any category. Then, the distinct count of groups is the size of each set.
For #3, I'd again use Collectors.groupingBy along with Collectors.counting, but I'd use a composite key to group by:
Map<List<String>, Long> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
r -> Arrays.asList(r.getCategory(), r.getGroup()), // or List.of
Collectors.counting()));
This groups Resource elements by category and group, counting how many of them belong to each (category, group) pair. For the grouping key, a two-element List<String> is being used, with the category being its 1st element and the component being its 2nd element.
Or, instead of using a composite key, you could use nested grouping:
Map<String, Map<String, Long>> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.groupingBy(
Resource::getGroup,
Collectors.counting())));
Thanks Fedrico for detailed response. #1 and #3 worked great. For #2, i would like to see an output of Map. Here's the code that i am using currently to get that count. This is without using collectors in old style.
HashMap<String, HashSet<String>> map = new HashMap<>();
for (Resource resource : resources) {
if (map.containsKey(resource.getCategory())) {
map.get(resource.getCategory()).add(resource.getGroup());
} else
HashSet<String> componentSet = new HashSet<>();
componentSet.add(resource.getGroup());
map.put(resource.getCategory(), componentSet);
}
}
log.info("Group count in each category");
for (Map.Entry<String, HashSet<String>> entry : map.entrySet()) {
log.info("{} - {}", entry.getKey(), entry.getValue().size());
}

Stream group by multiple keys

I want to use streams in java to group long list of objects based on multiple fields. This will result in map of map of map of map of map of .... of map of lists.
How can I only extract lists from that complex stream?
Here is some example code for demonstration (list of strings, looking for groups with same length and first letter). I'm not interested in keys, just in resulting grouped entities.
List<String> strings = ImmutableList.of("A", "AA", "AAA", "B", "BB", "BBB", "C", "CC", "CCC", "ABA", "BAB", "CAC");
Map<Character, Map<Integer, List<String>>> collect = strings.stream().collect(
groupingBy(s -> s.charAt(0),
groupingBy(String::length)
)
);
This will produce following result
My Map =
{
A =
{
1 = [A]
2 = [AA]
3 = [AAA, ABA]
}
B =
{
1 = [B]
2 = [BB]
3 = [BBB, BAB]
}
C =
{
1 = [C]
2 = [CC]
3 = [CCC, CAC]
}
}
What I'm interested in is actually just lists from the above results and I want to do it ideally as part of groupby operation. I know it can be done for example by looping resulting maps structure. But is there a way to achieve it using streams?
[
[A],
[AA],
[AAA, ABA],
[B],
[BB],
[BBB, BAB],
[C],
[CC],
[CCC, CAC]
]
Instead of creating nested groups by using cascaded Collectors.groupingBy, you should group by a composite key:
Map<List<Object>, List<String>> map = strings.stream()
.collect(Collectors.groupingBy(s -> Arrays.asList(s.charAt(0), s.length())));
Then, simply grab the map values:
List<List<String>> result = new ArrayList<>(map.values());
If you are on Java 9+, you might want to change from Arrays.asList to List.of to create the composite keys.
This approach works very well for your case because you stated that you were not interested in keeping the keys, and because the List implementation returned by both Arrays.asList and List.of are well-defined in terms of their equals and hashCode methods, i.e. they can be safely used as keys in any Map.
I want to use streams in java to group long list of objects based on multiple fields.
This is trickier than your (invalid) example code leads me to think you expect. Nevertheless, you can flatten a stream via its appropriately-named flatMap() method. For a stream such as you describe, you might need to flatten multiple times or to define a custom mapping method or a complex lambda to flatten all the way down to what you're after.
In the case of a Map of the form presented in the question, you might do something like this:
List<List<String>> result = myMap.values().stream()
.flatMap(m -> m.values().stream()) // as many of these as needed
.collect(Collectors.toList());
If you want to get List<List<String>> as in your example you can use :
List<List<String>> list = collect.entrySet().stream()
.flatMap(e -> e.getValue().entrySet().stream())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
Also if you want to get single list of strings, you can add one more flatMap operation:
...
.flatMap(e -> e.getValue().entrySet().stream())
.flatMap(e -> e.getValue().stream())
...
As #John Bollinger mentioned, using stream of values, but not an entries will be more simpler.

Existing condition into JAVA 8 stream

How can i convert the below condition to Java 8 streams way ?
List<String> name = Arrays.asList("A", "B", "C");
String id;
if(name.contains("A")){
id = "123";
}else if(name.contains("B")){
id = "234";
}else if(name.contains("C")){
id = "345";
}
I am in process of learning Streams and was wondering how i can convert this one. I tried with foreach, map, filter but it was not getting at it
Yet another (but compact) solution:
Arrays.asList("B", "C", "A", "D").stream()
.map(s -> s.equals("A") ? new SimpleEntry<>(1, "123")
: s.equals("B") ? new SimpleEntry<>(2, "234")
: s.equals("C") ? new SimpleEntry<>(3, "345")
: null)
.filter(x -> x != null)
.reduce((a, b) -> a.getKey() < b.getKey() ? a : b)
.map(Entry::getValue)
.ifPresent(System.out::println);
I cannot see why do you have to convert it to stream. This doesn't seem to be stream API case for me.
But if you want to easily add new items and make code more readable, I can suggest you to use map instead.
private static final ImmutableMap<String, String> nameToId = new ImmutableMap.Builder<String, String>()
.put("A", "123")
.put("B", "234")
.put("C", "345")
.build();
Now you can add new items without changing much code and just call nameToId.get(name) to fetch id by name.
You can add more flexibility here using streams
Stream.of("A", "B", "C").map(nameToId::get)collect(Collectors.toList());
Inspired by Serghey Bishyr's answer to use a map I also used a map (but ordered) and I will rather go through the keys of the map instead of the list to find the appropriate id. That might of course not be the best solution, but you can play with Streams that way ;-)
Map<String, String> nameToId = new LinkedHashMap<>();
// the following order reflects the order of your conditions! (if your first condition would contain "B", you would move "B" at the first position)
nameToId.put("A", "123");
nameToId.put("B", "234");
nameToId.put("C", "345");
List<String> name = Arrays.asList("A", "B", "C");
String id = nameToId.keySet()
.stream()
.filter(name::contains)
.findFirst()
.map(nameToId::get)
.orElse(null)
You gain nothing really... don't try to put too much into the filtering predicates or mapping functions, because then your Stream solution might not be that readable anymore.
The problem you describe is to get a single value (id) from application of a function to two input sets: the input values and the mappings.
id = f(list,mappings)
So basically your question is, to find a f that is based on streams (in other words, solutions that return a list don't solve your problem).
First of all, the original if-else-if-else construct mixes three concerns:
input validation (only considering the value set "A","B","C")
mapping an input value to an output value ("A" -> "123", "B" -> "234", "C" -> "345")
defining an implicit prioritization of input values according to their natural order (not sure if that is intentional or conincidental), "A" before "B" before "C"
When you want to apply this to a stream of input value, you have to make all of them explicit:
a Filter function, that ignores all input value without a mapping
a Mapper function, that maps the input to the id
a Reduce function (BinaryOperator) the performs the prioritization logic implied by the if-else-if-else construct
Mapping Function
The mapper is a discrete function mapping the input values to a one-element-stream of outputput values:
Function<String,Optional<String>> idMapper = s -> {
if("A".equals(s)){
return Optional.of("123");
} else if("B".equals(s)){
return Optional.of("234");
} else if("C".equals(s)){
return Optional.of("345");
}
return Optional.empty();
} ;
For more mappings an immutable map should be used:
Map<String,String> mapping = Collections.unmodifiableMap(new HashMap<String,String>(){{
put("A", "123");
put("B", "234");
put("C", "345");
}}); //the instance initializer is just one way to initialize the map :)
Function<String,Optional<String>> idMapper = s -> Optional.ofNullable(mapping.get(s));
Filter Function
As we only allow input values for which we have a mapping, we could use the keyset of the mapping map:
Predicate<String> filter = s -> mapping.containsKey(s);
Reduce Function
For find the top-priority element of the stream using their natural order, use this BinaryOperator:
BinaryOperator<String> prioritizer = (a, b) -> a.compareTo(b) < 0 ? a : b;
If there is another logic to prioritize, you have to adapt the implementation accordingly.
This operator is used in a .reduce() call. If you prioritize based on natural order, you could use .min(Comparator.naturalOrder()) on the stream instead.
Because the natur
Stream Pipeline
Now you first have to reduce the stream to a single value, using the prioritizer, the result is an Optional which you flatMap by applying the idMapper function (flatMap to not end with Optional>
Optional<String> id = Arrays.asList("C", "B", "A")
.stream()
.filter(filter) //concern: input validation
.reduce(prioritizer) //concern: prioritization
.flatMap(idMapper); //concern: id-mapping
Final Result
To wrap it up, for your particular problem, the most concise version (without defining functions first) using a stream and input validation would be:
//define the mapping in an immutable map (that's just one way to do it)
final Map<String,String> mapping = Collections.unmodifiableMap(
new HashMap<String,String>(){{
put("A", "123");
put("B", "234");
put("C", "345");
}});
Optional<String> result = Arrays.asList("C", "D", "A", "B")
.stream()
.filter(mapping::containsKey)
.min(Comparator.naturalOrder())
.flatMap(s -> Optional.ofNullable(mapping.get(s)));
which is the sought-for f:
BiFunction<List<String>,Map<String,String>,Optional<String>> f =
(list,map) -> list.stream()
.filter(map::containsKey)
.min(Comparator.naturalOrder())
.flatMap(s -> Optional.ofNullable(mapping.get(s)));
There is certainly some appeal to this approach, but the elegance-through-simplicity of the if-else approach cannot be denied either ;)
But for the sake of completeness, let's look at complexity. Assuming the number of mappings and the number of input values is rather large (otherwise it wouldn't really matter).
Solutions based on iterating over the map and searching using contains (as in your if-else construct):
Best-Case: o(1) (first branch in the if-else construct, first item in list)
Worst-Case: O(n^2) (last branch in the if-else construct, last item in list)
For the streaming solution with reduce, you have to iterate completely through the input list (O(n)) while the map lookup is O(1)
Best-Case: o(n)
Worst-Case: O(n)
Thx to Hamlezz for the reduce idea and Holger for pointing out that applying the mapper function directly to the stream does not yield the same result (as first match wins and not the first entry in the if-else construct) and the min(Comparator.naturalOrder()) option.

Java Stream or Map Merge by key

What would be the simplest way to merge Map key values like keys "55", "55004", "550009", "550012" into one key: "55" and a sum of all those values().
I'm trying to think of ways to use containsKey or trimming the key. It's very hard to think about this.
Maybe a flatMap to flatten the map and reduce.
#Test
public void TestM(){
Map<String,Object> map1 = new HashMap();
map1.put("55", 3453.34);
map1.put("55001", 5322.44);
map1.put("55003", 10112.44);
map1.put("55004", 15555.74);
map1.put("77", 1000.74); // instead of 1000 it should be ~1500
map1.put("77004", 444.74);
map1.put("77003", 66.74);
// in real example I'll need "77" and "88" and "101" etc.
// All of which has little pieces like 77004, 77006
Map<String,Double> SumMap = new HashMap<String, Double>();
SumMap = map1.entrySet().stream().map
(e->e.getValue()).reduce(0d, Double::sum);
// INCORRECT
// REDUCE INTO ONE KEY startsWith 55
System.out.println("Map: " + SumMap);
// RESULT should be :
// Map<String, Double> result = { "55": TOTAL }
// real example might be "77": TOTAL, "88": TOTAL, "101": TOTAL
//(reducing away the "77004", "88005" etc.)
}
Basically this code reduces and rolls subitem totals into a bigger key.
It looks like you could use Collectors.groupingBy.
It requires Function which would allow us decide which elements belong to same group. Such function for elements from same group should always return same value which will be used as key in resulting map. In your case it looks like you want to group elements with same first two characters stored in key, which suggest mapping to substring(0,2).
When we already have way to determine which elements belong to same group, we can now specify how we want map to collect them. By default it collects them in list so we have key->[elemnt0, element1, ...] mapping.
But we can specify your own way of handling elements from same group by providing our own Collector. Since we want to create sum of values we can use Collectors.summingDouble(mappingToDouble).
DEMO:
Map<String, Double> map1 = new HashMap<>();
map1.put("661", 123d);
map1.put("662", 321d);
map1.put("55", 3453.34);
map1.put("55001", 5322.44);
map1.put("55003", 10112.44);
map1.put("55004", 15555.74);
Map<String, Double> map = map1.entrySet()
.stream()
.collect(
Collectors.groupingBy(
entry -> entry.getKey().substring(0, 2),
Collectors.summingDouble(Map.Entry::getValue)
)
);
System.out.println(map);
Output: {66=444.0, 55=34443.96}

How to partition a list by predicate using java8?

I have a list a which i want to split to few small lists.
say all the items that contains with "aaa", all that contains with "bbb" and some more predicates.
How can I do so using java8?
I saw this post but it only splits to 2 lists.
public void partition_list_java8() {
Predicate<String> startWithS = p -> p.toLowerCase().startsWith("s");
Map<Boolean, List<String>> decisionsByS = playerDecisions.stream()
.collect(Collectors.partitioningBy(startWithS));
logger.info(decisionsByS);
assertTrue(decisionsByS.get(Boolean.TRUE).size() == 3);
}
I saw this post, but it was very old, before java 8.
Like it was explained in #RealSkeptic comment Predicate can return only two results: true and false. This means you would be able to split your data only in two groups.
What you need is some kind of Function which will allow you to determine some common result for elements which should be grouped together. In your case such result could be first character in its lowercase (assuming that all strings are not empty - have at least one character).
Now with Collectors.groupingBy(function) you can group all elements in separate Lists and store them in Map where key will be common result used for grouping (like first character).
So your code can look like
Function<String, Character> firstChar = s -> Character.toLowerCase(s.charAt(0));
List<String> a = Arrays.asList("foo", "Abc", "bar", "baz", "aBc");
Map<Character, List<String>> collect = a.stream()
.collect(Collectors.groupingBy(firstChar));
System.out.println(collect);
Output:
{a=[Abc, aBc], b=[bar, baz], f=[foo]}
You can use Collectors.groupingBy to turn your stream of (grouping) -> (list of things in that grouping). If you don't care about the groupings themselves, then call values() on that map to get a Collection<List<String>> of your partitions.

Categories

Resources