Java 8

Lambdas

List<String> list = Arrays.asList("a", "b", "c");
// Those 4 lines are identical!
Collections.sort(list, (String a, String b) -> { return b.compareTo(a); });
Collections.sort(list, (String a, String b) -> b.compareTo(a));
Collections.sort(list, (a, b) -> b.compareTo(a));
Collections.sort(list, String::compareTo);

Scope

  • Local variables : Must be final or implicitly final to be read. Cannot be written.
    • OK :
      final int n = 1;
      ... = (from) -> String.valueOf(n);
    • OK :
      int n = 1;
      ... = (from) -> String.valueOf(n);
    • NOK :
      int n = 1
      ... = (from) -> String.valueOf(n);
      n++;
  • Fields / Static variables : Full access (like for anonymous class).
  • Default Interface Methods : No access !
    • NOK (from example earlier) :
      Formula formula = (a) -> sqrt( a * 100);

Functional interfaces

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
...
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");

This annotation makes the compiler prevent you to add more than one method to the interface (forbidden!)

Built-in

Interface Types Functional method Notable helper methods
Predicate<T> p1;
T → Boolean
p1.test(new T());
Predicate<T> p2 = p1.negate();
Function<T, U> f1;
Function<U, V> f2;
T → U
U u = f1.apply(new T());
Function<T, V> f3 = f1.andThen(f2);
Supplier<T> s;
() → T
T t = s.get();
Consumer<T> c;
T → ()
c.accept(new T());
Comparator<T> c1;
(T, T) → int
List<T> l = Arrays.asList(new T());
Collections.sort(l, c1);
Comparator<T> c2 = c1.reversed();

Easy factories

class Person {
   public Person(String firstName, String lastName) { ... }
}
interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}
...
PersonFactory<Person> f = Person::new;
Person p = f.create("Christophe", "Frattino");

Streams

Terminology

  • Stream : Sequence of elements on which operations can be performed. Created from a source (e.g. a Collection).
  • Stream operations
    • Intermediate : Returns a stream
    • Terminal : Returns an object
    • Can be executed sequentially or parallel.
    • Doesn't impact the source.

Create a stream

Stream <-> Collection

Collection<String> collection = ...;
Stream<String> stream             = collection.stream();         // uses one thread/core
Stream<String> parallelStream     = collection.parallelStream(); // uses multiple threads/cores
// ...
List<String> afterTreatment1 = stream.collect(Collectors.toList());
Set<String>  afterTreatment2 = stream.collect(Collectors.toSet());

Factory

Stream<String> stream         = Stream.of("a", "b", "c", "d");            // uses one thread/core
Stream<String> parallelStream = Stream.of("a", "b", "c", "d").parallel(); // uses multiple threads/cores

Primitive streams

// Range
IntStream intStream1 = IntStream.range(1, 10); // Numbers between 1 and 9
 
// Primitive array <-> Primitive stream
IntStream intStream2 = Arrays.stream(new int[] {1, 2, 3});
Stream<String> stream = intStream1.mapToObj(i -> "num" + i);
 
// Stream -> Primitive stream
DoubleStream doubleStream = Stream.of("1.0", "3.5").mapToDouble(Double::parseDouble);
  • Use specialized lambda expressions, e.g. IntFunction instead of Function or IntPredicate instead of Predicate.
  • Support additional terminal aggregate operations
    OptionalDouble average = intStream1.average(); // 5.0
    OptionalInt max = intStream2.max();            // 3
    double sum = doubleStream.sum()                // 4.5

Intermediate operations

These operations are lazy, and only executed when a terminal operation has been called. Think carefully in which order you'll put your intermediate operations to optimize the execution!

Filter the elements

stream = stream.filter(s -> s.startsWith("w")); // Values whose lambda evalution returns false are removed.

Sort the elements

stream = stream.sorted();                                  // Natural sort
stream = stream.sorted(new Comparator<Integer>() { ... }); // Custom sort

Map the elements

// 1-to-1 : map()
stream                  = stream.map(String::toUpperCase); // Values are sent through the method
Stream<Integer> stream2 = stream.map(String::length);      // Warning, stream type could change
 
// 1-to-n : flatMap()
class Foo {
    String name;
    List<Bar> bars = new ArrayList<>();
 
    Foo(String name) {
        this.name = name;
    }
}
 
class Bar {
    String name;
 
    Bar(String name) {
        this.name = name;
    }
}
 
IntStream.range(1, 4)
    .mapToObj(i -> new Foo("Foo" + i))
    .peek(f -> IntStream.range(1, 4)
        .mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
        .forEach(f.bars::add))
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

Terminal operations

Once one of those operations is called, you cannot call any other operation! See the next sub-chapter to know how to deal with this.

For each element

stream.forEach(s -> System.out.println(s)); // Prints all the strings in the stream
stream.forEach(System.out::println);        // Same

Do elements match ?

boolean atLeastOne = stream.anyMatch(s -> s.startsWith("a"));  // true if at least one element matches the lambda evaluation
boolean all        = stream.allMatch(s -> s.startsWith("a"));  // true if all elements match the lambda evaluation
boolean none       = stream.noneMatch(s -> s.startsWith("a")); // true if none of the elements match the lambda evaluation

Count elements

long elementCount = stream.count();

Reduce elements

Stream<String> stream    = Stream.of("a", "b", "c", "d");              // We create our stream
Optional<String> reduced = stream.reduce((s1, s2) -> s1 + ", " + s2);  // We reduce it
reduced.ifPresent(System.out::println);                                // Prints "a, b, c, d"

Get first element

Optional<String> firstElement = stream.findFirst();

Collectors

Grouping
Stream<String> stream = Stream.of("Albert", "Bob", "Chris", "Carl", "Christian");
Map<Character, List<String>> grouped1 = stream.collect(Collectors.groupingBy(s -> s.charAt(0)));                          // {A=[Albert], B=[Bob], C=[Chris, Carl, Christian]}
Map<Character, String> grouped2 = stream.collect(Collectors.toMap(s -> s.charAt(0), s -> s, (s1, s2) -> s1 + ", " + s2)); // {A=Albert, B=Bob, C=Chris, Carl, Christian}
Aggregation
double averageLength       = stream.collect(Collectors.averagingInt(String::length));   // 5.4
IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(String::length)); // IntSummaryStatistics{count=5, sum=27, min=3, average=5.400000, max=9}
String joined = stream.collect(Collectors.joining(", ", "Before ", " After")); // "Before Albert, Bob, Chris, Carl, Christian After"
Custom collector
Collector<String, StringJoiner, String> collector =
    Collector.of(
        () -> new StringJoiner(" | "),     // supplier
        (j, p) -> j.add(p.toUpperCase()),  // accumulator
        (j1, j2) -> j1.merge(j2),          // combiner
        StringJoiner::toString);           // finisher
 
String names = stream.collect(collector);  // ALBERT | BOB | CHRIS | CARL | CHRISTIAN

Stream suppliers

If you need the results of multiple terminal operations, use a stream supplier.

Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));
boolean any = streamSupplier.get().anyMatch(s -> true);
boolean non = streamSupplier.get().noneMatch(s -> true);

Data structures

Maps

New methods have been added to the maps in Java 8.

Map<Integer, String> map = new HashMap<>();
map.put(1, "hello");
System.out.println(map.putIfAbsent(1, "world")); // Prints "hello"
System.out.println(map.putIfAbsent(2, "world")); // Prints "world"
map.forEach((id, val) -> System.out.println(val));
map.compute         (1, (num, val) -> "bob" + num); // The element 1 now contains "bob1"
map.computeIfPresent(2, (num, val) ->   val + num); // The element 2 now contains "world2". If there was no element in 2, nothing is done.
map.computeIfAbsent (3, (num)      -> "sam" + num); // The element 3 now contains "sam3". If there was already an element in 3, nothing is done.
// Note : If lambda returned null, the entry is removed.
map.remove(3, "sam3"); // Only removes the element 3 if it was equal to "sam3"
String s1 = map.getOrDefault(1, "nothing");  // s1 = "bob1"
String s2 = map.getOrDefault(4, "nothing");  // s2 = "nothing"

Merge

The merge methods puts the value given in the second argument if there's nothing at the key given as first argument.

map.merge(9, "a", (value, newValue) -> value.concat(newValue)); // Lamba is ignored
map.get(9); // 9 -> "a"

If there was something though, it calls the lamba given as third argument with the old and new value.

map.merge(9, "b", (value, newValue) -> value.concat(newValue));
map.get(9); // 9 -> "ab"
// Note : If lambda returned null, the entry is removed.

Optionals

Optional<String> hasValue = Optional.of("value");
Optional<String> hasNoValue = Optional.empty();
 
hasValue.isPresent();   // returns true
hasNoValue.isPresent(); // returns false
 
Consumer<String> printer = (p) -> System.out.println(p);
hasValue.ifPresent(printer);    // prints "v"
hasNoValue.ifPresent(printer ); // does nothing
 
hasValue.get();   // returns "value"
hasNoValue.get(); // throws NoSuchElementException
 
hasValue.orElse("apple");   // returns "value"
hasNoValue.orElse("apple"); // returns "apple"

StringJoiner

StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add("George").add("Sally").add("Fred");
String desiredString = sj.toString();               // [George:Sally:Fred]

Date

Clocks, instants and time zones

A Clock contain a Time Zone and an Instant.

Clock clock = Clock.systemDefaultZone();
Instant instant = clock.instant();                      // time/date ("2016-05-24T12:50:26.370Z")
ZoneId zone = clock.getZone();                          // time zone ("Europe/Berlin")
ZoneOffset offset = zone.getRules().getOffset(instant); // offset at the time of the instant ("+02:00")

But we get old values/objects from them

long ms = clock.millis();       // Equivalent to System.currentTimeMillis()
Date date = Date.from(instant); // If we want to get one of those old java.util.Date objects

Local time and dates (no timezone)

Time

// Instancing
LocalTime now  = LocalTime.now();          // toString() --> "15:33:11.285"
LocalTime late = LocalTime.of(23, 00, 00);
 
// Manipulate
long hoursBetween = ChronoUnit.HOURS.between(now, late);     // in hours
long minutesBetween = ChronoUnit.MINUTES.between(now, late); // in minutes

Date

// Instancing
LocalDate today = LocalDate.now();                     // toString() -> "2016-05-24"
LocalDate birth = LocalDate.of(1985, Month.AUGUST, 8);
 
// Manipulate
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = today.minusDays(1);
DayOfWeek day = birth.getDayOfWeek();                // toString() -> "THURSDAY"

Time + Date

// Instancing
LocalDateTime now = LocalDateTime.now();
LocalDateTime birth = LocalDateTime.of(1985, Month.AUGUST, 8, 16, 13, 37);
 
// Manipulate
long daysBetween = ChronoUnit.DAYS.between(birth, now);      // in days
long currentMinute = now.getLong(ChronoField.MINUTE_OF_DAY); // Current minute, between 0 and (24 * 60) - 1

Parsing

The new DateTimeFormatter is immutable and thread-safe !

DateTimeFormatter simpleDate = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.FRENCH);
LocalDate date = LocalDate.parse("25.12.2014", simpleDate);
 
DateTimeFormatter simeTime = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.FRENCH);
LocalTime time = LocalTime.parse("13:37", formatter);
 
DateTimeFormatter complexDateTime = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");
LocalDateTime parsed = LocalDateTime.parse("24.05.2016 16:09:10", complexDateTime);  // String --> LocalDateTime
String string = complexDateTime .format(parsed);                                     // LocalDateTime --> String

Conversion to Instant

Instant instant ​= now.atZone(ZoneId.systemDefault()).toInstant();  // now can be of type LocalDate, LocalTime or LocalDateTime

Default methods for interface

interface Formula {
   double calculate(int a);
   default double sqrt(int a) { return Math.sqrt(a); }
}
Formula f = new Formula() {
   @Override
   public double calculate(int a) { return sqrt(a * 1000); }
}
Print/export