文章目录

  • 第十章 接口
  • 1. 抽象类和方法
  • 2. 接口创建
  • 2.1 接口创建
  • 2.2 默认方法
  • 2.3 多继承
  • 2.4 接口中的静态方法
  • 3. 抽象类和接口
  • 4. 完全解耦
  • 5. 多接口结合
  • 6. 使用继承扩展接口
  • 7. 接口适配
  • 8. 接口字段
  • 9. 接口嵌套
  • 10. 接口和工厂方法模式
  • 11. 本章小结


第十章 接口

接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。

抽象类,一种介于普通类和接口之间的折中手段。对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。

1. 抽象类和方法

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

abstract void f();

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

on java中文版 on java中文版目录_java

abstract class Basic {
    abstract void unimplemented();
}

试图创建抽象类的对象时,我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性,我们不用担心误用它。

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

可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。

—PS:包含抽象方法的类一定是抽象类,抽象类不一定包含抽象方法

为了创建可初始化的类,就要继承抽象类,并提供所有抽象方法的定义:

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();
    }
}

留意 @Override 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。

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

—PS:关键字 abstract

2. 接口创建

2.1 接口创建

使用 interface 关键字创建接口。

描述 Java 8 之前的接口更加容易,因为它们只允许抽象方法。像下面这样:

public interface PureInterface {
    int m1();

    void m2();

    double m3();
}

们甚至不用为方法加上 abstract 关键字。

Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法。

接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。

接口同样可以包含属性,这些属性被隐式指明为 staticfinal

使用 implements 关键字使一个类遵循某个特定接口(或一组接口),它表示:接口只是外形,现在我要说明它是如何工作的。

interface Concept {
    void idea1();

    void idea2();
}

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

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

你可以选择显式地声明接口中的方法为 public,但是即使你不这么做,它们也是 public 的。所以当实现一个接口时,来自接口中的方法必须被定义为 public。否则,它们只有包访问权限,这样在继承时,它们的可访问权限就被降低了,这是 Java 编译器所不允许的。

—PS:为了避免麻烦,不写修饰符就好了

2.2 默认方法

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

interface InterfaceWithDefault {
    void firstMethod();

    void secondMethod();

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

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

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

    public static void main(String[] args) {
        InterfaceWithDefault i = new Implementation2();
        i.firstMethod();
        i.secondMethod();
        i.newMethod();
    }
}

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

增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。

—PS:关键词 default ,不破坏已有(实现接口类)代码,扩展先前接口

2.3 多继承

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

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

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

只要基类方法中的方法名和参数列表不同,就能工作得很好,否则会得到编译器错误。

—PS:方法名不一样就好了

方法签名包括方法名和参数类型。

2.4 接口中的静态方法

Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具:

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);
    }
}
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());
    }
}

输出:

Bing
Crack
Twist

3. 抽象类和接口

特性

接口

抽象类

组合

新类可以组合多个接口

只能继承单一抽象类

状态

不能包含属性(除了静态属性,不支持对象

状态)

可以包含属性,非抽象方法可

能引用这些属性

默认方法 和

抽象方法

不需要在子类中实现默认方法。默认方法可

以引用其他接口的方法

必须在子类中实现抽象方法

构造器

没有构造器

可以有构造器

可见性

隐式 public

可以是 protected 或友元

有一条实际经验:尽可能地抽象。因此,更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。

—PS:所以顺序是 普通类>接口>抽象类

4. 完全解耦

当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。如果想把方法应用于那个继承层级结构之外的类,就会触霉头。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。

将接口与实现解耦使得接口可以应用于多种不同的实现,因而代码更具可复用性。

—PS:不爱就早点分开

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) {
        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);
    }
}

Applicator 的 apply() 方法可以接受任何类型的 Processor,并将其应用到一个 Object 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为策略设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。

—PS:不同子类继承同一个父类,分别重写父类方法。每个子类都是策略

5. 多接口结合

需要将所有的接口名称置于 implements 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。

—PS:一个类可以 implements 多个接口

结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。

—PS:先 extends 再 implements

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 {

    @Override
    public void swim() {

    }

    @Override
    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 hero = new Hero();
        t(hero);
        u(hero);
        v(hero);
        w(hero);
    }
}

—PS:上面的例子说明,子类可以向上转型为它的任何基类(继承与实现的)

6. 使用继承扩展接口

通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。

interface DangerousMonster {
    void destroy();
}

interface Lethal {
    void kill();
}

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

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

    @Override
    public void kill() {
    }

    @Override
    public void drinkBlood() {
    }
}

通常来说,extends 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。

—PS:extends 我还可以这样用,接口继承多个其他接口

on java中文版 on java中文版目录_开发语言_02

7. 接口适配

接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。

因此,接口的一种常见用法是前面提到的策略设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并 更具可复用性。

因为你可以以这种方式在已有类中增加新接口,所以这就意味着一个接受接口类型的方法提供了一种让任何类都可以与该方法进行适配的方式。这就是使用接口而不是类的强大之处。

—PS:任意类只要实现了这个接口,就可以传入一个接受该接口类型的方法

—PS:下面是从网上找了一个适配器模式的 demo

/**
 * 被适配类
 *
 * @author c01
 * @date 2022/11/10
 */
class AC220 {
    public int outputAC220V() {
        int output = 220;
        System.out.println("输出交流电" + output + "V");
        return output;
    }
}

/**
 * 目标接口
 *
 * @author c01
 * @date 2022/11/10
 */
interface DC5 {
    int outputDC5V();
}

/**
 * 适配类
 *
 * @author c01
 * @date 2022/11/10
 */
class PowerAdapter implements DC5 {
    private AC220 ac220 = new AC220();

    @Override
    public int outputDC5V() {
        int adapterInput = ac220.outputAC220V();
        // 变压器处理
        int adapterOutput = adapterInput / 44;
        System.out.println("使用PowerAdapter输入AC"+adapterInput+"V"+"输出DC"+adapterOutput+"V");
        return adapterOutput;
    }
}

public class TestP {
    public static void main(String[] args) {
        DC5 dc5 = new PowerAdapter();
        dc5.outputDC5V();
    }
}

—PS:原文:设计模式之适配器模式

8. 接口字段

因为接口中的字段都自动是 staticfinal 的,所以接口就成为了创建一组常量的方便的工具。在 Java 5 之前,这是产生与 C 或 C++ 中的 enum (枚举类型) 具有相同效果的唯一方式。所以你可能在 Java 5之前的代码中看到:

public interface Months {
    int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3, 
    APRIL = 4, MAY = 5, JUNE = 6, 
    JULY = 7, AUGUST = 8, SEPTEMBER = 9, 
    OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12;
}

注意 Java 中使用大写字母的风格定义具有初始化值的 static final 变量。接口中的字段自动是 public 的,所以没有显式指明这点。自 Java 5 开始,我们有了更加强大和灵活的关键字 enum,那么在接口中定义常量组就显得没什么意义了。

—PS:接口中的字段是 public static final 的,效果就是 Java 5 后的 枚举类 enum

初始化接口中的字段

接口中定义的字段不能是“空 final",但是可以用非常量表达式初始化。

public interface RandVals {
    Random RAND = new Random(47);
    int RANDOM_INT = RAND.nextInt(10);
}

空白 final 指的是没有初始化值的 final 属性。编译器确保空白 final 在使用前必须被初始化。

on java中文版 on java中文版目录_开发语言_03

9. 接口嵌套

接口可以嵌套在类或其他接口中。

class A {
    interface B {
        void f();
    }
}

interface E {
    interface G {
        void f();
    }
}

    public class BImp implements A.B {
        @Override
        public void f() {

        }
    }

在类中嵌套接口的语法是相当显而易见的。就像非嵌套接口一样,它们具有 public 或包访问权限的可见性。

—PS:知道这个就行了,特技不是一般人能掌握的

10. 接口和工厂方法模式

接口是多实现的途径,而生成符合某个接口的对象的典型方式是工厂方法设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。

interface Service {
    void method1();

    void method2();
}

interface ServiceFactory {
    Service getService();
}

class Service1 implements Service {
    Service1() {};

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

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

class Service1Factory implements ServiceFactory {
    @Override
    public Service getService() {
        return new Service1();
    }
}

class Service2 implements Service {
    Service2() {};

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

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

class Service2Factory implements ServiceFactory {
    @Override
    public Service getService() {
        return new Service2();
    }
}

public class Factories {
    static void serviceConsumer(ServiceFactory fact) {
        Service s = fact.getService();
        s.method1();
        s.method2();
    }

    public static void main(String[] args) {
        serviceConsumer(new Service1Factory());
        serviceConsumer(new Service2Factory());
    }
}

输出:

Service1 method1
Service1 method2
Service2 method1
Service2 method2

如果没有工厂方法,代码就必须在某处指定将要创建的 Service 的确切类型,从而调用恰当的构造器。为什么要添加额外的间接层呢?一个常见的原因是创建框架。

—PS:臣妾做不到啊

11. 本章小结

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

恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。

接口是一个伟大的工具,但它们容易被滥用。

on java中文版 on java中文版目录_抽象类_04


(图片来源网络,侵删)

自我学习总结:

  1. abstract 修饰的方法为抽象方法,没有方法体
  2. 含有抽象方法的类为抽象类,同样需要 abstract 修饰
  3. 包含抽象方法的类一定是抽象类,抽象类不一定包含抽象方法
  4. 声明接口关键词 implements
  5. 接口中的方法是隐式 public 修饰的
  6. 接口中的属性是隐式指明为 staticfinal
  7. Java 8 以后,接口可以增加 default 方法,用于扩展先前的接口且不破坏实现类
  8. Java 8 以后,接口可以增加静态方法,这样的接口可以当作工具类使用
  9. 可以 implements 多个接口,逗号隔开就好
  10. 接口非必要不使用,优先使用普通类