打怪升级之小白的大数据之旅(十二)
Java基础语法之面向对象的三大特性–继承
上次回顾:
上一章,对面向对象三大特性之一的封装进行介绍,接下来就是特性中的另一个,继承,继承是什么?没错,就是你脑海中想到的子承父业
面向对象三大特性之继承
继承概述
- 生活中的继承
- 父辈的基因可以被孩子继承
- xx女明星是混血,又漂亮,身高,身材都完美,她继承了父亲的身高和外貌,继承了她母亲的气质和身材.
- Java中的继承
- Java中有父类、子类的概念,类似上述生活的例子,父类中的一些属性和方法可以被子类继承下来使用,不再需要重复定义
- 假设我有两个类,猫类和狗类
- 上述示例中,当我有多个类,并且都有共同的属性和方法时,就可以将这两个类利用继承进行重构,将来如果还有别的类,都可以进行继承,如下:
- 图例解释:
1). 多个类称之为子类,也叫派生类,
2). 多个类抽取出来的类叫做父类,超类(superclass)或者基类,上图中,动物类就是子类猫类和狗类的父类 - 继承的优点
1). 提高代码的复用性
2). 提高代码的扩展性
3). 是学习多态的前提 - 继承的缺点
增加了类与类之间的耦合度
继承的语法格式
```java
【修饰符】 class 父类 {
...
}
【修饰符】 class 子类 extends 父类 {
...
}
```
- 示例代码
/* * 定义动物类Animal,做为父类 */ class Animal { // 定义name属性 String name; // 定义age属性 int age; // 定义动物的吃东西方法 public void eat() { System.out.println(age + "岁的" + name + "在吃东西"); } } /* * 定义猫类Cat 继承 动物类Animal */ class Cat extends Animal { // 定义一个猫抓老鼠的方法catchMouse public void catchMouse() { System.out.println("抓老鼠"); } } /* * 定义测试类 */ public class ExtendDemo01 { public static void main(String[] args) { // 创建一个猫类对象 Cat cat = new Cat(); // 为该猫类对象的name属性进行赋值 cat.name = "Tom"; // 为该猫类对象的age属性进行赋值 cat.age = 2; // 调用该猫的catchMouse()方法 cat.catchMouse(); // 调用该猫继承来的eat()方法 cat.eat(); } } 演示结果: 抓老鼠 2岁的Tom在吃东西
继承中变量成员的特点
- 父类的私有属性
- 子类继承父类的所有属性,但是私有属性不能直接访问
- 子类访问父类的私有属性时,可以通过getter.setter方法进行访问
- 示例代码:
/* * 定义动物类Animal,做为父类 */ class Animal { // 定义name属性 private String name; // 定义age属性 public int age; // 定义动物的吃东西方法 public void eat() { System.out.println(age + "岁的" + name + "在吃东西"); } } /* * 定义猫类Cat 继承 动物类Animal */ class Cat extends Animal { // 定义一个猫抓老鼠的方法catchMouse public void catchMouse() { System.out.println("抓老鼠"); } } /* * 定义测试类 */ public class ExtendDemo01 { public static void main(String[] args) { // 创建一个猫类对象 Cat cat = new Cat(); // 为该猫类对象的name属性进行赋值 //cat.name = "Tom";// 编译报错 // 为该猫类对象的age属性进行赋值 cat.age = 2; // 调用该猫的catchMouse()方法 cat.catchMouse(); // 调用该猫继承来的eat()方法 cat.eat(); } }
- 父子类成员变量重名(初始super关键字)
1). 父类成员变量会被子类继承,并可以直接使用,也可以在子类中定义同名的成员变量(不建议这样做,除非是实际需要)
2). 当想在子类中访问父类的成员变量或方法时,可以使用super关键字(下面会详细介绍)
3). super用于在当前类中访问其父类的成员
super.父类成员变量名
4). 示例代码:// 父类 public class Father { public int i=1; private int j=1; public int k=1; public int getJ() { return j; } public void setJ(int j) { this.j = j; } } -------------------------------------------- // 子类 public class Son extends Father{ public int i=2; private int j=2; public int m=2; }
// 在子类son中声明一个test()方法,并获取所有变量的值 public class Son extends Father{ public int i=2; private int j=2; public int m=2; public void test() { System.out.println("父类继承的i:" + super.i); System.out.println("子类的i:" +i); // System.out.println(super.j); System.out.println("父类继承的j:" +getJ()); System.out.println("子类的j:" +j); System.out.println("父类继承的k:" +k); System.out.println("子类的m:" +m); } }
// 调用结果 public class TestSon{ public static void main(String[] args){ Son s = new Son(); s.test(); } } 结果: 父类继承的i:1 子类的i:2 父类继承的j:1 子类的j:2 父类继承的k:1 子类的m:2
注:虽然我们可以区分子类的重名成员变量,但在实际开发中,不建议这么做
方法重写(Override)
- 当子类继承了父类的某个方法,发现父类的这个方法,并不适用于子类的需求,那么可以对这个方法进行重写
- 子类中定义与父类相同的方法,一般方法体不同,用于改造并覆盖父类的方法
- 前面我们学过方法重载,重写和重载是两个不同的东东,不要混淆了哈,教大家一个小技巧,用于区分重写和重载
- 在方法名相同的前提下,看它的参数列表,方法重载的参数列表可以与同名的方法参数列表不同,而重写的参数列表必须相同
- 示例代码:
package com.公司名称.项目名称; /* * 私有的方法不能被重写,子类中不可见 * final修饰符的方法不能被重写 * 静态的方法不能被重写,静态方法属于类分方法,重写的方法是实例方法 * */ public class Demo2{ public static void main(String[] args) { // 实例化 Student stu = new Student(); // 调用方法 stu.sleep(); } } // 父类 class Person{ public void sleep(){ System.out.println("躺着睡~~"); } } // 子类 class Student extends Person{ // 利用override关键字可以查看是否继承 @Override public void sleep(){ // 默认有一个隐藏的super()方法,用于加载父类方法 // 此处就是重写了父类的方法 System.out.println("学生趴着睡~~"); } }
注意事项:
- 静态方法不能被重写,方法重写指的是实例方法的重写,静态方法属于类的方法,不能被重写,而是隐藏了
- 私有方法等,在子类中不可见的方法也不可重写
- final方法不能被重写(这个关键字后面会详细讲)
继承中的构造方法
- 还记得上一章封装中提到的构造器么?当类与类之间产生了关系,那么,它们会产生什么影响呢?
- 通过子类构造器初始化子类对象,必然会先初始化父类数据
- 在子类声明并初始化构造器时,默认在子类构造体中首先使用了一个super(),用于调用父类的构造器
- 构造器名称必须与类名相同,因此,子类是无法继承父类的构造方法的
- 示例代码:
class Fu { private int n; Fu(){ System.out.println("Fu()"); } } class Zi extends Fu { Zi(){ // super(),调用父类构造方法 super();//默认隐藏 System.out.println("Zi()"); } } public class ExtendsDemo07{ public static void main (String args[]){ Zi zi = new Zi(); } } 输出结果: Fu() Zi()
- 继承中构造的一个小坑
运行上述代码,子类的代码会报错,为什么呢?因为父类没有无参构造public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } //其他成员方法省略 } public class Student extends Person{ private int score; }
解决方案 在子类构造器中,用super(实参列表),显示调用父类的有参构造解决:public class Student extends Person{ private int score; public Student(String name, int age) { super(name, age); } public Student(String name, int age, int score) { super(name, age); this.score = score; } //其他成员方法省略 }
- 通过上面的例子,我们可以得出:
- 子类对象实例化过程中必须先完成从父类继承的成员变量的实例初始化,这个过程是通过调用父类的构造方法来完成的
- 也就是说子类的构造方法中至少有一个构造方法显示或隐式的通过super关键字调用了父类的某一个构造方法,否则编译失败
- super():表示调用父类的无参实例初始化方法,要求父类必须有无参构造,而且可以省略不写
- super(实参列表):表示调用父类的有参实例初始化方法,当父类没有无参构造时,子类的构造器首行必须写super(实参列表)来明确调用父类的哪个有参构造(其实是调用该构造器对应的实例初始方法)
- super()和super(实参列表)都只能出现在子类构造器的首行
继承的单继承限制
- Java的继承中只有单继承,不支持多继承,像python中,它的继承有多继承
//一个类只能有一个父类,不可以有多个父类。 class C extends A{} //ok class C extends A,B... //error
- Java的类支持多层继承(继承体系),也就类似一个族谱
// 父亲继承爷爷,儿子继承父亲 class GrandFather{} class Father extends GrandFather{} class Son extends Father{}
- 顶层父类是Object类。所有的类默认继承Object,作为父类
- 子类和父类是一种相对的概念
例如,Father类对于GrandFather类来说是子类,但是对于Son类是父类 - 一个父类可以同时拥有多个子类
this和super关键字
到了this和super关键字了,我可以填补一下前面挖的坑了~如果前面还有坑没填完,欢迎后台吐槽…
this概述
- this前面提到过,它就代表当前对象的引用,谁调用它,它就代表谁
this使用位置
- this在实例初始化相关的代码块和构造器中:表示正在创建的那个实例对象,即正在new谁,this就代表谁
- this在非静态实例方法中:表示调用该方法的对象,即谁在调用,this就代表谁。
- this不能出现在静态代码块和静态方法中
this 使用格式
- this.成员变量名
- 当方法的局部变量与当前对象的成员变量重名时,就可以在成员变量前面加this.,如果没有重名问题,就可以省略this.
- this.成员变量会先从本类声明的成员变量列表中查找,如果未找到,会去从父类继承的在子类中仍然可见的成员变量列表中查找
- this.成员方法
- 调用当前对象的成员方法时,都可以加"this.",也可以省略,实际开发中都省略
- 当前对象的成员方法,先从本类声明的成员方法列表中查找,如果未找到,会去从父类继承的在子类中仍然可见的成员方法列表中查找
- this()或this(实参列表)
- 只能调用本类的其他构造器
- 必须在构造器的首行
- 如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(【实参列表】)",否则会发生递归调用死循环
super概述
super是用于在当前类中访问父类的一个特殊关键字,不是对象的引用。(区别this :super不能单独使用赋值给一个变量)
super使用前提
- 通过super引用父类的xx,都是在子类中仍然可见的
- 不能在静态代码块和静态方法中使用super
super使用格式
-
super.成员变量
在子类中访问父类的成员变量,特别是当子类的成员变量与父类的成员变量重名时public class Person { private String name; private int age; //其他代码省略 } public class Student extends Person{ private int score; //其他成员方法省略 } public class Test{ public static void main(String[] args){ Student stu = new Student(); } }
ublic class Test{ public static void main(String[] args){ Son s = new Son(); s.test(30); } } class Father{ int a = 10; } class Son extends Father{ int a = 20; public void test(int a){ System.out.println(super.a);//10 System.out.println(this.a);//20 System.out.println(a);//30 } }
内存分析图:
-
super.成员方法
在子类中调用父类的成员方法,特别是当子类重写了父类的成员方法时public class Test{ public static void main(String[] args){ Son s = new Son(); s.test(); } } class Father{ public void method(){ System.out.println("aa"); } } class Son extends Father{ public void method(){ System.out.println("bb"); } public void test(){ method();//bb this.method();//bb super.method();//aa } }
-
super()或super(实参列表)
1). 在子类的构造器首行,用于表示调用父类的哪个实例初始化方法
2). super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。
this,super的内存分析
就近原则和追根溯源
-
在继承关系中,如果要访问变量或方法,通常遵循就近原则和追根溯源原则,即先在调用位置最近的位置查找变量或方法,如果没有则去父类找查找,找不到则报错
-
这个很好理解,就是在访问成员时,先找最近的,没有就找它的父亲,再没有那就是真没有,就报错
-
示例一: 找变量
- 没有super和this
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量,
- 如果不是局部变量,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找父类的(非private,跨包还不能是缺省的)
- this :代表当前对象
- 通过this找成员变量时,先从当前执行代码的本类中找,没有的会往上找父类的(非private,跨包还不能是缺省的)。
- super :代表父类的
- 通过super找成员变量,直接从当前执行代码所在类的父类找
- super()或super(实参列表)只能从直接父类找
- 通过super只能访问父类在子类中可见的(非private,跨包还不能是缺省的
- 没有super和this
-
示例二: 找方法
- 没有super和this
- 先从当前对象(调用方法的对象)的本类找,如果没有,再从直接父类找,再没有,继续往上追溯
- this
- 先从当前对象(调用方法的对象)的本类找,如果没有,再从父类继承的可见的方法列表中查找
- super
- 直接从当前对象(调用方法的对象)的父类继承的可见的方法列表中查找
- 没有super和this
成员变量初始化赋值
- 这里比较绕,希望大家可以搞懂,搞明白,搞清楚这里,我们明天的多态就会好理解多了
成员变量初始化方式
首先,先介绍成员变量的初始化方式,为下面的类和示例变量初始化做铺垫:
- 成员变量有默认值
类别 | 具体类型 | 默认值 |
---|---|---|
基本类型 | 整数/浮点数/字符/布尔 | 0/0.0/0或’u0000’/false |
引用类型 | 数组,类,接口 | null |
-
显示赋值
显示赋值,一般都是赋常量值public class Student{ public static final String COUNTRY = "中华人民共和国"; private static String school = "尚硅谷"; private String name; private char gender = '男'; }
-
代码块
如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,那么就需要代码块进行赋值
3.1 静态代码块:为静态变量初始化【修饰符】 class 类名{ static{ 静态初始化块 } }
3.2 实例代码块(构造代码块) 为实例变量初始化
【修饰符】 class 类名{ { 实例初始化块 } }
3.3. 注意事项:
1. 静态初始化块:在类初始化时由类加载器调用执行,每一个类的静态初始化只会执行一次
2. 实例初始化块:每次new实例对象时自动执行,每new一个对象,执行一次
示例代码:public class Student{ private static String school; private String name; private char gender; static{ //获取系统属性,这里只是说明school的初始化过程可能比较复杂 school = System.getProperty("school"); if(school==null) { school = "尚硅谷"; } } { String info = System.getProperty("gender"); if(info==null) { gender = '男'; }else { gender = info.charAt(0); } } public static String getSchool() { return school; } public static void setSchool(String school) { Student.school = school; } public String getName() { return name; } public void setName(String name) { this.name = name; } public char getGender() { return gender; } public void setGender(char gender) { this.gender = gender; } }
-
构造器
构造器在上一章已经详细介绍到了,再次注意一下,通常构造器只为实例变量初始化,不为静态类变量初始化
类变量初始化
- 在使用类前,需要先把类加载到内存并对数据进行初始化,类的加载过程分为三个阶段: 加载—>链接—>初始化
- 类变量的初始化是在整个类的加载过程中完成,过程如下:
- 静态变量初始化为默认值(链接阶段完成)
- 静态变量直接显示赋值语句执行(初始化阶段完成)
- 静态代码块中语句执行(初始化阶段完成)
- 其中,2和3两部分代码是在类的初始化阶段完成,这两部分代码根据书写的先后顺序执行,即谁在前先执行谁。
- 整个类的初始化只会进行一次,如果子类初始化时,发现父类没有初始化,那么会先初始化父类。
- 整个类的加载过程只执行一次,但并不一定一次执行完整个过程,比如加载和链接阶段执行完后,并不一定马上进行初始化阶段,Java虚拟机对类的加载时机(加载阶段)并没有强制约束,但是对类的初始化阶段何时执行进行了严格规定,6种对类的主动使用方式,才会导致类的初始化阶段执行。
- 示例代码一:单个类的初始化
public class Test{ public static void main(String[] args){ Father.test(); } } class Father{ private static int a = getNumber();//这里调用方法为a变量显式赋值的目的是为了看到这个过程 static{ System.out.println("Father(1)"); } private static int b = getNumber(); static{ System.out.println("Father(2)"); } public static int getNumber(){ System.out.println("getNumber()"); return 1; } public static void test(){ System.out.println("Father:test()"); } } // 运行结果 getNumber() Father(1) getNumber() Father(2) Father:test()
- 示例代码二: 父子类
public class Test{ public static void main(String[] args){ Son.test(); System.out.println("-----------------------------"); Son.test(); } } class Father{ private static int a = getNumber(); static{ System.out.println("Father(1)"); } private static int b = getNumber(); static{ System.out.println("Father(2)"); } public static int getNumber(){ System.out.println("Father:getNumber()"); return 1; } } class Son extends Father{ private static int a = getNumber(); static{ System.out.println("Son(1)"); } private static int b = getNumber(); static{ System.out.println("Son(2)"); } public static int getNumber(){ System.out.println("Son:getNumber()"); return 1; } public static void test(){ System.out.println("Son:test()"); } } // 运行结果: 运行结果: Father:getNumber() Father(1) Father:getNumber() Father(2) Son:getNumber() Son(1) Son:getNumber() Son(2) Son:test() ----------------------------- Son:test()
实例变量初始化
- 实例变量的初始化是在实例初始化过程中完成的,每次通过调用构造器创建对象时都会执行一次实例初始化过程
- 实例初始化过程如下:
- 实例变量初始化为默认值
- 实例变量直接显示赋值语句执行
- 构造代码块中语句执行
- 构造器中代码执行
- 其中2和3 两部分代码是按照书写的先后顺序执行的。
- 通过构造器创建并初始化子类对象时,一定会先通过super调用父类某个构造器完成父类的数据的初始化。
- 示例代码一: 单个类
public class Test{ public static void main(String[] args){ Father f1 = new Father(); Father f2 = new Father("atguigu"); } } class Father{ private int a = getNumber(); private String info; { System.out.println("Father(1)"); } Father(){ System.out.println("Father()无参构造"); } Father(String info){ this.info = info; System.out.println("Father(info)有参构造"); } private int b = getNumber(); { System.out.println("Father(2)"); } public int getNumber(){ System.out.println("Father:getNumber()"); return 1; } } // 运行结果: 运行结果: Father:getNumber() Father(1) Father:getNumber() Father(2) Father()无参构造 Father:getNumber() Father(1) Father:getNumber() Father(2) Father(info)有参构造
-
示例代码二: 父子类
public class Test{ public static void main(String[] args){ Son s1 = new Son(); System.out.println("-----------------------------"); Son s2 = new Son("atguigu"); } } class Father{ private int a = getNumber(); private String info; { System.out.println("Father(1)"); } Father(){ System.out.println("Father()无参构造"); } Father(String info){ this.info = info; System.out.println("Father(info)有参构造"); } private int b = getNumber(); { System.out.println("Father(2)"); } public static int getNumber(){ System.out.println("Father:getNumber()"); return 1; } } class Son extends Father{ private int a = getNumber(); { System.out.println("Son(1)"); } private int b = getNumber(); { System.out.println("Son(2)"); } public Son(){ System.out.println("Son():无参构造"); } public Son(String info){ super(info); System.out.println("Son(info):有参构造"); } public static int getNumber(){ System.out.println("Son:getNumber()"); return 1; } } // 运行结果 运行结果: Father:getNumber() Father(1) Father:getNumber() Father(2) Father()无参构造 Son:getNumber() Son(1) Son:getNumber() Son(2) Son():无参构造 ----------------------------- Father:getNumber() Father(1) Father:getNumber() Father(2) Father(info)有参构造 Son:getNumber() Son(1) Son:getNumber() Son(2) Son(info):有参构造
-
示例代码三: 父子类,方法有重写
public class Test{ public static void main(String[] args){ Son s1 = new Son(); System.out.println("-----------------------------"); Son s2 = new Son("atguigu"); } } class Father{ private int a = getNumber(); private String info; { System.out.println("Father(1)"); } Father(){ System.out.println("Father()无参构造"); } Father(String info){ this.info = info; System.out.println("Father(info)有参构造"); } private int b = getNumber(); { System.out.println("Father(2)"); } public int getNumber(){ System.out.println("Father:getNumber()"); return 1; } } class Son extends Father{ private int a = getNumber(); { System.out.println("Son(1)"); } private int b = getNumber(); { System.out.println("Son(2)"); } public Son(){ System.out.println("Son():无参构造"); } public Son(String info){ super(info); System.out.println("Son(info):有参构造"); } public int getNumber(){ System.out.println("Son:getNumber()"); return 1; } } // 运行结果: Son:getNumber() //子类重写getNumber()方法,那么创建子类的对象,就是调用子类的getNumber()方法,因为当前对象this是子类的对象。 Father(1) Son:getNumber() Father(2) Father()无参构造 Son:getNumber() Son(1) Son:getNumber() Son(2) Son():无参构造 ----------------------------- Son:getNumber() Father(1) Son:getNumber() Father(2) Father(info)有参构造 Son:getNumber() Son(1) Son:getNumber() Son(2) Son(info):有参构造
类变量与实例变量初始化比较
- 类初始化: 如果父类没有初始化过,先初始化父类,类的初始化只会进行一次
主要目的是对类变量进行初始化值- 默认初始化值
- 执行类变量直接显示赋值语句
- 执行静态代码块中语句
- 其中,2,3的顺序根据书写的先后顺序执行
- 对象初始化 如果创建子类对象,必须先初始化父类数据,通过super调用父类构造器实现,每次创建对象都会进行初始化
主要目的是对实例变量进行初始化值- 默认初始化值
- 执行实例变量直接显示赋值语句
- 执行构造代码块中语句
- 执行构造器中语句
- 其中2,3根据代码书写先后顺序执行
总结
本章对面向对象的继承就介绍完毕了,里面细节比较多,总的来说,先记得怎么使用继承来简化代码,在实际情况需要的时候,对实例的方法进行重写,其他的知识点,慢慢消化,记得一定要理解初始化的运行顺序,为下一章的多态做好铺垫,多态更绕一些~