Java 11 here. I have the following POJO:
#Data // Lombok; adds getters, setters, all-args constructor and equals and hashCode
public class Fliflam {
private String merf;
private String tarf;
private Boolean isFlerf;
}
I have a method that validates a Flimflam and returns a List<String> of any errors encountered while validating the Flimflam. I can change this to return Optional<List<String>> if anyone thinks thats helpful for some reason, especially when dealing with the Stream API:
public List<String> validateFlimflam(Flimflam flimflam) {
List<String> errors = new ArrayList<>();
// ... validation code omitted for brevity
// 'errors' list is populated with any errors; otherwise it returns empty
return errors;
}
I want to stream (Stream API) through a List<Flimflam> and populate a Map<Flimflam,List<String>> errors map, where the key of the map is a Flimflam that failed validation, and its corresponding value is the list of validation error strings.
I can achieve this the "old fashioned" way like so:
List<Flimflam> flimflams = getSomehow();
Map<Flimflam,List<String>> errorsMap = new HashMap<>();
for (Flimflam ff : flimflams) {
List<String> errors = validateFlimflam(ff);
if (!errors.isEmpty() {
errorsMap.put(ff, errors);
}
}
How can I accomplish this via the Stream API?
Like this
Map<Flimflam,List<String>> errorsMap = flimflams.stream().collect(Collectors.toMap(f -> f, f-> f::validateFlimflam));
toMap takes 2 parameters (keyMapper,valueMapper)
In your case key mapper is object from stream itself, and value is calling validateFlimflam on that object
It is hard to tell where exactly your validateFlimflam method is defined. I suspect it is not in the Flimflam class itself since there would be no need to pass an instance of itself to the method. So I presume it is an external method to that class. Assuming that I would proceed as follows:
thisClass = instance containing validateFlimflam. Could be set to this
Map<Flimflam, List<String>> errorsMap =
flimflams.stream().collect(Collectors.toMap(f -> f,
thisClass::validateFlimflam));
If by chance, Flimflam does contain validateFlimflam you could do it like this. Note that this presumes the method takes no arguments as they wouldn't be necessary
Map<Flimflam, List<String>> errorsMap =
flimflams.stream().collect(Collectors.toMap(f -> f,
Flimflam::validateFlimflam));
Finally, if the containing class is some other class and the validateFlimflam method is declared static, then you could do it like this by using the containing class name, not instance. Also, in this case, the method would take an argument as defined.
Map<Flimflam, List<String>> errorsMap =
flimflams.stream().collect(Collectors.toMap(f -> f,
SomeClass::validateFlimflam));
Related
I have code :
#GetMapping("/goal/{id}")
public String goalInfo(#PathVariable(value = "id") long id, Model model) {
if (!goalRepository.existsById(id)) {
return "redirect:/goal";
}
Iterable<SubGoal> subGoal = subGoalRepository.findAll();
ArrayList<SubGoal> subGoals = new ArrayList<>();
//How refactor this?
for(SubGoal sub : subGoal){
if(sub.getParentGoal().getId().equals(id)){
subGoals.add(sub);
}
}
if(subGoals.size() > 0) {
goalPercent(id, subGoal);
}
Optional<Goal> goal = goalRepository.findById(id);
ArrayList<Goal> result = new ArrayList<>();
goal.ifPresent(result::add);
model.addAttribute("goal", result);
model.addAttribute("subGoal",subGoals);
return "goal/goal-info";
}
Here I get sub-goals from the repository and filter these values.
How I can do it without foreach? I want to use Streams or something else.
You don't need to declare an iterable on your code to filter your ArrayList. The filter method already provides one for you. You can use:
subGoals = subGoals.stream().filter(subGoal ->
/*Here goes your filter condition*/ ).collect(Collectors.toList());
To convert Iterable to Stream use StreamSupport.stream(iter.spliterator(), par).
Iterable<SubGoal> subGoal = subGoalRepository.findAll();
List<SubGoal> subGoals = StreamSupport
.stream(subGoal.spliterator(), false)
.filter(sub -> sub.getParentGoal().getId().equals(id))
.collect(toList()) // static import `Collectors.toList()`
...
Additionally, this part can be also single statement.
before (three statement)
Optional<Goal> goal = goalRepository.findById(id);
ArrayList<Goal> result = new ArrayList<>();
goal.ifPresent(result::add);
after (single statement)
List<Goal> result = goalRepository.findById(id)
.map(goal -> singletonList(goal)) // Collections.singletonList()
.orElse(emptyList()); // Collections.emptyList()
Updates
1. singletonList(), emptyList()
These are just factory methods used when creating single entity list and empty list.
you can change this part any kind of function that has Goal as input and List as output and any empty list.
For example,
.map(goal -> Arrays.asList(goal))
.orElse(new ArrayList<>());
or
.map(goal -> {
ArrayList<Goal> l = new ArrayList<>();
l.add(goal);
return l;
})
...
2. I changed the List Type to List<Goal>, not ArrayList<Goal>
Sorry, I missed explanation about that.
In OOP, using Interface will be better practices than using Concrete Class in many situation.
If you have to use ArrayList<> Type explicitly or want to specify actual list instance in some reason, you can also use toCollection() like below.
.collect(toCollection(ArrayList::new)) // you can specify the actual list instance
Thanks to #John Bollinger #hfontanez for pointing this out.
This is client-side filtering and is extremely inefficient. Instead, simply declare this method on your repository interface:
Collection<SubGoal> findByParentId(Long id); // or Stream, Iterable
The model:
public class MyModel{
private int id;
private String name;
....
....
//getters and setters
}
I have a list of MyModel object:
//First Object to be added to list
MyModel myModelObject1 = new MyModel();
myModelObject1.setId(1);
myModelObject1.setName("abc");
//Second Object to be added to list
MyModel myModelObject2 = new MyModel();
myModelObject1.setId(2);
myModelObject1.setName("pqr");
List<MyModel> myModelList = new ArrayList<MyModel>();
myModelList.add(myModelObject1);
myModelList.add(myModelObject2);
I want to get a list of names present in the MyModel List i.e. I want to create a list of names (List<String> in this case) from myModelList. So, I want my list to have:
{"abc", "pqr"}
There is always a way to iterate and create another list but is there any better way to do that? (Not necessarily to be efficient but if it can be done in a line using streams, foreach e.t.c.).
EDIT:
The answers worked for me but I have some modifications in my use case: If I want to add a condition that only name which contains character 'a' should be added to the list and I want to add a logger message to debug for each element then how should I approach this?
I tried doing the following using a method (charAPresent()) which checks that if the passed String contains character 'a' but it didn't work:
List<String> modelNameList = myModelList.stream()
.map(model -> {
if (charAPresent(model.getName)) {
model.getName()
}
})
.collect(Collectors.toList());
Let me know if I am missing something.
Using Java 8 Stream:
List<String> modelNameList = myModelList.stream()
.map(Model::getName)
.collect(Collectors.toList());
Model::getName is called as method reference. It equivalents to model -> model.getName()
You can use streams and map your object to its name and collect to a list as:
List<String> names = myModelList.stream()
.map(MyModel::getName)
.collect(Collectors.toList());
There is always a way to iterate and create another list but is there
any better way to do that
Even with the use of streams, you would have to iterate the complete collection. There is no better way of doing it than iterating the complete collection.
You can use Stream.map() to get only the names of your model. Use Stream.filter() to get only the names matching your charAPresent() method. To log the entries before collecting you can use Stream.peek():
List<String> modelNameList = myModelList.stream()
.map(Model::getName) // map only the name
.filter(YourClass::charAPresent) // filter the items
.peek(System.out::println) // print all items which are in the filter
.collect(Collectors.toList());
You can also use foreach like this:
public static List<String> modelNames(List<MyModel> myModelList)
List<String> list = new ArrayList<String>();
for(MyModel mm : myModelList) {
if(mm.getName().contains("a") {
list.add(mm.getName());
}
}
return list;
}
I just started messing around with Java streams and I wrote something like this:
List<Device> devicesToDelete = new ArrayList<>();
List<Device> oldDeviceList = getCurrentDevices();
for (Device deviceFromOldList : oldDeviceList)
{
// part to simplify
boolean deviceNotExistOnDeleteList =
devicesToDelete.stream().noneMatch(nd -> nd.id == deviceFromOldList.id);
if (deviceNotExistOnDeleteList) {
devicesToDelete.add(deviceFromOldList);
}
// part to simplify end
}
Can it be simplified even more?
I'm not using Set because my Device class .equals() implementation compares all fields in that class. And here I need to compare only id field.
Just use a Map
Map<Object, Device> devicesToDelete = new HashMap<>();
List<Device> oldDeviceList = getCurrentDevices();
for(Device deviceFromOldList: oldDeviceList) {
devicesToDelete.putIfAbsent(deviceFromOldList.id, deviceFromOldList);
}
// in case you need a Collection:
Collection<Device> allDevicesToDelete = devicesToDelete.values();
putIfAbsent will only store the mapping if the key is not already present. This will get you the performance of hashing while only considering the ID.
You may change the type argument Object in Map<Object,Device> to whatever type your ID has, though it doesn’t matter for the operation, if all you need at the end, is the Collection<Device>.
You can use a Stream, e.g.
Map<Object, Device> devicesToDelete = getCurrentDevices().stream()
.collect(Collectors.toMap(
deviceFromOldList -> deviceFromOldList.id, Function.identity(), (a,b) -> a));
though, it’s debatable whether this is a necessary change. The loop is not bad.
i want to cast List<HashMap<String, Object>> into Set<StudentInfo>
i have method
public List<HashMap<String,Object>> getStudentData(studentId);
i want to convert the result into Set so i used
Set<StudentInfo> studentFilteredInfo = new HashSet<>();
List<Map<String, Object>> studentCompleteRecord = getStudentData(1005);
studentFilteredInfo.addAll((Collection<? extends StudentInfo>studentCompleteRecord ));
initially when i executed on localhost it with java 8, eclipse and tomcat 8 it is working fine.
when i tried to build it with maven
mvn clean package
it will through an Error:
incompatible types: java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
cannot be converted to java.util.Collection<? extends com.school.model.StudentInfo>
You are mistaken: there is no casting from List<Map<String, Object>> into some Set<Whatever>!
Casting basically means: you know that some "Object" has a more specific type; thus you tell the compiler: "you can safely assume that this thingy here is something else in reality".
But that means: in reality (at runtime), that "thingy" really is "something else"! And alone the generic types that you provide in your question make it very clear: you can't be doing a cast here!
In other words: you have to write code that iterates your List of Maps; to extract that information that is required to create new StudentInfo objects. Then you collect those newly created objects; and put them into a new Set; which you then can return from your method!
And finally: always avoid "concrete" implementation types; you used List<HashMap<... - instead, you should go for List<Map<... !
You need to write code to explicitly convert a Map<String,Object> to a StudentInfo instance. Suppose StudentInfo has a method like this:
static StudentInfo create(Map<String, Object> info) {
String name = info.get("name");
Transcript transcript = info.get("grades");
return new StudentInfo(name, transcript);
}
Then you would need to iterate over each element in the list and use your method to convert the Map instances to StudentInfo objects.
With lambdas:
Set<StudentInfo> studentFilteredInfo = studentCompleteRecord.stream()
.map(StudentInfo::create)
.collect(Collectors.toSet());
Without lambdas:
Set<StudentInfo> studentFilteredInfo = new HashSet<>();
for (Map<String,Object> e : studentCompleteRecord)
studentFilteredInfo.add(StudentInfo.create(e);
I have a stream of objects (a List) and want to create new objects from that stream, to be inserted into a Set. However, two or more objects in the incoming List may hash to the same Key in the Set, in which case I want to append a String from the nth List object to the one already in the Set instead of creating a new one.
Something like this, but in functional form:
HashSet<ClassB> mySet = new HashSet<>();
for (ClassA instanceA : classAList) {
if (mySet.contains(ClassB.key(instanceA))) { //static method call to find the key
mySet.get(instanceA).appendFieldA(instanceA.getFieldA());
} else {
mySet.add(new ClassB(instanceA));
}
}
return mySet;
In functional form I though of creating something like this:
List classAList = new ArrayList<>();
classAList.stream()
.map(instanceA -> new ClassB(instanceA))
.collect(Collectors.toSet());
But then of course that ignores the hashmap and I don't get to combine fields my multiple instances of ClassA that would all resolve to the same ClassB. I'm not sure how to put that in there. Do I need ignore the map() call and create a custom collector instead to do this job? There seems to be more than one way to do this, but I'm new to Streams.
It’s hard to understand what you actually want as your code example does not work at all. The problem is that a Set does not work like a Map, you can’t ask it for the contained equivalent object. Besides that, you are using different objects for your contains(…) and get(…) call. Also, it’s not clear what the difference between ClassB.key(instanceA) and new ClassB(instanceA) is.
Let’s try to redefine it:
Suppose we have a key type Key and a method Key.key(instanceA) to define the group candidates. Then we have a ClassB which is the resulting type, created via new ClassB(instanceA) for a single (or primary ClassA instance), having an .appendFieldA(…) method to receive a value of another ClassA instance when merging two group members. Then, the original (pre Java 8) code will look as follows:
HashMap<Key, ClassB> myMap = new HashMap<>();
for(ClassA instanceA: classAList) {
Key key=Key.key(instanceA);
if(myMap.containsKey(key)) {
myMap.get(key).appendFieldA(instanceA.getFieldA());
} else {
myMap.put(key, new ClassB(instanceA));
}
}
Then, myMap.values() provides you a collection of the ClassB instances. If it has to be a Set, you may create it via
Set<ClassB> result=new HashSet<>(myMap.values());
Note that this also works, when Key and ClassB are identical as it seems to be in your code, but you may ask youself, whether you really need both, the instance created via .key(instanceA) and the one created via new ClassB(instanceA)…
This can be simplified via the Java 8 API as:
for(ClassA instanceA: classAList) {
myMap.compute(Key.key(instanceA), (k,b)-> {
if(b==null) b=new ClassB(instanceA);
else b.appendFieldA(instanceA.getFieldA());
return b;
});
}
or, if you want it look even more function-stylish:
classAList.forEach(instanceA ->
myMap.compute(Key.key(instanceA), (k,b)-> {
if(b==null) b=new ClassB(instanceA);
else b.appendFieldA(instanceA.getFieldA());
return b;
})
);
For a stream solution, there is the problem, that a merge function will get two instances of the same type, here ClassB, and can’t access the ClassA instance via the surrounding context like we did with the compute solution above. For a stream solution, we need a method in ClassB which returns that first ClassA instance, which we passed to its constructor, say getFirstInstanceA(). Then we can use:
Map<Key, ClassB> myMap = classAList.stream()
.collect(Collectors.toMap(Key::key, ClassB::new, (b1,b2)->{
b1.appendFieldA(b2.getFirstInstanceA().getFieldA());
return b1;
}));
You can group the entries into a map that maps the hashed key to the list of elements and then call map again to convert that map into the set you are after. Something like this:
List classAList = new ArrayList<>();
classAList.stream()
.collect(Collectors.groupingBy(instanceA -> ClassB.key(instanceB)))
.entrySet()
.map(entry -> entry.getValue().stream()
.map(instanceA -> new ClassB(instanceA))
.reduce(null, (a,b) -> a.appendFieldA(b)))
.collect(Collectors.toSet());