Java学习笔记之深入理解接口和抽象类

抽象类

  • 如果一个类包含一个或多个抽象方法,该类必须被声明为抽象类
  • 抽象类可以包含具体的方法,也可以不包含任何抽象方法
  • 如果一个类继承自抽象类,那么该类必须实现抽象类中所有的抽象方法(即提供方法体{}),否则该类必须声明为抽象类
  • 不能为抽象类创建任何对象,即使该抽象类不包含任何抽象方法
  • abstract不能与final并列修饰同一个类
  • abstract不能与private、static、final或native并列修饰同一个方法

抽象方法:仅有声明而没有方法体的方法。如abstract void f();

例1:

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
abstract class People{
abstract void f();
}

class Man extends People{
@Override
void f() {
System.out.println("This is a man.");
}
}

class Women extends People{
@Override
void f() {
System.out.println("This is a women.");
}
}

public class Test {
public static void main(String[] args) {
People man = new Man();
People women = new Women();
man.f();
women.f();
}
}

输出结果:

1
2
This is a man.
This is a women.

抽象类中的抽象方法可以由其子类进行不同的实现,而且抽象类中的抽象方法子类必须实现,否则会出现编译错误。

接口

  • 接口可以包含域,这些域隐式地是static和final的,而且只能是static和final的,即常量,非普通成员变量,其必须在声明时显式的初始化
  • 在实现一个接口时,接口中被定义的方法必须被定义为public的,否则会出现编译错误
  • 接口中的方法都默认为public的和abstract的,而且只能是public的和abstract
  • 当实现某个接口时,并不需要实现嵌套在其内部的任何接口
  • 在实现多接口时,一定要避免方法名的重复

注意:在最新的Java 8中,接口可以拥有非抽象的普通方法。


Java中的多重继承

  • 一个类可以继承多个接口类,但只能继承一个非接口类(具体类/抽象类)
  • implements必须在extends之后,否则编译器会报错
  • 当想要创建对象时,所有方法(包括各个接口中的方法以及具体类/抽象类中的方法)的定义首先必须都存在

下面的例子展示了一个具体类组合数个接口之后产生的一个新类:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface CanFight{
void fight();
}

interface CanSwim{
void swim();
}

interface Canfly{
void fly();
}

class ActionCharacter{
public void fight() {
System.out.println("ActionCharacter.fight()");
}
}

class Hero extends ActionCharacter
implements CanFight, CanSwim, Canfly{

public void swim() {
System.out.println("CanSwim.swim()");
}
public void fly() {
System.out.println("Canfly.fly()");
}
}

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

输出结果:

1
2
3
4
ActionCharacter.fight()
CanSwim.swim()
Canfly.fly()
ActionCharacter.fight()

特别注意,在类Hero中并没有提供fight()方法的定义,但为什么编译没有出错呢?细心的同学会发现,CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的。即使类Hero中没有显式地提供fight()方法的定义,其定义也因ActionCharacter类而随之而来。
同时,上述例子也向我们展示了多重继承可以实现一个类能够向上转型为多个基类型,及由此带来的灵活性。

抽象类和接口的区别


从语法定义层面上看抽象类和接口

  • 使用抽象类的方式定义Demo类
1
2
3
4
5
6
public abstract class Demo {  
abstract void method1();
void method2(){

}
}
  • 使用接口的方式定义Demo类
1
2
3
4
interface Demo {  
void method1();
void method2();
}

在抽象类方式中,Demo类可以有任意范围的成员变量,也可以有非abstract的成员方法;而在接口方式中,Demo类只能拥有静态的且不可修改的成员变量(即static final型),而且所有的成员方法都是abstract的。从某种意义上说,接口是一种特殊形式的抽象类,它比抽象类更加抽象。
抽象类在Java语言中表示一种继承关系,一个类只能使用一次继承关系;但是一个类却可以实现多个接口。
在抽象类的定义中,可以实现某些方法,赋予其默认行为;而在接口中,所有的方法都是抽象的,即不能拥有默认行为


从设计理念层面上看抽象类和接口
前面已经提到过,abstract class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在”is-a”关系,即父类和派生类在概念本质上应该是相同的。对于interface来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。
例2:假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:

  • 使用抽象类的方式定义Door类

    1
    2
    3
    4
    abstract class Door{  
    abstract void open();
    abstract void close()
    }

  • 使用接口的方式定义Door类

    1
    2
    3
    4
    interface Door{  
    void open();
    void close();
    }

其他具体的Door类型可以extends使用抽象类方式定义的Door或者implements使用接口方式定义的Door。看起来好像使用抽象类和接口没有大的区别。
如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢?


解决方案一:
简单的在Door的定义中增加一个alarm方法

1
2
3
4
5
abstract class Door{  
abstract void open();
abstract void close();
abstract void alarm();
}

或者

1
2
3
4
5
interface Door{  
void open();
void close();
void alarm();
}

那么具有报警功能的AlarmDoor的定义方式如下:

1
2
3
4
5
class AlarmDoor extends Door{
void open(){}
void close(){}
void alarm(){}
}

或者

1
2
3
4
5
class AlarmDoor implements Door
void open(){}

void close(){}
void alarm(){}

这种方法违反了面向对象设计中的一个核心原则ISP (Interface Segregation Principle),在Door的定义中把Door概念本身固有的行为方法和另外一个概念”报警器”的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为”报警器”这个概念的改变(比如:修改alarm方法的参数)而改变,反之亦然。


解决方案二:

  • 如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。那么可以这样进行设计:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    abstract class Door{
    abstract void open();
    abstract void close();
    }
    interface Alarm{
    void alarm();
    }
    class AlarmDoor extends Door implements Alarm{
    void open(){}
    void close(){}
    public void alarm(){}
    }
  • 如果我们对于问题领域的理解是:AlarmDoor在概念本质上是报警器,同时它有具有Door的功能。那么可以这样进行设计:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface Door{
    void open();
    void close();
    }
    abstract class Alarm{
    abstract void alarm();
    }
    class AlarmDoor extends Alarm implements Door{
    public void open(){}
    public void close(){}
    void alarm(){}
    }

上述两种不同的实现方式反映了我们对问题领域的不同理解,正确的揭示我们的设计意图。

抽象类表示的是“is-a”关系,接口表示的是“has-a”关系。

面试中的相关问题

1.关于构造函数
抽象类和接口都是不可实例化的,但为什么抽象类可以有构造函数而接口却不能拥有?
这是因为抽象类需要被其他类继承,其子类是可以实例化的,在实例化时需要调用子类的构造函数,而默认情况下,在调用子类构造函数前都会调用父类的构造函数。
而接口没有这种继承关系,故不需要。


2.抽象类只能被继承?
这种说法是不正确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Test {
public abstract void say();

public static void main(String[] args) {
new Test() {
@Override
public void say() {
System.out.println("hello!");
}
}.say();
}
}
/*output:
hello!
*/

通过上面代码可知,抽象类不仅仅可以用来继承,还可以单纯地只输出。

参考资料

  1. Java编程思想
  2. 深入理解Java的接口和抽象类
  3. java提高篇(四)—–抽象类与接口
  4. 详细解析Java中抽象类和接口的区别