深入理解类和对象:构造方法、封装、引用传递与生命周期
类和对象2这个标题放在课程体系里往往是第二讲但真正写过业务代码的人都知道这个话题够你啃很久。第一讲大家学会了“class 定义一个类型new 出一个对象”看懂了语法但一落到实际项目里问题就来了对象到底存在哪为什么这个对象传进方法里改一改外面的数据也变了static 到底该不该用封装就是把字段设成 private 吗作为一个跟代码打交道十来年的人我的建议很直接类和对象第二讲别急着学继承和多态先把对象的创建、内存布局、数据保护和生命周期这些底层机制吃透。这些东西不搞清楚后面写再多代码都是沙上建塔。这篇内容适合刚学完类和对象入门语法、准备做第一个“正经一点”的项目或者在面试前想系统补一补对象底层知识的同学。接下来我用自己实际写代码时的理解方式把这几个核心环节拆开讲。1. 对象从无到有构造方法里值得较真的细节1.1 不写构造方法系统真的会送你一个吗很多新手会有个误解觉得构造方法可有可无因为不写代码也能编译通过。确实编译器会提供一个无参的默认构造方法但这里有个特别容易踩的坑只要你写了任何一个带参构造方法默认的无参构造方法就没了。也就是说你定义了public User(String name)之后再想写new User()直接编译报错。我见过太多人栽在这个地方。写了一个带参构造然后某个工具类里还在new User()报错之后一脸茫然。解决办法要么显式补一个无参构造要么把调用处改成带参方式。个人的经验是如果你的类必然需要某些字段才能工作那就别留无参构造强制调用方必须传值这从源头上挡住了大量“对象状态不完整”的问题。1.2 多个构造方法之间怎么优雅地复用初始化逻辑业务类经常有多个构造方法一个接收全量参数一个只接收核心参数另一个可能是为了兼容旧接口。如果每个构造方法都写一遍字段赋值和校验逻辑代码会变得又臭又长。而且一旦校验规则变了你很可能只改了一个构造另外几个忘了改bug 就悄悄埋下来了。优雅的做法是让一个“主构造方法”持有完整的初始化逻辑其他构造方法通过this(...)链式调用它。以 Java 为例public class Order { private final long orderId; private String status; public Order(long orderId) { this(orderId, 待支付); } public Order(long orderId, String status) { if (orderId 0) { throw new IllegalArgumentException(订单号非法); } this.orderId orderId; this.status status; } }这里有个硬性要求this(...)调用必须写在构造方法的第一行。这不是编译器故意刁难而是因为 Java 要求先完成构造链之后才能执行当前构造方法体内的语句保证初始化顺序是确定的。另外要注意final字段必须在构造方法里被赋值。用上面的写法orderId被声明为final而在主构造方法中完成赋值后所有链式调用路径都不会漏掉它正因为this(orderId, 待支付)最终会落到有参的主构造上。所以从经验来看把必填字段设为final并用链式构造统一赋值是一种值得推广的模式。1.3 构造方法里别写太重的业务逻辑新手容易犯的另一个问题是把构造方法当成一个“初始化即干活”的地方在构造里查询数据库、调用远程接口、启动线程。看起来方便实际上后患无穷。因为在测试里 new 一个对象就要连数据库跑一遍单测可能触发一系列你根本不想触发的副作用。构造方法的职责应该限定在“让对象状态合法”。怎么理解这个“合法”就是校验参数、赋初值、建立不可变的状态。真正需要连数据库、发消息、启动后台任务的逻辑应该放到专门的方法或工厂里由调用方显式触发。这既让对象容易测试也让对象的使用意图更清晰。我实际写代码时经常把“创建对象”和“初始化外部资源”分开创建是纯内存操作资源获取则放到显式调用的方法中。2. 封装真正把数据保护起来2.1 访问权限不是只有 private 和 public很多人理解的封装就是把字段private然后生成一堆 getter/setter。封装的核心其实是“把不变量掌握在类自己手里”。不过在细讲之前先看全 JavaC# 也类似的四种访问级别很多不起眼的 bug 都是因为选错了访问级别造成的。访问修饰符同类同包子类不同包全局public可访问可访问可访问可访问protected可访问可访问可访问不可访问默认无修饰符可访问可访问不可访问不可访问private可访问不可访问不可访问不可访问我自己的原则很简单字段一律private对外暴露的方法才考虑public如果你想让某个行为只给子类扩展用protected同包可见性这个默认级别往往就是团队里两个人写不同模块时因为包结构变化导致的诡异问题来源。所以跨模块协作时我宁可多写几行访问方法也不用包可见性来偷懒。2.2 getter/setter 不是机械地生成很多 IDE 一键生成的 getter/setter 只是“字段的窗户”但封装的价值在于setter 里可以做校验和约束。比如一个学生成绩的字段public class Student { private int score; public int getScore() { return score; } public void setScore(int score) { if (score 0 || score 100) { throw new IllegalArgumentException(分数必须在0-100之间); } this.score score; } }这样写任何调用方都不可能把成绩设成 150这个约束由类自己维护而不是靠每个调用方自觉。如果你哪天发现业务里到处都在if (score 0 || score 100)那说明这个约束放错了位置应该收拢到类内部去。getter 同样需要设计。如果字段是一个集合比如ListString tags直接return tags会把内部可变引用暴露出去调用方可以student.getTags().clear()直接把对象内部数据清空。安全的做法是返回不可修改视图public ListString getTags() { return Collections.unmodifiableList(tags); }这样外部可以读取但没办法修改。不能小看这个细节线上很多数据被莫名其妙篡改的事故源头就是返回了内部可变引用。2.3 什么时候可以大胆不封装凡事都有例外。有些类本身就是“数据的搬运工”比如从接口接收参数、传给数据库或中间件的数据传输对象它们的字段天然就是公开读写的。如果这种类也做一堆 getter/setter除了代码膨胀没有别的好处。另外类内部的私有辅助类或局部使用的数据结构也不值得过度封装。我的判断标准是一个类如果承担了业务规则、状态流转、数据一致性就值得认真设计封装如果它只是“结构化的数据容器”直接暴露字段反而让代码简洁。封装是手段不是目的目的永远是保护状态的不变性。3. 静态成员属于类而不是属于对象3.1 static 变量到底存在哪static 变量也叫类变量它在内存中只占一份空间所有的对象共享这一份。而实例变量是每个对象各自持有一份。用网上的比喻就是同一个班级的白板是大家共用的static每个人的笔记本是私有的实例变量。举一个常见场景统计当前类创建过多少个实例。public class User { private static int instanceCount 0; public User() { instanceCount; } public static int getInstanceCount() { return instanceCount; } }这个计数器就属于类不能用user.instanceCount这种思路去访问因为它是类级别而不是对象级别的。访问静态成员规范的做法是通过类名调用User.getInstanceCount()。虽然 Java 也允许用对象访问静态成员但那样会产生“这个值好像属于这个对象”的误导我建议团队规范里直接禁止这种写法能少很多迷惑。3.2 为什么静态方法里不能直接访问实例成员静态方法没有this。这是一个很直白的解释。this指向的是当前对象而静态方法是挂在类上的调用时可能根本没有任何对象存在。所以你在静态方法里直接写name 张三编译器根本不知道这个name是哪个对象的。实际开发中静态方法适合放三类逻辑不依赖对象状态的计算工具方法、读取类级别状态的访问器、以及创建对象的静态工厂方法。如果写静态方法时发现自己想去访问某个实例字段大概率说明这个方法应当改成实例方法或者需要把对象作为参数传进来。3.3 用静态工厂方法替代 new是进阶的正解比new更值得掌握的是静态工厂方法。它本质还是一个构造对象的入口但相比直接用构造方法它有几个实实在在的好处方法名可以表达意图比如User.of(张三)比new User(张三)更有可读性还可以在方法里做校验和缓存实例甚至返回类的子类型。举个例子public class User { private final String name; private User(String name) { this.name name; } public static User of(String name) { if (name null || name.isBlank()) { throw new IllegalArgumentException(用户名不能为空); } return new User(name); } }注意这里构造方法被声明为private外部不能直接new User(...)只能通过User.of(...)。这样创建对象的唯一入口就是静态工厂方法所有校验规则都被收拢在一个地方。你可以看到一个很有意思的点静态工厂方法内部仍然用了new但它把“创建对象的入口”和“创建对象的细节”分离了。4. 对象的引用传递内存模型决定行为4.1 等号赋值到底复制了什么很多人以为User a new User(张三); User b a;是把整个对象复制了一份。实际上这里复制的是“引用”也就是对象在堆内存中的地址。对象本身只有一份a和b是两根同时指向它的“绳子”。用大白话说new User(张三)在堆里盖了一间房子门上写着地址User a是让a记住这个地址User b a是让b也记住同一个地址。之后你通过b把房子里的家具换了再从a进去看家具当然也是新的因为那是同一间房。4.2 方法参数Java 永远是值传递这是面试高频题也是工作里最容易犯迷糊的地方。Java 的参数传递永远是值传递只不过这个“值”是基本类型的值还是引用类型的引用地址值要看参数类型。区别在于如果方法内部修改了引用对象的属性外部会受影响如果方法内部把参数变量重新指向一个新对象外部不会受影响。public void changeName(User u) { u.setName(李四); // 外部对象的名字会变 } public void rebind(User u) { u new User(王五); // 外部引用不会变u只是本地变量被重新赋值 }我在带新人时最爱用这个例子。很多 bug 的根源是“我以为把对象换成新的了结果外面还是旧的”。理解了引用传递你就知道方法内部对参数重新赋值是无效的除非把返回值传出去重新赋值给外部变量。4.3 来一份特别的对比浅拷贝和深拷贝前面说的是引用传递的“共享”那如果确实想要一份独立的副本呢这里就要分清浅拷贝和深拷贝。先做个小实验给一个类实现clone()然后复制对象、修改副本内部的集合观察原对象是否变化。public class Cart implements Cloneable { private ListString items new ArrayList(); Override protected Cart clone() { try { Cart c (Cart) super.clone(); // 只复制了引用 return c; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } }这个clone()完成的是浅拷贝items字段指向的还是同一个ArrayList对象。你往副本的items里加一个商品原对象的items也能看到。要真正做到深拷贝需要手动把内部集合也复制一份Override protected Cart clone() { try { Cart c (Cart) super.clone(); c.items new ArrayList(this.items); // 深拷贝内部可变对象 return c; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } }写业务代码时我的原则是默认不提供拷贝。等真正需要拷贝时优先考虑深拷贝或者干脆用不可变对象。浅拷贝的“半半拉拉”状态最坑人使用方往往误以为拿到了独立副本结果数据在悄悄共享。4.4 对象共享带来的线程安全隐患对象共享另一个绕不开的问题是并发。多个线程同时读写同一个对象的状态如果没有同步轻则读到过期数据重则直接进程崩溃。这并不是只有写服务器才会碰到的问题桌面程序里多个线程同时操作同一个 UI 对象也很常见。最简单的规避手段之一就是让对象不可变所有字段final不提供 setter不把内部可变对象泄露出去。不可变对象天然线程安全因为没人能改它。如果对象确实需要在运行时变化那就得靠加锁或使用并发容器。我个人的经验是在设计阶段优先问一句“这个对象真的需要可变吗”有一半情况下答案是不需要。5. 对象的生命周期从创建到回收5.1 对象从创建到回收的全过程一个对象从new开始到变得不可达再到被垃圾收集器回收这个过程比你想象的长。对象刚创建时肯定在堆里栈上有一个引用指向它。当这个引用变量出了作用域或者被显式置为null对象就失去了“存活”线索变成垃圾。举个例子public void run() { ListString list new ArrayList(); // 方法结束时list 引用出栈对象成为垃圾 }这里要注意垃圾收集器回收对象的具体时机是不确定的所以你不能依赖“对象被回收时一定会执行某个方法”这种假设来做关键操作比如释放数据库连接、关闭文件流。这类资源必须用显式的close()或 try-with-resources 来保证释放而不是等着垃圾回收来救你。5.2 空指针异常的三个最常见来源对象生命周期和引用传递的话题最终都会汇聚到一个具体症状上空指针异常。我总结下来空指针的来源无外乎三种方法返回了null但调用方没判断、从集合里取元素没判断是否存在、隐式自动拆箱出现空值。排查这类问题先靠堆栈信息定位到行号再看这段代码里哪个变量可能为null。多说一句堆栈信息只能告诉你“在哪一行崩了”它不会告诉你“为什么变成 null”。所以排查时不要纠结崩溃那行代码应当往上去看变量的来源看看是哪一次方法调用把null传进来的。5.3 资源释放对象不只是构造那么简单构造方法对应对象的诞生那对象销毁呢垃圾收集器会回收内存但回收不了文件句柄、网络连接、数据库会话这类操作系统资源。实际开发的惯例是凡是需要显式释放资源的类就实现一个关闭方法并用语法糖确保调用。Java 里的 try-with-resources 就是一个典型try (Connection conn dataSource.getConnection(); PreparedStatement stmt conn.prepareStatement(sql)) { // 执行查询 }这段代码最大的价值是把释放逻辑交给 try 结构去管理即使中途抛异常资源也会被正确关闭。很多线上“连接泄漏”问题根源就是有人手动new了一个连接对象用完之后随意丢给垃圾回收从来不关。所以对象生命周期的话题本质上是要记住两件事内存由垃圾回收负责非内存资源必须显式释放。6. 实战经验彩蛋设计对象时先在脑子里过一遍内存图最后分享一个我自己的习惯。每当我定义一个比较复杂的类或者准备把某个对象传给别的方法之前我都会先在脑海里画一张内存图这个对象在堆里是哪一块哪个变量持有了指向它的引用方法参数传进去的是引用还是副本有没有可能造成共享对象的生命周期大概多久。别看这个习惯不起眼它能提前消灭一大批 bug。你可以这样练习随便找一段代码把一个对象的创建语句、方法调用、传参过程都标注出来研究每一步在内存里发生了什么。练过那么十几次你就不会再被“为什么这里变了那里没变”这种问题困住了。这一讲的内容到这里已经覆盖了构造方法、封装、静态成员、引用传递和生命周期这几个核心环节。下一讲可以放心去啃继承和多态了因为你现在已经知道对象是怎么活的也清楚它的边界在哪里。写代码的时候多想想你创建的这个对象在内存里是什么样子很多棘手问题就没那么难了。

相关新闻

Tidewater海洋渲染原理:四重级联FFT(Tessendorf)在WebGPU Compute上的快速实现

Tidewater海洋渲染原理:四重级联FFT(Tessendorf)在WebGPU Compute上的快速实现

【免费下载链接】tidewater Coastal town built with Opus 5.5 项目地址: https://gitcode.com/gh_mirrors/ti/tidewater 点击查看 免费下载 Tidewater 是一个直接在浏览器运行的海岛钓鱼游戏,它用 WebGPU WGSL 手写了一套渲染引擎,海洋渲染…

2026/10/14 23:45:59 阅读更多 →
软件测试全栈知识库:9大领域与成长路线指南

软件测试全栈知识库:9大领域与成长路线指南

最近翻团队内部的简历池,发现一个普遍现象:很多同学写“熟悉软件测试”,但若细问AI测试怎么做、性能分析从哪个指标开始看,往往就只剩下一句“了解一点”。测试岗位发展到今天,“软件测试全栈知识库”已经不是培训机构…

2026/10/14 23:45:59 阅读更多 →
单目3D目标检测实战:ROI-10D的6D位姿与度量形状回归

单目3D目标检测实战:ROI-10D的6D位姿与度量形状回归

1. 项目背景与核心问题拆解1.1 单目3D目标检测到底难在哪做自动驾驶感知或者机器人抓取方向的朋友,大概率都碰过这样一个问题:手头只有单目RGB相机,却想拿到目标物体的完整6D位姿和真实尺寸。这件事的难度,不在于“检测”本身&…

2026/10/14 23:45:59 阅读更多 →

最新新闻

ECharts柱状图配置全解析:从基础到动态排序与性能优化

ECharts柱状图配置全解析:从基础到动态排序与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 1:57:11 阅读更多 →
LVGL 图像按钮(lv_imgbtn)实战指南:Simple Image button 示例全解与源码原理

LVGL 图像按钮(lv_imgbtn)实战指南:Simple Image button 示例全解与源码原理

嵌入式物联网智能硬件嵌入式GUI硬件开发 【免费下载链接】HoloCubic 带网络功能的伪全息透明显示桌面站 项目地址: https://gitcode.com/gh_mirrors/ho/HoloCubic 点击查看 免费下载 本篇文章围绕 HoloCubic 固件所采用的 LVGL 图形库中的 lv_imgbtn(图…

2026/10/15 1:57:11 阅读更多 →
深入解析 LAMMPS `compute msd/chunk`:逐块均方位移的计算原理与实战配置

深入解析 LAMMPS `compute msd/chunk`:逐块均方位移的计算原理与实战配置

科研科学计算高性能计算 【免费下载链接】lammps Public development project of the LAMMPS MD software package 项目地址: https://gitcode.com/gh_mirrors/la/lammps 点击查看 免费下载 导读 compute msd/chunk 是 LAMMPS 中用于按 chunk(原子分组…

2026/10/15 1:57:11 阅读更多 →
车机双系统下用ASAN抓内存踩踏:8155p上Android.mk与Android.bp配置实战

车机双系统下用ASAN抓内存踩踏:8155p上Android.mk与Android.bp配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 1:57:11 阅读更多 →
Go 1.27.1 内存局部性调优实录:压榨 CPU 缓存行命中率的工程实践

Go 1.27.1 内存局部性调优实录:压榨 CPU 缓存行命中率的工程实践

在微服务吞吐量达到数万 QPS 的高性能网关或高频事件调度引擎中,单纯依赖算法时间复杂度(如将 $O(N)$ 降为 $O(1)$)已经无法进一步带来数量级的性能突破。很多底层服务开发者发现,即便 CPU 利用率跑满了 100%,服务处理…

2026/10/15 1:57:11 阅读更多 →
Start Bootstrap 主题 One Page Wonder 详解:基于 Bootstrap 4 快速构建单页网站

Start Bootstrap 主题 One Page Wonder 详解:基于 Bootstrap 4 快速构建单页网站

前端文档 【免费下载链接】startbootstrap A library of free and open source Bootstrap themes and templates 项目地址: https://gitcode.com/gh_mirrors/st/startbootstrap 点击查看 免费下载 One Page Wonder 是 Start Bootstrap 开源仓库中的一款经典单页主题…

2026/10/15 1:56:09 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →