API Design with Java 8

EclipseCon - March 18, 2014

John Arthorne / @jarthorne / github.com/jarthorn
Eclipse PMC, Orion committer, IBM Canada
Clone this talk

Talk Goals

  • Not an introduction to Java 8 features
  • Look at Java 8 features from API designer perspective
  • How to add them to existing libraries in a compatible way
  • Use when designing new APIs

Why do we define API?

  • Provide a well specified and supported building block
  • Limit inter-component coupling
  • A cover story to save you from having to tell the truth
  • Allow implementations to change without ripple effect
  • Stability for clients is paramount

Forms of Compatibility

  • Binary: clients must continue to work without recompiling
  • Contract: can weaken preconditions, strengthen postconditions
  • Environment: avoid increasing restrictions on runtime environment
  • Source: not required, but make reasonable efforts

Will you please start talking about Java 8 now?

API Designer's Guide to Java Releases

  1. Ensure you run properly on new version
  2. Wait 5 years
  3. Consider adopting new language features

So Why Care About Java 8?

  • Your clients may be using Java 8 right away
  • Key Java 8 features are forwards-compatible!
  • Finally some new tools for API designers
  • Java 8 designed with compatibility in mind

New Feature Summary

  • Lambda expressions
  • Method references
  • Streams
  • Interfaces with method bodies
  • Annotations on type references

Lambda Expressions

  • Lambda expressions will appear in client code, not API
  • An interface with exactly one abstract method can be represented as lambda
  • @FunctionalInterface is optional
  • Encourages functional programming style
  • Your clients can use this today

Lambda != inner class

  • Not just syntactic sugar!!
  • Typically no class file created for lambda
  • Compiled into invokedynamic byte code that can be optimized further by JVM

Functional Example: Resource Visitor


private int doCount(IResource root) {
  int count = 1;
  if (root instanceof IContainer) {
    IResource[] kids = ((IContainer) root).members();
    for (IResource resource : kids) {
      count += doCount(resource);
    }
  }
  return count;
}
					
180 milliseconds to count 28,000 resources

Functional Example: Resource Visitor


private int doCount(IResource root) {
  final int[] count = new int[] {0};
  root.accept(proxy -> ++count[0] > 0, IResource.NONE); 
  return count[0];
}
					
5 milliseconds to count 28,000 resources (36x faster)

Why is it faster?

  • Described what we wanted, rather than how
  • Implemented with direct walk over data structure
  • Vastly less garbage


This is nothing to do with lambda expressions, but demonstrates advantage of functional style

Many Eclipse Examples

  • Most listener APIs
  • Job / IJobFunction
  • IContextFunction
  • IAdaptable
  • IResourceVisitor
  • IWorkspaceRunnable
  • IRunnableWithProgress

Once you require Java 8

  • Use class library function types where possible (java.util.function)
  • Use consistent terms: Function, Consumer, Supplier, Predicate, BinaryOperator
  • Use default methods as needed to ensure one abstract method

Gotcha: Ambiguous References


public void accept(IResourceProxyVisitor v, int d, int f)
...
public void accept(IResourceVisitor v, int d, int f)
...
//error: ambiguous reference
resource.accept(res->true, IResource,DEPTH_INFINITE, IResource.NONE);
					
Binary compatible but not source compatible

Streams

  • Represent a pipeline on which a series of functions operate
  • Infinite, lazy
  • Don't modify underlying source
  • Provide filter, collect, map, reduce, sort
  • Enables parallel execution via spliterator
  • Consider providing stream-oriented view of your data structures

Method References

  • Can provide API methods that implement common functions
  • Clients can use method references to invoke them
    • Reference static method
    • Reference instance method on an object
    • Reference instance method on objects of a given type
    • Constructor references
  • Can add to API without Java 8 dependency

Method Reference Example


public class ResourceCounter {
  private int count = 0;
  public boolean countResources(IResource resource) {
    count++;
    return true;
  }
  public boolean countFiles(IResource resource) {
    if (proxy.getType() == IResource.FILE)
      count++;
    return true;
  }
  public int getCount() {
    return count;
  }
}
				

Method Reference Example

  • Method reference appears in client code
  • None of the API involved has Java 8 dependency

ResourceCounter counter = new ResourceCounter();
root.accept(counter::countResources);
return counter.getCount();
				

Static methods in interfaces

“Not even the fifth most exciting thing in Java 8” - Alex Buckley

  • Handy for convenience methods
  • Binary compatible to add
  • Require dependency on Java 8
  • Conversion between static and default not compatible

Default Methods

  • Interfaces can now provide method bodies
  • Ideal for evolving existing interfaces implemented by clients
  • Somewhat limited: only public, no access to state

public interface Comparator<T> {
  default Comparator<T> thenComparing(Comparator<? super T> other) { ... }
}
Comparator<String> cmp = Comparator.comparingInt(String:length)
  .thenComparing(String.CASE_INSENSITIVE_ORDER);

Compatibility of Default Methods

  • Binary compatible to convert to default
  • Adding new default method: binary compatible, may break contract compatibility
  • Introducing ambiguous inheritance creates source incompatibility

Interfaces vs Abstract Classes

  • Interfaces:
    • Clean separation between API and implementation
    • Multiple inheritance, doesn't lock client into your class hierarchy
    • Can completely swap implementations
  • Abstract classes:
    • Can evolve without breaking subclasses
    • Provide function that accesses/manipulates state
    • Private state and function

Interface Evolution

  • Can't change an interface without breaking implementations
  • Anti-pattern: IPerspectiveListener4, ITextViewerExtension8
  • Solution: specify when interfaces should not be implemented by clients
  • Use abstract classes in cases where client extension is needed (IMoveDeleteHook -> TeamHook)

Interfaces With Default Methods

  • Abstract classes still much more powerful and flexible
  • Use on functional interfaces to maintain SAM restriction
  • Not needed for interfaces not implemented by clients
  • Main usage for evolving existing interfaces

Type Annotations


public String findOldest(List<@ReadOnly String> list) 
  throws @Fatal IllegalStateException { ... }
  • Can annotate type references in your API signatures
  • Can add new annotation types in your API for these contexts
  • Watch out for contract compatibility
  • Use @Target to indicate where annotation can be used
  • Feels incomplete: no built in annotations, no class library integration

Other Annotation Changes

  • Repeating annotations @Repeatable
  • Converting existing annotation to repeatable is compatible
  • Can add "this" as first formal method parameter
  • Adding "this" parameter is binary compatible

public class Dragon implements Animal{
  public String roar(Dragon this) {
    return "Roar!";
  }
}
					

Key Impressions

  • Finally some tools to help evolve APIs
  • Can start making your APIs Java 8 friendly without Java 8
  • Lambdas and streams will be a big mental shift for traditional Java developers
  • Complete integration with Java class libraries
  • Oracle's Java 8 documentation rocks, always look there first

THE END

John Arthorne / @jarthorne / github.com/jarthorn
Eclipse PMC, Orion committer, IBM Canada