Jim Cheung

Java 8

(notes on Java 8 Tutorial )

Default Methods for Interfaces

a non-abstract method to interface, by utilizing the default keyword. also known as Extension Methods

interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}
// usage (anonymous object)
Formula formula = new Formula() {
    @Override
    public doble calculate(int a) {
        return sqrt(a * 100);
    }
};
formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0

Lambda expresssions

before Java 8:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collection.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

with Java 8 lambda:

Collection.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});
// one line method body you can skip {}
Collection.sort(names, (String a, String b) -> return b.compareTo(a));
// you can skip parameter types too, java will get it from the context
Collection.sort(names, (a, b) -> return b.compareTo(a));

Function Interfaces

each lambda corresponds to a given type, specified by an interface - functional interface

the interface must contain exactly one abstract method declaration.

since default methods (mentioned above) are not abstract, you can add default methods to your functional interface.

@FunctionalInterface annotation allows compiler check your interface meets the requirements.

example:

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

Method and Constructor References

the above example can be further simplified by utilizing static method reference:

Converter<String, Integer> converter = Integer::valueOf;

Java 8 enables you to pass references of methods or constructors via the :: keyword.

// can also reference non-static methods
class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java"); // "J"

how :: keyword works for constructors:

class Person {
    String firstName;
    String lastName;
    Person() {}
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

Java autometically choose the right constructor by matching the signature of PersonFactory.create for the reference Person::new

Lambda Scopes

similar to anonymous objects, you can access final variables from the local outer scope, instance fields, and static variables.

read local variables

final int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3

different to anonymous object, non-final varables also available in lambda:

int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3

however, num must be implicitly final

// does not compile
int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
num = 3;

writing to num from within the lambda expresssion is also prohibited.

Accessing fields and static variables

we have both read and write access to instance fields and static variables from within lambda expresssions (just like anonymous objecs)

class Lambda4 {
    static int outerStaticNum;
    int outerNum;
    void testScope() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };
        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

Accessing default interface methods

Default methods cannot be accessed from within lambda expresssions.

// will not compile:
Formula formula = (a) -> sqrt( a * 100 );

Build-in Functional Interfaces

Existing interfaces are extended to enable lambda support via the @FunctionalInterface annotation, like Comparator and Runnable.

New functional interfaces:

Predicates

Predicate<T>: take a T as input, return a boolean as output

Predicates are boolean-valued functions of one argument. The interface contains various default methods for composing predicates to complex logical terms (and, or, negate)

Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Functions

Function<T,R>: take a T as input, return an R as output

Functions accept one argument and produce a result. Default methods can be used to chain multiple functions together (compose, andThen)

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"

Suppliers

Supplier<T>: with nothing as input, return a T

Suppliers produce a result of a given generic type. Unlike Functions, Suppliers don't accept arguments.

Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person

Consumers

Consumer<T>: take a T as input, return nothing

Consumers represents operations to be performed on a single input argument.

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

Java 8 addes various default methods to the interface.

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0 

Optionals

Optionals are not functional interface, it's a nifty utility to prevent NullPointerException.

Optional is a simple container for a value which maybe null or no-null.

for a method which may return nothing, instead of returning null you return an Optional in Java 8.

Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"

Streams

A java.util.Stream represents a sequence of elements on which one or more operations can be performed.

Stream operations are either intermediate (return stream itself) or terminal (return result).

Stream operations can either be executed sequential or parallel.

Streams are created on a source (e.g. a java.util.Collection like lists or sets [maps are not supported])

List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
// ...

Collections in Java 8 are extended, you can create streams either by Collection.stream() or Collection.parallelStream()

Filter

intermediate operation

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

forEach is a terminal operation.

Sorted

intermediate operation

sorted in natural order unless you pass a custom Comparator

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

sorted() only creates a sorted view of the stream, the ordering of stringCollection is untouched.

Map

intermediate operation

you can use map to transform each object into another type. the generic type of the resulting stream depends on the generic type of the function you pass to map.

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

Match

terminal operations and return a boolean result.

boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true
boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false
boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true

Count

terminal operation and returns the number of elements in the stream as a long.

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

Reduce

terminal operation and returns an Optional holding the reduced value.

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);

Parallel Streams

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}
// sequential sort
long t0 = System.nanoTime();
long count = value.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// parallel sort
...
long count = value.parallelStream().sorted().count();
...

Map

As already mentioned maps don't support streams. Instead maps now support various new and useful methods for doing common tasks.

Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}
map.forEach(id, val) -> System.out.println(val));
map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33
// utilizing functions
map.computeIfPresent(9, (num, val) -> null);
map.containKey(9); // false
map.computeIfAbsent(23, num -> "val" + num);
map.containKey(23); // true
map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33
// remove
map.remove(3, "val3");
map.get(3); // val33
map.remove(3, "val33");
map.get(3); // null
// another helpful method
map.getOrDefault(42, "not found"); // not found
// merging
map.merge(9, "val9", (value, newValue) -> vale.concat(newValue));
map.get(9); // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat

merge either put the key/value into the map if no entry for the key exists, or the merging function will be called to change the existing value.

Date API

the new Date API is not the same as Joda-Time library.

Clock

Clock provides access to the current date and time.

Clocks are aware of a timezone.

Instant class can be used to create legacy java.util.Date objects.

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date

Timezones

Timezones are represented as a ZoneId

// prints all available timezone ids
System.out.println(ZoneId.getAvailableZoneIds());
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());

LocalTime

LocalTime represents a time without a timezone.

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2)); // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2));
System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239

LocalTime comes with various factory method to simplify the creation of new instances.

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59
DateTimeFormatter germanFormatter = 
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37

LocalDate

LocalDate represents a distinct date, e.g. 2014-03-11.

it's immutable.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DateOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

parsing a LocalDate from string is just like parsing a LocalTime

DateTimeFormatter germanFormatter = 
    DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24

LocalDateTime

LocalDateTime represents a date-time. immutable.

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439
// convert to instant
Instant instant = sylvester
    .atZone(ZoneId.systemDefault())
    .toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014
// formatting
DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

Unlike java.text.NumberFormat the new DateTimeFormatter is immutable and thread-safe.