在Java语言中,抽象类是一种很特殊的类。抽象类中往往都包含没有实现过程的抽象方法,这些抽象方法虽然没有具体的实现过程,但它们却能够让父类以统一的形式调用不同子类的同名方法。抽象类和抽象方法的引入使得Java语言具有了多态性。
如果有一些矩形和三角形的纸片,它们的大小各不相同,如何用Java程序求出这些纸片的总面积呢?我们知道:Java语言中一个类就表示一类事物,因此可以定义一个Rectangle类来表示矩形,再定义一个Triangle类来表示三角形。只要在Rectangle和Triangle类中都定义一个计算面积的area()方法,再创建一些Rectangle和Triangle类的对象来表示每一张纸片,最后调用每个对象的area()方法就能算出每张纸片的面积,把这些面积的数值进行相加就能算出所有纸片的总面积。但是,如果纸片的数量比较多,依次求出每张纸片的面积会导致代码量很大,而且因为代码中会依次调用每一个对象的area()方法,所以代码的重复性也很强。为了解决这个问题,可以创建两个数组,这两个数组的类型分别是Rectangle和Triangle,这样就可以把矩形和三角形对象分别存入数组中,然后用循环的方式分别计算矩形和三角形纸片的总面积,最后把计算出的两个总面积数值相加即可。因为有两种形状的纸片,所以要用两个循环来分别计算每种形状纸片的总面积。是否可以只用一个循环就把所有纸片的总面积算出来呢?在Java语言中,父类的引用可以指向子类的对象,并且子类重写了父类的方法时,通过引用调用到的其实是子类的方法。按照这个思路,可以先定义一个表示形状的Shape类,以Shape类作为Rectangle和Triangle共同的父类,并且在Shape类中也定义一个计算面积的area()方法。因为Shape类是Rectangle和Triangle共同的父类,所以一个Shape类型的数组就能够存放所有Rectangle和Triangle的对象。接下来只需要依次调用数组中每一个元素的area()方法求出每个对象的面积,同时在循环中对每个元素的面积值进行累加,这样就能实现只用一个循环计算出所有纸片的总面积。下面的【例06_07】就是按照这样的思路完成了计算总面积的操作。
【例06_07 计算总面积】
Shape.java
- public class Shape {
- //计算面积
- public double area() {
- return 0.0;
- }
- }
Rectangle.java
- public class Rectangle extends Shape{
- double width;//矩形宽度
- double height;//矩形高度
- //构造方法
- Rectangle(double width,double height){
- this.width = width;
- this.height = height;
- }
-
- //计算矩形面积
- public double area(){
- return this.width*this.height;
- }
- }
Triangle.java
- public class Triangle extends Shape{
- double length;//三角形底边长度
- double height;//三角形高
- //构造方法
- Triangle(double length,double height){
- this.length = length;
- this.height = height;
- }
- //计算三角形面积
- public double area(){
- return this.length*this.height/2;
- }
- }
Exam06_07.java
- public class Exam06_07 {
- public static void main(String[] args) {
- double sum = 0;//总面积初始值
- Shape[] shapes = new Shape[4];//创建Shape数组
- //在Shape数组存入4个对象
- shapes[0] = new Rectangle(2, 3);
- shapes[1] = new Triangle(4,6);
- shapes[2] = new Rectangle(4, 5);
- shapes[3] = new Triangle(8,10);
- //通过循环求出所有图形对象总面积
- for(int i=0;i
- sum = sum+shapes[i].area();
- }
- System.out.println("所有图形总面积为:"+sum);
- }
- }
【例06_07】计算出的总面积值为78.0。程序的计算结果虽然是正确的,但Shape类中所定义的area()方法很不合理。在这个方法中,没有任何计算过程,直接返回一个double型常量0.0。之所以这样来定义area()方法,就是因为Shape这个类表示图形,而图形又是一个很宽泛的概念,矩形、三角形、圆形、梯形都是图形。如果没有说明一个图形的具体形状,根本无法写出计算其面积的公式,所以只能简单的把Shape类的area()方法的实现过程定义为“return 0.0;”。方法返回一个double型常量,仅仅是为了与area()方法所声明的返回值类型相匹配,但这样的写法会让人误解为:任何一个Shape类对象的面积都为0。有人会问:既然在Shape类中无法定义出合理的area()方法,能否把这个方法从Shape类当中删除掉呢?假如从Shape类中删除掉area()方法,那么在Exam06_07类的main()方法中,语句“shapes[i].area();”又会因调用不到area()方法而出现语法错误,因为shapes这个数组的类型是Shape,数组中的元素想要调用到area()方法,就要求Shape类中必须存在area()方法。综上所述可以看出:程序员必须要在Shape类中定义一个area()方法,但又无法写出它的实现过程,只能用一种不合理的方式 “勉为其难”的实现它。
为了解决这个问题,Java语言提供了一种特殊的方法,它被称为“抽象方法”。所谓抽象方法”就是没有实现过程的方法。如果程序员必须要定义某个方法,但又无法定义出它的实现过程,就可以把这个方法定义为抽象方法。定义抽象方法的基本格式为:
从这个基本格式中可以看出:抽象方法的返回值类型前面有一个abstract关键字,abstract意为“抽象的”,Java语言就是用这个关键字表明了某个方法是抽象方法。同时,抽象方法没有任何实现过程,在参数列表后面仅有一个分号。抽象方法虽然没有任何实现过程,但只要它被定义在类当中,就表示该类拥有这个方法,这样就能确保通过“引用.方法名()”的形式可以调用到这个方法。另外,抽象方法不需要定义实现过程,又可以避免让程序员用不合理的方式实现它。当一个类中出现了抽象方法,这个类也必须被定义成一种特殊的类,它被称为“抽象类”。抽象类的定义格式为:
如果把Shape类定义成抽象类,并且把它的area()方法定义成抽象方法,那么它的定义代码如下。
- public abstract class Shape {
- //计算面积
- public abstract double area();
- }
可以看出,在重新定义后的Shape类中既保留了计算面积的area()方法,又去掉了它不合理的实现过程。各位读者可以按照以上方式修改【例06_07】中的Shape类。修改代码之后,【例06_07】依然能够运行出正确结果。虽然Shape类的area()方法没有实现过程,但Shape类的两个子类Rectangle和Triangle都重写了area()方法,并且给出了完整的实现过程,而main()方法中的shapes数组中存放的是Shape类的引用,这些引用都指向了一个Rectangle或Triangle类的对象。当在循环中执行语句“shapes[i].area();”时,实际上就是那些Rectangle或Triangle类对象执行了被重写过的area()方法,因此程序能够运行出正确的结果。此处还需说明:子类重写父类的抽象方法,并且给出了完整的实现过程,这个操作在专业上叫做“实现”。例如:Rectangle类重写了Shape类的抽象方法area()并给出完整的实现过程,就可以表述为:Rectangle类实现了Shape类的抽象方法area()。
6.4.2抽象类的详细说明
抽象方法也能被继承。无论子类继承了父类多少个抽象方法,只要没有把这些抽象方法全部实现,那么子类也必须被声明为抽象类,因为那些没有实现的抽象方法虽然是继承而来的,但它们却实实在在的存在于子类当中,前文讲过:只要类中存在抽象方法,这个类就必须被声明为抽象类。所以子类继承了父类的抽象方法后,要么把它们全部都实现,如果不能全部实现,只能把自身也声明为抽象类。事实上,程序员在类中定义抽象方法,就是希望它被子类继承并实现,如果子类不能继承并实现父类的抽象方法,那么抽象方法被定义出来就完全没有任何意义,因为抽象方法自身根本没有任何可以执行的代码。所以,任何阻止抽象方法被继承的操作都会导致语法错误,例如:以final关键字修饰抽象类或抽象方法,以及把抽象方法的访问度设为private,这些操作都是不允许的。
在实际开发过程中,程序员往往会通过抽象方法来为子类设定标准化的行为规范。例如:温度计都可以测量温度,如果一个物品不具备测温功能,它就不能算温度计。假设程序员定义了一个表示温度计的Thermometer类,一旦其他开发者在创建Thermometer的子类时没有为其定义测量温度的方法,那么这个子类实际上就不能表示温度计了。不仅如此,测量温度的方法也必须有统一的名称、参数形式和返回值类型,这样才能够用同一种方式调用子类对象测量温度的方法。如何让Thermometer的每一个子类都有测量温度的方法,并且还能让这些方法具有相同的名称、参数形式和返回类型呢?程序员可以在Thermometer类中定义一个表示测量温度的measureTemp()抽象方法,这样所有Thermometer的子类都必须按照规定好的形式去实现这个抽象方法,否则子类只能成为一个无法创建对象的抽象类。通过这种方式就能确保所有Thermometer的子类都具有测量温度的measureTemp()方法,并且每个子类的measureTemp()方法都有相同的名称、参数形式和返回值类型,这样就能用相同的形式调用这个方法。由此可见:父类中的抽象方法实际上相当于规定了子类必须有哪些方法,并且这些方法的名称、参数和返回值类型也都被规定好了,因此能够起到设定子类行为标准的作用。有读者会问:为什么要把measureTemp()方法定义成抽象方法呢?直接把它定义成一个有实现过程的方法,每个子类都能继承这个方法,这样不就能保证子类都具有测量温度的功能了吗?没有把measureTemp()方法设定为一个有实现过程的普通方法,是因为温度计有很多种,例如水银温度计、电子温度计等,它们都属于温度计这个父类的子类,而每种温度计的测温原理不同,因而measureTemp()方法的实现过程也不尽相同,所以在父类Thermometer中把measureTemp()方法定义成了抽象方法。
抽象类与抽象方法之间并不构成充要条件,具体来说就是:具有抽象方法的类一定是抽象类,但抽象类中却不一定有抽象方法。所以在一个没有任何抽象方法的类前面添加abstract关键字也不会导致语法错误,但类的前面只要类的前面有abstract关键字,它就是一个抽象类。
抽象类不能被创建对象,因为在编译器看来,抽象类是一个没有定义完整的类。抽象类不能创建对象,就如同不能依照一张没有画完的图纸去盖房子一样。有读者会问:能否在抽象类中定义静态方法呢?这样就可以在没有对象的情况下调用这个方法。抽象类中可以定义静态方法,但这个方法一定不能是抽象方法。因为静态方法可以直接通过类名调用,如果调用的是一个没有任何可执行代码的抽象方法,将不会有任何执行结果,这样的静态方法毫无意义,因此静态方法不能被定义成抽象方法。
虽然抽象类不能被创建对象,但它却可以拥有自己的构造方法。很多读者都会认为:对象都是通过调用构造方法创建的,而抽象类根本不能被创建对象,所以它的构造方法也就毫无用处。其实并非如此,构造方法能够对属性进行初始化,虽然抽象类自身不能创建对象,但实现了抽象方法的子类却能创建对象,并且可以用这个构造方法来初始化抽象父类中定义的那些属性。下面的【例06_08】就展示了如何在抽象类中定义构造方法,并且在子类的构造方法中通过super关键字调用这个构造方法。
【例06_08 抽象类的构造方法】
Shape.java
- public abstract class Shape {
- public String name;
- //抽象类的构造方法
- public Shape(String name) {
- this.name = name;
- }
-
- public Shape() {//无参数的构造方法
- }
-
- //计算面积
- public abstract double area();
- }
Rectangle.java
- public class Rectangle extends Shape{
- double width;//矩形宽度
- double height;//矩形高度
- //构造方法
- Rectangle(double width,double height){
- this.width = width;
- this.height = height;
- }
-
- Rectangle(String name,double width,double height){
- super(name);//调用抽象父类的构造方法
- this.width = width;
- this.height = height;
- }
-
- //计算矩形面积
- public double area(){
- return this.width*this.height;
- }
- }
Exam06_08.java
- public class Exam06_08 {
- public static void main(String[] args) {
- Rectangle rectangle = new Rectangle("1号矩形",4,5);
- System.out.println("rectangle对象的名称是:"+rectangle.name);
- }
- }
【例06_08】总共涉及到3个类,分别是Shape、Rectangle和Exam06_08。其中Shape类和Rectangle类都是从【例06_07】的类修改而来的。其中,Shape类中增加了一个String类型的name属性,它表示图形对象的名称。同时,它还增加了两个构造方法,其中有参数的构造方法用来初始化name属性,而添加无参数的构造方法是为了让【例06_07】不出现语法错误,因为Shape类如果没有无参数的构造方法,那么【例06_07】的Rectangle和Triangle类的构造方法都会因为无法自动调用父类无参数构造方法而出现语法错误。【例06_08】的Rectangle类中增加了一个带有3个参数的构造方法,在这个构造方法的第一行通过super关键字完成了对抽象父类构造方法的调用。通过【例06_08】可以看出:抽象类可以定义属于自己的构造方法,虽然不能直接通过这个构造方法创建出对象,但它可以在子类的构造方法中被调用,从而完成初始化父类属性的工作。读者还可以仿照【例06_08】的Rectangle类去修改Triangle类,并尝试在Triangle类中调用抽象父类Shape的构造方法。
除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!