I need to find a method (using streams) to return a Map<Category,Optional<ToDo>, which help me group an ArrayList and give me a ToDo object with the highest priority of each category.
public record ToDo(String name, Category category,
int priority, LocalDate date) {}
public enum Category { HOME, WORK }
An example of the input data:
List<ToDo> todo = List.of(
new ToDo("Eat", Category.HOME, 1, LocalDate.of(2022, 8, 29)),
new ToDo("Sleep", Category.HOME, 2, LocalDate.of(2022, 8, 30)),
new ToDo("Learn", Category.WORK, 2, LocalDate.of(2022, 9, 3)),
new ToDo("Work", Category.WORK, 3, LocalDate.of(2022, 10, 3))
);
And in the end, I want to have something like this as a result:
{HOME=[ToDo{Description='Eat', category=HOME, priority=1, deadline=2022-08-29},
WORK=[ToDo{Description='Learn', category=WORK, priority=2, deadline=2022-09-03]}
I was trying to use
.collect(Collectors.groupingBy(p -> p.getCategory()));
and
.sorted(Comparator.comparing(ToDo::getPriority)).findFirst();
But I can't do it in a single method and get Optional as a result. How can I resolve this problem?
The practice of storing Optionals in a Collection is discouraged.
It might seem as a smart move at first. But in fact you're creating a Map which give you a null or potentially empty Optional, which doesn't sound very handy.
Besides that it goes against the design goal of Optional which is intended to be used as a return type. Optional is meant only for transitioning data (not storing), for that reason it was designed non-serializable, and it might cause issues.
And for every category that would be encountered in the list, there always would be a corresponding ToDo object. If your intention was to have all members of Category in the map in order to be able to safely fire an action on the Optional returned by get() via ifPresent(), then instead you can implement Null-object pattern and map your null-object ToDo to every category that wasn't present in the list via putIfAbsent().
If you want to find ToDo with the highest priority (lowest value of priority) using collector groupingBy() as you've mentioned in the question. Then you can use collector minBy() in conjunction with a collector collectingAndThen() as a downstream of groupingBy(). It would be way more efficient than combination .sorted().findFirst().
But since we need a single value mapped to each key (and not a collection of values) as #Holger has pointed out, the proper way of handling this task is by using collector toMap() instead of groupingBy() + downstream collectors. It results in less verbose and more intuitive code.
List<ToDo> todo = List.of(
new ToDo("Eat", Category.HOME, 1, LocalDate.of(2022, 8, 29)),
new ToDo("Sleep", Category.HOME, 2, LocalDate.of(2022, 8, 30)),
new ToDo("Learn", Category.WORK, 2, LocalDate.of(2022, 9, 3)),
new ToDo("Work", Category.WORK, 3, LocalDate.of(2022, 10, 3))
);
Map<Category, ToDo> highestPriorityTaskByCategory = todo.stream()
.collect(Collectors.toMap(
ToDo::category,
Function.identity(),
BinaryOperator.minBy(Comparator.comparingInt(ToDo::priority))
));
highestPriorityTaskByCategory.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
WORK -> ToDo[name=Learn, category=WORK, priority=2, date=2022-09-03]
HOME -> ToDo[name=Eat, category=HOME, priority=1, date=2022-08-29]
I assume that the DOM should be HOME, because that's the category inside your object. I also assume that the ToDoList should be an Optional<ToDo>.
The groupingBy you used will return a Map<Category, List<ToDo>>. That's the right key but the wrong value. You need to solve that by also supplying a downstream collector, that will collect ToDo elements:
Map<Category, Optional<ToDo>> result = todo.stream()
.collect(Collectors.groupingBy(
ToDo::getCategory,
Collectors.minBy(Comparator.comparingInt(ToDo::getPriority))
));
You can improve the comparator to Comparator.comparingInt(ToDo::getPriority).thenComparing(ToDo::getDeadline) to find the entry with the earliest deadline in case some have the same lowest priority.
Note that this will only include entries for categories that are part of your input. If you need all, use an additional loop:
for (Category category : Category.values()) {
result.computeIfAbsent(category, c -> Optional.empty());
}
Related
I have two custom lists as follows.
List<OfficeName> = [{id: 1, offname: "Office1"}{id: 2, offname: "Office2"}]
List<OfficeLocation> = [{id: 1, offlocation: "location1"}{id: 2, offlocation: "locatio2"}]
I want result as follows:
list<OfficeDetails> = [{id: 1, offname: "Office1",offlocation: "location1" },
{id: 2, offname: "Office2", offlocation: "location2"}]
The first two lists needs to be joined on basis of "id" to give a new list which is equivalent to the join operation in sql tables.
My model classes are
public class OfficeName {
int id;
String offname;
//getter and setter
}
.................
public class OfficeLocation{
int id;
String offlocation;
//getter and setter
}
.........
Currently I am Iterating and manually adding as follows to a LinkedHashSet .
{
List<OfficeName> officeName = new ArrayList<OfficeName>();
onr.findById(id).forEach(officeName::add); // adding values from auto wired Repository
List<OfficeLocation> officeLocation = new ArrayList<OfficeLocation>();
olr.findById(id).forEach(officeLocation::add); // adding values from auto wired Repository
LinkedHashSet<LinkedHashSet<String>> lhs = new LinkedHashSet<LinkedHashSet<String> >();
OfficeName officeName1 = new OfficeName();
OfficeLocation officeLocation1 = new OfficeLocation();
Iterator<OfficeName> onIterator = officeName.iterator();
Iterator<OfficeLocation> olIterator = officeLocation.iterator();
while (onIterator.hasNext()) {
officeName1 =onIterator.next();
int idon =officeName1.getId();
while(olIterator.hasNext()){
officeLocation1 = olIterator.next();
int idol = officeLocation1.getId();
if(idon==idol)
{
lhs.add(new LinkedHashSet<String>(Arrays.asList( String.valueOf(officeName1.getId()),officeName1.getOffname(),officeLocation1.getOfflocation())));
olIterator.remove();
break;
}
};
}
I am not sure whether this is correct way to achieve the same as I am new to java. In C#, this could able to achieve through data tables. Please suggest whether there is any faster way?
Assuming both input lists:
Are distinct, with no duplicate id values in either, and…
Are complete, with a single object in both lists for each possible id value
… then we can get the work done with little code.
I use NavigableSet or SortedSet implementations to hold our input lists, the names and the locations. Though I have not verified, I assume being sorted will yield better performance when searching for a match across input collections.
To get the sorting done, we define a Comparator for each input collection: Comparator.comparingInt( OfficeName :: id ) & Comparator.comparingInt( OfficeLocation :: id ) where the double-colons make a method reference. To each NavigableSet we add the contents of our inputs, an unmodifiable list made with the convenient literals syntax of List.of.
To get the actual work done of joining these two input collections, we make a stream of either input collection. Then we produce a new object of our third joined class using inputs from each element of the stream plus its counterpart found via a stream of the other input collection. These newly produced objects of the third joined class are then collected into a list.
NavigableSet < OfficeName > officeNames = new TreeSet <>( Comparator.comparingInt( OfficeName :: id ) );
officeNames.addAll( List.of( new OfficeName( 1 , "Office1" ) , new OfficeName( 2 , "Office2" ) ) );
NavigableSet < OfficeLocation > officeLocations = new TreeSet <>( Comparator.comparingInt( OfficeLocation :: id ) );
officeLocations.addAll( List.of( new OfficeLocation( 1 , "location1" ) , new OfficeLocation( 2 , "locatio2" ) ) );
List < Office > offices = officeNames.stream().map( officeName -> new Office( officeName.id() , officeName.name() , officeLocations.stream().filter( officeLocation -> officeLocation.id() == officeName.id() ).findAny().get().location() ) ).toList();
Results:
officeNames = [OfficeName[id=1, name=Office1], OfficeName[id=2, name=Office2]]
officeLocations = [OfficeLocation[id=1, location=location1], OfficeLocation[id=2, location=locatio2]]
offices = [Office[id=1, name=Office1, location=location1], Office[id=2, name=Office2, location=locatio2]]
Our three classes, the two inputs and the third joined one, are all written as records here for their convenient brevity. This Java 16+ feature is a brief way to declare a class whose main purpose is to communicate data transparently and immutably. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString. Note that a record can be defined locally as well as nested or separate.
public record OfficeName( int id , String name ) { }
public record OfficeLocation( int id , String location ) { }
public record Office( int id , String name , String location ) { }
Given the conditions outlined above, we could optimize by hand-writing loops to manage the matching of objects across the input collections, rather than using streams. But I would not be concerned about the performance impact unless you had huge amounts of data that had proven to be a bottleneck. Otherwise, using streams makes for less code and more fun.
One of the lists (e.g. locations) should be converted into a map (HashMap) by a key on which the joining should be made, in this case id field.
Then, assuming that OfficeDetails class has an all-args constructor, the resulting list may be retrieved by streaming the other list offices and mapping its contents into new OfficeDetails, filling the remaining location argument by looking up the map.
List<OfficeName> offices = Arrays.asList(
new OfficeName(1, "Office1"), new OfficeName(2, "Office2"), new OfficeName(3, "Office3")
);
List<OfficeLocation> locations = Arrays.asList(
new OfficeLocation(1, "Location 1"), new OfficeLocation(2, "Location 2"), new OfficeLocation(4, "Location 4")
);
Map<Integer, OfficeLocation> mapLoc = locations
.stream()
.collect(Collectors.toMap(
OfficeLocation::getId,
loc -> loc,
(loc1, loc2) -> loc1 // to resolve possible duplicates
));
List<OfficeDetails> details = offices
.stream()
.filter(off -> mapLoc.containsKey(off.getId())) // inner join
.map(off -> new OfficeDetails(
off.getId(), off.getOffname(),
mapLoc.get(off.getId()).getOfflocation() // look up the map
))
.collect(Collectors.toList());
details.forEach(System.out::println);
Output (assuming toString is implemented in OfficeDetails):
{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}
If offices list is not filtered by condition mapLoc.containsKey, an implementation of LEFT JOIN is possible (when null locations are stored in the resulting OfficeDetails).
To implement RIGHT JOIN (with null office names and all available locations), a lookup map should be created for offices, and main iteration has to be run for locations list.
To implement FULL JOIN (where either name or location parts of OfficeDetails can be null), two maps need to be created and then joined:
Map<Integer, OfficeName> mapOff = offices
.stream()
.collect(Collectors.toMap(
OfficeName::getId,
off -> off,
(off1, off2) -> off1, // to resolve possible duplicates
LinkedHashMap::new
));
List<OfficeDetails> fullDetails = Stream.concat(mapOff.keySet().stream(), mapLoc.keySet().stream())
.distinct()
.map(id -> new OfficeDetails(
id,
Optional.ofNullable(mapOff.get(id)).map(OfficeName::getOffname).orElseGet(()->null),
Optional.ofNullable(mapLoc.get(id)).map(OfficeLocation::getOfflocation).orElseGet(()->null)
))
.collect(Collectors.toList());
fullDetails.forEach(System.out::println);
Output:
{id: 1, offname: "Office1", offlocation: "Location 1"}
{id: 2, offname: "Office2", offlocation: "Location 2"}
{id: 3, offname: "Office3", offlocation: null}
{id: 4, offname: null, offlocation: "Location 4"}
I have the following problem:
I want to remove duplicate data from a list of a Vo depending if the registered field is the same, I show you the solution that I am trying. Then this is the data from the list that I am making
List<MyVo> dataList = new ArrayList<MyVo>();
MyVo data1 = new MyVo();
data1.setValidated(1);
data1.setName("Fernando");
data1.setRegistered("008982");
MyVo data2 = new MyVo();
data2.setValidated(0);
data2.setName("Orlando");
data2.setRegistered("008986");
MyVo data3 = new MyVo();
data3.setValidated(1);
data3.setName("Magda");
data3.setRegistered("008982");
MyVo data4 = new MyVo();
data4.setValidated(1);
data4.setName("Jess");
data4.setRegistered("006782");
dataList.add(data1);
dataList.add(data2);
dataList.add(data3);
dataList.add(data4);
The first thing I have to do and separate it into two different lists depending on whether the data is validated or not, for that the value of the registered validated.
List<MyVo> registeredBusinesses = new ArrayList<MyVo>();
List<MyVo> unregisteredBusinesses = new ArrayList<MyVo>();
for (MyVo map : dataList) {
if (map.getValidated == 0) {
unregisteredBusinesses.add(map);
}else {
registeredBusinesses.add(map);
}
}
now the list of registered businesses I want to remove the data that is repeated with the same value from its registered field and make a new list. this is what it took but it doesn't work right
List<MyVo> duplicateList = registeredBusinesses.stream().filter(distictByRegistered(MyVo::getRegistered)).collect(Collectors.toList());
public static <T> Predicate<T> distictByRegistered(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
however using this method I get the following output:
{["validated":1,"name":"Fernando","registered":"008982"],
["validated":1,"name":"Jess","registered":"006782"]}
the output I want to obtain is the following:
the unregisteredBusinesses list:
{["validated":0,"name":"Orlando","registered":"008986"]}
the registeredBusinesses list:
{["validated":1,"name":"Jess","registered":"006782"]}
the registeredDuplicateBusinesses list:
{["validated":1,"name":"Fernando","registered":"008982"],
["validated":1,"name":"Magda","registered":"008982"]}
I don't know how to do it, could you help me? I would like to use lambdas to reduce the code, for example of the first for when I separate into two lists
You are looking for both registered and unregistered businesses. This is where instead of making use of 0 and 1, you could choose to implement the attribute as a boolean isRegistered such as 0 is false and 1 is true going forward. Your existing code with if-else could be re-written as :
Map<Boolean, List<MyVo>> partitionBasedOnRegistered = dataList.stream()
.collect(Collectors.partitioningBy(MyVo::isRegistered));
List<MyVo> unregisteredBusinesses = partitionBasedOnRegistered.get(Boolean.FALSE); // here
List<MyVo> registeredBusinesses = partitionBasedOnRegistered.get(Boolean.TRUE);
Your approach looks almost correct, grouping by Function.identity() will properly flag duplicates (based on equals() implementation!), you could also group by an unique property/id in your object if you have one, what you're missing is to manipulate the resulting map to get a list with all duplicates. I've added comments describing what's happening here.
List<MyVo> duplicateList = registeredBusinesses.stream()
.collect(Collectors.groupingBy(Function.identity()))
.entrySet()
.stream()
.filter(e -> e.getValue().size() > 1) //this is a stream of Map.Entry<MyVo, List<MyVo>>, then we want to check value.size() > 1
.map(Map.Entry::getValue) //We convert this into a Stream<List<MyVo>>
.flatMap(Collection::stream) //Now we want to have all duplicates in the same stream, so we flatMap it using Collections::stream
.collect(Collectors.toList()); //On this stage we have a Stream<MyVo> with all duplicates, so we can collect it to a list.
Additionally, you could also use stream API to split dataList into registered and unRegistered.
First we create a method isUnregistered in MyVo
public boolean isUnregistered() {
return getrRegistered() == 0;
}
Then
Map<Boolean, List<MyVo>> registeredMap = dataList.stream().collect(Collectors.groupingBy(MyVo::isUnregistered));
Where map.get(true) will be unregisteredBusinesses and map.get(false) registeredBusinesses
Familiarizing yourself with the concept of the Collectors.partitioningBy shall help you problem-solve this further. There are two places amongst your current requirement where it could be implied.
You are looking for both registered and unregistered businesses. This is where instead of making use of 0 and 1, you could choose to implement the attribute as a boolean isRegistered such as 0 is false and 1 is true going forward. Your existing code with if-else could be re-written as :
Map<Boolean, List<MyVo>> partitionBasedOnRegistered = dataList.stream()
.collect(Collectors.partitioningBy(MyVo::isRegistered));
List<MyVo> unregisteredBusinesses = partitionBasedOnRegistered.get(Boolean.FALSE); // here
List<MyVo> registeredBusinesses = partitionBasedOnRegistered.get(Boolean.TRUE);
After you try to groupBy the registered businesses based on the registration number(despite of identity), you require both the duplicate elements and the ones which are unique as well. Effectively all entries, but again partitioned into two buckets, i.e. one with value size == 1 and others with size > 1. Since grouping would ensure, minimum one element corresponding to each key, you can collect the required output with an additional mapping.
Map<String, List<MyVo>> groupByRegistrationNumber = // group registered businesses by number
Map<Boolean, List<List<MyVo>>> partitionBasedOnDuplicates = groupByRegistrationNumber
.entrySet().stream()
.collect(Collectors.partitioningBy(e -> e.getValue().size() > 1,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
If you access the FALSE values of the above map, that would provide you the groupedRegisteredUniqueBusiness and on the other hand values against TRUE key would provide you groupedRegisteredDuplicateBusiness.
Do take a note, that if you were to flatten this List<List<MyVo> in order to get List<MyVo> as output, you could also make use of the flatMapping collector which has a JDK inbuilt implementation with Java-9 and above.
I have a table in sql of doctor names and their clients
Each doctor has multiple clients
And one client can visit multiple do doctors
array and a simple table
[
{doctor="illies",client=4},
{doctor="illies",client=7},
{doctor="illies",client=1},
{doctor="houari",client=5},
{doctor="abdou",client=1},
{doctor="illies",client=2},
{doctor="abdou",client=1},
]
These data are already ordered So the task is To teach client know it's place in the queue
For example
The client with ID 1 Is in the third place in the doctor "illies"
And he's in the first place in the doctor "abdou"
I don't know if I explain it to you well A friend of mine suggest me to
Rearrange the array to a nested array like this (well this array is not totally correct but i has the idea)
[doctor="abdou" => clients=[cleint1="1",client2="2" ], doctor="illies"=>clients=[...] ]
now i just need an idea that could help me with my projet , all this work it to display the queue of the client (the position of the client in the doctor's queue), and thank you so much.
It seems that each row in the input array can be presented as a class like this:
class DocClient {
private String doctor;
private int client;
public String getDoctor() { return this.doctor; }
public int getClient() { return this.client; }
}
Then array or list of <DocClient> needs to be converted not into the "nested array" but into the map where doctor is used as a key, and the value is list of clients: Map<String, Integer> docClients.
This map can be conveniently built using Java Stream API using collectors Collectors.groupingBy and Collector.mapping:
List<DocClient> list = Arrays.asList(
new DocClient("illies", 4), new DocClient("illies", 4), new DocClient("illies", 1),
new DocClient("houari", 5), new DocClient("abdou", 1), new DocClient("illies", 2),
new DocClient("abdou", 2)
);
Map<String, List<Integer>> map = list
.stream()
.collect(Collectors.groupingBy(
DocClient::getDoctor, // use doctor as key via reference to getter
Collectors.mapping(
DocClient::getClient, // use `client` field
Collectors.toList() // convert to list
) // List<Integer> is value in map entry
));
// print the map
// map.forEach((doc, clients) -> System.out.printf("%s -> %s%n", doc. clients));
I am posting my query after having searched in this forum & google, but was unable to resolve the same.
eg: Link1 Link2 Link3
I am trying to filter List 2 (multi column) based on the values in List 1.
List1:
- [Datsun]
- [Volvo]
- [BMW]
- [Mercedes]
List2:
- [1-Jun-1995, Audi, 25.3, 500.4, 300]
- [7-Apr-1996, BMW, 35.3, 250.2, 500]
- [3-May-1996, Porsche, 45.3, 750.8, 200]
- [2-Nov-1998, Volvo, 75.3, 150.2, 100]
- [7-Dec-1999, BMW, 95.3, 850.2, 900]
expected o/p:
- [7-Apr-1996, BMW, 35.3, 250.2, 500]
- [2-Nov-1998, Volvo, 75.3, 150.2, 100]
- [7-Dec-1999, BMW, 95.3, 850.2, 900]
Code
// List 1 in above eg
List<dataCarName> listCarName = new ArrayList<>();
// List 2 in above eg
List<dataCar> listCar = new ArrayList<>();
// Values to the 2 lists are populated from excel
List<dataCar> listOutput = listCar.stream().filter(e -> e.getName().contains("BMW")).collect(Collectors.toList());
In the above code if I provide a specific value I can filter, but not sure how to check if Car Name in List 2 exits in List 1.
Hope the issue I face is clear, await guidance (Am still relatively new to Java, hence forgive if the above query is very basic).
Edit
I believe the link-3 provided above should resolve, but in my case it is not working. Maybe because the values in list-1 are populated as
org.gradle04.Main.Cars.dataCarName#4148db48 .. etc.
I am able to get the value in human readable format only when I do a forEach on List 1 by calling the getName method.
It's not clear why you have a List<DataCarName> in first place instead of a List/Set<String>.
The predicate you have to provide must check if for the corresponding data car instance, there's its name in the list.
e -> e.getName().contains("BMW") will only check if the name of the data car contains BMW which is not what you want. Your first attempt then may be
e -> listCarName.contains(e.getName())
but since listCarName is a List<DataCarName> and e.getName() a string (I presume), you'll get an empty list as a result.
The first option you have is to change the predicate so that you get a stream from the list of data car names, map them to their string representation and check that any of these names corresponds to the current data car instance's name you are currently filtering:
List<DataCar> listOutput =
listCar.stream()
.filter(e -> listCarName.stream().map(DataCarName::getName).anyMatch(name -> name.equals(e.getName())))
.collect(Collectors.toList());
Now this is very expensive because you create a stream for each instance in the data car stream pipeline. A better way would be to build a Set<String> with the cars' name upfront and then simply use contains as a predicate on this set:
Set<String> carNames =
listCarName.stream()
.map(DataCarName::getName)
.collect(Collectors.toSet());
List<DataCar> listOutput =
listCar.stream()
.filter(e -> carNames.contains(e.getName()))
.collect(Collectors.toList());
in your DataCar type, does getName() return a String or the DataCarName enum type? If it is the enum, you might follow Alexis C's approach but instead of building a HashSet using Collectors.toSet(), build an EnumSet, which gives O(1) performance. Modifying Alexis' suggestion, the result would look like:
Set<DataCarName> carNames =
listCarName.stream()
.collect(Collectors.toCollection(
()-> EnumSet.noneOf(DataCarName.class)));
List<DataCar> listOutput =
listCar.stream()
.filter(car -> carNames.contains(car.getName()))
.collect(Collectors.toList());
Try this:
SortedMap<String, Account> accountMap, List<AccountReseponse> accountOwnersList
List<Map.Entry<String, Account>> entryList = accountMap.entrySet().stream().filter(account -> accountOwnersList.stream()
.anyMatch(accountOwner -> accountOwner.getAccount()
.getIdentifier().equals(account.getValue().getIdentifier())))
.collect(Collectors.toList());
Can also use .noneMatch().
#Alexis'a answer is nice, but I have another way around to get use of performance from Map and improve the part you do listCarName.stream().map(DataCarName::getName).anyMatch(name -> name.equals(e.getName())) for each item, first I make a map from listCar and making the key with the field that I want to compare, in this instance is car's name and filter out null values when I map the list1 to be CarData.
So it should be something like:
final Map<String, CarData> allCarsMap = listCar // Your List2
.stream().collect(Collectors.toMap(CarData::getName, o -> o));
final List<CarData> listOutput = // Your expected result
listCarName // Your List1
.stream()
.map(allCarsMap::get) // will map each name with a value in the map
.filter(Objects::nonNull) // filter any null value for any car name that does not exist in the map
.collect(Collectors.toList());
I hope this helps, maybe a little better performance in some scenarios?
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());