Dabai的个人博客

深入理解Java:抽象类和接口

抽象类和接口是Java中的重要类型,对Java实现灵活的继承和多态有极其重要的作用。关于抽象类和接口的用法教程有很多,本篇文章主要用来记录Java中抽象类和接口的一些重要的概念和特殊的用法,从而加深对抽象类和接口的理解。

1. 关于抽象类

抽象类可以不包含抽象方法,在这种情况下,这种类是用来定义子类的基类,这也是抽象类最常用的一种方式。抽象类的抽象方法不能在抽象类中实现,当子类继承一个抽象类时,该子类要么实现父类的所有抽象方法,要么将该子类定义为一个抽象类。虽然不能根据抽象类new一个对象实例,但抽象类也有构造方法,包括默认的和自定义的构造方法,当子类继承抽象类并生成一个对象时,会先调用抽象类的构造方法。

2. 关于接口

2.1 接口的定义和声明

由于接口中所有的数据域都是public、static、final,而且所有的方法都是public abstract,所以Java允许忽略这些修饰符。因此下面的接口是等价的:

1
2
3
4
interface A {
int k = 1;
void m1();
}

等价于:

1
2
3
4
interface A {
public static final int k = 1;
public abstract void m1();
}

接口中不能包含方法的实现,下面定义的接口类型是错误的:

1
2
3
4
5
interface A {
int k = 1;
//接口中所有的方法都是public abstract,抽象方法不能在抽象类中实现
void m1() {}
}

但要注意的是继承该接口的子类实现接口的方法时要明确指明该方法的访问权限是public,不能是默认访问权限或其它访问权限,下面的是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class B implements A {
@Override
void m1() {
System.out.println("m1" + k);
}
@Override
public String toString() {
return "class B";
}
public static void main(String[] args) {
B c = new B();
System.out.println(c);
}
}

正确的接口继承方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class B implements A {
@Override
public void m1() {
System.out.println("m1" + k);
}
@Override
public String toString() {
return "class B";
}
public static void main(String[] args) {
B c = new B();
System.out.println(c);
}
}

2.2 使用接口声明对象的引用

使用接口声明一个对象的引用时,实际上是将该对象向上转型为对应的接口类型,这样可以方便我们编写代码时与接口打交道,而不用考虑具体的实现。例如,可以使用List声明一个指向ArrayList对象的引用:

1
List<Apple> apples = new ArrayList<>();

在这里,ArrayList已经向上转型为List,这样声明可以方便我们想要修改实现的时候,只需在创建时修改它:

1
List<Apple> apples = new LinkedList<>();

因此,在创建一个具体对象时,应多考虑将其转型为一个对应的接口,然后在其余的代码中使用这个接口。这也呼应了Java编程中的“针对接口编程”的思想了。

注意: 由于一个类可以实现多个接口,接口也可以继承,因此可以通的接口类型声明一个对象的引用。而对象的属性是由类本身决定的,因此对该对象的本身来说,没有本质的差异。但由于引用可访问的方法是和声明的接口类型是相关的,因此不同的声明方式对使用者来说有区别的。如:

1
2
private ConcurrentMap<Integer,Group> groupMap = new ConcurrentHashMap<Integer, Group>();
private Map<Integer,Group> groupMap = new ConcurrentHashMap<Integer, Group>();

这两种声明方式都可以引用一个线程安全的map,但若需要调用ConcurrentHashMap的putIfAbsent方法来保证map里面有且只有一个特定的资源,这种时候,就应该声明为ConcurrentHashMap,表明代码语义是跟一个支持同步语义的map进行交流,而不是一个普通的一般性的map。即使这样,两种声明方式在使用putIfAbsent时没有本质区别,这是因为最终调用的还是ConcurrentHashMap中实现的putIfAbsent方法,是线程安全的。

参考:

知乎:使用不同的接口声明对象引用

2.3 接口与匿名类

接口不可以new,但可以用new产生一个实现接口的匿名类。
如Swing中常用到的new ActionListener() { } ,就是在{}中重写ActionListen接口中的方法。

1
2
3
4
5
6
button2.addActionListener (  
new ActionListener() {
publicvoid actionPerformed(ActionEvent e) {
System.out.println("你按了按钮");
}
});

2.4 函数式接口

当一个接口只有一个抽象方法时,这个接口就是函数式接口,函数式接口用作lambda表达式的类型。详细内容请参考:
Java 8 新特性:lambda表达式

2.5 Java 8接口中的默认方法

Java 8中接口可以实现方法,而不需要实现类去实现方法,通过在接口方法中加入关键字default,就可以在接口中实现方法了。

为什么要有这个特性?首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。

引入接口的默认方法后,接口和抽象类有了更多的相同点了,但二者依然有区别,下面是Java 8抽象类和接口的对比:

相同点:

  1. 都是抽象类型;
  2. 都可以有实现方法(以前接口不行);
  3. 都可以不需要实现类或者继承者去实现所有方法,(以前不行,现在接口中默认方法不需要实现者实现)

不同点:

  1. 抽象类不可以多重继承,接口可以(无论是多重类型继承还是多重行为继承);
  2. 抽象类和接口所反映出的设计理念不同。其实抽象类表示的是”is-a”关系,接口表示的是”like-a”关系;
  3. 接口中定义的变量默认是public static final 型,且必须给其初值,所以实现类中不能改变其值;抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。

注意: 若在实现类中实现了接口的默认方法,则最终会调用实现类的方法;若子接口覆盖了父接口的默认方法,则会调用子接口的默认方法;若子接口声明了和父接口一样的方法但没有实现,那么无论该方法是不是父接口的默认方法,在实现类中都要实现。看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface A {
void foo();
default void test() {
System.out.println("Calling A.test()");
}
}

interface B extends A {
default void foo() {
System.out.println("Calling B.foo()");
}
void test();
}

public class C implements B {
//test() must be implemented in this class
@Override
public void test() {
System.out.println("Calling C.test()");
}
public static void main(String[] args){
A obj = new C();
obj.foo();//Calling B.foo()
obj.test();//Calling C.test()
}
}

关于接口的默认方法在Java 8的JDK库中有很多使用,可以参照Map,ConcurrentMap,ConcurrentHashMap学习接口的默认方法。