Java Stream Generate Map from List of Objects - java

I have a class like this.
public class Foo {
private String prefix;
private String sector;
private int count;
}
Given a foo list:
//Args: prefix, sector, count
fooList.add(new Foo("44",,"RowC", 1 ));
fooList.add(new Foo("1",,"Rowa", 1 ));
fooList.add(new Foo("1",,"RowB", 1 ));
fooList.add(new Foo("1",,"Rowa", 1 ));
And I need to return the request an object sorted by Prefix asc like this:
{
"1": {
"Rowa": "2",
"RowB": "1"
},
"44": {
"RowC": "1"
}
}
So the problem is:
I have to group the list by the prefix, and then show, every sector and the count(*) of items on the list with the same row and sector.
The far that I got is using stream like this:
fooList.stream()
.collect(Collectors.groupingBy(
Foo::getPrefix,
Collectors.groupingBy(
Foo::getSector,
Collectors.mapping(Foo::getSector , Collectors.counting())
)
));
The problem is, that the code above, is that the count is a Long, and I need to return as a String.
I've tried with .toString but it gives me an error (Can assign java.lang.String to java.util.stream.Collector).
UPDATE
With the help of Andreas and Naman, now I can map count as String.
I just need it sorted by Prefix.
Can anyone help me?

You were almost there, just replace the Collectors.mapping line with:
Collectors.summingInt(Foo::getCount))
As in:
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo("44", "RowC", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
fooList.add(new Foo("1", "RowB", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
Map<String, Map<String, String>> result = fooList.stream().collect(
Collectors.groupingBy(
Foo::getPrefix,
TreeMap::new, // remove if prefix sorting not needed
Collectors.groupingBy(
Foo::getSector,
() -> new TreeMap<>(Collator.getInstance()), // remove if sector sorting not needed
Collectors.collectingAndThen(
Collectors.summingInt(Foo::getCount),
String::valueOf
)
)
)
);
System.out.println(result); // prints: {1={Rowa=2, RowB=1}, 44={RowC=1}}
Notice the TreeMap constructors added to the groupingBy() calls, which ensures that the maps are sorted. The first is sorting lexicographically, while the second sorts according to spoken language, i.e. upper- vs lowercase doesn't affect order.

Is this what you need?
Code :
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingInt;
public class Main {
public static void main(String [] args){
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo("44", "RowC", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
fooList.add(new Foo("1", "RowB", 1 ));
fooList.add(new Foo("1", "Rowa", 1 ));
Map<String, Map<String, Integer>> result = grouper(fooList);
result.forEach( (k,v) -> System.out.printf("%s\n%s\n", k,v) );
}
/* group the list by the prefix, and then show, every sector and the
* count(*) of items on the list with the same row and sector.*/
public static Map<String, Map<String, Integer>> grouper(List<Foo> foos){
//Map<prefix, Map<sector, count>
Map<String, Map<String, Integer>> result = foos.stream()
.collect(
//Step 1 - group by prefix. Outer map key is prefix.
groupingBy( Foo::getPrefix,
//Use a tree map which wil be sorted by its key, i.e prefix.
TreeMap::new,
//Step 2 - group by sector.
groupingBy( Foo::getSector,
//Step 3 - Sum the Foo's in each sector in each prefix.
summingInt(Foo::getCount)
)
)
);
return result;
}
}
Output :
1
{Rowa=2, RowB=1}
44
{RowC=1}
PS - I referred to this tutorial to answer your question. I referred to the examples in "2.5. Grouping by Multiple Fields" and "2.7. Getting the Sum from Grouped Results". The next step was to find out how to also order by the key of a map while grouping which I got from "2.11. Modifying the Return Map Type" and also checked it here.

Related

Get the 3 max value in a Map

I'm storing information of the lastByFirst variable.
{Peter=[Leigh], George=[Barron, Trickett,Evans],
Paul-Courtenay=[Hyu], Ryan=[Smith], Toby=[Geller, Williams],
Simon=[Bush, Milosic, Quarterman,Brown]}
How can I print the first 3 which appeared the most and also the number of appereance.
I would like to list those which 3 value appeared the most. In the lastByFirst contains something like that. I would like to print in this way:
Simon: 4
George: 3
Toby:2
Map<String, List<String>> lastByFirst = PeopleProcessor.lastnamesByFirstname(PeopleSetup.people);
My attempt was something like that:
var store = lastByFirst.entrySet()
.stream()
.collect( Collectors.groupingBy(Person::getLastName,
Collectors.counting())
.toString();
store should be equal with
Simon: 4
George: 3
Toby:2
Here's one that first converts the map of lists to a map of the sizes of the list, and then picks the top three such sizes:
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Demo {
public static void main(String[] args) {
Map<String, List<String>> lastByFirst =
Map.of("Peter", List.of("Leigh"), "George", List.of("Barron", "Trickett", "Evans"),
"Paul-Courtenay", List.of("Hyu"), "Ryan", List.of("Smith"),
"Toby", List.of("Geller", "Williams"), "Simon", List.of("Bush", "Milosic", "Quaterman", "Brown"));
List<Map.Entry<String, Integer>> topThree =
lastByFirst.entrySet().stream()
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> e.getValue().size()))
.entrySet()
.stream()
.sorted(Comparator.<Map.Entry<String, Integer>, Integer>comparing(Map.Entry::getValue).reversed())
.limit(3)
.collect(Collectors.toList());
System.out.println(topThree);
}
}
You can:
sort in descending mode by size
select the first three elements
reduce to one string
//1
List<Map.Entry<String, List<String>>> entryList = lastByFirst.entrySet()
.stream()
.sorted((e2, e1) -> Integer.compare(e1.getValue().size(), e2.getValue().size()))
.toList();
//2
String result = IntStream.range(0, 3)
.mapToObj(entryList::get)
.map(e -> String.format("%s: %d\n", e.getKey(), e.getValue().size()))
.collect(Collectors.joining()); //3
If you already have a map of people grouped by first name, you can address the problem of finding the 3 most frequent first names in a linear time O(n). Which is faster than sorting the whole data set.
If instead of picking 3 most frequent first names it would be generalized to m most frequent, then the time complexity would be O(n + m * log m) (which for small values of m would be close to linear time).
To implement it using streams, we can utilize a custom comparator, which can be created using the static method Collector.of().
As a mutable container of the collector, we can use a TreeMap sorted in the natural order, where the key would represent the of people having the same first name and the value would be a first name itself.
In order to retain only m most frequent names we need to track the size of the TreeMap and when it gets exceeded we have to remove the first entry (i.e. an entry having the lowest key).
public static <K, V> Collector<Map.Entry<K, List<V>>, ?, NavigableMap<Integer, K>>
getEntryCollector(int size) {
return Collector.of(
TreeMap::new,
(NavigableMap<Integer, K> map, Map.Entry<K, List<V>> entry) -> {
if (map.size() < size || map.firstKey() < entry.getValue().size()) { // the container hasn't reached the maximum size of the frequency of the offered name is higher than the lowest existing frequency
map.put(entry.getValue().size(), entry.getKey());
}
if (map.size() > size) map.remove(map.firstKey()); // the size of the container has been exceeded
},
(NavigableMap<Integer, K> left, NavigableMap<Integer, K> right) -> { // merging the two containers with partial results obtained during the parallel execution
left.putAll(right);
while (left.size() > size) left.remove(left.firstKey());
return left;
}
);
}
main()
public static void main(String args[]) {
Map<String, List<String>> lastByFirst =
Map.of("Peter", List.of("Leigh"), "George", List.of("Barron", "Trickett", "Evans"),
"Paul-Courtenay", List.of("Hyu"), "Ryan", List.of("Smith"), "Toby", List.of("Geller", "Williams"),
"Simon", List.of("Bush", "Milosic", "Quarterman", "Brown"));
NavigableMap<Integer, String> nameByFrequency =
lastByFirst.entrySet().stream()
.collect(getEntryCollector(3));
nameByFrequency.entrySet().stream() // printing the result, sorting in reversed order applied only for demo purposes
.sorted(Map.Entry.comparingByKey(Comparator.<Integer>naturalOrder().reversed()))
.forEach(entry -> System.out.println(entry.getValue() + ": " + entry.getKey()));
}
Output:
Simon: 4
George: 3
Toby: 2
A link Online Demo
Here is another solution by StreamEx:
EntryStream.of(lastByFirst)
.mapValues(List::size) //
.reverseSorted(Comparators.comparingByValue())
.limit(3)
.toList()
.forEach(System.out::println);

How to map multiple properties under groupingBy property in java streams

I am trying to get aggregate Marks of each student grouped by the department and sorted by their aggregate marks.
This is how I am trying.
Student class properties:
private String firstName,lastName,branch,nationality,grade,shName;
private SubjectMarks subject;
private LocalDate dob;
SubjectMarks class:
public SubjectMarks(int maths, int biology, int computers) {
this.maths = maths;
this.biology = biology;
this.computers = computers;
}
public double getAverageMarks() {
double avg = (getBiology() + getMaths() + getComputers())/3;
return avg;
}
Main class:
Collections.sort(stList, new Comparator<Student>() {
#Override
public int compare(Student m1, Student m2) {
if(m1.getSubject().getAverageMarks() == m2.getSubject().getAverageMarks()){
return 0;
}
return m1.getSubject().getAverageMarks()< m2.getSubject().getAverageMarks()? 1 : -1;
}
});
Map<String, List<Student>> groupSt=stList.stream().collect(Collectors.groupingBy(Student::getBranch,
LinkedHashMap::new,Collectors.toList()));
groupSt.forEach((k, v) -> System.out.println("\nBranch Name: " + k + "\n" + v.stream()
.flatMap(stud->Stream.of(stud.getFirstName(),stud.getSubject().getAverageMarks())).collect(Collectors.toList())));
updated code: This is how I am getting the output.
Branch Name: ECE
[Bob, 96.0, TOM, 84.33333333333333]
Branch Name: CSE
[Karthik, 94.33333333333333, Angelina, 91.0, Arun, 80.66666666666667]
Branch Name: EEE
[Meghan, 85.0]
This is the actual sorted order but Student objects are getting flattened in one line separated by a comma(,).
In the above output, since Bob got the highest aggregate marks of all branches, ECE comes first and followed by other branches sorted with student aggregate marks.
The Expected result is :
List of student names with their aggregate marks sorted.
Branch Name: ECE
[{Bob, 96.0},{TOM, 84.33333333333333}]
Branch Name: CSE
[{Karthik, 94.33333333333333}, {Angelina, 91.0}, {Arun,
80.66666666666667}]
Branch Name: EEE
[Meghan, 85.0]
Is there any way to map both name and average on groupingBy a property using streams?
You could rather prefer to choose the return type to be a Map<String, Map<String, Double>> or a custom class with appropriate equals and hashCode to ensure the uniqueness amongst the inner List<Custom>. I would frame the solution based on the former, and you can convert it to the one which is more readable to your actual code.
Once you have grouped each branch specific students, what you could do to ensure firstName is mapped to maximum average marks of that student is to perform a reduction using toMap with merge based on Double::max... and then collect these entries soted based on the marks (values).
Might look slightly complicated with the following code, but it could be broken into steps as well.
Map<String, LinkedHashMap<String, Double>> branchStudentsSortedOnMarks = stList.stream()
.collect(Collectors.groupingBy(Student::getBranch, // you got it!
Collectors.collectingAndThen(
Collectors.toMap(Student::getFirstName,
s -> s.getSubject().getAverageMarks(), Double::max), // max average marks per name
map -> map.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed()) // sorted inn reverse based on value
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (a, b) -> b, LinkedHashMap::new))
)));
Firstly, in your Map<String, List<Map<String,Double>>> the map inside the list would contain only one key-value pair. So I would suggest you to return Map<String, List<Entry<String, Double>>>. (Entry in java.util.Map)
Also, create a getAverageMarks in your student class which would return:
return subject.getAverageMarks();
// First define a function to sort based on average marks
UnaryOperator<List<Entry<String, Double>>> sort =
list -> {
Collections.sort(list, Collections.reverseOrder(Entry.comparingByValue()));
return list;
};
// function to create entry
Function<Student, Entry<String, Double>> getEntry =
s -> Map.entry(s.getFirstName(), s.getAverageMarks());
// return this
list.stream()
.collect(Collectors.groupingBy(
Student::getBranch,
Collectors.mapping(getEntry, // map each student
// collect and apply sort as finisher
Collector.of(ArrayList::new,
List::add,
(x,y) -> {x.addAll(y); return x;},
sort))));
Input:
List<Student> stList = Arrays.asList(
new Student("John", "Wall", "A", "a", "C", "sa", new SubjectMarks(65, 67, 100), LocalDate.now()),
new Student("Arun", "Wall", "B", "a", "C", "sa", new SubjectMarks(45, 61, 95), LocalDate.now()),
new Student("Marry", "Wall", "A", "a", "C", "sa", new SubjectMarks(90, 80, 92), LocalDate.now())
);
Idea:
group by "branch"
For each group (list) - sort by grade and map each student to a map of "name","grade".
Now it's easy to code:
Map<String, List<Map<String, Double>>> branchToSortedStudentsByGrade = stList.stream().collect(Collectors.groupingBy(
Student::getBranch, Collectors.collectingAndThen(Collectors.toList(),
l -> l.stream()
.sorted(Comparator.comparing(st -> st.getSubject().getAverageMarks(), Comparator.reverseOrder()))
.map(student -> Collections.singletonMap(student.getFirstName(), student.getSubject().getAverageMarks()))
.collect(Collectors.toList()))));
Output:
{
A=[{Marry=87.0}, {John=77.0}],
B=[{Arun=67.0}]
}
By the way:
Note that you divide by an integer in a floating-point context in getAverageMarks:
public double getAverageMarks() {
double avg = (getBiology() + getMaths() + getComputers())/3;
return avg;
}
This will cause all grades to be in this format- xx.0
If it's by mistake, I would recommend on this approach:
public double getAverageMarks() {
return DoubleStream.of(maths, biology, computers)
.average()
.getAsDouble();
}

Java Using Multiple predicate for anyMatch

I am having a List of HashMap i.e List<Map<String, Object>> result. I want to check if some key value is present on this list or not.
For Example:
Name Count
Marvel 10
DC 0
I want that my list of map should contain at least one entry for the above table for that I am using anyMatch
Assert.assertTrue(
result.stream().anyMatch(
ag -> ( "Marvel".equals(ag.get("Name")) && 10==(int)ag.get("Count"))
));
How can I use multiple Predicates ? I can have a list of 1000 HashMap, I just want to check if any two Hash-map from my list contains those two entries.
Your data structure seems wrong for a start. A List<Map<String, Object>>, where each map represents a row and has only one key and one value? You just need a Map<String, Integer>. The string is the name, the integer is the count. Remember to give it a meaningful name, e.g. comicUniverseToSuperheroFrequency.
With that said, Predicate has an and method which you can use to chain conditions together. It might look something like this:
public static void main(String[] args)
{
Map<String, Integer> comicUniverseToSuperheroFrequency = /*something*/;
boolean isMarvelCountTen = comicUniverseToSuperheroFrequency.entrySet().stream()
.anyMatch(row -> isMarvel().and(isTen()).test(row));
}
private static Predicate<Map.Entry<String, Integer>> isMarvel()
{
return row -> "Marvel".equals(row.getKey());
}
private static Predicate<Map.Entry<String, Integer>> isTen()
{
return row -> row.getValue() == 10;
}

Converting List<MyObject> to Map<String, List<String>> in Java 8 when we have duplicate elements and custom filter criteria

I have an instances of Student class.
class Student {
String name;
String addr;
String type;
public Student(String name, String addr, String type) {
super();
this.name = name;
this.addr = addr;
this.type = type;
}
#Override
public String toString() {
return "Student [name=" + name + ", addr=" + addr + "]";
}
public String getName() {
return name;
}
public String getAddr() {
return addr;
}
}
And I have a code to create a map , where it store the student name as the key and some processed addr values (a List since we have multiple addr values for the same student) as the value.
public class FilterId {
public static String getNum(String s) {
// should do some complex stuff, just for testing
return s.split(" ")[1];
}
public static void main(String[] args) {
List<Student> list = new ArrayList<Student>();
list.add(new Student("a", "test 1", "type 1"));
list.add(new Student("a", "test 1", "type 2"));
list.add(new Student("b", "test 1", "type 1"));
list.add(new Student("c", "test 1", "type 1"));
list.add(new Student("b", "test 1", "type 1"));
list.add(new Student("a", "test 1", "type 1"));
list.add(new Student("c", "test 3", "type 2"));
list.add(new Student("a", "test 2", "type 1"));
list.add(new Student("b", "test 2", "type 1"));
list.add(new Student("a", "test 3", "type 1"));
Map<String, List<String>> map = new HashMap<>();
// This will create a Map with Student names (distinct) and the test numbers (distinct List of tests numbers) associated with them.
for (Student student : list) {
if (map.containsKey(student.getName())) {
List<String> numList = map.get(student.getName());
String value = getNum(student.getAddr());
if (!numList.contains(value)) {
numList.add(value);
map.put(student.getName(), numList);
}
} else {
map.put(student.getName(), new ArrayList<>(Arrays.asList(getNum(student.getAddr()))));
}
}
System.out.println(map.toString());
}
}
Output would be :
{a=[1, 2, 3], b=[1, 2], c=[1, 3]}
How can I just do the same in java8 in a much more elegant way, may be using the streams ?
Found this Collectors.toMap in java 8 but could't find a way to actually do the same with this.
I was trying to map the elements as CSVs but that it didn't work since I couldn't figure out a way to remove the duplicates easily and the output is not what I need at the moment.
Map<String, String> map2 = new HashMap<>();
map2 = list.stream().collect(Collectors.toMap(Student::getName, Student::getAddr, (a, b) -> a + " , " + b));
System.out.println(map2.toString());
// {a=test 1 , test 1 , test 1 , test 2 , test 3, b=test 1 , test 1 , test 2, c=test 1 , test 3}
With streams, you could use Collectors.groupingBy along with Collectors.mapping:
Map<String, Set<String>> map = list.stream()
.collect(Collectors.groupingBy(
Student::getName,
Collectors.mapping(student -> getNum(student.getAddr()),
Collectors.toSet())));
I've chosen to create a map of sets instead of a map of lists, as it seems that you don't want duplicates in the lists.
If you do need lists instead of sets, it's more efficient to first collect to sets and then convert the sets to lists:
Map<String, List<String>> map = list.stream()
.collect(Collectors.groupingBy(
Student::getName,
Collectors.mapping(s -> getNum(s.getAddr()),
Collectors.collectingAndThen(Collectors.toSet(), ArrayList::new))));
This uses Collectors.collectingAndThen, which first collects and then transforms the result.
Another more compact way, without streams:
Map<String, Set<String>> map = new HashMap<>(); // or LinkedHashMap
list.forEach(s ->
map.computeIfAbsent(s.getName(), k -> new HashSet<>()) // or LinkedHashSet
.add(getNum(s.getAddr())));
This variant uses Iterable.forEach to iterate the list and Map.computeIfAbsent to group transformed addresses by student name.
First of all, the current solution is not really elegant, regardless of any streaming solution.
The pattern of
if (map.containsKey(k)) {
Value value = map.get(k);
...
} else {
map.put(k, new Value());
}
can often be simplified with Map#computeIfAbsent. In your example, this would be
// This will create a Map with Student names (distinct) and the test
// numbers (distinct List of tests numbers) associated with them.
for (Student student : list)
{
List<String> numList = map.computeIfAbsent(
student.getName(), s -> new ArrayList<String>());
String value = getNum(student.getAddr());
if (!numList.contains(value))
{
numList.add(value);
}
}
(This is a Java 8 function, but it is still unrelated to streams).
Next, the data structure that you want to build there does not seem to be the most appropriate one. In general, the pattern of
if (!list.contains(someValue)) {
list.add(someValue);
}
is a strong sign that you should not use a List, but a Set. The set will contain each element only once, and you will avoid the contains calls on the list, which are O(n) and thus may be expensive for larger lists.
Even if you really need a List in the end, it is often more elegant and efficient to first collect the elements in a Set, and afterwards convert this Set into a List in one dedicated step.
So the first part could be solved like this:
// This will create a Map with Student names (distinct) and the test
// numbers (distinct List of tests numbers) associated with them.
Map<String, Collection<String>> map = new HashMap<>();
for (Student student : list)
{
String value = getNum(student.getAddr());
map.computeIfAbsent(student.getName(), s -> new LinkedHashSet<String>())
.add(value);
}
It will create a Map<String, Collection<String>>. This can then be converted into a Map<String, List<String>> :
// Convert the 'Collection' values of the map into 'List' values
Map<String, List<String>> result =
map.entrySet().stream().collect(Collectors.toMap(
Entry::getKey, e -> new ArrayList<String>(e.getValue())));
Or, more generically, using a utility method for this:
private static <K, V> Map<K, List<V>> convertValuesToLists(
Map<K, ? extends Collection<? extends V>> map)
{
return map.entrySet().stream().collect(Collectors.toMap(
Entry::getKey, e -> new ArrayList<V>(e.getValue())));
}
I do not recommend this, but you also could convert the for loop into a stream operation:
Map<String, Set<String>> map =
list.stream().collect(Collectors.groupingBy(
Student::getName, LinkedHashMap::new,
Collectors.mapping(
s -> getNum(s.getAddr()), Collectors.toSet())));
Alternatively, you could do the "grouping by" and the conversion from Set to List in one step:
Map<String, List<String>> result =
list.stream().collect(Collectors.groupingBy(
Student::getName, LinkedHashMap::new,
Collectors.mapping(
s -> getNum(s.getAddr()),
Collectors.collectingAndThen(
Collectors.toSet(), ArrayList<String>::new))));
Or you could introduce an own collector, that does the List#contains call, but all this tends to be far less readable than the other solutions...
I think you are looking for something like below
Map<String,Set<String>> map = list.stream().
collect(Collectors.groupingBy(
Student::getName,
Collectors.mapping(e->getNum(e.getAddr()), Collectors.toSet())
));
System.out.println("Map : "+map);
Here is a version that collects everything in sets, and converts the final result to array lists:
/*
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import java.util.function.*;
*/
Map<String, List<String>> map2 = list.stream().collect(groupingBy(
Student::getName, // we will group the students by name
Collector.of(
HashSet::new, // for each student name, we will collect result in a hash set
(arr, student) -> arr.add(getNum(student.getAddr())), // which we fill with processed addresses
(left, right) -> { left.addAll(right); return left; }, // we merge subresults like this
(Function<HashSet<String>, List<String>>) ArrayList::new // finish by converting to List
)
));
System.out.println(map2);
// Output:
// {a=[1, 2, 3], b=[1, 2], c=[1, 3]}
EDIT: made the finisher shorter using Marco13's hint.

Java 8 streams: sum values on a distinct key

I have a file with rows with the following column headers:
CITY_NAME COUNTY_NAME POPULATION
Atascocita Harris 65844
Austin Travis 931820
Baytown Harris 76335
...
I am using streams to attempt to generate an output similar to:
COUNTY_NAME CITIES_IN_COUNTY POPULATION_OF_COUNTY
Harris 2 142179
Travis 1 931820
...
So far I have been able to use streams to get a list of distinct county names (as these are repetitive), but now I am having issues with getting the count of cities in a distinct county and consequently the sum of the populations of cities in these counties. I have read the file into an ArrayList of type texasCitiesClass, and my code thus far looks like:
public static void main(String[] args) throws FileNotFoundException, IOException {
PrintStream output = new PrintStream(new File("output.txt"));
ArrayList<texasCitiesClass> txcArray = new ArrayList<texasCitiesClass>();
initTheArray(txcArray); // this method will read the input file and populate an arraylist
System.setOut(output);
List<String> counties;
counties = txcArray.stream()
.filter(distinctByKey(txc -> txc.getCounty())) // grab distinct county names
.distinct() // redundant?
.sorted((txc1, txc2) -> txc1.getCounty().compareTo(txc2.getCounty())); // sort alphabetically
}
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, String> seen = new ConcurrentHashMap<>();
return t -> seen.put(keyExtractor.apply(t), "") == null;
}
At this point, I have a stream containing the names of unique counties. Since the sorted() operator will return a new stream, how can I obtain (and thus sum) the population values for the counties?
Given the classes (ctor, getter, setter omitted)
class Foo {
String name;
String countyName;
int pop;
}
class Aggregate {
String name;
int count;
int pop;
}
You could aggregate your values by mapping them to Aggregate Objects using Collectors.toMap and merging them using its mergeFunction. Using the TreeMap, its entries are ordered by its key.
TreeMap<String, Aggregate> collect = foos.stream()
.collect(Collectors.toMap(
Foo::getCountyName,
foo -> new Aggregate(foo.countyName,1,foo.pop),
(a, b) -> new Aggregate(b.name, a.count + 1, a.pop + b.pop),
TreeMap::new)
);
Using
List<Foo> foos = List.of(
new Foo("A", "Harris", 44),
new Foo("C", "Travis ", 99),
new Foo("B", "Harris", 66)
);
the map is
{Harris=Aggregate{name='Harris', count=2, pop=110}, Travis =Aggregate{name='Travis ', count=1, pop=99}}

Categories