We all understand that we should code to interfaces. Interfaces give the client a contract which they should use without relying on implementation details (i.e. classes). Hence, promoting loose coupling. Designing clean interfaces is one of the most important aspect of API design. One of the SOLID principle Interface segregation talks about designing smaller client-specific interfaces, instead of designing one general purpose interface. Interface design is the key to clean and effective APIs for your libraries and applications.
Code for this section is inside ch01 package.
If you have designed any API then with time you would have felt the need to add new methods to the API. Once an API is published, it becomes difficult to add methods to an interface without breaking existing implementations. To make this point clear, suppose you are building a simple Calculator
API that supports add
,subtract
, divide
, and multiply
operations. We can write a Calculator
interface, as shown below. To keep things simple we will use int
.
publicinterfaceCalculator { intadd(intfirst, intsecond); intsubtract(intfirst, intsecond); intdivide(intnumber, intdivisor); intmultiply(intfirst, intsecond); }
To back this Calculator
interface, you created a BasicCalculator
implementation, as shown below.
publicclassBasicCalculatorimplementsCalculator { @Overridepublicintadd(intfirst, intsecond) { returnfirst + second; } @Overridepublicintsubtract(intfirst, intsecond) { returnfirst - second; } @Overridepublicintdivide(intnumber, intdivisor) { if (divisor == 0) { thrownewIllegalArgumentException("divisor can't be zero."); } returnnumber / divisor; } @Overridepublicintmultiply(intfirst, intsecond) { returnfirst * second; } }
Suppose the Calculator API turned out to be very useful and easy to use. Users just have to create an instance of BasicCalculator
, and then they can use the API. You start seeing code like that shown below.
Calculatorcalculator = newBasicCalculator(); intsum = calculator.add(1, 2); BasicCalculatorcal = newBasicCalculator(); intdifference = cal.subtract(3, 2);
Oh no! Users of the API are not coding to Calculator
interface -- instead, they are coding to its implementation. Your API didn't enforce users to code to interfaces, as the BasicCalculator
class was public. If you make BasicCalculator
package protected, then you would have to provide a static factory class that will take care of providing the Calculator
implementation. Let's improve the code to handle this.
First, we will make BasicCalculator
package protected so that users can't access the class directly.
classBasicCalculatorimplementsCalculator { // rest remains same }
Next, we will write a factory class that will give us the Calculator
instance, as shown below.
publicabstractclassCalculatorFactory { publicstaticCalculatorgetInstance() { returnnewBasicCalculator(); } }
Now, users will be forced to code to the Calculator
interface, and they will not have access to implementation details.
Although we have achieved our goal, we have increased the surface area of our API by adding the new class CalculatorFactory
. Now users of the API have to learn about one more class before they can use the API effectively. This was the only solution available before Java 8.
Java 8 allows you to declare static methods inside an interface. This allows API designers to define static utility methods like getInstance
in the interface itself, hence keeping the API short and lean. The static methods inside an interface could be used to replace static helper classes (CalculatorFactory
) that we normally create to define helper methods associated with a type. For example, the Collections
class is a helper class that defines various helper methods to work with Collection and its associated interfaces. The methods defined in the Collections
class could easily be added to Collection
or any of its child interfaces.
The above code can be improved in Java 8 by adding a static getInstance
method in the Calculator
interface itself.
publicinterfaceCalculator { staticCalculatorgetInstance() { returnnewBasicCalculator(); } intadd(intfirst, intsecond); intsubtract(intfirst, intsecond); intdivide(intnumber, intdivisor); intmultiply(intfirst, intsecond); }
Some of the consumers decided to either extend the Calculator
API by adding methods like remainder
, or write their own implementation of the Calculator
interface. After talking to your users you came to know that most of them would like to have a remainder
method added to the Calculator
interface. It looked a very simple API change, so you added one more method to the API.
publicinterfaceCalculator { staticCalculatorgetInstance() { returnnewBasicCalculator(); } intadd(intfirst, intsecond); intsubtract(intfirst, intsecond); intdivide(intnumber, intdivisor); intmultiply(intfirst, intsecond); intremainder(intnumber, intdivisor); // new method added to API }
Adding a method to an interface broke the source compatibility of the API. This means users who were implementing Calculator
interface would have to add implementation for the remainder
method, otherwise their code would not compile. This is a big problem for API designers, as it makes APIs difficult to evolve. Prior to Java 8, it was not possible to have method implementations inside interfaces. This often became a problem when it was required to extend an API, i.e. adding one or more methods to the interface definition.
To allow API's to evolve with time, Java 8 allows users to provide default implementations to methods defined in the interface. These are called default, or defender methods. The class implementing the interface is not required to provide an implementation of these methods. If an implementing class provides the implementation, then the implementing class method implementation will be used -- otherwise the default implementation will be used. The List
interface has a few default methods defined, like replaceAll
, sort
, and splitIterator
.
defaultvoidreplaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); finalListIterator<E> li = this.listIterator(); while (li.hasNext()) { li.set(operator.apply(li.next())); } }
We can solve our API problem by defining a default method, as shown below. Default methods are usually defined using already existing methods -- remainder
is defined using the subtract
, multiply
, and divide
methods.
defaultintremainder(intnumber, intdivisor) { returnsubtract(number, multiply(divisor, divide(number, divisor))); }
A class can extend a single class, but can implement multiple interfaces. Now that it is feasible to have method implementation in interfaces, Java has multiple inheritance of behavior. Java already had multiple inheritance at the type level, but now it also has multiple inheritance at the behavior level. There are three resolution rules that help decide which method will be picked:
Rule 1: Methods declared in classes win over methods defined in interfaces.
interfaceA { defaultvoiddoSth(){ System.out.println("inside A"); } } classAppimplementsA{ @OverridepublicvoiddoSth() { System.out.println("inside App"); } publicstaticvoidmain(String[] args) { newApp().doSth(); } }
This will print inside App
, as methods declared in the implementing class have precedence over methods declared in interfaces.
Rule 2: Otherwise, the most specific interface is selected
interfaceA { defaultvoiddoSth() { System.out.println("inside A"); } } interfaceB {} interfaceCextendsA { defaultvoiddoSth() { System.out.println("inside C"); } } classAppimplementsC, B, A { publicstaticvoidmain(String[] args) { newApp().doSth(); } }
This will print inside C
.
Rule 3: Otherwise, the class has to call the desired implementation unambiguously
interfaceA { defaultvoiddoSth() { System.out.println("inside A"); } } interfaceB { defaultvoiddoSth() { System.out.println("inside B"); } } classAppimplementsB, A { @OverridepublicvoiddoSth() { B.super.doSth(); } publicstaticvoidmain(String[] args) { newApp().doSth(); } }
This will print inside B
.