10. Interface (2)

Chapter Summary

  • Abstract classes and interfaces
  • Completely decoupled
  • Combination of multiple interfaces
  • Use inheritance to extend the interface
    • Naming conflicts when combining interfaces

Abstract classes and interfaces

Especially after the introduction of default methods in Java 8, the choice between abstract classes and interfaces has become even more confusing. The following table makes a clear distinction:

characteristic interface abstract class
combination New classes can combine multiple interfaces Can only inherit from a single abstract class
state Cannot contain properties (except static properties, object state is not supported) Can contain properties, which may be referenced by non-abstract methods
Default methods and abstract methods There is no need to implement default methods in subclasses. Default methods can reference methods of other interfaces Abstract methods must be implemented in subclasses
constructor no constructor can have a constructor
visibility implicit public Can be protected or "friendly"

An abstract class is still a class and only one of it can be inherited when creating a new class. In the process of creating a class, multiple interfaces can be implemented.

There is a practical rule of thumb: be as abstract as possible within reason. Therefore, prefer to use interface instead of abstract class. Use abstract classes only when necessary. Do not use interfaces and abstract classes unless you must. Most of the time, normal classes are already good enough, if not, move to interfaces or abstract classes.

Completely decoupled

Whenever a method works with a class rather than an interface (when the method's parameter is a class rather than an interface), you can only apply that class or its subclasses. If you want to apply this method to a class outside the inheritance hierarchy, you can't do it. Interfaces relax this restriction to a great extent, so you can write more reusable code using interfaces.

For example, there is a class Processor with two methods name()and process(). process()Methods accept input, modify it and output it. Use this class as a base class to create various types of Processors . In the following example, each subclass of Processor modifies a String object (note that the return type may be a covariant type rather than a parameter type):

// interfaces/Applicator.java
import java.util.*;

class Processor {
    
    
    public String name() {
    
    
        return getClass().getSimpleName();
    }
    
    public Object process(Object input) {
    
    
        return input;
    }
}

class Upcase extends Processor {
    
    
    // 返回协变类型
    @Override 
    public String process(Object input) {
    
    
        return ((String) input).toUpperCase();
    }
}

class Downcase extends Processor {
    
    
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toLowerCase();
    }
}

class Splitter extends Processor {
    
    
    @Override
    public String process(Object input) {
    
    
        // split() divides a String into pieces:
        return Arrays.toString(((String) input).split(" "));
    }
}

public class Applicator {
    
    
    public static void apply(Processor p, Object s) {
    
    
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }
    
    public static void main(String[] args) {
    
    
        String s = "We are such stuff as dreams are made on";
        apply(new Upcase(), s);
        apply(new Downcase(), s);
        apply(new Splitter(), s);
    }
}

Output:

Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on
Using Processor Splitter
[We, are, such, stuff, as, dreams, are, made, on]

The Applicatorapply() method can accept any type of Processor and apply it to an Object object to output the result. Creating a method that behaves differently depending on the type of parameters passed in, like in this example, is called a strategy design pattern. The method contains the constant parts of the algorithm, and the strategy contains the changing parts. A strategy is the object passed in that contains the code to be executed. Here, the Processor object is the strategy, and main()the methods show three different strategies applied to String s .

split()It is a method in the String class. It accepts an object of String type and uses the passed parameters as the dividing boundary and returns an array String[] . It is used here to create String arrays faster .

Suppose you now discover a set of electronic filters that look like they can use Applicator methods apply():

public class Waveform {
    
    
    private static long counter;
    private final long id = counter++;
    
    @Override
    public String toString() {
    
    
        return "Waveform " + id;
    }
}
public class Filter {
    
    
    public String name() {
    
    
        return getClass().getSimpleName();
    }
    
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class LowPass extends Filter {
    
    
    double cutoff;
    
    public LowPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input; // Dummy processing 哑处理
    }
}
public class HighPass extends Filter {
    
    
    double cutoff;
    
    public HighPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class HighPass extends Filter {
    
    
    double cutoff;
    
    public HighPass(double cutoff) {
    
    
        this.cutoff = cutoff;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}
public class BandPass extends Filter {
    
    
    double lowCutoff, highCutoff;
    
    public BandPass(double lowCut, double highCut) {
    
    
        lowCutoff = lowCut;
        highCutoff = highCut;
    }
    
    @Override
    public Waveform process(Waveform input) {
    
    
        return input;
    }
}

The Filter class has the same interface elements as the Processor class, but because it doesn't inherit from Processor —because the creator of the Filter class didn't know you wanted to use it as a Processor —you can't apply the Applicatorapply() method to the Filter class , it still works fine even if you do this. Mainly because the Applicator 's apply()methods are too coupled with the Processor , which prevents the Applicator 's apply()methods from being reused. Another thing to note is that process()the input and output of the methods in the Filter class are Waveform .

But if the Processor is an interface, then the restrictions become loose enough to reuse Applicator 's apply()methods to accept parameters from that interface. Below are the modified Processor and Applicator versions:

public interface Processor {
    
    
    default String name() {
    
    
        return getClass().getSimpleName();
    }
    
    Object process(Object input);
}
public class Applicator {
    
    
    public static void apply(Processor p, Object s) {
    
    
        System.out.println("Using Processor " + p.name());
        System.out.println(p.process(s));
    }
}

The first way to reuse code is for the client programmer to write classes that follow the interface, like this:

// interfaces/interfaceprocessor/StringProcessor.java
// {java interfaces.interfaceprocessor.StringProcessor}
package interfaces.interfaceprocessor;
import java.util.*;

interface StringProcessor extends Processor {
    
    
    @Override
    String process(Object input); // [1]
    String S = "If she weighs the same as a duck, she's made of wood"; // [2]
    
    static void main(String[] args) {
    
     // [3]
        Applicator.apply(new Upcase(), S);
        Applicator.apply(new Downcase(), S);
        Applicator.apply(new Splitter(), S);
    }
}

class Upcase implements StringProcessor {
    
    
    // 返回协变类型
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toUpperCase();
    }
}

class Downcase implements StringProcessor {
    
    
    @Override
    public String process(Object input) {
    
    
        return ((String) input).toLowerCase();
    }
}

class Splitter implements StringProcessor {
    
    
    @Override
    public String process(Object input) {
    
    
        return Arrays.toString(((String) input).split(" "));
    }
}

Output:

Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]

[1] This declaration is not necessary and the compiler will not complain if it is removed. But notice that the covariant return type here changes from Object to String.

[2] S is automatically final and static because it is defined in the interface.

[3] Methods can be defined in interfaces main().

This approach works very well, however what you often encounter is that the class cannot be modified. In the case of electronic filters, for example, the class library was discovered rather than created. In these cases, the _Adapter_ design pattern can be used. Adapters allow code to accept existing interfaces and generate the required interfaces, as follows:

// interfaces/interfaceprocessor/FilterProcessor.java
// {java interfaces.interfaceprocessor.FilterProcessor}
package interfaces.interfaceprocessor;
import interfaces.filters.*;

class FilterAdapter implements Processor {
    
    
    Filter filter;
    
    FilterAdapter(Filter filter) {
    
    
        this.filter = filter;
    }
    
    @Override
    public String name() {
    
    
        return filter.name();
    }
    
    @Override
    public Waveform process(Object input) {
    
    
        return filter.process((Waveform) input);
    }
}

public class FilterProcessor {
    
    
    public static void main(String[] args) {
    
    
        Waveform w = new Waveform();
        Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
        Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
        Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w);
    }
}

Output:

Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0

In this way of using an adapter, the constructor of FilterAdapter accepts the existing interface Filter and then generates the required Processor interface object. You may also notice the use of delegates in FilterAdapter .

Covariance allows us process()to generate a Waveform instead of an Object from the method .

Decoupling the interface from the implementation allows the interface to be applied to many different implementations, making the code more reusable.

Combination of multiple interfaces

Interfaces don't have any implementations—that is, there isn't any storage associated with them—so there's no way to prevent combined multiple interfaces. This is valuable because you sometimes need to say "an x is an a and a b and a c ".

Insert image description here

A derived class is not required to inherit from an abstract or "concrete" (without any abstract methods) base class. If you inherit a non-interface class, you can only inherit one class, and the rest of the base elements must be interfaces. All interface names need to be placed after the implements keyword and separated by commas. There can be any number of interfaces, and upcasts can be made to each interface, since each interface is a separate type. The following example shows a new class resulting from a concrete class composed of multiple interfaces:

// interfaces/Adventure.java
// Multiple interfaces
interface CanFight {
    
    
    void fight();
}

interface CanSwim {
    
    
    void swim();
}

interface CanFly {
    
    
    void fly();
}

class ActionCharacter {
    
    
    public void fight(){
    
    }
}

class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
    
    
    public void swim() {
    
    }
    
    public void fly() {
    
    }
}

public class Adventure {
    
    
    public static void t(CanFight x) {
    
    
        x.fight();
    }
    
    public static void u(CanSwim x) {
    
    
        x.swim();
    }
    
    public static void v(CanFly x) {
    
    
        x.fly();
    }
    
    public static void w(ActionCharacter x) {
    
    
        x.fight();
    }
    
    public static void main(String[] args) {
    
    
        Hero h = new Hero();
        t(h); // Treat it as a CanFight
        u(h); // Treat it as a CanSwim
        v(h); // Treat it as a CanFly
        w(h); // Treat it as an ActionCharacter
    }
}

The class Hero combines the concrete class ActionCharacter and the interfaces CanFight , CanSwim and CanFly . When combining concrete classes and interfaces in this way, you need to put the concrete class first, followed by the interface (otherwise the compiler will report an error).

The method signatures of the interface CanFight and the class ActionCharacterfight() are the same, but there is no definition of in the class Hero fight(). You can extend an interface, but get another interface. When trying to create an object, all definitions must first exist. The definition of is not explicitly provided in the class Herofight() , because this method has been defined in the class ActionCharacter , which makes it possible to create a Hero object.

Four methods can be seen in the class Adventure , which take different interfaces and concrete classes as parameters. When a Hero object is created, it can be passed into any of these methods, meaning it can be cast up to each interface in turn. This interface in Java is designed in such a way that no special effort is required on the part of the programmer.

Remember, the previous example demonstrates one of the core reasons for using interfaces: to be able to upcast to multiple base types (and the flexibility that comes with it). However, the second reason for using an interface is the same as for using an abstract base class: to prevent client programmers from creating objects of this class, ensuring that this is just an interface. This brings up a question: Should I use an interface or an abstract class? If you create a base class without any method definitions or member variables, choose an interface instead of an abstract class. In fact, if you know something is a base class, consider implementing it as an interface (this topic is discussed again at the end of this chapter).

Extending interfaces using inheritance

Inheritance makes it easy to add method declarations to an interface and combine multiple interfaces in a new interface. Both cases can get the new interface, as shown in the following example:

// interfaces/HorrorShow.java
// Extending an interface with inheritance
interface Monster {
    
    
    void menace();
}

interface DangerousMonster extends Monster {
    
    
    void destroy();
}

interface Lethal {
    
    
    void kill();
}

class DragonZilla implements DangerousMonster {
    
    
    @Override
    public void menace() {
    
    }
    
    @Override
    public void destroy() {
    
    }
}

interface Vampire extends DangerousMonster, Lethal {
    
    
    void drinkBlood();
}

class VeryBadVampire implements Vampire {
    
    
    @Override
    public void menace() {
    
    }
    
    @Override
    public void destroy() {
    
    }
    
    @Override
    public void kill() {
    
    }
    
    @Override
    public void drinkBlood() {
    
    }
}

public class HorrorShow {
    
    
    static void u(Monster b) {
    
    
        b.menace();
    }
    
    static void v(DangerousMonster d) {
    
    
        d.menace();
        d.destroy();
    }
    
    static void w(Lethal l) {
    
    
        l.kill();
    }
    
    public static void main(String[] args) {
    
    
        DangerousMonster barney = new DragonZilla();
        u(barney);
        v(barney);
        Vampire vlad = new VeryBadVampire();
        u(vlad);
        v(vlad);
        w(vlad);
    }
}

The interface DangerousMonster is a new interface that is simply extended by Monster , and the class DragonZilla implements this interface.

The syntax used in Vampire only applies to interface inheritance. Generally speaking, extends can only be used for a single class, but multiple base class interfaces can be referenced when building an interface. Notice that the interface names are separated by commas.

Naming conflicts when combining interfaces

There can be a small pitfall when implementing multiple interfaces. In the previous example, CanFight and ActionCharacter have exactly the same fight()methods. There's no problem with the exact same methods, but what if their signatures or return types are different? Here's an example:

// interfaces/InterfaceCollision.java
interface I1 {
    
    
    void f();
}

interface I2 {
    
    
    int f(int i);
}

interface I3 {
    
    
    int f();
}

class C {
    
    
    public int f() {
    
    
        return 1;
    }
}

class C2 implements I1, I2 {
    
    
    @Override
    public void f() {
    
    }
    
    @Override
    public int f(int i) {
    
    
        return 1;  // 重载
    }
}

class C3 extends C implements I2 {
    
    
    @Override
    public int f(int i) {
    
    
        return 1; // 重载
    }
}

class C4 extends C implements I3 {
    
    
    // 完全相同,没问题
    @Override
    public int f() {
    
    
        return 1;
    }
}

// 方法的返回类型不同
//- class C5 extends C implements I1 {}
//- interface I4 extends I1, I3 {}

The unpleasant mix of overriding, implementation, and overloading creates difficulties. At the same time, overloaded methods are indistinguishable based solely on their return type. When the last two lines are not commented, the error message is as follows:

error: C5 is not abstract and does not override abstract
method f() in I1
class C5 extends C implements I1 {}
error: types I3 and I1 are incompatible; both define f(),
but with unrelated return types
interfacce I4 extends I1, I3 {}

When you plan to combine interfaces, using the same method name in different interfaces will often cause code readability confusion, try to avoid this situation.

Guess you like

Origin blog.csdn.net/GXL_1012/article/details/132325894