Java 101: The essential Java language features tour, Part 7

how-to
Apr 16, 201529 mins
Core JavaDevelopment ToolsJava

Java 8's method references, interface default and static methods, and more; plus features to watch for in Java 9

hopscotch game skip hop
Credit: Thinkstock
End your tour of Java’s essential language features with Jeff’s introduction to Java 8’s method references, interface default and static methods, and three lesser known language updates that every Java developer should know about. You’ll also preview some interesting additions coming up in Java 9.

The Essential Java language features tour has explored the most popular and commonly used features introduced with every version of Java since JDK 1.4. With this article we come to the end of the journey — at least for now. Lambdas and functional interfaces are two of the best-known features added to Java 8, and I covered them in Part 6. In this article we’ll get to know Java 8’s contributed method references, interface default and static methods, as well as type annotations, repeating annotations, and improvements to generic type inference. We’ll complete our tour with a preview of Java 9’s language refinements.

Method references in Java 8

Lambda expressions define anonymous methods that are treated as instances of functional interfaces. Sometimes, these expressions do nothing more than call existing methods. For example, the following code fragment uses a lambda to invoke System.out‘s void println(s) method on the lambda’s single argument — s‘s type is not yet known:


(s) -> System.out.println(s)

The lambda presents (s) as its formal parameter list and a code body whose System.out.println(s) expression prints s‘s value to the standard output stream. It doesn’t have an explicit interface type. Instead, the compiler infers from the surrounding context which functional interface to instantiate. For example, consider the following code fragment:


Consumer<String> consumer = (s) -> System.out.println(s);

The compiler analyzes the previous declaration and determines that the java.util.function.Consumer predefined functional interface’s void accept(T t) method matches the lambda’s formal parameter list ((s)). It also determines that accept()‘s void return type matches println()‘s void return type. The lambda is thus bound to Consumer.

More specifically, the lambda is bound to Consumer<String>. The compiler generates code so that an invocation of Consumer<String>‘s void accept(String s) method results in the string argument passed to s being passed to System.out‘s void println(String s) method. This invocation is shown below:


consumer.accept("Hello"); // Pass "Hello" to lambda body. Print Hello to
standard output.

To save keystrokes, you can replace the lambda with a method reference, which is a compact reference to an existing method. For example, the following code fragment replaces (String s) -> System.out.println(s) with System.out::println, where :: signifies that System.out‘s void println(String s) method is being referenced:


Consumer<String> consumer2 = System.out::println; // The method
reference is shorter.
consumer2.accept("Hello"); // Pass "Hello" to lambda body. Print Hello to
standard output.

It’s not necessary to specify a formal parameter list for the previous method reference because the compiler can infer this list based on Consumer<String>. This parameterized type’s java.lang.String actual type argument replaces T in void accept(T t), and is also the type of the single parameter in the lambda body’s System.out.println() method call.

Using method references in your programs

A method reference is a syntactic shortcut for creating a lambda from an existing method. Instead of providing an implementation body, a method reference refers to an existing class’s or object’s method. As with a lambda, a method reference requires a target type.

You can use method references to refer to a class’s static methods, bound and unbound non-static methods, and constructors. You can also use method references to refer to instance methods in superclass and current class types. I’ll introduce you to each of these method reference categories and show how they’re used in a small demo.

References to static methods

A static method reference refers to a static method in a specific class. Its syntax is className::staticMethodName, where className identifies the class and staticMethodName identifies the static method. An example is Integer::bitCount. Listing 1 demonstrates a static method reference.

Listing 1. MRDemo version 1: A static method reference


import java.util.Arrays;

import java.util.function.Consumer;

public class MRDemo
{
   public static void main(String[] args)
   {
      int[] array = { 10, 2, 19, 5, 17 };
      Consumer<int[]> consumer = Arrays::sort;
      consumer.accept(array);
      for (int x: array)
         System.out.println(x);
      System.out.println();
      int[] array2 = { 19, 5, 14, 3, 21, 4 };
      Consumer<int[]> consumer2 = (a) -> Arrays.sort(a);
      consumer2.accept(array2);
      for (int x: array2)
         System.out.println(x);
   }
}

Listing 1’s main() method sorts a pair of integer arrays via the java.util.Arrays class’s static void sort(int[] a) method, which appears in static method reference and equivalent lambda expression contexts. After sorting an array, the enhanced for loop prints the sorted array’s contents to the standard output stream.

Before we can use a method reference or a lambda, it must be bound to a functional interface. I’m using the predefined Consumer functional interface, which meets the method reference/lambda requirements. The sort operation commences by passing the array to be sorted to Consumer‘s accept() method.

Compile Listing 1 (javac MRDemo.java) and run the application (java MRDemo). You’ll observe the following output:


2
5
10
17
19

3
4
5
14
19
21

References to bound non-static methods

A bound non-static method reference refers to a non-static method that’s bound to a receiver object. Its syntax is objectName::instanceMethodName, where objectName identifies the receiver and instanceMethodName identifies the instance method. An example is s::trim. Listing 2 demonstrates a bound non-static method reference.

Listing 2. MRDemo version 2: A bound non-static method reference


import java.util.function.Supplier;

public class MRDemo
{
   public static void main(String[] args)
   {
      String s = "The quick brown fox jumped over the lazy dog";
      print(s::length);
      print(() -> s.length());
      print(new Supplier<Integer>()
      {
         @Override
         public Integer get()
         {
            return s.length(); // closes over s
         }
      });
   }

   public static void print(Supplier<Integer> supplier)
   {
      System.out.println(supplier.get());
   }
}

Listing 2’s main() method assigns a string to String variable s and then invokes the print() class method with functionality to obtain the string’s length as this method’s argument. print() is invoked in method reference (s::lengthlength() is bound to s), equivalent lambda, and equivalent anonymous class contexts.

I’ve defined print() to use the java.util.function.Supplier predefined functional interface, whose get() method returns a supplier of results. In this case, the Supplier instance passed to print() implements its get() method to return s.length(); print() outputs this length.

s::length introduces a closure that closes over s. You can see this more clearly in the lambda example. Because the lambda has no arguments, the value of s is only available from the enclosing scope. Therefore, the lambda body is a closure that closes over s. The anonymous class example makes this even clearer.

Compile Listing 2 and run the application. You’ll observe the following output:


44
44
44

References to unbound non-static methods

An unbound non-static method reference refers to a non-static method that’s not bound to a receiver object. Its syntax is className::instanceMethodName, where className identifies the class that declares the instance method and instanceMethodName identifies the instance method. An example is String::toLowerCase.

String::toLowerCase is an unbound non-static method reference that identifies the non-static String toLowerCase() method of the String class. However, because a non-static method still requires a receiver object (in this example a String object, which is used to invoke toLowerCase() via the method reference), the receiver object is created by the virtual machine. toLowerCase() will be invoked on this object. String::toLowerCase specifies a method that takes a single String argument, which is the receiver object, and returns a String result. String::toLowerCase() is equivalent to lambda (String s) -> { return s.toLowerCase(); }.

Listing 3 demonstrates this unbound non-static method reference.

Listing 3. MRDemo version 3: Unbound non-static method reference


import java.util.function.Function;

public class MRDemo
{
   public static void main(String[] args)
   {
      print(String::toLowerCase, "STRING TO LOWERCASE");
      print(s -> s.toLowerCase(), "STRING TO LOWERCASE");
      print(new Function<String, String>()
      {
         @Override
         public String apply(String s) // receives argument in parameter s;
         {                             // doesn't need to close over s
            return s.toLowerCase();
         }
      }, "STRING TO LOWERCASE");
   }

   public static void print(Function<String, String> function, String
s)
   {
      System.out.println(function.apply(s));
   }
}

Listing 3’s main() method invokes the print() class method with functionality to convert a string to lowercase along with the string to be converted as the method’s arguments. print() is invoked in method reference (String::toLowerCasetoLowerCase() isn’t bound to a user-specified object), equivalent lambda, and equivalent anonymous class contexts.

I’ve defined print() to use the java.util.function.Function predefined functional interface, which represents a function that accepts one argument and produces a result. In this case, the Function instance passed to print() implements its R apply(T t) method to return s.toLowerCase(); print() outputs this string.

Although the String part of String::toLowerCase makes it look like a class is being referenced, only an instance of this class is referenced. The anonymous class example makes this fact more obvious. Note that the anonymous class example shows that the lambda receives an argument; it doesn’t close over parameter s and is therefore not a closure.

Compile Listing 3 and run the application. You’ll observe the following output:


string to lowercase
string to lowercase
string to lowercase

References to constructors

You can use a method reference to refer to a constructor without instantiating the named class. This kind of method reference is known as a constructor reference. Its syntax is className::new. className must support object creation; it cannot name an abstract class or interface. Keyword new names the referenced constructor. Here are some examples:

  • Character::new: Equivalent to lambda (Character ch) -> new Character(ch)
  • Long::new: Equivalent to lambda (long value) -> new Long(value) or (String s) -> new Long(s)
  • ArrayList<City>::new: Equivalent to lambda () -> new ArrayList<City>()
  • float[]::new: Equivalent to lambda (int size) -> new float[size]

The last constructor reference example specifies an array type instead of a class type, but the principle is the same. The example demonstrates an array constructor reference to the “constructor” of an array type.

To create a constructor reference, specify new without a constructor. When a class such as java.lang.Long declares multiple constructors, the compiler compares the functional interface’s type against all of the constructors and chooses the best match. Listing 4 demonstrates a constructor reference.

Listing 4. MRDemo version 4: A constructor reference


import java.util.function.Supplier;

public class MRDemo
{
   public static void main(String[] args)
   {
      Supplier<MRDemo> supplier = MRDemo::new;
      System.out.println(supplier.get());
   }
}

Listing 4’s MRDemo::new constructor reference is equivalent to lambda () -> new MRDemo(). Expression supplier.get() executes this lambda, which invokes MRDemo‘s default no-argument constructor and returns the MRDemo object, which is passed to System.out.println(). This method converts the object to a string, which it prints.

Suppose you have a class with a no-argument constructor and a constructor that takes an argument, and you want to call the constructor that takes an argument. You can accomplish this task by choosing a different functional interface, such as the predefined Function interface shown in Listing 5.

Listing 5. MRDemo version 5: A functional interface


import java.util.function.Function;

public class MRDemo
{
   private String name;

   MRDemo()
   {
      name = "";
   }

   MRDemo(String name)
   {
      this.name = name;
      System.out.printf("MRDemo(String name) called with %s%n", name);
   }

   public static void main(String[] args)
   {
      Function<String, MRDemo> function = MRDemo::new;
      System.out.println(function.apply("some name"));
   }
}

Function<String, MRDemo> function = MRDemo::new; causes the compiler to look for a constructor that takes a String argument, because Function‘s apply() method requires a single (in this context) String argument. Executing function.apply("some name") results in "some name" being passed to MRDemo(String name).

Compile Listing 5 and run the application. You’ll observe output that’s similar to the following:


MRDemo(String name) called with some name
MRDemo@2f92e0f4

References to instance methods in superclass and current class types

Java’s super keyword, which can be used only in instance contexts, references an overridden method in a superclass. You may occasionally need to create a method reference that refers to a superclass method. You can do so via the following syntax:


className.super::instanceMethod

Java’s this keyword, which can be used only in instance contexts, invokes a constructor or references an instance method in the current class. You may occasionally need to create a method reference that refers to a method in the current class. You can do so via the following syntax:


this::instanceMethod

The application in Listing 6 demonstrates both syntaxes.

Listing 6. MRDemo version 6: Referencing instance methods


import java.util.function.Consumer;

class Superclass
{
   void print(String msg)
   {
      System.out.printf("Superclass print(): %s%n", msg);
   }
}

class Subclass extends Superclass
{
   Subclass()
   {
      Consumer<String> consumer = Subclass.super::print;
      consumer.accept("Subclass.super::print");
      consumer = this::print;
      consumer.accept("this::print");
   }

   @Override
   void print(String msg)
   {
      System.out.printf("Subclass print(): %s%n", msg);
   }
}

public class MRDemo
{
   public static void main(String[] args)
   {
      new Subclass();
   }
}

Listing 6 introduces the Superclass and Subclass classes, where Subclass extends Superclass. Subclass introduces a constructor and overrides Superclass‘s void print(String msg) method. I’ve also introduced an MRDemo class whose main() method drives the application by instantiating Subclass.

Subclass‘s constructor assigns a method reference to Superclass‘s print() method to a Consumer<String> variable (void print(String) matches void accept(String)‘s return type and parameter list), and invokes the method. Next, the constructor assigns a method reference to Subclass‘s print() method to this variable and invokes the method.

Compile Listing 6 and run the application. You’ll observe the following output:


Superclass print(): Subclass.super::print
Subclass print(): this::print

Method references are used to create a lambda from an existing method. In this section you’ve learned how to use them for a variety of shortcuts in your programs. Next we’ll explore Java 8’s introduction of default and static methods.

Default and static methods in Java 8

Perhaps Java 8’s most memorable language contributions are default and static methods, which you can use to add concrete functionality to interfaces. In this section you’ll learn how to use default and static methods in your programs, and also find out why including them in Java 8 was controversial.

Default methods

A default method is a concrete instance method that’s defined in an interface and whose method header is prefixed with the default keyword. An example of a default method in Java’s standard class library is java.util.Comparator<T>‘s default Comparator<T> reversed() method.

To understand the usefulness of interface-based default methods, suppose you’ve created Listing 7’s Drivable interface, which describes any kind of drivable in terms of its basic operations. (I could have called this interface Vehicle but would a horse qualify as a vehicle?)

Listing 7. Drivable.java version 1: The Drivable interface

public interface Drivable
{
   public void drive(int numUnits);
   public void start();
   public void stop();
   public void turnLeft();
   public void turnRight();
}

Being satisfied with Drivable, you make it available to other developers who use it extensively in their applications. As an example, Listing 8 shows a simple application that introduces a Car class, which implements Drivable, along with a DMDemo demonstration class.

Listing 8. DMDemo.java version 1: Car implements Drivable


class Car implements Drivable
{
   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }

   @Override
   public void start()
   {
      System.out.println("Starting car");
   }

   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }

   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }

   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}

public class DMDemo
{
   public static void main(String[] args)
   {
      Car car = new Car();
      car.start();
      car.drive(20);
      car.turnLeft();
      car.drive(10);
      car.turnRight();
      car.drive(8);
      car.stop();
   }
}

Compile Listing 8 (javac DMDemo.java) and run the application (java DMDemo). You’ll observe the following output:


Starting car
Driving car 20 kilometers
Turning car left
Driving car 10 kilometers
Turning car right
Driving car 8 kilometers
Stopping car

Suppose your manager now wants you to add a demo() method to Drivable. Adding a method to an interface breaks binary compatibility, so every class implementing Drivable must be retrofitted to also implement demo(). To avoid this aggravation, you add demo() to Drivable as a default method, as shown in Listing 9.

Listing 9. Drivable version 2: Drivable with a default demo() method


public interface Drivable
{
   public void drive(int numUnits);
   public void start();
   public void stop();
   public void turnLeft();
   public void turnRight();

   public default void demo()
   {
      start();
      drive(20);
      turnLeft();
      drive(10);
      turnRight();
      drive(8);
      stop();
   }
}

demo() exists as an instance method with a default implementation. Because it doesn’t need to be implemented by classes that implement Drivable, older code won’t break when executed with a binary version of this interface. Listing 10 proves that binary compatibility isn’t compromised and shows you how to invoke demo().

Listing 10. DMDemo.java version 2: Car doesn’t implement demo()


class Car implements Drivable
{
   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }

   @Override
   public void start()
   {
      System.out.println("Starting car");
   }

   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }

   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }

   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}

public class DMDemo
{
   public static void main(String[] args)
   {
      Car car = new Car();
      car.demo();
   }
}

Listing 10 shows that the Car class hasn’t been modified; it doesn’t have to implement demo(). It also shows that demo() is invoked like any other instance method that’s a member of the Car class. If you run this application, you’ll observe the same output I showed previously.

Overriding a default method

Default methods can be overridden when necessary. Remember that default methods are implicitly public, so don’t assign a weaker access privilege when you override one. If you do that you’ll receive a compiler error. Listing 11 shows how to override demo().

Listing 11. Car excerpt from third version of DMDemo.java


class Car implements Drivable
{
   @Override
   public void demo()
   {
      start();
      drive(20);
      stop();
   }

   @Override
   public void drive(int numUnits)
   {
      System.out.printf("Driving car %d kilometers%n", numUnits);
   }

   @Override
   public void start()
   {
      System.out.println("Starting car");
   }

   @Override
   public void stop()
   {
      System.out.println("Stopping car");
   }

   @Override
   public void turnLeft()
   {
      System.out.println("Turning car left");
   }

   @Override
   public void turnRight()
   {
      System.out.println("Turning car right");
   }
}

More about default methods

You cannot implement any of Object‘s public non-final methods as default methods in an interface. Brian Goetz has explained the rationale for this restriction in a posting to the Project Lambda mailing list.

To invoke the default implementation of a method that has been overridden or isn’t available because of conflicting default implementations in different interfaces, you would prefix keyword super with the name of the interface containing the default method. Listing 12 presents a demonstration.

Listing 12. DMDemo.java version 4: Invoking a default implementation with super


interface A
{
   default void method()
   {
      System.out.println("A.method() invoked");
   }
}

public class DMDemo implements A
{
   @Override
   public void method()
   {
      System.out.println("DMDemo.method() invoked");
      A.super.method();
   }

   public static void main(String[] args)
   {
      DMDemo dmdemo = new DMDemo();
      dmdemo.method();
   }
}

After compiling Listing 12, run the application and you’ll observe the following output:

DMDemo.method() invoked
A.method() invoked

Static methods

A static method is a method that’s associated with the class in which it’s defined. Every object shares the static methods of its class. Java 8 also lets you define static methods in interfaces. An example from Java’s standard class library is java.lang.invoke.MethodHandleInfo‘s static String referenceKindToString(int referenceKind) method.

To understand the usefulness of interface-based static methods, consider a Drawable interface with a draw() method whose int argument contains the drawing color. It’s convenient to express this color as red, green, and blue components, so you add an rgb() static method that converts these components to an int. Check out Listing 13.

Listing 13. Drawable.java


public interface Drawable
{
   public void draw(int color);

   public static int rgb(int r, int g, int b)
   {
      return r << 16 | g << 8 | b;
   }
}

Continuing, you create an application consisting of a Circle class that describes a circle via integer-based center coordinates and a radius, and which implements Drawable to draw a circle. You also create an SMDemo class whose main() method runs the application. Listing 14 presents the source code for these classes.

Listing 14. SMDemo.java


class Circle implements Drawable
{
   private int x, y, r;

   Circle(int x, int y, int r)
   {
      this.x = x;
      this.y = y;
      this.r = r;
   }

   @Override
   public void draw(int color)
   {
      System.out.printf("Drawing circle(%d, %d, %d) in color %x%n", x, y, r,
                        color);
   }
}

public class SMDemo
{
   public static void main(String[] args)
   {
      Circle circle = new Circle(10, 20, 5);
      circle.draw(Drawable.rgb(0x80, 0x60, 0x40));
   }
}

After instantiating Circle, its draw() method is called to draw the circle. Notice that the rgb() method call is prefixed by this static method’s interface. You cannot prefix this method with the interface-implementing class (as in Circle.rgb(0x80, 0x60, 0x40)) because the static method belongs to the interface.

Compile Listing 14 (javac SMDemo.java) and run the application (java SMDemo). You’ll observe the following output:


Drawing circle(10, 20, 5) in color 806040

The case for and against default and static methods

Many developers have criticized Oracle’s decision to add default and static methods to interfaces. An example is this question from a Stack Exchange user, making the case that static methods belong in utility classes and that default methods shouldn’t be allowed.

Some developers see adding default and static methods to interfaces as a corruption of the interface’s purpose, which is to describe a contract without implementation. According to this logic, it now appears that there’s no difference between interfaces and abstract classes, except for interfaces supporting multiple inheritance and abstract classes supporting non-constant fields.

This type of interface “abuse” has happened before: years ago, developers tended to create constant-only interfaces in order to save keystrokes. A class would implement a constant-only interface so that constant names without their interface name and period character prefixes could be specified. Static imports made this practice obsolete.

So the question is, why add these controversial methods to Java 8? Two answers have emerged from Oracle’s development team.

In the paper “Interface evolution via virtual extension methods” (PDF) Brian Goetz noted that the addition of closures placed new stress on Java’s collections interfaces: “one of the most significant benefits of closures is that it enables the development of more powerful libraries. It would be disappointing to add a language feature that enables better libraries while at the same time not extending the core libraries to take advantage of that feature.”

To evolve the Java Collections Framework to support Java 8’s Streams API, it was necessary to evolve the interface feature (on which the framework is based) to support default and static methods without breaking binary compatibility.

Another reason for supporting default and static methods in interfaces is to better organize library methods. For example, now that default void sort(Comparator<? super E> c) has been added to java.util.List<E>, you can more naturally write myList.sort(null); instead of having to write Collections.sort(myList);.

Three lesser-known Java 8 language features

Lambdas, functional interfaces, method references, and interface default and static methods have significantly changed the Java language, but these weren’t Java 8’s only new language contributions. Type annotations, repeating annotations, and improved generic type inference are some of Java 8’s lesser known, but still powerful, language features.

Type annotations

Before Java 8, you could only apply annotations to declarations. In Java 8 annotations can be applied to any use of a type, such as object creation expressions (e.g., new @Immutable Contact("John Doe", "x@y")), type casts (e.g., id = (@NonNull String) getID()), and throws clauses (e.g., void login() throws @Fatal AccessDeniedException).

Type annotations were introduced to help improve the analysis of Java programs by ensuring stronger type checking. Because Java 8 doesn’t provide a type-checking framework, download the Checker Framework to play with type annotations. For more Checker tutorials, see Dustin Marx’s introduction to the Checker Framework and “Practical pluggable types for Java” (PDF) by Matthew Papi.

Java 8 supports type annotations by extending the java.lang.annotation.ElementType enum with new constants TYPE_PARAMETER (type parameter declaration) and TYPE_USE (use of a type). TYPE_PARAMETER annotates type variables, such as E in Queue<E>. TYPE_USE annotates any use of a type (including type parameter declarations).

Repeating annotations

Java 8 also introduced repeating annotations, an annotations enhancement for applying multiple instances of an annotation type to a declaration or type use. For example, as Listing 15 shows, say you’ve declared an Account class and want to annotate it with multiple @ToDo annotations to specify the various tasks left to accomplish.

Listing 15. Account.java

@ToDo("Add Javadoc")
@ToDo("Add constructor")
@ToDo("Add deposit() method")
@ToDo("Add withdrawal() Method")
public class Account
{
}

A pre-Java 8 compiler would generate an error upon encountering multiple instances of the same annotation type being applied to an element. However, this is no longer a problem. To make Listing 15 legal, you first need to declare an appropriate ToDo annotation type, such as the type shown in Listing 16.

Listing 16. ToDo.java


import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ToDos.class)
public @interface ToDo
{
   String value();
}

The difference between a repeating and a non-repeating annotation type is the application of the @Repeatable meta-annotation to the annotation type. The java.lang.annotation.Repeatable annotation type (new in Java 8) indicates that the annotation type whose declaration it annotates (ToDo) is repeatable.

For compatibility reasons with previous Java versions, repeating annotations are stored in a container annotation that is automatically generated by the Java 8 compiler. The container annotation type must be specified as @Repeatable‘s value. In this case, ToDos.class is specified as this value. Listing 17 presents ToDos.java.

Listing 17. ToDos.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ToDos
{
   ToDo[] value();
}

The containing annotation type (ToDos) must have a value element with an array type. Also, the component type of the array type must be the repeatable annotation type (ToDo). Otherwise, the compiler will report one or more errors. Although you must specify the container annotation type, you don’t have to specify the container annotation.

I’ve specified the same target type and retention policy to avoid compiler errors. For example, if I had omitted the @Target annotation, I would have received an error about ToDos being applicable to more targets than ToDo. If I had omitted @Retention, the error would have stated that ToDos has a shorter retention than ToDo.

java.lang.Class declares a <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass) method that you can call to return an array of type annotations identified by annotationClass. Listing 18 shows you how to use this method to return Account‘s @ToDo annotations.

Listing 18. RADemo.java


public class RADemo
{
   public static void main(String[] args) throws ClassNotFoundException
   {
      ToDo[] annotations =
         Class.forName("Account").getAnnotationsByType(ToDo.class);
      for (ToDo annotation: annotations)
         System.out.println(annotation.value());
   }
}

Listing 18’s main() method first invokes Class.forName("Account") to load Account — this method throws ClassNotFoundException when it cannot find Account. When found, it invokes getAnnotationsByType(ToDo.class) to return an array of @ToDo annotations. The array is iterated over and each annotation’s value is output.

Compile Listing 18 (javac RADemo.java) and run the application (java RADemo). You should observe the following output:


Add Javadoc
Add constructor
Add deposit() method
Add withdrawal() Method

Improved generic type inference

Java 8 enhances the Java compiler to use target typing when inferring a generic method invocation’s type parameters — an expression’s target type is the type that the Java compiler expects the expression to have depending on the expression’s location. Consider Collections.emptyList, which has the following declaration:


static <T> List<T> emptyList()

Now, consider the following assignment statement:


List<String> moons = Collections.emptyList();

This statement expects to receive an instance of List<String>, which is the target type. Because emptyList() returns List<T>, the compiler infers that type argument T must be String. Java 7 and Java 8 support this inference. Alternatively, you could employ a type witness and specify T‘s value as shown below:

List<String> moons =
Collections.<String>emptyList();

Using a type witness in this context isn’t necessary. However, it is necessary in other contexts. For example, consider the following constructor that initializes a Planet object to its name and list of moons:


Planet(String name, List<String> moons)
{
   this.name = name;
   this.moons = moons;
}

Suppose you invoke this constructor using an empty list, as follows:


Planet mercury = new Planet("Mercury", Collections.emptyList());

This statement doesn’t compile under Java 7, which outputs an error message that’s similar to the following message:


List<Object> cannot be converted to List<String>

Java 7’s compiler needs a value for type argument T and chooses Object. As a result, Collections.emptyList() returns a value of type List<Object>, which isn’t compatible with Planet(String name, List<String> moons). To make it compatible, you must use a type witness, as follows:

Planet mercury = new Planet("Mercury",
Collections.<String>emptyList());

Java 8 doesn’t need a type witness because method arguments are eligible target types. In this case, Planet(String name, List<String> moons) needs a second argument of type List<String>. Because Collections.emptyList() returns a value of type List<T>, Java 8 infers that T‘s value is String.

Type annotations, repeating annotations, and improved generic type inference haven’t risen to the attention of many Java developers, but these are three features that will improve the efficiency of your Java programs if you use them correctly.

Refining the Java language in Java 9

As of this writing, Java 9 is expected to debut in early 2016. Like lambda expressions in Java 8, modularity will receive the lion’s share of attention from developers and others, but smaller language updates are worth looking out for in Java 9. These updates focus on Project Coin and smarter compilation.

Small changes in Project Coin

Java Enhancement Proposal 213: Milling Project Coin offers the following changes to the Project Coin language features introduced in JDK 7:

Smarter compilation

Three projects seek to improve compiler shortcomings in Java 9:

JEP 216 would enable javac to accept and reject programs “regardless of the order of import statements and extends and implements clauses.” The code fragment below demonstrates the need for this enhancement:


package P;

import static P.Outer.Nested.*;

import P.Q.*;

public class Outer
{
   public static class Nested implements I
   {
   }
}

package P.Q;

public interface I
{
}

The above code describes the contents of two source files, Outer.java and I.java. Outer.java declares class Outer in package P whose nested Nested class implements interface I. I.java declares interface I in package P.Q.

JEP 216 describes the steps that the compiler takes to compile this source code. These steps lead to the following error message:


POuter.java:9: error: cannot find symbol
   public static class Nested implements I
                                         ^
  symbol:   class I
  location: class Outer
1 error

Fortunately the fix is simple: if you swap the order of import static P.Outer.Nested.*; and import P.Q.*;, the compiler is able to locate interface I and successfully compiles the source code.

In conclusion

In Java 8 the Java language evolved dramatically via the addition of lambdas and functional interfaces. In this article you’ve seen how method references, interface default and static methods, and lesser-known features can also make a difference in the quality and efficiency of your Java programs. Java 9 is similar; while modularity takes center stage, subtler language enhancements will improve the overall experience of writing Java code, as well as the function of your programs. As an example I’ve demonstrated how Java 9 will smooth the rough edges from the Project Coin feature set and enhance the function and intelligence of the Java compiler.

This is the final article in the Essential Java language features tour, which started with an introduction to assertions in JDK 1.4 and has proceeded through every major platform release since then. I’m sure I’ll return to Java language features in a future Java 101 article, so watch this space for updates.