跳转至

类与对象概述

类,包含:

  • 属性
  • 行为

——>对象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
  1. 核心概念:引用 vs 实体

  2. 栈 (Stack) 存引用:

    • 变量(如 cat)只存储一个 首地址(例如 0x0011)。
    • 它相当于“钥匙”或“门牌号”。
  3. 堆 (Heap) 存实体:

    • 对象真实数据存储在堆中。
    • 占用的是一块 连续的内存空间
  4. 地址的本质:线头与偏移

所谓的“地址”不是一个孤立的点,而是这块连续空间的 起点 (Base Address)

  • 首地址 (Base Address): 对象的入口(大门)。
  • 偏移量 (Offset): 属性相对于大门的距离。
  • 寻址公式:

    \[实际数据位置 = 首地址 + 偏移量\]

    (JVM 只要拿到首地址,根据类的结构图,就能算出 age、name 具体在房间的哪个角落)

  • 内存内部结构 (连续空间里有什么?)

当我们说 new Cat() 时,开辟的那块连续空间通常包含三部分:

  1. 对象头 (Header): 存元数据(哈希码、GC标记、锁状态)。
  2. 实例数据 (Instance Data):

    • 基本类型 (int age):直接存数值。
    • 引用类型 (String name):存指向其他地方的地址。
  3. 对齐填充 (Padding): (可选) 为了内存规整补齐的空白字节。

  4. 推导结论

  5. 数组下标原理: arr[0] 为什么从0开始?因为它是第一个元素,相对于首地址的 偏移量为 0

  6. 引用传递: cat2 = cat1。是把 首地址 (钥匙) 复制了一份,并没有复制 房子 (连续空间)。两个钥匙开同一扇门。

属性

  • 成员变量 = 属性 = field(字段)
  • 基本数据类型和引用类型都可以
  • 属性如果不赋值,有默认值,规则和数组一样
  • 访问:对象名.属性名

对象创建

  • 先声明再创建:Cat cat; cat = new Cat();
  • 直接创建:Cat cat = new Cat();

当我们执行 Person p = new Person() 时,底层发生了这 4 步:

  1. 加载类信息 (Class Loading)

    JVM 读取类图纸(属性和方法)存入方法区。

    • 注:这是基础,全过程只会执行一次。
  2. 堆分配 & 默认初始化 (Default Init)

    在堆内存中开辟空间,并将所有属性设为默认零值:

    • int0
    • booleanfalse
    • Stringnull
  3. 地址赋值 (Reference Assignment)

    将堆内存的地址(如 0x11)交给栈里的变量 p。

    • 此时 p 已经指向对象,但对象里存的还是零值。
  4. 指定初始化 (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("我是一个好人");
    }
}

方法的调用机制:

  1. 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
  2. 当方法执行完毕,或者执行到return语句时,就会返回,
  3. 返回到调用方法的地方
  4. 返回后,继续执行方法后面的代码

使用细节

  1. 访问修饰符 (Access Modifiers)

    • 作用:控制方法使用的范围(即该方法可以在哪些地方被调用)。

    • 默认规则:如果不写访问修饰符,则默认为“默认访问权限”(default/package-private)。

💡 补充解答:Java 的四种访问修饰符

  1. public:对外公开,**任何地方**都可以访问。

  2. protected:受保护的,**同包**下或**子类**中可以访问。

  3. 默认 (default):不写修饰符时,仅**同包**(同一个 package)下可以访问。

  4. private:私有的,仅**本类**内部可以访问。

  1. 返回类型 (Return Type)

  2. 数量限制:一个方法最多有一个返回值。

    🤔 思考:如何返回多个结果?

    虽然 return 只能返回一个值,但我们可以通过以下方式“变相”返回多个结果:

    • 返回数组:将多个数据放入 int[]String[] 等数组中返回。

    • 返回集合:使用 List, Map, Set 等集合容器。

    • 返回对象:创建一个类(如 Result 类),将多个结果作为该类的属性,然后返回这个对象。

  3. 类型范围:返回类型可以是任意类型,包含**基本数据类型**(如 int, double)或**引用数据类型**(如数组、对象)。

  4. 语法要求

    • 如果方法定义了返回数据类型(即不是 void),则方法体中最后的执行语句必须为 return 值;

    • 类型一致性:要求返回值的类型必须和定义时的返回类型**一致**或**兼容**。

      💡 补充:什么是“兼容”?

      • 自动类型转换:例如方法定义返回 double,实际 return 10; (int),int 会自动转为 double,这是兼容的。

      • 多态(继承):方法定义返回 Animal 类型,实际 return new Dog();(Dog 是 Animal 的子类),这也是兼容的。

  5. Void 情况:如果方法返回类型是 void,则方法体中可以没有 return 语句,或者只写 return;(用于提前结束方法,不带返回值)。

  6. 方法名 (Method Name)

  7. 命名规范:遵循**驼峰命名法 (CamelCase)**,首字母小写,后续单词首字母大写。

    • 命名原则:最好**见名知义**,能够表达出该功能的意思。

      • 示例:得到两个数的和 \(\rightarrow\) getSum
  8. 开发建议:在实际开发中必须严格按照规范命名,以提高代码可读性。

  9. 形参列表

  10. 参数定义规则

    • 数量:可以是 0 个或多个,中间用逗号 , 隔开。

      • 例:getSum(int n1, int n2)
    • 类型:可以是任意类型(包含基本类型或引用类型)。

      • 例:printArr(int[][] map)
  11. 概念区分

    • 形参 (形式参数):方法**定义**时的参数。

    • 实参 (实际参数):方法**调用**时实际传入的值。

  12. 调用匹配原则

    调用方法时,实参与形参必须严格匹配:

    • 类型:必须一致或兼容。
    • 个数:必须一致。
    • 顺序:必须一致。
  13. 方法体:完成功能的具体方法,但是再定义新的方法。

  14. 方法调用细节

  15. 同类调用(内部)

    • 规则:直接写方法名调用。
    • 语法方法名(参数);
    • 场景:A类中的方法 sayOk 调用本类中的 print()

    • 跨类调用(外部)

    • 规则:必须通过**对象名**调用(先创建对象/实例化)。
    • 语法对象名.方法名(参数);
    • 场景:B类中的 sayHello 想要调用 A类中的 print()
  16. 权限说明

    • 跨类调用受**访问修饰符**(如 public, private, protected)的限制。只有对外公开的方法才能被跨类调用。
  17. TIPS:

    • 静态方法 (static):如果A类的方法是 static 修饰的,跨类调用时推荐使用 类名.方法名(),而不需要创建对象。
    • 封装性 (Encapsulation):如果A类的 print() 方法被修饰为 private,则B类无法调用(即使创建了对象也不行)。这是面向对象编程的重要特性。

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); // 方法内交换成功
}
  • 结论
  • 方法内部 ab 交换了。
  • 方法外部(main中)的变量值保持不变。
  • 原理补充:基本数据类型存储在**栈 (Stack)** 中。调用方法时,是在栈开辟了新的独立空间存储形参 ab,它们与实参互不干扰。

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. 内存图解简述
  1. 调用 copyPerson(p1) 时,栈中传入 p1 的地址。
  2. 方法内部执行 new Person()堆 (Heap) 中产生一个新的地址(假设为 0x99)。
  3. 数据拷贝完成后,方法返回 0x99
  4. main 方法中的 p2 接收到 0x99。此时 p1 指向 0x11p2 指向 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 不能在类定义的外部使用,只能在类定义的方法中使用。