面向对象基础(下)-抽象类

145 阅读15分钟

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 注意事项

关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

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接口等思想,也是接口的思想。

image-20220616183122869.png

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 接口{
    // 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
    // 重写接口中默认方法【可选】
}

注意:

  1. 如果接口的实现类是非抽象类,那么必须==重写接口中所有抽象方法==。

  2. 默认方法可以选择保留,也可以重写。

    重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了

  3. 接口中的静态方法不能被继承也不能被重写

示例代码:

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)左右为难
  • 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

选择困难.jpg

无论你多难抉择,最终都是要做出选择的。

声明接口:

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);//没有重名问题,可以直接访问
    }
}