Java 基础语言 ④ —— 面向对象基础-类与对象
- 前言
- 一、类的定义
- 1.1 类的理解
- 1.2 类的组成
- 1.3 示例:手机类
- 1.4 深入理解:类作为蓝图与 ADT
- 二、对象的创建与使用
- 2.1 创建对象:`new` 关键字
- 2.2 引用与对象的分离
- 2.3 赋值即拷贝引用
- 2.4 引用相等 vs 内容相等
- 三、对象内存模型
- 3.1 内存区域回顾
- 3.2 单个对象内存图
- 3.3 多个对象内存图
- 四、成员变量与局部变量
- 4.1 四大区别
- 五、构造方法
- 5.1 构造方法的本质:初始化而非创建
- 5.2 默认构造函数
- 5.3 构造器重载
- 5.4 构造器间的互调:`this(...)`
- 六、`this` 关键字
- 6.1 本质:隐式的隐藏参数
- 6.2 核心应用:消除命名歧义
- 6.3 `this` 的内存模型
- 6.4 关键约束
- 6.5 总结
- 总结
🎬 博主名称:超级苦力怕
🔥 个人专栏:《Java 后端修炼手册》《Java 基础语言》
🚀 每一次思考都是突破的前奏,每一次复盘都是精进的开始!
文章元信息:
- 标签:
#Java基础#面向对象#类与对象- 建议顺序:04
- 前置知识:建议先读《流程控制与数组》(③)
前言
从这一篇开始,我们正式踏入 Java 最核心的领域——面向对象编程(OOP)。如果说流程控制和数组让程序有了"逻辑",那类与对象就让程序有了"结构"。类是蓝图,对象是实物;类是抽象的模板,对象是堆内存中真实存在的数据仓库。本文将带你从零理解类的定义、对象的创建、
new关键字背后的内存操作、构造方法的初始化职责、以及this关键字如何将对象实体与执行逻辑绑定在一起。适合刚学完基本语法、准备系统入门面向对象的 Java 初学者,也适合想搞懂底层内存模型的进阶读者。
一、类的定义
1.1 类的理解
客观存在的事物皆为对象,所以我们也常说万物皆对象。
类是对现实生活中一类具有共同属性和行为的事物的抽象。它不是具体实体,而是一张"蓝图"——规定了这一类事物有哪些特征(属性)以及能做什么(行为)。
| 概念 | 理解 | 举例 |
|---|---|---|
| 类(Class) | 对一类事物的抽象描述,是对象的数据类型和模板 | "手机"这个类别 |
| 对象(Object) | 看得见摸得着的具体实体 | 你口袋里那台 iPhone 16 |
简单理解:类是对事物的一种描述,对象是具体存在的事物。类是"图纸",对象是按图纸造出来的"实物"。
1.2 类的组成
类由两部分组成:
- 属性(字段/成员变量):描述事物的特征,在类中方法外定义
- 行为(成员方法):描述事物能执行的操作,与普通方法的区别是去掉了
static关键字
类的组成: ├── 属性 → 成员变量(Field / Instance Variable) │ 例如:手机有品牌(brand)、价格(price) └── 行为 → 成员方法(Method) 例如:手机可以打电话(call)、发短信(sendMessage)类的定义步骤:
- 定义类:
public class 类名 { } - 编写成员变量:
数据类型 变量名; - 编写成员方法:去掉
static关键字
publicclass类名{// 成员变量数据类型 变量1;数据类型 变量2;// 成员方法public返回类型 方法名(){// 方法体}}1.3 示例:手机类
/* 手机类 Phone 成员变量: 品牌 brand (String) 价格 price (int) 成员方法: 打电话 call() 发短信 sendMessage() */publicclassPhone{// 成员变量Stringbrand;intprice;// 成员方法publicvoidcall(){System.out.println("打电话");}publicvoidsendMessage(){System.out.println("发短信");}}⚠️注意:成员变量声明在类中、方法外。它们有默认初始值(
int为0,引用类型为null),不需要手动初始化即可使用。
1.4 深入理解:类作为蓝图与 ADT
从软件工程的视角看,类不仅是代码的"收纳盒",更承担了两个关键角色:
① 类定义了对象的内存布局
类规定了该类型的每个对象在堆内存中需要分配多少空间、存储哪些变量。编译器根据类定义,精确计算每个对象所需的内存大小。
② 类是实现抽象数据类型(ADT)的工具
通过将字段设为private并仅暴露public方法,类可以强制维护不变性——即内部数据始终处于合法状态。外部代码无法直接"弄脏"对象内部的数据结构。
publicclassDate{privateintday;privateintmonth;// 通过构造方法强制校验publicDate(intday,intmonth){if(day<1||day>31||month<1||month>12){thrownewIllegalArgumentException("日期不合法");}this.day=day;this.month=month;}}这种"隐藏内部实现、只暴露安全接口"的思想,贯穿了 Java 的整个面向对象设计。
二、对象的创建与使用
2.1 创建对象:new关键字
对象是通过new关键字创建的。new在堆内存中为对象分配空间,然后调用构造方法初始化对象的成员变量。
格式:
类名 对象名=new类名();这条语句拆开看,其实做了三件事:
Phone p = new Phone(); │ │ │ │ │ └── ③ 调用构造方法,初始化对象 │ └── ② 在堆内存中为对象分配空间 └── ① 在栈内存中声明一个引用变量 p![[new关键字执行三步.png]]
✅ 创建对象并访问成员
publicclassPhoneDemo{publicstaticvoidmain(String[]args){// 创建对象Phonep=newPhone();// 访问成员变量(默认值)System.out.println(p.brand);// nullSystem.out.println(p.price);// 0// 为成员变量赋值p.brand="小米";p.price=2999;System.out.println(p.brand);// 小米System.out.println(p.price);// 2999// 调用成员方法p.call();// 打电话p.sendMessage();// 发短信}}⚠️区分字段与方法:
p.brand没有括号,访问的是字段;p.call()带有括号,调用的是方法。
2.2 引用与对象的分离
这是 Java 中最容易混淆的概念之一,必须明确区分
- 引用变量存储在栈(或包含它的对象)中,本质是一个内存地址
- 对象实体始终在堆中,包含实际的成员变量数据
Phone p = new Phone();中,p是引用变量,new Phone()才是对象
2.3 赋值即拷贝引用
当一个引用变量赋值给另一个引用变量时,拷贝的是地址,不是对象本身。
Phonep1=newPhone();p1.brand="华为";Phonep2=p1;// p2 和 p1 指向堆中**同一个**Phone 对象p2.brand="苹果";// 通过 p2 修改,实际上改的是同一个对象System.out.println(p1.brand);// "苹果" ← p1 看到的也变了!赋值前: 赋值后: p1 ──▶ [Phone对象] p1 ──▶ [Phone对象] ↗ p2关键结论:两个引用指向同一个对象,通过任意一个引用修改数据,另一个引用也会"看到"这个变化。
2.4 引用相等 vs 内容相等
Phonea=newPhone();a.brand="三星";Phoneb=newPhone();b.brand="三星";System.out.println(a==b);// false ← 比较的是地址System.out.println(a.brand.equals(b.brand));// true ← 比较的是内容| 比较方式 | 比较什么 | 适用场景 |
|---|---|---|
== | 两个引用是否指向同一个对象(地址相等) | 判断是否同一实例 |
.equals() | 两个对象是否内容相同(逻辑相等) | 判断数据是否等价 |
默认情况下
.equals()等同于==。需要内容比较时,必须在类中重写equals()方法,后面的继承章节会详细讨论。
三、对象内存模型
3.1 内存区域回顾
JVM 将内存划分为三个主要区域:
| 内存区域 | 存储内容 | 生命周期 | 共享性 |
|---|---|---|---|
| 栈(Stack) | 局部变量、方法参数、引用变量 | 方法执行期间 | 线程私有 |
| 堆(Heap) | 对象实体(成员变量)、数组 | 由 GC 管理 | 线程共享 |
| 方法区(Method Area) | 类的字节码、静态变量、方法代码 | 类加载到卸载 | 线程共享 |
3.2 单个对象内存图
栈内存 堆内存 方法区 ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ main() │ │ Phone对象 │ │ Phone.class │ │ │ │ │ │ │ │ Phone p ────┼──────────────┼──▶ brand=null│ │ void call() │ │ │ │ price=0 │◀────────│ void sendMsg() │ └──────────────┘ └──────────────┘ └─────────────────┘执行p.brand = "小米"; p.price = 2999;后:
栈内存 堆内存 ┌──────────────┐ ┌──────────────┐ │ main() │ │ Phone对象 │ │ │ │ │ │ Phone p ────┼──────────────┼──▶ brand="小米"│ │ │ │ price=2999 │ └──────────────┘ └──────────────┘调用p.call()时,JVM 找到 p 指向的对象,拿到对象的类信息,在方法区中找到call()的字节码并执行。方法执行时,会在栈顶新开一个栈帧。
3.3 多个对象内存图
Phonep1=newPhone();p1.brand="华为";Phonep2=newPhone();p2.brand="苹果";结论:
- 成员变量:每个对象在堆中有独立的内存区域,各自存储各自的变量值
- 成员方法:多个对象共享方法区中的同一份代码——方法代码只存一份,通过
this区分是哪个对象在调用
四、成员变量与局部变量
4.1 四大区别
| 区别维度 | 成员变量 | 局部变量 |
|---|---|---|
| 类中位置 | 类中、方法外 | 方法内部或方法声明(参数)上 |
| 内存中位置 | 堆内存 | 栈内存 |
| 生命周期 | 随对象存在而存在,随对象被 GC 回收而消失 | 随方法调用而存在,方法结束即消失 |
| 初始化值 | 有默认值(int→0,double→0.0, 引用→null,boolean→false) | 无默认值,必须先赋值后使用 |
✅ 位置差异示例
publicclassStudent{// 成员变量 —— 类中方法外Stringname;intage;publicvoidstudy(){inthours=2;// 局部变量 —— 方法内部System.out.println(name+"学习了"+hours+"小时");}publicvoidexam(intscore){// score —— 局部变量(方法参数)System.out.println(name+"考了"+score+"分");}}⚠️ 如果在方法内部定义了与成员变量同名的局部变量,局部变量会遮蔽成员变量。此时必须用
this.变量名来访问成员变量。这引出了下一节的内容——this关键字。
五、构造方法
5.1 构造方法的本质:初始化而非创建
构造方法常被误解为"创建对象的方法",但事实上:
- JVM 负责创建对象——在堆中分配空间、赋予默认值
- 构造方法负责初始化——把有意义的值填入已分配好的空间
语法特征:
- 方法名必须与类名完全相同
- 没有返回类型(连
void都不能写) - 通过
new关键字调用
publicclassStudent{Stringname;intage;// 构造方法publicStudent(Stringn,inta){name=n;// 初始化 name 字段age=a;// 初始化 age 字段}}new Student("张三", 18) 的执行过程: ① JVM 在堆中分配空间,字段获得默认值(name=null, age=0) ↓ ② JVM 调用构造方法 Student("张三", 18) ↓ ③ 构造方法将 name 设为 "张三",age 设为 18 ↓ ④ 返回对象的引用地址5.2 默认构造函数
如果类中没有写任何构造方法,Java 会自动提供一个默认的无参构造方法:
publicclassStudent{Stringname;intage;// 没有显式写构造方法// Java 自动提供:public Student() { }}// 使用时:Students=newStudent();// 可以,有默认无参构造⚠️关键规则:一旦你手动写了任意一个构造方法(无论是否有参),Java 就不再自动提供默认无参构造。
publicclassStudent{Stringname;intage;// 自定义了一个有参构造publicStudent(Stringn,inta){name=n;age=a;}}Students1=newStudent("张三",18);// ✅ 可以Students2=newStudent();// ❌ 编译错误!无参构造已消失解决方案:如果仍然需要无参构造,必须手动写出:
publicStudent(){}// 手动添加无参构造publicStudent(Stringn,inta){...}// 有参构造5.3 构造器重载
一个类可以有多个构造方法,只要它们的参数数量或类型不同。(这和方法重载的规则一样。)
publicclassPhone{Stringbrand;intprice;// 无参构造publicPhone(){}// 只传品牌publicPhone(Stringb){brand=b;}// 传品牌和价格publicPhone(Stringb,intp){brand=b;price=p;}}// 三种创建方式Phonep1=newPhone();// 无参Phonep2=newPhone("华为");// 一个参数Phonep3=newPhone("苹果",5999);// 两个参数5.4 构造器间的互调:this(...)
当一个类有多个构造方法时,可以用this(...)让一个构造方法调用另一个,避免重复代码。
publicclassPhone{Stringbrand;intprice;// 主构造方法publicPhone(Stringbrand,intprice){this.brand=brand;this.price=price;}// 只传品牌——委托给主构造方法publicPhone(Stringbrand){this(brand,0);// 调用 Phone(String, int),价格默认为 0}// 无参——委托给单参构造方法publicPhone(){this("未知品牌");// 调用 Phone(String)}}⚠️
this(...)必须是构造方法体中的第一条语句,且一个构造方法中只能调用一次this(...)。
六、this关键字
6.1 本质:隐式的隐藏参数
this是指向当前正在操作的对象的引用。每当调用非静态方法时,Java 会自动将调用该方法的对象的引用作为隐藏参数传入,这个隐藏参数就是this。
publicclassStudent{Stringname;publicvoidintroduce(){// 这里的 this 就是调用 introduce 的那个对象System.out.println("我是"+this.name);}}Students1=newStudent();s1.name="张三";s1.introduce();// 隐藏传入了 s1 → method 内部 this = s1Students2=newStudent();s2.name="李四";s2.introduce();// 隐藏传入了 s2 → method 内部 this = s26.2 核心应用:消除命名歧义
当方法的参数名与成员变量名相同时,局部变量(参数)会遮蔽成员变量。此时必须用this.变量名来访问成员变量:
publicclassStudent{privateStringname;// 成员变量privateintage;// 成员变量// 参数名故意与字段名相同——这是常见的命名惯例publicStudent(Stringname,intage){this.name=name;// this.name 是成员变量,name 是参数this.age=age;// this.age 是成员变量,age 是参数}publicvoidsetName(Stringname){this.name=name;// 同样:this.name 是字段,name 是参数}}在构造方法和 Setter 中,
this.字段 = 参数是最常见的 Java 编码惯例。
6.3this的内存模型
this本身是一个局部变量,存储在方法调用的栈帧中:
6.4 关键约束
① 静态方法中没有this
静态方法通过static修饰,属于类本身而非对象,调用时不会传入对象引用,因此内部不存在this。
publicclassStudent{Stringname;publicstaticvoidprintInfo(){System.out.println(this.name);// ❌ 编译错误!静态方法中不能使用 this}}②this不可被重新赋值
在 Java 中,this是一个不可改变的隐式 final 参数。你不能让this指向另一个对象。
publicvoidsomeMethod(){this=newStudent();// ❌ 编译错误!不能给 this 赋值}6.5 总结
this的用法 | 说明 | 场景 |
|---|---|---|
this.字段名 | 访问当前对象的成员变量 | 参数与字段同名时消除歧义 |
this.方法名() | 调用当前对象的另一个成员方法 | 方法间调用(通常可省略this) |
this(...) | 调用本类的另一个构造方法 | 构造器重载中消除重复代码 |
this是 Java 中连接"对象实体"与"执行逻辑"的纽带——它确保了方法始终知道自己操作的是哪个对象的数据。
总结
| 知识点 | 核心要点 |
|---|---|
| 类与对象 | 类是蓝图/模板,对象是堆内存中的具体实体;类 = 成员变量(属性)+ 成员方法(行为) |
new关键字 | 三步:声明引用变量(栈)→ 分配堆空间 → 调用构造方法初始化 |
| 引用与对象 | 引用变量存地址,对象实体在堆中;赋值拷贝的是引用,不是对象本身 |
==vsequals() | ==比较地址(同一对象),equals()比较内容(需重写) |
| 对象内存模型 | 栈存局部变量/引用,堆存对象实体,方法区存类信息/方法字节码;成员变量各对象独立,成员方法多对象共享 |
| 成员变量 vs 局部变量 | 位置不同(类中方法外 vs 方法内)、内存不同(堆 vs 栈)、生命周期不同、默认值不同 |
| 构造方法 | 只初始化不创建;与类同名无返回类型;一旦自定义构造方法,默认无参构造消失;支持重载;this(...)实现构造器链式调用 |
this关键字 | 隐式传入当前对象的引用;消除参数与字段的命名歧义;静态方法中不存在;不可被重新赋值 |
💡 核心结论:类定义了对象的"长相"和"行为",new在堆中把蓝图变成实物,构造方法为实物填充初始数据,this确保每个方法都知道自己在操作哪个对象。理解这四者的关系,就理解了 Java 面向对象编程的基石。
💡 关键提醒:始终牢记引用与对象的分离——一个引用变量只是一个"遥控器",多个遥控器可以指向同一个对象;构造方法的第一职责是初始化而非创建;在构造方法和 Setter 中坚持使用this.字段 = 参数的命名惯例,可以有效避免字段遮蔽带来的隐蔽 Bug。