文章目录
- 接口
- 定位
- 内容构成
- 抽象方法
- 缺省实现的抽象方法
- private 方法
- 静态方法
- 静态常量
- 应用特点
- 多态
- IDEA 快捷自动实现接口
- 接口的继承
- 接口是否属于多继承?
- 从不同接口继承相同方法
- 阻绝抽象方法的缺省实现
- 抽象类
- 抽象类 vs. 接口
- 如何让接口更像抽象类
接口
定位
接口的本质是一种特殊的类,特殊在于接口仅仅是为了提供一种规范/标准。
内容构成
接口由一堆抽象方法(public abstract
)和静态常量(public static final
)组成。
// 接口1定义示例
public interface Expired{// 接口中的抽象方法(省略 public abstract)void sayHi();// 定义静态常量(省略 public static final)int expiredays = 365;
}// 接口2定义实例
public interface VirtualGoods {
}// 实现接口
public class Product implements Expired,VirtualGoods{}// 调用类
public class TestUse{public static void main(String[] args){Expired exp = new Product();}
}
抽象方法
接口里的方法(全都是抽象方法)都是 public abstract
修饰的,方法有名字、参数和返回值,但没有方法体,以分号结束。
因为接口里的方法都是(且只能)用
public abstract
修饰,所以这两个修饰符可以省略不写。
接口实际上是定义“标准”或者说“规范”(有哪些方法、方法签名、方法返回值类型),但不定义具体如何实现这些标准(方法体)。
由于方法没有方法体,所以建议给方法写好注释,说明定义这个方法的目的。
缺省实现的抽象方法
java 8 中,接口中开始允许有缺省实现的抽象方法。即可以在抽象方法中定义方法体,如果子类没有实现这个抽象方法,则以这个事先定义的“缺省”方法体作为这个抽象方法在子类中的具体实现。如果子类中对这个抽象方法进行了实现(覆盖),则无论抽象方法是否定义了“缺省”方法体,都以子类实现(覆盖)的方法体为准,作为具体实现。
// 接口中的抽象方法
void sayHi();// 等同于下面写法(也就是说 public abstract 可写可不写)
public abstract void sayHi();// 但如果定义缺省实现的抽象方法,就不能再写 abstract 了,语法是如下:
default void sayHi(){xxxx;
}// 或写成:
public default void sayHi(){xxxx;
}// 测试下方代码写法也正常运行:
default public void sayHi(){xxxx;
}// 但如果同时出现 default 和 abstract 那么无论怎么排列组合都会报错:java: 非法的修饰符组合: abstract和default
// public abstract default
// public default abstract
// default public abstract// 即,只要有 abstract 修饰,就不能有方法体。default 与 abstract 在接口中是无法同时出现的。
Kevin:我觉得这个 default 定义的所谓“抽象方法的缺省实现”,已经不能算是抽象方法了,它就仅仅是一个普通方法而已,看子类继承的时候要不要覆盖。
private 方法
java 9 中,接口中开始允许有 private 方法,当然这个 private 方法自然是只能服务于抽象方法的缺省实现了,否则也没有地方能用得上(若用在接口中的静态方法中显然也是不行的,因为静态方法中不允许调用非静态的方法,否则报错)。本质上,private 方法只是把抽象方法的缺省实现代码块中可以复用的一部分逻辑抽取出来而已。使用 private 方法是语法上允许了,但实际上真的不建议把接口中抽象方法的缺省实现搞得这么复杂。
优化代码
多说一句:其实实际上 java 在运行期间优化代码的时候,很多时候就是把 private 方法的代码拷贝/插入到调用这个 private 方法的地方,以减少一次方法的调用。(很多语言都是这样优化的)
静态方法
java 9 中,接口中还开始允许有静态方法(static)。静态方法可以被接口中的 public、private 方法调用,也可以在调用类中通过 接口名.静态方法名(参数列表)
调用,但在调用类中不能使用 实现了接口的子类的实例名.静态方法名(参数列表)
调用,(甚至不能用实现了接口的子类名.静态方法名(参数列表)
调用)否则会报错。这一点和普通类中的静态方法有明显区别。
另外,接口中的静态方法的访问修饰符只能是 public (可以省略不写,java 会默认是 public 的),不能定义为 protected、private 等其他的。
// 接口1
public interface Expired {// 抽象方法的缺省(默认)实现default boolean isDamaged() {boolean isDamaged = !qualityCondition();// describe(); // 可以在缺省实现方法体中调用 static 方法,正常执行。return isDamaged;}// private 方法private boolean qualityCondition() {// describe(); // 可以在 private 方法体中调用 static 方法,正常执行。return Math.random() < 0.5;}// 静态方法// 其实下方的静态方法也能写成 public static void describe(){ 但 IDEA 会提示 public 是多余的static void describe(){System.out.println("This is a method from Expired.");}}
// 接口2
public interface VirtualGoods {
}// 实现接口的类
public class Product implements Expired,VirtualGoods { // 因为接口的抽象方法有默认实现,所以可以选择不覆盖此抽象方法
}// 调用类
public class TestUse {public static void main(String[] args) {Expired exp = new Product();System.out.println("当前商品损坏情况: " + exp.isDamaged()); // 当前商品损坏情况: true// exp.describe(); // 报错:java: 静态接口方法调用非法,应将接收方表达式替换为类型限定符 'org.test.interfacetest.Expired'Expired.describe(); // This is a method from Expired.// Product.describe(); // 报错。Product 类中找不到 describe 方法。IDEA 建议替换成 Expired 来调用。}
}
由于很多公司仍旧沿用 java 8,因此在设计时,尽量还是别在接口中定义 private 方法以及静态方法。
静态常量
接口里不能定义成员变量,定义的变量默认(且必须)都是 public static final
的(静态常量),这三个修饰符同样可以省略。
并且本身接口就不能实例化,定义成员变量也白搭,所以也没有这个需求。
// 接口的定义示例:/*** 这是一个过期商品的接口,可以检测这个商品是否在卖,大概还能在架上多少天*/
public interface Expired {/*** 返回当前在架情况** @return 是否在架*/boolean isOnSale(); // 定义抽象方法/*** 根据过期时间计算还能在商品货架上放多少天** @param day 过期前剩余天数* @return 大概能再放多少天*/int saledays(int day); // 定义抽象方法/*** 返回出厂设定的过期时间*/public abstract void expireday(); // 不会报错,但 IDEA 会提示建议你删除多余的 public abstract 修饰符public static final String brand = "Big Rabbit"; // 定义静态常量int expiredays = 365; // 定义静态常量(省略 public static final)}// 再定义一个完全没有任何抽象方法和静态常量的接口
public interface VirtualGoods {
}
应用特点
接口可以让一个类有多种类型,类似于多继承。即一个类可以同时 implements 多个接口。
接口无法被实例化。
// 接口的应用示例:
// 定义一个类来实现接口(需要覆盖接口中的所有抽象方法写出方法体,只要有抽象方法未被覆盖实现,都会报错)
public class Product implements Expired,VirtualGoods {@Overridepublic boolean isOnSale() {return true;}@Overridepublic int saledays(int day) {return (int)(day * 0.95);}@Overridepublic void expireday() {System.out.println("Total :"+expiredays);}
}// 调用类
public class TestUse {public static void main(String[] args) {Product candy = new Product();System.out.println(candy.isOnSale()); // trueSystem.out.println(candy.saledays(200)); // 190candy.expireday(); // Total :365System.out.println(candy.expiredays); // 365System.out.println(Product.expiredays); // 365System.out.println(Expired.expiredays); // 365System.out.println(candy.brand); // Big RabbitSystem.out.println(Product.brand); // Big RabbitSystem.out.println(Expired.brand); // Big Rabbit}
}
注意在什么情况下使用接口。就比如说这个例子中,商品是否过期,可以作为一个接口被商品所实现。但如果说你定义的接口是 Animal(动物),再用 People(人类)去实现这个接口就很奇怪了,因为 Animal 和 People 更合理的是设计成一个类继承关系,而不是接口实现的方式。
多态
接口虽然不能实例化,但是可以用实现接口的类的引用/实例,给接口的引用赋值。类似于可以使用子类的引用/实例给父类引用赋值。
与类继承中存在的方法覆盖导致的多态一样,实现接口的不同类也会表现出这种多态现象。
因为毕竟接口本质上就是一种特殊的类,“实现某个接口”,其实和“继承某个类” 本质上没太大区别。
// 接口 1
public interface Expired {/*** 返回当前在架情况** @return 是否在架*/boolean isOnSale(); // 定义抽象方法/*** 根据过期时间计算还能在商品货架上放多少天** @param day 过期前剩余天数* @return 大概能再放多少天*/int saledays(int day); // 定义抽象方法/*** 返回出厂设定的过期时间*/public abstract void expireday(); // 不会报错,但 IDEA 会提示建议你删除多余的 public abstract 修饰符public static final String brand = "Big Rabbit"; // 定义静态常量int expiredays = 365; // 定义静态常量(省略 public static final)}// 接口 2
public interface VirtualGoods {
}// 实现接口的类 1
public class Product implements Expired,VirtualGoods {@Overridepublic boolean isOnSale() {return true;}@Overridepublic int saledays(int day) {return (int)(day * 0.95);}@Overridepublic void expireday() { // 注意这个方法的实现System.out.println("Total :"+expiredays);}
}// 实现接口的类 2
public class Goods implements Expired{@Overridepublic boolean isOnSale() {return false;}@Overridepublic int saledays(int day) {return 0;}@Overridepublic void expireday() { // 注意这个方法的实现System.out.println("Never expire!");}
}// 调用类
public class TestUse {public static void main(String[] args) {Expired exp = new Product();Expired exp2 = new Goods();exp.expireday(); // Total :365exp2.expireday(); // Never expire!Product product = (Product) exp;VirtualGoods vir = (VirtualGoods) exp; // 不报错,t
// VirtualGoods vir2 = (VirtualGoods) exp2; // 报错。无法将 Goods 转换成 VirtualGoods
// Goods goods = (Goods)exp; // 报错。无法将 Product 转换成 Goodsproduct.expireday(); // Total :365System.out.println(exp instanceof Product); // trueSystem.out.println(exp2 instanceof Product); // falseSystem.out.println(exp2 instanceof Goods); // trueSystem.out.println(exp2 instanceof Expired); // trueSystem.out.println(exp instanceof Expired); // trueSystem.out.println(exp instanceof VirtualGoods); // true}
}// 即关于子类实例给父类引用赋值、父类引用(实际指向子类实例)可以强制转换成子类后给子类引用复制。继承和接口实现的表现是一样的。
IDEA 快捷自动实现接口
可以借助 IDEA 快速实现接口的所有抽象方法。当然这个实现只是搭建出一个不报错的代码框架,里面的方法体内容还是需要我们根据业务逻辑去编写的。
public class Product implements Expired,VirtualGoods {}
将光标定位到接口处,按下 alt + enter
,在弹出的下拉框中点击 Implement methods
,选中要自动实现的抽象方法,点击 OK
确认生成,就会得到如下代码框架,再根据需要自行编写代码。
public class Product implements Expired,VirtualGoods {@Overridepublic boolean isOnSale() {return false;}@Overridepublic int saledays(int day) {return 0;}@Overridepublic void expireday() {}
}
接口的继承
接口也可以继承接口。
接口可以继承多个接口,接口之间的继承也是用 extends
。
继承的接口,可以有重复的方法,但是签名相同时,返回值必须完全一样,否则会有编译错误。
- 当然如果是方法名一样,但参数类型不一样(方法签名不完全相同)则是允许返回类型不同的(方法重载)。
public interface Intf1 {void m1();
}public interface Intf2 {int m1();void m2();
}public interface Intf3 extends Intf1,Intf2 { // 报错,因为 Intf2 的 int m1() 与 Intf1 的 void m1() 冲突了(签名一样,返回值类型不一样,并且若定义为一个 double 另一个 int,仍然会报错,并不存在类型兼容的情形)void m3();
}
接口是否属于多继承?
接口虽然可以继承自多个接口,但和真正的多继承还是有区别的。因为接口本身没有成员变量,所以无论是类实现多个接口,还是接口继承自多个接口,都只能获得一堆方法(并且方法也是有限制的),而不会获得成员变量。这和多继承还是有区别的。
从不同接口继承相同方法
如果继承两个接口,两个接口里有同样的抽象方法,这是没有问题的。
但如果一个类实现了两个接口,并且两个接口里有相同的缺省方法,编译器会报错。
因为这样一来,java 也无法判别到底对于这个方法,是以哪个接口中的缺省方法作为方法体,所以报错。
- 从不同接口,继承相同的抽象方法(均无缺省实现)
public interface Intf1 {void m1();
}public interface Intf2 {void m1();
}public interface Intf3 extends Intf1,Intf2 {
}// 实现接口的类
public class Test implements Intf3 {@Overridepublic void m1() {}
}// 调用类
public class TestUse extends Test{public static void main(String[] args) {Test a = new Test(); // 正常编译运行}
}
- 从不同接口,继承相同的抽象方法,其中一个有缺省实现。
// 实现接口的类不覆盖实现该抽象方法
public interface Intf1 {default void m1(){};
}
public interface Intf2 {void m1();
}
public interface Intf3 extends Intf1,Intf2 {}
public class Test implements Intf3 {}
public class TestUse extends Test{public static void main(String[] args) {Test a = new Test(); // 报错。java: 类型 Intf1 和 Intf2 不兼容;接口 Intf3从类型 Intf1 和 Intf2 中继承了m1() 的抽象和默认值}
}// 要想不报错,做以下尝试:
// 1. 在接口中加入对抽象方法的覆盖实现
// 调用 Intf1 中的 default 方法
public interface Intf3 extends Intf1,Intf2 {@Overridedefault void m1() {Intf1.super.m1();// 若改为以下两种方式,会报错。// Intf1.m1(); // 报错:java: 无法从静态上下文中引用非静态 方法 m1()// 或// super.m1(); // 报错:找不到变量 super。}
}
// 或自己重新写一个方法覆盖
public interface Intf3 extends Intf1,Intf2 {@Overridedefault void m1() {// 留空也能正常运行}
}// 2. 不在 Intf3 中修改,而在 Test 对抽象方法进行实现覆盖。
public class Test implements Intf3 {public void m1(){System.out.println("abc");}
}
// 实测,仍旧会报错:java: 类型 Intf1 和 Intf2 不兼容;接口 Intf3从类型 Intf1 和 Intf2 中继承了m1() 的抽象和默认值// 3. 在 Intf3 中重新声明为抽象方法,然后在 Test 中对抽象方法进行覆盖实现。
public interface Intf3 extends Intf1,Intf2 {void m1();
}
public class Test implements Intf3 {@Overridepublic void m1() {// 留空也能正常运行}
}// 综上所述:
// 从不同接口中继承相同的抽象方法,其中一个有缺省实现,必须在发生继承冲突的节点(例子中是 Intf3 接口)处覆盖实现该抽象方法(可以在覆盖方法中调用有缺省方法体的那个接口的抽象方法,或重新声明为无缺省实现的抽象方法),否则报错。
- 从不同接口,继承相同的抽象方法,两个都有缺省实现。
// 缺省方法体实际内容相同
public interface Intf1 {default void m1(){};
}
public interface Intf2 {default void m1(){};
}
public interface Intf3 extends Intf1,Intf2 {}
public class Test implements Intf3 {}
public class TestUse extends Test{public static void main(String[] args) {Test a = new Test(); // 报错。java: 类型 Intf1 和 Intf2 不兼容;接口 Intf3从类型 Intf1 和 Intf2 中继承了m1() 的抽象和默认值}
}// 要不报错,做以下尝试:
// 1. 在接口中加入对抽象方法的覆盖实现
// 调用 Intf1 中的 default 方法
public interface Intf3 extends Intf1,Intf2 {@Overridedefault void m1() {// Intf2.super.m1(); // 也是可以的。Intf1.super.m1();// 若改为以下两种方式,会报错。// Intf1.m1(); // 报错:java: 无法从静态上下文中引用非静态 方法 m1()// 或// super.m1(); // 报错:找不到变量 super。}
}
// 或自己重新写一个方法覆盖
public interface Intf3 extends Intf1,Intf2 {@Overridedefault void m1() {// 留空也能正常运行}
}// 2. 不在 Intf3 中修改,而在 Test 对抽象方法进行实现覆盖。
public class Test implements Intf3 {public void m1(){System.out.println("abc");}
}
// 实测,仍旧会报错:java: 类型 Intf1 和 Intf2 不兼容;接口 Intf3从类型 Intf1 和 Intf2 中继承了m1() 的抽象和默认值// 3. 在 Intf3 中重新声明为抽象方法,然后在 Test 中对抽象方法进行覆盖实现。
public interface Intf3 extends Intf1,Intf2 {void m1();
}
public class Test implements Intf3 {@Overridepublic void m1() {// 留空也能正常运行}
}// 缺省方法体实际内容不同
public interface Intf1 {default void m1(){};
}
public interface Intf2 {default void m1(){System.out.println();};
}// 实测,和上面的情况一致,没有任何区别,都需要在 Intf3 中进行方法覆盖。// 综上所述:
// 从不同接口中继承相同的抽象方法,其中两个都有缺省实现(无论方法体中实际内容是否相同),必须在发生继承冲突的节点(例子中是 Intf3 接口)处覆盖实现该抽象方法(可以在覆盖方法中调用有缺省方法体的那个接口的抽象方法,或重新声明为无缺省实现的抽象方法),否则报错。
结论:当继承两个(或以上)接口,不同接口有相同的抽象方法(返回值类型相同+方法签名相同),只要其中有一个缺省实现的方法体(不论其他接口中是否也有缺省实现/缺省实现代码实际是否相同),就要在发生继承冲突(extends)的当前接口/发生实现冲突(implements)的当前类中覆盖该抽象方法(反正就是在继承发生交汇冲突的那个节点进行覆盖),否则报错。
- 其中,这个在冲突交汇点的“覆盖”实现,其实还可以是重新去掉方法体,声明为不带缺省实现的抽象方法。
阻绝抽象方法的缺省实现
实现接口或用接口继承接口后,面对有缺省实现的抽象方法,可以有 3 种选择:
-
默默继承(extends)/实现(implements)这个抽象方法的缺省实现;
-
在当前类中覆盖实现该抽象方法(替代原本的缺省方法)/当前接口中重新用另一个缺省方法体覆盖父接口中的的缺省方法;
-
阻绝来自父接口的抽象方法缺省实现,将该方法重新定义为抽象方法。(如果当前是类而非接口,则需要把类定义为抽象类)。
阻绝的目的是让后续的子类不会使用到之前定义的缺省方法体。
抽象类
抽象类可以看作是接口和类的混合体。用 abstract
修饰的类就是抽象类。
抽象类可以有抽象方法,也可以没有抽象方法,但不管包不包含抽象方法,抽象类都不能被实例化。
即便抽象类不含抽象方法,也是有实际意义的,因为有些类从设计(业务逻辑)来说,就不应该被实例化,而是用来被普通子类继承,再将子类实例化的。
抽象类可以继承别的类或抽象类,也可以实现接口。
抽象类中的抽象方法可以是自己定义的,也可以是继承自接口的。
-
这里说的继承自接口不是说抽象类可以
extends
接口。只有接口才能extends
接口,抽象类和其他类一样,只能implements
接口。只不过抽象类可以implements
接口之后不去将接口中的抽象方法实现(写出方法体),也就是实际上抽象类的确是继承了该接口的抽象方法。
接口可以实现类似“多继承”的效果,但抽象类和普通类一样,只允许类的单继承。
抽象类也可以被抽象类继承。
与接口不同,抽象类中不但可以定义非抽象方法(普通的方法),方法还可以是非 public 的,可以是任意的(protected、缺省、private)。
// 接口
public interface Battery {public static final double batteryAmount = 1000;public abstract double getBatteryAmount();String getBatteryBrand();
}// 抽象类
public abstract class Phone implements Battery {public String brand;protected String phoneCode = "ABC"; // 可以有非 public 属性protected abstract String getCountryOfOrigin();public void getCode() { // 可以有非抽象方法。System.out.println("The phone code is: " + phoneCode);}
}// 普通类
public class Iphone extends Phone{String countryOfOrigin = "China";public Iphone(){this.brand = "Apple";}@Overridepublic double getBatteryAmount() {return batteryAmount;}@Overridepublic String getBatteryBrand() {String batterBrand = brand + ".battery.co";return batterBrand;}@Overrideprotected String getCountryOfOrigin() {return countryOfOrigin;}
}// 调用类
public class TestUse {public static void main(String[] args) {Iphone iphone = new Iphone();System.out.println(iphone.brand); // AppleSystem.out.println(iphone.countryOfOrigin); // ChinaSystem.out.println(iphone.countryOfOrigin); // Chinaiphone.getCode(); // The phone code is: ABCSystem.out.println(iphone.getBatteryAmount()); // 1000.0System.out.println(iphone.getBatteryBrand()); // Apple.battery.coPhone iphone2 = new Iphone();System.out.println(iphone2.brand); // AppleSystem.out.println(iphone2.getCountryOfOrigin()); // China// System.out.println(iphone2.countryOfOrigin); // 报错。因为 Phone 类中是没有 countryOfOrigin 这个属性的。iphone2.getCode(); // The phone code is: ABCSystem.out.println(iphone2.getBatteryAmount()); // 1000.0System.out.println(iphone2.getBatteryBrand()); // Apple.battery.coIphone iphone3 = (Iphone) iphone2;System.out.println(iphone3.getBatteryBrand()); // Apple.battery.co}
}
抽象类 vs. 接口
其实接口从 java 8 抽象方法有了默认方法体,java 9 进一步有了 private 方法,就越来越像抽象类了,甚至你可以简单理解接口就是没有成员属性的抽象类。但接口本身定位是“制定规范”,所以业务逻辑的设计中,其实还是尽可能不要跑偏“制定规范”这个目的。
抽象类 | 接口 | |
---|---|---|
被继承 | 单继承 | 被类多实现(implements )/被接口“多继承”(extends ) |
属性修饰符 | 可以是任意的 | 必须为 public static final (可以不写,不写也是这个) |
方法修饰符 | 可以是任意的 | 必须为 public abstract (可以不写,不写也是这个) |
实例化 | 不能 | 不能 |
引用被子类引用/子类实例赋值 | 允许 | 允许 |
如何让接口更像抽象类
因为接口和抽象类的一个很大区别就是接口是没有成员变量的,但接口其实最终是要被一个具体的类实现的,然后这个类可以有实例,实例就可以有属性。可以通过如下的思路设计,让接口更像一个抽象类。
// 接口
public interface Intf1 {String getName(); // 定义一个获取成员属性 name 的值的方法 default void sayHi(){System.out.println(getName() + " says 'hi!'"); // 通过 getName() 方法实现了类似成员属性的效果。// System.out.println(this.getName() + " says 'hi!'"); // 加不加 this 都行,这个this并不是指接口,而是实现接口的类的实例。所以虽然接口不能实例化,还是可以写 this 的。 }
}// 实现接口的类
public class Nor1 implements Intf1 {private String name = "Tom";@Overridepublic String getName() { // 因为具体类是要覆盖抽象类的所有抽象方法的,所以一定会覆盖到这个方法return this.name;}
}// 那么现在对于接口来说,其实就可以通过调用方法来实现类似“拥有成员变量”的效果的// 调用类
public class TestUse {public static void main(String[] args) {Intf1 a = new Nor1();System.out.println(a.getName()); // Tom// System.out.println(a.name); // 报错。而且即便声明 Nor1 a = Nor1(), name 在 Nor1 类中也是 private 的。a.sayHi(); // Tom says 'hi!'}
}