一批人栽在同一道题上关于Person类练习的复盘刚带完一轮Java基础班这批学完循环和数组、刚踏入面向对象大门的初学者遇到的第一道像样的练习几乎都是“编写一个类Person”。题目本身确实不难一个字符串类型的名字name、一个字符类型的性别gender、一个整型的序号number再加若干方法。但让我有点意外的是这道题的正确率并没有想象中高。好多同学把代码敲出来、编译通过、运行也没报错可你把代码拿过来一问为什么name要用String为什么属性要写成私有的构造方法里的this到底干嘛用的他答不上来。这道练习表面上是在“定义一个类”实际上练的是Java面向对象里最核心的一整套基本功类与对象的关系、字段的可见性设计、构造器的重载、this关键字、封装思想以及“字段-构造器-方法”三段式的标准类写法。这篇就来复盘一下我在实操中带大家完成这道题时反复强调的东西、踩过的坑以及我建议每个人都做一遍的扩展练习。1. 这个练习到底在练什么——不要小看三个属性的设计题目给的信息很克制属性就三个字符串类型的名字name、字符类型的性别gender、整型的序号number。没有给方法也没说用什么修饰符。很多人的第一反应是“这不就照抄吗”但恰恰是这种看起来简单的东西最能暴露基础扎不扎实。先说第一个问题为什么这些属性用不同的数据类型name是“字符串类型的名字”那为什么不是char[]Java里的String本身是对字符数组的封装而且做了不可变设计直接赋值、直接比较、直接拼接比手动维护一个char[]安全得多。gender用char这是题目刻意设计的点——在Java里char是16位的Unicode字符类型它不仅支持英文字母也支持男、女这样的中文字符。很多初学者惊讶于“char还能存中文”其实就是因为Java的char在内存里存的是Unicode编码不是单纯的ASCII单引号里放一个中文字符完全合法。而number用int就是一个普通的32位整数用于编号、计数这类场景范围足够又省内存。其次是可见性。题目只说“包含属性”没有说public还是private但任何一本正经的Java教材这个练习的核心目标就是让你体会“封装”——把属性和操作属性的方法打包在一个类里对外隐藏字段通过方法访问。所以这三个属性无一例外都要写成private。你可能会想练习程序就我自己用写成public不是更省事我在初学的时候也这么干过直到后来被同事review代码时问了一句“你这个字段直接暴露别人随便就能把number改成负数你怎么拦住”我才真正理解封装不是形式主义的条条框框而是一种可控性一旦属性暴露在外任何调用方都能随意修改你连校验的机会都没有。这道练习放在这个时间节点也是有讲究的。前面的语法阶段你写的是“过程式”代码main函数从上到下执行定义变量、写循环、调方法。而Person类要求你换一种组织代码的思路数据和操作数据的方法应该被绑在一起。比如“显示一个人的信息”在过程式里是写一个printPerson(name, gender, number)函数把三个变量传进去在面向对象里则是Person类自己提供一个showInfo()方法对象对自己负责。这个思维转变比记住语法重要一百倍。练习的适合人群也很明确刚学完Java基础语法第一次接触构造器、方法重载、this关键字的学习者。如果你正处于这个阶段别急着去刷各种算法题把这道练习从头到尾吃透后面学继承、集合、框架都会顺畅不少。2. 从零到能跑Person类的完整实现过程2.1 第一步属性定义与初始值问题先写最基础的部分。新建一个文件命名为Person.java,注意文件名和public类名必须完全一致包括大小写。在类体内部首先声明三个私有属性public class Person { private String name; private char gender; private int number; }这里有个很隐蔽的知识点也是面试里爱问的三个属性都“还没赋值”那它们的默认值是什么Java对类成员变量会自动初始化引用类型String的默认值是nullchar的默认值是\u0000可以理解为Unicode的空字符输出时看不到内容int的默认值是0。这个知识点直接决定了无参构造器的意义——通过无参构造器创建的对象它的属性就是这些默认值需要后续用setter方法填充。注意这个“默认初始化”只对成员变量生效。如果是在方法内部定义一个局部变量却不赋值编译根本过不去会报“variable might not have been initialized”。很多初学者在这里被坑过——明明在类里写int number;没事方法里写int number;再用就直接报错。原因就在上面这句。2.2 第二步构造方法——一个类里的“初始化关卡”属性定义好之后题目没写方法但一个标准类不能没有构造器。构造器是创建对象时必须执行的特殊方法用来把属性设置成用户希望的样子。我一般要求大家写两个构造器一个无参构造器一个带全部三个参数的有参构造器。public Person() { } public Person(String name, char gender, int number) { this.name name; this.gender gender; this.number number; }两个构造器共存的现象叫作“构造器重载”方法名相同参数列表不同一个是无参一个是三参Java根据创建对象时传入的参数决定调用哪一个。为什么必须有无参构造器很多人不理解。我举个例子当你用Person p new Person();创建对象后你期待的是一个“空人物”它的属性全部是默认值然后再通过setter逐步赋值。如果没有无参构造器编译直接报错。Java编译器有一个规则如果你在类里什么都没写它会自动生成一个无参构造器但一旦你手写了任何一个带参构造器那个默认的无参构造器就“消失”了。所以要么你写一个无参构造器要么调用有参构造器创建对象否则就编译失败。再来说this关键字。有参构造器里形参也叫name、gender、number和成员变量重名。Java在这里的变量解析规则是就近原则局部变量优先。如果不加thisname name就是把形参赋值给形参自己成员变量根本没被设置。this.name表示“当前对象的name成员变量”this指代那个正在被创建的对象。这个理解很重要后续setter方法里同样要用到。2.3 第三步getter/setter——封装的落地属性是private的外面访问不到那就提供公共的访问方法。这就是Getter和Setter名字遵循JavaBean规范get属性名()负责返回属性值set属性名()负责修改属性值属性名首字母大写。习惯上boolean类型的getter写成isXxx()但这里三个属性都不是boolean所以用get开头。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) { this.number number; }封装带来的第一个好处是“可控”。比如setNumber里可以加校验如果传入的number小于等于0拒绝赋值并打印提示。如果不走setter在public字段上你根本插不进这种逻辑。第二个好处是“隔离变化”。将来如果number类型从int升级成long你只需要改Person内部和setter/getter所有调用方都不用动——他们只依赖方法不依赖字段。这时有同学会问那我直接用p.name不就行了写这么多getter/setter不累吗累但值得。等你用到框架的时候会发现几乎所有主流框架都默认你遵守这个JavaBean规范字段private、提供getter/setter。现在养成习惯后面学框架的时候你会感谢自己。另外现在主流IDE都有自动生成功能右键Generate就能一键生成getter/setter和构造器但我不建议初学者一上来就用自动生成。至少前十个类请手写你只有在手写时才会记住这个关键字用在哪、那个命名规范长什么样。2.4 第四步行为方法showInfo一个只存数据的类叫“数据类”没有行为。题目虽然没有明确要求但通常这类练习都会有“显示信息”的方法我教学生用showInfo或者重写toString两种方式。先提供一个简单的showInfopublic void showInfo() { System.out.println(名字 name 性别 gender 序号 number); }这个方法的本质是“让对象自己把自己打印出来”。你不需要在main里手动拼接对象内部字段只需要调用p.showInfo()对象自己把家底介绍一遍。这种“对象负责自己的行为”的思维正是面向对象和过程式编程的核心差异。字符串用双引号字符男、女用单引号符号别用英文半角务必去IDE或文本编辑器里检查一遍。3. 实操现场测试类怎么编、怎么跑、怎么验证写到这里代码还只是“躺”在Person.java里的一个类没有入口。一个类必须有调用方跑起来才有意义。再创建一个PersonTest.java用来验证Person类的功能。public class PersonTest { public static void main(String[] args) { Person person1 new Person(); person1.setName(小王); person1.setGender(男); person1.setNumber(1); person1.showInfo(); Person person2 new Person(小周, 女, 2); person2.showInfo(); } }这个测试类里故意用了两种创建对象的方式对照意义很强。person1走的路径是无参构造器创建 → 对象处于默认值的“空白状态” → 连续调用三个setter逐个赋值 → 调用showInfo查看结果。person2走的是另一条路一行代码直接调用带参构造器三个参数一步到位。两条路殊途同归最后输出都是两个完整的Person信息。编译运行时在命令行或终端里进入两个java文件所在的目录顺序执行javac Person.java PersonTest.java java PersonTest这里有个易错点javac可以一次编译多个文件但java命令后面跟的是包含main方法的类名——这里是PersonTest不是Person。有人会在第二步写java Person然后报“Main method not found in class Person”当场蒙圈。还有一点如果你在IDE里运行IDE会自动帮你编译但请你至少做一次命令行编译。命令行编译能让你亲眼看到.class文件的产生理解“编译和运行是两件事”javac把.java变成.class字节码java再把.class加载进虚拟机执行。不理解这层后面遇到“找不到或无法加载主类”这类报错会非常痛苦。运行时输出如下名字小王性别男序号1 名字小周性别女序号2看到输出正常就算一把过我的建议是不够。这只是“能运行”你还要主动改代码去“测”几个边界情况。比如Person p3 new Person(小赵, 女, -1); p3.showInfo();如果setNumber里没有校验这个p3对象就带着一个荒谬的序号-1成功出生了。但现实业务里这根本不该发生。看到这个结果你就能理解为什么我在2.3小节强调“setter里可以加校验”——这不是多余而是现实需求。甚至可以说setter里的校验才是封装思想最精彩的体现。我再给一段带校验的版本供你参考public void setNumber(int number) { if (number 0) { System.out.println(序号必须为正数本次赋值已忽略); return; } this.number number; }注意这里校验失败后只是打印提示并直接return没有抛出异常。在入门阶段这个写法足够直观但你心里要清楚真实项目中更规范的做法是抛IllegalArgumentException让调用方感知错误而不是静默忽略。这个知识点后面学异常处理时会正式接触到。4. 常见报错与排查技巧我把新手现场踩过的坑整理了一遍这部分是实战经验的大头。我在这道练习上见过的报错翻来覆去就那么几个列出来给各位排雷。4.1 编译报错找不到符号cannot find symbol最常见的三个原因。第一个是拼写错误把String写成string把System写成systemJava严格区分大小写方法和变量名也是。第二个是没有定义某个方法就调用这通常发生在Person类和PersonTest类之间——测试类里调用了getName()但Person类里根本没写这个方法那编译器当然“找不到符号”。第三个是作用域问题在main方法里直接访问Person的private字段比如p.name编译器也会报这个错——因为你违反了封装规则。这个报错信息很长但真正有用的信息是“哪一行、什么符号”先看行号再找符号。4.2 静态上下文问题non-static method getName() cannot be referenced from a static context这条报错一般出现在main方法里不加对象直接写getName()的情况。main是静态方法它不依赖于任何对象就能执行但getName是一个实例方法必须通过某个对象.get才能调用。你可以把一个类理解成“模具”对象才是“产品”。静态方法相当于工厂车间里的公共设备直接就能用实例方法必须等产品造出来才能用。解决办法很明确先Person p new Person();再p.getName();。4.3 中文字符乱码与编译编码问题用命令行编译时如果源文件里有中文注释或中文字符串编译可能报“编码GBK的不可映射字符”。这跟Java源文件的字符编码有关Windows中文系统下文件默认可能是GBK而javac默认也用平台编码读取但如果文件是UTF-8编码保存的就容易冲突。老大难的解决办法是统一要么把源文件用带BOM的UTF-8保存再指定javac -encoding UTF-8编译要么在IDE里把项目编码统一成UTF-8。如果只是交作业最简单方式是保证编辑器右下角显示UTF-8后编译时加一行参数javac -encoding UTF-8 Person.java PersonTest.java这个问题非常容易忽视我见过太多人代码逻辑全对、输出乱码最后发现只是保存编码不一致。4.4 单引号与双引号的混用gender是char类型赋值男用单引号name是String类型赋值小王用双引号。两者绝对不能混用男表示一个字符男表示一个字符串对象。有初学者把setGender(男)传进去编译直接报“incompatible types: String cannot be converted to char”。这其实还是类型意识不牢的问题——String和char是两个完全不同的概念前者是引用类型后者是基本类型。这个例子我每次都要讲。4.5 一个文件里写了多个public类有人把Person类和PersonTest类的代码都写进同一个Person.java文件还在同文件里写了两个public class。Java规定一个.java文件只能有一个public类而且这个public类的类名必须等于文件名。两个public类不可能共存于一个文件。解决办法也很简单按约定拆成两个文件Person.java和PersonTest.java各管各的类。这个规则刚学时觉得很烦但习惯了就好了而且它在大型项目里避免了文件与类对不上号的混乱。4.6 对象数组导致的小坑等这个练习做顺了很多人会尝试写一批数据“一个班有10个人”。于是写出类似代码Person[] persons new Person[10]; for (int i 0; i persons.length; i) { persons[i].setName(同学 i); }运行到persons[i].setName(...)时报空指针异常。原因很简单new Person[10]只是创建了一个能装10个Person引用的数组数组里每个元素一开始都是null你还没创建任何Person对象就直接往null上调用方法当然报空指针。正确做法是在循环体里先persons[i] new Person();再调setter。这个坑我每次都特意让学生踩一次因为空指针异常是Java开发里出现频率最高的运行时异常你早晚要和它打交道不如早点和它见个面。5. 别停留在“能跑”给Person类做一轮扩展你能学到更多这道练习做完、编译通过、运行正常只是及格线。接下来我建议根据自己的节奏做几个扩展每一个扩展都能牵引出一片新的知识点。5.1 重写toString方法showInfo的问题在于它不是Java官方的“对象描述”接口。如果你直接System.out.println(p);输出的是一段不太友好的内存地址类似Person15db9742。Java里所有类都继承自ObjectObject有一个toString()方法默认返回“类名十六进制哈希值”。你可以在Person类中重写它Override public String toString() { return Person{name name , gender gender , number number }; }加Override注解是个好习惯它让编译器帮你检查如果父类根本没有这个方法、或者方法签名写错编译器会直接报错而不是让你在运行时困惑。重写之后再System.out.println(p);就能输出自定义的描述信息。很多框架在日志打印、调试信息时都是依赖toString的。这个扩展会让你第一次接触到“继承”和“重写”这两个面向对象的核心概念。5.2 按序号判重重写equals方法有了number这个属性一个很自然的场景是两个Person对象的number相同我们应该认为它们是同一个人。但默认情况下Java判断p1.equals(p2)用的是Object的equals它等价于比较的是内存地址。两个分别new出来的对象即使所有属性都一样也不相等。为此你需要重写equals方法Override public boolean equals(Object obj) { if (this obj) return true; if (obj null || getClass() ! obj.getClass()) return false; Person person (Person) obj; return number person.number; }这里牵扯出“和equals的区别”“为什么重写equals通常也必须重写hashCode”等一连串经典面试题。能把这个扩展做明白说明你已经不是停留在“写代码”层面而是开始思考“对象如何比较、语义是什么”。5.3 让Person可排序实现Comparable接口题目里给序号number天然暗示了排序场景。给Person类实现Comparable接口重写compareTo方法public class Person implements ComparablePerson { Override public int compareTo(Person other) { return this.number - other.number; } }这样你就能直接用Arrays.sort或Collections.sort对一组Person对象按编号排序。这个扩展会引入“接口”这个概念而且让你动手实现一个真实有用的接口方法——比单纯背接口定义有感觉得多。5.4 集合存储从单个对象到对象的批量管理试试把Person对象放进ArrayListPerson然后写一个循环把所有人的信息打印出来ArrayListPerson list new ArrayList(); list.add(new Person(小A, 男, 3)); list.add(new Person(小B, 女, 1)); list.add(new Person(小C, 男, 2)); for (Person p : list) { System.out.println(p); }配合上面实现的Comparable你还可以Collections.sort(list);一行搞定排序。集合和对象组合在一起基本就是Java入门阶段最常用的数据管理方式了。这个练习做好了后面学数据库操作、学框架里的对象持久化会感觉顺理成章。5.5 一个进阶思考gender用char真的够用吗这道题规定gender是char类型但在真实业务里我更推荐用枚举Enum或者一对常量来表示比如定义public static final int MALE 1; public static final int FEMALE 2;甚至直接用boolean isMale。为什么char类型输入自由调用方完全可以传x之类不合理的值校验起来也繁琐。枚举则可以在编译期就限定合法取值从源头杜绝脏数据。当然眼下作为练习请老老实实地用char把char的基本操作练熟。练习是练习生产是生产两者分别对待但心里要有这根弦。提示以上的扩展练习不要求一次全做完。如果你时间有限我会优先建议做5.1的toString其次是5.4的ArrayList版本这两个和后面框架课的关联最紧。最后说几句实在的这道练习我在不同阶段带过很多次每次复盘都会发现凡是愿意把Person类“折腾”出各种花样的同学后面学继承、集合、多态、异常处理都明显轻松而只打算“编译通过就算完”的后面一用上框架到处都是认知盲区。你说这道题本身有多难吗没有。但它是你面向对象的第一块地基——概念和代码双双过关才是真的过关。我的建议是先别用IDE的自动补全和自动生成动手把文件建出来一个字符一个字符地把代码敲进去敲完之后再故意改错几处观察编译器报什么错、为什么错最后再从零写一遍中间不看书不看笔记。能做到这一步这道练习才算真正吃透。接下来再回头去看下一道关于继承的练习你会发现眼前的路一下宽了很多。