Java编程思想精粹(九)-接口

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

这种机制在编程语言中并不常见

  • C++ 只对这种概念提供间接支持

  • Java 为它们提供了直接支持,关键字

尽管你的第一想法是创建接口,但对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。毕竟你不可能总是使用纯粹的接口。

1 抽象类

创建通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立了一个基本形式,以此表达所有派生类的共同部分。
有时把 Instrument 称为抽象基类,或简称抽象类。

对于抽象类,它的对象几乎总是没有意义。创建一个抽象类是为了通过通用接口操纵一系列类。因此只是表示接口,不是具体实现,所以创建一个抽象类的对象毫无意义,我们可能希望阻止用户这么做。通过让抽象类所有的方法产生错误,就可以达到这个目的,但是这么做会延迟到运行时才能得知错误信息,并且需要用户进行可靠、详尽的测试。最好能在编译时捕捉问题。

1.1 抽象方法

Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。如果一个抽象类是不完整的,当试图创建这个类的对象时,Java 会怎么做呢?它不会创建抽象类的对象,所以我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性,我们不用担心误用它。
如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。

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

为了创建可初始化的类,就要继承抽象类,并提供所有抽象方法的定义。留意 @Override 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。因此,你可能认为这里的 @Override 是多余的。但是,@Override 还提示了这个方法被覆写——我认为这是有用的,所以我会使用 @Override,即使在没有这个注解,编译器告诉我错误的时候。

接口只允许 public 方法,即使不加访问修饰符。然而,抽象类啥都允许。
private abstract 被禁止了是有意义的,因为你不可能在 AbstractAccess 的任何子类中合法地定义它。

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

2 接口

使用 interface 关键字创建接口。描述 Java 8 之前的接口更加容易,因为它们只允许抽象方法。

我们不用为方法加上 abstract 关键字



因为方法在接口中,Java 知道这些方法不能有方法体(仍然可以为方法加上 abstract 关键字,但是看起来像是不明白接口的小白)。

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

一个接口表示:所有实现了该接口的类看起来都像这样。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。所以,接口被用来建立类之间的协议。

Java 8 中接口稍微有些变化,因为 Java 8 允许接口包含默认方法和静态方法——基于某些重要原因,看到后面你会理解。
接口的基本概念仍然没变,介于类型之上、实现之下。
接口与抽象类最明显的区别可能就是使用上的惯用方式。

接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable

而抽象类通常是类层次结构的一部分或一件事物的类型

和类一样,需要在关键字 interface 前加上 public 关键字(但只是在接口名与文件名相同的情况下),否则接口只有包访问权限,只能在接口相同的包下才能使用它。


接口同样可以包含属性,这些属性被隐式指明为 static 和 final。这就很妙了,尤其适合存储常量,避免了一大堆重复的 public static final 修饰。

让一个类遵循某个特定接口(或一组接口)使用 implements 关键字,它表示:接口就像它看起来一样,现在我要说明它是如何工作的。除此之外,它看起来像继承。
可以显式地声明接口中的方法为 public,但是即使你不这么做,它们也是 public 的。所以当实现一个接口时,来自接口中的方法必须被定义为 public。否则,它们只有包访问权限,这样在继承时,它们的可访问权限就被降低了,这是 Java 编译器所不允许的。

2.1 默认方法

关键字 default 允许在接口中提供方法实现——在 Java 8 之前被禁止。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。
在 Java 8 之前,这些定义要在每个实现中重复实现,显得多余且令人烦恼。
默认方法比抽象类中的方法受到更多的限制,但是非常有用。
如果我们在接口中增加一个新方法 newMethod(),而在实现类中没有实现它,编译器就会报错。如果我们使用关键字 default 为 newMethod() 方法提供默认的实现,那么所有与接口有关的代码能正常工作,不受影响,而且这些代码还可以调用新的方法 newMethod()。
增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。

2.2 多继承

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

Java 在设计之初,C++ 的多继承机制饱受诟病。Java 过去是一种严格要求单继承的语言:只能继承自一个类(或抽象类),但可以实现任意多个接口。在 Java 8 之前,接口没有包袱——它只是方法外貌的描述。

现在,Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。因为接口中仍然不允许存在属性(只有静态属性),所以属性仍然只会来自单个基类或抽象类,即不会存在状态的多继承。
当两种接口中有相同签名的方法,子类实现他俩时,需要覆写冲突的方法:可以重定义 jim() 方法,也能使用 super 关键字选择基类实现中的一种。

2.3 静态方法

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

3 抽象类和接口


抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。

3.1 尽可能地抽象

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

4 完全解耦

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

5 多接口组合

接口没有任何实现——也就是说,没有任何与接口相关的存储——因此无法阻止组合多接口。这是有价值的,因为你有时需要表示“一个 x 是一个 a 和一个 b 以及一个 c”。

子类并不要求必须继承自abstract的或“具体的”(没有任何抽象方法)的父类。如果继承一个非接口类,那么只能继承一个类,其余的基元素必须都是接口。需要将所有的接口名称置于 implements 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。

类结合了具体类和接口。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。

使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。
然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。
这带来了一个问题:应该使用接口还是抽象类呢?
如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它。

6 使用继承扩展接口

通过继承,可以很容易

  • 在接口中增加方法声明

  • 在新接口中结合多个接口

这两种情况都可以得到新接口。

6.1 多接口时的命名冲突

当实现多个接口时可能会存在一个小陷阱。之前说到完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?
覆写、实现和重载会令人不快地搅和在一起、。同时,重载方法仅根据返回类型是区分不了的。所以,当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

7 接口适配

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

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

例如,类 Scanner 的构造器接受的是一个 Readable 接口。

你会发现 Readable 没有用作 Java 标准库中其他任何方法的参数——它是单独为 Scanner 创建的,因此 Scanner 没有将其参数限制为某个特定类。通过这种方式,Scanner 可以与更多的类型协作。如果你创建了一个新类并想让 Scanner 作用于它,就让它实现 Readable 接口。

假设你有一个类没有实现 Readable 接口,怎样才能让 Scanner 作用于它呢?
可以再次使用适配器模式,通过关键字 interface 提供的多继承。因为你可以以这种方式在已有类中增加新接口,所以这就意味着一个接受接口类型的方法提供了一种让任何类都可以与该方法进行适配的方式。这就是使用接口而不是类的强大之处。

8 接口字段

接口的字段都自动 static  final ,所以成为了创建一组常量的方便工具。

在 Java 5 之前,这是产生与 C 或 C++ 中的 enum (枚举类型) 具有相同效果的唯一方式。所以你可能在 Java 5 之前的代码中看到

自 Java 5 开始,我们有了更加强大和灵活的关键字 enum,那么在接口中定义常量组就显得没什么意义了。然而当你阅读遗留的代码时,在很多场合你还会碰到这种旧的习惯用法。

8.1 初始化接口中的字段

接口中定义的字段不能是“空 final”,但是可以用非常量表达式初始化。
因为字段是 static 的,所以它们在类第一次被加载时初始化,这发生在任何字段首次被访问时。
这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。

9 接口嵌套

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

在类中嵌套接口就像非嵌套接口一样,具有 public 或包访问权限的可见性。

作为一种新添加的方式,接口也可以是 private 的,例如 A.D(同样的语法同时适用于嵌套接口和嵌套类)。
但 private 嵌套接口有什么好处呢?
你可能猜测它只是被用来实现一个 private 内部类,实现 private 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式。

  • 接口中的元素必须是 public 的——所以嵌套在另一个接口中的接口自动就是 public 的


  • 不能指明为 private


    当实现某个接口时,并不需要实现嵌套在其内部的接口。

    同时,private 接口不能在定义它的类之外被实现。

    添加这些特性的最初原因看起来像是出于对严格的语法一致性的考虑,但是我通常认为,一旦你了解了某种特性,就总能找到其用武之地。

10 接口和工厂方法模式

接口是多实现的途径,而生成符合某个接口的对象的典型方式是工厂方法设计模式。

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

如果没有工厂方法,代码就必须在某处指定将要创建的 Service 的确切类型,从而调用恰当的构造器。

为什么要添加额外的间接层呢?一个常见的原因是创建框架。

11 总结

认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和一个工厂。

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

任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。这种复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到“以防万一”而添加新接口,而没有其他具有说服力的原因——好吧,如果我碰上了这种设计,就会质疑此人所作的所有其他设计了。

恰当的原则

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

往期推荐

程序员删库跑路?教你打造高可用的MySQL

从JVM 源码看init和clinit到底什么区别

使用SpringCloud三年,才知道@EnableEurekaServer注解到底有什么用

Netty源码面试解析实战(一)- 引导篇

别再用@Autowired注解了 !Spring官方都发话了

点“在看你懂得

猜你喜欢

转载自blog.csdn.net/qq_33589510/article/details/106205564