目录
前言
在之前的文章中已经解释了多态,同样的方法,根据传入对象的不同,表现出不同的行为模式称之为多态性。说到底,多态就是依赖继承和重写实现的,本篇文章将进一步,浅谈一下抽象类和接口,为什么要提一嘴多态,是因为它的本质就在于多态!
一、抽象类
1、什么是抽象类&&为什么需要抽象类?
先谈为什么:上一篇文章提到了,多态方式的本质需要继承+方法重写共同组成多态。但有一个漏洞:子类可以选择性的覆写方法,父类无法对子类覆写哪些方法做出约束,若需要强制子类覆写一些方法,就可以使用抽象类与抽象方法。
再说说什么是抽象方法:在java中使用abstract定义抽象方法和抽象类(抽象方法所在的类一定是抽象类)抽象方法没有方法体(但是注意,没有方法体的不一定是抽象方法,native本地方法也没有方法体)。
像这种没有具体方法实现的都应该定义为抽象方法。
需要注意的是,抽象类无法直接实例化子类,可以理解为抽象类被定义出来就是做父类的,而子类继承抽象类必须覆写抽象类中的所有抽象方法。
抽象类是普通类的超集,即 普通类有的抽象类全都有,普通类中的普通方法、构造方法抽象类中全部可以存在(但不必须)。换句话说,抽象类就是比普通类多了一些抽象方法。
2、使用原则
1.被abstract定义的类称之为抽象类,抽象类无法直接实例化对象,可以通过向上转型进行实例化。
比如,定义了一个抽象的Sharp类,子类Rect继承了这个抽象类,在new(实例化对象)的时候,可以创建Rect对象向上转型为Sharp,但不能实例化一个抽象类。具体可以看下图:
这样就可以了👇
2.抽象类中可以没有抽象方法,但只要有一个抽象方法,那这个类必须被定义为抽象类,并且继承它的子类中必须重写所有的抽象方法
Rect作为普通类,继承了抽象类,必须覆写其中的抽象方法:
这就是上文提到的,可以用抽象类来限制子类,强制要求子类覆写某些方法,将这些方法定义为抽象方法。
但是,当子类也是抽象类时,就可以不覆写任何的抽象方法。
3.abstract和final以及private不能同时出现
abstract 能否和final关键字一块写出来一定编译出错!原因在于:
- 被final修饰的方法不能重写,抽象方法子类必须重写—>矛盾
2)被final修饰的类不能继承,抽象类生来就是要被继承做父类的—>矛盾
抽象方法 abstract 能否和私有权限private一起出现也是坚决不行的!原因在于:- private abstract一定编译出错!私有方法无法覆写,抽象方法必须覆写—>矛盾!
4.抽象类是普通类的超集,抽象类中仍然可以存在普通方法和构造方法,且子类实例化时仍然满足继承的原则,先调用父类的构造方法,在调用子类的构造方法!
抽象类和抽象方法存在的意义就在于强制要求子类覆写某些方法,但是抽象类仍然满足 is a原则,子类必须是满足继承关系才能继承抽象类,且一个子类只能继承一个抽象类!!!(单继承局限)
抽象类的使用和设计是比较复杂的,一般都是作为一个模板存在的(java中的Servlet就是抽象类的使用)
二、接口
接口,顾名思义,要么表示一种规范(Type C接口 USB 接口),要么表示一种能力(脑机接口)
接口可以理解为 更加纯粹的抽象类(但不是抽象类),因为接口中只有全局常量和抽象方法(JDK8之前)
1、接口的定义与使用方法
1.在java中,使用interface关键字定义接口
在java中 子类使用implements表示实现一个接口,同样的,必须覆写接口中所有的抽象方法:
2.因为接口中普遍只存在全局常量和抽象方法,因此接口中 public、abstract、static、final统统可以省略不写。不写abstract也默认是抽象方法,定义的东西都默认是public访问权限。
需要注意的是,只有接口可以省略这些关键字,抽象类不写public就是默认的default(包)权限。
3.接口同样无法直接实例化对象,必须通过子类向上转型将其实例化(这里说的子类就是实现了该接口的类)。
4.一个子类可以使用implements关键字同时实现多个父接口!(接口优先原则,优先考虑使用接口)
一般来说,除了类似于USB这种特殊接口外,接口一般采用大写的I开头,子类使用impl结尾表示是一个接口的实现子类。
//定义一个IRun接口 这里每条注释下都是不同的文件,需要新建
public interface IRun {
// 抽象方法,没有方法体
void run();
}
//定义一个ISwim接口
public interface ISwim {
void swim();
}
//定义一个IFly接口
public interface IFly {
void fly();
}
//分别定义三个子类,分别实现若干个接口
//猫猫
public class Cat implements IRun{
@Override
public void run() {
System.out.println("小猫只会跑!");
}
}
//修勾
public class Dog implements IRun,ISwim{
@Override
public void run() {
this.testRun();
System.out.println("小狗正在跑");
}
@Override
public void swim() {
System.out.println("小狗正在狗刨");
}
}
//鸭鸭
public class Duck implements IRun,ISwim,IFly{
@Override
public void fly() {
System.out.println("鸭子飞起来了");
}
@Override
public void run() {
System.out.println("鸭子正在陆地上吃虫子");
}
@Override
public void swim() {
System.out.println("鸭子正在惬意的游泳");
}
}
上述代码定义了三个接口,并定义了三个子类分别实现了若干个接口。此时写一个Test文件去测试
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
Duck duck = new Duck();
runTest(cat);
runTest(dog);
runTest(duck);
swimTest(dog);
swimTest(duck);
flyTest(duck);
}
// 定义这个方法时,采用的都是接口参数,接收所有该接口的子类
// 都属于向上转型(多态)
public static void runTest(IRun iRun) {
iRun.run();
}
public static void swimTest(ISwim iSwim) {
iSwim.swim();
}
public static void flyTest(IFly iFly) {
iFly.fly();
}
}
问题来了,为什么这些runTest、swimTest需要使用父接口作为形参进行传参,而不是使用具体的子类?
因为 当使用IRun这个接口作为形参时,只要这个类实现了这个接口,就可以传递到这个方法中,若使用具体的子类作为形参,则这个方法只能接受这一个类型! 使用父接口作为参数,就是为了参数统一化
2、举一个很形象的栗子:电脑和USB设备
一个类要实现USB接口,目的就是为了能接入到另一个设备上,鼠标实现USB接口,连接到电脑上、键盘实现USB接口、摄像头实现USB接口这些都是为了接入到电脑上,所以打印机、摄像头、麦克风、鼠标键盘这些类,都相当于USB接口的实现者,而电脑是USB接口的使用者!
jackUSB就是电脑使用USB的方式,相当于电脑上的USB插孔,而鼠标键盘那些设备只要实现了USB接口(有USB插头)就都可以接入电脑,若参数使用子类的话,相当于参数是键盘子类的口只能插键盘
, 需要插鼠标时有需要新开一个插口,显然是不合理的,因此选择父接口作为参数进行传参。
3、接口的继承与实现
一个类是可以实现多个接口的,正如共享充电宝一样,既有Type-C接口,又有USB接口,这也是接口和抽象类的一个很大的区别,即接口是多继承的,而类是单继承的。因此,若子类同时继承一个类并且实现多个接口时,一定要先使用extends单继承父类,再使用implements多实现接口,否则报错。
同时,接口和接口之间是可以使用extends关键字,接口可以继承父接口,并且是可以多继承的,没有is a原则。
但是!接口一定是不能继承一个类的,因为接口中只有全局常量和抽象方法,若接口继承类,相当于接口中也有了普通方法和构造方法!
4、拓展内容
从JDK8开始,接口中也允许存在普通方法,接口中的普通方法使用default关键字定义,有方法体,子类实现接口后可以直接使用接口中的普通方法,但不需要强制实现或覆写普通方法。
值得一提的是,这里的default关键字只是告知编译器,这个方法是接口中 的普通方法,但其权限仍然是public的!
通过this.关键字直接调用普通方法:
为什么JDK8做出了这样的改进呢,因为有些接口从JDK1.0就有了,当时的接口只有抽象方法,假设JDK1.0有一个接口A,到了JDK8,已经有了10w个子类实现了接口A,若JDK8想给A这个接口拓展一个新的方法,千万不敢!之前的接口只有抽象方法,若拓展新方法,则这10w个子类全部需要重写代码。因此才有了接口中的普通方法,将需要拓展的方法定义为普通方法写入接口,之前的10w个子类就不用重写,而之后定义的实现A接口的类也能使用拓展的方法。
三、抽象类和接口的比较
1、语法
抽象类:
1.由abstract关键词修饰的类称之为抽象类。
2.抽象类中没有实现的方法称之为抽象方法,也需要加关键字abstract。
3.抽象类中也可以没有抽象方法,比如HttpServlet方法。
4.抽象类中可以有已经实现的方法,可以定义成员变量。
接口:
1.由interface关键词修饰的称之为接口;
2.接口中可以定义成员变量,但是这些成员变量默认都是public static final的常量。
3.接口中没有已经实现的方法,全部是抽象方法。
4.一个类实现某一接口,必须实现接口中定义的所有方法。
5.一个类可以实现多个接口
2、使用情景
类在继承抽象类的时候,一个儿子一个爹,爹的财产(非abstract方法和属性)我都继承,同时爹的梦想(abstract方法)我帮他去实现。
类在使用接口就像是一个模块化的机器,安上个音频接口,内部就得做个功放功能。我设计好了要按什么接口就得有什么功能。
总结
总的来说,抽象类和接口的使用都可以优化程序,实现高内聚,低耦合。但在后续的工作学习中,其实大部分情况下都是使用接口的(接口优先不是单纯的语法hhh)。
以上就是关于抽象类和接口的全部学习笔记了,如果有写的不对的地方欢迎指正~