接口
什么是接口
接口表示某种能力,是若干行为的集合。接口可以被看作为某种数据引用类型。接口是比抽象类更抽象的“类型”。
接口描述着具备某种能力所应该包含的行为集合,它只提供抽象层面的概念。具体的实现是由接口的实现类(可以看作是接口的子类)来完成的。
这样可以将行为的定义与实现分离开,对程序的代码结构进行优化。
一切对象都具备着各种能力,即从一切对象中都可以抽象出不同的接口。
声明接口
声明接口使用interface
关键字。接口代码也是被保存在Java的源代码文件中的,编译后生成的字节码文件后缀同样是.class。
声明接口的例程:
public interface Moveable {
void move();
}
各版本JDK对接口的定义有些不同:
- 相同点:
- 接口中的所有变量都是公有的静态常量(public static final修饰)
- 接口中的所有非static方法都是公有的
- 不同点:
- Java 7及以前版本:接口中只能包含公有的抽象方法,不能出现有实现体的具体方法
- Java 8:接口中可以包含还有实现的具体方法,这种方法分为两类:默认方法(default)、静态方法(static),但它们必须是公有的
- Java 9至当前版本:接口中可以加入私有的静态方法和私有的strictfp方法
实现接口
类与接口之间的关系被称为实现,即类能实现某些接口。实现关系的声明使用implements关键字。
当类实现接口以后,相当于声明此类具备了已经实现接口中定义的能力,实现类需要重写接口中的所有抽象方法,除非此类是抽象类。
public class Car implements Moveable {
private int currentSpeed;
public Car(int currentSpeed) {
super();
this.currentSpeed = currentSpeed;
}
// 汽车类的对象实现了Moveable接口中定义的抽象方法
// 表示汽车拥有可移的能力,具体行为由以下方法实现
@Override
public void move() {
System.out.println(String.format("汽车在公路上以%d的时速行驶着。", currentSpeed));
}
}
实现类会继承接口中的所有公有方法。
类实现接口的意义
类实现接口,即把接口的类型赋予实现类,实现类就具备了接口定义的能力。
在实现类中重写接口里的方法即让接口中定义的抽象行为具体化。
接口与实现类,分离了代码中的高层功能契约与具体行为实现,可以让优化程序代码结构的工作更加轻松。
类可以同时实现多个接口
Java的类是单继承体系,即一个类的直接父类只能有一个。虽然此特性避免了多继承体系带来的混乱,但同时也让Java类的继承成本大大提高。
接口可以弥补单继承体系的缺点,因为Java的类可以同时实现多个接口,也就是说实现了多个接口的类同样也拥有多种类型(多种功能)。即Java使用多接口实现替代了类的多重继承,保留了多重继承的优点,避免了它带来的问题。
多继承的弊端:当多个父类中有相同行为时,子类调用会产生不确定性。
其根本原因在于多继承父类中的方法是有具体实现的,而导致调用运行时,不确定运行哪个父类中的方法。而接口中的行为往往都是抽象方法,这些方法由子类具体实现,所以避免了以上问题。
多接口实现的例程:
public interface Moveable {
void move();
}
public interface Flyable {
void fly();
}
public class Plane implements Moveable, Flyable {
private String name;
public Plane(String name) {
super();
this.name = name;
}
@Override
public void fly() {
System.out.println(String.format("%s正在天空中飞行。", name));
}
@Override
public void move() {
System.out.println(String.format("%s正在航空跑道上加速,准备起飞。", name));
}
}
接口的继承
接口之间也有继承关系,同样使用extends
关键字声明,但与类不同的是,一个接口可以同时继承自多个父接口,即在Java中,接口是多重继承的。
接口的多重继承例程:
// SuperHero接口中拥有3个方法:
// 从两个父类继承来的fly()和move()
// 自己扩展出的swim()
public interface SuperHero extends Flyable, Moveable {
void swim();
}
public class MonkyKing implements SuperHero {
@Override
public void fly() {
}
@Override
public void move() {
}
@Override
public void swim() {
}
}
如果在多个父接口中存在着相同方法,实现类只需要实现此方法一次即可
public interface Attackable {
void attack();
}
public interface DPS extends Attackable {
void heavyHit();
}
public interface Treat extends Attackable {
void addBlood();
}
public interface Tank extends Attackable {
void pullMonster();
}
public class Monk implements DPS, Tank, Treat {
private String name;
private int dv;
private int tv;
public Monk(String name, int dv, int tv) {
super();
this.name = name;
this.dv = dv;
this.tv = tv;
}
@Override
public void attack() {
System.out.println(String.format("%s对怪物打出了一次佛山无影踹,造成了%d点伤害。", name, dv));
}
@Override
public void addBlood() {
System.out.println(String.format("%s制作了一个魔法酱肘子塞到队友嘴里,为其恢复了%d点血量。", name, tv));
}
@Override
public void pullMonster() {
System.out.println(String.format("%s断喝一声:“你大爷!”,成功燃起一大群怪物的如火。", name));
}
@Override
public void heavyHit() {
System.out.println(String.format("%s对怪物打出了一次天马流星揣,造成了%d点伤害。", name, (int) (dv + dv * Math.random())));
}
}
public class Test01 {
public static void main(String[] args) {
Monk m1 = new Monk("瞬龙", 300, 120);
m1.pullMonster();
m1.attack();
m1.heavyHit();
m1.addBlood();
}
}
继承父类的同时实现接口
如果一个类已经扩展自一个父类,又想对其进行功能扩展,就可以让此类去实现相应的功能接口。
父类提供基础属性和行为,接口类型提供扩展功能
接口的优点
- 接口为类提供了功能的扩展点
- 接口公开了通用的行为契约或规范,而具体的实现被隐藏到实现类中
- 接口降低了程序代码的耦合度,让程序的扩展和优化更加轻松灵活
接口与抽象类的异同(Java 12)
-
相同点:
- 都可以创建程序的抽象层次结构
- 都不可以被直接实例化
- 通常都声明抽象方法来定义行为能力,让子类去扩展这些功能
-
不同点:
- 抽象类中可以声明构造方法,接口中不能存在构造方法
- 子类只能继承一个抽象类,但可以同时实现多个接口
- 类之间是单继承关系,接口之间是多继承关系
- 抽象类为子类提供基础数据和方法,它们之间是is-a关系;接口为实现类提供扩展功能,它们是like-a关系
- Java 7及以前的版本,抽象类中可以声明程序访问修饰符修饰的抽象和具体方法,而接口中只有声明公有的抽象方法
- Java 8版本时,接口中可以定义具有方法体的默认方法和静态方法,但它们一定是公有的
- 虽然是不推荐的做法,但在抽象类中的静态方法是可以通过引用变量名进行调用的;而接口中的静态方法必须使用接口名称调用
- Java 9以后,接口中的静态方法可以被声明为私有的,但不可以是protected和package级别的
- Java 9以后,接口中可以加入私有的被strictfp关键字修饰的具体方法,此类方法可以是静态的,也可以是非静态的,但除了private以外,不能使用其它访问修饰符修饰。
如何在接口和抽象类中进行选择
- 优先使用接口
- 当子类中存在共有行为和属性时,才让子类继承抽象类
接口中的默认方法
Java 8以后,在接口中可以添加以default
关键字修饰的方法,这种方法被称为默认方法,默认方法是有具体实现的方法。
添加默认方法的主要目的:
在软件的长期维护过程中,如果向既有接口里添加新的抽象方法,会导致要在所有此接口的实现类中都去重写新加入的方法,对代码有巨大的影响。
而引入具有实现的默认方法就解决了以上问题。默认方法中有着相对通用的默认实现,如果实现类需要重写此方法,再去进行子类中的重写。默认方法以一种相对平缓的方式扩展接口的行为,不是强制让实现类重写新方法,而是引导实现类去重写新方法。
默认方法可以被认为是Lambda表达式和JDK标准API之间的桥梁
public interface Attackable {
void attack();
// 如果直接在接口中添加以下抽象方法
// 则会对既有的实现类产生巨大影响
// 它们必须重写此方法
// void remoteAttack();
default void remoteAttack() {
throw new UnsupportedOperationException("暂不支持此操作");
}
}
public class Warrior implements Attackable {
@Override
public void attack() {
System.out.println("致死打击");
}
@Override
public void remoteAttack() {
System.out.println("用崩弓子打怪物家的玻璃");
}
}
public class Paladin implements Attackable {
@Override
public void attack() {
System.out.println("圣光审判");
}
}
public class Rogue implements Attackable {
@Override
public void attack() {
System.out.println("背刺");
}
@Override
public void remoteAttack() {
System.out.println("向怪物旁边砍了一块石头,引开它的注意。");
}
}
public class Hunter implements Attackable {
@Override
public void attack() {
System.out.println("迅猛攻击");
}
@Override
public void remoteAttack() {
System.out.println("发起自动射击");
}
}
接口中的静态方法
在Java 8以前,接口中不能出现非抽象方法,所有与接口提供的能力相关的工具方法要被组织到另一个类中。如java.util.Collection接口和java.util.Collections。
现在可以把与某接口相关的工具方法以静态方法的形式组织到此接口的内部,代码的结构会更加紧凑和优雅。
还可以使用工具接口替代以前的工具类。工具类为了避免被实例化,而必须编写私有构造方法。因为接口中不存在构造方法,所以也就没有这方面的困扰。
注意:在接口中声明的静态方法,被调用时只能使用接口名称,不能使用引用变量,否则不能编译。
public interface NumberUtils {
Random RANDOM = new Random();
static int getRandomInt(int min, int max) {
return RANDOM.nextInt(max) % (max - min + 1) + min;
}
}
public class Instance implements NumberUtils {
}
public class Test01 {
public static void main(String[] args) {
System.out.println(NumberUtils.getRandomInt(1, 50));
NumberUtils in = new Instance();
// 以下代码不能被编译
// 在接口中声明的静态方法,被调用时只能使用接口名称,不能使用引用变量,否则不能编译。
// System.out.println(in.getRandomInt(1, 50));
}
}