Effective Java notes (20) interface is better than abstract class

        Java provides two mechanisms that can be used to define types that allow multiple implementations: interfaces and abstract classes. Since Java 8 introduced default methods for inheritance , both mechanisms allow providing implementations for certain instance methods. The main difference is that in order to implement a type defined by an abstract class, the class must be a subclass of the abstract class. Because Java only allows single inheritance, the use of abstract classes as type definitions is limited. Any class that defines all the necessary methods and obeys the general contract is allowed to implement an interface, no matter where the class is in the class hierarchy.

        Existing classes can easily be updated to implement new interfaces . If the methods don't exist yet, all you need to do is add the necessary methods and then add an implements clause to the class declaration. For example, when the Comparable, Iterable, and Autocloseable interfaces were introduced to the Java platform, many existing classes were updated to implement these interfaces. In general, existing classes cannot be updated to extend new abstract classes. If you want two classes to extend the same abstract class, you must place the abstract class high up in the type hierarchy so that it becomes an ancestor of those two classes. Unfortunately, doing this indirectly hurts the class hierarchy, forcing all descendant classes of this common ancestor to extend this new abstract class, whether or not it is appropriate for those descendant classes.

        Interfaces are ideal for defining mixins (mixed types) . Strictly speaking, the mixin type means that in addition to implementing its "basic type", a class can also implement this mixin type to indicate that it provides some optional behaviors. For example Comparable is a mixin interface that allows a class to indicate that its instances can be ordered with other mutually comparable objects. Such an interface is called a mixin because it allows optional functionality to be mixed into the main functionality of the type. Abstract classes cannot be used to define mixins, also because they cannot be updated into existing classes: classes cannot have more than one parent class, and there is no suitable place in the class hierarchy to insert mixins.

        Interfaces allow the construction of non-hierarchical type frameworks. Type hierarchies are great for organizing some things, but other things don't neatly organize into a strict hierarchy. For example, suppose we have an interface representing a singer (singer) and another interface representing a songwriter (composer):
 

public interface Singer {
    AudioClip sing(Song s);
}
public interface Songwriter {
    Song compose(int chartPosition);
}

        In real life, some singers are also composers themselves. Because we use interfaces instead of abstract classes to define these types, it is perfectly permissible for a single class to implement both Singer and Songwriter. In fact, we can define a third interface that extends both Singer and Songwriter and adds some new methods suitable for this combination:

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

        Maybe this kind of flexibility isn't always needed, but when it is, interfaces can be the savior. Another approach is to write a bloated class hierarchy, containing a separate class for each combination of attributes to be supported. If there are n properties in the overall type system, then 2" possible combinations must be supported. This phenomenon is called "combinatorial explosion". Bloated class hierarchies lead to bloated classes, which contain many methods , and the methods differ only in the types of the parameters, since no type in the class hierarchy embodies common behavioral characteristics.

Interfaces make it possible to safely enhance the functionality of a class         through the wrapper class pattern introduced in Item 18 . If abstract classes are used to define types, programmers have no other choice but to use inheritance to increase functionality. The resulting class is less functional and more fragile than the wrapper class.

        When an interface method has an obvious implementation based on other interface methods, it can be considered to provide implementation assistance to the programmer in the form of a default method. There is a limit to the implementation assistance that can be provided by default methods. Although many interfaces define the behavior of object methods, such as equals and hashCode, they are not allowed to provide default methods. And the interface is not allowed to contain instance fields or non-public static members (except for private static methods). Finally, there is no way to add default methods to interfaces that are not under your control.

        However, by providing an abstract skeleton implementation (skeletal implementation) class for the interface, you can combine the advantages of the interface and the abstract class. The interface is responsible for defining the type, and may also provide some default methods, while the skeleton implementation class is responsible for implementing the remaining non-basic type interface methods except for the basic type interface methods. Extending the skeleton implementation accounts for most of the work beyond implementing the interface. This is the template method.

        By convention, the skeleton implementation class is called AbstractInterface, where Interface refers to the name of the implemented interface. For example, the Collections Framework provides a skeletal implementation for each of the important collection interfaces, including AbstractCollection, AbstractSet, AbstractList, and AbstractMap. It also makes sense to call them SkeletalCollection, SkeletalSet, SkeletalList, and SkeletalMap, but the usage of Abstract is now ingrained. If designed properly, a skeleton implementation (whether a single abstract class, or the only default method contained in an interface) makes it very easy for programmers to provide their own implementations of the interface. For example, here's a static factory method that includes a complete, full-featured List implementation in addition to AbstractList:

static List<Integer> intArrayAsList(int[] a) {
    Objects. requireNonNull(a);
    return new AbstractList<>() {
        @Override 
        public Integer get(int i) {
            return a[i];
        }
        @Override 
        public Integer set(int i, Integer val) {
            int oldVal = a[i];
            a[i] = val;
            return oldVal;
        }
        @Override 
        public int size() {
            return a.length;
        }
    };
}

        If you want to know what a List implementation should do for you, this example fully demonstrates the power of the skeleton implementation. Incidentally, this example is an Adapter that allows treating int arrays as lists of Integer instances. It won't perform very well due to the overhead of converting back and forth between int values ​​and Integer instances. Note that this implementation takes the form of an anonymous class.

        The beauty of skeletal implementation classes is that they provide implementation assistance for abstract classes without imposing the strict restrictions that are typical of abstract classes when they are used as type definitions. For most implementations of an interface, extending the skeleton implementation class is an obvious choice, but not required. If the preset class cannot extend the skeleton implementation class, this class can always implement this interface manually. At the same time, the class itself still benefits from any default methods present in the interface. Also, the skeleton implementation class still contributes to the implementation of the interface. Classes that implement this interface can forward calls to interface methods to instances of an internal private class that extends the skeleton implementation class. This approach is called simulated multiple inheritance, and it's closely related to the wrapper class pattern discussed in Item 18. This technology has most of the advantages of multiple inheritance, while avoiding the corresponding defects.

        Writing a skeleton implementation class is relatively simple, but the process is a bit tedious. First, you must study the interface carefully and determine which methods are the most basic, and other methods can be implemented based on them. These basic methods will become abstract methods in the skeleton implementation class. Next, provide default methods in the interface for all methods that can be directly implemented on top of the basic methods, but remember that you cannot provide default methods for object methods (such as equals and hashCode). If the base and default methods override the interface, your job is done, no skeletal implementation class is needed. Otherwise, write a class that declares that it implements the interface, and implements all remaining interface methods. This class can contain any non-public fields, as well as any methods appropriate for the task.

        Take the Map.Entry interface as an example to give a simple example. The obvious basic methods are getKey, getValue, and (optionally) setValue. The interface defines the behavior of equals and hashCode, and has an explicit toString implementation. Since it is not allowed to provide a default implementation for the object method, all implementations are placed in the skeleton implementation class:

public abstract class AbstractMapEntry<K, V>
        implements Map.Entry<K,V> {

    @Override
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry) o;
        return Objects.equals(e.getKey(), getKey())
                && Objects.equals(e.getValue(), getValue());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    @Override
    public String toString() {
        return getKey() + "=" + getValue();
    }
}

        Note that this skeleton implementation cannot be implemented in the Map.Entry interface, nor as a subinterface, because default methods are not allowed to override object methods such as equals, hashCode, and toString. Because skeleton implementation classes are designed for inheritance purposes, all design and documentation guidelines presented in Item 19 should be followed. For the sake of brevity, the documentation comment section in the above example has been omitted, but good documentation is absolutely necessary for the skeleton implementation class, whether it contains default methods in the interface or a separate abstract class.

        There is a small difference in the skeleton implementation, which is simple implementation, AbstractMap.SimpleEntry is an example. A simple implementation is like a skeletal implementation in that it implements the interface and is designed for inheritance, but the difference is that it is not abstract: it is the simplest possible valid implementation. You can use it as is, or subclass it as appropriate.

        All in all, interfaces are often the best way to define types that allow multiple implementations. If you export an important interface, you should firmly consider providing a skeleton implementation class at the same time. Moreover, as much as possible, a skeleton implementation should be provided in the interface through default methods so that all implementing classes of the interface can use it. That is to say, the restriction on the interface usually also restricts the form of the abstract class that the skeleton implementation will take.

Guess you like

Origin blog.csdn.net/java_faep/article/details/132075562