Day10-第7章 面向对象基础(下)
7.1 抽象类
7.1.1 由来
抽象:即不具体、或无法具体
例如:当我们声明一个几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长、获取图形详细信息。那么这些共同特征应该抽取到一个公共父类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类。
7.1.2 语法格式
- 抽象方法:被abstract修饰没有方法体的方法。
- 抽象类:被abstract修饰的类。
抽象类的语法格式
【权限修饰符】 abstract class 类名{
}
【权限修饰符】 abstract class 类名 extends 父类{
}
抽象方法的语法格式
【其他修饰符】 abstract 返回值类型 方法名(【形参列表】);
注意:抽象方法没有方法体
代码举例:
public abstract class Animal {
public abstract void eat();
}
public class Cat extends Animal {
public void run (){
System.out.println("小猫吃鱼和猫粮");
}
}
public class CatTest {
public static void main(String[] args) {
// 创建子类对象
Cat c = new Cat();
// 调用eat方法
c.eat();
}
}
此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。
7.1.3 注意事项
关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。
-
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
-
抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。
-
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
-
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
7.1.4 修饰符一起使用问题?
| 外部类 | 成员变量 | 代码块 | 构造器 | 方法 | 局部变量 | 内部类(后面讲) | |
|---|---|---|---|---|---|---|---|
| public | √ | √ | × | √ | √ | × | √ |
| protected | × | √ | × | √ | √ | × | √ |
| 缺省 | √ | √ | × | √ | √ | × | √ |
| private | × | √ | × | √ | √ | × | √ |
| static | × | √ | √ | × | √ | × | √ |
| final | √ | √ | × | × | √ | √ | √ |
| abstract | √ | × | × | × | √ | × | √ |
| native | × | × | × | × | √ | × | × |
不能和abstract一起使用的修饰符?
(1)abstract和final不能一起修饰方法和类
(2)abstract和static不能一起修饰方法
(3)abstract和native不能一起修饰方法
(4)abstract和private不能一起修饰方法
static和final一起使用:
(1)修饰方法:可以,因为都不能被重写
(2)修饰成员变量:可以,表示静态常量
(3)修饰局部变量:不可以,static不能修饰局部变量
(4)修饰代码块:不可以,final不能修改代码块
(5)修饰内部类:可以一起修饰成员内部类,不能一起修饰局部内部类
7.2 接口
7.2.1 为什么要使用接口?
多态的使用前提必须是“继承”。而类继承有如下问题:
(1)类继承有单继承限制
(2)类继承表示的是事物之间is-a的关系,但是is-a的关系要求太严格了。
为了解决这两个问题,引入了接口,接口支持:
(1)多实现
(2)实现类和接口是is-like-a关。只要A类想要B接口声明的行为功能,就可以让A类实现B接口,不用考虑逻辑关系。
Bird is a Animal. 鸟是一种动物。
Plane is not a Animal. 飞机不是一种动物。
Plane is a Vehicle. 飞机是一种交通工具。
Bird is like a Flyable。 鸟具有飞的能力。或 鸟会飞。
Plane is like a Flyable。 飞机具有飞的功能。或飞机会飞。
is-a解决的是:是不是的问题
is-like-a解决的是:要不要的问题
生活中的USB接口等思想,也是接口的思想。
7.2.2 定义和使用格式
接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
引用数据类型:数组,类,枚举,接口,注解。
1、接口的声明格式
【修饰符】 interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法
// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}
2、接口的成员说明
接口定义的是多个类共同的公共行为规范,这些行为规范是与外部交流的通道,这就意味着接口里通常是定义一组公共方法。
在JDK8之前,接口中只允许出现:
(1)公共的静态的常量:其中public static final可以省略
(2)公共的抽象的方法:其中public abstract可以省略
理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现
在JDK1.8时,接口中允许声明默认方法和静态方法:
(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略
(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略
在JDK1.9时,接口又增加了:
(5)私有方法:其中private不可以省略
(6)除此之外,接口中不能有其他成员,没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。
3、示例代码
public interface Flyable {
long MAX_SPEED = 299792458;//光速:299792458米/秒, 省略public static final
void fly();//省略public abstract
static void start(){//省略public
System.out.println("start");
}
default void end(){//省略public
System.out.println("end");
}
private void show(){
System.out.println("cool!");
}
}
7.2.3 接口的使用
1、使用接口的静态成员
接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。
public class TestFlyable {
public static void main(String[] args) {
System.out.println(Flyable.MAX_SPEED);//调用接口的静态常量
Flyable.start();//调用接口的静态方法
}
}
2、类实现接口(implements)
接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。
类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。
【修饰符】 class 实现类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
注意:
-
如果接口的实现类是非抽象类,那么必须==重写接口中所有抽象方法==。
-
默认方法可以选择保留,也可以重写。
重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了
-
接口中的静态方法不能被继承也不能被重写
示例代码:
public class Animal {
public void eat(){
System.out.println("吃东西");
}
}
public class Bird extends Animal implements Flyable{
//重写父接口的抽象方法,【必选】
@Override
public void fly() {
System.out.println("我要飞的更高~~~");
}
//重写父接口的默认方法,【可选】
@Override
public void end() {
System.out.println("轻轻落在树枝上~~~");
}
}
3、使用接口的非静态方法
-
对于接口的静态方法,直接使用“接口名.”进行调用即可
- 也只能使用“接口名."进行调用,不能通过实现类的对象进行调用
-
对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
- 接口不能直接创建对象,只能创建实现类的对象
public class TestBirdFlyable {
public static void main(String[] args) {
Bird bird = new Bird();
Flyable.start();//调用接口的静态方法,只能通过 接口名.
//必须依赖于实现类的对象
bird.fly();//调用接口的抽象方法
bird.end();//调用接口的默认方法
bird.eat();//调用父类继承的方法
}
}
4、接口的多实现(implements)
之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。
实现格式:
【修饰符】 class 实现类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
定义多个接口:
public interface Jumpable {
void jump();
}
public interface Runnable {
void jump();
void run();
}
定义实现类:
public class Bird implements Flyable,Jumpable,Runnable{
//重写父接口的抽象方法,【必选】
@Override
public void fly() {
System.out.println("我要飞的更高~~~");
}
//重写父接口的默认方法,【可选】
@Override
public void end() {
System.out.println("轻轻落在树枝上~~~");
}
@Override
public void jump() {
System.out.println("我会跳跳~~~");
}
@Override
public void run() {
System.out.println("我会跑~~");
}
}
测试类
public class TestBird {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly();//调用Flyable接口的抽象方法
bird.jump();//调用Jumpable接口的抽象方法
bird.run();//调用Runnable接口的抽象方法
}
}
5、接口的多继承 (extends)
一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。
定义父接口:
public interface A {
void a();
}
public interface B {
void b();
}
定义子接口:
public interface C extends A,B {
void c();
}
定义子接口的实现类:
public class D implements C {
@Override
public void c() {
System.out.println("重写C接口的抽象方法c");
}
@Override
public void a() {
System.out.println("重写C接口的抽象方法a");
}
@Override
public void b() {
System.out.println("重写C接口的抽象方法b");
}
}
所有父接口的抽象方法都有重写。
方法签名相同的抽象方法只需要实现一次。
6、接口与实现类对象构成多态引用
实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。
接口的不同实现类:
public class Plane implements Flyable{
@Override
public void fly() {
System.out.println("我直入云霄");
}
}
public class Kite implements Flyable {
@Override
public void fly() {
System.out.println("我怎么飞也挣脱不了线");
}
}
测试类
public class TestFlyableImpl {
public static void main(String[] args) {
Flyable f1 = new Bird();
f1.fly();
Flyable f2 = new Plane();
f2.fly();
Flyable f3 = new Kite();
f3.fly();
}
}
7.2.4 接口的特点总结
- 接口本身不能创建对象,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
- 声明接口用interface,接口的成员声明有限制:(1)公共的静态常量(2)公共的抽象方法(3)公共的默认方法(4)公共的静态方法(5)私有方法(JDK1.9以上)
- 类可以实现接口,关键字是implements,而且支持多实现。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后。
- 接口可以继承接口,关键字是extends,而且支持多继承。
- 接口的默认方法可以选择重写或不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
- 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。
7.2.5 关于接口的其他问题
1、面试题拷问?(低频)
1、为什么接口中只能声明公共的静态的常量?
因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。
例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA
USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA
2、为什么JDK1.8之后要允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。
静态方法:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。
默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。
2、默认方法冲突问题
(1)亲爹优先原则
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:
定义接口:
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() {
//(1)不重写默认保留父类的
//(2)调用父类被重写的
// super.date();
//(3)保留父接口的
// Friend.super.date();
//(4)完全重写
System.out.println("学Java");
}
}
定义测试类:
public class TestSon {
public static void main(String[] args) {
Son s = new Son();
s.date();
}
}
(2)左右为难
- 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
无论你多难抉择,最终都是要做出选择的。
声明接口:
public interface BoyFriend {
default void date(){//约会
System.out.println("神秘约会");
}
}
选择保留其中一个,通过“接口名.super.方法名"的方法选择保留哪个接口的默认方法。
public class Girl implements Friend,BoyFriend{
@Override
public void date() {
//(1)保留其中一个父接口的
// Friend.super.date();
// BoyFriend.super.date();
//(2)完全重写
System.out.println("学Java");
}
}
测试类
public class TestGirl {
public static void main(String[] args) {
Girl girl = new Girl();
girl.date();
}
}
- 当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?
另一个父接口:
public interface Usb2 {
//静态常量
long MAX_SPEED = 60*1024*1024;//60MB/s
//抽象方法
void in();
void out();
//默认方法
public default void start(){
System.out.println("开始");
}
public default void stop(){
System.out.println("结束");
}
//静态方法
public static void show(){
System.out.println("USB 2.0可以高速地进行读写操作");
}
}
子接口:
public interface Usb extends Usb2,Usb3 {
@Override
default void start() {
System.out.println("Usb.start");
}
@Override
default void stop() {
System.out.println("Usb.stop");
}
}
小贴士:
子接口重写默认方法时,default关键字可以保留。
子类重写默认方法时,default关键字不可以保留。
3、常量冲突问题
- 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
- 当子类同时继承多个父接口,而多个父接口存在相同同名常量。
此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。
父类和父接口:
public class SuperClass {
int x = 1;
}
package com.atguigu.interfacetype;
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);//没有重名问题,可以直接访问
}
}