Java · 第 6 章

第 6 章 类与对象

类与对象的关系和内存示意;构造方法(写了有参构造后默认无参消失的坑);this 三种用法;static 变量与方法;四种访问修饰符可见性表;封装与 getter/setter、toString;综合示例:学生成绩对象数组。

6.1 类与对象:模板与实例

类是图纸,对象是按图纸造出来的具体东西——这是面向对象最重要的一句话。

// ① 定义类(图纸)
public class Student {
    // 成员变量(属性)—— 描述"有什么"
    String name;
    int age;

    // 成员方法(行为)—— 描述"能做什么"
    void sayHi() {
        System.out.println("大家好,我是" + name + ",今年" + age + "岁");
    }
}

// ② 创建对象(按图纸造东西)
class Test {
    public static void main(String[] args) {
        Student s = new Student();   // new:开辟空间,返回引用
        s.name = "小明";             // 给成员变量赋值
        s.age = 20;
        s.sayHi();                   // 调用方法

        Student t = new Student();   // 再造一个,各自独立
        t.name = "小红";
        t.sayHi();
    }
}
内存里发生了什么
Java 栈与堆的内存分布示意图,栈中存引用、堆中存对象
图 6-1 栈存引用、堆存对象:s 里放的是对象在堆中的地址
  • s 里存的不是对象本身,而是对象在堆里的地址(引用)。
  • Student a = s; 后 a 和 s 指向同一对象,改 a.name 会影响 s.name。
易错
  • 成员变量有默认值(0 / false / null),但局部变量没有——这是第 2 章的坑,别忘了。
  • 一个 .java 文件里可以有多个类,但只能有一个 public 类,且文件名要与它同名。
课堂练习(3 题)
  1. 类和对象的关系怎么一句话说清? 答:类是对象的模板/图纸,对象是类的一个具体实例;一个类可以 new 出任意多个对象,彼此数据独立。
  2. 写出创建 Student 对象并调用 sayHi 的两行代码。 答:Student s = new Student(); s.sayHi();
  3. 成员变量不赋值就使用,会怎样? 答:不会报错,取默认值(int→0、double→0.0、boolean→false、引用类型→null)。

6.2 构造方法:创建对象时就初始化

构造方法是 new 的时候自动调用的那段代码。

public class Student {
    String name;
    int age;

    // 构造方法:① 与类同名 ② 不写返回值类型(连 void 都不写)
    public Student(String name, int age) {
        this.name = name;      // this.name 指成员变量,右边的 name 是参数
        this.age = age;
    }

    // 无参构造(重载)
    public Student() {
        this.name = "未命名";
        this.age = 18;
    }
}

Student a = new Student("小明", 20);   // 调有参构造
Student b = new Student();             // 调无参构造
易错(几乎必考)
  • 只要写了任意构造方法,系统就不再赠送默认无参构造。上面的例子如果只写有参构造,new Student() 会编译报错。要保留无参构造就显式写出来。
  • 构造方法不能写返回值类型,写了就变成普通方法(且名字与类同名的普通方法是合法的,但会让人困惑)。
  • this(...) 可以在构造里调用另一个构造,但必须放在第一行。
课堂练习(2 题)
  1. 类里已经写了 Student(String n),还能 new Student() 吗? 答:不能。只要定义了构造方法,默认无参构造就不再自动生成,需要自己补一个 public Student() {}。
  2. 构造方法和普通方法在写法上最明显的两个区别? 答:① 名称必须与类名相同;② 不写返回值类型(连 void 都不能写)。

6.3 this 与 static

this 指"当前对象",static 指"属于类、不属于某个对象"。

this 的三种用法
  • this.成员:区分同名的成员变量和参数(最常见)。
  • this.方法():调用本对象的其他方法(通常省略)。
  • this(...):构造里调用另一个构造(必须第一行)。
static(静态)
public class Student {
    String name;              // 实例变量:每个对象一份
    static int count = 0;     // 静态变量:全类共享一份,所有对象共用

    public Student(String name) {
        this.name = name;
        count++;                    // 每造一个对象就 +1
    }

    static int getCount() {         // 静态方法
        return count;               // 只能访问静态成员
        // return name;  ← 错!静态方法里不能直接访问非静态成员
    }
}

Student.getCount();       // 静态方法用「类名.方法」调用,不用 new
  • 静态成员属于类,随类加载而存在,被所有对象共享;实例成员属于对象。
  • 静态方法里不能用 this,也不能直接访问非静态成员(因为不需要对象就能调用)。
  • 常见用途:计数器、工具类方法(Math.max、Arrays.sort 都是静态方法)。
易错
  • 在 static 方法(比如 main)里直接调用本类的非静态成员 → 编译报错,必须先 new 出对象。
  • 把"所有对象共享的数据"误写成实例变量,结果每个对象都从 0 开始。
课堂练习(3 题)
  1. static 变量和实例变量在内存上的区别? 答:static 变量在类装载时分配、只有一份、被所有对象共享;实例变量每个对象各一份。
  2. 为什么 main 方法必须是 static 的? 答:程序启动时还没有任何对象,JVM 需要直接用「类名.main()」调用,所以 main 必须是静态的。
  3. 静态方法里能不能用 this?为什么? 答:不能。this 代表当前对象,而静态方法可以在没有对象的情况下被调用,没有"当前对象"可言。

6.4 封装与访问修饰符

封装 = 把数据藏起来,只留受控的入口——这是面向对象三大特性之一。

public class Student {
    private String name;      // private:只能在本类里访问
    private int age;

    // getter / setter:受控的读写入口
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {     // ← 校验!这就是封装的价值
            this.age = age;
        } else {
            System.out.println("年龄不合法");
        }
    }

    // 重写 toString:打印对象时自动调用
    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}
修饰符同类同包子类(不同包)其他包
public✓✓✓✓
protected✓✓✓✗
默认(不写)✓✓✗✗
private✓✗✗✗
为什么要封装
  • 能拦住非法数据:如上例,年龄 −5 会被 setter 拒绝;直接 s.age = -5 就拦不住。
  • 内部实现可改:字段改名或换存储方式,只要 getter/setter 签名不变,外部代码不用动。
易错
  • 直接打印对象默认输出地址,重写 toString() 才能看到内容(很多 IDE 能自动生成)。
  • boolean 的 getter 通常写成 isXxx() 而不是 getXxx()。
课堂练习(3 题)
  1. private 成员怎么在别的类里读写? 答:通过 public 的 getter/setter 方法访问,不能直接 对象.字段。
  2. 四种访问修饰符中,范围最大和最小的分别是? 答:最大 public(到处可访问),最小 private(仅本类)。
  3. 直接 System.out.println(student) 打印出地址,怎么办? 答:在类里重写 toString() 方法返回想要的字符串,println 会自动调用它。

6.5 综合示例:学生成绩管理(对象数组)

把类、构造、封装、toString 串起来,这是课内实验的典型题。

import java.util.Arrays;

public class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        setScore(score);              // 复用 setter 里的校验
    }
    public String getName() { return name; }
    public int getScore() { return score; }
    public void setScore(int score) {
        this.score = (score >= 0 && score <= 100) ? score : 0;
    }
    @Override
    public String toString() {
        return name + ":" + score + " 分";
    }

    public static void main(String[] args) {
        // 对象数组:一格装一个对象
        Student[] cls = {
            new Student("小明", 85),
            new Student("小红", 92),
            new Student("小刚", 58)
        };

        for (Student s : cls) {
            System.out.println(s);          // 自动调 toString
        }

        // 找出最高分的学生
        Student best = cls[0];
        for (Student s : cls) {
            if (s.getScore() > best.getScore()) best = s;
        }
        System.out.println("最高分:" + best);
    }
}
这段代码体现的三件事
  • 对象数组的每个元素都是引用:Student[] cls 里存的是地址,new Student(...) 出来的对象在堆上。
  • 构造里复用 setter:校验逻辑只写一遍。
  • 重写 toString 让打印可读:System.out.println(s) 直接输出"小明:85 分"。

复制到 Java 在线运行环境 跑一遍,再加一个"求平均分"的静态方法试试。

课堂练习(2 题)
  1. 对象数组 Student[] cls = new Student[3]; 创建后,数组元素是什么?能直接用吗? 答:元素默认是 null(引用类型默认值),直接用会抛 NullPointerException;必须逐个 cls[i] = new Student(...) 后才能用。
  2. 给上面程序加一个静态方法 static double average(Student[] cls),大致怎么写? 答:遍历数组累加 s.getScore(),最后返回 sum * 1.0 / cls.length;主方法里用 Student.average(cls) 调用。