抽象是面向对象编程的一大特征,Java关于抽象最常被讨论的是抽象类和接口。本文详细介绍下这两者的异同。
一、抽象类
在介绍抽象类之前,先来了解一下抽象方法。
1.1 抽象方法
抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:
abstract void fun();
抽象方法必须用abstract关键字进行修饰。
1.2 抽象类
抽象类是对事物的抽象。如果一个类含有抽象方法,则称这个类为抽象类。抽象类必须在类前用abstract关键字修饰,如果不加,会报编译错误。如下:
这里要注意,如果一个类前用abstract关键字修饰,但是类里面并没有抽象方法,那它也是一个抽象类,只是失去了设计抽象类的意义,等于白白定义了一个抽象类,却不能用它来做任何事情。
在《JAVA编程思想》一书中,将抽象类定义为“包含抽象方法的类”,但这并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。如下图:
抽象类是为了继承而存在的。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为抽象方法,此时这个类也就成为抽象类了。下面通过具体代码来介绍下抽象类的使用。
1.3 抽象类使用
举个简单的例子,鸟是一个种类,自然界有不同种类的鸟,可能有的鸟是4条腿,有的鸟是2条腿。这时,我们就可以把鸟类设计成一个抽象类,不同种类的鸟直接继承这个抽象类。Bird类(代码A):
package com.moi.test.abstractTest;
public abstract class Bird {
abstract int getLegs();
}
Bird类中包含一个抽象方法,用于获取鸟类有几条腿。
TwoLegsBird类(代码B):
package com.moi.test.abstractTest;
public class TwoLegsBird extends Bird{
@Override
int getLegs() {
return 2;
}
}
TwoLegsBird类继承了Bird类,并实现了getLegs方法。
FourLegsBird类(代码C):
package com.moi.test.abstractTest;
public class FourLegsBird extends Bird{
@Override
int getLegs() {
// TODO Auto-generated method stub
return 4;
}
}
FourLegsBird类同样继承了Bird类,和TwoLegsBird类区别在于getLegs方法的实现不同。
1.4 抽象类总结
对于Java中的抽象类,需要注意以下3点:
1、抽象方法不能是private。因为如果是private,那么该类在被继承时,它的子类便无法实现该方法。如图:
2、抽象类不能被实例化,但它可以通过它的子类(派生类)产生它的对象。
new对象是最常用的实例化的途径,抽象类也可以new,但是要实现它的抽象方法。如下(代码D):
package com.moi.test.abstractTest;
public class Test{
public static void main(String[] args) {
Test test1 = new Test();
Bird bird = new Bird() {
@Override
int getLegs() {
// TODO Auto-generated method stub
return 3;
}
};
NormalClass normalClass = new NormalClass();
InnerClass innerClass = test1.new InnerClass();
System.out.println("bird:"+bird.getClass());
System.out.println("normalClass:"+normalClass.getClass());
System.out.println("innerClass:"+innerClass.getClass());
}
class InnerClass {//内部类
public InnerClass() {
}
public void InnerClassMethod() {
System.out.println("innerClassMethod");
}
}
}
上述代码中new了Bird,并实现了它的抽象方法,但并不是实例化了一个Bird对象。这是怎么回事儿呢?为了对比说明,我新建了一个普通类NormalClass,同时在Test类里面创建了一个内部类InnerClass,然后分别输出getClass()方法。NormalClass代码(代码E):
package com.moi.test.abstractTest;
public class NormalClass {
}
输出结果如下:
从图中可以看出,bird和normalClass在getClass()方法上输出不一致,反而和innerClass输出相似,它们都带着一个$符号。我们知道,在Java中编译后的class带$符代表内部类,不同的是InnerClass是普通内部类,Bird是匿名内部类,因为它输出后用数字1表示。所以,normalClass是NormalClass的实例化对象,而bird不是Bird的实例化。
抽象类中含有无具体实现的方法,所以不能被实例化。但它可以通过它的子类(派生类)产生它的对象。如下(代码F):
package com.moi.test.abstractTest;
public class BirdSubClass extends Bird{
@Override
int getLegs() {
// TODO Auto-generated method stub
return 6;
}
}
我新建了Bird的子类BirdSubClass,并实现了getLegs方法,然后在Test类中添加如下代码:
Bird bird2 = new BirdSubClass();
System.out.println(bird2.getLegs());
输出结果是6。
所以说,Java抽象类不可以直接借助操作符new被实例化,但它可以通过完善了它的抽象方法定义的子类(派生类),借助操作符new,产生它 (Bird) 的对象。这种案例,被称作"向上转型 Upcasting"。其优点是,可以将多种不同子类对象的引用,储存于单一种类 (抽象类/父类 Bird) 的 引用之中。
3、如果一个类继承了一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
这个在上面的TwoLegsBird和FourLegsBird中都已经体现了,如果子类不实现抽象方法,会报编译错误。如下:
要么实现抽象方法,要么定义为抽象类。
二、接口
接口,英文称作 Interface,在软件工程中,接口泛指供别人调用的方法或者函数。接口是对行为的抽象。在Java中,定一个接口的形式如下:
public interface InterfaceTest {
}
接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量,并且接口中的方法必须都是抽象方法,不能有具体的实现。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。
下图中定义了一个接口InterfaceTest,如果我们在接口中的方法和变量前面加上private修饰符,会编译报错:
如果我们在接口中创建有实现的方法,同样会编译报错:
如何证明接口中的变量是final的呢?我们创建一个类InterfaceSubClass,实现InterfaceTest接口,当试图改变num值的时候,会出现如下提示:
2.1 接口的使用
一个类实现某个接口需要使用implements关键字,具体格式如下:
public class InterfaceSubClass implements InterfaceTest,InterfaceTest2{
}
可以看到,Java中允许一个类实现多个接口。如果一个非抽象类实现了某个接口,就必须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法。如下,并没有提示编译错误:
下面用具体的代码示例来介绍接口的使用。
在抽象类的介绍中,我们创建了Bird类,我们知道,鸟是会飞的,飞机也是会飞的,飞是一种行为,所以我们在InterfaceTest接口里面创建 fly 方法。
package com.moi.test.abstractTest;
public interface InterfaceTest {
public void fly();
}
我们再创建一个类 AirPlain,实现InterfaceTest接口:
package com.moi.test.abstractTest;
public class AirPlain implements InterfaceTest{
@Override
public void fly() {
System.out.println("飞机会飞");
}
}
然后我们把Bird类也实现InterfaceTest接口:
package com.moi.test.abstractTest;
public abstract class Bird implements InterfaceTest{
abstract int getLegs();
}
由于Bird类是抽象类,所以它不需要实现 fly 方法,交由它的子类来实现。
TwoLegsBird类需要实现 fly 方法:
package com.moi.test.abstractTest;
public class TwoLegsBird extends Bird{
@Override
int getLegs() {
return 2;
}
@Override
public void fly() {
System.out.println("2条腿的鸟会飞");
}
}
FourLegsBird类也是同样的实现:
package com.moi.test.abstractTest;
public class FourLegsBird extends Bird implements InterfaceTest{
@Override
public void fly() {
System.out.println("4条腿的鸟会飞");
}
@Override
int getLegs() {
return 4;
}
}
Test类分别创建AirPlain、FourLegsBird、TwoLegsBird的实例化对象,并调用方法进行输出:
package com.moi.test.abstractTest;
public class Test{
public static void main(String[] args) {
TwoLegsBird twoLegsBird = new TwoLegsBird();
System.out.println("腿的个数:"+twoLegsBird.getLegs());
twoLegsBird.fly();
System.out.println("------------");
FourLegsBird fourLegsBird = new FourLegsBird();
System.out.println("腿的个数:"+fourLegsBird.getLegs());
fourLegsBird.fly();
System.out.println("------------");
AirPlain airPlain = new AirPlain();
airPlain.fly();
}
}
输出结果如下:
以上介绍了接口的使用。其实,上述代码是存在问题的,并不是所有的鸟都会飞,例如企鹅是鸟类,但是不会飞,所以我们可以在Bird类的子类中实现InterfaceTest接口,这样只有会飞的鸟自行实现 fly 方法;同时,飞机也会分为多种,飞机都是会飞的,所以可以将AirPlain定义为一个抽象类并实现InterfaceTest接口,它的子类都会实现 fly 方法。代码就不再赘述,读者可自行处理。
三、抽象类和接口的区别
1、语法层面上的区别
- 抽象类中可以存在非抽象方法,即包含实现体的方法,而接口中只能存在抽象方法;
- 抽象类中的成员变量可以是各种类型的(public、private、protected),而接口中的成员变量只能是public static final类型,并且是隐式定义,需要变量初始化;
- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2、设计层面上的区别
- 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为特性)进行抽象。抽象类是需要继承的,接口是需要实现的。继承是一个 "是不是"的关系,而接口实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
- 抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?举个例子,如果子类A和子类B继承了抽象类C,那么如果需要改动A和B的公共部分,那么只需要改动C就可以了,不需要对A和B进行改动。而辐射式设计,如果类1和类2实现了接口3,那么如果3内新增了某个方法,那么都需要在1和2中进行实现。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
下面针对网上门和警报的例子说明抽象类和接口的使用。
门都有open( )和close( )两个动作,然后有的门具有报警功能,火灾报警器也具有报警的作用,门和火灾报警器并没有任何的关系。我们该怎么设计呢?
首先,门是一个种类,所以肯定是需要将门设计成一个类,门又可以分为好多种,并且它们都可以开和关,所以把门设计成抽象类,里面包含open( )和close( )两个抽象方法:
package com.moi.test.abstractTest;
public abstract class Door {
abstract void open();
abstract void close();
}
报警是一个功能,也就是一个行为,并不是所有的门都具有报警功能,所以我们将报警设计为接口,包含alarmVoice()行为:
package com.moi.test.abstractTest;
public interface Alarm {
void alarmVoice();
}
有的门具有报警的功能,所以有报警功能的门AlarmDoor就需要继承Door并实现Alarm接口:
package com.moi.test.abstractTest;
public class AlarmDoor extends Door implements Alarm{
@Override
public void alarmVoice() {
System.out.println("报警");
}
@Override
void open() {
System.out.println("开门");
}
@Override
void close() {
System.out.println("关门");
}
}
以上介绍了Java中抽象方法、抽象类、接口的相关知识,同时,也是Java面试中常见的知识点。
欢迎在评论区留言,我会尽快回复~