[ Java学习基础 ] Java的继承与多态

看到自己写的东西(4.22的随笔[ Java学习基础 ] Java构造函数)第一次达到阅读100+的成就还是挺欣慰的,感谢大家的支持!希望以后能继续和大家共同学习,共同努力,一起进步!共勉!

------------------------------------

一、Java继承

      继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

      继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

生活中的继承如图:

      兔子和羊属于食草动物类,狮子和豹属于食肉动物类;食草动物和食肉动物又是属于动物类。所以继承需要符合的关系是:父类更通用,子类更具体。虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。

为了更好地了解继承性,先看这样一个场景:一位面向对象的程序员小赵,在编程过程中需要描述和处理个人信息,于是定义了类Person,如下所示:

 1 //Person.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Person {
 7 
 8     // 名字
 9     private String name;
10     // 年龄
11     private int age;
12     // 出生日期
13     private Date birthDate;
14 
15     public String getInfo() {
16         return "Person [name=" + name
17                 + ", age=" + age
18                 + ", birthDate=" + birthDate + "]";
19     }
20 
21 }

一周以后,小赵又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类Student,如下所示:

 1 //Student.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Student {
 7 
 8     // 所在学校
 9     public String school;
10     // 名字
11     private String name;
12     // 年龄
13     private int age;
14     // 出生日期
15     private Date birthDate;
16 
17     public String getInfo() {
18         return "Person [name=" + name
19                 + ", age=" + age
20                 + ", birthDate=" + birthDate + "]";
21     }
22 }

很多人会认为小赵的做法能够理解并相信这是可行的,但问题在于Student和Person两个类的结构太接近了,后者只比前者多了一个属性school,却要重复定义其他所有的内容,实在让人“不甘心”。Java提供了解决类似问题的机制,那就是类的继承,代码如下所示:

1 //Student.java文件
2 package com.Kevin;
3 
4 import java.util.Date;
5 
6 public class Student extends Person {
7     // 所在学校
8     private String school;
9 }

Student类继承了Person类中的所有成员变量和方法,从上述代码可见继承使用的关键字是extends,extends后面的Person是父类。

      如果在类的声明中没有使用extends关键字指明其父类,则默认父类为Object类,java.lang.Object类是Java的根类,所有Java类包括数组都直接或间接继承了Object类,在Object类中定义了一些有关面向对象机制的基本方法,如equals()、toString()和finalize()等方法。

Tips:一般情况下,一个子类只能继承一个父类,这称为“单继承”,但有的情况下一个子类可以有多个不同的父类,这称为“多重继承”。在Java中,类的继承只能是单继承,而多重继承可以通过实现多个接口实现。也就是说,在Java中,一个类只能继承一个父类,但是可以实现多个接口。

Tips:面向对象分析与设计(OOAD)时,会用到下面的UML图,其中类图非常重要,用来描述系统静态结构。Student继承Person的类图如下图2所示。类图中的各个元素说明如图2所示,类用矩形表示,一般分为上、中、下三个部分,上部分是类名,中部分是成员变量,下部分是成员方法。实线+空心箭头表示继承关系,箭头指向父类,箭头末端是子类。UML类图中还有很多关系,如图虚线+空心箭头表示实线关系,箭头指向接口,箭头末端是实线类。

图1

图2

继承的特性:

  • 子类拥有父类非private的属性,方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如A类继承B类,B类继承C类,所以按照关系就是C类是B类的父类,B类是A类的父类,这是java继承区别于C++继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系)。

二、调用父类构造方法

当子类实例化时,不仅需要初始化子类成员变量,也需要初始化父类成员变量,初始化父类成员变量需要调用父类构造方法,子类使用super关键字调用父类构造方法。下面看一个示例,现有父类Person和子类Student,它们类图如下图所示:

父类Person代码如下:

 1 //Person.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Person {
 7 
 8     // 名字
 9     private String name;
10     // 年龄
11     private int age;
12     // 出生日期
13     private Date birthDate;
14 
15     // 三个参数构造方法
16     public Person(String name, int age, Date d) {
17         this.name = name;
18         this.age = age;
19         birthDate = d;
20     }
21 
22     public Person(String name, int age) {
23         // 调用三个参数构造方法
24         this(name, age, new Date());
25     }
26     ...
27 }

子类Student代码如下:

 1 //Student.java文件
 2 package com.Kevin;
 3 
 4 import java.util.Date;
 5 
 6 public class Student extends Person {
 7 
 8     // 所在学校
 9     private String school;
10 
11     public Student(String name, int age, Date d, String school) {
12         super(name, age, d);                                        
13         this.school = school;
14     }
15 
16     public Student(String name, int age, String school) {
17         // this.school = school;//编译错误
18         super(name, age);                                           
19         this.school = school;
20     }
21 
22     public Student(String name, String school) { // 编译错误        
23         // super(name, 30);
24         this.school = school;
25     }
26 }

在Student子类代码第12行和第18行是调用父类构造方法,代码第12行super(name, age, d)语句是调用父类的Person(String name, int age, Date d)构造方法,代码第18行super(name, age)语句是调用父类的Person(String name, int age)构造方法。

Tips: super语句必须位于子类构造方法的第一行。

代码第22行构造方法由于没有super语句,编译器会试图调用父类默认构造方法(无参数构造方法),但是父类Person并没有默认构造方法,因此会发生编译错误。解决这个编译错误有三种办法:

  1. 在父类Person中添加默认构造方法,子类Student会隐式调用父类的默认构造方法。
  2. 在子类Studen构造方法添加super语句,显式调用父类构造方法,super语句必须是第一条语句。
  3. 在子类Studen构造方法添加this语句,显式调用当前对象其他构造方法,this语句必须是第一条语句。

三、成员变量隐藏和方法覆盖

3.1 成员变量隐藏      

      子类成员变量与父类一样,会屏蔽父类中的成员变量,称为“成员变量隐藏”。示例代码如下:

 1 //ParentClass.java文件
 2 package com.Kevin;
 3 
 4 class ParentClass {
 5     // x成员变量
 6     int x = 10;                                
 7 }
 8 
 9 class SubClass extends ParentClass {
10     // 屏蔽父类x成员变量
11     int x = 20;                                
12 
13     public void print() {
14         // 访问子类对象x成员变量
15         System.out.println("x = " + x);                
16         // 访问父类x成员变量
17         System.out.println("super.x = " + super.x);    
18     }
19 }

调用代码如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7         //实例化子类SubClass
 8         SubClass pObj = new SubClass();
 9         //调用子类print方法
10         pObj.print();
11     }
12 }

运行结果如下:

x = 20
super.x = 10

上述代码第6行是在ParentClass类声明x成员变量,那么在它的子类SubClass代码第11行也声明了x成员变量,它会屏蔽父类中的x成员变量。那么代码第15行的x是子类中的x成员变量。如果要调用父类中的x成员变量,则需要super关键字,见代码第17行的super.x。

3.2 方法的覆盖

      如果子类方法完全与父类方法相同,即:相同的方法名、相同的参数列表和相同的返回值,只是方法体不同,这称为子类覆盖(Override)父类方法。示例代码如下:

 1 //ParentClass.java文件
 2 package com.Kevin;
 3 
 4 class ParentClass {
 5     // x成员变量
 6     int x;
 7 
 8     protected void setValue() {                     
 9         x = 10;
10     }
11 }
12 
13 class SubClass extends ParentClass {
14     // 屏蔽父类x成员变量
15     int x;
16 
17     @Override
18     public void setValue() { // 覆盖父类方法        
19         // 访问子类对象x成员变量
20         x = 20;
21         // 调用父类setValue()方法
22         super.setValue();
23     }
24 
25     public void print() {
26         // 访问子类对象x成员变量
27         System.out.println("x = " + x);
28         // 访问父类x成员变量
29         System.out.println("super.x = " + super.x);
30     }
31 }

调用代码如下:

//HelloWorld.java文件
package com.Kevin;

public class HelloWorld {

    public static void main(String[] args) {
        //实例化子类SubClass
        SubClass pObj = new SubClass();
        //调用setValue方法
        pObj.setValue();
        //调用子类print方法
        pObj.print();
    }
}

运行结果如下:

x = 20
super.x = 10

上述代码第8行是在ParentClass类声明setValue方法,那么在它的子类SubClass代码第18行覆盖父类中的setValue方法,在声明方法时添加@Override注解,@Override注解不是方法覆盖必须的,它只是锦上添花,但添加@Override注解有两个好处:

  1. 提高程序的可读性。
  2. 编译器检查@Override注解的方法在父类中是否存在,如果不存在则报错。

注意:方法覆盖时应遵循的原则:

  1. 覆盖后的方法不能比原方法有更严格的访问控制(可以相同)。例如将代码第18行访问控制public修改private,那么会发生编译错误,因为父类原方法是protected。
  2. 覆盖后的方法不能比原方法产生更多的异常。

四、多态

4.1 

      多态是同一个行为具有多个不同表现形式或形态的能力,也就是同一个接口,使用不同的实例而执行不同操作,如图所示:

多态性是对象多种表现形式的体现。

现实中,比如我们按下 F1 键这个动作:

  • 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
  • 如果当前在 Word 下弹出的就是 Word 帮助;
  • 在 Windows 下弹出的就是 Windows 帮助和支持。

同一个事件发生在不同的对象上会产生不同的结果。

多态的优点:

  • 1. 消除类型之间的耦合关系
  • 2. 可替换性
  • 3. 可扩充性
  • 4. 接口性
  • 5. 灵活性
  • 6. 简化性

4.2 发生多态的三个前提条件:

  1. 继承。多态发生一定要子类和父类之间。
  2. 覆盖。子类覆盖了父类的方法。
  3. 声明的变量类型是父类类型,但实例则指向子类实例。

      下面通过一个示例让我们更好地理解多态。如下图所示,父类Figure(几何图形)类有一个onDraw(绘图)方法,Figure(几何图形)它有两个子类Ellipse(椭圆形)和Triangle(三角形),Ellipse和Triangle覆盖onDraw方法。Ellipse和Triangle都有onDraw方法,但具体实现的方式不同。

具体代码如下:

 1 //Figure.java文件
 2 package com.Kevin;
 3 
 4 public class Figure {
 5 
 6     //绘制几何图形方法
 7     public void onDraw() {
 8         System.out.println("绘制Figure...");
 9     }
10 }
11 
12 //Ellipse.java文件
13 package com.Kevin;
14 
15 //几何图形椭圆形
16 public class Ellipse extends Figure {
17 
18     //绘制几何图形方法
19     @Override
20     public void onDraw() {
21         System.out.println("绘制椭圆形...");
22     }
23 
24 }
25 
26 //Triangle.java文件
27 package com.Kevin;
28 
29 //几何图形三角形
30 public class Triangle extends Figure {
31 
32     // 绘制几何图形方法
33     @Override
34     public void onDraw() {
35         System.out.println("绘制三角形...");
36     }
37 }

调用代码如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 public class HelloWorld {
 4     public static void main(String[] args) {
 5 
 6         // f1变量是父类类型,指向父类实例
 7         Figure f1 = new Figure();                        
 8         f1.onDraw();
 9 
10         //f2变量是父类类型,指向子类实例,发生多态
11         Figure f2 = new Triangle();                      
12         f2.onDraw();
13 
14         //f3变量是父类类型,指向子类实例,发生多态
15         Figure f3 = new Ellipse();                       
16         f3.onDraw();
17 
18         //f4变量是子类类型,指向子类实例
19         Triangle f4 = new Triangle();                    
20         f4.onDraw();
21 
22     }
23 }

上述带代码第11行和第15行是符合多态的三个前提,因此会发生多态。而代码第7行和第19行都不符合,没有发生多态。

运行结果如下:

绘制Figure...
绘制三角形...
绘制椭圆形...
绘制三角形...

从运行结果可知,多态发生时,Java虚拟机运行时根据引用变量指向的实例调用它的方法,而不是根据引用变量的类型调用。

4.3 引用类型检查

有时候需要在运行时判断一个对象是否属于某个引用类型,这时可以使用instanceof运算符,instanceof运算符语法格式如下:

obj instanceof type

其中obj是一个对象,type是引用类型,如果obj对象是type引用类型实例则返回true,否则false。

      为了介绍引用类型检查,先看一个示例,如下图所示的类图,展示了继承层次树,Person类是根类,Student是Person的直接子类,Worker是Person的直接子类。

继承层次树中具体实现代码如下:

 1 //Person.java文件
 2 package com.Kevin;
 3 public class Person {
 4 
 5     String name;
 6     int age;
 7 
 8     public Person(String name, int age) {
 9         this.name = name;
10         this.age = age;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person [name=" + name
16                 + ", age=" + age + "]";
17     }
18 }
19 
20 //Worker.java文件
21 package com.Kevin;
22 public class Worker extends Person {
23 
24     String factory;
25 
26     public Worker(String name, int age, String factory) {
27         super(name, age);
28         this.factory = factory;
29     }
30 
31     @Override
32     public String toString() {
33         return "Worker [factory=" + factory
34                 + ", name=" + name
35                 + ", age=" + age + "]";
36     }
37 }
38 
39 //Student.java文件
40 package com.Kevin;
41 public class Student extends Person {
42 
43     String school;
44 
45     public Student(String name, int age, String school) {
46         super(name, age);
47         this.school = school;
48     }
49 
50     @Override
51     public String toString() {
52         return "Student [school=" + school
53                 + ", name=" + name
54                 + ", age=" + age + "]";
55     }
56 
57 }

调用代码如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7 
 8         Student student1 = new Student("Tom", 18, "清华大学");          
 9         Student student2 = new Student("Ben", 28, "北京大学");
10         Student student3 = new Student("Tony", 38, "香港大学");        
11 
12         Worker worker1 = new Worker("Tom", 18, "钢厂");                 
13         Worker worker2 = new Worker("Ben", 20, "电厂");                 
14 
15         Person[] people = { student1, student2, student3, worker1, worker2 };    
16 
17         int studentCount = 0;
18         int workerCount = 0;
19 
20         for (Person item : people) {                 
21             if (item instanceof Worker) {            
22                 workerCount++;
23             } else if (item instanceof Student) {    
24                 studentCount++;
25             }
26         }
27         System.out.printf("工人人数:%d,学生人数:%d", workerCount, studentCount);
28     }
29 }

上述代码第8行、9行和第10行创建了3个Student实例,代码第12行和13行创建了两个Worker实例,然后程序把这5个实例放入people数组中。

      代码第20行使用for-each遍历people数组集合,当从people数组中取出元素时,元素类型是People类型,但是实例不知道是哪个子类(Student和Worker)实例。代码第21行item instanceof Worker表达式是判断数组中的元素是否是Worker实例;类似地,第23行item instanceof Student表达式是判断数组中的元素是否是Student实例。

输出结果如下:

工人人数:2,学生人数:3

4.4 引用类型转换:

      引用类型可以进行转换,但并不是所有的引用类型都能互相转换,只有属于同一棵继承层次树中的引用类型才可以转换。示例代码如下:

 1 //HelloWorld.java文件
 2 package com.Kevin;
 3 
 4 public class HelloWorld {
 5 
 6     public static void main(String[] args) {
 7 
 8         Person p1 = new Student("Tom", 18, "清华大学");
 9         Person p2 = new Worker("Tom", 18, "钢厂");
10 
11         Person p3 = new Person("Tom", 28);
12         Student p4 = new Student("Ben", 40, "清华大学");
13         Worker p5 = new Worker("Tony", 28, "钢厂");
14         …
15     }
16 }

上述代码创建了5个实例p1、p2、p3、p4和p5,它们的类型都是Person继承层次树中的引用类型,p1和p4是Student实例,p2和p5是Worker实例,p3是Person实例。首先,对象类型转换一定发生在继承的前提下,p1和p2都声明为Person类型,而实例是由Person子类型实例化的。

下表归纳了p1、p2、p3、p4和p5这5个实例与Worker、Student和Person这3种类型之间的转换关系。

作为这段程序的编写者是知道p1本质上是Student实例,但是表面上看是Person类型,编译器也无法推断p1的实例是Person、Student还是Worker。此时可以使用instanceof操作符来判断它是哪一类的实例。

      引用类型转换也是通过小括号运算符实现,类型转换有两个方向:将父类引用类型变量转换为子类类型,这种转换称为向下转型(downcast);将子类引用类型变量转换为父类类型,这种转换称为向上转型(upcast)。向下转型需要强制转换,而向上转型是自动的。

下面通过示例详细说明一下向下转型和向上转型,在HelloWorld.java的main方法中添加如下代码:

 1 // 向上转型
 2 Person p = (Person) p4;            
 3 
 4 // 向下转型
 5 Student p11 = (Student) p1;        
 6 Worker p12 = (Worker) p2;          
 7 
 8 // Student p111 = (Student) p2;    //运行时异常    
 9 if (p2 instanceof Student) {
10     Student p111 = (Student) p2;
11 }
12 // Worker p121 = (Worker) p1;    //运行时异常      
13 if (p1 instanceof Worker) {
14     Worker p121 = (Worker) p1;
15 }
16 // Student p131 = (Student) p3;    //运行时异常    
17 if (p3 instanceof Student) {
18     Student p131 = (Student) p3;
19 }

上述代码第2行将p4对象转换为Person类型,p4本质上是Student实例,这是向上转型,这种转换是自动的,其实不需要小括号(Person)进行强制类型转换。

      代码第5行和第6行是向下类型转换,它们的转型都能成功。而代码第8、12、16行都会发生运行时异常ClassCastException,如果不能确定实例是哪一种类型,可以在转型之前使用instanceof运算符判断一下。

五、final关键字

5.1 final修饰变量

      final修饰的变量即成为常量,只能赋值一次,但是final所修饰局部变量和成员变量有所不同。

  1. final修饰的局部变量必须使用之前被赋值一次才能使用。
  2. final修饰的成员变量在声明时没有赋值的叫“空白final变量”。空白final变量必须在构造方法或静态代码块中初始化。

final修饰变量示例代码如下:

 1 //FinalDemo.java文件
 2 package com.Kevin;
 3 
 4 class FinalDemo {
 5 
 6     void doSomething() {
 7         // 没有在声明的同时赋值
 8         final int e;                    
 9         // 只能赋值一次
10         e = 100;                        
11         System.out.print(e);
12         // 声明的同时赋值
13         final int f = 200;              
14     }
15 
16     //实例常量
17     final int a = 5; // 直接赋值        
18     final int b; // 空白final变量       
19 
20     //静态常量
21     final static int c = 12;// 直接赋值     
22     final static int d; // 空白final变量    
23 
24     // 静态代码块
25     static {
26         // 初始化静态变量
27         d = 32;                          
28     }
29 
30     // 构造方法
31     FinalDemo() {
32         // 初始化实例变量
33         b = 3;                           
34         // 第二次赋值,会发生编译错误
35         // b = 4;                        
36     }
37 }

上述代码第8行和第10行是声明局部常量,其中第8行只是声明没有赋值,但必须在使用之前赋值(见代码第10行),其实局部常量最好在声明的同时初始化。

      代码第17、18、21和22行都声明成员常量。代码第17和18行是实例常量,如果是空白final变量(见代码第18行),则需要在构造方法中初始化(见代码第33行)。代码第21和22行是静态常量,如果是空白final变量(见代码第22行),则需要在静态代码块中初始化(见代码第27行)。

另外,无论是那种常量只能赋值一次,见代码第⑩行为b常量赋值,因为之前b已经赋值过一次,因此这里会发生编译错误。

5.2 final修饰类

      final修饰的类不能被继承。有时出于设计安全的目的,不想让自己编写的类被别人继承,这是可以使用final关键字修饰父类。

示例代码如下:

//SuperClass.java文件
package com.Kevin;

final class SuperClass {
}

class SubClass extends SuperClass { //编译错误
}

在声明SubClass类时会发生编译错误。

5.3 final修饰方法

      final修饰的方法不能被子类覆盖。有时也是出于设计安全的目的,父类中的方法不想被别人覆盖,这时可以使用final关键字修饰父类中方法。

示例代码如下:

 1 //SuperClass.java文件
 2 package com.Kevin;
 3 
 4 class SuperClass {
 5     final void doSomething() {
 6         System.out.println("in SuperClass.doSomething()");
 7     }
 8 }
 9 
10 class SubClass extends SuperClass {
11     @Override
12     void doSomething() { //编译错误
13         System.out.println("in SubClass.doSomething()");
14     }
15 }

子类中的void doSomething()方法试图覆盖父类中void doSomething()方法,父类中的void doSomething()方法是final的,因此会发生编译错误。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阮一峰的网络日志

Ramda 函数库参考教程

学习函数式编程的过程中,我接触到了 Ramda.js。 我发现,这是一个很重要的库,提供了许多有用的方法,每个 JavaScript 程序员都应该掌握这个工具。...

8618
来自专栏老九学堂

【必读】超全的C语言基础知识大全

我们用一个简单的c程序例子,介绍c语言的基本构成、格式、以及良好的书写风格,加深小伙伴们对C语言的认识。

2392
来自专栏游戏开发那些事

【Cocos2d-x游戏开发】细数Cocos2d-x开发中那些常用的C++11知识

  自从Cocos2d-x3.0开始,Cocos2dx就正式的使用了C++11标准.C++11简洁方便的特性使程序的可拓展性和可维护性大大提高,也提高了代码的书...

1043
来自专栏jojo的技术小屋

原 三、基本概念

作者:汪娇娇 时间:2017年11月4日 一、语法 1、区分大小写 2、标识符 指变量、函数、属性的名字,采用驼峰大小写格式。 3、注释 单行:// 多行:/*...

2605
来自专栏chenjx85的技术专栏

leetcode-80-删除排序数组中的重复项 II

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

1371
来自专栏Golang语言社区

Go 语言数据类型

在 Go 编程语言中,数据类型用于声明函数和变量。 数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充...

3197
来自专栏北京马哥教育

Python语言中list及tuple的使用示例

Python语言中的list Python有一种内置数据类型被称为列表:list。 1.list基本定义 list是一种有序的集合,可以随时添加和删除其中的元...

3257
来自专栏ml

c语言格式大整理

1、C语言中,非零值为真,真用1表示;零值为假,假用0表示。 2、转义字符参考: \a 蜂鸣,响铃  \b 回退:向后退一格 ...

5317
来自专栏Python研发

go基础编程 day-2

  零值并不等于空值,而是当变量声明为某种来兴后的默认零值,通常情况下默认值为0,bool为false,string为空字符串。

1092
来自专栏程序猿DD

第二章 正则表达式位置匹配攻略

第二章 正则表达式位置匹配攻略 正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。 然而大部分人学习正则时,对于匹配位置的重视程度没有那么高。 本...

21310

扫码关注云+社区

领取腾讯云代金券