主页 > 开源代码  > 

类和对象(5)——抽象类和接口

类和对象(5)——抽象类和接口

目录

1. 抽象类

1.1 抽象类的概念

1.2 抽象类语法:abstract关键字

1.3 抽象类的特性

1.4 抽象类的作用

2. 接口

2.1 接口的概念

2.2 接口语法:interface关键字

2.3 接口的实现:implements关键字

2.4 接口的特性

2.5 实现多个接口

2.6 接口间的继承

2.7 接口的多态性

2.8 再谈instanceof

2.8.1 检查接口的实现

2.8.2 类型可见性问题

2.8.3 易混淆的可见性

3. 抽象类与接口的区别


1. 抽象类 1.1 抽象类的概念

在面向对象的概念中,所有的对象都是通过类来描绘的;但是反过来,并不是所有的类都是用来描绘对象的,如果 一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。

抽象类是一种不能被实例化,只能用作其他类的父类的类。它通常用于定义一组具有共同特征和行为的子类的基础框架,这些共同特征和行为以抽象方法的形式呈现,子类需要提供这些抽象方法的具体实现。

1.2 抽象类语法:abstract关键字

那么在java语言中,抽象类要如何表示呢?

java提供了一个abstract关键字,被该关键字修饰的类就是抽象类。

抽象类的语法:

(其他修饰词)  abstract  class 抽象类名{

        ……

}

abstract除了可以修饰类,还可以修饰成员方法使其成为抽象方法。需要注意的是,抽象方法只能存在于抽象类当中

抽象方法的语法:

(其他修饰词)  abstract  class 抽象类名{

        ……

        (其他修饰词)  abstract  返回值类型  抽象方法名 (参数表);   //抽象方法无具体实现

        ……

}

例如:

public abstract class Animal { private String name; private int age; //抽象方法 abstract public void eat(); } 1.3 抽象类的特性

抽象类具有很多的特性和使用要求,我在下面一一列举出来。

1. 抽象类不能被直接实例化成对象。

以上面抽象类Animal为例:

其实这样的要求也很好理解,因为抽象类的产生就不是为了描述对象的,它也没有包含足够的信息来描绘一个具体的对象,必须由它的非抽象子类来实例化对象。


2. 抽象类被继承后,其子类要重写父类中的抽象方法;否则子类也必须设为抽象类,被abstract修饰。

子类Dog没有重写抽象方法,且没有设为抽象类而报错:

package demo3; //包名:demo3 //抽象父类Animal public abstract class Animal { public String name; public int age; //抽象方法 abstract public void eat(); } //Dog类继承自抽象类 public class Dog extends Animal{ public void bark(){ System.out.println(name+"在汪汪叫"); } }

补充:非抽象子类需要重写的抽象方法都来自其直接父类的抽象方法。

假如有A、B、C三个类,A是B的直接父类,B是C的直接父类;A和B都是抽象类,C是普通类。如果B重写了A中的部分抽象方法,那么C要重写的抽象方法有 A剩下的抽象方法 和 B新增加的抽象方法。

例如:

​//抽象类Animal public abstract class Animal { public String name; //2个抽象方法 abstract public void eat(); abstract public void sleep(); } //抽象类Dog public abstract class Dog extends Animal{ //重写了抽象方法eat() public void eat(){ System.out.println(name+"在吃狗粮"); } //新增1个抽象方法walk() abstract public void walk(); } //普通类 柯基 public class Corgi extends Dog{ //报错:没有重写抽象方法sleep()和walk() }

这里Dog继承自Animal,所以Dog包含成员变量name、父类方法eat和sleep 以及自己新增的抽象方法walk。

又因为Dog重写了抽象方法eat,eat方法在Dog类已经不是抽象方法,所以Dog类中只有sleep和walk两个抽象方法。

由于非抽象子类需要重写的抽象方法都来自其直接父类的抽象方法,所以柯基Corgi类必须要重写sleep和walk这两个直接来自Dog类的抽象方法。


3. 抽象方法不能被private、final和static修饰,因为抽象方法要被子类重写。


4. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量。


5. 抽象类中不一定有抽象方法,但是抽象方法只能存在于抽象类当中。

无抽象方法的抽象类:

报错·含抽象方法的普通类:

1.4 抽象类的作用

抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类。 然后让子类重写抽象类中的抽象方法。

有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?

确实如此. 但是使用抽象类相当于多了一重编译器的校验.

使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类 了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不 就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们. 充分利用编译器的校验, 在实际开发中是非常有意义的

2. 接口 2.1 接口的概念

在现实生活中,接口的例子比比皆是,比如:笔记本上的USB口,电源插座等。

电脑侧面的USB口上,可以插:U盘、鼠标、键盘...所有符合USB协议的设备

电源插座的插孔上,可以插:电脑、电视机、电饭煲...所有符合规范的设备

通过上述例子可以看出:接口就是公共的行为规范标准。大家在实现时,只要符合规范标准,就可以通用。

我们知道,在编程上我们用类来描述对象,像电脑、鼠标、U盘这些对象都可以用类来描述。自然的,我们也会有编程上的接口。

在Java中,接口可以看成是多个类的公共规范,也是一种引用数据类型。

2.2 接口语法:interface关键字

接口的语法格式 与 类的语法格式类似,将 class关键字 换成 interface关键字,就定义了一个接口。

接口的语法格式:

(其他限定词)  interface  接口名{

        ……

}

例如:写一个图形接口,规范是要实现画图形方法draw。

public interface IShape { void draw(); //画一个图形 }

注意:

接口的命名采用大驼峰命名法。接口的命名一般以大写字母 I 开头,它的后一个字母也要大写。

2.3 接口的实现:implements关键字

接口不能直接使用,必须要有一个"实现类"来"实现"该接口。而实现指的是:重写接口中的所有抽象方法。(在接口中定义的方法都是抽象方法)

在java中,提供一个关键字implements来让我们完成对接口的实现

实现的语法:

(其他限定词) class 类名 implements 接口名{

        ……

        //重写的接口方法

}

例如:

//接口 public interface IShape { void draw(); //画一个图形 } //实现类 public class Flower implements IShape{ @Override public void draw() { System.out.println("画一朵花……"); } } //测试 public class Test { public static void main(String[] args) { IShape iShape = new Flower();//向上转型 iShape.draw(); } }

可以看到,实现了接口,可以通过向上转型的方式来调用接口函数,这是接口多态性的一种体现。

2.4 接口的特性

1. 与抽象类一样,接口虽是一种引用类型,但是不能直接new接口的对象。

例如:


2. 接口中的成员变量都是被“ public static final ”修饰的。

也就是说,接口中的成员变量是一种静态全局常量。无论是无修饰(default),还是显式用public修饰、static修饰或final修饰,它们最终都是由“ public static final ”共同修饰。

以下面的USB接口为例:

public interface USB { double brand = 10; }


3. 接口中的方法都是由“ public abstract ”修饰。

无论是无修饰(default),还是显式用public修饰或abstract修饰,接口方法都是由“ public abstract ”共同修饰。也就是说接口方法都是公共的抽象方法,必须要求实现类进行方法重写,这体现了接口作为公共规范的作用。

例如:

// 接口IShape

// Flower类

Flower类没有重写IShape接口的draw方法,由于接口方法都是抽象的,所以会报错。

补充:如果实现类没有重写接口的所有抽象方法,那么该实现类要设为抽象类。


4. 由于接口的变量和方法都有固定的修饰符修饰,所以不能用其他修饰符 修饰接口的变量和方法。(比如private、protected)

错误例子:

补充:接口的变量和方法都有默认修饰符,一般建议接口的变量和方法前面都不写任何修饰符。


5. 接口中不能有静态代码块、动态代码块 和 构造方法。

静态代码块一般用来初始化静态变量,动态代码块和构造方法一般用来初始化成员变量,但是接口中的变量都是常量,不能修改。

错误例子:

public interface USB { int BRAND = 10; static { //静态代码块 } { //实例代码块 } USB(int brand){ BRAND = brand; } }


6. 实现类重写接口方法时,重写的方法的包访问权限不能比接口方法的权限要低。即:不能是无修饰词(default)、不能是protected、不能是private。

例如:

【补充:其实 实现类的重写方法的访问权限在一种情况可以是无修饰词(default)的,这种情况我在第8点说】


7(小知识). 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class。


8(拓展). 从Java8版本开始,接口中可以定义default方法(默认方法)。它允许在接口中为方法提供默认的实现。

default方法在接口中的定义方式:

显示使用default关键字:在接口的方法签名前使用default关键字来定义一个默认方法。例如,default void methodName() {...}。包含方法体:与接口中的抽象方法不同,默认方法是具体的方法实现,需要在接口中给出方法的具体代码逻辑。

【注意】: 接口方法被default修饰后,就不能再被final和static修饰。

例如:

//图形·接口 public interface IShape { //抽象方法 void draw1(); //默认方法 default void draw2(){ System.out.println("画两次图形"); } } //圆形·类 :只重写抽象方法 public class Circle implements IShape{ @Override public void draw1() { System.out.println("画一个圆……"); } } //花形·类:抽象方法和默认方法都重写了 public class Flower implements IShape{ @Override public void draw1() { System.out.println("画一朵花……"); } @Override public void draw2() { System.out.println("画两朵花"); } } public class Test { public static void main(String[] args) { Circle circle = new Circle(); circle.draw1(); circle.draw2(); //会使用接口的默认方法 System.out.println("============================"); Flower flower = new Flower(); flower.draw1(); flower.draw2(); } }

【这里的第8点特性属于拓展内容,了解即可】


2.5 实现多个接口

java中没有多继承,即一个子类不能继承自多个父类,但是一个类可以实现多个接口。

语法格式:(所有的接口都跟在implements的后面,每个接口之间用逗号隔开)

【无继承】

  class 实现类 implements 接口1,接口2,……,接口K{

          ……  //实现类的成员

  }

【有继承】(只能有一个父类) (“继承extends”要在“实现implements”前面)

  class 实现类 extends 父类名称 implements 接口1,接口2,……,接口K{

          ……  //实现类的成员

  }

下面演示如何用接口和继承来表示企鹅类:

先有父类Animal:

abstract public class Animal { protected String name; public Animal(String name){ this.name = name; } }

另外我们再提供一组接口, 分别表示 "会飞的", "会跑的", "会游泳的":

interface IFlying { void fly(); } interface IRunning { void run(); } interface ISwimming { void swim(); }

最后我们创建具体的动物——企鹅类(企鹅既能在路上走,也能在水中游)

public class Penguin extends Animal implements IRunning,ISwimming{ public Penguin(String name) { super(name); } @Override public void run() { System.out.println(name+"用两只脚走路"); } @Override public void swim() { System.out.println(name+"用两只翅膀游泳"); } }

tips:在IDEA中使用 ctrl + i 可快速实现接口。

2.6 接口间的继承

类与类之间可以有继承关系,那接口与接口之间可以有继承关系吗?

接口与接口之间可以有继承关系,而且接口可以多继承,即一个接口可以继承自多个接口。

语法格式:(继承语法是extends,每个父接口之间用逗号隔开)

interface 子接口 extends 父接口1,父接口2,……,父接口k{

        ……

}

青蛙是两栖动物,我们用接口间的继承可以这样表示:

interface IRunning { void run(); } interface ISwimming { void swim(); } // 两栖的动物:既能跑, 也能游 interface IAmphibious extends IRunning, ISwimming { } class Frog implements IAmphibious { ... }

2.7 接口的多态性

当实现类实现了接口,那么它就可以向接口类型进行向上转型。这体现了接口的多态性。

接口不仅仅是一种规范标准,它还是一种引用类型,这样的设计提高了代码的可重复性。

【实现多个接口】

实现类实现了多个接口,那么被实现的接口都可以接收实现类的向上转型。

例如:

//接口 public interface ISwimming { void swim(); } public interface IRunning { void run(); } //实现类 public class Penguin implements ISwimming,IRunning{ @Override public void swim() { System.out.println("企鹅游泳"); } @Override public void run() { System.out.println("企鹅走路"); } } public class Dog implements IRunning{ @Override public void run() { System.out.println("狗在奔跑"); } } //多态方法 public class Test { public static void swim(ISwimming iSwimming){ //ISwimming接口类型作为多态类型参数 iSwimming.swim(); //方法会根据动态绑定来使用 } public static void run(IRunning iRunning){ //IRunning接口类型作为多态类型参数 iRunning.run(); } public static void main(String[] args) { Penguin penguin = new Penguin(); swim(penguin); //向上转型传参 run(penguin); System.out.println("=============="); Dog dog = new Dog(); run(dog); } }

这里的Penguin类实现了ISwimming接口和IRunning接口,所以swim方法和run方法可以完成对penguin的向上转型。


【接口间的继承】

如果子接口继承自父接口,而实现类“直接实现”的是子接口,那么父接口也可以接收实现类的向上转型。

例如:

//IAmphibious接口继承自上面的ISwimming、IRunning接口 public interface IAmphibious extends ISwimming,IRunning{ } //Penguin类直接实现IAmphibious接口 public class Penguin implements IAmphibious{ @Override public void swim() { System.out.println("企鹅游泳"); } @Override public void run() { System.out.println("企鹅走路"); } } public class Test { public static void swim(ISwimming iSwimming){ //ISwimming接口类型作为多态类型参数 iSwimming.swim(); } public static void run(IRunning iRunning){ //IRunning接口类型作为多态类型参数 iRunning.run(); } public static void main(String[] args) { Penguin penguin = new Penguin(); swim(penguin); run(penguin); } }

Penguin类实现了Imphibious接口,所以Penguin可以向Imphibious转型。而Imphibious继承自ISwimming接口和IRunning接口,所以Imphibious可以向ISwimming接口和IRunning接口转型。于是乎Penguin可以向ISwimming接口和IRunning接口向上转型

【注意】

如果子接口继承自父接口,实现类直接实现的也是这个父接口,那么该实现类不能向子接口进行向上转型。

错误例子:

//父接口 public interface IRunning { void run(); } //子接口:表示爬行类的动物 public interface ICreeping extends IRunning{ } //实现类:蜥蜴 public class Lizard implements IRunning{ @Override public void run() { System.out.println("蜥蜴在快速爬行"); } } //用子接口ICreeping接收实现类Lizard public class Test { public static void run(ICreeping iCreeping){ iCreeping.run(); } public static void main(String[] args) { Lizard lizard = new Lizard(); run(lizard); //报错:Lizard类型与ICreeping类型不兼容 } }


接口多态性与继承多态性的对比:

2.8 再谈instanceof 2.8.1 检查接口的实现

我在《类和对象(4)——多态》中讲解过instanceof关键字,当时说过“该关键字不仅能判断类,还能判断接口”。但当时还没学到接口,现在让我们看看instanceof对于接口的作用。

instanceof的作用:(对象  instanceof  类名/接口名)

对于类:检查对象是否是指定类型的实例,或该类型的子类实例。对于接口:检查对象是否实现了指定接口,或该接口的父接口。

为什么instanceof检查类的时候,可以检查其子类,而在检查接口的时候,却是可以检查其父接口呢?

这就不得不说一下instanceof检查的实际用途了:

类和instanceof的用途:确保向下转型的安全,从而让子类实例变量接收向下转型后的父类实例变量,使用子类特有的功能。

接口和instanceof的用途:确保对象具有某类多态行为,从而调用该多态方法。

类的举例和解析:

abstract public class Animal { public String name; public Animal(String name){ this.name = name; } } public class Dog extends Animal{ public Dog(String name) { super(name); } public void eat(){ System.out.println(super.name+"在吃狗粮"); } } public class Corge extends Dog{ public Corge(String name) { super(name); } public void eat(){ System.out.println(name+"在吃科技合成肉"); } } public class Test { public static void main(String[] args) { Animal animal1 = new Dog("小狗"); if (animal1 instanceof Dog){ //检查为Dog类的实例 Dog dog = (Dog) animal1; dog.eat(); } Animal animal2 = new Corge("小柯基"); if(animal2 instanceof Dog){ //检查为Dog类的子类实例 Dog dog = (Dog) animal2; dog.eat(); } } }

Dog类和Corge类都有自己特有的eat方法。在animal1中,由于对象的本质是Dog类的实例,故可以安全地将animal1向下转型成Dog类。在animal2中,虽然对象的本质是Corge类的实例,但由于Corge类是Dog类的子类(即Corge类可以向上转型成Dog类),所以它也可以安全地将animal2向下转型成Dog类。这就解释了:为什么instanceof能检查指定类型的子类。

接口的举例和解析:

//父接口 public interface IRunning { void run(); } public interface ISwimming { void swim(); } //子接口 public interface IAmphibious extends ISwimming,IRunning{ } //实现类 public class Penguin extends Animal implements IAmphibious{ @Override public void swim() { System.out.println("企鹅游泳"); } @Override public void run() { System.out.println("企鹅走路"); } } //多态方法 public class Test { public static void run(IRunning iRunning){ iRunning.run(); } public static void main(String[] args) { Animal animal = new Penguin(); //向上转型 if(animal instanceof IAmphibious){ 检查到指定接口的父接口的run行为 run( (IAmphibious)animal ); //此时调用的是Test类中的多态方法run //或写成((IAmphibious)animal).run(); 此时调用的是自己的重写方法run } if(animal instanceof IRunning){ 检查指定接口 run((IRunning) animal); //或写成((IRunning)animal).run(); } } }

子接口继承自多个父接口,实现类直接实现子接口 就相当于是 把所有父接口的行为都实现了一遍,那自然可以使用 以父接口类型为参数类型的多态方法。这解释了:为什么instanceof能检查指定接口的父接口。

小结:


2.8.2 类型可见性问题

如果我们将上面接口例子中Test类(含多态方法的那个)修改成下面这样会发生什么:

可以发现,我们把强制类型转换用的“(IAmphibious)”去掉后居然报错了。这是编译时类型对成员可见性的限制惹的祸。


要明白 “编译时类型对成员可见性的限制”,我们必需先了解编译时类型和运行时类型的概念。

编译时类型(也称 声明类型)

变量在代码中声明的类型。(如" Parent parent;",此时实例变量parent的数据类型Parent就是编译时类型)(编译时类型也包括基础类型,如int、double)编译器仅根据此类型检查可访问的成员变量和方法。

运行时类型(也称 实际类型)

对象实际所属的类型。(如“ new Child() ”)运行时动态决定,但编译时不可见。

当用编译时类型的变量 访问 运行时类型的特有成员时,编译器检查发现该声明类型不具有访问特有成员的能力,直接触发了编译错误。这就是编译时类型对可见性的限制。

强制转换的作用 强制转换的本质是 告诉编译器,请按强转类型检查成员。编译器接受新的编译时类型,从而允许访问该类型定义的成员。

运行时验证 强制转换可能在运行时失败(如 强转类型 与 对象实际类型 不符),因此需配合 instanceof 提前检查类型安全。


2.8.3 易混淆的可见性

上述发生的编译错误属于类型可见性的问题,而不是权限的问题。

成员可见性:

在java中,权限指的就是各种访问修饰符。而成员可见性的控制机制是访问修饰符(比如public、protected),为每个成员变量和方法赋予权限。


类可见性:

类可见性的控制机制也是访问修饰符,为类赋予访问权限。成员可见性是 成员变量和成员方法 被访问修饰符修饰,共有4种权限;类可见性是 类 被访问修饰符修饰,顶级类只有public类或默认类(default)2种。类的可见性是成员可见性的 前提。(如果顶级类是默认类,且内部所有成员都是public修饰的,那么不是同一个包的类就无法访问该顶级类及其所有成员。)

错误例子:

在包test1中有默认类Demo:

当我们在包test2中尝试使用Demo会报错:


包可见性:

包可见性 = default 权限 = 包访问权限 = 仅包内可见。包可见性 包含于 成员可见性和类可见性当中。(包可见性是权限中的一种具体类型)

成员可见性和类的可见性通过访问修饰符定义,具体规则如下:


最终总结:

类可见性 和 成员可见性属于访问权限控制的同一维度(层级不同),且 类可见性是成员可见性的前提。

包可见性是访问权限控制中的一种具体类型(默认修饰符),属于成员可见性和类可见性的一部分。

类型可见性是独立于访问控制的另一维度,属于编译时类型系统的限制。

3. 抽象类与接口的区别

核心区别: 抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法(但是可以包含default方法),子类必须重写所有的抽象方法。

下面的表格是抽象类和接口不同的地方:

抽象类和接口的相同之处:

可以没有任何成员。(此时的接口被称作空接口或标记接口)抽象方法不能被private、static、final修饰。子类/实现类 重写方法的包访问权限不能更低。子类/实现类 没有重写完所有的抽象方法时,必须设置为抽象类。字节码文件的后缀都是.class。

本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ

标签:

类和对象(5)——抽象类和接口由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“类和对象(5)——抽象类和接口