☕ 第三章 面向对象(上)

3.4 构造方法

📄 一句话概括

构造方法是对象出生时自动执行的特殊方法——负责在 new 的那一刻把属性装上真值。相当于新手机的"开机设置向导",出厂(new)就完成,不用你事后挨个补。


1. 为什么需要构造方法——3.2/3.3 留下的两步麻烦

回顾一下现在造一辆车的流程:

Car car = new Car();        // 出生:属性全是默认值(null、0.0)
car.setName("宝马");         // 第 1 步补:名字
car.setPrice(200000);       // 第 2 步补:价格
⚠️ 问题所在

new Car() 出来的车是一辆"空白车":brand 是 null、price 是 0.0。然后必须记得挨个调 setter——忘了补一行,车就带着脏状态跑。而且这两步谁也拦不住:任何人都可以 new 完不 set,直接 car.run()。

需求很明确:能不能在 new 那一刻直接把值传进去? new Car("宝马", 200000)——有值当场给,没值别出厂。这就是构造方法。


2. 构造方法的写法——三个特征

public class Car {
    private String brand;
    private double price;

    // 这就是构造方法:方法名 = 类名,没有返回值类型
    public Car(String b, double p) {
        brand = b;        // 有 this 更好:this.brand = b;
        price = p;
    }
}

三个特征,一个都不能少(上面那张图的紫色条):

特征 说明 对照普通方法
① 方法名 = 类名 Car 类的构造方法只能叫 Car 普通方法随意起名
② 没有返回值类型 连 void 都不写——写了它就变成普通方法了 普通方法必须有
③ new 时自动执行 你永远不能手动喊 car.Car(...) 普通方法要手动调用
💡 误区预警:构造方法"没有返回值" ≠ "返回值类型是 void"

void 意思是"返回空";构造方法是压根不写返回值这一栏。public void Car() 这行写出来,它就不再是构造方法,只是一个名字叫 Car 的普通方法——这是新手第一大坑,自测题专门考。

调用方式就是 3.2 学的 new:

Car car = new Car("宝马", 200000);   // 参数进构造方法,出生即满配

这一行其实是三步(上面那张图):堆里开格子 → 构造方法自动上岗装值 → 地址返回给 car。以前你以为 new 只是一句话,现在知道它肚子里有个"开机向导"在干活。


3. 默认构造方法——免费送的,但会收回

现在有个问题:3.2 和 3.3 里我们从来没写过构造方法,new Car() 照样能跑——为什么?

💡 Java 的赠送规则

一个类一个构造方法都没写时,Java 编译器免费送一个看不见的空构造方法:

public Car() { }   // 什么也不干,但让你 new Car() 不报错

但是:只要你自己写了任何一个构造方法,免费送的当场收回。类里只剩你写的那一个。

这就是本节最大的坑:

public class Car {
    public Car(String b, double p) { ... }   // 只写了带参的
}

// 测试类里:
Car c1 = new Car("宝马", 200000);   // ✅ 没问题
Car c2 = new Car();                 // ❌ 编译报错!
✅ 报错长相

java: 无法将类 Car中的构造器 Car应用到给定类型 (翻译成人话:你喊的"空白出生"这种开法不存在了)

解法(上面那张图右边):两种都自己写——

public class Car {
    public Car() {                       // 无参构造:空白出生
    }

    public Car(String brand, double price) {   // 带参构造:出生带设定
        this.brand = brand;
        this.price = price;
    }
}

参数不同、方法名相同——这正是方法重载的思想(同名积木按原料自动匹配)。以后写类,无参 + 带参成对写,两种开法都留着,谁也坑不了谁。

❓ 无参构造里可以干活吗?

可以。它也是正经构造方法,new 时一样自动执行。常见用法:给属性装个兜底默认值(比如 brand = "未知品牌";)——保证 new 出来的车再空白,也不是 null。


4. 构造方法 × 封装——安检不能绕过去

3.3 的 setter 安检,构造方法能绕过吗?看这段:

public Car(double p) {
    price = p;    // 直接装值,安检呢?!
}

构造方法是从类内部直接给属性赋值——private 锁的是"类外用点号摸属性",管不了类自己。所以构造方法里的赋值必须自己带安检:

public Car(double p) {
    if (p >= 0) {
        price = p;
    } else {
        System.out.println("价格不能为负,按 0 出厂");
        price = 0;
    }
}

两种思路:

  1. 构造里重复写安检——直观,但规则写了两遍,改一处忘一处就出 bug
  2. 构造方法调用 setter(更地道)——this.setPrice(p);,安检只在 setter 一处,构造方法当个转发门
💡 顺手收网:3.3 埋的那句话

3.3 说过"private 了外面还能 new,锁了个寂寞"。现在补全:private 管"出生后的属性访问"(getter/setter),构造方法管"出生瞬间的状态"——两道闸门配合,对象从头到尾都别想脏。


5. IDEA 快捷生成——构造方法也不用手写

  1. 在类里按 Alt + Insert(或右键 → Generate)
  2. 选 Constructor
  3. 弹窗里按住 Ctrl 点选要进构造的属性(不选直接 OK = 无参构造;全选 = 全参构造)
  4. 回车生成——注意它生成的字段赋值自动带 this,正好复习 3.3

6. 动手部分:给封装版 Car 加上出生设定

第 1 步:打开 3.3 的 Car 类(或新建 Car2),先按 Alt + Insert 生成无参 + 全参两个构造方法。

第 2 步:把全参构造改成"带安检"版(价格安检转发给 setter):

public class Car {
    private String brand;
    private double price;

    public Car() {
        this.brand = "未知品牌";    // 无参构造:装个兜底值
    }

    public Car(String brand, double price) {
        this.brand = brand;
        this.setPrice(price);      // 走 setter,安检只在门里写一遍
    }

    public void setPrice(double price) {
        if (price >= 0) {
            this.price = price;
        } else {
            System.out.println("价格不能为负,赋值失败");
        }
    }

    public double getPrice() {
        return price;
    }

    public String getBrand() {
        return brand;
    }

    public void run() {
        System.out.println(brand + " 在跑,价格 " + price);
    }
}

第 3 步:新建测试类 TestCar2:

public class TestCar2 {
    public static void main(String[] args) {
        Car c1 = new Car();                          // 空白出生 → 兜底值
        c1.run();                                    // 未知品牌 在跑,价格 0.0

        Car c2 = new Car("宝马", 200000);            // 出生带设定
        c2.run();                                    // 宝马 在跑,价格 200000.0

        Car c3 = new Car("废铁", -100);              // 出生也过安检
        c3.run();                                    // 提示 + 废铁 在跑,价格 0.0
    }
}

看到什么算成功:控制台三行——"未知品牌 在跑,价格 0.0"、"宝马 在跑,价格 200000.0"、"价格不能为负,赋值失败" + "废铁 在跑,价格 0.0"。第三行证明出生那一瞬也在过安检。

三个 30 秒实验:

  1. 删掉无参构造,把 new Car() 那行留着——编译报错,亲眼看看"免费送的收回"长什么样(这个报错以后一定自己撞,先看一次)
  2. 在构造方法返回值位置加个 void,再 new 一次——报错变了,说明它已经不是构造方法
  3. 无参构造里打印一句 System.out.println("一辆车出生了");——new 三次打印三句,每 new 一次构造方法就跑一次,现场证明"自动执行"

7. 常见误区

⚠️ 误区 1:构造方法的返回值类型写 void

写了 void 它就不是构造方法了,只是个撞名的普通方法。构造方法的"没有返回值"是整栏不写,不是写 void。特征②里专门划过重点。

⚠️ 误区 2:写了带参构造,new Car() 还能用

不能。免费送的默认构造只在你一个都没写时存在。写了任何一个,它就消失——所以"无参 + 带参成对写"是铁纪律。

⚠️ 误区 3:构造方法可以手动调用

不行。它只能被 new 触发,写 car.Car("宝马", 200000); 是编译错误。想改属性,走 setter——构造方法管出生,setter 管售后,分工不同。

⚠️ 误区 4:构造方法里可以 return 一个值

不能 return 值(连类型都没写),但可以提前结束:return;(不带值)合法,用于参数不合法时直接放弃后续赋值逻辑。

⚠️ 误区 5:构造方法里的赋值不用安检,反正有 setter 兜着

反了——构造方法绕过了 setter(除非你主动调 this.setXxx(...))。从类内部直接 this.price = p; 是不经过任何门的。所以出生时的安检要你在构造里自己安排,见第 4 节。


8. 自测题

❓ Q1:构造方法和普通方法的三个区别是什么?

①名字必须是类名(普通方法随意);②没有返回值类型,连 void 都不写(普通方法必须有);③new 创建对象时自动执行、不能手动调用(普通方法手动喊)。写反任何一个都编译报错或行为异常。

❓ Q2:类里只写了一个带参构造,`new Car()` 会怎样?为什么?

编译报错"无法将构造器应用到给定类型"。因为 Java 只在一个构造方法都没写时才赠送默认无参构造;你写了带参的,赠送的就被收回,"空白出生"这种开法不存在了。解法:自己补一个无参构造。

❓ Q3:`new Car("宝马", 200000);` 这一行,构造方法在什么时刻执行?执行几次?

在 new 的瞬间自动执行,new 一次执行一次。它负责把参数装进新对象的属性,执行完把堆里对象的地址返回给变量 car。每 new 一次都是"开格子 → 构造 → 交地址"完整一轮。

❓ Q4:想让"出生时价格就过安检",代码该怎么安排?

两种:①构造方法里直接写 if 安检(规则写两遍,易漏);②构造方法里调 this.setPrice(price);,安检只在 setter 写一遍(推荐)。因为构造方法的内部赋值绕过了 private 门禁,安检必须自己安排,而转发给 setter 是最省心的安排法。

❓ Q5(挑战):`public void Car() { }` 这行代码编译不报错,但它是构造方法吗?后果是什么?

不是。加了 void 它就是"一个碰巧叫 Car 的普通方法",不参与 new。后果:这个类里实际一个构造方法都没写,Java 会送默认无参构造——程序碰巧能跑,但那不是你写的 Car()。这种"看起来是、其实不是"最迷惑,特征②(返回值整栏不写)必须记死。


9. 下一步学什么


相关笔记