Java 101: The Next Generation

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

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

Java 101: The Next Generation

Show More
1 2 3 Page 2
Page 2 of 3

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."

1 2 3 Page 2
Page 2 of 3