Existing condition into JAVA 8 stream - java

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.

Related

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.

Simplifying loop with Java 8

I have a method that adds maps to a cache and I was wondering what I could do more to simplify this loop with Java 8.
What I have done so far:
Standard looping we all know:
for(int i = 0; i < catalogNames.size(); i++){
List<GenericCatalog> list = DummyData.getCatalog(catalogNames.get(i));
Map<String, GenericCatalog> map = new LinkedHashMap<>();
for(GenericCatalog item : list){
map.put(item.name.get(), item);
}
catalogCache.put(catalogNames.get(i), map);};
Second iteration using forEach:
catalogNames.forEach(e -> {
Map<String, GenericCatalog> map = new LinkedHashMap<>();
DummyData.getCatalog(e).forEach(d -> {
map.put(d.name.get(), d);
});
catalogCache.put(e, map);});
And third iteration that removes unnecessary bracers:
catalogNames.forEach(objName -> {
Map<String, GenericCatalog> map = new LinkedHashMap<>();
DummyData.getCatalog(objName).forEach(obj -> map.put(obj.name.get(), obj));
catalogCache.put(objName, map);});
My question now is what can be further done to simplify this?
I do understand that it's not really necessary to do anything else with this method at this point, but, I was curios about the possibilities.
There is small issue with solution 2 and 3 they might cause a side effects
Side-effects in behavioral parameters to stream operations are, in
general, discouraged, as they can often lead to unwitting violations
of the statelessness requirement, as well as other thread-safety
hazards.
As an example of how to transform a stream pipeline that
inappropriately uses side-effects to one that does not, the following
code searches a stream of strings for those matching a given regular
expression, and puts the matches in a list.
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s)); // Unnecessary use of side-effects!
So instead of using forEach to populate the HashMap it is better to use Collectors.toMap(..). I am not 100% sure about your data structure, but I hope it is close enough.
There is a List and corresponding Map:
List<Integer> ints = Arrays.asList(1,2,3);
Map<Integer,List<Double>> catalog = new HashMap<>();
catalog.put(1,Arrays.asList(1.1,2.2,3.3,4.4));
catalog.put(2,Arrays.asList(1.1,2.2,3.3));
catalog.put(3,Arrays.asList(1.1,2.2));
now we would like to get a new Map where a map key is element from the original List and map value is an other Map itself. The nested Map's key is transformed element from catalog List and value is the List element itself. Crazy description and more crazy code below:
Map<Integer, Map<Integer, Double>> result = ints.stream().collect(
Collectors.toMap(
el -> el,
el -> catalog.get(el).stream().
collect(Collectors.toMap(
c -> c.intValue(),
c -> c
))
)
);
System.out.println(result);
// {1={1=1.1, 2=2.2, 3=3.3, 4=4.4}, 2={1=1.1, 2=2.2, 3=3.3}, 3={1=1.1, 2=2.2}}
I hope this helps.
How about utilizing Collectors from the stream API? Specifically, Collectors#toMap
Map<String, Map<String, GenericCatalog>> cache = catalogNames.stream().collect(Collectors.toMap(Function.identity(),
name -> DummyData.getCatalog(name).stream().collect(Collectors.toMap(t -> t.name.get(), Function.identity(),
//these two lines only needed if HashMap can't be used
(o, t) -> /* merge function */,
LinkedHashMap::new));
This avoids mutating an existing collection, and provides you your own individual copy of a map (which you can use to update a cache, or whatever you desire).
Also I would disagree with arbitrarily putting end braces at the end of a line of code - most style guides would also be against this as it somewhat disturbs the flow of the code to most readers.

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.

groupingBy and filter in one step

I have a Stream<String>, and I want a Map<Integer, String>. Let's call my classifier function getKey(String) - it can be expensive. Sometimes it returns zero, which means that the String should be discarded and not included in the resulting map.
So, I can use this code:
Stream<String> stringStream;
Map<Integer, String> result =
stringStream.collect(Collectors.groupingBy(this::getKey, Collectors.joining());
result.remove(0);
This first adds the unwanted Strings to the Map keyed by zero, and then removes them. There may be a lot of them. Is there an elegant way to avoid adding them to the map in the first place?
I don't want to add a filter step before grouping, because that would mean executing the decision/classification code twice.
You said that calling getKey is expensive, but you could still map the elements of the stream up-front before filtering them. The call to getKey will be only done once in this case.
Map<Integer, String> result =
stringStream.map(s -> new SimpleEntry<>(this.getKey(s), s))
.filter(e -> e.getKey() != 0)
.collect(groupingBy(Map.Entry::getKey, mapping(Map.Entry::getValue, joining())));
Note that there is no tuple classes in the standard API. You may roll your own one or use AbstractMap.SimpleEntry as a substitute.
Alternatively, if you think the first version creates a lot of entries, you can use the collect method where you provide yourself the supplier, accumulator and combiner.
Map<Integer, String> result = stringStream
.collect(HashMap::new,
(m, e) -> {
Integer key = this.getKey(e);
if(key != 0) {
m.merge(key, e, String::concat);
}
},
Map::putAll);
You may use a stream of pairs like this:
stringStream.map(x -> new Pair(getKey(x), x))
.filter(pair -> pair.left != 0) // or whatever predicate
.collect(Collectors.groupingBy(pair -> pair.left,
Collectors.mapping(pair -> pair.right, Collectors.joining())));
This code assumes simple Pair class with two fields left and right.
Some third-party libraries like my StreamEx provide additional methods to remove the boilerplate:
StreamEx.of(stringStream)
.mapToEntry(this::getKey, x -> x)
.filterKeys(key -> key != 0) // or whatever
.grouping(Collectors.joining());

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