2.1.8. Favor abstract classes over interfaces for decoupling API from implementation

Note: I’ve picked this Java API Design Checklist item to let you know that  I’ve been quietly adding similar details to the checklist and will add more in the future. To see these details, open the list (also available from the main menu at the top of the page) and click on [explain] where available.

Rationale:

Safety and Evolution. Abstract classes can be used everywhere Java interfaces can with the limitation that a class can only extend a single (abstract) superclass while it can implement multiple interfaces. In most APIs Java’s lack of support for multiple class inheritance is not an issue because the interfaces are not intended to be implemented by clients. In these cases abstract classes are the better design choice.

In Java you cannot prevent clients from implementing public interfaces. When you use an interface for a method parameter type, the compiler only guarantees that it implements the correct method signatures; you can never be sure it also implements the correct behavior. You are left with only two choices: either you perform extensive runtime checks or blindly trust the implementation, risking serious consequences, such as data corruption. Unlike interfaces, abstract classes can have code to enforce constraints on behavior. For example, you can prevent clients from extending abstract classes, check preconditions, verify postconditions, and guarantee invariants as illustrated by the examples bellow. You cannot do any of these with interfaces.

Do this:

public abstract class ApiClass {

    //Package scope constructor prevents clients
    //from extending this class
    ApiClass() {}
}

Do this:

public abstract class ApiClass {

    //This public method guarantees preconditions and postconditions
    //for any abstract method implementation
    public final Collection find(String query) {
        if(query == null) {
            throw new NullPointerException("Null query parameter");
        }
        if(query.isEmpty()) {
            throw new IllegalArgumentException("Empty query parameter");
        }
        Collection result = findImplementation(query);
        if(result == null) {
            result = new ArrayList();
        }
        return result;
    }

    protected abstract Collection findImplementation(String query);
}

Do this:

public abstract class ApiClass {

    /*
    * Garanteed invariant:
    *   value != null &&
    *   value != this.lastModified &&
    *   value.before(new Date()) &&
    *   oldValue.before(newValue) &&
    *   value unchanged => class unchanged
    */
    public final Date getLastModified() {return (Date)lastModified.clone();}

    public final void anyMutatorMethod(Param param) {
        anyMutatorMethodImplementation(param);
        updateLastModified();
    } 

    protected final void updateLastModified() {
        Date newValue = new Date();
        if(lastModified.before(newValue)) {
            lastModified = newValue;
        }
    }

    protected abstract void anyMutatorMethodImplementation(Param param); 

    private Date lastModified = new Date();
}

Exceptions:

You should use abstract classes only when they are not intended for client extension. You may provide abstract classes for client extension, but you should not force clients to extend them, since Java does not support multiple inheritance. Use Java interfaces instead and provide abstract classes which implement them for convenience. You can see this approach used in the Java collections library, which contains pairs like Collection and AbstractCollection, List and AbstractList, and so on. Clients are encouraged to extend the abstract classes and even when they decide to implement the interfaces directly, they can use the provided abstract classes as guidance for a correct implementation.

For simple interfaces, such as callbacks, there is no need to provide corresponding abstract classes.

Of course, whenever possible, concrete classes are preferable to abstract classes.

Advertisement