Java Person类练习全解析:从类型选择到封装与测试

📅 发布时间:2026/10/12 21:23:41
Java Person类练习全解析:从类型选择到封装与测试
很多人第一眼看到这个练习题会觉得它简单到没什么可写——无非就是定义一个类、塞三个属性、补上 getter/setter 和构造方法新手三分钟搞定老手可能眼睛都不抬就写完了。但我见过太多人在这一道上栽跟头有的同学变量命名随心所欲有的把 gender 定义成 String有的 number 用了 long还有人压根没写构造方法就直接在 main 里 new Person() 然后报编译错误。一个看似人畜无害的 Person 类把 Java 面向对象里最基础也最关键的约定全部串了一遍属性类型怎么选、封装边界怎么划、构造方法怎么配、对象怎么真正用起来。这篇我就拿这道题展开讲透从类型选择、类设计到测试代码、排错经验一次说完。这道题本身是某高校 Java 练习册上的原题我在平时带新人、批改练习时也经常拿它作为第一道从语法走向设计的过渡题。它的完整要求是编写一个类 Person包含的属性和方法如下属性字符串类型的名字 name字符类型的性别 gender和整型的序号 number。没有多余的自由发挥空间但恰恰因为约束得这么死反而更考验你对每一个设计决策的理解程度。1. 这个练习题的真正考点不是会写类而是知道为什么这么写1.1 题目到底在考什么如果你把这个练习仅仅理解为定义一个类、声明三个属性那你交上去的代码大概长这样class Person { String name; char gender; int number; }这确实能通过编译但它离合格答案还有一大截。为什么因为题目的后半句是包含的属性和方法如下它点名了方法而一个只有属性没有任何方法的类本质上跟一个工具箱有什么区别从更深的层面讲这个练习真正想让你确认的知识点其实有四个第一你是否理解 Java 的封装原则。属性应该用 private 修饰通过 public 的 getter/setter 对外暴露而不是把属性直接摊开。第二你是否理解构造方法的作用。一个类通常需要至少一个构造方法因为外部代码创建对象时必然要经过构造这一步。如果你自己没定义构造方法Java 虽然会送你一个无参构造器但它不会帮你给属性赋初值对象的 name 就是 null、gender 就是 \u0000、number 就是 0——这显然不是想要的初始状态。第三你是否知道对象行为该以什么形式暴露。所谓的方法不只是 getter/setter还应该包括 toString()、equals() 这些从 Object 继承来的方法——你重写它们不是为了凑数量而是为了让这个类真正能被打印、能比较、能放进集合里干活。第四你是否能把一个类放到真实场景中去测试。类不是写在记事本里自我欣赏的它要被 new、被调用、被验证。很多人写完 Person 类就结束了根本不写 main 函数去跑一遍结果一运行就报空指针或者打印出来一串Person15db9742这种地址值完全看不出有没有写对。1.2 从语法正确到设计合理的距离说句实在话能编译通过和这道题算做得好之间隔着一整条设计思维。我经常和一个概念类比类定义就像一张商品设计图纸属性是原材料清单方法是加工工序而你写在 main() 里的测试代码是拿这套设计图做出来的第一件样品。样品都不做图纸再漂亮也没法证明它能用。在批改练习时我发现很多同学能写出语法全对、逻辑零分的 Person 类。比如有人定义属性时全写成了 public int age有人 getter/setter 全部手动敲但拼写错误有人 toString 不加 Override有人居然在类里面直接 System.out.println——这些都不是思路问题而是对类的职责边界没有概念。所以本篇博文我不打算先给答案而是带着你从决策过程一路走到成品最后再给出完整的、可以直接抄作业的实现同时把每一步的理由讲清楚。2. 类型不是拍脑袋选的拆解 name/gender/number 的数据类型设计逻辑2.1 字符串类型的 name为什么不是 StringBuffer 或 StringBuilder题目里明确写了 name 是字符串类型在 Java 里字符串类型默认就是指String。但我想展开一层为什么用 String而不是 StringBuffer 或 StringBuilder因为 String 是不可变的它天然适合做标识型的数据。一个人的名字不需要被反复拼接修改——它要么是张三要么是李四一次性赋值之后不再改变。StringBuffer/StringBuilder 是为频繁增删改字符串内容设计的适合循环拼接 SQL、拼接日志之类场景放在 name 属性上纯属杀鸡用牛刀还破坏了不可变性。另外题目说字符串类型的名字所以即使有人想用 char[] 来表示名字——这确实也行能省点内存——但在实际业务里没有人这么干。char[]无法直接支持equals比较内容、无法用拼接、无法调用toLowerCase()等丰富方法。String 已经是 Java 为字符串操作封好的一整套工具库你没必要自己手搓。2.2 字符类型的 gender为什么用 char 而不是 String 或 intgender 用什么类型是这道题最容易引起分歧的地方。很多现代项目里性别会用一个整型枚举0 表示未知、1 表示男、2 表示女也有的项目直接用 String 存 male/female还有的更严谨直接用枚举类型 Gender。但在这道练习里要求字符类型也就是char。你要理解这样设计的两层原因第一层是教学意图。char 是 Java 的基本类型之一练习的目的就是让你熟悉 char 的声明、赋值和比较——char 的赋值要用单引号gender 男或gender M而不是双引号。很多人在这里犯错误把单引号写成双引号编译器直接报不兼容的类型。第二层是语义取舍。char 确实不如枚举严谨但它简单、直观、内存开销极小2字节作为初学阶段的数据类型恰到好处。如果说要给这个练习加一个进阶加分项那就是把 gender 改成一个枚举类型 Gender { MALE, FEMALE, UNKNOWN }把非法值的可能性从源头掐死。但那是后话——在练习题阶段先把 char 用对才是硬道理。这里有一个实操提示如果你用gender 男要注意 Java 源文件的编码格式。如果用 GBK 编码保存文件男 会占两个字节而 char 在 Java 里恰好是 2 字节编译没问题但如果用 UTF-8 编码一个中文字符也是变长的IDE 一般会自动处理但在一些老旧的命令行环境下用 javac 编译时就很容易出现未结束的字符文字这类编码错误。所以我建议练习阶段统一用 M / F 这种英文字符省得在编码上浪费时间。2.3 整型的 number为什么是 int 而不是 long、short、Integernumber 表示序号题目要求整型。在 Java 里整型家族有 byte、short、int、long 四种基本类型而整型最常用的代名词就是int。为什么不用 long因为序号如果只是在练习场景里从 1 数到几百int 的范围约 ±21 亿绰绰有余。为什么不用 short虽然序号可能不会超过 32767但 short 在表达式中会自动提升为 int在后续做运算时反而会带来不必要的类型转换噪音。int 是 Java 虚拟机最偏爱的类型运算效率高、表达自然、API 接受度高。这里我想多聊一句 int 和Integer的区别因为这是基础题里最容易埋的坑int 是基本类型Integer 是包装类。Person 类的 number 用 int意味着这个属性永远不可能为 null——对象 new 出来时它自动是 0。这有好有坏好处是你不需要判空直接拿来参与运算就行坏处是你无法表达序号尚未分配这个业务状态。如果你想让 number 可以是 null比如数据库里该字段允许为空就得用 Integer。练习里没这个要求int 就是最合适的选择。我把三个属性的选型逻辑总结成一张表方便你复习属性要求的类型推荐声明方式为什么不用其他类型name字符串类型private String nameString 不可变、API 丰富不用 char[]/StringBuildergender字符类型private char genderchar 语义简单、省内存问题是单引号别写成双引号number整型private int numberint 范围足够、运算效率高不需要表达 null 时不要用 Integer注意这些为什么不用不是说你永远不能换而是作为练习你要清楚自己每一个类型选择的代价。真到了实际开发里该用枚举用枚举、该用 Long 用 Long那是另一套基于业务场景的决策体系。练习的目的是把默认选择背后的那根弦绷紧。3. 类的骨架怎么搭属性、构造方法、getter/setter 和 toString 的完整实现3.1 第一步属性必须私有封装从源头做起有了前面的铺垫属性声明就顺理成章了public class Person { private String name; private char gender; private int number; }这里的关键决策是private。为什么属性一定要私有很简单如果你把属性声明成 public外部代码就能绕过所有规则直接操作——比如person.name null、person.number -100没有任何拦截。而把属性声明成 private等于圈出了一块只有类自己能改的自留地外部想读写必须经过你设计好的方法你就有机会在里面加校验、加日志、加防御逻辑。这就是封装最朴素的意义不是给代码添麻烦而是给数据上保险。有人会说练习而已搞这么多 private 不累吗 我的回答是练习时写 private和正式项目里写 private用的是同一块肌肉记忆。如果你在练习阶段就习惯了 public 一把梭等进了真实项目再改习惯要付出的代价比现在大得多。3.2 第二步构造方法——默认构造和两个带参构造怎么取舍类里不写任何构造方法时编译器会自动生成一个无参构造器形式是public Person() { }。但这时候它什么也没做name、gender、number 全是默认值。为了让 new 出来的对象有点东西我建议实现两个构造方法public Person() { } public Person(String name, char gender, int number) { this.name name; this.gender gender; this.number number; }无参构造器保留它的意义在于有些场景下你需要先 new 一个空对象后面再通过 setter 逐个赋值比如从输入流里一行行读数据填充。带参构造器的意义在于如果创建对象时三个信息是齐的一步到位更干净。this关键字在这里的作用要重点说方法参数名和属性名都叫 name直接写name name会把两个都当成参数属性根本没被赋值。this.name指代当前这个对象的实例属性这是在构造方法、setter 里最常见的用法。如果你不理解 this可以把它理解为自己对自己说话——用来消除属性和参数之间的同名歧义。还有一个实用约定构造方法的重载顺序。我习惯把无参构造放在前面带参构造放在后面这样阅读代码时先看到最朴素的版本再看到功能完整的版本逻辑递进比较自然。你如果不喜欢这个顺序也没有编译问题但在团队协作里最好统一一个风格。3.3 第三步getter/setter 不是摆设setter 里可以加防御面向对象的惯例是每个私有属性配一对 public 的 getter 和 setter。getter 负责安全地读取setter 负责安全地写入。下面这组代码是这个练习的核心部分public String getName() { return name; } public void setName(String name) { this.name name; } public char getGender() { return gender; } public void setGender(char gender) { this.gender gender; } public int getNumber() { return number; } public void setNumber(int number) { if (number 0) { throw new IllegalArgumentException(序号不能为负数: number); } this.number number; }注意我在 setNumber 里加了一行防御序号如果是负数直接抛IllegalArgumentException。这是练习之外的加分动作——它展示了写 setter 不只是一个赋值动作而是一个数据校验时机的设计思想。你想想如果没有这个校验外部代码person.setNumber(-5)也能通过编译程序在后续逻辑里用这个负数去索引数组就会产生极其难查的隐蔽 bug。而 setter 里的一个 if 判断就能把问题暴露在发生的那一刻。当然题目没有要求你写 setter 校验你可以保持最朴素的写法。但如果你打算在练习之外再往前走一步这个校验就是很好的进阶预告。3.4 第四步重写 toString()让对象能被看清不重写 toString() 的话你打印 Person 对象时会得到类似Person1b6d3586的一串字符这是对象的哈希地址不是你想看的张三 男 1这种可读信息。所以几乎所有的实体类都应该重写 toString()Override public String toString() { return Person{name name , gender gender , number number }; }Override注解的作用是告诉编译器我要重写父类的方法如果你方法签名写错了比如写成public String toString()但拼错成tostring编译器会立刻报错提醒你根本没有这个方法可以覆盖比闷头运行后找不到问题要高效得多。这也是我批改作业时最常看到同学们漏掉的关键点——不是忘了写这个方法就是忘了加注解。toString() 的输出格式还有一个讲究如果你希望它更贴近人话可以直接返回姓名张三性别男序号1这样打印出来一眼就能看懂如果你希望它贴近调试和日志的场景更推荐Person{name张三, gender男, number1}这种类似 JSON 的格式结构清晰、字段名一目了然。两种都行但至少输出里要能同时看到属性名和属性值否则排起错来非常痛苦。4. 光有类还不够写一个能跑的 main()把 Person 真正用起来一个 Person 类写得再漂亮如果没人去 new 它它就像一辆没通电的展车。所以你还需要一个测试类。放在同一个文件里或者单独建一个 Main 类都可以我建议单独建一个类把业务定义和测试入口分开这也是一种好的习惯。public class Main { public static void main(String[] args) { // 使用带参构造方法创建对象 Person person1 new Person(张三, 男, 1); // 使用无参构造方法创建对象然后通过 setter 赋值 Person person2 new Person(); person2.setName(李四); person2.setGender(女); person2.setNumber(2); // 使用 getter 读取属性 System.out.println(person1.getName() 性别 person1.getGender() 序号 person1.getNumber()); // 直接打印对象会调用重写后的 toString() System.out.println(person1); System.out.println(person2); // 测试 setter 的防御校验 try { person1.setNumber(-1); } catch (IllegalArgumentException e) { System.out.println(捕获到异常: e.getMessage()); } } }这段代码的输出应该是张三性别男序号1 Person{name张三, gender男, number1} Person{name李四, gender女, number2} 捕获到异常: 序号不能为负数: -1你可以看到两种创建对象的方式都跑通了getter 读取正常toString() 输出变得可读setter 校验的异常也被正常捕获。到这里这个练习题才算真正做完——不是写完类而是类对、测试通过、行为可验证。有一个经常被忽略的细节如果你把 Person 和 Main 放在同一个文件里那么这个文件的名字必须和 public 类同名。也就是说public class Person必须在Person.java中定义而Main类可以是文件里的第二个类不能也是 public。新手经常在这里踩坑把两个 public 类写在一个文件里编译器直接报错类 X 是公共的应在名为 X.java 的文件中声明。我建议干脆分两个文件——Person.java 和 Main.java干净利落没有历史包袱。5. 从练习到实战给 Person 加上比较逻辑push 到集合里玩起来5.1 equals/hashCode让两个 Person 能按内容比较比较的是引用地址这对对象来说意义不大。你真正想要的是两个 Person 内容相同就认为它们是同一个 Person。这时你需要重写 equals() 和 hashCode()。不需要你手动去实现算法IDE 都可以一键生成但你要理解生成出来的逻辑Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; Person person (Person) o; return gender person.gender number person.number Objects.equals(name, person.name); } Override public int hashCode() { return Objects.hash(name, gender, number); }为什么要同时重写 equals 和 hashCode因为 Java 里有个约定两个对象 equals 相等hashCode 也必须相等。如果你只重写 equals 而不重写 hashCode那么把 Person 放进 HashSet 或 HashMap 时就会出问题——两个内容相同的 Person 会算出不同的哈希值被放进不同的桶里HashMap 中明明内容相同的 key 却能同时存在查也查不到。练习阶段我建议你把 equals/hashCode 也一并写出来这会把你的代码从能交作业提升到具备集合可用性。5.2 按 number 排序一个最简单的排序场景有了 Person 类之后最常见的操作就是一群 Person 排个序。比如按照 number 从小到大排。有两种写法一种是让 Person 实现ComparablePerson另一种是单独写一个ComparatorPerson。练习题没要求但作为扩展我推荐你掌握后一种因为它更灵活、不用改动 Person 本身的定义import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class MainPlus { public static void main(String[] args) { ListPerson people new ArrayList(); people.add(new Person(王五, 男, 3)); people.add(new Person(张三, 男, 1)); people.add(new Person(李四, 女, 2)); // 按 number 升序排序 people.sort(Comparator.comparingInt(Person::getNumber)); for (Person p : people) { System.out.println(p); } } }输出会是number从 1 到 3 依次打印。这里Comparator.comparingInt需要你引入java.util.Comparator而Person::getNumber是方法引用语法它等价于写(Person p) - p.getNumber()。对于初学者来说方法引用是个很好用的语法糖看到就能猜到意思按照 getNumber 的返回值来比较。这个扩展的核心价值在于从单个对象过渡到对象集合。这才是实际开发的常态——你写的大多数类最后都会被扔进 List、Set、Map 里折腾而不是孤零零地 new 出来看一眼就结束。6. 我在批改这类练习时最常看到的坑以及我的几点个人经验6.1 陷阱一把类名写成 person 或 class 关键字冲突Java 规范里类名要大写开头这是约定俗成的强规则。如果你写成class person编译器不报错但阅读体验和团队规范都会被打破。另一方面变量名不能用关键字比如你不能写String class abc因为 class 是 Java 的关键字。别看这些是基础我在批改时真的见过有人把 Person 的文件名存成了person.java然后在另一个文件里用new Person()结果编译直接报找不到符号。6.2 陷阱二gender 的赋值符号用错char 类型的赋值必须是单引号gender 男。有人会不小心写成gender 男编译器会报不兼容的类型: String 无法转换为 char。还有一种常见错误是写了gender 空字符这也是非法的——char 至少得有一个字符。如果你确实想表示性别未知可以定义private char gender U;或者在 setter 里允许传入 U而不是绞尽脑汁造一个空 char——它不存在。6.3 陷阱三没有测试入口或者测试入口和 Person 互相干扰我见过的最普遍的问题是在 Person 类里直接写public static void main(String[] args)然后把测试逻辑全写在里面。这不算错但它模糊了类的定义和类的使用之间的边界尤其是当测试代码越写越多时Person.java 会变成一个四不像的大杂烩。所以我更推荐单独建一个 Demo 或 Main 类来做测试。另一个和入口相关的问题是包名没有对应目录结构。你在 Person.java 第一行写了package com.demo;却又把它放在默认包的目录下javac 会报错。如果你用 IDEIDEA、Eclipse这类问题通常会被自动处理但如果你用命令行javac Person.java Main.java来编译就要格外注意包目录和文件路径的一致性。6.4 一点个人的经验谈这道题我自己也给学生讲过很多轮有几条建议每次都会提在这里也分享给你第一从第一次写类开始就坚持 private 属性 getter/setter不要觉得麻烦。你可以做一个对比实验把属性改成 public写一百个 Person 对象看看表面省了几行代码但等到你哪天真出了数据错误你会发现根本无法确定是谁在哪个角落偷偷改坏了它。第二写完类之后一定要跑一个至少覆盖创建、读取、打印、修改全流程的测试代码。哪怕只是十行 main 方法也能帮你把 70% 的语法错误和逻辑错误暴露出来。很多人学到后面越来越怕环境问题其实哪来那么多环境问题大多是连对象都没成功创建就开始调试了。第三不要满足于题做完试着给自己追加两个小任务一个是把 gender 从 char 升级成枚举看看代码会怎么变另一个是给 Person 增加一个birthYear属性和一个age()方法然后让 age 基于当前年份计算。这两个小改进会立刻让你体会到设计模式是怎么从这些细节里长出来的。这道题虽然叫练习题目8但它背后暴露的是一个完整的思考链路从类型选择、封装、构造、行为方法到对象生命周期、集合使用、编码规范、代码组织方式。把这套链路走通后面不管你再写 Student、Employee 还是 Order都只是在换皮罢了。