Using streams to find the highest unique values - java

Let's say I have this simple Car object:
class Car {
String id;
String model;
Integer price;
}
Now I have a list of cars which might look like so:
{ Car(1, Commodore, 55000), Car(2, F150, 120000), Car(3, F150, 130000),
Car(4, Camry, 50000), Car(5,Commodore,50000)}
I would like to filter any duplicate models out of the List, ensuring that I'm only keeping in the most expensive priced car of each duplicate, e.g:
{ Car(1, Commodore, 55000), Car(3, F150, 130000), Car(4, Camry, 50000) }
I believe that I can do this using the Streams API, but I'm just struggling to chain the right calls together.
In my head, I would imagine the pseudocode would be something like:
Collect all the cars, group by model
Use Collectors.maxBy() on each individual model's List to get the priciest
Amalgamate the resulting one-item lists into a global car list again
Trying to stitch that together got a bit messy though - any ideas?

You can create a Map of the max valued car by model:
List<Car> cars = new ArrayList<> ();
Map<String,Optional<Car>> maxCarByModel =
cars.stream ()
.collect (Collectors.groupingBy (c -> c.model,
Collectors.maxBy (Comparator.comparing (c->c.price))));

Use Collectors.toMap with a merge function that keeps the car with the max price:
Collection<Car> carsWithMaxPrice = cars.stream().collect(Collectors.toMap(
Car::getModel,
Function.identity(),
(c1, c2) -> c1.getPrice() > c2.getPrice() ? c1 : c2))
.values();

Related

Find max size of an element among list of objects

#Data
class Person {
private String fname;
private String lname;
private List<String> friends;
private List<BigDecimal> ratings;
...
}
#Data
class People {
private List<Person> people;
...
}
suppose i have the above classes, and need to figure out what is the most number of friends any one person has. I know the pre-streams way of doing it... by looping over every element in the list and
checking if friends.size() is greater than the currently stored number of longest list. is there a way to streamline the code with streams? something like this answer?
Compute the max Person contained in People according to a comparator that relies on the size of Person.getFriends() :
Optional<Person> p = people.getPeople()
.stream()
.max(Comparator.comparingInt(p -> p.getFriends()
.size()));
Note that you could also retrieve only the max of friends without the Person associated to :
OptionalInt maxFriends = people.getPeople()
.stream()
.mapToInt(p -> p.getFriends()
.size())
.max();
You can declare the following method in Person class:
public int getNumberOfFriends(){
return friends.size();
}
and then use it in a stream like this:
Optional <Person> personWithMostFriends = people.stream().max(Comparator.comparingInt(Person::getNumberOfFriends));
With this approach you will get the Person object with the most friends, not only the maximum number of friends as someone suggested.
Your question already answered. but i add this for one thing. if your list of person size large and if you have multi-core pc and you want to used this efficiently then use parallelstream().
To get person:
Person person = people.getPeople()
.parallelStream()
.max(Comparator.comparingInt(p-> p.getFriends().size()))
.orElse(null);
To get size:
int size = people.getPeople()
.parallelStream()
.mapToInt(p -> p.getFriends().size())
.max().orElse(0);

Add a value to a key in a hashmap if it satisfies a condition

Let us say I have a HashMap that stores a Fruit as its key and an ArrayList of Cars as its value.
A Fruit is an Object with a String name, int height, int width. A Car is an object with a String name, String FruitItRunsOver, int weight.
I am supposed to add Fruits and Cars that run over this Fruit into my HashMap. I am initially given Fruits sequentially and then I am given Cars sequentially.
Now, the problem I am having is that I know which fruit a Car runs over by the FruitItRunsOver member variable which, as you can probably guess, will be the same as the name of a Fruit. Initially, I am given the Fruits, and as I am given them, I put(Fruit, new ArrayList<Car>()).
However, once I get to the cars, I am wondering what is the best way to add them? I am tempted to have something that looks like this:
for (Fruit f : hashmap.keySet()) {
if (f.getName().equals(car.getFruit())) {
ArrayList<Car> cars = hashmap.get(f);
cars.add(car);
hashmap.put(f, cars)
}
}
.. But I think that looks egregious. I was thinking I could have the keys, instead of being the Fruits, be the fruit names. But keep in mind I need to have the other member variables of the fruit saved. I will need them later on in the code. So, what, I should have a nested HashMap? If so, how should the structure look like?
Thanks
Edit: I suppose this is my fault for not clarifying. car.getFruit() will return the fruit name FruitItRunsOver, not a Fruit object.
Make a Map<String,Fruit> to map fruit names to Fruit objects. This map could be temporary if you plan no further modifications to the fruit-to-cars map, or you could keep it for the duration of the program's run:
Map<String,Fruit> fruitByName = new HashMap<>();
for (Fruit fruit : allFruits) {
fruitByName.put(fruit.getName(), fruit);
}
Map<Fruit,List<Car>> carByFruit = new HashMap<>();
for (Car car : allCars) {
Fruit f = fruitByName.get(car.getFruit());
if (f == null) {
... // Throw an exception
}
List<Car> cars = carByFruit.get(f);
if (cars == null) {
cars = new ArrayList<Car>();
carByFruit.put(f, cars);
}
cars.add(car);
}
You could also make a Map<String,FruitAndItsCars>, with an additional FruitAndItsCars class to combine a Fruit object with a List<Car> object list.
For each Car you add, you iterate on fruits, it is not efficient and in a some way counter intuitive as you have a Map that provides a direct access by key.
It would be more efficient to use as key the same value that the car has as field to represent a fruit. Use the String FruitItRunsOver if it is suitable
Map<String, List<Car>> carsByFruitName = new HashMap<>();
// populate the map with Fruit name and empty ArrayList
..
// for each car to create, retrieve the List of Car from the String name representing the fruit
List<Car> cars = carsByFruitName.get(car.getFruitItRunsOver());
cars.add(car);
You can use computeIfAbsent:
The method computeIfAbsent receives as first argument the key and as second argument a Function (function of one argument). This function receives the key and is executed only when the key is not present, then it should return the value. In this case an empty list of cars the when the key does not exist, and the current list of cars when it does exist. Then the new car is added.
Fruit carFruit = getFruitByName(car.getFruit());
hashmap.computeIfAbsent(carFruit, (fruit) -> new ArrayList<>()).add(car);
This way you don't need a for loop, and you are already handling the case when the key does not exists, adding the new list.
To obtain the fruit by name, if you you only have a collection of Fruits you can create the map with java's stream, this map has a string with the name as key and the Fruit itseld as the value:
Map<String, Fruit> fruitByName = getFruits() // however that you get all the fruits
// e.g. hashmap.keySet()
.stream()
.collect(Collectors.toMap(Fruit::getName, Function.identity()));
Having this map Map<String, Fruit>, you could replace the method getFruitByName() with a simple call to fruitByName.get(fruitName).
note: you normally should have the equals() and hashcode() methods defined to use a object as key, the Fruit in this case.
Edit:
Using sugestion of Andy Turner

Group by multiple values

For example, I have a list of students with their semesters and subjects.
Subject Semester Attendee
---------------------------------
ITB001 1 John
ITB001 1 Bob
ITB001 1 Mickey
ITB001 2 Jenny
ITB001 2 James
MKB114 1 John
MKB114 1 Erica
When I need to group them by one value with Stream api, I can make something like that;
Map<String, List<Student>> studlistGrouped =
studlist.stream().collect(Collectors.groupingBy(w -> w.getSubject()));
and it does work. However, when I want to group them by two or more values (like the Sql handles), I cannot figure out how I need to do.
One approach is to group by a class that holds all the fields you want to group by. This class must implement hashCode and equals using all these fields in order to work as expected.
In your example this would be:
public class SubjectAndSemester {
private final String subject;
private final int semester; // an enum sounds better
public SubjectAndSemester(String subject, int semester) {
this.subject = subject;
this.semester = semester;
}
// TODO getters and setters, hashCode and equals using both fields
}
Then, you should create this method in your Student class:
public SubjectAndSemester bySubjectAndSemester() {
return new SubjectAndSemester(subject, semester);
}
Now, you can group as follows:
Map<SubjectAndSemester, List<Student>> studlistGrouped = studlist.stream()
.collect(Collectors.groupingBy(Student::bySubjectAndSemester));
This creates a one level grouping of students.
There's another option that doesn't require you to use a Tuple or to create a new class to group by. I mean you can have an n-level grouping of students, by using downstream groupingBy collectors:
Map<String, Map<Integer, List<Student>>> studlistGrouped = studlist.stream()
.collect(Collectors.groupingBy(Student::getSubject,
Collectors.groupingBy(Student::getSemester)));
This creates a 2-level grouping, so the returned structure is now a map of maps of lists. If you can live with this, even for grouping of more levels, then you can avoid creating a class that acts as the key of the map.
You could create a calculated field that is combination of the two columns
Map<String, List<Student>> studlistGrouped =
studlist.stream().collect(Collectors.groupingBy(w -> w.getSubject() + "-" w.getAttendee()));
EDIT:
Another more "appropriate" solution: Accoridng to this blog post , you need to define a Tuple class that can hold the two columns:
class Tuple {
String subject;
String attendee;
}
Map<String, List<Student>> studlistGrouped =
studlist.stream().collect(Collectors.groupingBy(w -> new Tuple(w.getSubject(), w.getAttendee())));

Compare two lists of unequal lengths and remove partial matches?

Lets say I have two lists like:
List1 = Fulton Tax Commissioner 's Office, Grady Hospital, Fulton Health Department
List2 = Atlanta Police Department, Fulton Tax Commissioner, Fulton Health Department,Grady Hospital
I want my final list to look like this:
Final List = Fulton Tax Commissioner 's Office,Grady Hospital,Fulton Health Department,Atlanta Police Department
I can remove duplicates from these lists by adding both the lists to a set. But how do I remove partial matches like Fulton Tax Commissioner?
I suggest: Set the result to a copy of list 1. For each member of list 2:
If the result contains the same member, skip it.
If the result contains a member that starts with the list 2 member, also skip the list 2 member
If the result contains a member that is a prefix of the list 2 member, replace it by the list 2 member
Otherwise add the list 2 member to the result.
If using Java 8, the tests in the 2nd and 3rd bullets can be conveniently done with streams, for example result.stream().anyMatch(s -> s.startsWith(list2Member));.
There is room for optimization, for example using a TreeSet (if it’s OK to sort the items).
Edit: In Java:
List<String> result = new ArrayList<>(list1);
for (String list2Member : list2) {
if (result.stream().anyMatch(s -> s.startsWith(list2Member))) { // includes case where list2Member is in result
// skip
} else {
OptionalInt resultIndex = IntStream.range(0, result.size())
.filter(ix -> list2Member.startsWith(result.get(ix)))
.findAny();
if (resultIndex.isPresent()) {
result.set(resultIndex.getAsInt(), list2Member);
} else {
result.add(list2Member);
}
}
}
The result is:
[Fulton Tax Commissioner 's Office, Grady Hospital, Fulton Health Department, Atlanta Police Department]
I believe this exactly the result you asked for.
Further edit: In Java 9 you may use (not tested):
resultIndex.ifPresentOrElse(ix -> result.set(ix, list2Member), () -> result.add(list2Member));
Add to set by passing a comparator, like below:
Set s = new TreeSet(new Comparator() {
#Override
public int compare(Object o1, Object o2) {
// add the logic to say that partial match is considered same.
}
});
s.addAll(yourList);

java: Collect and combine data in a list

In my program I have a List of Plants, each plant has a measurement (String), day (int), camera (int), and replicate number(int). I obtain a List of all plants wanted by using filters:
List<Plant> selectPlants = allPlants.stream().filter(plant -> passesFilters(plant, filters)).collect(Collectors.toList());
What I would like to do now is take all Plants that have the same camera, measurement, and replicate values. And combine them in order of day. So if I have days 1,2,3,5 I would want to find all similar plants and append the values to one plant where the getValues (function).
I added a method to Plant that appends values by just using addAll( new plant values ).
Is there any way of doing this without iterating through the list over and over to find the similar plants, and then sorting each time by day then appending? I'm sorry for the horrible wording of this question.
While Vakh’s answer is correct, it is unnecessarily complex.
Often, the work of implementing your own key class does not pay off. You can use a List as a key which implies a slight overhead due to boxing primitive values but given the fact that we do operations like hashing here, it will be negligible.
And sorting doesn’t have to be done by using a for loop and, even worse, an anonymous inner class for the Comparator. Why do we have streams and lambdas? You can implement the Comparator using a lambda like (p1,p2) -> p1.getDay()-p2.getDay() or, even better, Comparator.comparing(Plant::getDay).
Further, you can do the entire operation in one step. The sort step will create an ordered stream and the collector will maintain the encounter order, so you can use one stream to sort and group:
Map<List<?>, List<Plant>> groupedPlants = allPlants.stream()
.filter(plant -> passesFilters(plant, filters))
.sorted(Comparator.comparing(Plant::getDay))
.collect(Collectors.groupingBy(p ->
Arrays.asList(p.getMeasurement(), p.getCamera(), p.getReplicateNumber())));
That’s all.
Using Collectors.groupBy:
private static class PlantKey {
private String measurement;
private int camera;
private int replicateNumber;
// + constructor, getters, setters and haschode equals
}
Map<PlantKey, List<Plant>> groupedPlants =
allPlants.stream().filter(plant -> passesFilters(plant, filters))
.collect(Collectors.groupBy(p ->
new PlantKey(p.getMeasurement(),
p.getCamera(),
p.getReplicateNumber())));
// order the list
for(List values : groupedPlants.values()) {
Collections.sort(values, new Comparator<Plant>(){
#Override
public int compare(Plant p1, Plant p2) {
return p1.getDay() - p2.getDay();
}
});
}
I would group them by the common characteristics and compare similar results.
for(List<Plant> plantGroup : allPlants.stream().collect(Collectors.groupingBy(
p -> p.camera+'/'+p.measurement+'/'+p.replicate)).values()) {
// compare the plants in the same group
}
There is a function called sorted which operates on a stream
selectPlants.stream().sorted(Comparator.comparingInt(i -> i.day)).collect(Collectors.toList());

Categories