JavaSE-面向对象-类和对象

53 阅读9分钟

“类(class)是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。由类构造(construct)对象的过程称为创建类的实例(instance)。” 摘录来自 Java核心技术 卷Ⅰ 基础知识(原书第10版) 【美】凯 S.霍斯特曼

1、类

What(什么是类)

类是一个抽象的概念或模版,它定义了对象的属性和行为。 类通过关键字class来定义。一个类通常包括:

  • 成员变量 属性(Fields):用于描述对象的状态或特征(数据)。
  • 方法 (Methods):描述对象能做什么(行为/功能)。
  • 构造器 (Constructors):负责在创建对象时进行初始化。
  • 访问修饰符 :控制类、方法、变量等的访问权限。

访问修饰符

Java类的访问权限可以通过访问修饰符来控制,常见的有:

  • public:类可以被任何其他类访问。

  • private:类只能在当前类内部访问(对于内部类)。

  • default:没有指定访问修饰符时,类只能在同一个包内访问。

  • protected:不能单独使用,但可以用于类的成员,表示仅能在同包或子类中访问。

How (怎么写)

public class ClassName {
    // 成员变量
    private String name;
    private int age;

    // 构造方法
    public ClassName(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 方法
    public void sayHello() {
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
    }

    // Getter方法
    public String getName() {
        return name;
    }

    // Setter方法
    public void setName(String name) {
        this.name = name;
    }
}

Why (原理)

类在JVM中的实现机制我们可以通过几个方面来理解。

类的生命周期

一个类从被编译的 .java 文件到最终在 JVM 中实例化对象,需要经历 加载、连接(验证、准备、解析)、初始化 三个阶段。

阶段作用关键机制
加载 (Loading)查找和导入 .class 文件的二进制数据。类加载器 (ClassLoader):负责从文件系统、网络或 JAR 包中找到 Class 文件,将其二进制数据读入内存,并生成一个代表该类的 java.lang.Class 对象。
连接 (Linking)将类的二进制数据合并到 JVM 运行时状态中。包括:



1. 验证 (Verification):确保 .class 文件符合 JVM 规范,没有安全问题。



2. 准备 (Preparation):为类的静态变量分配内存,并设置默认初始值(例如 int 为 0,引用为 null)。



3. 解析 (Resolution):将常量池中的符号引用(如方法名、字段名)替换为直接引用(内存地址)。
初始化 (Initialization)执行类中的 Java 代码。执行 <clinit> 方法:这是 JVM 自动为类生成的构造器,用于执行所有静态代码块 {} 和为静态变量赋予我们在代码中设定的初始值。这个阶段是线程安全的。

类的加载

类加载器

Java中的类加载是由类加载器(ClassLoader)来完成的。类加载器负责将类的字节码加载到内存,并执行上述的类加载过程。类加载器的层次结构分为几种类型:

  • Bootstrap ClassLoader:负责加载JDK内置的类库,如java.lang.*等。这些类通常存在于JDK的rt.jar文件中。

  • Extension ClassLoader:负责加载JDK扩展目录(jre/lib/ext)中的类库。

  • Application ClassLoader:负责加载应用程序类路径(classpath)中的类,通常是项目中的classes目录或jar包中的类。

  • 自定义 ClassLoader:开发者可以自定义类加载器,用于特殊需求,比如动态加载类或从网络加载类。

双亲委派模型

双亲委派模型是Java类加载机制的核心设计模式,它定义了类加载器在加载类时的优先级顺序:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器无法完成加载时,子加载器才会尝试自己加载

⚙️模型工作流程

  1. 请求委派:当前 ClassLoader 收到加载请求。
  2. 向上委托:它将加载任务委托给其父加载器。这个过程层层向上,直到最顶层的启动类加载器 (Bootstrap ClassLoader)
  3. 尝试加载:启动类加载器检查其路径(<JAVA_HOME>/jre/lib)下能否找到并加载该类。
  4. 反馈下放
    • 成功:如果父类加载器成功加载,则直接返回 Class 对象,子加载器不再介入。
    • 失败:如果父类加载器无法加载(找不到 .class 文件),则通知子加载器。
  5. 自我加载:子加载器接到通知后,才会在自己的搜索路径中尝试加载该类。

好处

  • 避免重复加载:当一个类已经被父加载器加载过一次,子加载器就不会再加载,确保了类的唯一性
  • 保证核心 API 安全:防止核心 Java API(如 java.lang.Objectjava.lang.String)被恶意代码或自定义代码篡改

类信息存储

类加载完成后,其核心结构信息会被存储在 方法区 (Method Area),在 Java 8 及其以后,这部分被称为元空间 (Metaspace)

存储区域存储内容作用
方法区 / 元空间类模板 (Class Template)存储类的静态信息,是所有实例共享的“蓝图”。包括:



1. 类元数据 包括类的完整名称、父类名称、实现的接口、访问修饰符等。



2. 运行时常量池:类的所有常量、字面量、符号引用。



3. 字段信息:字段名、类型、修饰符。



4. 方法信息:方法名、参数类型、返回类型、方法字节码。

可以通过反射(Reflection)来访问这些信息。

例如,下面是通过反射来获取类信息的代码:

Class<?> clazz = Person.class;  // 获取Person类的Class对象
System.out.println(clazz.getName());  // 打印类名

类的卸载

当一个类不再被任何地方引用,并且加载该类的类加载器也被垃圾回收时,JVM可以卸载该类,释放方法区中的内存空间。这通常发生在应用重新部署

类的实例化

当我们使用new关键字创建对象时,JVM会执行以下步骤:

  1. 类加载检查:JVM首先检查该类是否已经被加载,如果没有则JVM 执行加载、连接、初始化,将类模板放入元空间
  2. 内存分配:在堆内存中为对象分配空间。
  3. 内存初始化:将分配到的内存空间初始化为零值(不包括对象头)。
  4. 对象头设置:设置对象头(Object Header),包含:
    • Mark Word:存储对象的hashCode、GC分代年龄、锁状态标志等
    • Class Pointer:指向类元数据的指针,JVM通过这个指针确定对象是哪个类的实例
  5. 执行构造方法:调用构造方法进行初始化。构造器是对象实例化的关键。
  6. 返回对象的引用:JVM返回一个指向新创建对象的引用,该引用通常存储在栈区的局部变量中。

通过这个类指针,JVM 才能在运行时知道这个对象(小甜饼)是由哪个类(切割机)构造的,从而能找到并执行它所属的方法字节码。

2、对象

What(什么是对象)

对象是类的具体实例,是面向对象编程的基本单元。每个对象都具有自己独立的状态(通过成员变量来描述)和行为(通过方法来定义)。在JVM中,对象是存储在堆内存中的实体

对象的核心特性:

  1. 唯一性:每一个实例都有一个唯一的身份。通过==运算符可验证身份唯一性。
  2. 状态性:对象保存实例变量的值,这些值定义了对象的当前状态。可通过get、set方法来验证。
  3. 生命周期:从new创建到GC回收,经历创建→使用→不可达→回收的过程

💡 面向对象的本质:不是"面向类",而是面向具体的对象实例进行编程。我们通过对象的状态变化和行为交互来解决问题。

How (怎么写)

// 1. 对象创建(实例化)
ClassName obj1 = new ClassName("张三", 18);
ClassName obj2 = new ClassName("李四", 20);

// 2. 访问对象状态
String name = obj1.getName();  // 获取状态
obj2.setName("李四四");        // 修改状态

// 3. 调用对象行为
obj1.sayHello();  // 输出: Hello, my name is 张三 and I am 18 years old.

// 4. 对象身份比较(内存地址)
boolean isSameObject = (obj1 == obj2);  // false - 不同对象

// 5. 对象内容比较(需要重写equals方法)
boolean isSameContent = obj1.equals(obj2);  // 通常为false,取决于equals实现

// ⚠️ 重要提醒:
// 1. == 比较的是对象内存地址(身份),不是内容
// 2. equals()默认行为同==,需重写才能比较业务逻辑相等
// 3. 重写equals()必须同时重写hashCode()(契约要求)

// 6. 对象引用传递
void changeName(ClassName obj) {
    obj.setName("修改后的名字");  // 修改原对象状态
}
changeName(obj1);  // obj1的状态被改变

Why (原理)

对象在JVM中的内存布局

当执行new ClassName()时,JVM在堆内存中为对象分配空间,布局如下:

┌────────────────────────────────────---------------------─┐
│            对象内存布局   (堆内存)                         │
├───────────────────┬─────────────────---------------------┤
│   1.对象头 (Header)│  2.实例数据      │     3.对齐补充      |
│ (12-16 bytes)     │ (Instance Data) │      (Padding)   |
├─────────┬─────────┼─────────────────┤--------------------|
│ Mark Word │ Klass │ 字段1 │ 字段2 ...│                    |
│ (8 bytes)│ Pointer│       │         │                    |
│          │(4-8B)  │       │         │       (0-7B)      |
└─────────┴─────────┴─────────────────----------------------|
  1. 对象头(Header)
  • Mark Word (标记字)
    • 存储对象的hashCode(25位)
    • GC分代年龄(4位)
    • 锁状态标志(1-2位):无锁/偏向锁/轻量级锁/重量级锁
    • 线程ID(偏向锁时)
    • CMS GC标记(垃圾回收时)
  • Klass Pointer(类指针):
    • 指向方法区中类元数据的指针
    • 通过此指针找到类的方法表、字段信息等
    • 64位JVM默认开启指针压缩(4字节),否则8字节
  1. 实例数据
  • 存储对象的实例字段(非静态字段)
  • 字段排列遵循从大到小、同类型连续原则(JVM优化)
  • 父类字段在前,子类字段在后
  1. 对齐补充
  • 保证对象大小是8字节的整数倍(HotSpot要求)
  • 纯粹为了内存对齐,无实际意义

对象的垃圾回收

  • 可达性分析:从GC Roots(栈帧局部变量、静态变量等)出发,无法到达的对象被标记为垃圾
  • finalize()机制:对象被回收前的最后机会(不推荐使用)
  • 内存释放:JVM回收堆内存空间,供后续对象分配使用

3、总结

  • 是模板/蓝图,对象是具体实例
  • 存储在方法区/元空间,对象存储在堆内存
  • 定义了对象的结构和行为,对象承载具体的状态和交互
  • 面向对象编程的本质是通过对象间的协作解决问题

🎯 核心思想

"类描述世界,对象改变世界"
类定义了可能性,对象实现了具体行为。我们编程时操作的是对象,而非类本身。理解对象的内存布局、生命周期和访问机制,是写出高效Java代码的关键。