抽象类与抽象方法
概述
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
当声明一些几何图形类,比如:圆、矩形、三角形类等,发现这些类都有共同特征,求面积、求周长、获取图形详细信息等。那么这些共同特征应该抽取到一个公共父类中,但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时就只有方法签名而没有方法体,此时把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类。
语法格式
抽象类:被abstract修饰的类就是抽象类。
抽象类的语法格式:
java
复制代码
[权限修饰符] abstract class 类名{} [权限修饰符] abstract class 类名 extends 父类{}
抽象方法:被abstract修饰且没有方法体的方法就是抽象方法。
抽象方法的语法格式:
java
复制代码
[其他修饰符] abstract 返回值类型 方法名([形参列表]); //抽象方法没有方法体
使用说明
- 抽象类不能创建对象,如果创建则编译无法通过会报错,只能创建其非抽象子类的对象(抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体,若没有重写全部的抽象方法,则扔为抽象类)。
- 抽象类中也有构造方法,是供子类创建对象时初始化父类成员变量使用的(子类的构造方法中有默认的super()或手动的super(实参列表),需要访问父类的构造方法)。
- 抽象类中不一定包含抽象方法,但有抽象方法的类必定是抽象类(未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计中)。
- 抽象类的子类必须重写抽象父类中所有的抽象方法,否则编译无法通过会报错,除非该子类也是抽象类(假设不重写所有的抽象方法,则类中可能包含抽象方法,那么创建对象后,调用抽象的方法将会没有意义)。
注意事项
- 不能用abstract修饰变量、代码块、构造器
- 不能用abstract修饰私有方法、静态方法、final的方法、final的类
应用举例
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展和改造,但子类总体上会保留抽象类的行为方式。
比如:当功能内部一部分实现是确定的,一部分实现是不确定的,这时就可以把不确定的部分暴露出去,让子类去实现。再比如在软件开发中实现一个算法时,整体步骤很固定很通用,这些步骤已经在父类中写好了,但是某些部分易变,易变部分可以抽象出来供不同子类实现,这就是一种模板模式。
java
复制代码
//模板方法的设计模式 public class TemplateTest { public static void main(String[] args) { BankTemplateMethod btm = new DrawMoney(); btm.process(); BankTemplateMethod btm2 = new ManageMoney(); btm2.process(); } } abstract class BankTemplateMethod { //具体方法 public void takeNumber() { System.out.println("取号排队"); } public abstract void transact(); //办理具体的业务,钩子方法 public void evaluate() { System.out.println("反馈评分"); } //模板方法,把基本操作组合到一起,子类一般不能重写 public final void process() { this.takeNumber(); this.transact(); //像个钩子,具体执行时挂哪个子类就执行哪个子类的实现代码 this.evaluate(); } } class DrawMoney extends BankTemplateMethod { public void transact() { System.out.println("我要取款"); } } class ManageMoney extends BankTemplateMethod { public void transact() { System.out.println("我要理财"); } }
接口
概述
接口就是规范,定义的是一组规则,体现了现实世界中如果你是/要...则必须能...的思想。继承是一个是不是的is-a关系,而接口实现则是能不能的has-a关系。
比如:Java程序是否能连接使用某种数据库产品,要看该数据库产品是否实现了Java设计的JDBC规范。
接口的本质是契约、标准、规范,就像法律一样,制定好了都要遵守。
语法格式
接口的定义与类的定义方式相似,不同的是接口使用的是interface
关键字,接口也会被编译成.class文件,但要明确接口不是类,而是另一种引用数据类型(引用数据类型:数组、类、枚举、接口、注解)。
接口的声明格式:
java
复制代码
[修饰符] interface 接口名{ //接口的成员列表有: //公共的静态常量 //公共的抽象方法 //公共的默认方法(JDK1.8以上) //公共的静态方法(JDK1.8以上) //私有方法(JDK1.9以上) }
示例:
java
复制代码
public interface USB3{ //静态常量 long MAX_SPEED = 500*1024*1024; //抽象方法 void in(); void out(); //默认方法 default void start(){ System.out.println("开始"); } default void stop(){ System.out.println("结束"); } //静态方法 static void show(){ System.out.println("USB 3.0可以同步全速的进行读写操作"); } }
在JDK8.0之前,接口中只能出现:
- 公共的静态常量,其中
public static final
可以省略 - 公共的抽象方法,其中
public abstract
可以省略(理解:接口是从多个相似的类中抽象出来的规范,不需要提供具体实现)
在JDK8.0时,接口中允许声明默认方法和静态方法:
- 公共的默认的方法,其中
public
可以省略,建议保留,但default
不能省略 - 公共的静态的方法,其中
public
可以省略,建议保留,但static
不能省略
在JDK9.0时,接口又增加了私有方法。
另外,接口中没有构造器,也没有初始化块,因为接口中没有成员变量需要动态初始化。
使用说明
使用接口的静态成员
接口不能直接创建对象,但可以通过接口名直接调用接口的静态方法和静态常量使用。
java
复制代码
public class Test { public static void main(String[] args) { //通过“接口名.”调用接口的静态方法(JDK8.0才能开始使用) USB3.show(); //通过“接口名.”直接使用接口的静态常量 System.out.println(USB3.MAX_SPEED); } }
使用接口的非静态成员
- 对于接口的静态方法,可以直接使用“接口名.”进行调用,不能通过实现类的对象进行调用
- 对于接口的抽象方法和默认方法,只能通过实现类对象才能调用,不能直接创建对象,只能创建实现类的对象
java
复制代码
public class TestHDD { public static void main(String[] args) { MobileHDD b = new MobileHDD(); //创建实现类对象 //通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法 b.start(); b.in(); b.stop(); //通过接口名调用接口的静态方法 //MobileHDD.show(); //b.show(); Usb3.show(); } }
类实现接口
类可以创建对象,接口不能创建对象,但是接口可以被实现,类似于继承。
类与接口的关系为实现关系,即实现类的接口,该类可以称为接口的实现类。实现类似于继承,格式相仿,不同的只是关键字,实现用的是implements
关键字。
java
复制代码
【修饰符】 class 实现类 implements 接口{ //重写接口中抽象方法(必须),当然如果实现类是抽象类,那么可以不重写 //重写接口中默认方法(可选) } 【修饰符】 class 实现类 extends 父类 implements 接口{ //重写接口中抽象方法(必须),当然如果实现类是抽象类,那么可以不重写 //重写接口中默认方法(可选) }
注意:
- 如果接口的实现类是非抽象类,那么必须重写接口中的所有抽象方法,默认方法可以选择不重写(重写默认方法时,不需要保留default关键字了,因为类中没有默认方法)。
- 接口中的静态方法不能被继承也不能被重写。
java
复制代码
public class MobileHDD implements USB3 { //重写/实现接口的抽象方法(必须) public void out() { System.out.println("读取数据并发送"); } public void in(){ System.out.println("接收数据并写入"); } //重写接口的默认方法(可选),重写默认方法时default单词可去掉 public void end(){ System.out.println("清理回收站内容"); } }
接口的多实现
在前面讲过的继承中,一个类只能继承一个父类,而对于接口而言,一个类是可以实现多个接口的,这就叫做接口的多实现,且一个类能继承一个父类的同时实现多个接口。
下面是多实现的格式:
java
复制代码
【修饰符】 class 实现类 implements 接口1,接口2,接口3。。。{ //重写接口中抽象方法(必须),当然如果实现类是抽象类,那么可以不重写 //重写接口中默认方法(可选) } 【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{ //重写接口中抽象方法(必须),当然如果实现类是抽象类,那么可以不重写 //重写接口中默认方法(可选) }
提示: 接口中有多个抽象方法时,实现类必须重写所有抽象方法,如果抽象方法有重名的,只需要重写一次即可。
接口的多继承
一个接口能继承另一个或多个接口,这就叫做接口的多继承,接口的继承也使用extends
关键字,然后子接口就会继承父接口的方法。
定义父接口:
java
复制代码
public interface Chargeable { void charge(); void in(); void out(); }
定义子接口:
java
复制代码
public interface UsbC extends Chargeable,USB3 { void reverse(); }
定义子接口的实现类:
java
复制代码
public class TypeCConverter implements UsbC { @Override public void reverse() { System.out.println("正反面都支持"); } @Override public void charge() { System.out.println("可充电"); } @Override public void in() { System.out.println("接收数据"); } @Override public void out() { System.out.println("输出数据"); } }
所有父接口的抽象方法都要重写,方法名相同的抽象方法只需要实现一次。
接口与类对象构成的多态引用
实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间也可以构成多态的引用,通过接口类型的变量调用方法,最后执行的是实现类对象实现的方法。
接口的不同实现类:
java
复制代码
public class Mouse implements USB3 { @Override public void out() { System.out.println("发送脉冲信号"); } @Override public void in() { System.out.println("不接收信号"); } }
java
复制代码
public class KeyBoard implements USB3{ @Override public void in() { System.out.println("不接收信号"); } @Override public void out() { System.out.println("发送按键信号"); } }
测试类:
java
复制代码
public class TestComputer { public static void main(String[] args) { Computer computer = new Computer(); USB3 usb = new Mouse(); //多态,左边接口,右边实现类 computer.setUsb(usb); usb.start(); usb.out(); usb.in(); usb.stop(); System.out.println("------"); usb = new KeyBoard(); //多态,左边接口,右边实现类 computer.setUsb(usb); usb.start(); usb.out(); usb.in(); usb.stop(); System.out.println("------"); } }
提示: 接口本身并不复杂,但要想用好接口需要下些功夫。
注意事项
默认方法冲突问题
类优先原则
当一个类既继承一个父类又实现了若干个接口,且父类中的成员方法与接口中的抽象方法重名时,子类就会就近选择执行父类中的成员方法。
示例:
java
复制代码
//定义接口 public interface Friend { default void date(){ System.out.println("逛街"); } } //定义父类 public class Father { public void date(){ System.out.println("吃饭"); } } //定义子类 public class Son extends Father implements Friend { @Override public void date() { //不重写默认保留父类的 //调用父类被重写的 //super.date(); //保留父接口的 //Friend.super.date(); //完全重写 System.out.println("完全重写"); } } //测试类 public class TestSon { public static void main(String[] args) { Son s = new Son(); s.date(); } }
接口冲突
当一个类同时实现了多个接口,而多个父接口中包含方法名相同的默认方法时,这个时候该怎么办呢?
示例:
java
复制代码
//定义接口 public interface BoyFriend { default void date(){ System.out.println("吃饭..."); } } //选择保留其中一个,通过`接口名.super.方法名`的方法选择保留哪个接口的默认方法 public class Girl implements Friend,BoyFriend{ @Override public void date() { //保留其中一个父接口的 //Friend.super.date(); //BoyFriend.super.date(); //完全重写 System.out.println("完全重写..."); } } //测试类 public class TestGirl { public static void main(String[] args) { Girl girl = new Girl(); girl.date(); } }
提示: 子接口重写默认方法时,default关键字可以保留。 子类重写默认方法时,default关键字不可以保留。
常量冲突问题
当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,且该成员变量名在子类中仍然可见,此时在子类中想要引用父类或父接口同名的常量或成员变量时就会有冲突问题。
另外当子类同时实现多个接口,而多个接口中存在相同同名常量时,在子类中想要引用父类或父接口的同名常量或成员变量时也会有冲突问题。
示例:
java
复制代码
//父类 public class SuperClass { int x = 1; } //父接口 public interface SuperInterface { int x = 2; int y = 2; } public interface MotherInterface { int x = 3; } //子类 public class SubClass extends SuperClass implements SuperInterface,MotherInterface { public void method(){ //System.out.println("x = " + x); //模糊不清 System.out.println("super.x = " + super.x); System.out.println("SuperInterface.x = " + SuperInterface.x); System.out.println("MotherInterface.x = " + MotherInterface.x); System.out.println("y = " + y); //没有重名问题,可以直接访问 } }
总结
接口的特点:
- 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类构成多态引用
- 声明接口用关键字
interface
,接口的成员声明有限制,只能是下面几种- 公共的静态常量
- 公共的抽象方法
- 公共的默认方法(JDK8.0及以上)
- 公共的静态方法(JDK8.0及以上)
- 私有方法(JDK9.0及以上)
- 类可以实现接口,关键字是
implements
,且支持多实现,如果实现类不是抽象类,就必须实现接口中所有的抽象方法,如果实现类既要继承父类又要实现父接口,那么继承在前实现在后 - 接口可以继承接口,关键字是
extends
,且支持多继承 - 接口的默认方法可以选择不重写,如果有冲突问题另行处理,子类重写父接口的默认方法要去掉default,子接口重写父接口的默认方法不用去掉default
- 接口的静态方法不能被继承也不能被重写,接口的静态方法只能通过”接口名.静态方法名“进行调用
接口与抽象类的对比:
区别点 | 抽象类 | 接口 |
---|---|---|
定义 | 可以包含抽象方法的类 | 主要是抽象方法和全局常量的集合 |
组成 | 构造方法、抽象方法、普通方法、常量、变量 | 常量、抽象方法(JDK8.0有默认方法和静态方法) |
使用 | 子类继承抽象类(extends) | 子类实现接口(implements) |
关系 | 抽象类可以实现多个接口 | 接口不能继承抽象类,但允许继承多个接口 |
常见设计模式 | 模板方法 | 简单工厂、工厂方法、代理模式 |
对象 | 通过对象的多态性产生实例化对象 | 通过对象的多态性产生实例化对象 |
局限 | 抽象类有单继承的局限 | 接口没有此局限 |
实际 | 作为一个模板 | 是作为一个标准或是表示一种能力 |
选择 | 当设计出的结构需要继承其他类时只能使用抽象类,因为接口不能继承类 | 当子类已经继承了其他的父类,只能使用接口,常把接口用于建立类和类之间的一个协议,如果抽象类和接口都可以使用的话,优先使用接口,因为避免单继承的局限 |
提示: 在开发中,常看到一个类不是去继承一个已经实现好的类,而是要么继承抽象类,要么实现接口。