The Java 8 Way – 7 → 8 – Interface defaults



The Java 8 Way – 7 → 8 – Interface defaults

0 0


java-8-way


On Github rskonnord-plos / java-8-way

The Java 8 Way

7 → 8

Java 8 finally brings lambda syntax to Java.

Lambdas don't do anything that you couldn't have also done with anonymous nested classes, but lambdas do it...

  • More tersely
  • More efficiently

Many lambda-friendly utilities have entered the core libraries along with them.

This session will focus on the difference between the "old" and "new" way of doing the same thing.

Interface defaults

Java 8 adds the ability to declare a default implementation for an interface method.

This lets you add new methods to interfaces without breaking downstream implementations, as long as you can define their behavior in terms of existing methods.

Before

public interface Doi {

  /**
   * @return a DOI name such as "10.1371/journal.pone.0000000"
   */
  String getDoiName();

}

After

public interface Doi {

  /**
   * @return a DOI name such as "10.1371/journal.pone.0000000"
   */
  String getDoiName();

  default URI getAsUri() {
    return URI.create("doi:" + getDoiName());
  }

}

Interfaces with default methods are similar to abstract classes. The differences are that interfaces:

  • Can't have instance variables (fields)
  • Can't define constructors
  • Can't make their methods final

This looks spookily like multiple inheritance. Because interfaces inherit only multiple behaviors, not multiple states, it avoids the nastiest problems.

The compiler will throw an error if one class implements two interfaces, and each one provides a different default body for the same method signature.

Functional interfaces

Java is still a statically typed language. Every value must have a declared type.

A lambda expression's type is inferred from syntax.

The inferred type must be a functional interface.

What is a functional interface?

An interface is functional if it has exactly one non-default, non-static method.

(Object methods (e.g., equals, hashCode, toString) don't count.)

The @FunctionalInterface annotation

You can use the @FunctionalInterface annotation to mark interfaces that meet the definition of functional.

It works like @Override: it is never required (even if you actually use lambdas with that interface), but the compiler will complain if you put it somewhere invalid.

You generally want to use it only on interfaces that you actually expect to get anonymous implementations. It can be misleading otherwise.

Don't put it on interfaces that you expect to add methods to one day – for example, a *Service class from our codebases.

Examples of functional interfaces

Runnable has been part of the language since 1.0, and meets the definition of functional. It didn't need the @FunctionalInterface annotation to make it so.

package java.lang;

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread...
 */
public interface Runnable {

  /**
   * When an object implementing interface <code>Runnable</code> is
   * used to create a thread, starting the thread causes the object's
   * <code>run</code> method to be called in that separately executing
   * thread...
   * @see     java.lang.Thread#run()
   */
  public abstract void run();

}

Some of us have already implemented HibernateCallback, a functional interface from Spring, many times.

package org.springframework.orm.hibernate3;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import java.sql.SQLException;

public interface HibernateCallback<T> {
  T doInHibernate(Session session)
      throws HibernateException, SQLException;
}

Java 8 provides some very flexible new ones.

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

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

@FunctionalInterface public interface Consumer<T> {
  void accept(T t);
}

@FunctionalInterface public interface Supplier<T> {
  T get();
}

Lambda syntax

@FunctionalInterface
public interface Consumer<T> {
  void accept(T t);

  public static void main(String[] args) {
    Consumer<String> printTrimmed = (String s) -> {
      String trimmed = s.trim();
      System.out.println(trimmed);
    };
  }
}

You can return things.

@FunctionalInterface
public interface Predicate<T> {
  boolean test(T t);

  public static void main(String[] args) {
    Predicate<Integer> isEven = (Integer n) -> {
      int modulus = n % 2;
      return (modulus == 0);
    };
  }
}

If it is a single return statement, you can leave off the braces and write only the returned expression.

@FunctionalInterface public interface Predicate<T> {
  boolean test(T t);

  public static void main(String[] args) {
    Predicate<Integer> isEven = (Integer n) -> {
      return (n % 2 == 0);
    };
  }
}
@FunctionalInterface public interface Predicate<T> {
  boolean test(T t);

  public static void main(String[] args) {
    Predicate<Integer> isEven = (Integer n) -> (n % 2 == 0);
  }
}

If the argument type can be inferred, you can leave it off.

@FunctionalInterface public interface Predicate<T> {
  boolean test(T t);

  public static void main(String[] args) {
    Predicate<Integer> isEven = (Integer n) -> (n % 2 == 0);
  }
}
@FunctionalInterface public interface Predicate<T> {
  boolean test(T t);

  public static void main(String[] args) {
    Predicate<Integer> isEven = n -> (n % 2 == 0);
  }
}

If it uses only a single method call, you can make it even terser with method reference syntax.

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

  public static void main(String[] args) {
    Function<String, Integer> getLength = s -> s.length();
  }
}
@FunctionalInterface public interface Function<T, R> {
  R apply(T t);

  public static void main(String[] args) {
    Function<String, Integer> getLength = String::length;
  }
}

You can even use a method reference on a particular instance.

@FunctionalInterface public interface Supplier<T> {
  T get();

  public static void main(String[] args) {
    String theValue = "resonance cascade";
    Supplier<Integer> getValueLength = () -> theValue.length();
  }
}
@FunctionalInterface public interface Supplier<T> {
  T get();

  public static void main(String[] args) {
    String theValue = "resonance cascade";
    Supplier<Integer> getValueLength = theValue::length;
  }
}

Lambdas can be inlined.

This is usually good style (but, as always, break things apart when they need to be clearer).

  List<ArticleIdentity> getIdentities(List<Article> articles) {
    Function<Article, ArticleIdentity> extractId =
          article -> ArticleIdentity.create(article.getDoi());
    return Lists.transform(articles, extractId);
  }
  List<ArticleIdentity> getIdentities(List<Article> articles) {
    return Lists.transform(articles,
        article -> ArticleIdentity.create(article.getDoi()));
  }

Your IDE is your best friend while learning new syntax.

My process for learning all of this, when I needed to supply a lambda value to something, was:

Begin typing "new..." and let IntelliJ auto-complete an old-fashioned anonymous class. It shows you explicit types for all parameters and the return value. Fill in the body. Press Alt+Enter and convert it to a lambda. Press Alt+Enter again and convert the lambda to a method reference, if possible.
  List<String> getDois(List<Article> articles) {
    return Lists.transform(articles,
        new Function<Article, String>() {
          @Override
          public String apply(Article article) {
            return article.getDoi();
          }
        });
  }
  List<String> getDois(List<Article> articles) {
    return Lists.transform(articles, article -> article.getDoi());
  }
  List<String> getDois(List<Article> articles) {
    return Lists.transform(articles, Article::getDoi);
  }

The loan pattern

A nice design pattern for surrounding a segment of code with guaranteed "before" and "after" steps.

public class NaiveService {
  private final DataSource dataSource;

  /**
   * @param  resourceName  name of resource you want
   * @return stream to resource
   *         (YOU MUST CLOSE THIS! TRY-FINALLY!
   *          SEVEN YEARS' BAD LUCK IF YOU DON'T!)
   */
  public InputStream getResource(String resourceName) {
    return dataSource.open(resourceName);
  }
}

public class IncompetentClient {
  private final NaiveService service;

  public Model servePage(String pageName) {
    Model model = new Model();
    InputStream resource = service.getResource(pageName);
    byte[] data = ByteStreams.toByteArray(resource);
    model.addAttribute("data", data);
    return model; // ...oops, forget something?
  }
}
public class CynicalService {
  private final DataSource dataSource;

  public void getResource(String resourceName,
                          Consumer<InputStream> callback) {
    try (InputStream resource = dataSource.open(resourceName)) {
      callback.accept(resource);
    }
  }
}

public class IncompetentClient {
  private final CynicalService service;

  public Model servePage(String pageName) {
    Model model = new Model();
    service.getResource(pageName, resource -> {
      byte[] data = ByteStreams.toByteArray(resource);
      model.addAttribute("data", data);
    });
    return model;
  }
}

The Optional class

Optional is a utility class that helps prevent null pointer exceptions.

Its purpose is to explicitly mark a value that, logically, may be either present or absent.

It works by convention: we NEVER allow null to be assigned to a variable of the Optional type.

Optional objects are immutable.

How to create Optional instances

Method Use it if... Optional.of(x) x is definitely not null Optional.ofNullable(x) x may be null Optional.empty() The value is always logically absent

How to unpack Optional instances

Method Use it if... boolean isPresent() Checks whether it is present. T get() Gets value only if present. Throws an exception otherwise. T orElse(T other) Gets value, or a default. orElse(null) is common.

Optional has several lambda-licious utility methods that capture the most commons use patterns.

If you get comfortable with them, you'll almost never have to write if (optional.isPresent()).

The simplest is ifPresent, which takes the if body as a Consumer.

  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    if (currentIssue.isPresent()) {
      volume.getIssues().add(currentIssue.get());
    }
  }
  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    currentIssue.ifPresent(issue -> {
      volume.getIssues().add(issue);
    });
  }

orElse provides a default value.

  private final Issue defaultIssue;
  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    volume.getIssues().add(
        currentIssue.isPresent() ? currentIssue.get() : defaultIssue);
  }
  private final Issue defaultIssue;
  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    volume.getIssues().add(currentIssue.orElse(defaultIssue));
  }

What if getting the default value is expensive?Use lazy evaluation with a Supplier!

  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    Issue next = currentIssue.isPresent() ? currentIssue.get()
        : fetchDefaultIssue();
    volume.getIssues().add(next);
  }
  private final Optional<Issue> currentIssue;

  public void addTo(Volume volume) {
    Issue next = currentIssue.orElseGet(() -> fetchDefaultIssue());
    volume.getIssues().add(next);
  }

orElseThrow takes an exception supplier. This is the best way to indicate that you expect a value to be present. It is clearer than relying on get()'s exception.

  String unpackName(Optional<String> name) {
    if (name.isPresent()) {
      return name.get();
    } else {
      throw new IllegalArgumentException("Name must be present");
    }
  }
  String unpackName(Optional<String> name) {
    return name.orElseThrow(() ->
        new IllegalArgumentException("Name must be present"));
  }

Use map to apply an operation to an Optional object's value, leaving it empty if it is empty.

  Optional<String> makeAttribution(Optional<String> name) {
    return name.isPresent()
        ? Optional.of("By: " + name.get())
        : Optional.empty();
  }
  Optional<String> makeAttribution(Optional<String> name) {
    return name.map(nameValue -> "By: " + nameValue);
  }

map and orElse chain nicely.

  String makeAttribution(Optional<String> name) {
    return name.isPresent()
        ? "By: " + name.get()
        : "(anonymous)";
  }
  String makeAttribution(Optional<String> name) {
    return name.map(nameValue -> "By: " + nameValue)
        .orElse("(anonymous)");
  }

Use filter to apply a condition.

  private final Optional<Issue> currentIssue;

  Optional<Issue> getCurrentImageIssue() {
    if (currentIssue.isPresent()) {
      return (currentIssue.get().getImageUri() != null)
          ? currentIssue : Optional.empty();
    } else {
      return Optional.empty();
    }
  }
  private final Optional<Issue> currentIssue;

  Optional<Issue> getCurrentImageIssue() {
    return currentIssue.filter(issue -> issue.getImageUri() != null);
  }

Use flatMap to capture conditional logic by mapping onto another Optional.

  private final Optional<Issue> currentIssue;

  Optional<String> getCurrentImageUri() {
    return currentIssue.isPresent()
        ? Optional.ofNullable(currentIssue.get().getImageUri())
        : Optional.empty();
  }
  private final Optional<Issue> currentIssue;

  Optional<String> getCurrentImageUri() {
    return currentIssue.flatMap(image ->
        Optional.ofNullable(currentIssue.get().getImageUri()));
  }

New Map Utilities

  private final Map<Category, Double> weights;

  public double lookUpWeight(Category category) {
    return weights.containsKey(category) ?
        weights.get(category) : 0.0;
  }
  private final Map<Category, Double> weights;

  public double lookUpWeight(Category category) {
    return weights.getOrDefault(category, 0.0);
  }
  private final CategoryService categoryService;
  private final Map<Category, Double> memo;

  public double getWeight(Category category) {
    if (memo.containsKey(category)) {
      return memo.get(category);
    }
    double weight = categoryService.lookUpWeight(category);
    memo.put(category, weight);
    return weight;
  }
  private final CategoryService categoryService;
  private final Map<Category, Double> memo;

  public double getWeight(Category category) {
    return memo.computeIfAbsent(category,
        categoryService::lookUpWeight);
  }

New Comparator Utilities

  Comparator<ArticlePerson> personOrder =
      (ArticlePerson o1, ArticlePerson o2) -> {
        int cmp = o1.getSurnames().compareTo(o2.getSurnames());
        if (cmp != 0) return cmp;
        cmp = o1.getGivenNames().compareTo(o2.getGivenNames());
        if (cmp != 0) return cmp;
        return o1.getSuffix().compareTo(o2.getSuffix());
      };
  Comparator<ArticlePerson> personOrder =
      Comparator.comparing(ArticlePerson::getSurnames)
          .thenComparing(ArticlePerson::getGivenNames)
          .thenComparing(ArticlePerson::getSuffix);

Streams

This is the best part.

Streams are a more sophisticated way to think about iteration.

You declare a chain of operations that will be applied to every element in the stream.

If you only do one piece of homework on this subject, check out the stream documentation.

Advantages of streaming over conventional iteration:

  • It is usually terser.
  • You can apply single-threaded or parallelized iteration using the same code.
  • It can be applied to data sources other than in-memory collections.
  • It lets you think functionally.

Some stream methods are intermediate operations that set up new streams.

Others are terminal operations that consume the stream to give you a result value.

Intermediate operations...

  • must be free of side effects
  • are lazily evaluated
  • may be executed in any order or not at all

A word on Collectors

Stream.collect is a terminal operation that gathers the stream elements, for example, into a collection or map.

It needs to be passed a Collector object, which provides reduction operations that will be efficient even when parallelized.

We don't want to kill all our optimizations just so we can synchronize on a list.

Stream examples

  void printAll(Collection<String> values) {
    for (String value : values) {
      System.out.println(value);
    }
  }
  void printAll(Collection<String> values) {
    values.stream().forEach(System.out::println);
  }

forEach is a terminal operation. In an intermediate operation, the side effect would be wrong.

Generally, use forEach as little as possible. Most everything you want to do has its own terminal operation.

Transforming

  List<ArticleView> createViews(Collection<Article> articles) {
    List<ArticleOutputView> views = new ArrayList<>();
    for (Article article : articles) {
      views.add(new ArticleOutputView(article));
    }
    return views;
  }
  List<ArticleOutputView> createViews(Collection<Article> articles) {
    return articles.stream()
        .map(ArticleOutputView::new)
        .collect(Collectors.toList());
  }

Filtering

  List<Article> getResearchArticles(Collection<Article> articles) {
    List<Article> researchArticles = new ArrayList<>();
    for (Article article : articles) {
      if (article.getTypes().contains("research-article")) {
        researchArticles.add(article);
      }
    }
    return researchArticles;
  }
  List<Article> getResearchArticles(Collection<Article> articles) {
    return articles.stream()
        .filter(article ->
            article.getTypes().contains("research-article"))
        .collect(Collectors.toList());
  }

We can chain operations together.

  List<ArticleView> getResearchViews(Collection<Article> articles) {
    List<ArticleView> researchArticleViews = new ArrayList<>();
    for (Article article : articles) {
      if (article.getTypes().contains("research-article")) {
        ArticleView view = new ArticleView(article);
        researchArticleViews.add(view);
      }
    }
    return researchArticleViews;
  }
  List<ArticleView> getResearchViews(Collection<Article> articles) {
    return articles.stream()
        .filter(article ->
            article.getTypes().contains("research-article"))
        .map(ArticleView::new)
        .collect(Collectors.toList());
  }

flatMap can change the number of elements in a stream. It transforms each element into a new mini-stream.

  List<ArticleAuthor> getAllAuthors(Collection<Article> articles) {
    List<ArticleAuthor> authors = new ArrayList<>();
    for (Article article : articles) {
      for (ArticleAuthor author : article.getAuthors()) {
        authors.add(author);
      }
    }
    return authors;
  }
  List<ArticleAuthor> getAllAuthors(Collection<Article> articles) {
    return articles.stream()
        .flatMap(article -> article.getAuthors().stream())
        .collect(Collectors.toList());
  }
  private boolean isFamousEnough(ArticleAuthor author) {...}

  List<ArticleAuthor> getFamousAuthors(Collection<Article> articles) {
    Set<ArticleAuthor> famousAuthors = new HashSet<>();
    for (Article article : articles) {
      if (article.getTypes().contains("research-article")) {
        for (ArticleAuthor author : article.getAuthors()) {
          if (isFamousEnough(author)) {
            famousAuthors.add(author);
          }
        }
      }
    }
    List<ArticleAuthor> sorted = new ArrayList<>(famousAuthors);
    sorted.sort(Comparator.comparing(ArticleAuthor::getSurnames));
    return sorted;
  }
  private boolean isFamousEnough(ArticleAuthor author) {...}

  List<ArticleAuthor> getFamousAuthors(Collection<Article> articles) {
    return articles.stream()
        .filter(article ->
            article.getTypes().contains("research-article"))
        .flatMap(article ->
            article.getAuthors().stream())
        .distinct() // instead of adding to a HashSet
        .filter(this::isFamousEnough)
        .sorted(Comparator.comparing(ArticleAuthor::getSurnames))
        .collect(Collectors.toList());
  }

Some other terminal operations

  int computeSum(Collection<Integer> numbers) {
    int sum = 0;
    for (Integer number : numbers) {
      sum += number;
    }
    return sum;
  }
  int computeSum(Collection<Integer> numbers) {
    IntStream intStream = numbers.stream().mapToInt(Integer::intValue);
    return intStream.sum();
  }
  int computeSum(Collection<Integer> numbers) {
    return numbers.stream().mapToInt(Integer::intValue).sum();
  }

Define your own reductions!

  int computeProduct(Collection<Integer> numbers) {
    int product = 1;
    for (Integer number : numbers) {
      product *= number;
    }
    return product;
  }
  int computeProduct(Collection<Integer> numbers) {
    return numbers.stream().mapToInt(Integer::intValue)
        .reduce(1, (x, y) -> x * y);
  }
  Article findCorrection(Collection<Article> articles) {
    for (Article article : articles) {
      if (article.getTypes().contains("correction")) {
        return article;
      }
    }
    throw new IllegalArgumentException("Correction not found");
  }
  Article findCorrection(Collection<Article> articles) {
    Optional<Article> correction = articles.stream()
        .filter(article ->
            article.getTypes().contains("correction"))
        .findAny();
    return correction.orElseThrow(() ->
        new IllegalArgumentException("Correction not found"));
  }

Stream and Optional chain together very naturally.

  Article findCorrection(Collection<Article> articles) {
    for (Article article : articles) {
      if (article.getTypes().contains("correction")) {
        return article;
      }
    }
    throw new IllegalArgumentException("Correction not found");
  }
  Article findCorrection(Collection<Article> articles) {
    return articles.stream()
        .filter(article ->
            article.getTypes().contains("correction"))
        .findAny().orElseThrow(() ->
            new IllegalArgumentException("Correction not found"));
  }

Questions?

The Java 8 Way 7 → 8