Java 8 Group by and append to a set - java

I have function which returns a Map<String, Set<String>>, Code before java 8:
Map<String, Set<String>> degreeMap = new HashMap<>();
for(Course course : courses){
Set<String> cList = degreeMap.get(course.getCourseLevel().toString());
if(Objects.nonNull(cList)){
cList.addAll(course.getMasterDegree()); //this is what i want to append to the old set
degreeMap.put(course.getCourseLevel().toString(), cList);
} else{
degreeMap.put(course.getCourseLevel().toString(), new HashSet<>(course.getMasterDegree()));
}
}
return degreeMap;
Which return a map of courselevel -> set of degrees.
For example, it read all the courses and return a map like:
{"undergraduate" : ["BTech", "BSc", "BE"],
"masters": ["MTech", "MBA"],
"Executive": ["PGDBM", "EECP"]}
Here is my Course class:
public class Course {
private List<String> masterDegree;
private CourseLevel courseLevel;
}
But I want to write this piece of code in Java 8 style. For that, I tried this:
Map<String, Set<String>> degreeMap = courses.stream().collect(
Collectors.groupingBy(c -> c.getCourseLevel().toString(),
Collectors.mapping(c -> c.getMasterDegree(), Collectors.toSet()))
);
which is not working and I am getting the following compile-time error on this:
no instance(s) of type variable(s) exist so that List conforms to String inference variable T has incompatible bounds: equality constraints: String lower bounds: List
Any suggestion, how to achieve this?

Not tested, but looks like, you're looking for something like:
return courses.stream()
.collect(Collectors.toMap(course -> course.getCourseLevel().toString(),
course -> new HashSet<>(course.getMasterDegree()),
(set1, set2) -> Stream.of(set1, set2)
.flatMap(Set::stream).collect(Collectors.toSet())));

You might be interested in the Collectors.toMap() method.
Here is an example that you may need to tweak as I did not test it.
Map<String, Set<String>> degreeMap = courses.stream()
.collect(
Collectors.toMap(
item -> item.getCourseLevel().toString(), //your key mapping
item -> item.getMasterDegree(), //your value mapping
(oldValue, newValue) -> { //your process of aggregation
if (null == oldValue) {
return newValue;
} else {
oldValue.addAll(newValue);
return oldValue;
}
},
LinkedHashMap::new //your result initialiser
)
);
Also, another tip: you do not need to get by key and check for null, you can use the .compute(), .computeIfAbsent(), .computeIfPresent() methods on the map

I don't think you need lamda expression, you should refactor your codes to make it clear instead.
// supposed to be initialied with non-empty values
// Map<String, Set<String>> degreeMap = ...
for(Course course : courses){
// something like below
String key = course.getCourseLevel().toString();
// degreeSet should not be null
Set<String> degreeSet = course.getMasterDegree();
// TODO: check nullable
degreeSet.putAll(degreeMap.get(key));
degreeMap.put(key, degreeSet);
}
return degreeMap;

Related

How to use forEach after computeIfPresent

I have this snippet of code, and I want to use forEach after computeIfpresent function. Mainly, if the key is found, then we should loop over the values (list), and fetch each entry and add it to another list. Any idea how I can do that?
List<Long> myArrayList = new ArrayList();
Map<Long, Set<Long>> myMap = new HashMap();
Set<Long> mySet = MyMap().get(id);
if (mySet != null)
{
for (Long ex : mySet)
{
myArrayList.add(ex);
}
}
-->??
myMap.computeIfPresent(id, (key, value) -> value.forEach(ex -> myArrayList.add(ex)));
computeIfPresent is for changing the value inside the HashMap under the given key (id in your case). It means that such operation is not allowed because by running forEach on the key:value pair you are not providing any new mapping value for a map
What you could do would be
myMap.computeIfPresent("a", (k, v) -> {
((Set<Long>)v).forEach(e -> myArrayList.add(e));
return v;
});
but it looks bad and is violating the purpose of computeIfPresent method
You should just use rather traditional approach
if (myMap.containsKey(id)) {
myArrayList.addAll(myMap.get(id));
}
or use an Optional
Optional.ofNullable(map.get(id)).ifPresent(set ->
myArrayList.addAll((Set<Long>)set)
);
...but is this more readable? :)
If I get it right, you are looking for Map.getOrDefault instead of Map.computeIfPresent chained with a foreach. Using Map.getOrDefault the task could be rewritten to:
List<Long> myArrayList = new ArrayList<>();
Map<Long, Set<Long>> myMap = new HashMap<>();
long id = ...;
myArrayList.addAll(myMap.getOrDefault(id, Collections.emptySet()));

Iterate through a List of Map of Objects and get the length of an item using Java 8

I have a list of Map of Objects. I wanted to find the length of a particular item in a map using Java 8.
List<Map<Integer,String>> l = new ArrayList();
Map<Integer,String> m = new HashMap<>();
m.put(1,"Mark");
m.put(2,"Matthew");
l.add(m);
public int getLength(int param){
//This Method needs to return the length of Matthew/Mark when being passed 1/2
//l.stream()
when getLength(2) is called it should return 7
}
Even with some problems in your code, the answer could be as this:
public int getLength(String param, List<Map<Integer, String>> l) {
return l.stream()
.filter(e -> e.containsKey(param))
.findFirst()
.map(e -> e.get(param).length())
.orElseThrow(() -> new IllegalArgumentException("Element not found!"));
}
You have to find the Map which contain the key param, I guess you are looking to the first value then you can get the length or throw an exception if the element not found, you can replace orElseThrow with orElse if you want to return default value.

Java 8: How to get a value from a list contained as a map value?

I have the following situation:
I have a LinkedHashMap<> where the key type is a String and the values types varies: double, String, LinkedHashMap, etc.
I am trying to extract a value from a key of one of the LinkedHashMaps values which are a value of the main map.
For example, I'd like to get the result 1 from the following code (obviously it is a mess since it doesn't even compile):
Map<String, Object> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", new HashMap<String, String>());
HashMap<String, Object> input2 = (HashMap<String, Object>)(input.get("d"));
input2.put("d1", 1);
input2.put("d2", 2);
Optional<Integer> result = input.entrySet().stream()
.filter(e -> e.getKey().equals("d"))
.map(Map.Entry::getValue)
.filter(e -> e.getKey().equals("d1"))
.findFirst();
Where do I go wrong, and of course, what is the best way to get the result?
Thanks.
Once you use a Map with different value (and even key) types (and worse, nested maps). Then I suggest taking a step back and try to analyse what you've done. It seems that you're way better with a class than a Map. An example with your keys:
class YourClass {
String a;
String b;
String c;
YourOtherClass d;
}
class YourOtherClass {
Integer d1;
Integer d2;
}
I've omitted getters, setters and constructors for simplicity.
You can then create instances of those objects, like this:
YourOtherClass yoc = new YourOtherClass(1, 2);
YourClass yc = new YourClass("1234", "2345", "3456", yoc);
And then call the specific getter to receive a value with typesafety:
String a = yc.getA(); // works
Integer i = yc.getA(); // doesn't work
Or setting a new value via the setter:
yoc.setD1(4); // works
yoc.setD1("4"); // doesn't work
You're overcomplicating things imo. You could do it in a very straightforward manner. One liners are not always the ideal solutions.
I don't have the possibility to compile it, but it should be ok.
public Optional<Integer> getInnerValue(Map<String, Object> map, String outerKey, String innerKey) {
Object o = map.get(outerKey);
if (!(o instanceof Map)) {
return Optional.empty();
}
return Optional.ofNullable(((Map)o).get(innerKey));
}
Using a one-liner
public Optional<Integer> getInnerValue(Map<String, Object> map, String outerKey, String innerKey) {
return Optional.ofNullable(map.get(outerKey))
.filter(Map.class::isInstance)
.map(Map.class::cast)
.map(m -> m.get(innerKey))
.findFirst();
}

Refactor two for's into java 8 streams

I'm facing a small problem to rewrite my two for's into java 8 streams.
// This is a method parameter
Map<String, Collection<String>> preSelectedValues;
List<PersonModel> parameters = parameterSearchService.getParameterNames();
for(Iterator<Map.Entry<String, Collection<String>>> it = preSelectedValues.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Collection<String>> entry = it.next();
for(int i = 0; i < parameters.size(); i++) {
if (entry.getKey().startsWith(parameters.get(i).getName())) {
it.remove();
}
}
}
I've tried following streams to have the same behaviour as before:
Map<String, Collection<String>> filteredParameters = preSelectedValues.keySet().stream()
.filter(x -> isParameterValid(x, parameters))
.collect(Collectors.toMap(k -> k, v -> preSelectedValues.get(v)));
isParameterValid method:
private boolean isParameterValid(String parameterId, List<PersonModel> preSelectedValues) {
return preSelectedValues.stream()
.anyMatch(v -> !v.getName().startsWith(parameterId));
}
Basically what I'm trying to do is filter the "preSelectedValues" map which starts with "parameterId". But somehow when I'm using streams either it filters everything or nothing.
Your isParameterValid method doesn't have the same logic as the original loops, since:
You switched the instance and argument in the call to startsWith.
Calling anyMatch with a !v.getName().startsWith(parameterId) only tells you whether at least one element of the List<PersonModel> doesn't start with parameterId. Your original condition for keeping the entry in the Map was that all the elements of List<PersonModel> don't start with parameterId (or actually, the other way around - parameterId doesn't start with any of the names of elements of List<PersonModel>).
Therefore I negated the method to return the condition for removing an entry from the Map:
private boolean isParameterInvalid(String parameterId, List<PersonModel> preSelectedValues) {
return preSelectedValues.stream()
.anyMatch(v -> parameterId.startsWith(v.getName()));
}
And then the stream pipeline can look like this:
Map<String, Collection<String>> filteredParameters = preSelectedValues.entrySet().stream()
.filter(x -> !isParameterInvalid(x.getKey(), parameters))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
BTW, since your original loops mutate the original Map, you can achieve the same with removeIf.
If you are trying to modify the original map you can use removeIf:
preSelectedValues.keySet().removeIf(
key -> parameters.stream()
.map(PersonModel::getName)
.anyMatch(key::startsWith)
);

How to conditionally modify a Map in Java 8 stream API?

I am trying to modify a Map's keys based on conditional logic and struggling. I'm new to Java 8 streams API. Let's say I have a map like this:
Map<String, String> map = new HashMap<>();
map.put("PLACEHOLDER", "some_data1");
map.put("Google", "some_data2");
map.put("Facebook", "some_data3");
map.put("Microsoft", "some_data4");
When I would like to do is find the references of PLACEHOLDER and conditionally change that key to something else based on a boolean condition. I feel like it should be something like the below, but this doesn't even compile of course.
boolean condition = foo();
map = map.entrySet().stream().filter(entry -> "PLACEHOLDER".equals(entry.getKey()))
.map(key -> {
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}).collect(Collectors.toMap(e -> e.getKey(), Map.Entry::getValue));
I found this question which kind of makes me think maybe I can't do this with Java 8 stream APIs. Hopefully someone better at this than me knows how to do this. Ideone link if you want to play with it.
You've filtered out all elements that aren't PLACEHOLDER. You need to add that filter logic to your map operation:
final Map<String, String> output = input.entrySet().stream()
.map(e -> {
if (!e.getKey().equals("PLACEHOLDER")) {
return e;
}
if (condition) {
return new AbstractMap.SimpleImmutableEntry<>("Apple", e.getValue());
}
return new AbstractMap.SimpleImmutableEntry<>("Netflix", e.getValue());
}).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
But as you are guaranteed to only have a single instance of PLACEHOLDER in the Map, you can just do
String placeholderData = input.remove("PLACEHOLDER");
if (placeholderData != null) {
input.put(condition ? "Apple" : "Netflix", placeholderData);
}
If you really want to do it using Streams, you just need to move the conditional logic to the collection phase, like that:
boolean condition = true;
map.entrySet().stream().collect(Collectors.toMap(
entry -> mapKey(entry.getKey(), condition), Map.Entry::getValue
));
where:
private static String mapKey(String key, boolean condition) {
if (!"PLACEHOLDER".equals(key)) {
return key;
}
if (condition) {
return "Apple";
} else {
return "Netflix";
}
}
However, the second part of Boris the Spider's answer using Map.remove and Map.put seems the best way to go.

Categories