好看的皮囊千篇一律,有趣的灵魂万里挑一。我再来炒一份接口的冷饭,客官香吗?
本篇博客的主要内容简介如下:
接口和抽象类是Java面向对象设计的两个基础机制。
接口(Interface)是对行为的抽象,它是抽象方法的集合,利用接口可以达到API定义和实现分离的目的。接口不能实例化;不能包含任何非常量成员,任何field都是隐含着public static final的意义;同时没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法(Java 8)。Java 标准库中,定义了非常多的接口。比如java.util.list。
抽象类(abstract class)是不能实例化的类,用abstract 关键字修饰class,其目的主要是代码重用。除了不能实例化,形式上和一般的java类没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关Java类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java标准库中,比如collection框架,很多通用部分就被抽取成抽象类,例如java.util.AbstractList。
Java 类实现 interface 使用 implements关键词,继承abstract class 则是使用extends关键词。
接口 | 抽象类 | |
---|---|---|
实例化 | no | no |
常量成员 | yes | yes |
非常量成员 | no | yes |
抽象方法个数 | ≥0 | ≥0 |
构造方法 | no | yes |
静态方法实现 | java 1.8以后有 | yes |
普通方法 | no | yes |
多继承 | yes | no |
修饰符 | public | public,protected,default |
抽象方法比接口速度要快,接口需要时间去寻找在类中的实现方法
ps:抽象方法是亲生的,接口是后娘养的
如果往抽象类中添加新的方法,你可以额外给它提供默认的实现,因此你不需要改变子类中的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。
关于二者的优缺点,第一节有讲述过。相信大家还有印象,这里也就不在重述。
在effect java 一书中明确的表明接口是优于抽象类的。但是在实际开发过程中,只是单一使用接口往往是不够的。但是我们可以提供一个抽象的骨架实现类(abstract skeletal implementation class)来与接口一起使用,将接口和抽象类的优点接口起来。接口定义了类型,可能提供了一些默认的方法,而骨架实现类在原始接口方法的顶层实现了剩余的非原始接口方法。继承骨架实现需要大部分的工作来实现一个接口。这就是模板方法设计模式
按照惯例,骨架实现类被称为 AbstractInterface ,其中 Interface 是它们实现的接口的名称。 例如,
集合框架( Collections Framework)提供了一个框架实现以配合每个主要集合接口: AbstractCollection , AbstractSet , AbstractList 和 AbstractMap 。 可以说,将它们称为 SkeletalCollection , SkeletalSet , SkeletalList 和 SkeletalMap 是有道理的,但是现在已经确立了抽象约定。 如果设计得
当,骨架实现(无论是单独的抽象类还是仅由接口上的默认方法组成)可以使程序员非常容易地提供他们自己的接口
实现。
骨架实现类的优点在于,它们提供抽象类的所有实现的帮助,而不会强加抽象类作为类型定义时的严格约束。对于具有骨架实现类的接口的大多数实现者来说,继承这个类是显而易见的选择,但它不是必需的。如果一个类不能继承骨架的实现,这个类可以直接实现接口。该类仍然受益于接口本身的任何默认方法。此外,骨架实现类仍然可以协助接口的实现。实现接口的类可以将接口方法的调用转发给继承骨架实现的私有内部类的包含实例。这种被称为模拟多重继承的技术与包装类模式密切相关。它提供了多重继承的许多好处,同时避免了缺陷。
与骨架实现有稍许不同的是简单实现,以 AbstractMap.SimpleEntry 为例。 一个简单的实现就像一个骨架
实现,它实现了一个接口,并且是为了继承而设计的,但是它的不同之处在于它不是抽象的:它是最简单的工作实
现。 你可以按照情况使用它,也可以根据情况进行子类化。
总而言之,一个接口通常是定义允许多个实现的类型的最佳方式。 如果你导出一个重要的接口,应该强烈考虑
提供一个骨架的实现类。 在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可
以使用它。 也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。
在Java 8以前,不可能在不破坏现有实现的情况下为接口添加方法,添加了,则所有的实现类均需要实现新的方法。为了不添加冗余代码,破坏原来的代码结构,default 方法也就应运而生。目前,许多的默认方法都被添加到Java 8的核心集合接口中,主要是为了方便使用lambda表达式。
public interface InterfaceA {
void add();
default void delete() {
System.out.println("调用InterfaceA的减法");
}
}
public class TestImpl implements InterfaceA {
@Override
public void add() {
System.out.println("调用了加法");
}
}
public class Main {
public static void main(String[] args) {
TestImpl test = new TestImpl();
test.add();
test.delete();
}
}
输出结果:
调用了加法
调用InterfaceA的减法
public class TestImpl implements InterfaceA {
@Override
public void add() {
System.out.println("调用了加法");
}
@Override
public void delete() {
System.out.println("调用了TestImpl的加法");
}
}
输出结果:
调用了加法
调用了TestImpl的加法
当两个接口有同样命名的default方法时,虽然不常见,但是总是会遇到的。
我们在定义一个接口InterfaceB:
public interface InterfaceB {
default void delete() {
System.out.println("调用了InterfaceB的减法");
}
}
新建一个类同时实现InterfaceA,InterfaceB:
在编写代码的过程中,编辑器就会报错让你实现default方法。聪明的你会发现,这里只让我们选择了一个InterfaceB的方法。那InterfaceA的呢?不是我截图不完整。确实只有这一个提示。这到底是为什么呢?而且本来就是重写方法,随便一个都OK的。我们来看看实现类:
public class Test2Impl implements InterfaceB, InterfaceA{
@Override
public void add() {
System.out.println("调用了Test2Impl的加法");
}
@Override
public void delete() {
System.out.println("调用了Test2Impl的减法");
}
}
输入结果:
调用了Test2Impl的加法
调用了Test2Impl的减法
两个接口有相同命名的default方法,但是二者是继承关系,猜猜到底会调用谁的default的方法?
public interface InterfaceB extends InterfaceA{
default void delete() {
System.out.println("调用了InterfaceB的减法");
}
}
public class Test2Impl implements InterfaceB{
@Override
public void add() {
System.out.println("调用了Test2Impl的加法");
}
}
public class Main {
public static void main(String[] args) {
Test2Impl test = new Test2Impl();
test.add();
test.delete();
}
}
输出结果:
调用了Test2Impl的加法
调用了InterfaceB的减法
1、default方法不能重写Object中的方法,却可以重载Objcet中的方法。
eg:toString、equals、hashCode不能在接口中被覆盖,却可以被重载
接口不能提供对Object类的任何方法的默认实现。从接口里不能提供对equals,hashCode或toString的默认实现。因为若可以会很难确定什么时候该调用接口默认的方法。
如果一个类实现了一个方法,那总是优先于默认的实现的。一旦所有接口的实例都是Object的子类,所有接口实例都已经有对equals/hashCode/toString等方法非默认 实现。因此,一个在接口上的这些默认方法都是没用的,它也不会被编译。(简单地讲,每一个java类都是Object的子类,也都继承了它类中的equals/hashCode/toString方法,那么在类的接口上包含这些默认方法是没有意义的,它们也从来不会被编译。)
2、使用default方法的时候需要考虑一下实现类的场景,必要的时候需要重写。
例如,考虑在 Java 8 中添加到 Collection 接口的 removeIf 方法。此方法删除给定布尔方法(或
Predicate 函数式接口)返回 true 的所有元素。默认实现被指定为使用迭代器遍历集合,调用每个元素的谓
词,并使用迭代器的 remove 方法删除谓词返回 true 的元素。 据推测,这个声明看起来像这样:默认实现被指
定为使用迭代器遍历集合,调用每个元素的 Predicate 函数式接口,并使用迭代器的 remove 方法删除
Predicate 函数式接口返回 true 的元素。 根据推测,这个声明看起来像这样:
default boolean removeIf(Predicate super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
这是可能为 removeIf 方法编写的最好的通用实现,但遗憾的是,它在一些实际的 Collection 实现中失败了。 例如,考虑org.apache.commons.collections4.collection.SynchronizedCollection 方法。 这个类
出自 Apache Commons 类库中,与 java.util 包中的静态工厂 Collections.synchronizedCollection 方
法返回的类相似。 Apache 版本还提供了使用客户端提供的对象进行锁定的能力,以代替集合。 换句话说,它是一个
包装类),它们的所有方法在委托给包装集合类之前在一个锁定对象上进行同步。
Apache 的 SynchronizedCollection 类仍然在积极维护,但在撰写本文时,并未重写 removeIf 方法。 如
果这个类与 Java 8 一起使用,它将继承 removeIf 的默认实现,但实际上不能保持类的基本承诺:自动同步每个方
法调用。 默认实现对同步一无所知,并且不能访问包含锁定对象的属性。 如果客户端在另一个线程同时修改集合的
情况下调用 SynchronizedCollection 实例上的 removeIf 方法,则可能会导致
ConcurrentModificationException 异常或其他未指定的行为。
为了防止在类似的 Java 平台类库实现中发生这种情况,比如 Collections.synchronizedCollection 返回
的包级私有的类,JDK 维护者必须重写默认的 removeIf 实现和其他类似的方法来在调用默认实现之前执行必要的
同步。 原来不属于 Java 平台的集合实现没有机会与接口更改进行类似的改变,有些还没有这样做。
在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。 虽然不是
非常普遍,但这个问题也不是一个孤立的事件。 在 Java 8 中添加到集合接口的一些方法已知是易受影响的,并且已
知一些现有的实现会受到影响。
应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的,在这种情况下,你应该仔细考虑,
以确定现有的接口实现是否会被默认的方法实现所破坏。然而,默认方法对于在创建接口时提供标准的方法实现非常
有用,以减轻实现接口的任务
先看代码:
public interface InterfaceA {
void add();
default void delete() {
System.out.println("调用InterfaceA的减法");
}
static void hello() {
System.out.println("你好吗,InterfaceA");
}
}
调用方式: 接口名称.方法
public class Main {
public static void main(String[] args) {
InterfaceA.hello();
}
}
运行结果:
你好吗,InterfaceA
注意:别联想着上面的重写,继承之类的。这就是一个静态方法。和我们平常写的工具类的调用方法一致。别想太多,想太多的,怪我咯。
其实最初我们对于常量放哪里,没有什么疑问?如果只和该类相关的,我么就放在这个类中。如果共用的,就放到一个常量类中,将该类的构造方法设置为私有。但是在我们阅读源码的过程中,往往会发现有人将常量放到接口中,也有放到枚举中的。
到底放到哪里才好呢?
如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接
口中。 例如,所有数字基本类型的包装类,如 Integer 和 Double ,都会导出 MIN_VALUE 和 MAX_VALUE 常
量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(条目 34)导出它们。 否则,你应该用一个不可实
例化的工具类来导出常量。
这个是我们最常用的,就贴个代码吧:
public class Constants {
private Constants(){}
public static final int MAX = 1000;
public static final int MIN = 0;
}
public interface Contants {
int MAX = 1000;
int MIN = 0;
void hello();
}
简单对比一下常量类,和接口中常量吧:
一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属
性,每个输出一个常量。 使用这些常量的类实现接口,以避免需要用类名限定常量名。
public interface Contants {
int MAX = 1000;
int MIN = 0;
}
常量接口模式是对接口的糟糕使用。 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这
个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他
们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须
实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中
的常量所污染。
Java 平台类库中有多个常量接口,如 java.io.ObjectStreamConstants。 这些接口应该被视为不规范的,
不应该被效仿。
总之,接口只能用于定义类型。 它们不应该仅用于导出常量。
老规矩先上代码:
public enum Constants {
MIN(0),
MAAS(1000);
private int count;
private Constants(int count) {
setCount(count);
}
private void setCount(int count) {
this.count = count;
}
public int getCount() {
return count;
}
}
枚举类最直接的好处就是类型检测了。这样可以在判断条件的时候,避免不同的常量名称,相同的值的状况。还可以给对应变量添加描述。每一个枚举其实都是一个类,所以占用的内存会相应的增加。不过在Java中,是鼓励开发者使用枚举的
注意:android开发,官方是不建议使用枚举类的,建议我们使用@Intef,@Stringtef的注解
@IntDef({NONE, SCALE, ROTATE, ALL})
大功告成,收功,让我们看看青青的草原,放松一下大脑。
参考: