Lambda Usage
Java 8 Functional Programming
NOTES: All content is excerpted from Java 8 Functional Programming
Refactoring Legacy Code
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames=new HashSet<>();
for(Album album : albums) {
for (Track track : album.getTrackList()) {
if (track.getLength() > 60) {
String name=track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
Refactoring 1: Change for
to forEach
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames=new HashSet<>();
albums.stream()
.forEach(album-> {
album.getTracks()
.forEach(track-> {
if (track.getLength() > 60) {
String name=track.getName();
trackNames.add(name);
}
});
});
return trackNames;
}
Refactoring 2: Change if
to filter
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames=new HashSet<>();
albums.stream()
.forEach(album-> {
album.getTracks()
.filter(track-> track.getLength() > 60)
.map(track-> track.getName())
.forEach(name-> trackNames.add(name));
});
return trackNames;
}
Refactoring 3: Change outermost forEach
to flatMap
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames=new HashSet<>();
albums.stream()
.flatMap(album-> album.getTracks())
.filter(track-> track.getLength() > 60)
.map(track-> track.getName())
.forEach(name-> trackNames.add(name));
return trackNames;
}
Refactoring 4: Change forEach
element addition to collect
for collection
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap(album-> album.getTracks())
.filter(track-> track.getLength() > 60)
.map(track-> track.getName())
.collect(toSet());
}
Function Method Parameter Overloading
overloadedMethod((x, y)-> x+y);
private interface IntegerBiFunction extends BinaryOperator<Integer> {
}
private void overloadedMethod(BinaryOperator<Integer> Lambda) {
System.out.print("BinaryOperator");
}
private void overloadedMethod(IntegerBiFunction Lambda) {
System.out.print("IntegerBinaryOperator");
}
These methods accept
BinaryOperator
and a subclass of that interface as parameters respectively. When calling these methods, the type of Lambda expression that Java infers is exactly the type of the most specific functional interface. For example, the example above outputsIntegerBinaryOperator
when choosing between the two methods.
General Principles
-
If there is only one possible target type, it is inferred from the parameter types in the corresponding functional interface;
-
If there are multiple possible target types, it is inferred from the most specific type;
-
If there are multiple possible target types and the most specific type is unclear, manual type specification is required.
Interface Multiple Inheritance
public interface Jukebox {
public default String rock() {
return "... all over the world! ";
}
}
public interface Carriage {
public default String rock() {
return "... from side to side";
}
}
public class MusicalCarriage implements Carriage, Jukebox {
}
javac
is not clear which interface method should be inherited, so the compiler reports an error: class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox.
public class MusicalCarriage
implements Carriage, Jukebox {
@Override
public String rock() {
return Carriage.super.rock();
}
}
General Principles
-
Classes win over interfaces. If there are method bodies or abstract method declarations in the inheritance chain, then methods defined in interfaces can be ignored.
-
Subclasses win over parent classes. If an interface inherits from another interface, and both interfaces define a default method, then the method defined in the subclass wins.
-
There is no rule three. If the above two rules don't apply, the subclass must either implement the method or declare the method as abstract.
- For example, when using parallel streams, the
forEach
method cannot guarantee that elements are processed in order. If you need to guarantee order processing, you should use theforEachOrdered
method.
Sub-collector groupingBy
public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
return albums.collect(groupingBy(Album::getMainMusician, mapping(Album::getName, toList())));
}
mapping allows performing map-like operations on the collector's container. But you need to specify what kind of collection class to use to store the results, such as toList
.
Refactoring Domain Methods
public long countFeature(ToLongFunction<Album> function) {
return albums.stream()
.mapToLong(function)
.sum();
}
public long countTracks() {
return countFeature(album-> album.getTracks().count());
}
public long countRunningTime() {
return countFeature(album-> album.getTracks()
.mapToLong(track-> track.getLength())
.sum());
}
public long countMusicians() {
return countFeature(album-> album.getMusicians().count());
}
Tips
-
It's simple to determine whether an operation is lazy evaluation or eager evaluation: just look at its return value. If the return value is Stream, then it's lazy evaluation; if the return value is another value or empty, then it's eager evaluation.
-
After extracting lambda logic into a method, you can test that method and cover all edge cases
-
Use the peek method to observe intermediate values in lambda
Set<String> nationalities = album.getMusicians()
.filter(artist-> artist.getName().startsWith("The"))
.map(artist-> artist.getNationality())
.peek(nation-> System.out.println("Found nationality: "+nation))
.collect(Collectors.<String>toSet());