1. 文章
  2. Java 进阶线
  3. 对象与类型设计

static、初始化与继承:对象如何共享数据和复用行为

更新于 24 分钟阅读 第 1 / 15 站 #java#java-advanced#day01

理解类成员与实例成员,掌握单例、构造器链和继承的使用边界。

我的学习 →

Java 进阶线 · 线路目录
本页导读

day01|面向对象进阶(一)

本日进入面向对象的高级部分:先用 static 解决”数据要在所有对象之间共享”的问题,再通过代码块、单例设计模式理解类的初始化与对象的唯一性,最后进入面向对象的第二大特征——继承,掌握继承的特点、权限修饰符、方法重写、就近原则以及子类构造器与 this/super 的配合方式。 学完之后应当能够:判断一个成员该定义成类成员还是实例成员;写出饿汉式与懒汉式单例;熟练使用 extends 抽取父类、用 @Override 重写方法,并解释”子类构造器为什么一定先调用父类构造器”。

1. 本日范围:三个问题与知识地图

1.1 课程要回答的三个问题

课件用三个真实存在的问题引出本日全部内容:

  1. 之前定义的成员变量 name、score 属于每个对象,每个对象中都有一份该数据。有些数据是要被所有对象共享的,应该怎么定义? → static 修饰成员变量(类变量)。
  2. 如何让一个类对外只能产生一个对象(例如任务管理器对象)? → 单例设计模式。
  3. 系统中定义了很多实体类,它们中有很多属性、行为存在重复代码,如何降低代码冗余、提升代码复用? → 继承。

1.2 知识地图

课件目录给出的顺序如下(本文的章节顺序与之一致):

序号知识块本文章节
1static 修饰成员变量第 2 节
2static 修饰成员变量的应用场景第 2.4 节
3static 修饰成员方法第 3 节
4static 修饰成员方法的应用场景第 3.4 节
5static 的注意事项第 4 节
6static 的应用知识:代码块第 5 节
7static 的应用知识:单例设计模式第 6 节
8面向对象三大特征之二:继承第 7–12 节

2. static 修饰成员变量:类变量

2.1 成员变量的两种类型

static 叫静态,可以修饰成员变量、成员方法。 成员变量按照有无 static 修饰分为两种:

  • 类变量(静态成员变量):有 static 修饰,属于类,在计算机里只有一份,会被类的全部对象共享。
  • 实例变量(对象的变量):无 static 修饰,属于每个对象,每个对象中都有一份。

访问方式:

  • 类名.类变量(推荐)
  • 对象.类变量(不推荐)
  • 对象.实例变量(实例变量只能用对象访问)
public class Student {
    // 类变量
    static String name;

    // 实例变量(对象变量)
    int age;
}

Student 学生表示意,static name 属于类,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 各自独立。

s1 对象内存示意(实例变量 age 独立一份)

s2 对象内存示意(实例变量 age 独立一份)

小结(课件原文):

  • 类变量:属于类,与类一起加载一次,在内存中只有一份,可以被类和类的所有对象共享。
  • 实例变量:属于对象,每个对象中都有一份,只能用对象访问。

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 小结与追问

  1. 成员变量有几种?各自在什么情况下定义?
    • 类变量:数据只需要一份,且需要被共享(访问、修改)时。
    • 实例变量:每个对象都要有一份、数据各不同(如 name、score、age)。
  2. 访问自己类中的类变量,是否可以省略类名不写?可以。 注意:在某个类中访问其他类里的类变量,必须带类名访问。

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,调用类方法时不需要对象参与。

类方法 printHelloWorld() 的输出结果

实例方法 printPass() 的输出结果(成绩:不及格)

小结:

  1. static 修饰的成员方法叫类方法(静态方法),属于类,可以直接用类名访问,也可以用对象访问。
  2. 无 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 的注意事项

课件给出三条规则:

  1. 类方法中可以直接访问类的成员,不可以直接访问实例成员。
  2. 实例方法中既可以直接访问类成员,也可以直接访问实例成员。
  3. 实例方法中可以出现 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 单例的三步写法与饿汉式单例

写法三步:

  1. 把类的构造器私有。
  2. 定义一个类变量记住类的一个对象。
  3. 定义一个类方法返回对象。

饿汉式单例:拿对象时,对象早就创建好了。

// 单例类
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。

小结:

  1. 什么是继承?继承就是用 extends 关键字,让一个类和另一个类建立起父子关系;子类可以继承父类非私有的成员。
  2. 带继承关系的类,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 类

  1. Java 是单继承的:一个类只能继承一个直接父类,Java 中的类不支持多继承。
  2. 支持多层继承:class M {} → class N extends M {} → class P extends N {} 是合法的,P 间接继承了 M 的成员。
  3. 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);   // 重写后打印对象内容
    }
}

小结:

  1. 方法重写是什么?——子类写了一个方法名称、形参列表与父类某个方法一样的方法去覆盖父类的该方法。
  2. 有哪些注意事项?——建议加 @Override;访问权限必须大于或等于父类被重写方法的权限;返回值类型一样或范围更小;私有方法、静态方法不能被重写。
  3. 应用场景?——当子类觉得父类的方法不好用、或者不满足需求时。

12. 子类中访问其他成员的特点:就近原则

在子类方法中访问其他成员(成员变量、成员方法),依照就近原则:

  1. 先子类局部范围找。
  2. 然后子类成员范围找。
  3. 然后父类成员范围找;如果父类范围还没有找到则报错。

如果子父类中出现了重名的成员,会优先使用子类的;此时如果一定要在子类中使用父类的,可以通过 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() 走父类版本。

IDEA 中 F.java 与 Z.java,演示子类访问重名成员时的就近原则

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 子类构造器的特点

  1. 子类的全部构造器,都会先调用父类的构造器,再执行自己。
  2. 子类构造器是如何实现调用父类构造器的:
    • 默认情况下,子类全部构造器的第一行代码都是 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 为什么子类构造器一定要先调用父类的构造器

引入继承后,子类对象是由子类和父类这多张设计图共同创建出来的。当创建子类对象、调用子类构造器为子类的成员变量初始化时,子类构造器肯定需要先调用父类构造器,把父类中的成员变量先进行初始化,否则父类那部分数据就是”没准备好的”。

小结:

  1. 子类构造器有啥特点?——子类中的全部构造器,都必须先调用父类的构造器,再执行自己。
  2. super(...) 调用父类有参数构造器的常见应用场景?——为对象中包含父类这部分的成员变量进行赋值。
  3. this(...) 的作用?——在构造器中调用本类的其他构造器。
  4. 使用注意?——this(...) 与 super(...) 都必须放在构造器的第一行。

课件最后一页的叮嘱:千里之行、始于足下;好记性不如烂笔头;写代码、写代码、写代码。

14. 作业与练习

作业文档:day01-面向对象进阶(一)/day01作业/day01作业.md

重点知识(整理笔记 + 演讲)

  1. static 可以用来修饰什么?各自有何特点和应用场景?
  2. static 注意事项有哪些?
  3. 单例设计模式是什么意思?什么是懒汉式?什么是饿汉式?
  4. 代码块有几种?分别有何特点和作用?
  5. 继承是什么?为什么要在程序中使用继承?
  6. 方法重写和方法重载是什么样的?
  7. 子类构造器有什么特点?
  8. this 和 super 有哪些作用?

重点代码或案例(上机操作)

  1. 系统启动后,要求用户类可以记住自己创建了多少个用户对象(对应第 2.4 节)。
  2. 定义一个工具类,拥有生成指定位数验证码和获取 2 个整数最大值的功能(对应第 4 节)。
  3. 使用单例设计模式设计 Student 类,保证该类只有一个对象产生(对应第 7 节)。
  4. 在黑马的员工管理系统中,需要处理讲师、咨询师的数据:讲师的数据有姓名、具备的技能;咨询师的数据有姓名、解答问题的总人数(对应第 8.2 节)。

代码题

  1. 定义项目经理类(姓名、工号、工资、奖金,行为 work)与程序员类(姓名、工号、工资,行为 work),向上抽取一个父类,让这两个类都继承该父类,共有的属性写在父类中,子类重写父类中的方法,并编写测试类完成测试。
  2. 使用继承定义 Dog(姓名、颜色、价格;吃饭、看家)与 Cat(姓名、颜色、价格;吃饭、抓老鼠),把相同内容抽取到父类 Animal 中;在测试类中创建对象、赋值并调用各自方法。
  3. (拓展选做)定义银行卡的主卡与副卡类:主卡有余额、卡号、户主等属性,有存款和取款方法(取款余额不足要有提示);副卡额外有透支额度属性(可用主卡余额,余额不够可透支),副卡重写取款方法;写测试类分别测试存取款功能并显示余额。

15. 复习要点

  1. 类变量 vs 实例变量:归属(类 / 对象)、内存份数(一份 / 每对象一份)、访问方式(类名. 推荐 / 对象.)、典型场景(共享计数 / 个人属性)。
  2. static 三条注意事项:类方法只能直接访问类成员;实例方法两者都能直接访问;this 只能出现在实例方法中。
  3. 工具类 = 全是类方法 + 构造器私有化;用它的理由:调用方便、节省内存、提高复用。
  4. 代码块是类的 5 大成分之一;静态代码块在类初始化时执行一次(初始化类变量),实例代码块在每个对象创建时于构造器之前执行(初始化实例变量)。
  5. 单例三步:私有构造器 + 类变量存唯一对象 + 类方法返回该对象;饿汉式(提前创建)与懒汉式(延迟创建,注意 if (b == null) 判断)。
  6. 继承用 extends 建立父子关系;子类继承父类的非私有成员;子类对象由子父类多张设计图共同创建。
  7. 权限修饰符范围:private(本类)< 缺省(同包)< protected(同包 + 异包子孙类)< public(任意)。
  8. Java 单继承、不支持多继承、支持多层继承;Object 是所有类的祖宗类。
  9. 方法重写:名称与参数列表相同、@Override 校验、权限不能变小、返回值相同或更小、私有/静态方法不能重写;典型应用是重写 toString()。
  10. 就近原则:局部 → 子类成员 → 父类成员;要访问父类被遮蔽的成员用 super.。
  11. 子类构造器一定先调父类构造器:默认 super()(写不写都有),父类无无参构造器必须手写 super(...);this(...) 调用本类其他构造器,二者都必须在第一行且互斥。