Java 8 - by example!

Patterns and practices for Lambdas, Streams, Optional...

Created by Mark Harrison / @markglh

Interface default methods

  • Interfaces can now have default & static methods!
  • Allows new interface methods to be added without breaking existing implementations
  • Multiple inheritance!

public interface Comparator<T> {
  default Comparator<T> reversed() {
        // Implementation here
  }

  public static Comparator<T> naturalOrder() {
        // Implementation here
  }
}
	          			

Multiple Inheritance...

  • What if a class inherits the same default method from two interfaces???

    1. Class methods always win. Whether it’s this class or the superclass, it will take priority
    2. If multiple interfaces define the same method with a default implementation, then the most specific is selected (the child in inheritance terms)
    3. If it’s not obvious then the compiler will cry, we need to explicitly choose: INTERFACE.super.METHODNAME

Lambdas you say?

  • Basically just a shorthand method implementation
  • Concise and much improved replacement for Anonymous inner classes
  • Makes it easy to pass behaviour around
  • Pass the behaviour into the method, flips the design on its head
  • Lexically scoped (this is effectively shared with the surrounding method)

Syntax please!


(int x, int y)   ->   x + y
	          			

Argument types can be inferred by the compiler:


(x, y)   ->   x + y
	          			

Zero-arg Lambdas are also possible:


()   ->   "Java 8!"
	          			

But how can we use them?


Consumer<String> inferredConsumer = 
                    x -> System.out.println(x);
	          			

WAT?!?

Functional Interfaces

  • A Functional Interface is any interface with one Single Abstract Method
  • The parameters that the Lambda accepts and returns must match the interface (including exceptions)
  • @FunctionalInterface annotation is provided to highlight these interfaces, it is just a marker though, any SAM interface will work - the annotation simply enforces this at compile time


  • So... When we pass or assign a Lambda:
    1. First the Lambda is converted into a Functional Interface
    2. Secondly it is invoked via this generated implementation

Lambda Example 1 - Predicates

Have you ever written code like this?


@FunctionalInterface  //Added in Java8 
public interface Predicate<T> {
	boolean test(T t); 	
}

private void printMatchingPeople(
                List<Person> people, 
                Predicate<Person> predicate) {
    for (Person person : people) {
        if (predicate.test(person)) {
           System.out.println(person);
        }
    }
}						
						

Lambda Example 1 - Predicates

Pre Java 8:


public class PersonOver50Predicate 
                    implements Predicate<Person> {
    public boolean test(Person person) {
        return person.getAge() > 50;
    }
}

printMatchingPeople(loadsOfPeople, 
                    new PersonOver50Predicate());
	          			

Lambda Example 1 - Predicates

Java 8:


printMatchingPeople(loadsOfPeople, 
                    x -> x.getAge() > 50); 
	          			

Notice the signature matches that of the Predicate, the compiler automatically figures out the rest


What if we had an existing Predicate we wanted to enhance?


Predicate<Person> ageCheck = x -> x.getAge() > 50;
printMatchingPeople(loadsOfPeople, 
                ageCheck.and(x -> x.getIq() > 100));
		          			

Composite Predicates FTW... :-)

Lambda Example 2 - Runnable

Pre Java 8 mess:


Runnable r1 = new Runnable() {
    @Override
    public void run() {
         System.out.println("Meh!");
    }
};
r1.run();
	          			

Java 8:


Runnable r = () -> System.out.println("Woop!");
r.run();
	          			

Lambda Example 3 - Collections

Lambdas make lots of new Collection methods possible...


List<String> strings = new ArrayList<>();
Collections.addAll(strings, "Java", "7", "FTW");
	          			

Do something on every element in the List


strings.forEach(x -> System.out.print(x + " ")); 
//Prints: "Java 7 FTW"
		          			

Lambda Example 4 - More Collections

Replace every matching element in the List


strings.replaceAll(x -> x == "7" ? "8" : x); 
strings.forEach(x -> System.out.print(x + " "));
//Prints: "Java 8 FTW"
		          			

Remove matching elements from the List


strings.removeIf(x -> x == "8");
strings.forEach(x -> System.out.print(x + " "));
//Prints: Java FTW
		          			

Lambda Example 5 - Comparators


@FunctionalInterface //Added in Java8 version
public interface Comparator<T> {
    int compare(T o1, T o2);
    //Java 8 adds loads of default methods
}
						

Lambda Example 6 - Comparators 2


List<Person> loadsOfPeople = ...;
   						

Pre Java 8:


public class SortByPersonAge 
                    implements Comparator<Person> {
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
}
Collections.sort(loadsOfPeople, 
                 new SortByPersonAge());

	          			

Java 8:


Collections.sort(loadsOfPeople, 
            (p1, p2) -> p1.getAge() - p2.getAge());
		          			

Lambda Example 7 - Comparators 3

As usual in Java 8... the Comparator interface provides plenty of useful default & static methods...


//"comparing" static method simplifies creation
Comparator<Person> newComparator = 
               Comparator.comparing(e -> e.getIq());
						


//"thenComparing" combines comparators
Collections.sort(loadsOfPeople, 
        newComparator.thenComparing(
            Comparator.comparing(e -> e.getAge())));
						

Lambda Example 8 - Comparators 4

and more...


//More useful Collection methods...
loadsOfPeople4.sort(
        Comparator.comparing(e -> e.getIq()));
						


//And finally... Method references
loadsOfPeople.sort(
        Comparator.comparing(Person::getAge));
						

Introducing Method References

  • Any method can be automatically “lifted” into a function. It must simply meet contract of the FunctionalInterface
  • Can be easier to debug & test than Lambdas, more descriptive stack traces
  • Promotes re-use, keeping code DRY
  • Uses the "::" syntax

Method References Types

Reference to... Example
a static method Class::staticMethodName
an instance method of a specific object object::instanceMethodName
an instance method of an arbitrary object Class::methodName
a constructor ClassName::new

Reference to a static method

A simple reference to a static method


//isPersonOver50 is a static method
printMatchingPeople(loadsOfPeople, 
                  PersonPredicates::isPersonOver50);
						

This is equivalent to


printMatchingPeople(loadsOfPeople, 
                    x -> x.getAge() > 50);
						

Reference to an instance method of a specific object

A reference to a method on an object instance


List<String> strings = ...
//print is a method on the "out" PrintStream object
strings.forEach(System.out::print);
						

This is equivalent to


strings.forEach(x -> System.out.print(x));
						

Reference to an instance method of an arbitrary object

Examine this simplified definition of a map function


public interface Function<T,R> {
	public R apply(T t);
}

public <T, R> List<R> map(Function<T, R> function, 
                          List<T> source) {
    /* applies the function to each element, 
       converting it from T to R */
}
						

Reference to an instance method of an arbitrary object cont...

Although it looks like we're referencing a Class method, we're invoking an instance method on the object(s) passed in the call



List<Person> loadsOfPeople = ...
List<Integer> namesOfPeople = 
                map(Person::getAge, loadsOfPeople);
						

This is equivalent to


map(person -> person.getAge(), loadsOfPeople);
						

Reference to a constructor

Uses the constructor to create new objects, the constructor signature must match that of the @FunctionalInterface


List<String> digits = Arrays.asList("1", "2", "3");
						

//Transforms a String into a new Integer
List<Integer> numbers = map(Integer::new, digits);
						

This is equivalent to


map(s -> new Integer(s), digits);
						

What's wrong with Collections

  • Every application uses Collections, however Collections are difficult to query and aggregate, requiring several levels of iteration and conditionals, basically it’s messy and painful
  • Writing multi-threaded code to do this manually is difficult to write and maintain


Imagine writing this manually


Stream<String> words=Stream.of("Java", "8", "FTW");
Map<String, Long> letterToNumberOfOccurrences =
    words.map(w -> w.split(""))
            .flatMap(Arrays::stream)
            .collect(Collectors.groupingBy(
                            Function.identity(), 
                            Collectors.counting()));
//Prints: //{a=2, T=1, F=1, v=1, W=1, 8=1, J=1}
							

Introducing Streams!

  • A Stream is a conceptually fixed data structure in which elements are computed on demand
  • Streams iterate internally, you don’t have to worry about handling the iteration
  • Pipelining: Akin to the “pipe” command in unix, allowing aggregations to be chained together
  • Automatic optimisation: short-circuiting and lazy processing
  • Can be infinite
  • Can be executed in parallel automatically using parallelStream or parallel()

More Streams...

    Can be created from multiple sources:

  • Arrays.stream(...), Stream.of(1, 2, 3, 4), Stream.iterate(...), Stream.range(...), Random.ints(), Files.lines(...)...

  • Two types of operations:

  • Intermediate (aggregation): filter, map, flatMap, sorted ...
  • Terminal: collect, reduce, forEach, findAny ...

  • Specialised Streams:

  • IntStream, LongStream and DoubleStream: better performance for those unboxed types

map


public <R> Stream<R> map(Function<T, R> mapper);
						

The mapper Function converts each element from T to R. The result is then added, as is, to the Stream

flatMap


public <R> Stream<R> flatMap(Function<T, 
                             Stream<R>> mapper);
						

  • The mapper Function converts each element from T to a Stream of R
  • This is the key difference to map, the function itself returns a Stream rather than one element
  • This Stream is then flattened (merged) into the main Stream


To put it another way: flatMap lets you replace each value of a Stream with another Stream, and then it concatenates all the generated streams into one single stream

reduce


public T reduce(T identity, 
                BinaryOperator<T> accumulator);

public Optional<T> reduce(
                BinaryOperator<T> accumulator);

//This is the function contained in BinaryOperator
R apply(T t, U u);
						
  • Terminal Operation
  • Takes a Stream of values and repeatedly applies the accumulator to reduce them into a single value
  • The accumulator is passed the total so far (T) and the current element (U)
  • If passed, identity provides the initial value, rather than the first element

reduce continued...


int totalAgeUsingReduce = loadsOfPeople.stream()
  .map(Person::getAge) //contains 5, 10, 15
  .reduce(0, (total, current) -> total + current);
						
  1. First we map the Person to the age int
  2. reduce then starts at 0, and adds the current element, 5
  3. reduce continues by adding the current total 5, to the next element, 10
  4. finally reduce adds the current total 15, to the next element 15
  5. Tada, we've added up all the ages: 30!

collectors

The collect method is a terminal operation which takes various "recipes", called Collectors for accumulating the elements of a stream into a summary result, or converting to a specific type (such as a List)


List<String> listOfStrings = loadsOfPeople.stream()
             .map(x -> x.getName())
             .collect(Collectors.toList());
						
  • The argument passed to collect is an object of type java .util.stream.Collector
  • It describes how to accumulate the elements of a stream into a final result
  • Can be used for Grouping, Partitioning, Averaging, ...

Streams Example 1


List<Integer> numbers = Arrays.asList(1, 2 ... 8);
List<Integer> twoEvenSquares = numbers.stream()
    .filter(n -> n % 2 == 0) //Filter odd numbers
    .map(n -> n * n) ////Multiply by itself
    .limit(2)//Limit to two results
    .collect(Collectors.toList()); //Finish!
						

Imagine a println in each step...


filtering 1
						

filtering 2
						

mapping 2
						

filtering 3
						

filtering 4
						

mapping 4
						

twoEvenSquares = List[4, 16]
						

Streams Example 2


List<String> youngerPeopleSortedByIq =
    loadsOfPeople.stream()
        .filter(x -> x.getAge() < 50)
        .sorted(Comparator
              .comparing(Person::getIq).reversed())
        .map(Person::getName)
        .collect(Collectors.toList());
						
  1. Filter out all people older than 50
  2. Inverse sort the remaining people by IQ
  3. map each person to their name (convert to a Stream<String>)
  4. Convert the result to a List

Streams Example 3 - sum


int combinedAge = 
    loadsOfPeople.stream()
      .mapToInt(Person::getAge) //returns IntStream
      .sum(); //this HAS to be a specialised Stream
						
  1. map each person to their age, producing an IntStream
  2. sum the results, also supports average

Streams Example 4 - map, reduce


String xml = 
  "<people>" +
  loadsOfPeople.stream()
    .map(x -> "<person>"+ x.getName() +"</person>")
    .reduce("", String::concat) //start with "" 
  + "</people>";
						

map each Person to an XML element (<person>Steve</person>), then use String.concat to reduce the Stream into one XML String


<people>
	<person>Dave</person>
	<person>Helen</person>
	<person>Laura</person>
	<person>Ben</person>
</people>
						

Streams Example 5 - map


List<Stream<Person>> clonedPeople = 
 loadsOfPeople.stream()
   .map(person -> Stream.of(person, person.dolly()))
   .collect(Collectors.toList());
						
  1. map creates a new Stream containing two people
  2. This Stream is then added to the main Stream as-is, leaving us with a pretty useless: List<Stream<Person>>

Streams Example 6 - flatMap


List<Person> clonedPeople2 = loadsOfPeople.stream()
    .flatMap(person -> Stream.of(person, 
                                 person.dolly()))
    .collect(Collectors.toList());
						
  1. flatMap combines each element from the new Streams into one Stream<Person>
  2. So now we've got what we wanted in the first place, a List<Person>

Sweeet!

Streams Example 7 - Reduce


int totalAgeUsingReduce = loadsOfPeople.stream()
    .map(Person::getAge)
    .reduce((total, current) -> total + current)
    .get(); //get the result from the Optional
						
  • This is the same as the previous example, the difference being we don't specify a default value for reduce
  • Not specifying a default value means the result is Optional... if the Stream is empty then so is the result!

Streams Example 8 - Grouping


Map<Integer, List<Person>> peopleGroupedByAge =
  loadsOfPeople.stream()
    .filter(x -> x.getIq() > 110)
    .collect(Collectors.groupingBy(Person::getAge));
						
  • The collect method groups the filtered results by age, producing a Map<age, <Person>>

{52=[Person{... age=52, iq=113, gender=MALE}],
 60=[Person{... age=60, iq=120, gender=FEMALE}],
 28=[Person{... age=28, iq=190, gender=MALE}]}
						

Streams Example 9 - Partitioning


Map<Boolean, List<Person>> peoplePartitionedByAge = 
 loadsOfPeople.stream().filter(x -> x.getIq() > 110)
   .collect(Collectors
             .partitioningBy(x -> x.getAge() > 55));
						
  • The collect method partitions the filtered results by age
  • The Map will have two entries, true and false, according to the Predicate

{false=[Person{... age=28, iq=190, gender=MALE}],
 true=[Person{... age=60, iq=120, gender=FEMALE}]}
						

Streams Example 10 - Multiple Groups


Map<Integer, Double> peopleGroupedBySexAndAvgAge = 
  loadsOfPeople.stream()
    .filter(x -> x.getIq() > 110)
    .collect(
        Collectors.groupingBy(Person::getAge,
           Collectors.averagingInt(Person::getIq)));
						
  • We can group by multiple Collectors
  • Here we group by age and the average IQ of that group

{52=113.0, 60=117.5, 28=190.0}
						

Streams Example 11 - findAny


loadsOfPeople.stream()
    .filter(t -> t.getGender() == Person.Sex.FEMALE)
    .findAny()
    .ifPresent(System.out::println);
						
  • findAny either returns an element or nothing, hence we get an Optional
  • ifPresent executes the Lambda if we get a result

Streams Example 12 - Parallel

Lets iterate over 10,000,000 elements!



x -> Stream.iterate(1L, i -> i + 1)
        .limit(x)
        .reduce(Long::sum).get();
						

Executes in 80ms - we incur a penalty here because the long is repeatedly boxed and unboxed


x -> Stream.iterate(1L, i -> i + 1)
        .parallel().limit(x)
        .reduce(Long::sum).get();
							

Executes in 211ms?! It turns out parallel isn't always a free win!

Streams Example 13 - Parallel Win!


x -> LongStream.rangeClosed(1L, x)
        .reduce(Long::sum).getAsLong();
						

Executes in 24ms - much better using an unboxed Stream


x -> LongStream.rangeClosed(1L, x)
        .parallel()
        .reduce(Long::sum).getAsLong();
							

Executes in 7ms - now that the Stream isn't dynamic, parallel works much better!

Optional

  • Use Optional instead of passing null around, helps prevent NullPointerExceptions
  • Optional is a container that’s either empty or present, in which case it contains a value
  • So anytime that you’re thinking of return or accepting a null value in a method, use Optional instead!

public class Computer {
    private Optional<Mainboard> mainboard;
}
public class Mainboard {
    private Optional<Cpu> cpu;
}
public class Cpu {
    private String type;
}

						

Using Optional

Several ways to create an Optional:


Optional.of(cpu); //Throws an NPE if cpu is null
Optional.ofNullable(cpu); //empty if cpu is null
						

Getting the contents from the Optional:


cpu.get(); //get CPU or throw NoSuchElementException
cpu.orElse(new Cpu()); //safe get, provides default
							

And more...


cpu.isPresent(); //true if present, false if empty
cpu.ifPresent(x -> System.out.println(x.getType()));
							

Also supports map, flatMap and filter!

Optional Example 1 - basics


if (mainboard.isPresent() && 
      mainboard.get().getCpu().isPresent()) {
          mainboard.get().getCpu().get().getType();
}
						

Eww! Lets try something else!


Optional<String> cpuType = 
  mainboard.map(Mainboard::getCpu)
    .map(Cpu::getType); //Optional<Optional<Cpu>>
							

***Fails to compile, calling getType on an Optional... if only we could flatten it???

Optional Example 2 - flatMap


Optional<String> stringOptional = mainboard
    .flatMap(Mainboard::getCpu)
    .map(Cpu::getType);
						

Lets take this further and safely get cpu type of a Computer!


Computer computer = new Computer(mainboard);
  String cpu = computer.getMainboard()
    .flatMap(Mainboard::getCpu)
    .map(Cpu::getType) 
    .filter(x -> "Intel".equals(x))
    .orElse("Nothing we're interested in!");
							

It's been emotional...



Recommended Reading