I have a small snippet of code where I want to group the results by a combination of 2 properties of the type in the stream. After appropriate filtering, I do a map where I create an instance of a simple type that holds those 2 properties (in this case called AirportDay). Now I want to group them together and order them descending by the count. The trouble I am having is coming up with the correct arguments for the groupingBy method. Here is my code so far:
final int year = getYear();
final int limit = getLimit(10, 1, 100);
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.map(f -> new AirportDay(f.getOriginAirport(), f.getDate()))
.collect(groupingBy( ????? , counting())) // stuck here
.entrySet()
.stream()
.sorted(comparingByValue(reverseOrder()))
.limit(limit)
.forEach(entry -> {
AirportDay key = entry.getKey();
printf("%-30s\t%s\t%,10d\n",
key.getAirport().getName(),
key.getDate(),
entry.getValue()
);
});
My first instinct was to pass AirportDay::this but that obviously doesn't work...
I'd appreciate any assistance you can provide in coming up with a solution to the above problem.
-Tony
If you want to group by AirportDay, provide the function to create the key to groupingBy:
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.collect(groupingBy(f -> new AirportDay(f.getOriginAirport(), f.getDate()), counting()))
Note: The AirportDay class must implement sensible equals() and hashCode() methods for this to work.
Related
This question is about Java Streams' groupingBy capability.
Suppose I have a class, WorldCup:
public class WorldCup {
int year;
Country champion;
// all-arg constructor, getter/setters, etc
}
and an enum, Country:
public enum Country {
Brazil, France, USA
}
and the following code snippet:
WorldCup wc94 = new WorldCup(1994, Country.Brazil);
WorldCup wc98 = new WorldCup(1998, Country.France);
List<WorldCup> wcList = new ArrayList<WorldCup>();
wcList.add(wc94);
wcList.add(wc98);
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.groupingBy(WorldCup::getCountry, Collectors.mapping(WorldCup::getYear));
After running this code, championsMap will contain:
Brazil: [1994]
France: [1998]
Is there a succinct way to have this list include an entry for all of the values of the enum? What I'm looking for is:
Brazil: [1994]
France: [1998]
USA: []
There are several approaches you can take.
The map which would be used for accumulating the stream data can be prepopulated with entries corresponding to every enum-member. To access all existing enum-members you can use values() method or EnumSet.allOf().
It can be achieved using three-args version of collect() or through a custom collector created via Collector.of().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(
() -> EnumSet.allOf(Country.class).stream() // supplier
.collect(Collectors.toMap(
Function.identity(),
c -> new ArrayList<>()
)),
(Map<Country, List<Integer>> map, WorldCup next) -> // accumulator
map.get(next.getCountry()).add(next.getYear()),
(left, right) -> // combiner
right.forEach((k, v) -> left.get(k).addAll(v))
);
Another option is to add missing entries to the map after reduction of the stream has been finished.
For that we can use built-in collector collectingAndThen().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(WorldCup::getCountry,
Collectors.mapping(WorldCup::getYear,
Collectors.toList())),
map -> {
EnumSet.allOf(Country.class)
.forEach(country -> map.computeIfAbsent(country, k -> new ArrayList<>())); // if you're not going to mutate these lists - use Collections.emptyList()
return map;
}
));
I am trying to learn to work with streams and collectors, I know how to do it with multiple for loops but I want to become a more efficient programmer.
Each project has a map committedHoursPerDay, where the key is the employee and the value is the amount of hours expressed in Integer. I want to loop through all project's committedHoursPerDay maps and filter the maps where the committedHoursPerDay is more than 7(fulltime), and add each of the Employee who works fulltime to the set.
The code that i have written so far is this:
public Set<Employee> getFulltimeEmployees() {
// TODO
Map<Employee,Integer> fulltimeEmployees = projects.stream().filter(p -> p.getCommittedHoursPerDay().entrySet()
.stream()
.filter(map -> map.getValue() >= 8)
.collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())));
return fulltimeEmployees.keySet();
}
however the filter recognizes the map because I can access the key and values, but in the .collect(Collectors.toMap()) it doesnt recognize the map and only sees it as a lambda argument
There is one to many notion here. You can first flatten the maps using flatMap and then apply filter to the map entries.
Map<Employee,Integer> fulltimeEmployees = projects.stream()
.flatMap(p -> p.getCommittedHoursPerDay()
.entrySet()
.stream())
.filter(mapEntry -> mapEntry.getValue() >= 8)
.collect(Collectors.toMap(mapEntry -> mapEntry.getKey(), mapEntry -> mapEntry.getValue()));
The flatMap step returns a Stream<Map.Entry<Employee, Integer>>. The filter thus operates on a Map.Entry<Employee, Integer>.
You can also use method reference on the collect step as .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
This question already has an answer here:
Collect complex objects in one lambda expression
(1 answer)
Closed 3 years ago.
I have below class objects in one ArrayList:
new Pojo("Football","M",1000)
new Pojo("Football","M",1000)
new Pojo("Cricket","F",500)
new Pojo("Cricket","F",1500)
new Pojo("Cricket","M",500)
new Pojo("Cricket","M",500)
I want to get the average points of the "Sports" with respect to "Gender".
I came across Java 8 Streams and seems like it is good to use it here but I don't know to implement it.
I have tried it like below:
Map<Object,Long> mp = pojoList.stream().collect(Collectors.groupingBy(p -> p.sport,Collectors.counting()));
It's working good, giving me a sports count but I want to apply one more filter of gender and get the average point.
The output which I want:
new PojoOutput("Football","M",1000)
new PojoOutput("Cricket","F",1000)
new PojoOutput("Cricket","M",500)
You can group to a map by using 2 groupingBy calls, one of them being the downstream to the other:
Map<String, Map<String, Double>> res = pojos.stream()
.collect(Collectors.groupingBy(Pojo::getSport,
Collectors.groupingBy(Pojo::getGender,
Collectors.averagingDouble(i -> i.getPoints()))));
This results in {Cricket={F=1000.0, M=500.0}, Football={M=1000.0}}, which you can easily convert to Pojo instances using something like this:
List<Pojo> groups = result.entrySet()
.stream()
.flatMap(sport -> sport.getValue()
.entrySet().stream()
.map(gender -> new Pojo(sport.getKey(),
gender.getKey(),
gender.getValue())))
.collect(Collectors.toList());
Result with a generated toString:
[Pojo [sport=Cricket, gender=F, points=1000.0],
Pojo [sport=Cricket, gender=M, points=500.0],
Pojo [sport=Football, gender=M, points=1000.0]]
You can use a SimpleEntry to group your data with. After that you can map the results back to your PojoOutput:
List<PojoOutput> mp = pojoList.stream()
.collect(Collectors.groupingBy(x -> new AbstractMap.SimpleEntry<>(x.getSport(), x.getGender()), Collectors.averagingInt(Pojo::getScore)))
.entrySet().stream()
.map(e -> new PojoOutput(e.getKey().getKey(), e.getKey().getValue(), e.getValue()))
.collect(Collectors.toList());
You can improve that solution a bit using your PojoOutput directly in the group by clause. Therefore the PojoOutput needs a constructor with Pojo as attribute and implement equals() and hashCode() according to the fields: For example something similar to that:
private static class PojoOutput {
private String sport;
private String gender;
private double average;
public PojoOutput(Pojo pojo) {
this.sport = pojo.getSport();
this.gender = pojo.getGender();
}
public void setAverage(double average) {
this.average = average;
}
// equals, hashCode and getter
}
Now you can use that class like that:
List<PojoOutput> mp = pojoList.stream()
.collect(Collectors.groupingBy(PojoOutput::new, Collectors.averagingInt(Pojo::getScore)))
.entrySet().stream()
.peek(p -> p.getKey().setAverage(p.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
The result in both cases is:
PojoOutput[sport='Cricket', gender='M', average=500.0]
PojoOutput[sport='Football', gender='M', average=1000.0]
PojoOutput[sport='Cricket', gender='F', average=1000.0]
You could use Arrays::asList for the Key that you want to group by:
pojoList.stream()
.collect(Collectors.groupingBy(
x -> Arrays.asList(x.getSport(), x.getGender()),
Collectors.averagingInt(Pojo::getScore)));
Or if you have a PojoOutput, simply replace the x -> Arrays.asList(x.getSport(), x.getGender()) with that constructor.
You want the key of the map to be [sport, gender]. So you need to create a class SportAndGenderKey containing these two properties and a proper equals and hashCode methods, and then you need to group your objects by SportAndGenderKey.
And then you want to use an averaging collector rather than a counting collector.
Note: the key could simply be a List containing the two properties, but I find it less clear than a dedicated key class.
As #JB Nizet said you can follow bellow code:
list.stream()
.collect(Collectors.toMap(p ->
new PojoOutput(p.getSport(), p.getGender()),Pojo::getPoint, (v1, v2) -> (v1 + v2) / 2))
.entrySet()
.stream()
.map(entry -> entry.getKey().setAverage(entry.getValue()))
.collect(Collectors.toList())
just override equals and hashCode methods in PojoOutput class based on two properties (sport & geneder)
and also:
public PojoOutput setAverage(Integer average) {
this.average = average;
return this;
}
I need to apply a list of regex to a string, so I thought to use java8 map reduce:
List<SimpleEntry<String, String>> list = new ArrayList<>();
list.add(new SimpleEntry<>("\\s*\\bper\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bda\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bcon\\s+.*$", ""));
String s = "Tavolo da cucina";
String reduced = list.stream()
.reduce(s, (v, entry) -> v.replaceAll(entry.getKey(), entry.getValue()) , (c, d) -> c);
Actually this code may be is not very beautiful, but it works. I know this cannot be parallelised and for me is ok.
Now my question is: is there any chance with Java8 (or higher version) to write something more elegant? I mean also avoiding to add the useless combiner function.
Inspired by Oleksandr's comment and Holger I wrote this
String reduced = list.stream()
.map(entry->
(Function<String, String>) v -> v.replaceAll(entry.getKey(), entry.getValue()))
.reduce(Function.identity(), Function::andThen)
.apply(s);
This also reduce all entries to a function composition.
Here's another, interesting approach: reduce all entries to a function composition, then apply that composed function on the original input:
String result = list.stream()
.map(entry ->
(Function<String, String>) text ->
text.replaceAll(entry.getKey(), entry.getValue()))
//following op also be written as .reduce(Function::compose) (see comment by Eugene)
.reduce((f1, f2) -> f1.andThen(f2)) //compose functions
.map(func -> func.apply(s)) //this basically runs all `replaceAll`
.get();
The result of this is your expected string. While this function composition is not intuitive, it nonetheless seems to fit the idea that your original list is in fact a sort of "transformation logic" chain.
I have Map<Integer,Doctor> docLib=new HashMap<>(); to save class of Doctor.
Class Doctor has methods:getSpecialization() return a String,
getPatients() to return a collection of class Person.
In the main method, I type:
public Map<String,Set<Person>> getPatientsPerSpecialization(){
Map<String,Set<Person>> res=this.docLib.entrySet().stream().
map(d->d.getValue()).
collect(groupingBy(d->d.getSpecialization(),
d.getPatients()) //error
);
return res;
}
As you can see, I have problem with groupingBy,I try to send the same value d to the method, but it's wrong.
How to solve this?
You need a second Collector for that mapping :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
);
}
EDIT:
I think my original answer may be wrong (it's hard to say without being able to test it). Since Doctor::getPatients returns a Collection, I think my code may return a Map<String,Set<Collection<Person>>> instead of the desired Map<String,Set<Person>>.
The easiest way to overcome that is to iterate over that Map again to produce the desired Map :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
)
.entrySet()
.stream()
.collect (Collectors.toMap (e -> e.getKey(),
e -> e.getValue().stream().flatMap(c -> c.stream()).collect(Collectors.toSet()))
);
}
Perhaps there's a way to get the same result with a single Stream pipeline, but I can't see it right now.
Instead of groupingBy, you could use toMap:
public Map<String, Set<Person>> getPatientsPerSpecialization() {
return docLib.values()
.stream()
.collect(toMap(Doctor::getSpecialization,
d -> new HashSet<>(d.getPatients()),
(p1, p2) -> Stream.concat(p1.stream(), p2.stream()).collect(toSet())));
}
What it does is that it groups the doctors per specialization and map each one to a set of the patients it has (so a Map<String, Set<Person>>).
If, when collecting the data from the pipeline, you encounter a doctor with a specialization that is already stored as a key in the map, you use the merge function to produce a new set of values with both sets (the set that is already stored as a value for the key, and the set that you want to associate with the key).