🎈本章节掌握目标:
🎉(1)什么是抽象类?
🎉(2)为什么要使用抽象类?
🎉(3)抽象类使用的注意事项
🎉(4)接口的用法
目录
一、抽象类
1、抽象类的概念
对于抽象类,从字面意思上看,可以理解成被抽象化后的类。🤔可这又表示什么意思呢?
在面向对象的过程中,通过类的描述来实例化对象,但是有时会出现一些类,这些类没有足够的信息具体地描述某一个对象。这样的类就被称为“抽象类”。
比如,在一个分析图形的过程中,图形类中存在着圆、三角形、正方形等图形,这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域并不是直接存在的,它就是一个抽象概念。
图形Shape和圆、三角形、正方形之间存在继承关系。
图形Shape类不是具体的图形,所以即使其内部存在draw()方法,也没办法实现该方法。
如果我们对这些图形都进行draw()绘画的动作,那么根据对象的不同,所绘制出来的也是不一样的,可图形类(Shape)作为父类,没法具体绘画出具体的图形,导致draw()方法无法具体地实现,因此把图形类(Shape)称为“抽象类”。
在打印图形例子中, 父类 Shape 中的 draw()方法并没有什么实际工作, 主要的绘制图形都是由 Shape 的各种子类(三角形、圆形、正方形)的 draw()方法来完成的.
像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).
打印图形示例:
abstract class Shape{
abstract public void draw();
}
class Circle extends Shape{
@Override
public void draw() {
System.out.println("⚪");
}
}
class triangle extends Shape{
@Override
public void draw() {
System.out.println("▲");
}
}
class square extends Shape{
@Override
public void draw() {
System.out.println("□");
}
}
public class TestDemo1 {
public static void main(String[] args) {
//注意:不能实例化抽象类,此处实例化继承抽象类的子类
Shape shape=new Circle();
Shape shape1=new triangle();
Shape shape2=new square();
shape.draw();
shape1.draw();
shape2.draw();
}
}
2、抽象类的语法
一般情况下,我们把被抽象化的类称为“抽象类”,而在抽象类中被抽象化的方法被称为“抽象方法”。抽象方法不需要有具体的实现语句.
而抽象类和抽象方法都是由abstract修饰的。
抽象类:
abstract class Shape{
........
}
抽象方法:
abstract public void func();
【注意】:
- 当一个方法的具体实现不想写,加个abstract就可以让它抽象化,类也要加上abstract,让其变成抽象类。
- 类中多了一个抽象方法,这个类也得变成抽象类。
- 抽象类和普通类的差不多,内部也可以具有普通成员变量、普通方法和构造方法。
- 抽象类中可以没有抽象方法,但如果一个类中有抽象方法,那该类一定得是抽象类。
3、抽象类的注意事项
在Java中,抽象类的使用有许多要注意的地方。稍不注意,就容易导致程序运行时报错,或出现意想不到的错误结果。
(1)抽象类存在的最大意义就是为了被继承。
(2)抽象类也可以发生向上转型,进一步发生多态
(3)抽象类被实例化。(指实例化自己,还是可以实例化子类对象)
因为抽象类是无法完整地描述一个对象才被抽象化的,怎么可能还能实例化一个对象。本质上没法描述类,为什么还要实例化呢?
(4)抽象类必须被继承,如果继承抽象类的子类是普通类,那一定要重写抽象类中的所有抽象方法;否则该子类也应该为抽象类(被abstract修饰),如果子类也是抽象类,就可以不用重写父类中的抽象方法。
如果让一个抽象类Flower再继承Shape类。
该类确实不用再重写父类(抽象类)的抽象方法,但有一点还需重视。
即使该Flower类,没有重写,但如果后续有普通类继承Flower类,该普通类还是得重写前面抽象类中的所有抽象方法。(欠的总是要还的)
我们可以通过鼠标点击红波浪线该行,点击快捷键Alt+Enter,可以直接进行重写方法。
(5)抽象类和抽象方法都不能被final修饰。
被final修饰的抽象类是没法进行继承的,抽象类是一定要被继承的。抽象类存在的意义就是继承,怎么可能限制抽象类的继承呢?
被final修饰的抽象方法没法进行重写,而继承抽象类,一定要重写抽象类中的抽象方法。
(6)抽象方法不能被private修饰。
(7)抽象方法不可以被static修饰
(8) 抽象类中不一定有抽象方法,但有抽象方法的类一定是抽象类。
(9)抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量。
与普通类唯一的区别是:里面多了抽象方法。
4、抽象类的作用
一般情况下,我们都是用普通的父类和子类进行向上转型,之后再通过调用它们之间重写的方法,发生动态绑定,来具体实现某一个对象的功能。
但还是建议重写抽象类中的抽象方法。
而抽象类本身是不能被实例化的,要想使用,只能先创建一个继承该抽象类的子类,然后让该子类重写抽象类的抽象方法。
🤔如果普通类也可以发生向上转型,抽象类也可以发生向上转型,重写方法。那为什么建议使用抽象类和抽象方法呢?
这是因为,使用抽象类相当于多了一层编译器的校验。
在前面我们可以知道,抽象类存在的最大意义就是为了被继承。
通过子类引用父类(抽象类),重写父类中的所有抽象方法,进一步发生多态,看似与普通类发生多态的流程差不多。
我们使用了普通类继承抽象类,那么我们在编写代码的时候,编译器会提示我们重写抽象类中的抽象方法,否则该程序是无法进行编译的。为我们平时编写代码提供了一层保险,帮助我们不犯错。
充分利用编译器的校验, 在实际开发中是非常有意义的
二、接口
1、接口的概念
对于接口,在日常生活中,我们首先可以联想到的:我们平常充电USB口,电源插头等等。
根据接口的不同,可以插不同的设备。
比如:电脑的USB口上,可以插:U盘、鼠标、键盘...所有符合USB协议的设备
电源插座插孔上,可以插:电脑、电视机、电饭煲...所有符合规范的设备
从上面的例子可以看出,接口是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口是一种行为的规范和标准,给定一个统一的标准。接口是多个类的公共规范,是一种引用数据类型。
最需要知道的一点是,接口看似和类差不多。
但其实在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。(取自Java核心技术卷Ⅰ基础知识)
2、接口的语法
接口其实和类差不多,但还是有所差别。接口的定义格式和类的定义格式基本相同。
接口是用interface关键字修饰的。
将平时定义类的class关键字,更换成interface关键词,就定义了一个接口。
接口的语法格式:
public interface 接口名称{ //成员变量(接口中的成员变量一定得初始化) public static final int count=10;//public static final 是固定搭配,可以省略 int count1=12; // 抽象方法 public abstract void method1(); // public abstract 是固定搭配,可以省略 void method2(); abstract void method3(); void method4(); // 注意:在接口中上述写法都是抽象方法 }
接口的定义示例:
接口当中的静态方法可以有具体的实现public stratic void fun(){},在main{}里,可以不用new对象就可以调用接口内的静态方法,直接接口.方法名()就可以调用!
【注意】:
接口的语法规则:
- 接口是由interface关键字修饰的。
- 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一形式进行定义。
- 接口中的方法都是抽象方法,默认为public abstract。
- 接口中存在成员变量,但都默认为public static final静态常量,都得进行初始化。
- 创建接口时,接口的命名一般都是以大写字母I开头。
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
3、接口的使用
接口不能直接使用,需要有一个类来实现该接口,而该类还需重写接口中所有的抽象方法。
换句话说,一个类如果遵循了特定接口的规范,那么该类需要履行接口中的功能服务。
🤔那么要如何让一个类来实现接口呢?
- 首先需要让该类声明为实现给定的接口;
- 在该类中重写接口中的所有抽象方法;
而将类声明为实现某一接口,需要用implements关键字。
语法格式:
class 类名词 implements 接口名称{
...
}
【注意】:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。
接口使用的代码示例:
以Animal接口,Dog类和Cat类实现该接口为例子
4、接口的特性
接口具有必须注意的特性。
(1)接口虽然是一种引用数据类型,但是不可以实例化new接口的对象,需要通过类实现该接口,之后实例化这个类对象。
(2)接口内的成员变量默认为静态常量,必须初始化,接口当中的成员变量即使不写public static final也会默认是public static final!
(3)接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的默认为 public abstract,即使没有修饰符,编译器也会自动默认为public abstract(而且只能是 public abstract,其他修饰符都会报错)
(4)接口中的方法是不能在接口中实现的,但可以定义静态方法来实现该方法。
(5) 在继承抽象类或实现接口时所有不想重写抽象方法的类,都可以使它变成抽象类
(6) 重写接口中方法时,不能使用default访问权限修饰
(7)接口当中的方法如果要实现,需要用default来修饰。(这个默认的方法可以被重写)
(8)接口中不能有静态代码块和构造方法
(9)接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
以此处代码为例
5、实现多个接口
接口不像类的继承一样(在Java中,类和类之间是单继承的),子类在继承父类的时候,一次只能继承一个父类,没法继承多个。(Java中不支持多继承)
而类实现接口,可以同时实现多个。
实现的多个接口之间是通过逗号分隔开的。
语法格式:
interface Voice{ void voice(); } interface Move{ void move(); } interface Swim{ void swim(); } class Dog implements Voice,Move,Swim{ @Override public void voice() { System.out.println("汪汪叫!"); } @Override public void move() { System.out.println("狗在跑!"); } @Override public void swim() { System.out.println("狗在狗刨游泳!"); } }
【注意】:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类。
对于我们前面学习到的继承关系,子类继承父类的这种关系,比如Animal和Dog类之间表达的是一种is-a 关系(狗是动物)。
而在实现接口这个地方,表达的含义是该Dog类具有Swim,Move等特性.
狗是一种动物,具有跑,游泳的特性。
6、接口之间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,
而接口与接口之间可以多继承。
即:用接口可以达到多继承的目的。 接口可以继承一个接口, 达到复用的效果. 和类继承一样使用 extends 关键字.
也就是说,一个接口继承其他接口,就具备了其他接口的功能。
比如:
接口间的继承相当于把多个接口合并在一起.
三、接口的使用实例
1、Comparable接口
使用类来实现接口,可以帮助我们很方便地进行许多有意思地操作。
(具体内部源码暂不作分析)
🤔下面以学生类对象为例:
如果我们实例化了三个学生对象,给他们各自初始化属性,之后以他们的某一属性为比较,比较三个学生之间的年龄高低或成绩高低,应该得怎么做呢?
(1)首先我们先定义一个学生类
(2)定义一个Student学生类数组,里面存放实例化的学生对象.
(3)如果要对学生对象进行比较,我们得先确定要比较的属性是什么,类和普通的整型是不一样,整型是可以直接比较的, 大小关系明确,可不能说直接用学生的引用变量进行比较。
(4)比如,以学生的年龄作为比较条件,我们可以让Student学生类,实现一个Comparable的接口,该接口是Java中自带的。
Comparable接口在java.lang这个包下面,而java.lang这个包由java解释器自动引入,而不用import语句引入
需要注意的是,在Comparable接口后面有一个尖括号,里面应该存放要比较的类类型。
之后要在该Student类中重写ComparTo()方法
如果没有重写该方法,会报编译错误。
通过查阅java帮助手册:
而且Comparable接口中只有一个方法---compareTo(),
通过重写compareTo方法可以对学生对象数组的属性进行比较排序
(5)如果我们想比较的是学生的年龄,那么我们可以在compareTo()方法中,返回比较的两个学生对象年龄之差。(如果比较的是成绩,把age替换成score即可)
对于重写compareTo方法,我们要注意的是,调用该方法的类型和参数应该是“引用类型”,此处为引用Student类的对象,通过该对象访问其内部的属性来进行比较。
比较后进行的是年龄的升序排序。
this.代表当前引用的对象,o.代表对传入参数的引用
如果返回的是正数,说明前者的学生年龄大;this.age>o.age
如果返回的是负数,说明后者的学生年龄大;this.age<o.age;
如果返回的是0,说明两者的年龄一样大。this.age=o.age;
通过比较好后返回的数值,来确定这比较后两者的前后大小顺序,之后按对应的升序排序关系来放置当前的当前对象和参数对象。
(6)在前面我们定义了一个学生类数组,我们可以通过Arrays.sort()排序来对学生类进行排序。
在Arrays.sort方法中传入的是一个学生类对象数组,在调用Arrays的sort()排序时,就已经通过调用了重写Comparable接口中的compareTo方法学生类对象进行了排序,
之后我们再通过Arrays.toString ()方法将该数组转换为字符串输出,即可输出学生对象之间的年龄大小关系.
【注意】:使用Arrays时,记得要导包.
(7)如果比较的是学生对象的名字(字符串)该怎么比较呢?
只需再调用一个compareTo()方法比较, (具体内部源码暂不作分析)
因为Student类的name也是一个引用类型.
需要用到compareTo再进行一次比较。
此时调用compareTo方法是,String类( name也是一个引用类型)里重写Comparable接口中的compareTo方法。
2、Comparator接口(比较器)
可如果我们比较完年龄后,又想比较学生对象的另外一个属性,该怎么办?
总不能说在原有的基础上去更改吧,后续如果在做比较复杂的项目时,这么做容易导致自己,无法知道该比较是否被进行更改果。
下面引出Comparator(比较器)
通过实现Comparator接口,我们后续可以很方便地对类对象进行各种比较。
如果我们想比较学生的成绩,我们可以定义一个实现Comparator接口的成绩比较类ScoreIgnore.
Comparator接口和Comparable接口一样,后面的尖括号内为要比较的类类型。
通过重写compare方法,来返回比较的结果
之后再通过实例化ScoreIgnore对象,通过Arrays中的sort方法,传入该对象和要比较的学生数组students,进行比较。Arrays类中的sort()方法根据参数列表的不同,调用相对应重载的方法。
该方法中传入了两个参数,分别是学生类对象数组T [ ]a和相对于的实现comparator接口的一个类Comparato<> c。
如果要比较学生对象的名字大小,可以在重写的compare方法内,再调用compareTo方法进行比较.