Java Note 5

Java 抽象类和接口

Posted by JH on August 24, 2020

1.抽象类和方法

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。 下面是抽象方法的声明语法:

abstract void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的, 否则,编译器会报错。如:

abstract class Basic {
    abstract void unimplemented();
}

注意:当试图创建这个类的对象时,Java并不会创建抽象类的对象。

如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。 如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。

abstract class Basic2 extends Basic {
    int f() {
        return 111;
    }

    abstract void g() {
        // unimplemented() still not implemented
    }
}

如果不想创建一个类的对象时,可以将该类(可以不包含任何抽象方法)指明为 abstract

而为了创建继承于抽象类的新类,这个新类必须提供所有抽象方法的定义。

abstract class Uninstantiable {
    abstract void f();
    abstract int g();
}

public class Instantiable extends Uninstantiable {
    @Override
    void f() {
        System.out.println("f()");
    }

    @Override
    int g() {
        return 22;
    }

    public static void main(String[] args) {
        Uninstantiable ui = new Instantiable(); //可实例化类 Instantiable
    }
}

创建抽象类和抽象方法是有帮助的,因为它们使得类的抽象性很明确,并能告知用户和编译器使用意图。 抽象类同时也是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法。

2.接口创建

2.1 Java 8 之前

Java 8 之前的接口只允许创建抽象方法(不用为方法加上 abstract 关键字)。如:

public interface PureInterface {
    int m1(); 
    void m2();
    double m3();
}

在 Java 8 之前我们可以这么说:interface 关键字产生一个完全抽象的类,没有提供任何实现。 我们只能描述类应该像什么,做什么,但不能描述怎么做,即只能决定方法名、参数列表和返回类型, 但是无法确定方法体。接口只提供形式,通常来说没有实现,只在某些受限制的情况下可以有实现。

2.2 Java 8

Java 8 允许接口包含默认方法和静态方法。接口的基本概念仍然没变,介于类型之上、实现之下。 接口与抽象类最明显的区别可能就是使用上的惯用方式。 接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable, 而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。

2.3 接口实例

interface Concept { // Package access
    void idea1();
    void idea2();
}

class Implementation implements Concept {
    @Override
    public void idea1() {
        System.out.println("idea1");
    }

    @Override
    public void idea2() {
        System.out.println("idea2");
    }
}

3.默认方法

上面提过:如果我们在接口中增加一个新方法 newMethod(), 而在实现接口的类中没有实现它,编译器就会报错。

Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。 当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。如:

interface InterfaceWithDefault {
    void firstMethod();
    void secondMethod();

    default void newMethod() {
        System.out.println("newMethod");
    }
}

public class AnImplementation implements InterfaceWithDefault {
    public void firstMethod() {
        System.out.println("firstMethod");
    }

    public void secondMethod() {
        System.out.println("secondMethod");
    }

    public static void main(String[] args) {
        AnInterface i = new AnImplementation();
        i.firstMethod();
        i.secondMethod();
    }
}

尽管 Implementation2 中未定义 newMethod(),但是可以使用 newMethod() 了。

默认方法在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法虚拟扩展方法

4.多继承

多继承意味着一个类可能从多个父类型中继承特征和特性。

Java 过去是一种严格要求单继承的语言:只能继承自一个类(或抽象类),但可以实现任意多个接口。

Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。 因为接口中仍然不允许存在属性(只有静态属性,不适用),所以属性仍然只会来自单个基类或抽象类, 也就是说,不会存在状态的多继承。如:

import java.util.*;

interface One {
    default void first() {
        System.out.println("first");
    }
}

interface Two {
    default void second() {
        System.out.println("second");
    }
}

interface Three {
    default void third() {
        System.out.println("third");
    }
}

class MI implements One, Two, Three {}

public class MultipleInheritance {
    public static void main(String[] args) {
        MI mi = new MI();
        mi.first();
        mi.second();
        mi.third();
    }
}

另外两个接口中有相同的方法名但是签名不同——方法签名包括方法名和参数类型,编译器可以区分两种方法。类可以成功实现两个接口。 但返回类型不是方法签名的一部分,因此不能用来区分方法。两个接口中有相同的方法名签名相同,返回类型不同,编译器无法区分两种方法,编译失败。 为了解决这个问题,需要覆写冲突的方法。如:

import java.util.*;

interface Jim1 {
    default void jim() {
        System.out.println("Jim1::jim");
    }
}

interface Jim2 {
    default void jim() {
        System.out.println("Jim2::jim");
    }
}

public class Jim implements Jim1, Jim2 {
    @Override
    public void jim() {
        Jim2.super.jim();
    }

    public static void main(String[] args) {
        new Jim().jim();
    }
}

5.接口中的静态方法

Java 8 允许在接口中添加静态方法,这么做可以将工具功能置于接口中。如:

import java.util.*;

public interface Operations {
    void execute();

    static void runOps(Operations... ops) {
        for (Operations op: ops) {
            op.execute();
        }
    }

    static void show(String msg) {
        System.out.println(msg);
    }
}
import java.util.*;
import Operations;

class Bing implements Operations {
    @Override
    public void execute() {
        Operations.show("Bing");
    }
}

class Crack implements Operations {
    @Override
    public void execute() {
        Operations.show("Crack");
    }
}

class Twist implements Operations {
    @Override
    public void execute() {
        Operations.show("Twist");
    }
}

public class Machine {
    public static void main(String[] args) {
        Operations.runOps(
            new Bing(), new Crack(), new Twist());
    }

6.抽象类和接口

Java 8 引入 default 方法之后,选择用抽象类还是用接口变得令人困惑。下表做了明确的区分:

特性 接口 抽象类
组合 新类可以组合多个接口 只能继承单一抽象类
状态 不能包含属性(除了静态属性,不支持对象状态) 可以包含属性,非抽象方法可能引用这些属性
默认方法 & 抽象方法 不需没有构造器要在子类中实现默认方法。默认方法可以引用其他接口的方法 必须在子类中实现抽象方法
构造器 没有构造器 可以有构造器
可见性 隐式 public 可以是 protected 或友元

实际经验:尽可能地抽象。

  • 更倾向使用接口而不是抽象类,只有当必要时才使用抽象类。
  • 除非必须使用,否则不要用接口和抽象类。大多数时候,普通类足够完成任务,如果不行的话,再考虑移动到接口或抽象类中。

7.总结

几乎任何时候,创建类都可以替代为创建一个接口和工厂。很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。 这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。

任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。

恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。 接口是一个伟大的工具,但它们容易被滥用。