导读
本文首先分别介绍抽象类和接口的基础概念、特征和应用场景,然后介绍其区别和联系。
1 抽象类
1.1 定义抽象类
在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。
public abstract class AbstractPlayer {public abstract void eat();
}
关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”。
1.2 抽象类的特征
1 抽象类是用来捕捉子类的通用特性的,它不能被实例化,只能被继承。如果尝试通过new
关键字实例化的话,编译器会报错,提示类是抽象的不能被实例化:
抽象类的子类通过extends
关键词来继承抽象类:
public class BasketballPlayer extends AbstractPlayer {
}
但是要注意:使用extends只能单继承,同事继承多个抽象类编译器会报错class cannot extend multiple classes
。
2 包含抽象方法的一定是抽象类,但抽象类不一定含有抽象方法。
当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 abstract 关键字定义”,见下图。
第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”,见下图。
而在抽象类中,可以定义普通方法,如下面的代码所示:
public abstract class AbstractPlayer {public abstract void play();public void sayName(String name) {System.out.println("My name is " + name);}
}
3 抽象类中的抽象方法只能为public或protected(如果是private则不能被子类继承),默认为public。
4 抽象类中的抽象方法只有方法体,没有具体实现,但可以有普通方法。
5 如果一个子类实现了父类(抽象类)的所有抽象方法,那么该子类可以不必是抽象类,否则就是抽象类。
public abstract class AbstractPlayer {public abstract void eat();
}/*** AbstractFootballPlayer没有实现AbstractPlayer的抽象方法play,因此他也只能是抽象类*/
public abstract class AbstractFootballPlayer extends AbstractPlayer{public abstract void run();
}/*** FootballPlayer需要实现AbstractFootballPlayer中的所有抽象方法,否则它仍然是一个抽象类*/
public class FootballPlayer extends AbstractFootballPlayer{@Overridepublic void run() {}@Overridevoid play() {}
}
6 抽象类可以包含属性、方法、构造方法等,但是构造方法不能用于实例化,主要用途是被子类调用。
public abstract class AbstractPlayer {// 可以定义构造函数AbstractPlayer(int count) {this.count = count;}// 可以定义静态常量public static final int MAX_COUNT = 5;// 可以定义变量public int count;// 可以定义普通函数public void sayName(String name) {System.out.println("My name is " + name);}// 可以定义抽象方法abstract void play();
}// 如果抽象类定义了构造函数,其子类就需要调用抽象类的构造函数
public class BasketballPlayer extends AbstractPlayer{BasketballPlayer(int count) {super(count);}@Overridevoid play() {System.out.println(this.MAX_COUNT);System.out.println(this.count);}public static void main(String[] args) {AbstractPlayer player = new BasketballPlayer(11);player.sayName("Tom");player.play();}
}
1.3 抽象类的应用场景
场景1:希望一些通用的功能能够被多个子类复用
比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。
abstract class AbstractPlayer {public void sleep() {System.out.println("运动员也要休息而不是挑战极限");}
}// 子类 BasketballPlayer 继承了 AbstractPlayer 类,就拥有了 sleep() 方法。
class BasketballPlayer extends AbstractPlayer {
}// 子类 FootballPlayer 继承了 AbstractPlayer 类,也就拥有了 sleep() 方法。
class FootballPlayer extends AbstractPlayer {
}// BasketballPlayer 的对象可以直接调用父类的 sleep() 方法
BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();// FootballPlayer 的对象可以直接调用父类的 sleep() 方法
FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();
场景2:希望所有子类能够自行实现在抽象类中定义的抽象方法
比如说,AbstractPlayer 抽象类中定义了一个抽象方法 play(),表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。
abstract class AbstractPlayer {abstract void play();
}public class BasketballPlayer extends AbstractPlayer {@Overridevoid play() {System.out.println("我是张伯伦,我篮球场上得过 100 分,");}
}public class FootballPlayer extends AbstractPlayer {@Overridevoid play() {System.out.println("我是C罗,我能接住任意高度的头球");}
}
2 接口
2.1 定义接口
Java中的接口使用interface关键字修饰,接口是方法的集合。
public interface Runnable {public abstract void run();
}
2.2 接口的特征
public interface Electronic {// 常量String LED = "LED";// 抽象方法int getElectricityUse();// 静态方法static boolean isEnergyEfficient(String electtronicType) {return electtronicType.equals(LED);}// 默认方法default void printDescription() {System.out.println("电子");}
}
1 接口中可以含有 变量和方法,但是要注意,接口中定义的变量会在编译的时候自动加上 public static final 修饰符(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误)。
2 Java8以前接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。
从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。
3 从 Java 8 开始,接口中允许有静态方法,比如上例中的 isEnergyEfficient() 方法。
静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用,比如说 Electronic.isEnergyEfficient(“LED”)。
4 从 Java8 开始,接口中允许定义default方法,比如上例中的 printDescription() 方法。
default方法提供了默认的实现,实现该接口的子类可以不修改default方法直接使用模式实现,也可以override。
public class Computer implements Electronic{// 抽象方法必须在子类中实现@Overridepublic int getElectricityUse() {return 0;}// 可以选择覆写该default方法,不覆写也不会报错@Overridepublic void printDescription() {System.out.println("计算机");}public static void main(String[] args) {Computer computer = new Computer();computer.printDescription();}
}
5 接口不允许直接实例化,否则编译器会报错:
6 接口可以是空的,既可以不定义变量,也可以不定义方法。最典型的例子就是 Serializable 接口,Serializable 接口用来为序列化的具体实现提供一个标记,也就是说,只要某个类实现了 Serializable 接口,那么它就可以用来序列化了。
public interface Serializable {
}
7 接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题。
2.3 接口的应用场景
场景1 作为标记,使某些实现类具有我们想要的功能
比如说,实现了 Cloneable 接口的类具有拷贝的功能,实现了 Comparable 或者 Comparator 的类具有比较功能。Cloneable 和 Serializable 一样,都属于标记型接口,它们内部都是空的。实现了 Cloneable 接口的类可以使用 Object.clone() 方法,否则会抛出 CloneNotSupportedException。
public class CloneableTest implements Cloneable {@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}public static void main(String[] args) throws CloneNotSupportedException {CloneableTest c1 = new CloneableTest();CloneableTest c2 = (CloneableTest) c1.clone();}
}
运行后没有报错。现在把 implements Cloneable 去掉。
public class CloneableTest {@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}public static void main(String[] args) throws CloneNotSupportedException {CloneableTest c1 = new CloneableTest();CloneableTest c2 = (CloneableTest) c1.clone();}
}
运行后抛出 CloneNotSupportedException:
Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTestat java.base/java.lang.Object.clone(Native Method)at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)
场景2 借助接口实现多重继承
如果有两个类共同继承(extends)一个父类,那么父类的方法就会被两个子类重写。然后,如果有一个新类同时继承了这两个子类,那么在调用重写方法的时候,编译器就不能识别要调用哪个类的方法了。这也正是著名的菱形问题,见下图。
而借助接口可以达到多重继承的目的。
场景3 实现多态
使用抽象类和接口都可以实现多态,下面举个栗子:
public interface Shape {String name();
}// Circle 类实现了 Shape 接口,并重写了 name() 方法
public class Circle implements Shape {@Overridepublic String name() {return "圆";}
}// Square 类也实现了 Shape 接口,并重写了 name() 方法
public class Square implements Shape {@Overridepublic String name() {return "正方形";}
}// 调用看看效果
List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();shapes.add(circleShape);
shapes.add(squareShape);for (Shape shape : shapes) {System.out.println(shape.name());
}// 输出结果为:
// 圆
// 正方形
这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 shape.name() 方法的时候,Java 虚拟机知道该去调用 Circle 的 name() 方法还是 Square 的 name() 方法。
场景4 接口在设计模式中的应用
在使用接口的时候,经常会用到三种模式,分别是策略模式、适配器模式和工厂模式。
策略模式
策略模式的思想是,针对一组算法,将每一种算法封装到具有共同接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变。示例如下:
// 接口:教练
interface Coach {// 方法:防守void defend();
}// 何塞·穆里尼奥
class Hesai implements Coach {@Overridepublic void defend() {System.out.println("防守赢得冠军");}
}// 德普·瓜迪奥拉
class Guatu implements Coach {@Overridepublic void defend() {System.out.println("进攻就是最好的防守");}
}public class Demo {// 参数为接口public static void defend(Coach coach) {coach.defend();}public static void main(String[] args) {// 为同一个方法传递不同的对象defend(new Hesai());defend(new Guatu());}
}
适配器模式
适配器模式的思想是,针对调用者的需求对原有的接口进行转接。生活当中最常见的适配器就是HDMI(英语:High Definition Multimedia Interface,中文:高清多媒体接口)线,可以同时发送音频和视频信号。适配器模式的示例如下:
interface Coach {void defend();void attack();
}// 抽象类实现接口,并置空方法
abstract class AdapterCoach implements Coach {public void defend() {};public void attack() {};
}// 新类继承适配器
class Hesai extends AdapterCoach {public void defend() {System.out.println("防守赢得冠军");}
}public class Demo {public static void main(String[] args) {Coach coach = new Hesai();coach.defend();}
}
工厂模式
所谓的工厂模式理解起来也不难,就是什么工厂生产什么,比如说宝马工厂生产宝马,奔驰工厂生产奔驰,A 级学院毕业 A 级教练,C 级学院毕业 C 级教练。示例如下:
// 教练
interface Coach {void command();
}// 教练学院
interface CoachFactory {Coach createCoach();
}// A级教练
class ACoach implements Coach {@Overridepublic void command() {System.out.println("我是A级证书教练");}}// A级教练学院
class ACoachFactory implements CoachFactory {@Overridepublic Coach createCoach() {return new ACoach();}}// C级教练
class CCoach implements Coach {@Overridepublic void command() {System.out.println("我是C级证书教练");}}// C级教练学院
class CCoachFactory implements CoachFactory {@Overridepublic Coach createCoach() {return new CCoach();}}public class Demo {public static void create(CoachFactory factory) {factory.createCoach().command();}public static void main(String[] args) {// 对于一支球队来说,需要什么样的教练就去找什么样的学院// 学院会介绍球队对应水平的教练。create(new ACoachFactory());create(new CCoachFactory());}
}
3 抽象类和接口的共同点和区别
3.1 共同点
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)
3.2 区别
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类)。
- 一个类只能继承一个类,但是可以实现多个接口(如果想实现多继承就用接口)。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
4 抽象类和接口的应用场景
1、如果你拥有一些方法并且想让它们中的一些有默认实现,那么使用抽象类吧。
2、如果你想实现多重继承,那么你必须使用接口。由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。
3、如果基本功能在不断改变,那么就需要使用抽象类。如果不断改变基本功能并且使用接口,那么就需要改变所有实现了该接口的类。
本文参考博客
- JavaGuide-接口和抽象类有什么共同点和区别?
- Java抽象类,看这一篇就够了,豁然开朗
- Java接口,看这一篇就够了,简单易懂
- Java 抽象类和接口的区别,看这一篇就够了