logo

Java 8: De Facto Multiple Inheritance Language

Dec 26, 2018 · 824 words

Java's design regarding multiple inheritance is even inferior to C++. This argument is hard to accept, after all, we learned in our first Java class that "one of Java's advantages is the elimination of C++'s error-prone multiple inheritance". However, Java's design of single inheritance for classes and multiple inheritance for interfaces ultimately led Java down the old path of multiple inheritance. The final straw was Java 8's default keyword.

Why Java Was Designed with Single Inheritance

The Java language was clearly heavily influenced by C++ during its initial design. However, Java ultimately did not adopt C++'s multiple inheritance scheme. This was a distinguishing feature between Java and C++. In Java, "implementation multiple inheritance" is not allowed, meaning a class cannot inherit from multiple parent classes. But Java does allow "declaration multiple inheritance", meaning a class can implement multiple interfaces, and an interface can also inherit from multiple parent interfaces. Since interfaces only allow method declarations and not method implementations, this avoids the resolution problems associated with multiple inheritance in C++.

When designing Java's inheritance mechanism, James Gosling drew inspiration from Objective-C's concept of "pure interfaces". He found that pure interfaces without implementation bodies avoided many ambiguities and pitfalls present in C++. Thus, Java introduced the interface.

Java 8: The Introduction of the default Keyword

Java 8 can be considered the biggest change since Java 5. After the full introduction of lambda expressions and functional programming, many interfaces in the JDK also needed upgrading, such as Iterable.forEach:

 public interface Iterable<T> {
     Iterator<T> iterator();
+    void forEach(Consumer<? super T> action);
 }

However, if the forEach method were directly added to the Iterable interface, all classes implementing Iterable in Java 7 and earlier would fail to compile. To maintain backward compatibility with existing code, Java 8 introduced the default keyword and default methods, used to define methods with a body within an interface. By defining a default forEach, all classes implementing Iterable could call forEach on their objects without modifying their code.

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

default and Multiple Inheritance

At its inception, Java designed interfaces as "pure interfaces" with "no implementation" to avoid problems that might arise from multiple interface inheritance. If multiple inherited interfaces defined the same method, it was only necessary to check if the return types of the methods were consistent, for example:

public class Test {

    public interface Base {
        int doSomething();
    }

    public interface Foo extends Base {
    }

    public interface Bar extends Base {
    }

    public abstract class FooBar implements Foo, Bar {
        // The compiler checks if the return types of doSomething() in Foo and Bar are the same
    }
}

However, after the introduction of default methods, the situation became quite different. Sub-interfaces can override methods defined in parent interfaces. We can easily construct the "diamond problem" that frequently appears in C++ multiple inheritance:

public class Test {

    public interface Base {
        int doSomething();
    }

    public interface Foo extends Base {
        @Override
        default int doSomething() {
            System.out.println("Foo::doSomething");
            return 1;
        }
    }

    public interface Bar extends Base {
        @Override
        default int doSomething() {
            System.out.println("Bar::doSomething");
            return 2;
        }
    }

    public abstract class FooBar implements Foo, Bar {
        // Error: inherit unrelated default from super-interfaces
    }
}

In the example above, both Foo and Bar override doSomething, leading to an ambiguity in the meaning of doSomething within FooBar, and the compiler will report an error here.

Is an Interface Still an Interface?

The default methods introduced in Java 8 are, of course, a significant enhancement to interfaces (along with the introduction of static methods). But we can't help but wonder if the current interface is still the "pure interface" it was designed to be at Java's inception. From the diamond problem example above, we observe that interfaces with default methods behave increasingly similarly to abstract classes. Of course, interfaces cannot possess all the capabilities of abstract classes, such as private methods and fields. However, with the current capabilities of interfaces, they are already sufficient to cause multiple inheritance issues like the diamond problem.

In Java 9, to address the issue of duplicate code in default methods, private methods (and private static methods) were introduced for interfaces, further enhancing their capabilities. It can be expected that in the future, the capabilities of interfaces in Java will increasingly approach those of abstract classes. From this perspective, although Java strived to differentiate itself from C++ back then, it is still becoming more and more like C++.

Multiple inheritance is certainly a powerful language mechanism, and library and framework developers should be able to use it to implement some cool features. However, for average Java programmers, the semantic changes to interfaces introduce an additional cognitive load. Therefore, the best approach is to forget about default methods and let interfaces remain the purest form of interfaces. As for multiple inheritance, perhaps it's best left to more modern languages like Scala.