类与对象概述
类,包含:
- 属性
- 行为
——>对象A、对象B、……
public class Hello{
public static void main(String[] args){
// 实例化一只猫
Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";
System.out.println("第一只猫的信息"+cat1.name+" "+cat1.age+" "+cat1.color);
}
}
// 定义猫类
class Cat{
// 属性
String name;
int age;
String color;
}
对象内存布局¶
以上述cat1为例。
graph LR
%% 1. 定义样式
%% stack: 蓝色 (栈)
%% heap: 紫色 (堆)
%% pool: 粉色 (常量池)
%% meta: 深粉/虚线 (类元数据)
classDef stack fill:#dbeafe,stroke:#3b82f6,stroke-width:2px;
classDef heap fill:#e9d5ff,stroke:#9333ea,stroke-width:2px;
classDef pool fill:#fce7f3,stroke:#db2777,stroke-width:2px;
classDef meta fill:#fbcfe8,stroke:#be185d,stroke-width:2px;
%% 2. 栈区域
subgraph Stack_Area [栈 Stack]
direction TB
cat_var("变量: cat<br/>存值: 0x0011"):::stack
end
%% 3. 堆区域
subgraph Heap_Area [堆 Heap]
direction TB
cat_obj("对象实例 @0x0011<br/>-------------------<br/>name: 0x0022 (引用)<br/>age: 12 (值)<br/>color: 0x0033 (引用)"):::heap
end
%% 4. 方法区 (包含常量池 + 类信息)
subgraph Method_Area [方法区 Method Area]
direction LR
%% 4.1 常量池
subgraph Constant_Pool [常量池]
direction TB
str_name("String @0x0022<br/>'小白'"):::pool
str_color("String @0x0033<br/>'白色'"):::pool
end
%% 4.2 类信息 (新增部分)
class_info("加载 Cat 类信息<br/>-------------------<br/>1. 属性信息<br/>2. 行为(方法信息)"):::meta
end
%% 5. 连接关系
cat_var -- 指向地址 0x0011 --> cat_obj
cat_obj -- name 指向 --> str_name
cat_obj -- color 指向 --> str_color
%% 布局辅助:让类信息稍微靠右显示(非必须,取决于渲染器)
Constant_Pool ~~~ class_info
-
核心概念:引用 vs 实体
-
栈 (Stack) 存引用:
- 变量(如
cat)只存储一个 首地址(例如0x0011)。 - 它相当于“钥匙”或“门牌号”。
- 变量(如
-
堆 (Heap) 存实体:
- 对象真实数据存储在堆中。
- 占用的是一块 连续的内存空间。
-
地址的本质:线头与偏移
所谓的“地址”不是一个孤立的点,而是这块连续空间的 起点 (Base Address)。
- 首地址 (Base Address): 对象的入口(大门)。
- 偏移量 (Offset): 属性相对于大门的距离。
-
寻址公式:
\[实际数据位置 = 首地址 + 偏移量\](JVM 只要拿到首地址,根据类的结构图,就能算出 age、name 具体在房间的哪个角落)
-
内存内部结构 (连续空间里有什么?)
当我们说 new Cat() 时,开辟的那块连续空间通常包含三部分:
- 对象头 (Header): 存元数据(哈希码、GC标记、锁状态)。
-
实例数据 (Instance Data):
- 基本类型 (
int age):直接存数值。 - 引用类型 (
String name):存指向其他地方的地址。
- 基本类型 (
-
对齐填充 (Padding): (可选) 为了内存规整补齐的空白字节。
-
推导结论
-
数组下标原理:
arr[0]为什么从0开始?因为它是第一个元素,相对于首地址的 偏移量为 0。 - 引用传递:
cat2 = cat1。是把 首地址 (钥匙) 复制了一份,并没有复制 房子 (连续空间)。两个钥匙开同一扇门。
属性¶
- 成员变量 = 属性 = field(字段)
- 基本数据类型和引用类型都可以
- 属性如果不赋值,有默认值,规则和数组一样
- 访问:
对象名.属性名
对象创建¶
- 先声明再创建:
Cat cat; cat = new Cat(); - 直接创建:
Cat cat = new Cat();
当我们执行 Person p = new Person() 时,底层发生了这 4 步:
-
加载类信息 (Class Loading)
JVM 读取类图纸(属性和方法)存入方法区。
- 注:这是基础,全过程只会执行一次。
-
堆分配 & 默认初始化 (Default Init)
在堆内存中开辟空间,并将所有属性设为默认零值:
int→0boolean→falseString→null
-
地址赋值 (Reference Assignment)
将堆内存的地址(如 0x11)交给栈里的变量 p。
- 此时
p已经指向对象,但对象里存的还是零值。
- 此时
-
指定初始化 (Explicit Init)
执行构造方法或显式赋值。
- 将“零值”覆盖为你代码中写的“真实值”。
成员方法¶
public class Method{
public static void main(String[] args){
Person p1 = new Person();
p1.speak();
}
}
class Person{
String name;
int age;
public void speak(){
System.out.println("我是一个好人");
}
}
方法的调用机制:
- 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
- 当方法执行完毕,或者执行到return语句时,就会返回,
- 返回到调用方法的地方
- 返回后,继续执行方法后面的代码
使用细节¶
-
访问修饰符 (Access Modifiers)
-
作用:控制方法使用的范围(即该方法可以在哪些地方被调用)。
-
默认规则:如果不写访问修饰符,则默认为“默认访问权限”(default/package-private)。
-
💡 补充解答:Java 的四种访问修饰符
public:对外公开,**任何地方**都可以访问。
protected:受保护的,**同包**下或**子类**中可以访问。默认 (default):不写修饰符时,仅**同包**(同一个 package)下可以访问。
private:私有的,仅**本类**内部可以访问。
-
返回类型 (Return Type)
-
数量限制:一个方法最多有一个返回值。
🤔 思考:如何返回多个结果?
虽然 return 只能返回一个值,但我们可以通过以下方式“变相”返回多个结果:
-
返回数组:将多个数据放入
int[]或String[]等数组中返回。 -
返回集合:使用
List,Map,Set等集合容器。 -
返回对象:创建一个类(如
Result类),将多个结果作为该类的属性,然后返回这个对象。
-
-
类型范围:返回类型可以是任意类型,包含**基本数据类型**(如
int,double)或**引用数据类型**(如数组、对象)。 -
语法要求:
-
如果方法定义了返回数据类型(即不是
void),则方法体中最后的执行语句必须为return 值;。 -
类型一致性:要求返回值的类型必须和定义时的返回类型**一致**或**兼容**。
💡 补充:什么是“兼容”?
-
自动类型转换:例如方法定义返回
double,实际return 10;(int),int 会自动转为 double,这是兼容的。 -
多态(继承):方法定义返回
Animal类型,实际return new Dog();(Dog 是 Animal 的子类),这也是兼容的。
-
-
-
Void 情况:如果方法返回类型是
void,则方法体中可以没有return语句,或者只写return;(用于提前结束方法,不带返回值)。 -
方法名 (Method Name)
-
命名规范:遵循**驼峰命名法 (CamelCase)**,首字母小写,后续单词首字母大写。
-
命名原则:最好**见名知义**,能够表达出该功能的意思。
- 示例:得到两个数的和 \(\rightarrow\)
getSum。
- 示例:得到两个数的和 \(\rightarrow\)
-
-
开发建议:在实际开发中必须严格按照规范命名,以提高代码可读性。
-
形参列表
-
参数定义规则
-
数量:可以是 0 个或多个,中间用逗号
,隔开。- 例:
getSum(int n1, int n2)
- 例:
-
类型:可以是任意类型(包含基本类型或引用类型)。
- 例:
printArr(int[][] map)
- 例:
-
-
概念区分
-
形参 (形式参数):方法**定义**时的参数。
-
实参 (实际参数):方法**调用**时实际传入的值。
-
-
调用匹配原则
调用方法时,实参与形参必须严格匹配:
- ✅ 类型:必须一致或兼容。
- ✅ 个数:必须一致。
- ✅ 顺序:必须一致。
-
方法体:完成功能的具体方法,但是再定义新的方法。
-
方法调用细节:
-
同类调用(内部)
- 规则:直接写方法名调用。
- 语法:
方法名(参数); -
场景:A类中的方法
sayOk调用本类中的print()。 -
跨类调用(外部)
- 规则:必须通过**对象名**调用(先创建对象/实例化)。
- 语法:
对象名.方法名(参数); - 场景:B类中的
sayHello想要调用 A类中的print()。
-
权限说明
- 跨类调用受**访问修饰符**(如
public,private,protected)的限制。只有对外公开的方法才能被跨类调用。
- 跨类调用受**访问修饰符**(如
-
TIPS:
- 静态方法 (static):如果A类的方法是
static修饰的,跨类调用时推荐使用类名.方法名(),而不需要创建对象。 - 封装性 (Encapsulation):如果A类的
print()方法被修饰为private,则B类无法调用(即使创建了对象也不行)。这是面向对象编程的重要特性。
- 静态方法 (static):如果A类的方法是
Java 成员方法传参机制¶
方法的传参机制遵循 "Java 中只有值传递 (Pass by Value)" 这一基本原则。
| 类型 | 传递内容 | 内存行为 | 影响原数据? |
|---|---|---|---|
| 基本数据类型 | 真实的值 | 栈帧之间数据的独立拷贝 | No |
| 引用数据类型 | 内存地址 (引用) | 栈中不同变量指向堆中同一块区域 | Yes (仅限修改内容时) |
一句话总结:
Java 的参数传递,永远传的是**栈**里面的那个值。
- 基本类型:栈里存的就是**数值**,所以传的是数值的副本。
- 引用类型:栈里存的是**堆地址**,所以传的是地址的副本。
1. 基本数据类型的传参机制¶
(如 int, double, boolean 等)
- 核心表现:值拷贝。
- 机制:传递的是数据的**具体数值**(副本)。方法内对形参的修改,**绝不会**影响方法外的实参。
public void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
System.out.println("a=" + a + "\tb=" + b); // 方法内交换成功
}
- 结论:
- 方法内部
a和b交换了。 - 方法外部(
main中)的变量值保持不变。 - 原理补充:基本数据类型存储在**栈 (Stack)** 中。调用方法时,是在栈开辟了新的独立空间存储形参
a和b,它们与实参互不干扰。
2. 引用数据类型的传参机制¶
(如 Array (数组), Object (对象) 等)
- 核心表现:地址拷贝。
- 机制:传递的是对象在堆内存中的**地址值**(引用)。
- 形参和实参指向**堆 (Heap)** 中的同一个内存空间。
-
通过形参修改对象内部属性/数组元素,**会**影响实参。
-
情况 A:修改内部数据
-
修改数组 (
test100):传入数组,方法内arr[0] = 200。- 结果:原数组发生变化。
-
修改对象属性 (
test200):传入Person对象,方法内p.age = 100。- 结果:原对象的
age发生变化。
- 结果:原对象的
-
情况 B:修改引用本身(特殊陷阱)
如果在方法中执行以下操作,**不会**影响原对象:
public void test200(Person p) {
p = null; // 只是切断了形参p的指向,实参不受影响
// 或者
p = new Person(); // 形参p指向了新的堆内存地址,实参仍指向旧地址
p.name = "New Jack";
}
- 结论:
- 修改形参**指向的对象内容** \(\rightarrow\) 实参**变**。
- 修改形参**本身的指向** (即
=赋值新对象/null) \(\rightarrow\) 实参**不变**。
克隆对象¶
核心目标:创建一个新对象,使其属性值与原对象完全一致,但在内存中是**两个独立的空间**(互不影响)。
1. 需求背景¶
在 Java 中,如果直接将一个对象赋值给另一个变量(如 p2 = p1),实际上只是复制了地址。修改 p2 也会影响 p1。
为了得到一个内容相同但完全独立的“副本”,我们需要进行对象克隆。
2. 代码实现¶
class Person {
String name;
int age;
}
class MyTools {
// 编写 copyPerson 方法,返回一个新的 Person 对象
public Person copyPerson(Person p) {
// 1. 创建一个新对象(在堆内存中开辟新空间)
Person newP = new Person();
// 2. 把原来对象的属性值,逐个拷贝到新对象中
newP.name = p.name;
newP.age = p.age;
// 3. 返回新对象(返回的是新对象的地址)
return newP;
}
}
3. 测试与验证¶
验证克隆是否成功的关键在于:内容相同,地址不同。
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "Milan";
p1.age = 18;
MyTools tools = new MyTools();
Person p2 = tools.copyPerson(p1); // 克隆
// 【验证内容】
System.out.println("p1的属性: " + p1.name + " " + p1.age);
System.out.println("p2的属性: " + p2.name + " " + p2.age);
// 【验证独立性】
// p1 == p2 结果应为 false,说明是两个不同的内存地址
System.out.println("p1 和 p2 是同一个对象吗? " + (p1 == p2));
// 修改 p2 不会影响 p1
p2.name = "Smith";
System.out.println("修改p2后,p1.name=" + p1.name); // p1 仍然是 Milan
}
4. 必要的补充与底层原理解析¶
A. 引用赋值 vs 对象克隆¶
- 引用赋值 (
p2 = p1): - 就像给了同一个人两个名字。
- 内存:两个变量指向**同一个**堆内存块。
- 后果:一变全变。
- 对象克隆 (
copyPerson): - 就像按照原件复印了一份文件。
- 内存:
new关键字在堆中开辟了**全新**的区域。 - 后果:互不干扰。
B. 浅拷贝与深拷贝 (进阶补充)¶
上述案例属于**浅拷贝 (Shallow Copy)** 的一种手动实现形式。
- 如果
Person类中还有一个引用类型的属性(比如Address类),上述代码只是复制了Address的地址。 - 深拷贝 (Deep Copy):需要连同对象内部引用的其他对象也一并创建新的副本(层层克隆)。
C. 内存图解简述¶
- 调用
copyPerson(p1)时,栈中传入p1的地址。 - 方法内部执行
new Person(),堆 (Heap) 中产生一个新的地址(假设为0x99)。 - 数据拷贝完成后,方法返回
0x99。 main方法中的p2接收到0x99。此时p1指向0x11,p2指向0x99,彻底分离。
方法重载¶
同一个类中,多个同名方法存在,但是:
- 形参列表:必须存在不同
- 返回类型:无要求
public class OverLoad{
public static void main(String[] args){
MyCalculator mc = new MyCalculator();
System.out.println(mc.calculate(1, 2));
System.out.println(mc.calculate(1, 2.5));
System.out.println(mc.calculate(1.5, 2));
System.out.println(mc.calculate(1, 2, 3));
}
}
class MyCalculator{
public int calculate(int n1, int n2){
System.out.println("calculate(int n1, int n2)被调用");
return n1 + n2;
}
public double calculate(int n1, double n2){
System.out.println("calculate(int n1, double n2)被调用");
return n1 + n2;
}
public double calculate(double n1, int n2){
System.out.println("calculate(double n1, int n2)被调用");
return n1 + n2;
}
public double calculate(int n1, int n2, int n3){
System.out.println("calculate(int n1, int n2, int n3)被调用");
return n1 + n2 + n3;
}
}
calculate(int n1, int n2)被调用
3
calculate(int n1, double n2)被调用
3.5
calculate(double n1, int n2)被调用
3.5
calculate(int n1, int n2, int n3)被调用
6.0
可变参数¶
Java运行将同一个类中多个同名同功能但 参数不同 的方法,封装成一个方法。
访问修饰符 访问类型 方法名(数据类型... 形参名){
}
案例【计算N个数字之和】
public class VarParameter{
public static void main(String[] args){
MyCalculator mc = new MyCalculator();
System.out.println(mc.calculate(1, 2, 3, 4, 5));
}
}
class MyCalculator{
public int calculate(int... n){
System.out.println("接收的参数个数="+n.length);
int res = 0;
for(int i = 0; i < n.length; i++){
res += n[i];
}
return res;
}
}
接收的参数个数=5
15
- 可变参数的实参可以是0个和多个
- 可变参数的实参可以是数组
- 可变参数的本质就是数组
- 可变参数可普通类型的参数可以放在一起,但是必须把 可变参数放在最后
- 一个形参列表中只能出现**一个**可变参数
构造方法&构造器¶
[修饰符] 方法名(形参列表){
方法体;
}
- 构造器的修饰符可以默认,也可以是 public protected private
- 构造器没有返回值
- 方法名和类名字必须一样
- 参数列表和成员方法一样的规则
- 构造器的调用, 由系统完成
- 一个类可以定义多个构造器,即构造器重载。
- 如果没有定义构造器,系统会自动生成一个默认的无参构造器;一旦定义了自己的构造器,默认的构造器就被覆盖,不能再使用
public class Constructor{
public static void main(String[] args){
Person p1 = new Person("Smith", 65);
System.out.println("p1的信息如下:");
System.out.println("p1姓名:" + p1.name);
System.out.println("p1年龄:" + p1.age);
}
}
class Person{
String name;
int age;
public Person(String pName, int pAge){
System.out.println("构造器被调用");
name = pName;
age = pAge;
}
}
构造器被调用
p1的信息如下:
p1姓名:Smith
p1年龄:65
this关键词¶
Java给每个对象分配this,代表当前对象。
- this 关键字可以用来访问本类的属性、方法、构造器
- this 用于区分当前类的属性和局部变量
- 访问成员方法的语法:
this.方法名(参数列表); - 访问构造器语法:
this(参数列表);注意只能在构造器中使用(即**只能在构造器中访问另外一个构造器, 必须放在第一 条语句**) - this 不能在类定义的外部使用,只能在类定义的方法中使用。