I am trying to understand Lambdas in Java 8.
Say I have a Person class that looks like this:
public class Person implements {
String name;
GenderEnum gender;
int age;
List<Person> children;
}
Now what I want to do is find all persons which are female, that have children that are younger than 10 years old.
Pre java 8 I would do it like this:
List<Person> allPersons = somePeople();
List<Person> allFemaleWithChildren = new ArrayList<>();
for(Person p : allPersons) {
for(Person child : p.getChildren()) {
if(child.getAge() < 10 && p.getGender() == GenderEnum.Female) {
allFemaleWithChildren.add(p);
}
}
}
Now allFemaleWithChildren should have what I want.
I have been trying to do the same using streams
I think I need to use some sort of map, filter and reduce
allPersons.stream()
//filter females
.filter(p -> p.getGender == GenderEnum.Female)
//get the children
.map(c -> c.getChildren())
//filter the ones that are less than 10 years
.filter(c -> c.getAge() < 10)
//return a list with the result
.collect(Collectors.toList())
But this code does not compile.
What am I missing.
Also, I don't understand what the reduce method can be used for.
The compiler says
cannot resolve method getAge(). This is because c is apparently a collection and not the items in the collection, which is really what I want.
At the moment (once you fix the compilation error) you would be returning a list of Children. Assuming that in your original code you meant to break as soon as you find a children under 10, the equivalent could look like:
allPersons.stream()
//filter females
.filter(p -> p.getGender() == GenderEnum.Female)
//only keep females with at least one child < 10
.filter(f -> f.getChildren().stream()
.anyMatch(c -> c.getAge() < 10))
//return a list with the result
.collect(Collectors.toList())
And indeed as commented below, you could use a few static imports, add helper methods and refactor the original code to make it more readable:
allPersons.stream()
.filter(this::female)
.filter(this::hasChildrenUnder10)
.collect(toList())
//...
private boolean female(Person p) { return p.getGender() == Female; }
private boolean hasChildrenUnder10(Person parent) {
return parent.getChildren().stream()
.anyMatch(c -> c.getAge() < 10));
}
You have 2 for loops, that means at some point you need another stream. Here when you call map, you map your mothers to lists of children. You then carry on as if you had a stream of children, but you have a stream of collections of children actually.
Related
I am trying to filter a list of POJOs with two different predicates using a stream.
public class Toy {
public boolean isRed();
public boolean isBlue();
public boolean isGreen();
}
public class RedBlueExtravaganza {
public RedBlueExtravaganza(RedToy rt, BlueToy bt) {
//construct
}
}
// Wrappers around Toy with more verbose names
public class RedToy extends Toy { }
public class BlueToy extends Toy { }
public class GreenToy extends Toy { }
Basically, I want the first red and the first blue toy in the list of Toy objects.
List<Toy> toyList = Arrays.asList(greenToy1, redToy1, greenToy2, redToy2, blueToy1);
I want to write a stream that does the following:
RedBlueExtravaganza firstRedBlueList = toyList.stream()
// get first red but keep rest of list
// get first blue but keep rest of list
// discard rest of list
// map to a Tuple (or something) to distinguish the one red and one blue toy
// map to RedBlueExtravaganza
.findFirst()
.get();
log.info(firstRedBlueList); // now contains redToy1, blueToy1
Thanks for the help!
Here's a solution which traverses your list only once, giving you the first red and blue toys at the end. We can filter out all the other irrelevent colors and then create a map whose key is whether the toy is red and the value is the first toy matching the given criteria. Here's how it looks.
Map<Boolean, Toy> firstRedAndBlueToysMap = toyList.stream()
.filter(t -> t.isBlue() || t.isRed())
.collect(Collectors.toMap(Toy::isRed, t -> t, (a, b) -> a));
Toy firstRedToy = firstRedAndBlueToysMap.get(true);
Toy firstBlueToy = firstRedAndBlueToysMap.get(false);
And here's a one step approach to solve your problem.
RedBlueExtravaganza firstRedAndBlueToyPair = toyList.stream()
.filter(t -> t.isBlue() || t.isRed())
.collect(Collectors.collectingAndThen(Collectors.toMap(Toy::isRed,
t -> t, (a, b) -> a),
m -> new RedBlueExtravaganza(m.get(true), m.get(false))));
P.S. For this to work you need to have the following constructor in your RedBlueExtravaganza class contrary to the one you have provided above.
public RedBlueExtravaganza(Toy rt, Toy bt) {
if (!(rt instanceof RedToy) || !(bt instanceof BlueToy))
throw new IllegalArgumentException();
// remainder omitted.
}
I like this answers, similar solution can be with reduce and List as "Tuple (or something)"
List<Toy> reduceToList = toyList.stream()
.filter(t -> t.isBlue() || t.isRed())
.map(t -> Arrays.asList(t, t))
.reduce(Arrays.asList(null, null), (a, c) -> a.get(0) == null && c.get(0).isRed() ?
Arrays.asList(c.get(0), a.get(1)) : (a.get(1) == null && c.get(1).isBlue() ?
Arrays.asList(a.get(0), c.get(1)) : a)
);
If both values are not null then you can map to RedBlueExtravaganza
One way which first comes into my mind is to use 2 streams for finding first red and blue toy object, respectively. Then merge them into one stream by using Stream.concat() so that you can add more oprations for this.
Code snippet
RedBlueExtravaganza firstRedBlueList = Stream
.concat(
toyList.stream().filter(t -> t.isRed())
.findFirst()
.map(Collections::singletonList)
.orElseGet(Collections::emptyList)
.stream(),
toyList.stream().filter(t -> t.isBlue())
.findFirst()
.map(Collections::singletonList)
.orElseGet(Collections::emptyList)
.stream())
.map(x -> {
RedToy rt = new RedToy();
BlueToy bt = new BlueToy();
...
return new RedBlueExtravaganza(rt, bt);})
.findFirst()
.get();
Have two classes and two corresponding lists:
class Click {
long campaignId;
Date date;
}
class Campaign {
long campaignId;
Date start;
Date end;
String type;
}
List<Click> clicks = ..;
List<Campaign> campaigns = ..;
And want to find all Clicks in clicks that:
Have a corresponding Campaign in campaigns list, i.e., Campaign with the same campaignId AND
This Campaign has type = "prospective" AND
This Campaigns.start < click.date < Campaigns.end
So far I have the following implementation (which seems confusing and complex to me):
clicks.
stream().
filter(click -> campaigns.stream().anyMatch(
campaign -> campaign.getCampaignType().equals("prospecting") &&
campaign.getCampaignId().equals(click.getCampaignId()) &&
campaign.getStart().after(click.getDate()) &&
campaign.getEnd().before(click.getDate()))).
collect(toList());
I wonder if there is simpler solution for the problem.
Well, there is a very neat way to solve your problem IMO, original idea coming from Holger (I'll find the question and link it here).
You could define your method that does the checks (I've simplified it just a bit):
static boolean checkClick(List<Campaign> campaigns, Click click) {
return campaigns.stream().anyMatch(camp -> camp.getCampaignId()
== click.getCampaignId());
}
And define a function that binds the parameters:
public static <T, U> Predicate<U> bind(BiFunction<T, U, Boolean> f, T t) {
return u -> f.apply(t, u);
}
And the usage would be:
BiFunction<List<Campaign>, Click, Boolean> biFunction = YourClass::checkClick;
Predicate<Click> predicate = bind(biFunction, campaigns);
clicks.stream()
.filter(predicate::test)
.collect(Collectors.toList());
One thing that stands out is that your 2nd requirement has nothing to do with the matching, it's a condition on campaigns only. You'd have to test if this is any better for you:
clicks.stream()
.filter(click -> campaigns.stream()
.filter(camp -> "prospecting".equals(camp.type))
.anyMatch(camp ->
camp.campaignId == click.campaignId &&
camp.end.after(click.date) &&
camp.start.before(click.date)
)
)
.collect(Collectors.toList());
Otherwise, I have never seen a streams solution which does not involve streaming the 2nd collection inside the predicate of the 1st, so you can't do much better than what you did. In terms of readability, if it looks that confusing to you then create a method that test for the boolean condition and call it:
clicks.stream()
.filter(click -> campaigns.stream()
.filter(camp -> "pre".equals(camp.type))
.anyMatch(camp -> accept(camp, click))
)
.collect(Collectors.toList());
static boolean accept(Campaign camp, Click click) {
return camp.campaignId == click.campaignId &&
camp.end.after(click.date) &&
camp.start.before(click.date);
}
Finally, 2 unrelated suggestions:
Don't use the old Date class, instead use the new java.time API's LocalDate.
If Campaign's type can only have some predefined values (like "submitted", "prospecting", "accepted"...) then an enum would be a better fit than a general String.
My 2 cents:
Since there is no much boilerplate code in OP. So it may be not possible/necessary to reduce the lines/characters in the codes. we could rewrite it to make it a little more clearly:
Map<Long, List<Campaign>> map = campaigns.stream().filter(c -> c.type.equals("prospecting"))
.collect(Collectors.groupingBy(c -> c.campaignId));
clicks.stream().filter(k -> map.containsKey(k.campaignId))
.filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.before(k.date) && c.end.after(k.date)))
.collect(Collectors.toList());
The code is not much shorter than original code. but it will improve performance from O(nm) to O(n+m), as #Marco13 mentioned in the comments. if you want shorter, try StreamEx
Map<Long, List<Campaign>> map = StreamEx.of(campaigns)
.filter(c -> c.type.equals("prospecting")).groupingBy(c -> c.campaignId);
StreamEx.of(clicks).filter(k -> map.containsKey(k.campaignId))
.filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.after(k.date) && c.end.before(k.date)))
.toList();
public List<Click> findMatchingClicks(List<Campaign> cmps, List<Click> clicks) {
List<Campaign> cmpsProspective = cmps.stream().filter(cmp -> "prospective".equals(cmp.type)).collect(Collectors.toList());
return clicks.stream().filter(c -> matchesAnyCmp(c, cmpsProspective).collect(Collectors.toList());
}
public boolean matchesAnyCmp(Click click, List<Campaign> cmps) {
return cmps.stream().anyMatch(click -> cmp.start.before(click.date) && cmp.end.after(click.date));
}
Replace fields for getters, just wrote it quick.
I have an entity Employee
class Employee{
private String name;
private String addr;
private String sal;
}
Now i have list of these employees. I want to filter out those objects which has name = null and set addr = 'A'. I was able to achieve like below :
List<Employee> list2= list.stream()
.filter(l -> l.getName() != null)
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
Now list2 will have all those employees whose name is not null and then set addr as A for those employees.
What i also want to find is those employees which are filtered( name == null) and save them in DB.One way i achieved is like below :
List<Employee> list2= list.stream()
.filter(l -> filter(l))
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
private static boolean filter(Employee l){
boolean j = l.getName() != null;
if(!j)
// save in db
return j;
}
1) Is this the right way?
2) Can we do this directly in lambda expression instead of writing separate method?
Generally, you should not use side effect in behavioral parameters. See the sections “Stateless behaviors” and “Side-effects” of the package documentation. Also, it’s not recommended to use peek for non-debugging purposes, see “In Java streams is peek really only for debugging?”
There’s not much advantage in trying to squeeze all these different operations into a single Stream pipeline. Consider the clean alternative:
Map<Boolean,List<Employee>> m = list.stream()
.collect(Collectors.partitioningBy(l -> l.getName() != null));
m.get(false).forEach(l -> {
// save in db
});
List<Employee> list2 = m.get(true);
list2.forEach(l -> l.setAddr("A"));
Regarding your second question, a lambda expression allows almost everything, a method does. The differences are on the declaration, i.e. you can’t declare additional type parameters nor annotate the return type. Still, you should avoid writing too much code into a lambda expression, as, of course, you can’t create test cases directly calling that code. But that’s a matter of programming style, not a technical limitation.
If you are okay in using peek for implementing your logic (though it is not recommended unless for learning), you can do the following:
List<Employee> list2= list.stream()
.peek(l -> { // add this peek to do persistence
if(l.getName()==null){
persistInDB(l);
}
}).filter(l -> l.getName() != null)
.peek(l -> l.setAddr("A"))
.collect(Collectors.toList());
You can also do something like this:
List<Employee> list2 = list.stream()
.filter(l->{
boolean condition = l.getName()!=null;
if(condition){
l.setAddr("A");
} else {
persistInDB(l);
}
return condition;
})
.collect(Collectors.toList());
Hope this helps!
I have a method like:
private List<Person> findFilteredPersons(List<Person> persons,
Predicate<Person> filter) {
return persons
.stream()
.filter(filter)
.sorted(BY_NAME.thenComparing(BY_AGE))
.collect(Collectors.toList());
}
Now I would like to use this method in several places, but in one place I don't need to filter the list, I just want to sort it. What is the best way to achieve this?
It’s unlikely that you will run into performance issue when simply using a x -> true predicate to indicate “no filtering”, however, if you want to express the unfiltered operation as such for clarity, the solution is straight-forward: as always for such kind of code organization, split the operation into the distinct parts and the common part:
private List<Person> findFilteredPersons(List<Person> persons, Predicate<Person> filter) {
return collectSortedPersons(persons.stream().filter(filter));
}
private List<Person> findPersons(List<Person> persons) {
return collectSortedPersons(persons.stream());
}
private List<Person> collectSortedPersons(Stream<Person> persons) {
return persons
.sorted(BY_NAME.thenComparing(BY_AGE))
.collect(Collectors.toList());
}
To take all items (practically ignoring the filter), pass a predicate that takes all people:
p -> true
You can create another method:
private List<Person> findPeople(List<Person> persons) {
return findFilteredPersons(persons, p -> true);
}
I am looking for some help in converting some code I have to use the really nifty Java 8 Stream library. Essentially I have a bunch of student objects and I would like to get back a list of filtered objects as seen below:
List<Integer> classRoomList;
Set<ScienceStudent> filteredStudents = new HashSet<>();
//Return only 5 students in the end
int limit = 5;
for (MathStudent s : mathStudents)
{
// Get the scienceStudent with the same id as the math student
ScienceStudent ss = scienceStudents.get(s.getId());
if (classRoomList.contains(ss.getClassroomId()))
{
if (!exclusionStudents.contains(ss))
{
if (limit > 0)
{
filteredStudents.add(ss);
limit--;
}
}
}
}
Of course the above is a super contrived example I made up for the sake of learning more Java 8. Assume all students are extended from a Student object with studentId and classRoomId. An additional requirement I would require is the have the result be an Immutable set.
A quite literal translation (and the required classes to play around)
interface ScienceStudent {
String getClassroomId();
}
interface MathStudent {
String getId();
}
Set<ScienceStudent> filter(
Collection<MathStudent> mathStudents,
Map<String, ScienceStudent> scienceStudents,
Set<ScienceStudent> exclusionStudents,
List<String> classRoomList) {
return mathStudents.stream()
.map(s -> scienceStudents.get(s.getId()))
.filter(ss -> classRoomList.contains(ss.getClassroomId()))
.filter(ss -> !exclusionStudents.contains(ss))
.limit(5)
.collect(Collectors.toSet());
}
Multiple conditions to filter really just translate into multiple .filter calls or a combined big filter like ss -> classRoomList.contains(ss.getClassroomId()) && !exclusion...
Regarding immutable set: You best wrap that around the result manually because collect expects a mutable collection that can be filled from the stream and returned once finished. I don't see an easy way to do that directly with streams.
The null paranoid version
return mathStudents.stream().filter(Objects::nonNull) // math students could be null
.map(MathStudent::getId).filter(Objects::nonNull) // their id could be null
.map(scienceStudents::get).filter(Objects::nonNull) // and the mapped science student
.filter(ss -> classRoomList.contains(ss.getClassroomId()))
.filter(ss -> !exclusionStudents.contains(ss))
.limit(5)
.collect(Collectors.toSet());