文章目录
- 📚 今日学习目标
- 一、什么是面向对象?
- 二、类和对象
-
- 2.1 类和对象的区别
- 2.2 如何定义类和对象
-
- 2.2.1 定义类
- 2.2.2 创建对象
- 2.3 Java中的类分类
- 三、封装
-
- 3.1 什么是封装?
- 3.2 如何确定方法的归属?
- 3.3 访问权限控制
- 3.4 私有化后如何赋值和取值?
- 四、标准的Javabean类
- 五、对象内存理解
-
- 5.1 一个对象的内存图
- 5.2 this指针的内存原理
📚 今日学习目标
一、什么是面向对象?
面向对象编程(OOP) 是一种软件开发方法,它将现实世界的事物抽象成“对象”。每个对象都包含数据(属性) 和操作数据的方法(行为)。这种思想让程序更贴近人类的思维方式,从而提高代码的可读性、可维护性和可复用性。
🌰 例如:现实中的“人”有姓名、年龄等属性,有吃饭、睡觉等行为。在程序中,我们可以用一个Person对象来表示一个人,包含相应的属性和方法。
二、类和对象
2.1 类和对象的区别
- 类(Class):是一类事物的抽象描述,相当于一个模具或蓝图。它定义了该类事物应有的共同特征和行为,🌰例如“人”、“手机”。
- 对象(Object):是类的具体实例,是真实存在的个体。例如,“人”这个类的一个对象是“张三”,“手机”类的一个对象是“华为P40”。
*注意:必须先定义类,才能通过类创建对象。
2.2 如何定义类和对象
2.2.1 定义类
使用关键字 class,类中包含成员变量(属性) 和成员方法(行为)。
public class Person {
// 成员变量(属性数据)
String name;
int age;
// 成员方法(操作数据的方法)
public void display(){
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
2.2.2 创建对象
格式:类名 对象名 = new 类名();
Person p = new Person(); // 创建Person类的对象p
通过 对象名.成员变量 访问属性,通过 对象名.方法名() 调用方法。
p.name = "张三";
p.age = 25;
p.display(); // 输出:姓名:张三,年龄:25
2.3 Java中的类分类
- Javabean类:用来描述一类事物的类,只包含属性和方法,不包含main方法。例如上面的Person。
- 测试类:包含main方法的类,用于创建Javabean对象并对其进行操作。
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.name = "李四";
p.age = 30;
p.display();
}
}
三、封装
3.1 什么是封装?
封装是面向对象的三大特性之一(另两个是继承和多态)。它包含两层含义:
封装的好处:
- 保护数据:防止外部随意修改,保证数据的合法性和安全性。
- 降低耦合:隐藏内部实现,外部只需关心接口,便于维护和升级。
3.2 如何确定方法的归属?
一个常见的设计问题是:某个行为应该属于哪个类? 🌰例如,“人关门”或“人画圆”这两个行为,代码应该如何设计?
核心原则: 谁拥有数据,谁提供操作。想动谁的数据,就调用谁的方法。 再换句话说 执行这个动作,主要会改变或者涉及到谁的状态或者数据 ? 如果A要操作B的数据,A应该“请求”B去做,而不是“代替”B去做。
🌰解释:
- 人关门:关门的动作主要改变的是门的状态(从开到关),因此关门的方法应该属于门类,人只是调用门的close()方法。
- 人画圆:画圆需要使用圆的半径、圆心等数据,因此绘制圆的方法应该属于圆类,人调用圆的draw()方法。
再看一个例子:公司给员工发工资
class Company {
private Account account; // 公司的财务账本
void paySalary(Employee emp) {
this.account.deduct(emp.salary); // 公司修改自己的账本数据
emp.receiveSalary(emp.salary); // 通知员工更新他自己的数据
}
}
class Employee {
private BankCard card; // 员工的银行卡
void receiveSalary(double amount) {
this.card.deposit(amount); // 员工修改自己的银行卡数据
}
}
3.3 访问权限控制
如果成员变量直接暴露给外部,任何人都可以随意修改,可能导致数据不安全或不合法。Java提供了访问权限修饰符来控制成员的可见性。
| private | ✅ | ❌ | ❌ | ❌ |
| 默认(无) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
与C++的对比
| 访问权限类型 | public、protected、private | public、protected、private、默认(包私有) |
| 默认访问权限 | 类中默认为private,结构体中默认为public | 默认为包私有(仅同一包内可见,不同包中的类无论是否有继承关系都无法访问。) |
| 包/模块概念 | ❌ 无包概念,可见性由头文件包含和声明位置决定 | ✅ 有包机制,提供同包可见性 |
| private 作用 | • 仅本类内部可访问 | • 仅本类内部可访问 |
| protected 作用 | • 本类内部 • 派生类内部(只能通过派生类自身实例访问继承的成员) | • 本类内部 • 同一包内任何类(无论是否有继承关系) • 不同包子类内部(只能通过子类自身实例访问继承的成员) |
| public 作用 | 任何地方均可访问 | 任何地方均可访问 |
| 友元机制 | ✅ 支持 friend,可声明外部函数或类为友元,使其能够访问本类的私有和保护成员 | ❌ 不支持,但可通过包私有\\实现同一包内的紧密协作 |
3.4 私有化后如何赋值和取值?
方式一:get/set方法
通过编写getXxx()和setXxx()方法来控制权限。
*注:编写get set方法,可能会疑惑,既然提供编写了get和set,那你私有化的目的是什么?
- 为了自主决定读写权限,你可以只提供 get 方法而不提供 set 方法,这样外部代码可以获取数据,但不能随意修改它。反之,也可以只提供 set 而不提供 get,创建一个只写的属性
- 而且这种方法,在方法内部你可以添加校验逻辑,避免非法数据。例如,年龄不能为负数。
public class Person {
private String name;
private int age;
// setter:允许外部设置值,可以加入合法性校验
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
} else {
System.out.println("年龄不合法!");
}
}
// getter:允许外部获取值
public int getAge() {
return age;
}
// 可以只提供get而不提供set,实现只读属性
public String getName() {
return name;
}
}
方式二:构造方法
构造方法是一种特殊的方法,用于在创建对象时初始化对象的状态。
格式:权限修饰符 类名(参数列表) { 方法体 }
特点:
public class Person {
private String name;
private int age;
// 无参构造
public Person() {
}
// 有参构造
public Person(String name, int age) {
this.name = name; // this用于区分成员变量和局部变量
this.age = age;
}
}
关于this关键字: this代表当前调用者的地址。当构造方法的参数名与成员变量名相同时,必须用this来区分。
Java与C++构造方法对比:
| 内存管理 | – 手动内存管理:需要显式地分配和释放内存,生命周期精确可控。 – 追求性能与控制:提供底层操作能力,开发者需对资源管理负责。 | – 自动内存管理(垃圾回收 GC):JVM自动回收不再使用的对象内存。 – 追求安全与简洁:屏蔽内存细节,减少手动错误。 |
| 默认构造函数生成规则 | – 如果没有定义任何构造函数,编译器提供一个无参默认构造。 – 如果定义了有参构造函数,则不再生成无参默认构造,但仍然提供默认的拷贝构造函数。 – 拷贝构造函数是独立生成的:只要用户没有自定义拷贝构造,编译器总会生成一个默认拷贝构造(逐成员拷贝),即使已经定义了其他构造函数。如果定义了拷贝构造函数,则不提供其他任何构造函数。 | – 如果没有定义任何构造方法,编译器生成一个无参默认构造。 – 如果定义了有参构造方法,则不再生成无参默认构造。 – Java 没有“默认拷贝构造方法”的概念:不会自动生成接收同类型参数的构造方法,必须手动实现拷贝逻辑。这迫使你有意识地决定拷贝行为,而不是在不知情的情况下掉进浅拷贝陷阱。 |
| 初始化列表 | – 特有语法,在构造函数体执行前初始化成员变量、基类、常量成员和引用成员。 | – 无此语法。 |
| 成员变量直接赋值 | – C++98/03:不允许非静态成员在类内直接初始化,必须在构造函数初始化列表或构造体内赋值。 – C++11 起:支持类内初始化器(in-class initializers),允许在声明时直接赋值 | – 始终允许:成员变量可以在声明时直接赋值,编译器会将这些赋值处理为在构造方法执行前执行(按声明顺序)。 |
| 友元(friend) | – 支持:允许外部函数或类访问当前类的私有成员,破坏封装性以换取灵活性。 | – 不支持:没有直接对应的关键字,可通过包级可见性或反射实现类似效果,但语言层面不提供。 |
| 运算符重载 | – 支持:可重新定义大多数运算符(如 +、-、[])对自定义类型的含义,但需谨慎使用以保持代码可读性。 | – 不支持:除了 + 对字符串的拼接(语言内置特例),开发者不能自定义运算符行为。Java 设计者认为这会导致代码复杂且易读性差。 |
四、标准的Javabean类
- 无参构造方法
- 带全部参数的构造方法
- 提供每一个成员变量对应的getXxx()和setXxx()
- 其他方法也需写上
五、对象内存理解
5.1 一个对象的内存图
整个过程与数组的内存分配高度一致【详细可以参考数组学习篇】:数组也是引用类型,在堆中分配空间,默认初始化元素,变量保存地址。区别在于数组的类由JVM动态生成,没有独立的class文件加载过程,且数组只有length属性和继承来的方法,不能自定义成员方法。

过程描述(按执行顺序)
main方法字节码文件入方法区 JVM启动后,首先通过类加载器将TestStudent.class和Student.class加载到方法区(或元空间)。方法区中存储了类的结构信息,包括成员变量定义、方法字节码、静态常量等。此时main方法只是作为字节码的一部分存在于方法区中,尚未执行。
main方法入栈并声明局部变量 JVM调用main方法,在栈内存中为main方法创建栈帧,栈帧内包含局部变量表等。此时声明局部变量s(类型为Student),默认值为null,但还未指向任何对象。
new创建对象 执行new Student()时,JVM完成以下操作:
- 在方法区中查找Student类信息:如果类尚未加载,则先加载(本例已在第一步加载)。
- 在堆内存中开辟空间:为Student对象分配一块连续的内存,用于存放实例变量(name和age)以及对象头(包含哈希码、GC信息、指向方法区类元数据的指针等)。
- 默认初始化:将实例变量设为默认值——name = null,age = 0。
- 显示初始化:执行代码中定义的显式赋值——name = “张三”,age = 23。注意这里的"张三"是一个字符串常量,存储在方法区的字符串常量池中,对象中的name变量保存的是该字符串的引用。
- 构造方法初始化:调用构造方法(本例为默认无参构造,没有额外操作,但概念上这一步存在)。
- 将堆内存地址赋值给局部变量:将新创建对象的引用(堆地址)存入栈中s变量。
输出与修改成员变量
- System.out.println(s):输出的是对象的引用地址(如Student@15db9742),本质是调用toString()。
- 首次输出s.name和s.age时,通过s持有的地址访问堆中对象,得到显示初始化后的值“张三”和23。
- s.name = "阿强"和s.age = 23(这里其实没变)直接修改堆中对象的成员变量。
- 再次输出,看到修改后的值。
调用成员方法 s.study()执行时,JVM通过对象头的类型指针找到方法区中Student类的信息,定位到study()方法的字节码并执行。方法执行过程中,若有局部变量,会在栈中创建新的栈帧,但本例study()内部无额外变量。
5.2 this指针的内存原理
补充:成员变量和局部变量的区别
| 类中位置不同 | 类中,方法外 | 方法内、方法申明上 |
| 初始化值不同 | 有默认初始化值 | 没有,使用之前需要完成赋值 |
| 内存位置不同 | 堆内存 | 栈内存 |
| 生命周期不同 | 随着对象的创建而存在,随着对象的消失而消失 | 随着方法的调用而存在,随着方法的运行结束而消失 |
| 作用域 | 整个类中有效 | 当前方法中有效 |
以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。




