We've just decided to switch from Java 6 to Java 8 for new projects. The actual reason was that Java 6 is not really supported anymore which starts with problems on downloading the installer from Oracles page.
So now now that I've worked with Java 8 for some weeks, I found some of the new features very useful to avoid writing ugly boilerplate code.
Lambdas
Instead of doing this:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("I'm running!");
}
});
you can simplify that by writing this:
Thread t = new Thread(() -> System.out.println("I'm running!"););
Under the hood, Java is doing almost the same by using type inference to find out that this expects an interface with one void method with no paramters. by saying ()
your create a new anonymous function that received no arguments.
You can write multi-line lambdas by adding curly brackets:
Thread t = new Thread(() -> {
System.out.println("I'm running!");
System.out.println("Yes, I do");
});
You can even create function and store them in variables:
Function<Integer, Integer> addOne = (baseValue) -> baseValue + 1;
int i = addOne.apply(2); // i == 3
However, from the syntax you see that functions are not first-class citizens in Java. You cant do this:
int i = addOne(2)
or this
int i = ((int baseValue) -> baseValue + 1)(2);
Optional
It is actually a bad practice to use null as a value anywhere (there are people with other options about that and there might be some use cases that actually makes sense, but lets just assume it doesn't). So Java 8 provides a nice little generic class called Optional<T>
. Now how does that help?
Instead of doing this:
String toPositiveString(int number) {
String numberStr = null;
if(number >= 0) {
numberStr = Integer.toString(number);
}
return numberStr;
}
void main() {
String positiveStr = toPositiveString(-1);
if(null != positiveStr) {
System.out.println(positiveStr.substring(0,1));
}
}
you can write this:
Optional<String> toPositiveString(int number) {
String numberStr = null;
if(number >= 0) {
numberStr = Integer.toString(number);
}
return Optional.ofNullable(numberStr);
}
void main() {
Optional<String> positiveStr = toPositiveString(-1);
positiveStr.ifPresent(str -> System.out.println(str.substring(0,1)));
}
The method toPositiveString
now returns an Optional<String>
. Optional.ofNullable
expected an object that is allowed to be null. If it is null, it returns Optional.emtpy()
, otherwise Optional.of(T)
. We've actually used a Lambda within the Optional<T>.ifPresent(Consumer<T>)
method that is only executed if there is a string returned. as an argument, it takes a Consumer<T>
instance. Whenever you see that, it is expected to pass a lambda (Of course your could also write a class that implements that interface).
But what if you want to do that:
positiveStr.ifPresent(str -> throw new Exception(str));
You'll probably get some strange compiler error until you'll eventually figure out that the Consumer
interface (and specifically it apply
method) doesn't support any checked exceptions. So if you need to throw an exception, you'll have to use unchecked exceptions:
positiveStr.ifPresent(str -> {throw new RuntimeException(str);});
By some weird reason you also need to add the curly brackets.
Streams
Now, where Lambdas come in handy in particular are streams. A stream is a "kind-of" functional interface to any type of collections (e.g. lists or maps). You specifically have to transform a collection into a stream by calling the stream()
method on that collection:
List<Integer> ints = Arrays.asList(1,2,3,4);
Stream<Integer> stream = ints.stream();
Now you can do a lot of fun stuff with that collection.
Transformation
Transforming a list of Integers to a list of String by calling its toString()
method:
List<String> strings = ints.stream()
.map(number -> number.toString())
.collect(Collectors.toList());
Wherever you just calling a method without arguments, you could in fact use a method reference by using the ::
operator which is another feature in Java 8:
List<String> strings = ints.stream()
.map(Object::toString)
.collect(Collectors.toList());
Don't forget the collect()
method to actually create a new collection out of the stream. map()
won't do that because you could add more method calls after that. collect()
expects an instance that implements the Collector
interface. Luckily, there are a number of predefined implementation within the class Collectors
. Collectors.toList()
e.g. creates a Collector
that returns a List
Creating a map that contains the integer as the key and the string as the value:
Map<Integer, String> intStringMap = ints.stream()
.collect(Collectors.toMap(number -> number, Object::toString));
I know that calling collect is a bit too verbose and even ugly, but it's still better that it was before.
Reduction
Calculating the sum of all integers in that list:
int sum = ints.stream().mapToInt(number -> number).sum();
Calculating the number of character in all strings in that list:
int numberOfCharacters = strings.stream().mapToInt(String::length).sum();
Filtering
Return a list that only contains Strings longer that 3 character:
List<String> longStrings = strings.stream()
.filter(str -> strings.size() > 3)
.collect(Collectors.toList());
Return a list that only contains the lengths of Strings longer that 3 character:
List<Integer> longStringLengths = strings.stream()
.filter(str -> strings.size() > 3)
.map(String::length)
.collect(Collectors.toList());