类与对象:从图纸到实例
这是什么:类(Class) = 给「一类东西」做的图纸;对象(Object) = 照着图纸造出来的那个具体实例。
为什么用:学完Java 基础的八个章节,你已经能用变量、循环、方法写小程序了。但要写更大的程序,得靠 Java 的主战场——面向对象:把数据和操作数据的行为打包在一起,程序才不至于乱成一锅粥。
如何用:本章先分清「图纸」和「实例」,再动手定义类、创建对象,最后搞懂「对象变量里存的其实是遥控器」。
类是图纸,对象是实例
- 类(class):一张"图纸",规定了一类东西有哪些属性、哪些行为
- 对象(object):用图纸造出来的一个个具体"实例"
类 Dog 规定狗有名字、年龄,会叫;你家的旺财和邻居的大黄,就是两个对象。一张图纸,造无数条狗。
类和对象的关系,一张表看清:
| 对比项 | 类 | 对象 |
|---|---|---|
| 是什么 | 图纸、模板 | 按图纸造出来的具体个体 |
| 有几份 | 代码里只写一份 | new 一次造一个,想要多少造多少 |
| 举例 | Dog 这段代码本身 |
旺财、大黄 |
| 能不能直接用 | 不能,只是描述 | 能,d1.bark() 让它干活 |
再往深处看一眼:执行 new Dog("旺财", 2) 时,Java 会在一块叫堆内存的区域里开辟空间,把这只狗的名字、年龄都放进去;而变量 d1 里存的并不是狗本身,是一张指向它的"地址条"(术语叫引用)。变量像遥控器,对象像电视机:一台电视可以配好几只遥控器,一只遥控器同一时刻只对一台电视。这个模型先混个脸熟,后面"对象变量存的是遥控器"一节会亲手验证它。
定义类与创建对象
public class Dog {
// 成员变量:每只狗自己的属性
String name;
int age;
// 构造方法:new 的时候自动调用,方法名必须和类名相同
Dog(String name, int age) {
this.name = name; // this.name 是成员变量,name 是参数
this.age = age;
}
// 方法:狗的行为
void bark() {
System.out.println(name + ":汪汪!我 " + age + " 岁了");
}
// 新增:过生日,年龄加 1
void birthday() {
age++;
System.out.println(name + " 过生日啦!现在 " + age + " 岁了");
}
public static void main(String[] args) {
Dog d1 = new Dog("旺财", 2); // new 创建对象
Dog d2 = new Dog("大黄", 3);
d1.bark();
d2.bark();
d1.birthday(); // 旺财 过生日啦!现在 3 岁了
d1.bark(); // 旺财:汪汪!我 3 岁了
}
}
运行后输出:
旺财:汪汪!我 2 岁了
大黄:汪汪!我 3 岁了
旺财 过生日啦!现在 3 岁了
旺财:汪汪!我 3 岁了
两只狗互不打扰:d1.name 是"旺财",d2.name 是"大黄"——同一张图纸,各造各的狗。
成员变量有出厂默认值
值得较真一个细节:如果不在构造方法里赋值,成员变量会是什么值?答案是 Java 会自动发"出厂默认值",不会是乱码,也不会报错。
public class DefaultDemo {
String name; // 引用类型默认 null:啥也不指
int age; // 整数默认 0
boolean hungry; // 布尔默认 false
void show() {
System.out.println("name=" + name);
System.out.println("age=" + age);
System.out.println("hungry=" + hungry);
}
public static void main(String[] args) {
new DefaultDemo().show();
}
}
你会看到:
name=null
age=0
hungry=false
| 成员变量类型 | 出厂默认值 |
|---|---|
int、long 等整数 |
0 |
double、float |
0.0 |
boolean |
false |
char |
空字符 |
String 及一切对象类型 |
null(不指向任何对象) |
注意:这个待遇只有成员变量有,方法里的局部变量没有——局部变量不初始化就用,编译直接报错,下面的"翻车现场"会演示。
构造方法也能重载
一个类可以写好几个构造方法,只要参数列表不同——这叫重载(overload):同名不同参,各干各的活。
public class Dog2 {
String name;
int age;
Dog2() { // 无参构造:造一只"白板狗"
this.name = "无名";
this.age = 1;
}
Dog2(String name, int age) { // 全参构造:造一只信息齐全的狗
this.name = name;
this.age = age;
}
void bark() {
System.out.println(name + ":汪汪!我 " + age + " 岁了");
}
public static void main(String[] args) {
new Dog2().bark();
new Dog2("大黄", 3).bark();
}
}
你会看到:
无名:汪汪!我 1 岁了
大黄:汪汪!我 3 岁了
new 后面括号里传几个参数、什么类型,Java 就自动挑最匹配的那个构造方法——像奶茶店点单:说"随便来一杯"走默认配方,报具体需求就按你的来。
小心一个坑:只要你亲手写了带参构造,Java 就不再白送默认的无参构造,这时
new Dog2()会编译报错——想两种都要,就得像上面这样两个都写。记住口令:"写了一个构造,其余自己管。"
对象变量存的是"遥控器"
回头验证开头的引用模型。先自己猜猜下面代码的输出:
public class AliasDemo {
public static void main(String[] args) {
Dog d1 = new Dog("旺财", 2);
Dog d3 = d1; // 注意:没有 new!d3 和 d1 存的是同一个地址
d3.birthday(); // 通过 d3 过生日
d1.bark(); // 再看 d1——它也"长大"了!
System.out.println(d1 == d3); // true:它俩指向同一个对象
}
}
你会看到:
旺财 过生日啦!现在 3 岁了
旺财:汪汪!我 3 岁了
true
意外吗?通过 d3 改的状态,d1 也看得见——因为 Dog d3 = d1; 只是复制了地址条,堆里始终只有那一只狗,两个变量是同一台电视的两只遥控器。想真造第二条狗,老老实实 new 一遍。
| 写法 | 内存里发生了什么 |
|---|---|
Dog d3 = d1; |
复制引用,一只狗挂两只遥控器 |
Dog d4 = new Dog("旺财", 2); |
新开一块堆内存,真·第二只狗 |
d1 == d3 |
比的是地址 → true |
d1 == d4 |
哪怕名字年龄全一样,地址不同 → false |
== 对对象永远比地址不比内容——想要"内容一样就算相等",得用 equals 方法,这是后面学集合、字符串比较时的高频考点。
逐行解读
| 代码 | 作用 |
|---|---|
String name; int age; |
成员变量:每个对象各自的属性,存在对象内部 |
Dog(String name, int age) |
构造方法:new 时自动执行,负责赋初值;名字和类名相同,不写返回类型 |
this.name = name |
this 指"当前这个对象";左边是成员变量,右边是参数——不加 this 它俩重名分不清 |
void bark() |
方法:对象的行为,可以直接用本对象的成员变量 |
void birthday() |
新增方法:修改对象自身状态(年龄 +1) |
new Dog("旺财", 2) |
new 在内存里造出一个对象,并调用构造方法 |
d1.bark() |
用 . 让某个具体对象干活 |
记忆口诀
类是图纸,对象是实例,new 来造,this 指自己。
❓ 常见问题
Q1:构造方法为什么不写返回类型?
构造方法是特殊方法,名字必须和类完全一样,故意不写返回类型(连 void 都不写),因为它不"返回"任何东西,它的工作是"把新对象初始化好"。
Q2:this 必须写吗?
不一定。如果成员变量和参数不重名,可以省略 this。但写 this.name 能明确区分"成员变量"和"参数",避免歧义,是好习惯。
Q3:一个文件里能定义多个 public class 吗?
不能。一个 .java 文件里只能有一个 public class,且文件名必须和这个 public 类名一致。非 public 的类可以有多个。
Q4:成员变量和局部变量到底差在哪?
名字像、脾气不同,一张表分清楚:
| 对比项 | 成员变量 | 局部变量 |
|---|---|---|
| 写在哪 | 类里面、方法外面 | 方法(或代码块)里面 |
| 住哪 | 对象内部(堆内存) | 方法自己的栈空间 |
| 默认值 | 有(0、false、null) | 没有,不初始化编译报错 |
| 作用范围 | 全类通用,所有方法都能用 | 从定义那一行到所在大括号结束 |
方法里想用成员变量,直接写名字就行;一旦局部变量和成员变量重名,"离得近"的局部变量优先——所以构造方法里才需要 this. 来指明成员变量,这正是 Q2 说的好习惯。
Q5:d1.name 这种"点"出来的写法,什么时候能用?
对象在外部访问成员,都用 对象.成员。不过等下一章学了封装你会看到:正经项目里成员变量都藏成 private,外部只能通过方法访问——"点"出来的自由会收回大半,现在先享受,也要有心理准备。
⚠️ 新手翻车现场
翻车一:忘了 new,直接用类名当对象
// 错误:Dog 是图纸,不是狗本尊
Dog.bark(); // ❌ 编译报错
问题:类是图纸,不能直接调方法;必须先 new 出对象,再用对象调方法。
修正:Dog d1 = new Dog("旺财", 2); d1.bark();
翻车二:构造方法参数个数对不上
Dog d = new Dog("旺财"); // ❌ 构造方法要求 (String name, int age),少传了 age
问题:构造方法定义了两个参数,new 时必须传两个值。
修正:new Dog("旺财", 2),参数个数和类型都要对上。
翻车三:this 漏写导致赋值没生效
Dog(String name, int age) {
name = name; // ❌ 左右都是参数,成员变量 name 没被赋值!
age = age;
}
问题:左右都是局部参数 name、age,成员变量 this.name、this.age 没被赋值,对象的名字和年龄都是默认值。
修正:this.name = name; this.age = age;,用 this 指明是成员变量。
翻车四:局部变量不初始化就用
void test() {
int count;
System.out.println(count); // ❌ 编译报错:可能尚未初始化变量 count
}
问题:局部变量没有出厂默认值,不先赋值就使用,编译器直接拦下。错误信息形如 error: variable count might not have been initialized。
修正:先 int count = 0; 再用。对比记忆:成员变量有默认值所以不报错——但"靠默认值活着"不算好习惯,成员变量也应在构造方法里明确赋初值。
🎯 什么时候用类
- 一组数据和一组操作天然绑定(如"狗"有名字年龄,会叫、过生日)
- 需要创建多个拥有相同结构但数据不同的对象
- 想把相关数据和行为封装在一起,代码更整洁
🛠 写类的几个好习惯
- 构造方法里把成员变量全部赋上初值:别靠默认值碰运气,对象一出生就该是"完整状态"
- 一个类干一类事:
Dog管狗的事,别把"算账""发邮件"都塞进来;类越专一,越好复用 - 成员变量命名用名词(name、age),方法用动词(bark、birthday),读代码像读句子
- 先画图纸再造对象:动手前先在纸上列"这类东西有什么属性、会做什么",类结构自然清晰
📝 本章小结
- 类是图纸,对象是实例;
new按图纸造对象 - 构造方法与类同名、无返回类型,
new时自动执行;可以重载多个,new时按参数自动挑 this指当前对象本尊,用来和参数区分- 成员变量存数据,方法定行为,用
.访问对象成员 - 每个对象数据独立,互不干扰;成员变量有出厂默认值(0 / false / null),局部变量没有
- 对象变量存的是引用(地址条),
d3 = d1是复制遥控器不是复制狗;==比地址 - 记忆口诀:类是图纸,对象是实例,new 来造,this 指自己。
动手改
- 给
Dog加一个String color成员变量,构造方法也接收它,让bark打印"旺财是白色的狗" - 再加一个
birthday()方法,让age加 1 并打印新年龄(上面示例已加,可自行修改逻辑) - 创建三只狗试试——它们的名字、年龄互不干扰,因为每个对象都有自己的成员变量
下一章:封装,给数据上把"安全锁" 🐸