Java 是一门单继承的语言,一个子类只能继承一个父类。但是在编程实践中,对于某些类,只继承一个抽象类显然无法满足要求,需要实现多个抽象类的抽象方法才能解决问题,这就需要通过接口来实现。
在 Java 中,允许通过一个类实现多个接口来实现类似于多重继承的机制。
Java接口的定义
接口可以看作是从多个相似的类中抽象出来的规范,不提供任何实现,体现了规范和实现分离的思想。
例如,计算机上提供了 USB 插槽,只要一个硬件遵守 USB 规范,就能插入 USB 插槽,可以是鼠标、键盘、数据线等,计算机无须关心是和哪个硬件对象建立了联系。同样,软件系统的开发也需要采用规范和实现分离的思想,即采用面向接口的编程思想,从而降低软件系统模块之间的耦合度,提高系统的可扩展性和可维护性。
在 Java 语言中,提供了 interface 关键字,用于声明接口,其语法格式如下:
[public]interface 接口名[extends 接口1,接口2…] {
[public][static][final]数据类型 常量名 = 值;
[public][abstract] 返回值的数据类型 方法名(参数列表);
默认方法…
}
在上述语法中,当 interface 关键字前加上 public 修饰符时,接口可以被任何类的成员访问。如果省略 public,则接口只能被与它处在同一包中的成员访问。extends 语句与类继承中的 extends 语句基本相同,不同点在于接口可以继承自多个父接口,父接口之间使用逗号分隔。
接口中定义的变量和方法都包含默认的修饰符,其中变量默认声明为“public static final”,即全局静态常量,方法默认声明为“public abstract”,即抽象方法。
例如,定义一个 Charge 接口(收费接口),内有接口常量 PORT_STYLE 和成员方法 getCharge(),代码如下:
interface Charge(){
int PORT_STYLE = 1; // 接口常量
void getCharge(); // 接口方法声明
}
Java接口实现
接口与抽象类相似,也包含抽象方法,因此不能直接实例化接口,即不能使用 new 创建接口的实例。但是,可以利用接口的特性来创造一个新的类,然后再用新类来创建对象,利用接口创建新类的过程称为接口的实现。
实现接口的目的主要是在新类中重写抽象的方法,当然也可以使用抽象类来实现抽象方法的重写。
接口的实现需要使用 implements 关键字,即在声明一个类的同时用关键字 implements 来实现接口,实现接口的类一般称为接口的实现类,具体语法如下:
[修饰符]class 类名 implements 接口1,接口2, 接口3,… { // 如果实现多个接口,以逗号隔开
…
}
一个类实现一个接口时,需要注意如下问题:
如果实现接口的类不是抽象类,则该类必须实现接口的所有抽象方法;
在类中实现接口的抽象方法时,必须使用与接口中完全一致的方法名及参数列表,否则只是定义一个新的方法,而不是实现已有的抽象方法。
接下来,通过案例来演示接口的实现:
interface PCI { // 定义PCI接口
String serialNumber = "9CC0AC186027";
void start();
void run();
void stop();
}
public class VideoCard implements PCI { // 定义显卡类,实现PCI接口
@Override
public void start() {
System.out.println("显卡开始启动");
}
@Override
public void run() {
System.out.println("显卡序列号是:" + serialNumber);
System.out.println("显卡开始工作");
}
@Override
public void stop() {
System.out.println("显卡停止工作");
}
}
public class Demo {
public static void main(String[] args) {
VideoCard videoCard = new VideoCard();
videoCard.start();
videoCard.run();
videoCard.stop();
}
}
程序的运行结果为:
显卡开始启动
显卡序列号是:9CC0AC186027
显卡开始工作
显卡停止工作
从运行结果可以看到,程序中定义了一个 PCI 接口,该接口定义了一个全局常量 serialNumber 和 3 个抽象方法 start()、run()、stop(),显卡类 VideoCard 实现了 PCI 接口的这 3 个抽象方法,并在实现类的方法中调用接口的常量。最后,在 Demo 测试类中创建了显卡类 VideoCard 的实例,并输出运行结果。
Java接口的继承
在现实世界中,网络通信具有一定的标准,手机只有遵守相应的标准规范才可以使用相应的网络。然而,随着移动互联网技术的发展,网络通信标准从之前的 2G、3G 到目前的 4G、5G,而且 6G 也已在研发之中。
Java 程序中的接口与网络通信标准类似,定义之后,随着技术的不断发展以及应用需求的不断增加,接口往往也需要更新迭代,主要是功能扩展,增加新的方法,以适应新的需求。但是,使用直接在原接口中增加方法的途径来扩展接口可能会带来问题:所有实现原接口的实现类都将因为原来接口的改变而不能正常工作。
为了既能扩展接口,又不影响原接口的实现类,一种可行的方法是通过创建原接口的子接口来增加新的方法。
接口的继承与类的继承相似,都是使用 extends 关键字来实现,当一个接口继承父接口时,该接口会获得父接口中定义的所有抽象方法和常量。但是,接口的继承比类的继承要灵活,一个接口可以继承多个父接口,这样可以通过继承将多个接口合并为一个接口。
接口继承的语法格式如下:
interface 接口名 extends 接口1,接口2,接口3,… {
…
}
接下来,通过案例来演示接口的继承:
interface I3G { // 上网
void online(); // 上网
void call(); // 打电话
void sendMsg(); // 发短信
}
interface I4G extends I3G { // 看视频
void watchVideo();
}
class Nokia implements I3G {
@Override
public void call() {
System.out.println("打电话功能");
}
@Override
public void sendMsg() {
System.out.println("发短信功能");
}
@Override
public void online() {
System.out.println("上网功能");
}
}
class Mi implements I4G {
@Override
public void call() {
System.out.println("打电话功能");
}
@Override
public void sendMsg() {
System.out.println("发短信功能");
}
@Override
public void online() {
System.out.println("上网功能");
}
@Override
public void watchVideo() {
System.out.println("看视频功能");
}
}
public class Demo {
public static void main(String[] args) {
System.out.println("Nokia手机使用第3代通信技术,具有:");
Nokia nokia = new Nokia();
nokia.call();
nokia.online();
nokia.sendMsg();
System.out.println("小米手机使用第4代通信技术,具有:");
Mi mi = new Mi();
mi.call();
mi.online();
mi.sendMsg();
mi.watchVideo();
}
}
程序的运行结果如下:
Nokia手机使用第3代通信技术,具有:
打电话功能
上网功能
发短信功能
小米手机使用第4代通信技术,具有:
打电话功能
上网功能
发短信功能
看视频功能
从运行结果可以看到,I4G 接口继承了 I3G 接口,直接继承了 I3G 接口中的 3 个抽象方法 call()、onLine()、sendMsg(),并新增了一个抽象方法 watchVide(),在 main() 方法中 Nokia 类实现了 I3G 接口,从而实现父接口的 3 个抽象方法,而 Mi 类实现了 I4G 接口,实现了子接口的 4 个抽象方法。
如果一个类同时继承类并继承某个接口,需要先 extends 父类,再 implements 接口,格式如下:
子类 extends 父类 implements [接口列表]{
…
}
利用接口实现多重继承
Java 语言规定一个类只能继承一个父类,这给实际开发带来了许多困扰,因为许多类需要继承多个父类的成员才能满足需求,这种问题称为多重继承问题。
然而,我们也不能将多个父类简单地合并成一个父类,因为每个父类都有自己的一套代码,合并到一起之后可能会出现同一方法的多种不同实现,由此会产生代码冲突,增加代码的不可靠性。
有了接口以后多重继承问题就迎刃而解了,由于一个类可以实现多个接口,所以在程序设计的过程中我们可以把一些“特殊类”设计成接口,进而通过接口间接地解决多重继承问题。
一个类实现多个接口时,在 implements 语句中分隔各个接口名,此时这些接口就可以被理解成特殊的类,而这种做法实际上就是使子类获得了多个父类的成员,并且由于接口成员没有实现细节,实现接口的类只能有一个具体的实现细节,从而避免了代码冲突,保证了 Java 代码的安全性和可靠性。
接下来,通过案例来演示利用接口实现多重继承:
// 定义IFly接口
interface IFly {
void takeoff(); // 起飞方法
void land(); // 落地方法
void fly(); // 飞行方法
}
// 定义ISail接口
interface ISail {
void dock(); // 停靠方法
void cruise(); // 航行方法
}
// 定义交通工具Vehicle类
class Vehicle {
private double speed; // 设置速度方法
void setSpeed(int sd) {
this.speed = sd;
System.out.println("设置速度为" + speed);
}
void speedUp(int num) { // 加速方法
this.speed += num;
System.out.println("加速" + num + ",速度变为" + speed);
}
void speedDown(int num) { // 减速方法
this.speed -= num;
System.out.println("减速" + num + ",速度变为" + speed);
}
}
// 定义水上飞机类
class SeaPlane extends Vehicle implements IFly, ISail {
public void takeoff() {
System.out.println("水上飞机开始起飞");
}
public void land() {
System.out.println("水上飞机开始落地");
}
public void fly() {
System.out.println("水上飞机可以飞行");
}
public void dock() {
System.out.println("水上飞机可以停靠");
}
public void cruise() {
System.out.println("水上飞机可以航行");
}
}
public class Demo {
public static void main(String[] args) {
SeaPlane sp = new SeaPlane();
sp.takeoff();
sp.setSpeed(1);
sp.speedUp(2);
sp.fly();
sp.speedDown(2);
sp.land();
sp.cruise();
sp.speedDown(2);
sp.dock();
}
}
程序的运行结果如下:
水上飞机开始起飞
设置速度为2
加速2,速度变为4
水上飞机可以飞行
减速2,速度变为2
水上飞机开始落地
水上飞机可以航行
减速2,速度变为0
水上飞机可以停靠
程序中,水上飞机类 SeaPlane 继承了交通工具类 Vehicle,并且实现了 IFly 接口和 ISail 接口。从程序运行结果中可以看到,它不仅具有了交通工具的功能,还增加了飞行功能和航行功能。
Java接口默认方法
在程序开发中,如果之前创建了一个接口,并且已经被大量的类实现,当需要再添加新的方法以扩充这个接口的功能的时候,就会导致所有已经实现的子类都要重写这个方法。
但是,在接口中使用默认方法就不会有这个问题,所以从 JDK 8 开始新加了接口默认方法,便于接口的扩展。
接口默认方法是一个默认实现的方法,并且不强制实现类重写此方法,使用default关键字来修饰。接口新增的默认方法在实现类中可以直接使用。
接下来,通过案例来演示接口默认方法的使用:
public interface ICat { // 定义ICat接口
void play(); // 抽象方法
default void run() { // 默认方法
System.out.println("猫咪在跑,猫步");
}
}
class BlackCat implements ICat { // 黑猫类实现了ICat接口
@Override
public void play() { // 重写ICat接口的抽象方法
System.out.println("黑猫在玩耍");
}
}
public class Demo {
public static void main(String[] args) {
BlackCat cat = new BlackCat();
cat.play();
cat.run();
}
}
程序的运行结果如下:
黑猫在玩耍
猫咪在跑,猫步
程序中,ICat 接口定义了抽象方法 play() 和默认方法 run(),BlackCat 类实现了 ICat 接口,并重写了抽象方法 play(),通过测试类 Demo 中的 main() 方法创建了 BlackCat 类的实例,调用 play() 和 run() 方法后发现,ICat 接口的默认方法 run() 可以被它的实现类的对象直接调用。
注意,接口允许定义多个默认方法,其子类可以实现多个接口,因此接口默认方法可能会出现同名情况,此时子类在实现或者调用默认方法时通常遵循以下原则:
子类中的同名方法优先级最高。
如果第一条无法进行判断,那么子接口的优先级更高;方法名相同时,优先选择拥有最具体实现的默认方法的接口,即如果接口 B 继承了接口 A,那么接口 B 就比接口 A 更加具体。
Java接口实现多态
前面我们讲解了使用继承机制来实现多态,事实上使用接口也同样可以实现多态。
接下来,通过某汽车销售店案例演示如何通过接口实现多态:
// 定义ICar接口
interface ICar {
String showName(); // 显示汽车名称
double getPrice(); // 获取汽车价格
}
// 定义Haval汽车类
class Haval implements ICar {
@Override
public String showName() {
return "哈佛SUV";
}
@Override
public double getPrice() {
return 150000;
}
}
// 定义GreatWall汽车类
class GreatWall implements ICar {
@Override
public String showName() {
return "长城汽车";
}
@Override
public double getPrice() {
return 68000;
}
}
// 定义汽车销售店CarShop类
class CarShop {
private double money = 0;
public void sellCar(ICar car) {
System.out.println("车型:" + car.showName() + ",价格:" + car.getPrice());
money += car.getPrice();
}
public double getMoney() {
return money;
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
CarShop shop = new CarShop();
Haval haval = new Haval();
shop.sellCar(haval);
GreatWall greatWall = new GreatWall();
shop.sellCar(greatWall);
System.out.println("销售总收入:" + shop.getMoney());
}
}
程序的运行结果如下:
车型:哈佛SUV,价格:150000.0
车型:长城汽车,价格:68000.0
销售总收入:218000.0
程序中 ICar 接口定义了抽象方法 showName() 和 getPrice(),Haval 类和 GreatWall 类实现了 ICar 接口,汽车销售店类 CarShop 针对实现 ICar 接口的实现类进行销售并汇总金额。
在测试类 Demo 的 main() 方法中创建 CarShop 类的实例 shop、Haval 类的实例 haval、GreatWall 类的实例 greatWall,通过 shop 对象的 sellCar() 方法对汽车对象 havel 和 greatWall 销售,并调用 getMoney() 方法统计销售总收入。这样,我们便通过 ICar 接口实现了多态,
抽象类和接口的比较
抽象类与接口是 Java 中对于抽象类定义进行支持的两种机制,它们都用于为对象定义共同的行为,二者比较如下:
抽象类和接口都包含抽象方法;
抽象类可以有非抽象方法,接口中如果要定义非抽象方法,需要标注为接口默认方法;
接口中只能有常量,不能有变量;抽象类中既可以有常量,也可以有变量;
一个类可以实现多个接口,但只能继承一个抽象类。
在程序设计时,应该根据具体业务需求来确定是使用抽象类还是接口。如果子类需要从父类继承一些变量或继承一些抽象方法、非抽象方法,可以考虑使用抽象类;如果一个类不需要继承,只需要实现某些重要的抽象方法,可以考虑使用接口。