Java类用法详解:从封装到构造方法,彻底搞懂类的设计
2026/9/9 9:16:20 网站建设 项目流程

这期我们聊一个很基础、但几乎所有初学者都会卡壳的话题:Java里的类到底该怎么用。我在带新人或者帮读者看代码的时候,经常发现很多人不是不会写语法,而是不知道一个类里该放什么、不该放什么,更不明白为什么有些字段要私有、有些方法要公开。这次借着《Java程序设计(第3版)》里的“代码练习3:类的用法”,我把类的设计思路、手写代码时的注意事项、还有我实际踩过的坑一次性讲清楚。不管你是刚学完语法准备做练习,还是已经写了一阵子代码但总觉得自己的类“不太对劲”,这篇都值得你花十分钟看完。

1. 内容整体设计与思路拆解

1.1 这个练习到底在练什么

“类的用法”这个练习,表面上是让你定义一个class、写几个字段和方法、再new一个对象出来调用一下。但如果你只做到这一步,那和没练没什么区别。这个练习真正想让你掌握的是三件事:如何把一个现实中的东西抽象成类的结构如何控制外部对类内部数据的访问如何通过方法而不是直接操作字段来改变对象的状态

我在实际带人的过程中发现,很多初学者对类的理解停留在“字段就是变量,方法就是函数,把它们放在一起就是类”。这个理解不能算错,但是太浅了。类真正的价值在于它把数据和操作数据的行为绑定在了一起,而且通过访问控制符(public、private这些)告诉使用者:哪些东西你可以碰,哪些东西你不该碰。

在这个练习里,题目通常会给你一个具体的场景,比如设计一个学生类、图书类或者银行账户类。你需要做的不是写一个“能跑就行”的版本,而是要按照面向对象的思路,把类的职责划分清楚,把不该暴露的细节藏起来,把该提供的接口做对。

1.2 为什么类不是“变量+函数”的集合这么简单

我用一个生活化的例子来解释。你把手机递给别人用,别人能操作的只有屏幕和几个按键,他不会去动手机内部的电池和主板。这不是因为他不能拆开手机,而是因为设计手机的人通过外壳和螺丝把内部细节保护起来了。类也是一样的道理。

字段就是手机内部的元器件,方法就是手机外部的按键和屏幕,访问控制符就是那个外壳。如果你把字段全部设成public,让外部代码随便改,那就等于把手机主板露在外面,谁都能碰一下。短期看没什么问题,但一旦你的程序变大,多个人协作开发,这种“裸露字段”的设计会让代码迅速腐化。

我见过太多练习作业,学生把字段写成public,然后在main方法里直接stu.score = 100这样赋值。跑起来是没问题,但一旦你需要在赋值的时候做校验(比如分数必须在0到100之间),你就得去所有赋值的地方改代码。而如果你通过一个setScore方法来做,只需要改这一个方法就够了。这就是封装的意义,也是这个练习想让你体会的东西。

2. 核心细节解析与实操要点

2.1 访问控制符的选择:用不好就是给自己挖坑

讲类的用法,访问控制符是绕不开的坎。很多教材会给你一张表,告诉你public、protected、默认(包访问权限)、private分别是什么意思。这里我不重复那套东西,我只说我在实际编码里总结出来的经验:默认情况下,字段全部private,方法尽量public,需要被子类重写的方法才考虑protected

为什么字段要全部private?我举个具体的场景。假设你写了一个图书类Book,里面有一个字段int pages表示页数。如果它是public的,那么另一个开发者在某个地方写了book.pages = -100。编译器不会报错,程序也能运行,但显然一本页数为负数的书是有问题的。如果pages是private的,你就必须在构造方法或setter里做校验,非法值根本进不来。

当然,凡事有例外。有些场景下private反而给自己找麻烦。比如一个纯粹承载数据的类(类似DTO、VO),字段全私有、每个字段都要getter和setter,这套写法其实很繁琐,Java社区后来用Lombok的@Data注解来解决这个问题。但这属于工程实践范畴,作为练习,你还是老老实实按规范来,把字段私有化,用方法对外提供服务。

2.2 构造方法的设计:不只是用来初始化数据

构造方法是初学者最容易忽略的部分。很多人写类的时候只写一个无参构造方法,或者干脆不写,用系统默认的。但构造方法的用处远不止“新建对象时初始化字段”这么简单。它还是你控制一个类如何被创建的重要手段。

我举个最常见的例子。你设计一个学生类Student,有学号、姓名、班级三个字段。如果你只提供一个无参构造方法,使用者可以new出一个什么都没有的学生对象,然后慢慢往里面填数据。这没什么不好,但如果你希望学号是必填的、且不可修改的,就应该提供一个带学号参数的构造方法,并且不为学号提供setter。这样,学号只能在创建对象时设置一次,之后谁都没法改。这是一个很经典的不变式设计。

还有一个细节很多人不知道:构造方法可以重载。你可以提供多个构造方法,让使用者根据自己手头的数据选择用哪一个。比如new Student("s001")new Student("s001", "张三", "软件工程1班")。重载不是“偷懒”,而是一种常见的编码习惯,目的是让类的使用方在不同场景下都有的选。

2.3 方法设计里的“职责单一”有多重要

我改代码的时候最怕遇到那种一个方法写了几百行、什么都干的方法。这种方法的特征是名字很大而通用,比如processData()或者handleSomething(),打开一看,里面既处理业务逻辑,又负责连接数据库,还要更新界面显示。这种代码一旦出bug,定位问题的时间是正常代码的三倍以上。

在这个练习里,你的方法大概不会复杂到写几百行,但你要养成一个习惯:一个方法最好只做一件事。比如一个图书类Library,你要实现借书、还书、查询库存三个功能,就应该拆成三个方法:borrowBook(String bookId)returnBook(String bookId)queryStock(String bookId)。不要写一个manageBook(int operation, String bookId),然后根据operation参数去判断干什么。这种设计在练习阶段可能感觉无所谓,但工作以后你会发现,好的方法设计能省去你大量调试和重构的时间。

我还会建议你写方法的时候注意方法名的可读性。命名是很主观的事,但有一个通用的标准:别人看到方法名,大概能猜到它做什么borrowBookop1好,getStudentScoregetData好。这个练习就是培养这种意识的最好时机,因为题目足够简单,你不需要分心处理逻辑复杂性,可以刻意练习命名的规范性。

3. 实操过程与核心环节实现

3.1 从零开始设计一个图书管理类:代码逐步手写

我直接用一个实际练习题目来演示。假设题目要求你设计一个简单的图书类,包含书名、作者、价格、库存数量四个属性,并且能实现“查看图书信息”“修改库存”“判断是否有库存”这几个功能。拿着这个题目,我们一步步来。

第一步,明确字段。书名和作者是字符串,价格用double或BigDecimal,库存数量用int。这些字段都应该设为private。为何价格不用float而用double?这是个小细节:float精度低,价格运算容易出误差,虽然练习题目中介不介意,但习惯上我会用double。真实项目里我更建议用BigDecimal,但那一套涉及写法篇幅太长,练习阶段double够用。

第二步,设计构造方法。这里我提供两个:一个全参构造方法,用于一次性初始化所有字段;一个无参构造方法,先造一个空对象。写全参构造方法的时候,直接给字段赋值就行,但库存数量我建议做一下限制,如果传入的数量小于0,就抛一个IllegalArgumentException

第三步,设计业务方法。showInfo()方法用于打印图书信息;addStock(int count)subStock(int count)分别用来增加和减少库存;hasStock()用来判断库存是否大于0。注意subStock里必须检查:如果当前库存不足,应该提示还是抛异常?我建议抛IllegalStateException,因为“库存不足”属于状态非法,不是参数非法。

代码写出来大概是这样的:

public class Book { private String title; private String author; private double price; private int stock; public Book() { } public Book(String title, String author, double price, int stock) { this.title = title; this.author = author; this.price = price; if (stock < 0) { throw new IllegalArgumentException("库存数量不能为负数"); } this.stock = stock; } public void showInfo() { System.out.println("书名:" + title + ",作者:" + author + ",价格:" + price + ",库存:" + stock); } public void addStock(int count) { if (count < 0) { throw new IllegalArgumentException("增加数量不能为负数"); } this.stock += count; } public void subStock(int count) { if (count < 0) { throw new IllegalArgumentException("减少数量不能为负数"); } if (count > this.stock) { throw new IllegalStateException("库存不足,当前库存:" + this.stock); } this.stock -= count; } public boolean hasStock() { return this.stock > 0; } public int getStock() { return this.stock; } }

你看这个类,外部没有任何直接操作stock字段的入口,所有对库存的修改都必须通过addStocksubStock。这样一来,非法数据进不来,库存的约束逻辑也集中在类内部,不会散落在各处。这就是封装的一个完整示范。

3.2 编写测试代码验证类的行为

类写完之后,一定要写一个测试类来验证行为。哪怕题目没有要求你写测试,我也强烈建议你在练习阶段就养成写测试代码的习惯。所谓测试代码,不一定要用JUnit那一套,一个带main方法的简单验证类就足够了。

我的做法是:新建一个BookTest类,在main方法里创建Book对象,依次调用那些业务方法,然后用打印的方式观察结果对不对。比如,先new一本库存为10的《Java核心技术》,调用showInfo()看一眼输出;调用addStock(5)后看看库存变成多少;调用subStock(20)看看会不会抛出异常;最后调用hasStock()看看返回什么。每一步都打印一条日志,这样你能非常直观地看到对象状态是如何被一步步改变的。

这一步的意义在于,你不经过一次完整的验证,就不知道自己写的类是否真的“能用”。很多人写完类,在IDE里看到没有语法错误就觉得完事了,结果运行的时候才发现逻辑有漏洞。与其等到面试或者项目里再暴露问题,不如从练习阶段就老老实实做验证。

3.3 用类图思维检查你的设计是否合理

我发现很多初学者根本没听说过“类图”这个概念,但类图恰恰是检验类的设计是否合理的最佳工具。IDE里有自动生成类图的插件,比如IntelliJ IDEA里右键点击包名,选择Diagrams,就能看到当前项目里的类之间的关系。

类图主要看什么东西?一是看字段的状态:字段都是private的吗?有没有public字段直接露在外面?二是看方法:方法数量是否合理,有没有某个类只有一个getter挨个返回字段,却没有一个真正干活的业务方法?三是看类之间的关联:这个类是否依赖了它不该依赖的东西?

这里给大家一个简单自查表:

  • 字段全部私有,需要外部访问时通过getter获取,需要外部修改时通过setter或业务方法修改。
  • 类的字段是否都有意义,是否存在“这个字段其实根本用不到”的情况。
  • 方法是否都服务于这个类本身的核心职责,有没有出现某个方法只是给别的类打工的情况。
  • 构造方法是否能保证对象创建后处于一个合法状态,也就是创建出来的对象字段值不能自相矛盾。

拿上面Book类来对照,字段全私有,方法都围绕图书信息和库存操作展开,构造方法也做了非空数的约束。这个设计虽然简单,但结构上是合格的。

4. 常见问题与排查技巧实录

4.1 为什么“对象为null”的错误总是出现在我以为自己已经初始化了之后

写过Java的人应该都见过NullPointerException,而且它几乎总是出现在最不应该出错的地方。我给一个最常见的场景:你定义一个类,里面有一个数组或集合字段,但是没有在声明时初始化。你在类的方法里直接往这个数组的元素赋值,编译不报错,一到运行就崩。

比如这段代码:

public class TestClass { private String[] items; public void setItem(int index, String value) { this.items[index] = value; } }

初看上去没毛病,但items只是声明了,并没有初始化,它的值是null。this.items[index] = value就是在往null对象上操作,当然会抛空指针异常。解决办法有两种:一种是在声明时就new String[10];另一种是在构造方法里初始化。

我把这种问题叫“只挂号不看诊”——你告诉JVM有这样一个字段存在,却没有真正把它创建出来。排查的方法是:看到数组、集合或者自定义类型的字段,先问自己一句:这个字段到底有没有被new过?这是所有Java初学者都会犯的经典错误,练第一次类的练习时踩一次,后面写任何类都会记得。

4.2 方法参数和字段重名时,值为null或0:你忘了this

还有一个频率很高的坑:构造方法或setter方法里,形参名和字段名一模一样,然后直接赋值,却没写this关键字。比如:

public class Person { private String name; public void setName(String name) { name = name; // 这里给形参自己赋值,字段完全没变 } }

这段代码里的name = name,左边的name和方法参数name指向同一个变量,它把参数的值赋给了自己,类的name字段一点没动。结果就是setName之后,你再去get,得到的是null或者初始值。

解决方式就一个:在方法内部给字段赋值时,必须加上this.,写成this.name = namethis在这里的含义是“当前这个对象”,所以this.name表示对象自己的字段。这个知识点教材里一定讲过,但因为它太基础,很多人练习的时候根本没认真用,直到出了问题才想起来。

4.3 构造方法调用别的方法时,对象还没完全创建好

有一个比较进阶的坑,面试里也经常被问到。如果你在构造方法里调用了另一个方法,而这个方法依赖子类重写过的方法,就会调用到子类的方法——但此时子类的字段还没初始化,可能全是默认值或null。这属于构造方法设计层面的隐患。

在练习阶段你大概率不会碰到这么复杂的情况,但我要提醒的是:构造方法里尽量只做字段初始化和参数校验,不要调用带有业务逻辑的方法。如果你确实需要在创建对象的过程中做一些处理,推荐用静态工厂方法或者专门的初始化方法,而不是在构造方法里“隐式调用”一堆东西。

4.4 常见错误速查表

错误现象可能原因排查与修复
字段通过方法修改后没有变化方法参数和字段重名,且未使用this关键字检查方法内部赋值语句,确认为字段加this
运行到某行时报告无法识别变量变量作用域超出声明位置或变量名拼错确认变量声明位置,检查作用域范围
对数组或集合操作时报空指针异常字段声明后未初始化,直接使用在声明处直接new,或在构造方法中初始化
new对象时提示参数数量和构造器不匹配构造方法形参与实际调用参数不一致检查构造方法签名和调用处参数列表
打印对象时输出变成地址形式未重写toString方法为类添加toString方法并返回可读字符串
编译器提示类的某个成员缺少访问修饰符未区分public/private等权限明确为每个成员添加合适的访问控制符

4.5 给初学者的几个实操建议:从练习中建立类的直觉

最后说几个我自己带学生、带新人时反复强调的建议。第一个建议是:每次写完一个类,都尝试用一句话说清楚这个类的职责。比如“Book类负责存储图书基本信息并维护库存数量”。如果这句话说不清楚,说明这个类的职责可能不够单一。第二个建议是:不要跳过练习,直接在IDE里点几下生成getter和setter。我明白生成代码高效,但练习的时候手工写一遍,能让你对哪些字段需要getter、哪些需要setter更有感觉。很多人的类之所以臃肿,就是因为一键生成了所有getter和setter,不管用不用全都暴露出去。第三个建议是:试着给同一个类写两个完全不同的版本。第一个版本把所有字段都设为public,不写任何校验;第二个版本严格封装,所有字段私有,所有修改走方法。然后对比一下,你会发现第二个版本虽然写起来麻烦,但用起来更放心,改起来也更方便。

我个人在实际操作中还有一个比较笨、但确实有用的习惯。我会故意在测试代码里写一些“不合法”的操作,比如给库存减少一个超过当前数量的值,或者给一个本不该有负数的字段赋负值,然后看我的类能不能拦住这些操作。能拦住,说明类的设计是可靠的;拦不住,说明你的封装还没到位。这个习惯我保持了很多年,后来做工程时写DAO层、Service层的类,也是用这种思路来验证边界。把练习当成真正的项目来对待,很多问题就会提前暴露在成本最低的时候。

类的用法看似简单,但它决定了你之后写所有Java代码的基本功。把封装、构造方法、方法设计和异常处理这几件事练扎实,你后面学继承、多态、抽象类、接口的时候会顺很多。反过来,如果这个练习只是把类和对象当“语法糖”一样应付过去,那后面补概念的时候可能要花好几倍的时间。希望这一篇能把“类的正确用法”讲透,你写练习的时候也按这个标准来要求自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询