Streams were introduced many years ago; however, as Java developers, many of us still haven’t fully harnessed the power of this versatile tool. In this article, you will find some valuable tricks that can be saved as a reference for incorporating into your next project.
In the examples that follow, I will be using these classes:
@Getter
class Company {
private String name;
private Address address;
private List<Person> personList;
}
@Getter
class Person {
private Long id;
private String name;
}
@Getter
class Address {
private String street;
private City city;
}
@Getter
class City {
private String name;
private State state;
}
@Getter
class State{
private String name;
}
1. Simplify map by using method reference
Here is the code that retrieves the city names from the companies' addresses:
public List<String> getCityNames(List<Company> companyList){
return companyList.stream()
.map(company -> company.getAddress().getCity().getName())
.toList();
}
Can be replaced by this to be more readable:
public List<String> getCityNames(List<Company> companyList){
return companyList.stream()
.map(Company::getAddress)
.map(Address::getCity)
.map(City::getName)
.toList();
}
2. Null checks
The code above with null checks:
public List<String> getCityNames(List<Company> companyList){
return companyList.stream()
.map(Company::getAddress)
.filter(Objects::nonNull)
.map(Address::getCity)
.filter(Objects::nonNull)
.map(City::getName)
.filter(Objects::nonNull)
.toList();
}
3. Stream of stream to stream
The code below retrieves the list of individuals from every company.
public List<Person> getAllPerson(List<Company> companyList){
// Make a list of list of Person
List<List<Person>> partialResult = companyList.stream()
.map(Company::getPersonList)
.toList();
// For each list of Person, add to the result
List<Person> result = new ArrayList<>();
partialResult.forEach(result::addAll);
return result;
}
The same can be done as follow:
public List<Person> getAllPerson(List<Company> companyList){
return companyList.stream()
.map(Company::getPersonList) // It returns a Stream<List<Person>>
.flatMap(List::stream) // It returns a Stream<Person>
.toList();
}
4. Grouping by an attribute
The code below generates a Map that associates each city with a List of Company.
public Map<City,List<Company>> getCompaniesByCity(List<Company> companyList){
return companyList.stream()
.collect(Collectors.groupingBy(company -> company.getAddress().getCity()));
}
5. Verify the presence of an item in the stream.
The code below verifies the presence of a company in a specific city:
public boolean hasCompanyInCity(List<Company> companyList, String cityName){
return companyList.stream()
.map(Company::getAddress)
.map(Address::getName)
.anyMatch(cityName::equals);
}
The same principle applies to noneMatch when you need to verify that no companies exist in a particular city:
public boolean hasNoCompanyInCity(List<Company> companyList, String cityName){
return companyList.stream()
.map(Company::getAddress)
.map(Address::getName)
.noneMatch(cityName::equals);
}
6. Logging
Utilize the peek method to generate a log for each city name that is returned:
public List<String> getCityNames(List<Company> companyList){
return companyList.stream()
.map(Company::getAddress)
.map(Address::getCity)
.map(City::getName)
.peek(cityName -> log.info(cityName))
.toList();
}
7. Get unique city names
Employ distinct to eliminate any repeated city names from a stream:
public List<String> getUniqueCityNames(List<Company> companyList){
return companyList.stream()
.map(Company::getAddress)
.map(Address::getCity)
.map(City::getName)
.distinct()
.toList();
}