Use Java8's default methods sparingly

  • 2020-04-01 04:37:44
  • OfStack

Default methods add a nice new feature to the JVM's instruction set. After using the default method, if a new method is added to the interface in the library, the user class that implements the interface automatically gets the default implementation of the method. As soon as the user wants to update his implementation class, he simply overrides the default method and replaces it with an implementation that makes more sense in certain scenarios. Even better, the user can call the default implementation of the interface in the overridden method to add some additional functionality.

So far so good. However, to Adding default methods to existing Java interfaces can cause code incompatibilities . It's easy to see if you look at an example. Suppose you have a library that requires the user to implement one of its interfaces as input:


interface SimpleInput {
 void foo();
 void bar();
}
 
abstract class SimpleInputAdapter implements SimpleInput {
 @Override
 public void bar() {
 // some default behavior ...
 }
}
 

Before Java 8, this combination of interface and a corresponding adapter class was a common pattern in the Java language. The developer of the class library provides an adapter to reduce the amount of coding for the library consumer. However, the purpose of providing this interface is to implement a relationship similar to multiple inheritance.

Let's say a user USES the adapter:


class MyInput extends SimpleInputAdapter{
 @Override
 public void foo() {
 // do something ...
 }
 @Override
 public void bar() {
 super.bar();
 // do something additionally ...
 }
}
 

With this implementation, the user can interact with the library. Notice how this implementation overrides the bar method to add additional functionality to the default implementation.

What happens if the library is migrated to Java 8? First, the library will probably discard the adapter class and migrate the functionality to the default method. Eventually the interface will look like this:


interface SimpleInput {
 void foo();
 default void bar() {
 // some default behavior
 }
}}
 

With the new interface, the user has to update his code to use the default method instead of the adapter class. One of the great benefits of using the new interface instead of the adapter class is that the user can inherit from a different class instead of the adapter class. Let's do it by changing the MyInput class to use the default method. Now that we can inherit from other classes, let's try to extend a third party base class as well. It doesn't matter what the base class does, but let's assume that it makes sense for our use case.


class MyInput extends ThirdPartyBaseClass implements SimpleInput {
 @Override
 public void foo() {
 // do something ...
 }
 @Override
 public void bar() {
 SimpleInput.super.foo();
 // do something additionally ... 
 }
}
 

To achieve the same functionality as the original class, here we use the new Java 8 syntax to invoke the interface's default methods. Again, we put the myMethod logic in some base class called MyBase. You can kick your shoulders and relax. Great after refactoring!

The library we use has been greatly improved. However, the maintainer needs to add another interface to implement some additional functionality. This interface, called CompexInput, inherits the SimpleInput class and adds an additional method. Because it is generally assumed that default methods can be added safely, the maintainer overrides the default methods of the SimpleInput class and adds some additional actions to provide a better default implementation for the user. After all, this is also common when using adapter classes:


interface ComplexInput extends SimpleInput {
 void qux();
 @Override
 default void bar() {
 SimpleInput.super.bar(); 
 // so complex, we need to do more ...
 }
} 
 

This new feature looked so good that the maintainer of the ThirdPartyBaseClass class decided to use the library as well. To do this, he implements the ComplexInput interface with the ThirdPartyBaseClass class.

But what does that mean for the MyInput class? Because it inherits the ThirdPartyBaseClass class, it implements the ComplexInput interface by default, making it illegal to call the default method of SimpleInput. As a result, the user's code won't compile. Also, you can't call this method at all now, because Java considers it illegal to call super-super methods of its indirect parent class. You can only call the default method of the ComplexInput interface. However, you first need to explicitly implement the interface in the MyInput class. These changes are completely unexpected for users of the library.

(note: to put it simply:


interface A {
 default void test() {
  
 }
}

interface B extends A {
 default void test() {
  
 }
}

public class Test implements B {
 public void test() {
  B.super.test();
  //The (A.s uper); error
 }


}

Of course, the way to write this is that the user chooses to implement the B interface voluntarily. However, the example in this paper introduces a base class, so the user code cannot be compiled due to a seemingly insignificant change in both the library and the base class.

Oddly enough, Java does not distinguish between these at runtime. The JVM's validator allows a compiled class to make calls to the SimpleInput: : foo method, although the loaded class inherits a newer version of ThirdPartyBaseClass and implicitly implements the ComplexInput interface. Blame the compiler. (note: the compiler does not behave as the runtime does.)

So what did we learn? Simply put, do not override the default methods of the original interface in another interface. Don't override it with another default method, or with some abstract method. In summary, you should be careful when using the default method. While they make it easier to improve the interfaces of Java's existing collection libraries, they essentially add complexity by allowing you to make method calls in the class's inheritance structure. Before Java 7, you just had to walk through the linear class hierarchy to see the actual code being called. Use the default method when you feel you really need it.

The above is a detailed explanation of why you should be careful to use Java8's default methods.


Related articles: