坐标点几乎是最容易上手的面向对象练习对象了。一个 Point 类,只涉及 x、y 两个字段,却能把私有、getter、setter、构造重载、数据校验这些基础概念串成一条完整的学习链路。我见过很多人一上来就给所有字段加 public,等到参与实际项目才发现,数据被谁改的、在哪一步被改坏,全靠人肉排查。学 private 和 getter/setter,Point 类正好是那个最小但最完整的练习样本。这篇文章我会按自己的实操经验,把设计 Point 类的完整流程拆开讲,从需求清单、字段私有化、getter/setter 规范写法,到 setter 里加业务校验、Python property 的替代实现,以及初学者最容易踩的坑,全部过一遍。1. 这个练习到底在练什么1.1 Point 类的角色与定位在几何里,Point 就是一个坐标点,x 和 y 决定位置。到了程序里,Point 既可以是单纯的数据载体,也可以是一个带计算逻辑的坐标工具。它非常适合拿来练习面向对象基础,因为边界足够清晰,不用牵扯数据库、文件 IO、网络传输这些外部因素。你只需要回答一个问题:这个点的数据从哪进、从哪出、能被谁改。我在参与一些模拟项目时,经常用 Point 来演示封装的价值。有个跨平台系统里,屏幕坐标和逻辑坐标要做互转,最初实现把所有字段直接公开,结果某天发现某处的换算结果总是偏了几个像素。排查下来,是某个模块跳过转换方法、直接改了底层坐标。这类问题如果一开始就做成私有字段加 getter/setter,在编译期就能拦住一大半。所以,Point 类看着简单,练的其实是访问控制这种底层设计能力。一个点可以被任意改,和只能通过特定方法改,代码的安全性和可维护性完全不是一个等级。1.2 私有字段是封装的第一课封装这个词写起来很抽象,落到代码上,第一件事就是给字段标上 private。但要注意,私有化不是把数据锁死,而是强制所有读写都走方法。这样做的直接好处是,你可以在方法里随时加日志、做校验、触发联动,不需要让调用方改任何代码。举个例子,假设某个图形编辑系统里,屏幕允许的最大坐标是 10000。如果 x 是 public,你没法保证每个调用方都记得检查边界,总有人会漏;但如果所有写入都经过 setX(double x),你只需要在 setX 里加一次范围判断,整个系统的坐标安全就全部收口了。这就是封装的真正含义,把复杂留在内部,把简单留给外部。1.3 先列需求清单再动手很多初学者拿题就写,写到一半发现缺这个缺那个。正确的做法是先列一个需求清单,把 Point 类要具备的行为都列出来:需求说明表示坐标x、y 两个字段初始化无参构造、带参构造读取坐标getX()、getY()修改坐标setX()、setY()计算距离distanceTo(Point other)格式输出toString()判断相等equals()、hashCode()清单列完之后,写代码基本不会漏。而且你会发现,需求越清晰,类结构就越稳定。很多看起来高级的设计,本质都是在需求还没想清楚的时候,提前把方法架得很复杂,结果反而让自己的代码越来越难懂。Point 这种小类,更应该一开始就把职责定清楚。2. 手写一个干净的基础版 Point 类2.1 选 Java 还是 Python语言怎么选,取决于你想重点练什么。从教学顺序上,我的建议是先花时间用 Java 手写 getter/setter,再用 Python 的 property 重新实现一遍。Java 里 private、public 是显式关键字,每个字段的访问权限一目了然,getXxx/setXxx 也是全行业通用约定。对新手来说,这种“把约定写进代码”的体验非常有帮助。IDE 自动生成也默认遵循这套命名,不会给你自由发挥的空间,反而有利于养成肌肉记忆。Python 虽然也能用 @property 模拟 getter/setter,但它的私有化更弱,下划线前缀更多是约定俗成,不会强制外部遵守,初学者容易产生“这会有什么用”的错觉。特性Java 做法Python 做法私有声明private 关键字下划线前缀约定getterpublic double getX()@property 方法setterpublic void setX(double x)@x.setter 方法外部保护编译期强检查靠约定和代码规范代码量样板较多简洁清晰2.2 Java 基础版完整代码先给一个能直接“抄作业”的版本:public class Point { private double x; private double y; public Point() { this(0, 0); } public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } @Override public String toString() { return "Point(" + x + ", " + y + ")"; } }这段代码每一行都有讲究。private double x 把字段限定在类内部可见;无参构造里写 this(0, 0),是直接复用下面的全参构造,避免初始化逻辑写两遍;setX(double x) 的参数名和字段名都叫 x,方法体里就必须用 this.x = x 区分,this 指向当前对象,this.x 才是成员变量,没有 this 的那个 x 是参数。我建议可以先用这段代码跑一个小测试:Point p = new Point(); p.setX(3); p.setY(4); System.out.println(p); // 期望输出 Point(3.0, 4.0)如果输出结果不对,优先检查是不是 setter 里的 this 丢了。2.3 构造函数为什么推荐重载很多初学者只写一个带参构造,或者只写一个无参构造。实际场景里,你有时需要一个默认原点,有时需要指定坐标,两个都提供,调用方就灵活多了。重点是默认构造里调用 this(0, 0) 这个写法。它本质上是把初始化逻辑收敛到一个位置,以后如果要在构造时加校验,只需要改全参构造,不需要在多个构造函数里同步修改。可能你刚开始觉得这没什么,但等你参与真实项目,构造参数从两个变成五个,会发现这种收敛习惯能省下大量维护成本。2.4 关于 this 的细节在 setter 里最容易出错的点,就是 this 的用法。如果你写 setX(double x) { x = x; },左侧和右侧的 x 都指向参数,字段根本没被赋值。这种情况在调试时非常迷惑,坐标值怎么改都不更新,浪费大量时间。我的排查经验是:遇到“字段改了但没生效”的问题,第一反应就是检查 setter 里是不是漏写 this。如果是 Python,还有另一个变种,就是在 setter 里给属性赋值时触发了自身递归,后面我会单独展开。3. 进阶:让 getter/setter 不再是“傻赋值”3.1 setter 是拦截非法数据的关卡有些人写 getter/setter 只是为了应付练习题,方法体就是一行赋值,这种叫“傻赋值”。但私有字段的真正价值,就是在 setter 里加业务规则。你可以在 setX 里检查数值是否合法,甚至联动其他字段。比如在图形编辑器场景里,坐标不允许超出画布范围,setter 可以这样写:public void setX(double x) { if (Double.isNaN(x)) { throw new IllegalArgumentException("x 不能是 NaN"); } if (x < MIN_X || x > MAX_X) { throw new IllegalArgumentException("x 超出边界: " + x); } this.x = x; }有人会问,这不就是加了个 if 吗?对,关键不是 if,而是“所有修改都经过同一个 if”。如果字段是 public,这个检查要么没有,要么每个调用方各写一套,漏一处就是 bug。收口到 setter 之后,拦截点只有一个,维护起来非常轻松。3.2 不要只在构造里校验有些初学者会把校验逻辑放在构造函数里,觉得生成的对象就已经是合格的了。这里有个隐患:对象创建之后,外部还会继续调用 setter 改值。如果 setter 没有校验,就等于开了一个后门。安全口诀是:进入对象的所有通道,都必须经过同一套规则。所以更稳的做法是,构造函数内部也去调用 setter,或者把校验逻辑抽成一个私有方法,构造时调用一次,setter 里再调用一次。这样既保证初始化正确,也保证后续修改正确。3.3 Python 里的 property 写法切到 Python,代码会简洁很多,但设计思路完全一样:class Point: MIN_X = 0 MAX_X = 10000 def __init__(self, x=0, y=0): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, value): if value < self.MIN_X or value > self.MAX_X: raise ValueError(f"x 超出边界: {value}") self._x = value @property def y(self): return self._y @y.setter def y(self, value): if value < self.MIN_X or value > self.MAX_X: raise ValueError(f"y 超出边界: {value}") self._y = value注意,这里真正的字段是带下划线前缀的 _x、_y,对外暴露的 x 是 property 对象。当你写 p.x = 50 时,实际调用的是 x.setter 方法,校验逻辑就会执行。下划线前缀在 Python 里就是一个“私有信号”,看到 _x 就默认别去直接碰它,要改就走公共接口。3.4 getter 也要防:返回引用副本的坑setter 管住了写入,getter 同样有风险,尤其是字段类型是引用时。比如类里有一个 List 字段:public List<String> getTags() { return tags; }外部拿到的不是副本,而是同一个对象引用。那外部完全可以在无感知的情况下往列表里加数据,把不该改变的状态改了。这种问题在 Point 这种只含 double 的类里不会出现,但我建议借这个练习把原则记住:字段是数组、集合或对象引用时,getter 要么返回不可变封装,要么返回副本。同理,setter 接收引用参数时,也要把外部传进来的对象拷贝一份再存,防止调用方后续改动影响内部状态。这些细节在简单练习里用不到,但在真实项目里就是常见的隐蔽 bug。4. 常见问题与排查技巧4.1 常见的命名和大小写错误Java 规范要求 getter 是 getXxx 这种大驼峰写法。有人写成 getx(),或者 setX 和 SetX 大小写混用。IDE 编译时通常能发现大小写问题,但真正麻烦的是方法名和字段名相差太远,后续维护时找不到对应关系。我的习惯是:字段名 x,get 方法 getX,set 方法 setX,前缀永远一致,一眼就能看出哪个方法对应哪个字段。4.2 忘记提供无参构造很多初学者只写了带参构造,然后等到框架启动或数据绑定时报错,提示缺少默认构造方法。尤其是对象序列化、反射注入这些场景,强依赖无参构造。基础版里两个构造都写,是一个低成本高收益的习惯。4.3 setter 里漏写 this前面已经反复提过,这个是“改了没生效”的头号元凶。我再给一个排查方法:在 setter 里加一行打印,看看到底进了谁的值。如果你发现参数打印正常、字段还是没变,基本就是赋值表达式写错了。4.4 Python property 死循环Python 的 property 有个经典坑,就是递归无限调用。如果你在 setter 里写成 self.x = value,而 setter 本身就是要给 self.x 赋值,那这个赋值又会触发 setter 自己,最后直接栈溢出。@x.setter def x(self, value): self.x = value # 错误,触发递归@x.setter def x(self, value): self._x = value # 正确,操作真正存储字段这个坑在初学 property 时非常容易踩。记住一个原则:property 的名字用于对外接口,存储字段单独用 _x。setter 内部永远只操作存储字段,不要再走公共接口。4.5 getter 返回 null 的问题如果字段还没初始化就返回 null,外部调用方很容易遇到空引用异常。在 Point 类里,只要构造时总给默认值,这个问题很少见。但当你把同样的设计思路用到复杂对象上,就会频繁遇到。我的经验是:初始化字段时给一个明确的默认值,比如 0 或空集合,宁可给出一个空对象也不要给 null。下面整理一个速查表,可以直接收藏:问题现象可能原因解决办法坐标改了没变setter 里漏 this 或误用字段名使用 this.字段 = 参数坐标出现 NaNsetter 缺少 Double.isNaN 检查在 setter 中拦截非法数值getter 返回对象被外部改坏返回了内部可变引用返回副本或不可变集合Python setter 报 RecursionErrorsetter 内赋值触发自身使用 _x 作为存储字段序列化/框架报无默认构造只写了带参构造补无参构造并统一初始化5. 从 Point 继续延伸:封装与对象设计的扩展思路5.1 把 Point 当作组件:Line 与更多图形类Point 写完之后,下一步是把它当作组件,放进更大的类里。比如一条线段需要起点和终点,你可以写一个 Line 类,里面有两个 Point 字段,再对外提供长度计算方法:public class Line { private Point start; private Point end; public Line(Point start, Point end) { this.start = start; this.end = end; } public Point getStart() { return start; } public Point getEnd() { return end; } public double length() { double dx = start.getX() - end.getX(); double dy = start.getY() - end.getY(); return Math.sqrt(dx * dx + dy * dy); } }你会发现,Point 的私有字段和校验规则,在组合场景里依然生效。任何外部代码都没法绕过 Line 直接改动 Point 的内部数据,因为 Point 的方法把住了关口。这就是封装在大结构里的传染力,底层的小类设计得越严谨,上层类就越省心。5.2 不可变方向:record 与不可变对象如果你用的是新版本 Java,也许已经发现一个更简洁的写法:public record Point(double x, double y) {}这一行代码自动生成全参构造、getter(叫 x() 和 y())、equals、hashCode 和 toString。而且 record 是天然不可变的,没有 setter。对坐标这种语义简单的小值对象,不可变设计往往比可变类加 setter 更稳。因为坐标本身是个值,整体替换比原地修改字段更安全,也更适合并发场景。我的建议是,把这个练习做两遍:第一遍用可变类加 setter,理解封装的过程;第二遍用 record,对比两者在代码量和安全性上的差异。两种方案各有适用场景,理解以后面对选择就不纠结了。5.3 我的一些实践经验最后分享三个我实际用过、觉得很有效的方法。第一,别急着跳过练习。把 Point 类从头到尾手写一遍,然后再用 IDE 的自动生成功能生成一遍,对比一下差在哪。很多人用了自动生成就以为自己会了,真到面试或者笔试,手写时还是会漏语法。第二,写完类之后,写一个小测试,覆盖 getter/setter 的正常和异常两种情况。比如 setX 里如果加了边界校验,那就写一个测试专门验证边界值 10000 和 10001 的行为差异。你会很快理解 setter 里那些 if 的价值。第三,学 Python 时别躲下划线命名。它是这门语言对“私有”的约定表达,理解之后才能真正用好 property。一开始可能会觉得别扭,但它和 Java 的 private 承担的是同一个职责,只是换了一种语言习惯来表达。如果你还在纠结为什么一个 Point 类要搞得这么“麻烦”,我的建议是直接去看一次线上事故。等你真正经历一次“某个数据被不知道哪一行代码改了,查了半天才定位”的痛苦,就会明白,所有封装和访问控制,都是为了让你在关键时刻少掉几根头发。
坐标点几乎是最容易上手的面向对象练习对象了。一个 Point 类,只涉及 x、y 两个字段,却能把私有、getter、setter、构造重载、数据校验这些基础概念串成一条完整的学习链路。我见过很多人一上来就给所有字段加 public,等到参与实际项目才发现,数据被谁改的、在哪一步被改坏,全靠人肉排查。学 private 和 getter/setter,Point 类正好是那个最小但最完整的练习样本。这篇文章我会按自己的实操经验,把设计 Point 类的完整流程拆开讲,从需求清单、字段私有化、getter/setter 规范写法,到 setter 里加业务校验、Python property 的替代实现,以及初学者最容易踩的坑,全部过一遍。1. 这个练习到底在练什么1.1 Point 类的角色与定位在几何里,Point 就是一个坐标点,x 和 y 决定位置。到了程序里,Point 既可以是单纯的数据载体,也可以是一个带计算逻辑的坐标工具。它非常适合拿来练习面向对象基础,因为边界足够清晰,不用牵扯数据库、文件 IO、网络传输这些外部因素。你只需要回答一个问题:这个点的数据从哪进、从哪出、能被谁改。我在参与一些模拟项目时,经常用 Point 来演示封装的价值。有个跨平台系统里,屏幕坐标和逻辑坐标要做互转,最初实现把所有字段直接公开,结果某天发现某处的换算结果总是偏了几个像素。排查下来,是某个模块跳过转换方法、直接改了底层坐标。这类问题如果一开始就做成私有字段加 getter/setter,在编译期就能拦住一大半。所以,Point 类看着简单,练的其实是访问控制这种底层设计能力。一个点可以被任意改,和只能通过特定方法改,代码的安全性和可维护性完全不是一个等级。1.2 私有字段是封装的第一课封装这个词写起来很抽象,落到代码上,第一件事就是给字段标上 private。但要注意,私有化不是把数据锁死,而是强制所有读写都走方法。这样做的直接好处是,你可以在方法里随时加日志、做校验、触发联动,不需要让调用方改任何代码。举个例子,假设某个图形编辑系统里,屏幕允许的最大坐标是 10000。如果 x 是 public,你没法保证每个调用方都记得检查边界,总有人会漏;但如果所有写入都经过 setX(double x),你只需要在 setX 里加一次范围判断,整个系统的坐标安全就全部收口了。这就是封装的真正含义,把复杂留在内部,把简单留给外部。1.3 先列需求清单再动手很多初学者拿题就写,写到一半发现缺这个缺那个。正确的做法是先列一个需求清单,把 Point 类要具备的行为都列出来:需求说明表示坐标x、y 两个字段初始化无参构造、带参构造读取坐标getX()、getY()修改坐标setX()、setY()计算距离distanceTo(Point other)格式输出toString()判断相等equals()、hashCode()清单列完之后,写代码基本不会漏。而且你会发现,需求越清晰,类结构就越稳定。很多看起来高级的设计,本质都是在需求还没想清楚的时候,提前把方法架得很复杂,结果反而让自己的代码越来越难懂。Point 这种小类,更应该一开始就把职责定清楚。2. 手写一个干净的基础版 Point 类2.1 选 Java 还是 Python语言怎么选,取决于你想重点练什么。从教学顺序上,我的建议是先花时间用 Java 手写 getter/setter,再用 Python 的 property 重新实现一遍。Java 里 private、public 是显式关键字,每个字段的访问权限一目了然,getXxx/setXxx 也是全行业通用约定。对新手来说,这种“把约定写进代码”的体验非常有帮助。IDE 自动生成也默认遵循这套命名,不会给你自由发挥的空间,反而有利于养成肌肉记忆。Python 虽然也能用 @property 模拟 getter/setter,但它的私有化更弱,下划线前缀更多是约定俗成,不会强制外部遵守,初学者容易产生“这会有什么用”的错觉。特性Java 做法Python 做法私有声明private 关键字下划线前缀约定getterpublic double getX()@property 方法setterpublic void setX(double x)@x.setter 方法外部保护编译期强检查靠约定和代码规范代码量样板较多简洁清晰2.2 Java 基础版完整代码先给一个能直接“抄作业”的版本:public class Point { private double x; private double y; public Point() { this(0, 0); } public Point(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } @Override public String toString() { return "Point(" + x + ", " + y + ")"; } }这段代码每一行都有讲究。private double x 把字段限定在类内部可见;无参构造里写 this(0, 0),是直接复用下面的全参构造,避免初始化逻辑写两遍;setX(double x) 的参数名和字段名都叫 x,方法体里就必须用 this.x = x 区分,this 指向当前对象,this.x 才是成员变量,没有 this 的那个 x 是参数。我建议可以先用这段代码跑一个小测试:Point p = new Point(); p.setX(3); p.setY(4); System.out.println(p); // 期望输出 Point(3.0, 4.0)如果输出结果不对,优先检查是不是 setter 里的 this 丢了。2.3 构造函数为什么推荐重载很多初学者只写一个带参构造,或者只写一个无参构造。实际场景里,你有时需要一个默认原点,有时需要指定坐标,两个都提供,调用方就灵活多了。重点是默认构造里调用 this(0, 0) 这个写法。它本质上是把初始化逻辑收敛到一个位置,以后如果要在构造时加校验,只需要改全参构造,不需要在多个构造函数里同步修改。可能你刚开始觉得这没什么,但等你参与真实项目,构造参数从两个变成五个,会发现这种收敛习惯能省下大量维护成本。2.4 关于 this 的细节在 setter 里最容易出错的点,就是 this 的用法。如果你写 setX(double x) { x = x; },左侧和右侧的 x 都指向参数,字段根本没被赋值。这种情况在调试时非常迷惑,坐标值怎么改都不更新,浪费大量时间。我的排查经验是:遇到“字段改了但没生效”的问题,第一反应就是检查 setter 里是不是漏写 this。如果是 Python,还有另一个变种,就是在 setter 里给属性赋值时触发了自身递归,后面我会单独展开。3. 进阶:让 getter/setter 不再是“傻赋值”3.1 setter 是拦截非法数据的关卡有些人写 getter/setter 只是为了应付练习题,方法体就是一行赋值,这种叫“傻赋值”。但私有字段的真正价值,就是在 setter 里加业务规则。你可以在 setX 里检查数值是否合法,甚至联动其他字段。比如在图形编辑器场景里,坐标不允许超出画布范围,setter 可以这样写:public void setX(double x) { if (Double.isNaN(x)) { throw new IllegalArgumentException("x 不能是 NaN"); } if (x < MIN_X || x > MAX_X) { throw new IllegalArgumentException("x 超出边界: " + x); } this.x = x; }有人会问,这不就是加了个 if 吗?对,关键不是 if,而是“所有修改都经过同一个 if”。如果字段是 public,这个检查要么没有,要么每个调用方各写一套,漏一处就是 bug。收口到 setter 之后,拦截点只有一个,维护起来非常轻松。3.2 不要只在构造里校验有些初学者会把校验逻辑放在构造函数里,觉得生成的对象就已经是合格的了。这里有个隐患:对象创建之后,外部还会继续调用 setter 改值。如果 setter 没有校验,就等于开了一个后门。安全口诀是:进入对象的所有通道,都必须经过同一套规则。所以更稳的做法是,构造函数内部也去调用 setter,或者把校验逻辑抽成一个私有方法,构造时调用一次,setter 里再调用一次。这样既保证初始化正确,也保证后续修改正确。3.3 Python 里的 property 写法切到 Python,代码会简洁很多,但设计思路完全一样:class Point: MIN_X = 0 MAX_X = 10000 def __init__(self, x=0, y=0): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, value): if value < self.MIN_X or value > self.MAX_X: raise ValueError(f"x 超出边界: {value}") self._x = value @property def y(self): return self._y @y.setter def y(self, value): if value < self.MIN_X or value > self.MAX_X: raise ValueError(f"y 超出边界: {value}") self._y = value注意,这里真正的字段是带下划线前缀的 _x、_y,对外暴露的 x 是 property 对象。当你写 p.x = 50 时,实际调用的是 x.setter 方法,校验逻辑就会执行。下划线前缀在 Python 里就是一个“私有信号”,看到 _x 就默认别去直接碰它,要改就走公共接口。3.4 getter 也要防:返回引用副本的坑setter 管住了写入,getter 同样有风险,尤其是字段类型是引用时。比如类里有一个 List 字段:public List<String> getTags() { return tags; }外部拿到的不是副本,而是同一个对象引用。那外部完全可以在无感知的情况下往列表里加数据,把不该改变的状态改了。这种问题在 Point 这种只含 double 的类里不会出现,但我建议借这个练习把原则记住:字段是数组、集合或对象引用时,getter 要么返回不可变封装,要么返回副本。同理,setter 接收引用参数时,也要把外部传进来的对象拷贝一份再存,防止调用方后续改动影响内部状态。这些细节在简单练习里用不到,但在真实项目里就是常见的隐蔽 bug。4. 常见问题与排查技巧4.1 常见的命名和大小写错误Java 规范要求 getter 是 getXxx 这种大驼峰写法。有人写成 getx(),或者 setX 和 SetX 大小写混用。IDE 编译时通常能发现大小写问题,但真正麻烦的是方法名和字段名相差太远,后续维护时找不到对应关系。我的习惯是:字段名 x,get 方法 getX,set 方法 setX,前缀永远一致,一眼就能看出哪个方法对应哪个字段。4.2 忘记提供无参构造很多初学者只写了带参构造,然后等到框架启动或数据绑定时报错,提示缺少默认构造方法。尤其是对象序列化、反射注入这些场景,强依赖无参构造。基础版里两个构造都写,是一个低成本高收益的习惯。4.3 setter 里漏写 this前面已经反复提过,这个是“改了没生效”的头号元凶。我再给一个排查方法:在 setter 里加一行打印,看看到底进了谁的值。如果你发现参数打印正常、字段还是没变,基本就是赋值表达式写错了。4.4 Python property 死循环Python 的 property 有个经典坑,就是递归无限调用。如果你在 setter 里写成 self.x = value,而 setter 本身就是要给 self.x 赋值,那这个赋值又会触发 setter 自己,最后直接栈溢出。@x.setter def x(self, value): self.x = value # 错误,触发递归@x.setter def x(self, value): self._x = value # 正确,操作真正存储字段这个坑在初学 property 时非常容易踩。记住一个原则:property 的名字用于对外接口,存储字段单独用 _x。setter 内部永远只操作存储字段,不要再走公共接口。4.5 getter 返回 null 的问题如果字段还没初始化就返回 null,外部调用方很容易遇到空引用异常。在 Point 类里,只要构造时总给默认值,这个问题很少见。但当你把同样的设计思路用到复杂对象上,就会频繁遇到。我的经验是:初始化字段时给一个明确的默认值,比如 0 或空集合,宁可给出一个空对象也不要给 null。下面整理一个速查表,可以直接收藏:问题现象可能原因解决办法坐标改了没变setter 里漏 this 或误用字段名使用 this.字段 = 参数坐标出现 NaNsetter 缺少 Double.isNaN 检查在 setter 中拦截非法数值getter 返回对象被外部改坏返回了内部可变引用返回副本或不可变集合Python setter 报 RecursionErrorsetter 内赋值触发自身使用 _x 作为存储字段序列化/框架报无默认构造只写了带参构造补无参构造并统一初始化5. 从 Point 继续延伸:封装与对象设计的扩展思路5.1 把 Point 当作组件:Line 与更多图形类Point 写完之后,下一步是把它当作组件,放进更大的类里。比如一条线段需要起点和终点,你可以写一个 Line 类,里面有两个 Point 字段,再对外提供长度计算方法:public class Line { private Point start; private Point end; public Line(Point start, Point end) { this.start = start; this.end = end; } public Point getStart() { return start; } public Point getEnd() { return end; } public double length() { double dx = start.getX() - end.getX(); double dy = start.getY() - end.getY(); return Math.sqrt(dx * dx + dy * dy); } }你会发现,Point 的私有字段和校验规则,在组合场景里依然生效。任何外部代码都没法绕过 Line 直接改动 Point 的内部数据,因为 Point 的方法把住了关口。这就是封装在大结构里的传染力,底层的小类设计得越严谨,上层类就越省心。5.2 不可变方向:record 与不可变对象如果你用的是新版本 Java,也许已经发现一个更简洁的写法:public record Point(double x, double y) {}这一行代码自动生成全参构造、getter(叫 x() 和 y())、equals、hashCode 和 toString。而且 record 是天然不可变的,没有 setter。对坐标这种语义简单的小值对象,不可变设计往往比可变类加 setter 更稳。因为坐标本身是个值,整体替换比原地修改字段更安全,也更适合并发场景。我的建议是,把这个练习做两遍:第一遍用可变类加 setter,理解封装的过程;第二遍用 record,对比两者在代码量和安全性上的差异。两种方案各有适用场景,理解以后面对选择就不纠结了。5.3 我的一些实践经验最后分享三个我实际用过、觉得很有效的方法。第一,别急着跳过练习。把 Point 类从头到尾手写一遍,然后再用 IDE 的自动生成功能生成一遍,对比一下差在哪。很多人用了自动生成就以为自己会了,真到面试或者笔试,手写时还是会漏语法。第二,写完类之后,写一个小测试,覆盖 getter/setter 的正常和异常两种情况。比如 setX 里如果加了边界校验,那就写一个测试专门验证边界值 10000 和 10001 的行为差异。你会很快理解 setter 里那些 if 的价值。第三,学 Python 时别躲下划线命名。它是这门语言对“私有”的约定表达,理解之后才能真正用好 property。一开始可能会觉得别扭,但它和 Java 的 private 承担的是同一个职责,只是换了一种语言习惯来表达。如果你还在纠结为什么一个 Point 类要搞得这么“麻烦”,我的建议是直接去看一次线上事故。等你真正经历一次“某个数据被不知道哪一行代码改了,查了半天才定位”的痛苦,就会明白,所有封装和访问控制,都是为了让你在关键时刻少掉几根头发。