Java 8 new features study notes

Fundamentals

1. why should you care?

What made big change occur in java 8

  1. Simplifying - write programs more easily—instead of writing verbose code
  2. Parallelism - commodity CPUs have become multicore, fork/join framework in java 7 is too difficult.

Functions in Java

First-class values Vs second-class citizens - Only first-class values be passed around during program execution

  • Passing code example - Find Hidden Files
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

Streams

  • expose on collections
  • Parallel computations on data

Default methods

Behavior parameterization

In this section, we use a example to explain what’s Passing code with behavior parameterization and how to make code evolvable.

Prior to Java 8

1. filtering apples by color

public static List<Apple> filterApplesByColor(List<Apple> inventory, String color) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory){
        if(apple.getColor().equals(color)){
            result.add(apple);
        }
    }
    return result;
}

2. filtering apples by weight

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory){
        if(apple.getWeight() > weight){
            result.add(apple);
        }
    }
    return result;
}

3. filtering with every attribute you can think of

public static List<Apple> filterApples(List<Apple> inventory, String color, int weight, boolean flag){
    List<Apple> result = new ArrayList<>();
    for(Apple apple: inventory){
        if((flag && apple.getColor().equals(color)) || (!flag && apple.getWeight() > weight)){
            result.add(apple);
        }
    }
    return result;
}

Behavior parameterization

First define an interface to model the selection criteria:

public interface ApplePredicate {
    public boolean predicate(Apple apple);
}

Different strategies for selecting an Apple

static class AppleWeightPredicate implements ApplePredicate {
    public boolean predicate(Apple apple) {
        return apple.getWeight() > 150;
    }
}
    
static class AppleColorPredicate implements ApplePredicate {
    public boolean predicate(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

4. filtering by abstract criteria

public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : inventory){
        if(p.predicate(apple)){
            result.add(apple);
        }
    }
    return result;
}

Anonymous classes

  • don’t have a name
  • let you declare and instantiate a class at the same time
  • create ad hoc implementations

5. using an anonymous class

List<Apple> greenApples = filterApples(inventory, new ApplePredicate(){
    public boolean predicate(Apple apple) {
        return "green".equals(apple.getColor());
    }});

6. using a lambda expression

List<Apple> greenApples = filterApples(inventory, (Apple apple) -> "green".equals(apple.getColor()));

7. abstracting over List type

public interface Predicate<T> {
    boolean predicate(T t);
}
    
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for(T e : list){
        if(p.predicate(e)){
            result.add(e);
        }
    }
    return result;
}

List<Apple> greenApples = filter(inventory, (Apple apple) -> "green".equals(apple.getColor()));

Real-world examples

Sorting with a Comparator

List<Apple> inventory = Arrays.asList(new Apple(80,"green"), new Apple(155, "green"), new Apple(120, "red"));
        
inventory.sort(new Comparator<Apple>(){
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }});
        
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

Executing a block of code with Runnable

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    // do something
}));

GUI event handling

Button button = new Button("Send");
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));

Lambda expressions

What’s Lambda expressions

The Definition of Lambda:

  1. Anonymous - doesn’t have an explicit name
  2. Function - a lambda isn’t associated with a particular class like a method is, a lambda has a list of parameters, a body, a return type, and a possible list of exceptions that can be thrown
  3. Passed around - A lambda expression can be passed as argument to a method or stored in a variable.
  4. Concise - You don’t need to write a lot of boilerplate like you do for anonymous classes.

The basic syntax of Lambda

(parameters) -> expression

or

(parameters) -> { statements; }

Where and how to use lambdas

  • use a lambda expression in the context of a functional interface.

Functional interface and Function descriptor

  • A functional interface is an interface that specifies exactly one abstract method.
  • @FunctionalInterface

Functional interfaces

  1. Predicate - defines an abstract method named test that accepts an object of generic type T and returns a boolean
  2. Consumer - defines an abstract method named accept that takes an object of generic type T and returns no result (void).
  3. Function - defines an abstract method named apply that takes an object of generic type T as input and returns an object of generic type R.

NOTE: boxing/unboxing/autoboxing - boxing means convert a primitive type into a corresponding reference type, the opposite approach called unboxing. Java also has an autoboxing mechanism that boxing and unboxing operations are done automatically. Boxed values use more memory and comes with a performance cost, Java 8 brings addtional functional interfaces(IntPredicate, DoublePredicate, ToIntFunction, IntTo-DoubleFunction, etc) to avoid autoboxing.

Lambda Type

  • Type checking - deduce the type of lambda from the context in which the lambda is used.
  • Target typing
  • Type inference

Method references

List<Apple> inventory = Arrays.asList(new Apple(80,"green"), new Apple(155, "green"), new Apple(120, "red"));
inventory.sort(comparing(Apple::getWeight));
System.out.println(inventory);

Constructor references:

BiFunction<Integer, String, Apple> function = Apple::new;
System.out.println(function.apply(100, "red"));

Streams

Stream is a sequence of elements from a source that supports data processing operations.

  • Sequence of elements - a collection, a stream provides an interface to a sequenced set of values of a specific element type.
  • Source - Streams consume from a data-providing source such as collections, arrays, or I/O resources.
  • Data processing operations - filter, map, reduce, find, match, sort. Stream operations can be executed either sequentially or in parallel.

Streams API in Java 8 lets you write code:

  • Declarative - More concise and readable
  • omposable - Greater flexibility
  • Parallelizable - Better performance

Comparison Example of java 7 and java 8

There is a menu contains a lot of dishes as below

public static final List<Dish> menu =
    Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),
                  new Dish("beef", false, 700, Dish.Type.MEAT),
                  new Dish("chicken", false, 400, Dish.Type.MEAT),
                  new Dish("french fries", true, 530, Dish.Type.OTHER),
                  new Dish("rice", true, 350, Dish.Type.OTHER),
                  new Dish("season fruit", true, 120, Dish.Type.OTHER),
                  new Dish("pizza", true, 550, Dish.Type.OTHER),
                  new Dish("prawns", false, 400, Dish.Type.FISH),
                  new Dish("salmon", false, 450, Dish.Type.FISH));

Now we need the names of dishes that are low in calories and sorted by number of calories.

Java 7

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d : menu){
    if(d.getCalories() < 400){
        lowCaloricDishes.add(d);
    }
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>(){
    public int compare(Dish d1, Dish d2) {
        return Integer.compare(d1.getCalories(), d2.getCalories());
    }});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d : lowCaloricDishes){
    lowCaloricDishesName.add(d.getName());
}

Java 8

List<String> lowCaloricDishesName = menu.stream().filter(d -> d.getCalories() < 400).sorted(comparing(Dish::getCalories)).map(Dish::getName).collect(toList());

Streams vs. collections

  • Collections are about data; streams are about computations(filter, sorted, map).
  • A stream can be traversed only once.
  • Streams internal iteration Vs Collections external iteration

Stream operations

The Stream interface in java.util.stream.Stream defines many operations, it can be classified into two categories:

  • intermediate operations - filter, map, and limit, etc, which can be connected together to form a pipeline.
  • terminal operations - collect, count, forEach, etc, which causes the pipeline to be executed and closes it.

NOTE: The idea behind a stream pipeline is similar to the builder pattern(http://en.wikipedia.org/wiki/Builder_pattern).

Working with streams

Filtering and slicing

Filtering with a predicate

List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());

NOTE: The filter method takes as argument a predicate and returns a stream including all elements that match the predicate.

NOTE: Dish::isVegetarian is a method reference which equals (Dish d) -> d.isVegetarian() or d -> d.isVegetarian()

Filtering unique elements in a stream

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);

NOTE: The distinct method returns a stream consisting of the distinct elements.

Truncating a stream

List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());

NOTE: The limit method used to truncate a stream.

Skipping elements

List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
dishes.forEach(System.out::println);

NOTE: The skip method used to skip a stream.

Mapping

Applying a function to each element of a stream

menu.stream().map(Dish::getName).collect(toList()).forEach(System.out::println);

List<String> words = Arrays.asList("Java8", "Lambdas", "In", "Action");
words.stream().map(String::length).collect(toList()).forEach(System.out::println);

menu.stream().map(Dish::getName).map(String::length).collect(toList()).forEach(System.out::println);

NOTE: getName and length are applies method in above example.

Flattening streams

List<String> words = Arrays.asList("Hello", "World");
words.stream().map(word -> word.split("")).flatMap(Arrays::stream).distinct().collect(toList()).forEach(System.out::println);

NOTE: The above code return a list of all the unique characters for a list of words.

Finding and matching

menu.stream().anyMatch(Dish::isVegetarian);
menu.stream().allMatch(d -> d.getCalories() < 1000);
menu.stream().noneMatch(d -> d.getCalories() >= 1000);

Finding an element

Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
dish.ifPresent(d -> System.out.println(d.getName()));

Reducing

Summing the elements

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
Optional<Integer> sum = numbers.stream().reduce(Integer::sum);

int product = numbers.stream().reduce(1, (a, b) -> a * b);

Maximum and minimum

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);

Practice - traders executing transactions

In this section, we will try to solve 8 questons of traders executing transactions, before listing the questions, we first give the details of Trader and Transaction:

public class Trader {

    private String name;
    private String city;
    
    public Trader(String name, String city) {
        this.name = name;
        this.city = city;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Trader: "+this.name + " in " + this.city;
    }
}
public class Transaction {

    private Trader trader;
    private int year;
    private int value;
    
    public Transaction(Trader trader, int year, int value) {
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public int getYear() {
        return year;
    }

    public int getValue() {
        return value;
    }
    
    public String toString(){
        return "{" + this.trader + ", " + "year: "+this.year+", " + "value:" + this.value +"}";
    }
}

There are 4 traders and a transacton list:

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario","Milan");
Trader alan = new Trader("Alan","Cambridge");
Trader brian = new Trader("Brian","Cambridge");
        
List<Transaction> transactions = Arrays.asList(
        new Transaction(brian, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
    );

Questions

  1. Find all transactions in the year 2011 and sort them by value (small to high).
  2. What are all the unique cities where the traders work?
  3. Find all traders from Cambridge and sort them by name.
  4. Return a string of all traders’ names sorted alphabetically.
  5. Are any traders based in Milan?
  6. Print all transactions’ values from the traders living in Cambridge.
  7. What’s the highest value of all the transactions?
  8. Find the transaction with the smallest value.

Solutions

  • Question 1
List<Transaction> tr2011 = transactions.stream().filter(t -> t.getYear() == 2011).sorted(comparing(Transaction::getValue)).collect(toList());
tr2011.forEach(System.out::println);
  • Question 2
List<String> cities = transactions.stream().map(t -> t.getTrader().getCity()).distinct().collect(toList());
cities.forEach(System.out::println);
  • Question 3
List<Trader> traders = transactions.stream().map(Transaction::getTrader).filter(t -> t.getCity().equals("Cambridge")).distinct().sorted(comparing(Trader::getName)).collect(toList());
traders.forEach(System.out::println);
  • Question 4
String traderStr = transactions.stream().map(transaction -> transaction.getTrader().getName()).distinct().sorted().reduce("", (n1, n2) -> n1 + n2);
System.out.println(traderStr);
  • Question 5
boolean milanBased = transactions.stream().anyMatch(t -> t.getTrader().getCity().equals("Milan"));
System.out.println(milanBased);
  • Question 6
transactions.stream().map(Transaction::getTrader).filter(t -> t.getCity().equals("Milan")).forEach(t -> t.setCity("Cambridge"));
System.out.println(transactions);
  • Question 7
int highestValue = transactions.stream().map(Transaction::getValue).reduce(0, Integer::max);
System.out.println(highestValue);
  • Question 8
int smallestValue = transactions.stream().map(Transaction::getValue).reduce(0, Integer::min);
System.out.println(smallestValue);

Numeric streams

Primitive stream specializations

int calories = menu.stream().mapToInt(Dish::getCalories).sum();

Numeric ranges

IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0).forEach(System.out::println);

Building streams

Streams from values

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

Streams from arrays

int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
System.out.println(sum);

Streams from files

long uniqueWords = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()).flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();
System.out.println("There are " + uniqueWords + " unique words in data.txt");

Streams from functions

Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
Stream.generate(Math::random).limit(5).forEach(System.out::println);

Collecting data with streams

We will start this section with a comparison example - Grouping transactions by currency.

Java 7 - imperative style

Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : transactions){
    Currency currency = transaction.getCurrency();
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    if (transactionsForCurrency == null) {
        transactionsForCurrency = new ArrayList<>();
        transactionsByCurrencies.put(currency, transactionsForCurrency);
    }
    transactionsForCurrency.add(transaction);
}
System.out.println(transactionsByCurrencies);

Java 8 - functional style

Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
System.out.println(transactionsByCurrencies);

Reducing and summarizing

long howManyDishes = menu.stream().collect(counting());

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
System.out.println(mostCalorieDish.get());

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
System.out.println(totalCalories);

double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
System.out.println(avgCalories);

String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
System.out.println(shortMenu);

Parallel data processing and performance

Parallel streams

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

Avoiding shared mutable state

public static class Accumulator {
    public long total = 0;

    public void add(long value) {
        total += value;
    }
}

Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).parallel().forEach(accumulator::add);
long sum = accumulator.total

NOTE: above is a example of misuse of parallel stream, multiple threads are concurrently accessing the accumulator and in particular executing total += value, which, despite its appearance, isn’t an atomic operation.

The fork/join framework

More details refer to http://ksoong.org/fork-join.

Spliterator

Refactoring, testing, and debugging

Default methods

default methods and static methods

public interface Java8Interface {
    
    static void A(){
        System.out.println("A");
    }
    
    default void B() {
        System.out.println("B");
    }
}

Inheritance considered harmful

Inheritance shouldn’t be your answer to everything when it comes down to reusing code. For example, inheriting from a class that has 100 methods and fields just to reuse one method is a bad idea, because it adds unnecessary complexity.

Using Optional as a better alternative to null

CompletableFuture: composable asynchronous programming

New Date and Time API

LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration

LocalDate date = LocalDate.of(2016, 9, 18);
int year = date.getYear(); 
Month month = date.getMonth(); 
int day = date.getDayOfMonth(); 
DayOfWeek dow = date.getDayOfWeek(); 
int len = date.lengthOfMonth(); 
boolean leap = date.isLeapYear();

LocalDate today = LocalDate.now();

int y = date.get(ChronoField.YEAR);
int m = date.get(ChronoField.MONTH_OF_YEAR);
int d = date.get(ChronoField.DAY_OF_MONTH);

LocalTime time = LocalTime.of(13, 25, 40);
int hour = time.getHour(); 
int minute = time.getMinute(); 
int second = time.getSecond();

LocalDate date = LocalDate.parse("2016-09-18");
LocalTime time = LocalTime.parse("12:17:40");

LocalDateTime dt1 = LocalDateTime.of(2016, Month.SEPTEMBER, 18, 13, 25, 40);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 25, 40);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();

Instant instant1 = Instant.ofEpochSecond(3);
Instant instant2 = Instant.ofEpochSecond(3, 0);
Instant instant3 = Instant.ofEpochSecond(2, 1000000000);
Instant instant4 = Instant.ofEpochSecond(4, -1000000000);

Duration threeMinutes1 = Duration.ofMinutes(3);
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES);
Duration threeDays1 = Duration.ofDays(3);
Duration threeDays2 = Duration.of(3, ChronoUnit.DAYS);

Period tenDays = Period.ofDays(10);
Period fiveDays = Period.between(LocalDate.of(2016, 9, 13), LocalDate.of(2016, 9, 18));
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

Manipulating, parsing, and formatting dates

LocalDate date1 = LocalDate.of(2016, 9, 18);
LocalDate date2 = date1.withYear(2015);
LocalDate date3 = date2.withDayOfMonth(25); 
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 10);
LocalDate date5 = date4.plusWeeks(1);
LocalDate date6 = date5.minusYears(3);
LocalDate date7 = date6.plus(36, ChronoUnit.MONTHS);

LocalDate date = LocalDate.of(2016, 9, 18);
LocalDate date8 = date.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date9 = date.with(lastDayOfMonth());
LocalDate date10 = date.with(new NextWorkingDay());
LocalDate date11 = date.with(nextOrSame(DayOfWeek.MONDAY));
LocalDate date12 = date.with(temporal -> {
    DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
    int dayToAdd = 1;
    if (dow == DayOfWeek.FRIDAY) dayToAdd = 3;
    if (dow == DayOfWeek.SATURDAY) dayToAdd = 2;
    return temporal.plus(dayToAdd, ChronoUnit.DAYS);
    });

String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
        
LocalDate date1 = LocalDate.parse("20160918", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2016-09-18", DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String formattedDate = date.format(formatter);
LocalDate date3 = LocalDate.parse(formattedDate, formatter);

DateTimeFormatter cnaFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.CHINA);       
String formattedDate1 = date.format(cnaFormatter);
LocalDate date4 = LocalDate.parse(formattedDate1, cnaFormatter);

DateTimeFormatter cnaFormatter1 = new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral(". ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ")
        .appendText(ChronoField.YEAR)
        .parseCaseInsensitive()
        .toFormatter(Locale.CHINA);  
String formattedDate2 = date.format(cnaFormatter1);
LocalDate date5 = LocalDate.parse(formattedDate2, cnaFormatter1);

time zones and calendars

ZoneId shanghaiZone = ZoneId.of("Asia/Shanghai");
ZoneId zoneId = TimeZone.getDefault().toZoneId();

LocalDate date = LocalDate.of(2016, 9, 18);
LocalDateTime dateTime = LocalDateTime.of(2016, 9, 18, 13, 45);
Instant instant = Instant.now();
ZonedDateTime zdt1 = date.atStartOfDay(shanghaiZone);
ZonedDateTime zdt2 = dateTime.atZone(shanghaiZone);
ZonedDateTime zdt3 = instant.atZone(shanghaiZone);

ZoneOffset beijingOffset = ZoneOffset.of("+08:00");
OffsetDateTime dateTimeInBeiJing = OffsetDateTime.of(dateTime, beijingOffset);

Chronology cnaChronology = Chronology.ofLocale(Locale.CHINA);
ChronoLocalDate now = cnaChronology.dateNow();