How to partition a list by predicate using java8? - java

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.

Related

List<String> get count of all elements ending with one of strings from another list

Let's say I have one list with elements like:
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
And I have another large list of strings from which I would want to select all elements ending with any of the strings from the above list.
List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");
Ideally I would want a way to partition the second list so that it contains four groups, each group containing only those elements ending with one of the strings from first list. So in the above case the results would be 4 groups of 2 elements each.
I found this example but I am still missing the part where I can filter by all endings which are contained in a different list.
Map<Boolean, List<String>> grouped = fullList.stream().collect(Collectors.partitioningBy((String e) -> !e.endsWith("AAA")));
UPDATE: MC Emperor's Answer does work, but it crashes on lists containing millions of strings, so doesn't work that well in practice.
Update
This one is similar to the approach from the original answer, but now fullList is no longer traversed many times. Instead, it is traversed once, and for each element, the list of endings is searched for a match. This is mapped to an Entry(ending, fullListItem), and then grouped by the list item. While grouping, the value elements are unwrapped to a List.
Map<String, List<String>> obj = fullList.stream()
.map(item -> endings.stream()
.filter(item::endsWith)
.findAny()
.map(ending -> new AbstractMap.SimpleEntry<>(ending, item))
.orElse(null))
.filter(Objects::nonNull)
.collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, toList())));
Original answer
You could use this:
Map<String, List<String>> obj = endings.stream()
.map(ending -> new AbstractMap.SimpleEntry<>(ending, fullList.stream()
.filter(str -> str.endsWith(ending))
.collect(Collectors.toList())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
It takes all endings and traverses the fullList for elements ending with the value.
Note that with this approach, for each element it traverses the full list. This is rather inefficient, and I think you are better off using another way to map the elements. For instance, if you know something about the structure of the elements in fullList, then you can group it immediately.
To partition a stream, means putting each element into one of two groups. Since you have more suffixes, you want grouping instead, i.e. use groupingBy instead of partitioningBy.
If you want to support an arbitrary endings list, you might prefer something better than a linear search.
One approach is using a sorted collection, using a suffix-based comparator.
The comparator can be implemented like
Comparator<String> backwards = (s1, s2) -> {
for(int p1 = s1.length(), p2 = s2.length(); p1 > 0 && p2 > 0;) {
int c = Integer.compare(s1.charAt(--p1), s2.charAt(--p2));
if(c != 0) return c;
}
return Integer.compare(s1.length(), s2.length());
};
The logic is similar to the natural order of string, with the only difference that it runs from the end to the beginning. In other words, it’s equivalent to Comparator.comparing(s -> new StringBuilder(s).reverse().toString()), but more efficient.
Then, given an input like
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
List<String> fullList= Arrays.asList("111.AAA", "222.AAA",
"111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD");
you can perform the task as
// prepare collection with faster lookup
TreeSet<String> suffixes = new TreeSet<>(backwards);
suffixes.addAll(endings);
// use it for grouping
Map<String, List<String>> map = fullList.stream()
.collect(Collectors.groupingBy(suffixes::floor));
But if you are only interested in the count of each group, you should count right while grouping, avoiding to store lists of elements:
Map<String, Long> map = fullList.stream()
.collect(Collectors.groupingBy(suffixes::floor, Collectors.counting()));
If the list can contain strings which match no suffix of the list, you have to replace suffixes::floor with s -> { String g = suffixes.floor(s); return g!=null && s.endsWith(g)? g: "_None"; } or a similar function.
Use groupingBy.
Map<String, List<String>> grouped = fullList
.stream()
.collect(Collectors.groupingBy(s -> s.split("\\.")[1]));
s.split("\\.")[1] will take the yyy part of xxx.yyy.
EDIT : if you want to empty the values for which the ending is not in the list, you can filter them out:
grouped.keySet().forEach(key->{
if(!endings.contains(key)){
grouped.put(key, Collections.emptyList());
}
});
If your fullList have some elements which have suffixes that are not present in your endings you could try something like:
List<String> endings= Arrays.asList("AAA", "BBB", "CCC", "DDD");
List<String> fullList= Arrays.asList("111.AAA", "222.AAA", "111.BBB", "222.BBB", "111.CCC", "222.CCC", "111.DDD", "222.DDD", "111.EEE");
Function<String,String> suffix = s -> endings.stream()
.filter(e -> s.endsWith(e))
.findFirst().orElse("UnknownSuffix");
Map<String,List<String>> grouped = fullList.stream()
.collect(Collectors.groupingBy(suffix));
System.out.println(grouped);
If you create a helper method getSuffix() that accepts a String and returns its suffix (for example getSuffix("111.AAA") will return "AAA"), you can filter the Strings having suffix contained in the other list and then group them:
Map<String,List<String>> grouped =
fullList.stream()
.filter(s -> endings.contains(getSuffix(s)))
.collect(Collectors.groupingBy(s -> getSuffix(s)));
For example, if the suffix always begins at index 4, you can have:
public static String getSuffix(String s) {
return s.substring(4);
}
and the above Stream pipeline will return the Map:
{AAA=[111.AAA, 222.AAA], CCC=[111.CCC, 222.CCC], BBB=[111.BBB, 222.BBB], DDD=[111.DDD, 222.DDD]}
P.S. note that the filter step would be more efficient if you change the endings List to a HashSet.
One can use groupingBy of substrings with filter to ensure that the final Map has just the Collection of relevant values. This could be sone as :
Map<String, List<String>> grouped = fullList.stream()
.collect(Collectors.groupingBy(a -> getSuffix(a)))
.entrySet().stream()
.filter(e -> endings.contains(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static String getSuffix(String a) {
return a.split(".")[1];
}
You can use groupingBy with filter on endings list as,
fullList.stream()
.collect(groupingBy(str -> endings.stream().filter(ele -> str.endsWith(ele)).findFirst().get()))

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 Lambda stream into different collections

I have a Java lambda stream that parses a file and stores the results into a collection, based on some basic filtering.
I'm just learning lambdas so bear with me here if this is ridiculously bad. But please feel free to point out my mistakes.
For a given file:
#ignored
this
is
#ignored
working
fine
The code:
List<String> matches;
Stream<String> g = Files.lines(Paths.get(givenFile));
matches = g.filter(line -> !line.startsWith("#"))
.collect(Collectors.toList());
["this", "is", "working", "fine"]
Now, how would I go about collecting the ignored lines into a second list within this same stream? Something like:
List<String> matches;
List<String> ignored; // to store lines that start with #
Stream<String> g = Files.lines(Paths.get(exclusionFile.toURI()));
matches = g.filter(line -> !line.startsWith("#"))
// how can I add a condition to throw these
// non-matching lines into the ignored collection?
.collect(Collectors.toList());
I realize it would be pretty trivial to open a new stream, alter the logic a bit, and .collect() the ignored lines easily enough. But I don't want to have to loop through this file twice if I can do it all in one stream.
Instead of two streams you can use partitioningBy in Collector
List<String> strings = Arrays.asList("#ignored", "this", "is", "#ignored", "working", "fine");
Map<Boolean, List<String>> map = strings.stream().collect(Collectors.partitioningBy(s -> s.startsWith("#")));
System.out.println(map);
output
{false=[this, is, working, fine], true=[#ignored, #ignored]}
here I used key as Boolean but you can change it to a meaningful string or enum
EDIT
If the strings can starts with some other special characters you could use groupingBy
List<String> strings = Arrays.asList("#ignored", "this", "is", "#ignored", "working", "fine", "!Someother", "*star");
Function<String, String> classifier = s -> {
if (s.matches("^[!##$%^&*]{1}.*")) {
return Character.toString(s.charAt(0));
} else {
return "others";
}
};
Map<String, List<String>> maps = strings.stream().collect(Collectors.groupingBy(classifier));
System.out.println(maps);
Output
{!=[!Someother], #=[#ignored, #ignored], *=[*star], others=[this, is, working, fine]}
also you can nest groupingBy and partitioningBy
I think the closest you could come to a generic approach for this would be something like peek:
g.peek(line -> if (line.startsWith("#")) {
ignored.add(line);
})
.filter(line -> !line.startsWith("#"))
// how can I add a condition to throw these
// non-matching lines into the ignored collection?
.collect(Collectors.toList());
I mention it because unlike with the partitioning Collector you could, at least in theory, change together however many peeks you want--but, as you can see, you have to duplicate logic, so it's not ideal.

How can I do the following using Java 8 Lambdas

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.

Categories

Resources