Java面向对象经典练习:从MyPoint到ThreeDPoint的距离计算与类设计

发布时间:2026/10/15 15:56:28
Java面向对象经典练习:从MyPoint到ThreeDPoint的距离计算与类设计 1. 项目概述坐标系里的经典入门题写这篇东西的起因很简单我在带几个刚学面向对象的新人时发现很多人拿到“定义一个点”这种题目就懵了——不是不会写代码而是不知道从哪里下手。其实这个题目的价值被严重低估了它表面上只是定义一个带坐标的类实际上把封装、构造器重载、方法重载、继承、甚至浮点精度这些OOP核心概念全都串起来了。先把这个项目讲清楚。我们需要实现两个类MyPoint代表二维空间里的一个点有x和y两个坐标提供获取坐标的get函数以及计算两个点之间距离的函数。ThreeDPoint代表三维空间里的一个点继承或扩展自MyPoint加入第三个坐标z实现三维距离的计算。这个项目非常适合用来练习面向对象的基础设计。它没有复杂的业务逻辑所有注意力都能集中在“类怎么设计才合理”这件事上。不管你是刚学Java的大学生还是自学编程转行的新手把这两个类写明白后面写更大的项目时你会感谢当初认真整理过这段代码的自己。理由后面展开说先看整体设计思路。2. 核心细节解析从需求到类设计2.1 字段设计x、y凭什么不能直接访问很多人第一版代码会写成这样public class MyPoint { public double x; public double y; }然后觉得“这样不是挺好吗直接就能拿坐标。”但这就是典型的缺少封装意识。字段直接暴露给外部意味着外部代码可以随意篡改坐标值你的对象完全无法保证自己状态的合法性。比如某个业务场景要求点在坐标系内x不能为负如果是public字段外部一行point.x -100就直接绕过你的约束了。正确做法是私有化字段统一通过getter暴露只读访问public class MyPoint { private double x; private double y; public double getX() { return x; } public double getY() { return y; } }你可能觉得“这不多此一举吗”但在团队协作里封装的价值立刻体现出来如果哪天需求变了需要在取值时做一些处理比如四舍五入、单位换算你只需要改getter的内部实现所有调用方都不用动。这就是封装的核心价值——把变化隔离在内部。顺带提一下为什么用double而不用int或float。坐标点是个非常典型的小数场景比如(1.5, 2.75)这种位置用int直接废掉用float精度又不够坐标值大一点或者计算距离时浮点误差会放大得很明显。double是性价比最高的选择绝大多数图形、几何相关计算都默认用它。2.2 构造函数设计原点点也要能造出来一个类至少有两条默认行为需要定义清楚不传参数时怎么办传参数时怎么办。对应到构造函数就是无参构造和有参构造。无参构造的产物是一个位于原点(0,0)的点有参构造根据传入的x、y创建指定位置的点public MyPoint() { this(0, 0); } public MyPoint(double x, double y) { this.x x; this.y y; }这里有个小细节值得说说无参构造里写的不是this.x 0; this.y 0;而是调用this(0, 0)。这叫构造器重定向好处是后续如果构造逻辑变了比如增加坐标校验只需要在有参构造里改一处无参构造自动跟着走。实习生在代码评审里看到这种写法大概率会收到一个赞。至于为什么必须有默认构造原因很朴素有的场景就是需要先创建一个空白对象属性后续再设置。虽然我们用getter暴露的是只读字段没提供setter但这是这个经典题目的特殊场景——坐标点一般创建后再晚就不变了。实际项目里如果点需要移动可以加一个move方法而不是放开setter这是更规范的做法。2.3 距离计算的数学原理欧几里得距离距离计算是这个项目的核心功能数学公式并不复杂。二维空间中两点(x1, y1)和(x2, y2)的距离distance sqrt((x2 - x1)² (y2 - y1)²)三维空间中两点(x1, y1, z1)和(x2, y2, z2)的距离distance sqrt((x2 - x1)² (y2 - y1)² (z2 - z1)²)可以看到三维距离公式就是二维的扩展新增了z轴方向上的差平方和。这也就是为什么我们用继承来实现ThreeDPoint时特别自然——三维点在逻辑上确实是二维点的子集扩展它拥有二维点的一切只多了一个z维度。代码实现时Math.pow(diff, 2)或diff * diff都可以计算平方Math.sqrt计算开方。实际编码时我更喜欢直接用diff * diff因为Math.pow内部走的是通用幂运算性能比直接相乘差一些而且语义上这里就是求平方没必要绕一圈。3. 实操过程手把手写完整代码3.1 MyPoint类的完整实现接下来是MyPoint类的完整代码。我把注释写得详细一些方便对照着理解每一行的意图public class MyPoint { private double x; private double y; // 无参构造默认创建原点 (0,0) public MyPoint() { this(0, 0); } // 有参构造创建指定坐标的点 public MyPoint(double x, double y) { this.x x; this.y y; } public double getX() { return x; } public double getY() { return y; } // 计算当前点与另一个MyPoint对象之间的距离 public double distance(MyPoint another) { double dx this.x - another.x; double dy this.y - another.y; return Math.sqrt(dx * dx dy * dy); } // 重载版本根据传入的x、y坐标计算距离 public double distance(double x, double y) { double dx this.x - x; double dy this.y - y; return Math.sqrt(dx * dx dy * dy); } Override public String toString() { return ( x , y ); } }这里有个很重要的设计选择——distance我提供了两个版本。一个是接收MyPoint对象作为参数一个是直接接收x和y坐标。这就叫方法重载overload同一个方法名下适配不同的调用场景。写业务代码时你会发现这种模式非常常见接口设计得好不好重载方法做得顺手不顺手直接影响调用方的体验。另外我顺手加了toString()方法这样打印点对象时能直接看到(1.5, 2.5)而不是让人崩溃的MyPoint2f4d3709。这个不是题目的硬性要求但实际调试的时候太好用了建议养成习惯。3.2 ThreeDPoint类的完整实现ThreeDPoint的重点是继承的用法。它继承MyPoint自动获得x、y字段和对应getter再添加自己的z字段public class ThreeDPoint extends MyPoint { private double z; // 无参构造默认创建原点 (0,0,0) public ThreeDPoint() { this(0, 0, 0); } // 有参构造创建指定三维坐标的点 public ThreeDPoint(double x, double y, double z) { super(x, y); // 调用父类构造器完成x和y的初始化 this.z z; // 自己负责z } public double getZ() { return z; } // 计算当前点与另一个三维点对象之间的距离 public double distance(ThreeDPoint another) { double dx this.getX() - another.getX(); double dy this.getY() - another.getY(); double dz this.z - another.z; return Math.sqrt(dx * dx dy * dy dz * dz); } Override public String toString() { return ( getX() , getY() , z ); } }有几个细节值得单独提一下。super(x, y)这行是关键。子类继承了父类的字段但x和y在父类里是private的子类不能直接访问必须通过父类的构造器来初始化。所以ThreeDPoint的有参构造里第一行必须调用super(x, y)然后再初始化自己的z。如果你不写这行编译器会自动调用父类的无参构造——但那样x和y就变成默认的0了你传入的x和y就被丢掉了这是个很容易踩的隐蔽坑。在distance方法里注意我是通过getX()和getY()来取父类坐标的而不是直接访问this.x。因为x是父类私有字段子类代码里直接写this.x会编译报错。这是初学者最容易卡住的地方——继承不代表子类能随便摸父类的私有成员。distance(ThreeDPoint another)这个方法的参数类型是ThreeDPoint。这里可以思考一个问题如果调用时传入的是MyPoint对象能不能复用原来的距离逻辑后面的常见问题那节我再详细说。3.3 测试代码与运行结果写完类一定要写测试。我写了一个简单的测试类把主要场景都覆盖了一遍public class TestPoint { public static void main(String[] args) { // 测试二维点 MyPoint p1 new MyPoint(); MyPoint p2 new MyPoint(3, 4); System.out.println(p1: p1); System.out.println(p2: p2); System.out.println(p1到p2距离: p1.distance(p2)); System.out.println(p1到(6,8)距离: p1.distance(6, 8)); // 测试三维点 ThreeDPoint tp1 new ThreeDPoint(); ThreeDPoint tp2 new ThreeDPoint(1, 2, 2); System.out.println(tp1: tp1); System.out.println(tp2: tp2); System.out.println(tp1到tp2距离: tp1.distance(tp2)); } }运行结果p1: (0.0, 0.0) p2: (3.0, 4.0) p1到p2距离: 5.0 p1到(6,8)距离: 10.0 tp1: (0.0, 0.0, 0.0) tp2: (1.0, 2.0, 2.0) tp1到tp2距离: 3.0(0,0)到(3,4)的距离是5勾股定理的经典3-4-5三角形(0,0,0)到(1,2,2)的距离是3因为1449开方得3。结果都符合手算预期说明代码逻辑没问题。实际开发里我习惯用这种“结果可手算验证”的数据来测试数学相关方法确认逻辑正确后再去测边界数据。4. 常见问题与排查技巧实录4.1 距离方法参数类型到底应该是什么先看这个很容易被问到的点ThreeDPoint继承MyPoint之后如果调用tp1.distance(tp2)由于tp2既是ThreeDPoint又是MyPoint为什么没有自动调用父类的distance方法答案是Java的方法重载在编译时确定具体调用哪个版本它看的是“引用类型”。tp2声明为ThreeDPoint而ThreeDPoint类中定义了distance(ThreeDPoint)方法所以调用的是ThreeDPoint自己的版本。但是如果你写的是MyPoint p new ThreeDPoint(1, 2, 2); MyPoint q new ThreeDPoint(3, 4, 5); double d p.distance(q);那么这两个变量的引用类型都是MyPoint调用的就会是父类的distance(MyPoint)方法结果是二维距离z坐标被完全忽略了。这个问题暴露了一个设计上的模糊地带ThreeDPoint到底要不要重写父类的distance(MyPoint)方法保证传入MyPoint也能算出正确距离我见过的不少项目里处理方式是在ThreeDPoint里增加一个重载public double distance(MyPoint another) { if (another instanceof ThreeDPoint) { return distance((ThreeDPoint) another); } double dx this.getX() - another.getX(); double dy this.getY() - another.getY(); return Math.sqrt(dx * dx dy * dy); }这样做的好处是即使两个三维点被当作MyPoint引用距离计算仍然正确。代价是代码变复杂了而且instanceof用多了对性能和维护都有一定压力。到底是“面向接口设计”还是“图省事直接继承”每个团队都有自己的取舍。我的态度是这种入门练习场景搞清楚差异就够了真在业务里遇到三维点距离需求我大概率不会用继承而是用组合后面说为什么。4.2 继承设计和getter覆写ThreeDPoint看似自动继承了getX()和getY()但它们返回的是父类私有字段x和y。这里有一个经常被忽略的细节如果父类字段不是用this.x x而是通过setter初始化的那setter如果是private或者有额外校验逻辑调用链会复杂一些但核心还是那一条子类不能直接访问父类的private字段必须通过public的构造器或getter间接操作。更值得一提的坑是getter名字冲突。假设有人想给ThreeDPoint加一个getX()的重写版本虽然一般没必要注意字段x在父类里是private子类里如果也定义一个private double x那就是“字段遮蔽”父类和子类各有一个x读起来非常诡异// 反例千万不要这样写 public class ThreeDPoint extends MyPoint { private double x; // 遮蔽了父类的x private double y; // 遮蔽了父类的y private double z; }这种写法下子类里的getX()如果误写了return x;返回的是子类自己的x初始值0父类的x永远不更新bug极其隐蔽。再强调一次父类的已定义字段子类不要重复定义直接用就好。大家看一下这个题目的设计MyPoint的字段设计是否合理。字段一旦被设计为private就相当于划定了“只允许本类方法访问”的边界。这个边界对继承来说很麻烦因为子类天然想复用父类的字段但封装原则又要求字段私有。Java的折中方案是protected但这里我反而建议坚持private因为距离计算中通过getter取父类坐标完全够用不需要增加protected的暴露面。能用组合就别硬上继承能用getter就别想字段访问——这个原则可以帮你避开九成以上的设计坑。4.3 易错点速查表我把这个项目里我自己和带过的新人踩过的坑整理成了表格方便大家对照自查易错点错误表现正确做法原因说明字段直接publicpoint.x 99private getter破坏封装无法控制赋值合法性子类构造没调super(x,y)x、y是默认0第一行写super(x, y)子类不能直接初始化父类private字段子类访问this.x编译报错this.getX()x在父类中是private子类不可见距离计算忘开方距离值明显偏大用Math.sqrt差的平方和不是距离开方才是用float存坐标大数计算精度漂移用doublefloat有效精度约7位十进制数字子类重定义x字段属性遮蔽get器混乱不重复定义字段遮蔽导致数据不一致且难排查打印对象输出编号无法判断内容重写toString默认toString没业务含义这个表格里的第三行也就是“子类访问this.x编译报错”那个我见过至少三个人卡在这里每次都解释半天。其实底层原因就一句话JVM层面设计的就是private属于声明它的类所有无论谁继承都无法直接触碰。4.4 两点之间距离的精度问题最后说一个进阶的坑浮点精度。0.3 - 0.1在double里不是精确的0.2这是IEEE二进制浮点表示的天然限制。所以两个坐标差值平方再开方的结果经常会看到类似4.999999999997这种数而不是整齐的5.0。处理方式按场景来如果只是展示给人看直接用String.format(%.2f, distance)格式化输出。如果是比较距离大小比如判断两个点是否相同不要用要设定一个容差比如Math.abs(d1 - d2) 1e-6。如果需要绝对精确的十进制运算Java里用BigDecimal但性能开销明显几何计算一般不用。我在测试里故意选了(0,0)到(3,4)这种“能用勾股定理直接口算”的数据一个原因是方便验证另一个原因是这种经典三角形的距离恰好是精确的5.0能避开浮点误差的干扰。等确认逻辑正确了你再去测那些会产生循环小数的数据才不会被疑似bug的误差带偏。5. 进阶思考继承还是组合这是个问题写完ThreeDPoint之后值得冷静想一想继承真的是最优解吗从代码复用角度看ThreeDPoint继承MyPoint很合理x、y和对应的getter直接白嫖。但从现实世界的建模角度看三维点并不真的“是一个”二维点它们只是共享了“坐标点”这个抽象概念。如果把三个点包括二维都抽象成统一接口比如定义一个Coordinate接口再分别实现TwoDimensionalPoint和ThreeDimensionalPoint组合和实现的组合方式会更干净。这里我把两种方案放在一起对比一下维度继承实现组合/接口实现代码复用直接复用父类字段方法需要组合成员变量或重复定义层级关系is-a关系语义清晰has-a或implements关系扩展性加一个新维度要改继承树加新维度只需新增接口实现维护成本父类改动可能波及其他子类实现类各自独立互不干扰放在实际项目中如果只是处理二维和三维两种点继承够用如果猜测未来会出现四维、ND维甚至其他坐标系接口加组合明显更稳。这个题目作为练习继承没问题但你要清楚继承不是唯一的答案真实设计里没有一个模板能套用到所有场景。我个人的习惯是除非真的有明确的is-a关系否则优先组合这能减少很多父类变动带来的连锁反应。另外顺带探索一个扩展方向MyPoint类现在没有setter如何表示点的移动两种常用方案一种是加一个move方法public void move(double newX, double newY) { this.x newX; this.y newY; }这样既保持了字段私有又提供了受控的状态变更。另一种是不可变对象策略每次移动都返回一个新点public MyPoint movedBy(double deltaX, double deltaY) { return new MyPoint(this.x deltaX, this.y deltaY); }用哪种方式取决于业务场景和团队偏好。不可变方案在并发场景下更安全但创建对象的开销更高。这个经典题目不是做完了就完了后续能围绕它展开不少改进——比如添加极坐标支持、距离的静态版本、点的平移旋转等每一个小的扩展都能加深对面向对象设计的理解。我在教别人写这类练习时常常说一句话代码能跑只是及格设计得清爽、别人能轻松看懂、改需求时不痛苦那才是真正学会了。这个MyPoint和ThreeDPoint的题目虽然简单但你把封装、重载、继承、浮点精度这些点全都攻克了它就不再是一个作业而是一个帮你建立面向对象直觉的样板工程。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询