static、初始化与继承:对象如何共享数据和复用行为
理解类成员与实例成员,掌握单例、构造器链和继承的使用边界。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day01|面向对象进阶(一)
本日进入面向对象的高级部分:先用 static 解决”数据要在所有对象之间共享”的问题,再通过代码块、单例设计模式理解类的初始化与对象的唯一性,最后进入面向对象的第二大特征——继承,掌握继承的特点、权限修饰符、方法重写、就近原则以及子类构造器与 this/super 的配合方式。
学完之后应当能够:判断一个成员该定义成类成员还是实例成员;写出饿汉式与懒汉式单例;熟练使用 extends 抽取父类、用 @Override 重写方法,并解释”子类构造器为什么一定先调用父类构造器”。
1. 本日范围:三个问题与知识地图
1.1 课程要回答的三个问题
课件用三个真实存在的问题引出本日全部内容:
- 之前定义的成员变量
name、score属于每个对象,每个对象中都有一份该数据。有些数据是要被所有对象共享的,应该怎么定义? →static修饰成员变量(类变量)。 - 如何让一个类对外只能产生一个对象(例如任务管理器对象)? → 单例设计模式。
- 系统中定义了很多实体类,它们中有很多属性、行为存在重复代码,如何降低代码冗余、提升代码复用? → 继承。
1.2 知识地图
课件目录给出的顺序如下(本文的章节顺序与之一致):
| 序号 | 知识块 | 本文章节 |
|---|---|---|
| 1 | static 修饰成员变量 | 第 2 节 |
| 2 | static 修饰成员变量的应用场景 | 第 2.4 节 |
| 3 | static 修饰成员方法 | 第 3 节 |
| 4 | static 修饰成员方法的应用场景 | 第 3.4 节 |
| 5 | static 的注意事项 | 第 4 节 |
| 6 | static 的应用知识:代码块 | 第 5 节 |
| 7 | static 的应用知识:单例设计模式 | 第 6 节 |
| 8 | 面向对象三大特征之二:继承 | 第 7–12 节 |
2. static 修饰成员变量:类变量
2.1 成员变量的两种类型
static 叫静态,可以修饰成员变量、成员方法。
成员变量按照有无 static 修饰分为两种:
- 类变量(静态成员变量):有
static修饰,属于类,在计算机里只有一份,会被类的全部对象共享。 - 实例变量(对象的变量):无
static修饰,属于每个对象,每个对象中都有一份。
访问方式:
类名.类变量(推荐)对象.类变量(不推荐)对象.实例变量(实例变量只能用对象访问)
public class Student {
// 类变量
static String name;
// 实例变量(对象变量)
int age;
}
2.2 类变量与实例变量的区别
| 类型 | 修饰 | 归属 | 内存中的份数 | 访问方式 |
|---|---|---|---|---|
| 类变量(静态成员变量) | 有 static | 属于类 | 只有一份,可被类和类的所有对象共享 | 类名.类变量(推荐)、对象.类变量(不推荐) |
| 实例变量(对象变量) | 无 static | 属于每个对象 | 每个对象中一份 | 对象.实例变量 |
补充说明:课件把类变量的特点表述为”与类一起加载一次”;更准确的表述是在类初始化阶段完成类变量的赋值(类只会初始化一次,因此类变量在内存中确实只有一份)。
2.3 类变量与实例变量的执行原理
课件用一段代码 + 内存图说明:方法区里存着 Test.class、Student.class 两张”设计图”,类变量 static String name 跟着 Student.class 只有一份;每个 new Student() 在堆内存中各自拥有一个 int age。
public class Test {
public static void main(String[] args) {
// 1、类变量的用法
// 类名.类变量(推荐)
Student.name = "袁华";
// 对象.类变量(不推荐)
Student s1 = new Student();
s1.name = "马冬梅";
Student s2 = new Student();
s2.name = "秋雅";
System.out.println(s1.name); // 秋雅(共享同一份,最后一次赋值生效)
System.out.println(Student.name); // 秋雅
// 2、实例变量的用法
// 对象.实例变量
s1.age = 23;
s2.age = 18;
System.out.println(s1.age); // 23
// System.out.println(Student.age); // 报错:实例变量不能用类名访问
}
}
内存中的取值变化(课件图注):name 初始为 null,依次被赋为 袁华 → 马冬梅 → 秋雅,所以 s1.name、Student.name 都是 秋雅;而 s1.age = 23、s2.age = 18 各自独立。
小结(课件原文):
- 类变量:属于类,与类一起加载一次,在内存中只有一份,可以被类和类的所有对象共享。
- 实例变量:属于对象,每个对象中都有一份,只能用对象访问。
2.4 类变量的应用场景:统计创建了多少个对象
应用场景:开发中如果某个数据只需要一份,且希望能被共享(访问、修改),就可以定义成类变量来记住。
案例:系统启动后,要求用户类可以记住自己创建了多少个用户对象。
public class User {
// 类变量:只有一份,可以被共享
public static int number;
// 构造器:每创建一次对象就自增一次
public User() {
number++; // 访问当前类中的类变量,可以省略类名不写
}
}
public class Test2 {
public static void main(String[] args) {
new User();
new User();
new User();
System.out.println("创建了几个对象:" + User.number); // 3
}
}
2.5 小结与追问
- 成员变量有几种?各自在什么情况下定义?
- 类变量:数据只需要一份,且需要被共享(访问、修改)时。
- 实例变量:每个对象都要有一份、数据各不同(如
name、score、age)。
- 访问自己类中的类变量,是否可以省略类名不写?可以。 注意:在某个类中访问其他类里的类变量,必须带类名访问。
3. static 修饰成员方法:类方法
3.1 成员方法的分类与访问方式
成员方法按照有无 static 修饰也分为两种:
- 类方法(静态方法):有
static修饰的成员方法,属于类。可以直接用类名访问,也可以用对象访问。 - 实例方法(对象的方法):无
static修饰的成员方法,属于对象,只能用对象访问。
访问方式:
类名.类方法(推荐)对象名.类方法(不推荐)对象.实例方法
public class Student {
double score;
// 类方法:有 static 修饰,属于类
public static void printHelloWorld() {
System.out.println("Hello World!");
System.out.println("Hello World!");
}
// 实例方法(对象的方法)
public void printPass() {
System.out.println("成绩:" + (score >= 60 ? "及格" : "不及格"));
}
}
public class Test {
public static void main(String[] args) {
// 1、类方法的用法
// 类名.类方法(推荐)
Student.printHelloWorld();
// 对象.类方法(不推荐)
Student s = new Student();
s.printHelloWorld();
// 2、实例方法的用法
// 对象.实例方法
s.printPass();
// Student.printPass(); // 报错:实例方法不能用类名访问
}
}
课件给出的执行原理图说明:方法区中 Student.class 保存 public void printPass()… 与 public static void printHelloWorld()… 两张方法表,堆内存中的 Student s 对象只持有 double score,调用类方法时不需要对象参与。
小结:
static修饰的成员方法叫类方法(静态方法),属于类,可以直接用类名访问,也可以用对象访问。- 无
static修饰的成员方法叫实例方法(对象的方法),属于对象,只能用对象访问。
3.2 补充知识:main 方法为什么能直接运行
课件的”搞懂 main 方法”环节指出:public static void main(String[] args) 本身就是一个类方法,因此 JVM 不需要先创建对象,直接以 Test.main(...) 的形式用类名调用即可运行。这也解释了为什么程序入口必须写成 public static void main。
4. 类方法的常见应用场景:工具类
4.1 什么是工具类
类方法最常见的应用场景是做工具类。
- 工具类是什么? 工具类中的方法都是一些类方法,每个方法都是用来完成一个功能的,工具类是给开发人员共同使用的。
- 使用类方法来设计工具类有啥好处? 提高了代码复用;调用方便,提高了开发效率。
public class XxxxUtil {
public static void xxx() {
...
}
public static boolean xxxx(String email) {
...
}
public static String xxxxx(int n) {
...
}
...
}
(片段:课件用 XxxxUtil 示意工具类的骨架,方法体省略。)
课件演示了两个使用场景页面:登录页需要生成验证码,注册页也需要生成验证码——同一段逻辑被两处复用,正是”工具类 + 类方法”的典型价值。
工程中的对应实现(生成指定位数的验证码):
public class ItheimaUtil {
// 工具类没有创建对象的需求,建议将构造器私有化
private ItheimaUtil() {
}
public static String createCode(int n) {
String code = "";
String data = "abcdFGHIJKLMNOPQRSTUVWXYZ01234evwxyzABCDfghijklmnopqrstuE56789";
Random r = new Random();
for (int i = 0; i < n; i++) {
int index = r.nextInt(data.length()); // 随机产生一个索引
char c = data.charAt(index); // 取出该索引处的字符
code += c; // 拼接
}
return code;
}
}
4.2 为什么用类方法而不是实例方法
- 实例方法需要创建对象来调用,此时对象只是为了调用方法而存在,对象占内存,会浪费内存。
- 类方法直接用类名调用即可,调用方便,也能节省内存。
多学一招:工具类没有创建对象的需求,建议将工具类的构造器私有化(如上面的 private ItheimaUtil(){})。
4.3 小结
| 问题 | 回答 |
|---|---|
| 类方法有啥应用场景? | 可以用来设计工具类 |
| 工具类是什么,有什么好处? | 工具类中的方法都是类方法,每个类方法完成一个功能;提高代码复用性,调用方便,提高开发效率 |
| 为什么工具类要用类方法而不是实例方法? | 实例方法需要创建对象来调用,会浪费内存 |
| 工具类定义时有什么要求? | 不需要创建对象,建议将构造器私有化 |
5. static 的注意事项
课件给出三条规则:
- 类方法中可以直接访问类的成员,不可以直接访问实例成员。
- 实例方法中既可以直接访问类成员,也可以直接访问实例成员。
- 实例方法中可以出现
this关键字,类方法中不可以出现this关键字。
public class Test {
// 类变量
public static String schoolName = "深圳黑马";
// 类方法
public static void inAddr() {
System.out.println("我们都在深圳黑马学Java~~");
}
// 实例变量
private String name;
// 实例方法
public void printInfo() {
System.out.println("名字叫:" + name);
}
// 1、类方法中可以直接访问类的成员,不可以直接访问实例成员
public static void testStatic() {
System.out.println(schoolName);
inAddr();
// System.out.println(this); // 报错:类方法中不能出现 this
// System.out.println(name); // 报错:不能直接访问实例变量
// printInfo(); // 报错:不能直接访问实例方法
}
// 2、实例方法中两种成员都可以直接访问;3、实例方法中可以出现 this
public void testNoStatic() {
System.out.println(schoolName);
inAddr();
System.out.println(this);
System.out.println(name);
printInfo();
}
}
记忆方式:类方法没有”当前对象”,this 无处指向,所以凡是要靠对象才能确定的东西(实例变量、实例方法、this)都不能在类方法里直接写。
6. 代码块:类的初始化与对象的初始化
6.1 代码块概述
代码块是类的 5 大成分之一(成员变量、构造器、方法、代码块、内部类)。 代码块分为两种:
| 类型 | 格式 | 特点 | 作用 |
|---|---|---|---|
| 静态代码块 | static { } | 类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次 | 完成类的初始化,例如对类变量的初始化赋值 |
| 实例代码块 | { } | 每次创建对象时都会执行实例代码块,并且在构造器前执行 | 和构造器一样,都是用来完成对象的初始化的,例如对实例变量进行初始化赋值 |
补充说明(措辞校正):课件说法”类加载时自动执行”;更准确的表述是类初始化时自动执行,且只执行一次——类加载之后紧接着做初始化,静态代码块与类变量的赋值都发生在这一步。
6.2 静态代码块:初始化类变量
public class CodeTest1 {
public static String schoolName = "深圳黑马";
public static ArrayList<String> names = new ArrayList<>();
// 静态代码块:与类一起优先加载,自动执行一次;用于初始化静态变量的数据
static {
System.out.println("==static静态代码块执行了==");
names.add("张麻子");
names.add("叶飞");
names.add("林楚漩");
}
public static void main(String[] args) {
System.out.println("==main方法执行了==");
System.out.println(names);
}
}
输出顺序说明:静态代码块在 main 方法之前执行——JVM 要执行 main 必须先初始化 CodeTest1 这个类,初始化时就跑完了静态代码块,所以 names 在 main 里已经有数据。
6.3 实例代码块:初始化实例变量
public class CodeTest2 {
private String name;
private ArrayList<String> names = new ArrayList<>();
// 实例代码块:属于每个对象,每次创建对象都执行一次,且先于构造器执行
{
System.out.println("===实例代码块执行了一次====");
names.add("东南");
names.add("西北");
}
public CodeTest2() {
System.out.println("======构造器执行了一次======");
}
public static void main(String[] args) {
new CodeTest2();
new CodeTest2();
new CodeTest2();
}
}
输出顺序说明:每 new 一次依次输出”实例代码块执行了一次 → 构造器执行了一次”,共三轮。对象创建顺序:实例代码块 → 构造器(若有继承关系,父类构造器还会先于子类构造器,见第 12 节)。
7. 单例设计模式
7.1 什么是设计模式
- 一个问题通常有 n 种解法,其中肯定有一种解法是最优的,这个最优的解法被人总结出来,称为设计模式(Design pattern)。
- 设计模式有 20 多种,对应 20 多种软件开发中会遇到的问题。
- 学习设计模式主要学两点:解决什么问题(01)、怎么写(02)。
单例设计模式解决的问题:确保一个类只有一个对象。
7.2 单例的三步写法与饿汉式单例
写法三步:
- 把类的构造器私有。
- 定义一个类变量记住类的一个对象。
- 定义一个类方法返回对象。
饿汉式单例:拿对象时,对象早就创建好了。
// 单例类
public class A {
// 2、定义一个类变量记住类的一个对象
private static A a = new A();
// 1、私有构造器
private A() {
}
// 3、定义一个类方法返回对象
public static A getObject() {
return a;
}
}
public class Test {
public static void main(String[] args) {
// 目标:实现单例设计模式
A a1 = A.getObject();
A a2 = A.getObject();
System.out.println(a1); // 两次打印的地址相同
System.out.println(a2);
}
}
饿汉式的应用场景(课件举例):任务管理器对象、获取运行时对象(Runtime)。在这些场景下使用单例模式,可以避免浪费内存。
7.3 懒汉式单例
懒汉式单例:拿对象时,才开始创建对象(延迟加载对象)。
写法:把构造器私有;定义一个类变量用于存储对象;提供一个类方法,保证返回的是同一个对象。
public class B {
// 2、定义一个类变量用于存储对象
private static B b; // 初始为 null
// 1、单例必须私有构造器
private B() {
}
// 3、提供一个类方法返回类的一个对象
public static B getObject() {
// 第一次来拿对象时才创建,之后不再创建
if (b == null) {
b = new B();
}
return b;
}
}
注意”保证返回的是同一个对象”这一步:如果忘了 if (b == null) 判断,每次调用都会 new 一个新对象,单例就失效了。
7.4 小结
| 问题 | 回答 |
|---|---|
| 什么是设计模式,设计模式主要学什么? | 设计模式就是具体问题的最优解决方案;学”解决什么问题”和”怎么写” |
| 单例模式解决了什么问题? | 确保一个类只有一个对象 |
| 单例怎么写? | 把类的构造器私有;定义一个类变量存储类的一个对象;提供一个类方法返回对象 |
| 饿汉式单例的特点是什么? | 在获取类的对象时,对象已经创建好了 |
| 懒汉单例模式的特点是什么? | 要用类的对象时才创建对象(延迟加载) |
| 单例有啥应用场景,有啥好处? | 任务管理器对象、获取运行时对象;可避免浪费内存 |
8. 面向对象三大特征之二:继承
8.1 继承的快速入门
Java 中提供了关键字 extends,用这个关键字,可以让一个类和另一个类建立起父子关系。
public class B extends A {
}
- 继承的特点:子类能继承父类的非私有成员(成员变量、成员方法)。
- 继承后对象的创建:子类的对象是由子类、父类共同完成的——用子父类这”多张设计图”一起创建对象;对象能直接访问什么成员,也是由这多张设计图共同决定的。
public class A {
public int i;
public void print1() {
System.out.println("===print1===");
}
private int j;
private void print2() {
System.out.println("===print2===");
}
}
public class B extends A {
private int k;
public void print3() {
// 子类可以继承父类的非私有成员
System.out.println(i);
print1();
// System.out.println(j); // 报错
// print2(); // 报错
}
}
public class Test {
public static void main(String[] args) {
// 目标:认识继承、掌握继承的特点
B b = new B();
System.out.println(b.i);
// System.out.println(b.j); // 报错
// System.out.println(b.k); // 报错
b.print1();
// b.print2(); // 报错
b.print3();
}
}
课件内存图:方法区中 A.class(含 public int i;、public void print1()…、private int j;、private void print2()…)与 B.class(含 private int k;、print3())共同决定 B 对象的结构;堆内存中的对象同时拥有来自父类的 i 与子类的 k。
小结:
- 什么是继承?继承就是用
extends关键字,让一个类和另一个类建立起父子关系;子类可以继承父类非私有的成员。 - 带继承关系的类,Java 会怎么创建它的对象?——会用类和其父类这多张设计图一起创建对象;对象能直接访问哪些成员,由这多张设计图对外暴露的成员共同决定。
8.2 继承的好处与应用场景
需求(黑马的员工管理系统):需要处理讲师、咨询师的数据。
- 讲师的数据有:姓名、具备的技能。
- 咨询师的数据有:姓名、解答问题的总人数。
不用继承时,两个类中都要写一遍 name 及其 get/set(重复代码):
public class Teacher {
private String name;
private String skill; // 技能
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSkill() { return skill; }
public void setSkill(String skill) { this.skill = skill; }
}
public class Consultant {
private String name;
private int number; // 解答问题的人数
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getNumber() { return number; }
public void setNumber(int number) { this.number = number; }
}
向上抽取父类 People 之后,重复代码只剩一份:
public class People {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
public class Teacher extends People {
private String skill; // 技能
public String getSkill() { return skill; }
public void setSkill(String skill) { this.skill = skill; }
}
public class Consultant extends People {
private int number; // 解答问题的总人数
public int getNumber() { return number; }
public void setNumber(int number) { this.number = number; }
}
使用继承的好处:减少了重复代码的编写,提高了代码的复用性。抽取原则就是”把多个类中相同的属性、行为放到父类里”,课件把这个动作形象地称为”认爸爸”。
9. 权限修饰符
9.1 概念
权限修饰符是用来限制类中的成员(成员变量、成员方法、构造器、代码块…)能够被访问的范围。
9.2 四种权限的作用范围
| 修饰符 | 访问范围 |
|---|---|
private | 只能本类 |
| 缺省(不写) | 本类、同一个包中的类 |
protected | 本类、同一个包中的类、不同包的子孙类中 |
public | 任意位置 |
可见范围从小到大:private < 缺省 < protected < public。
(说明:课件第 43 页的表格在 PPT 中以图形方式呈现,上表依据第 42 页的文字说明与工程中的验证代码整理。)
9.3 工程验证
public class Fu {
// 1、private 私有的,只能在本类中访问
private void privateMethod() {
System.out.println("private");
}
// 2、缺省:只能在本类、同一个包下的其他类中访问
void method() {
System.out.println("缺省");
}
// 3、protected 子类权限:本类、同一个包下的类、子孙类中访问
protected void protectedMethod() {
System.out.println("protected");
}
// 4、public 公开权限:任意类中访问
public void publicMethod() {
System.out.println("public");
}
}
// 同一个包中的其他类:private 不可见,其余三个可见
public class Demo1 {
public static void main(String[] args) {
Fu f = new Fu();
// f.privateMethod(); // 报错
f.method();
f.protectedMethod();
f.publicMethod();
}
}
// 不同包中的类:只有 public 可见
public class Demo2 {
public static void main(String[] args) {
Fu f = new Fu();
// f.privateMethod(); // 报错
// f.method(); // 报错(跨包,缺省不可见)
// f.protectedMethod(); // 报错(不是子孙类)
f.publicMethod();
}
}
// 不同包中的子类:protected 与 public 可见(protected 通过继承访问)
public class Zi extends Fu {
public void print() {
// privateMethod(); // 报错
// method(); // 报错
protectedMethod();
publicMethod();
}
}
10. 单继承与 Object 类
- Java 是单继承的:一个类只能继承一个直接父类,Java 中的类不支持多继承。
- 支持多层继承:
class M {}→class N extends M {}→class P extends N {}是合法的,P间接继承了M的成员。 - Object 类是 Java 中所有类的祖宗类:我们写的任何一个类,其实都是
Object的子类或子孙类。
为什么不支持多继承?课件用”反证法”说明:
public class A {
public void method() {
System.out.println("复习数学~");
}
}
public class B {
public void method() {
System.out.println("复习语文~");
}
}
假设可以写 public class C extends A, B {},那么
public class Test {
public static void main(String[] args) {
C c = new C();
c.method(); // 懵了,不知道听哪个爸爸的
}
}
A 和 B 都有 method(),C 到底继承哪一个无法确定(方法冲突),因此 Java 中的类不支持多继承。
// 1、Java 是单继承的:一个类只能继承一个直接父类
//class A {}
//class B {}
//class C extends A, B {} // 报错:不支持多继承
// 2、Java 支持多层继承
class M {}
class N extends M {}
class P extends N {}
// 3、Object 类是 Java 中所有类的祖宗类
11. 方法重写
11.1 什么是方法重写
当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后方法的访问,Java 会遵循就近原则(调用的是子类重写后的版本)。
public class Animal {
public void run() {
System.out.println("动物会跑~~~~");
}
}
public class Tiger extends Animal {
// 方法重写:名称和参数列表必须与父类被重写的方法一样
@Override // 重写的校验注解:安全、可读性好、优雅
public void run() {
System.out.println("老虎跑的贼快~~~~");
}
}
11.2 方法重写的注意事项
| 注意事项 | 说明 |
|---|---|
加 @Override 注解 | 可以指定 Java 编译器检查我们方法重写的格式是否正确,代码可读性也更好 |
| 访问权限 | 子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限(public > protected > 缺省) |
| 返回值类型 | 必须与被重写方法的返回值类型一样,或者范围更小 |
| 不能重写的方法 | 私有方法、静态方法不能被重写,如果重写会报错 |
11.3 应用场景:重写 Object 的 toString()
子类重写 Object 类的 toString() 方法,以便返回对象的内容(而不是默认的”地址值”形式)。
public class Student {
private String name;
private char sex;
private double height;
private String desc;
// 构造器与 get/set 方法省略
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex=" + sex +
", height=" + height +
", desc='" + desc + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student("赵敏", '女', 169.5, "执着,真诚,没有私心");
// 输出对象,默认调用继承自父类 Object 的 toString(),返回所谓的地址值
System.out.println(s); // 重写后打印对象内容
}
}
小结:
- 方法重写是什么?——子类写了一个方法名称、形参列表与父类某个方法一样的方法去覆盖父类的该方法。
- 有哪些注意事项?——建议加
@Override;访问权限必须大于或等于父类被重写方法的权限;返回值类型一样或范围更小;私有方法、静态方法不能被重写。 - 应用场景?——当子类觉得父类的方法不好用、或者不满足需求时。
12. 子类中访问其他成员的特点:就近原则
在子类方法中访问其他成员(成员变量、成员方法),依照就近原则:
- 先子类局部范围找。
- 然后子类成员范围找。
- 然后父类成员范围找;如果父类范围还没有找到则报错。
如果子父类中出现了重名的成员,会优先使用子类的;此时如果一定要在子类中使用父类的,可以通过 super 关键字指定:super.父类成员变量 / super.父类成员方法。
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
zi.showName();
}
}
class Zi extends Fu {
String name = "子类名称";
public void showName() {
String name = "局部名称";
System.out.println(name); // 局部名称(就近原则,先找局部范围)
System.out.println(this.name); // 子类名称(访问子类的成员 name)
System.out.println(super.name); // 父类名称(访问父类的成员 name)
}
}
class Fu {
String name = "父类名称";
}
方法与成员变量同理:重写后调用 run() 走子类版本,super.run() 走父类版本。
class Zi2 extends Fu2 {
@Override
public void run() {
System.out.println("子类跑的贼溜~~");
}
public void go() {
run(); // 子类的 run()
super.run(); // 父类的 run
}
}
class Fu2 {
public void run() {
System.out.println("父类跑");
}
}
13. 子类构造器的特点与 this/super
13.1 子类构造器的特点
- 子类的全部构造器,都会先调用父类的构造器,再执行自己。
- 子类构造器是如何实现调用父类构造器的:
- 默认情况下,子类全部构造器的第一行代码都是
super()(写不写都有),它会调用父类的无参数构造器。 - 如果父类没有无参数构造器,则我们必须在子类构造器的第一行手写
super(...),指定去调用父类的有参数构造器。
- 默认情况下,子类全部构造器的第一行代码都是
public class Animal {
public Animal() {
System.out.println("===父类Animal的无参数构造器被执行了===");
}
public Animal(String n) {
System.out.println("===父类Animal的有参数构造器被执行了===" + n);
}
}
public class Wolf extends Animal {
public Wolf() {
// super(); // 写不写都有,调用父类无参数构造器
System.out.println("===子类Wolf的无参数构造器执行了====");
}
public Wolf(String n) {
// super(); // 写不写都有,调用父类无参数构造器
System.out.println("===子类Wolf的有参数构造器执行了====");
}
}
运行 new Wolf() 与 new Wolf("七匹狼"),都会先打印父类构造器的输出,再打印子类构造器的输出。
13.2 常见应用场景:用 super(...) 初始化父类部分的数据
子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先初始化赋值,再回来把对象里包含子类这部分的数据也进行初始化赋值。
class People {
private String name;
private int age;
public People() {
}
public People(String name, int age) {
this.name = name;
this.age = age;
}
// get + set 方法...
}
class Teacher extends People {
private String skill;
public Teacher() {
}
public Teacher(String name, int age, String skill) {
super(name, age); // 初始化继承自父类部分的数据
this.skill = skill;
}
// get + set 方法...
}
public class Test2 {
public static void main(String[] args) {
// 目标:子类调用父类构造器的常见应用场景
Teacher t = new Teacher("李四", 36, "Java");
System.out.println(t.getName()); // 李四
System.out.println(t.getAge()); // 36
System.out.println(t.getSkill()); // Java
}
}
内存图中的顺序:new Teacher(...) 时,堆中对象先包含父类部分(name、age)再包含子类部分(skill);super(name, age) 先把父类部分赋值,this.skill = skill 再赋值子类部分,因此三个 get 都能取到值。
13.3 this(...) 调用兄弟构造器
补充知识:任意类的构造器中,是可以通过 this(...) 去调用该类的其他构造器的。
public class Student {
private String name;
private int age;
private String schoolName;
public Student() {
}
public Student(String name, int age) {
// this 调用兄弟构造器:不给学校时默认"黑马程序员"
this(name, age, "黑马程序员");
}
public Student(String name, int age, String schoolName) {
this.name = name;
this.age = age;
this.schoolName = schoolName;
}
// get + set 方法...
}
课件示例中等价于:
public Student(String name) {
this(name, "黑马程序员");
}
13.4 this(...) 和 super(...) 的使用注意事项
this(...)、super(...)都只能放在构造器的第一行,因此,有了this(...)就不能再写super(...)了,反之亦然(二者互斥)。super(...)必须放在构造器的第一行,否则报错。
this 与 super 的作用对照(依据课件第 66 页的表格整理):
| 关键字 | 含义 | 访问本类成员 | 访问父类成员 | 构造器用法 |
|---|---|---|---|---|
this | 代表本类对象的引用 | this.本类成员变量 / this.本类成员方法() | — | this(...) 调用本类其他构造器 |
super | 代表父类存储空间的标识 | — | super.父类成员变量 / super.父类成员方法() | super(...) 调用父类构造器 |
(课件提示:以上总结中,唯独”this 调用本类其他构造器”是前面没有接触过的用法。)
13.5 为什么子类构造器一定要先调用父类的构造器
引入继承后,子类对象是由子类和父类这多张设计图共同创建出来的。当创建子类对象、调用子类构造器为子类的成员变量初始化时,子类构造器肯定需要先调用父类构造器,把父类中的成员变量先进行初始化,否则父类那部分数据就是”没准备好的”。
小结:
- 子类构造器有啥特点?——子类中的全部构造器,都必须先调用父类的构造器,再执行自己。
super(...)调用父类有参数构造器的常见应用场景?——为对象中包含父类这部分的成员变量进行赋值。this(...)的作用?——在构造器中调用本类的其他构造器。- 使用注意?——
this(...)与super(...)都必须放在构造器的第一行。
课件最后一页的叮嘱:千里之行、始于足下;好记性不如烂笔头;写代码、写代码、写代码。
14. 作业与练习
作业文档:day01-面向对象进阶(一)/day01作业/day01作业.md
重点知识(整理笔记 + 演讲)
static可以用来修饰什么?各自有何特点和应用场景?static注意事项有哪些?- 单例设计模式是什么意思?什么是懒汉式?什么是饿汉式?
- 代码块有几种?分别有何特点和作用?
- 继承是什么?为什么要在程序中使用继承?
- 方法重写和方法重载是什么样的?
- 子类构造器有什么特点?
this和super有哪些作用?
重点代码或案例(上机操作)
- 系统启动后,要求用户类可以记住自己创建了多少个用户对象(对应第 2.4 节)。
- 定义一个工具类,拥有生成指定位数验证码和获取 2 个整数最大值的功能(对应第 4 节)。
- 使用单例设计模式设计
Student类,保证该类只有一个对象产生(对应第 7 节)。 - 在黑马的员工管理系统中,需要处理讲师、咨询师的数据:讲师的数据有姓名、具备的技能;咨询师的数据有姓名、解答问题的总人数(对应第 8.2 节)。
代码题
- 定义项目经理类(姓名、工号、工资、奖金,行为
work)与程序员类(姓名、工号、工资,行为work),向上抽取一个父类,让这两个类都继承该父类,共有的属性写在父类中,子类重写父类中的方法,并编写测试类完成测试。 - 使用继承定义
Dog(姓名、颜色、价格;吃饭、看家)与Cat(姓名、颜色、价格;吃饭、抓老鼠),把相同内容抽取到父类Animal中;在测试类中创建对象、赋值并调用各自方法。 - (拓展选做)定义银行卡的主卡与副卡类:主卡有余额、卡号、户主等属性,有存款和取款方法(取款余额不足要有提示);副卡额外有透支额度属性(可用主卡余额,余额不够可透支),副卡重写取款方法;写测试类分别测试存取款功能并显示余额。
15. 复习要点
- 类变量 vs 实例变量:归属(类 / 对象)、内存份数(一份 / 每对象一份)、访问方式(
类名.推荐 /对象.)、典型场景(共享计数 / 个人属性)。 static三条注意事项:类方法只能直接访问类成员;实例方法两者都能直接访问;this只能出现在实例方法中。- 工具类 = 全是类方法 + 构造器私有化;用它的理由:调用方便、节省内存、提高复用。
- 代码块是类的 5 大成分之一;静态代码块在类初始化时执行一次(初始化类变量),实例代码块在每个对象创建时于构造器之前执行(初始化实例变量)。
- 单例三步:私有构造器 + 类变量存唯一对象 + 类方法返回该对象;饿汉式(提前创建)与懒汉式(延迟创建,注意
if (b == null)判断)。 - 继承用
extends建立父子关系;子类继承父类的非私有成员;子类对象由子父类多张设计图共同创建。 - 权限修饰符范围:
private(本类)< 缺省(同包)<protected(同包 + 异包子孙类)<public(任意)。 - Java 单继承、不支持多继承、支持多层继承;
Object是所有类的祖宗类。 - 方法重写:名称与参数列表相同、
@Override校验、权限不能变小、返回值相同或更小、私有/静态方法不能重写;典型应用是重写toString()。 - 就近原则:局部 → 子类成员 → 父类成员;要访问父类被遮蔽的成员用
super.。 - 子类构造器一定先调父类构造器:默认
super()(写不写都有),父类无无参构造器必须手写super(...);this(...)调用本类其他构造器,二者都必须在第一行且互斥。