三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Java面向对象编程:Point类的封装与设计模式实践

Java面向对象编程:Point类的封装与设计模式实践

1. Point类设计基础:封装与访问控制

在面向对象编程中,Point类是最经典的入门案例之一。这个看似简单的二维坐标点,却蕴含着面向对象三大特性之一的封装思想。我见过不少初学者直接使用public属性暴露x、y坐标,这就像把自家大门钥匙插在门锁上一样危险。

私有成员的正确声明方式

public class Point { private double x; private double y; }

这里的private关键字就是我们的"门锁",它确保:

  • 外部代码无法直接访问x、y字段
  • 所有访问必须通过我们预设的方法进行
  • 可以在方法中添加校验逻辑保证数据安全

关键经验:即使像Point这样简单的类,也应该从一开始就养成使用private的习惯。我在代码审查中经常发现,临时图省事暴露的public字段,后期要改造时往往需要修改几十处调用点。

2. Getter/Setter方法设计艺术

2.1 基础访问器实现

标准的getter/setter看似简单,但魔鬼藏在细节里:

public double getX() { return x; // 直接返回基本类型值 } public void setX(double x) { this.x = x; // 直接赋值 }

容易踩的坑

  1. 忘记this关键字导致参数遮蔽(shadowing)字段
  2. 对浮点数直接使用==比较(应该用阈值比较)
  3. 忽略参数校验(比如坐标值是否在合理范围)

2.2 增强型Setter实现

实际项目中,setter应该承担数据校验职责:

public void setX(double x) { if (Double.isNaN(x)) { throw new IllegalArgumentException("坐标不能为NaN"); } if (Math.abs(x) > 1e6) { throw new IllegalArgumentException("坐标值超出合理范围"); } this.x = x; }

我在GIS系统开发中就遇到过因未校验NaN值导致的定位漂移bug,这种防御性编程能避免很多线上问题。

3. 进阶设计技巧

3.1 不可变Point实现

对于多线程环境,可以考虑不可变设计:

public final class ImmutablePoint { private final double x; private final double y; public ImmutablePoint(double x, double y) { this.x = x; this.y = y; } // 只有getter没有setter public double getX() { return x; } }

3.2 Builder模式应用

当Point需要多个构造参数时:

Point p = new Point.Builder() .setX(10.5) .setY(20.3) .build();

3.3 方法链式调用

通过返回this实现流畅接口:

public Point setX(double x) { this.x = x; return this; }

4. 实际应用中的问题排查

常见问题1:Setter未生效

  • 检查方法签名是否正确(常见拼写错误)
  • 确认是否真的调用了setter(可能直接访问了字段)
  • 多线程环境下考虑volatile关键字

常见问题2:性能热点

  • 过度封装导致getter/setter成为性能瓶颈(实测显示现代JVM会内联简单访问器)
  • 考虑使用@Contended注解避免伪共享

我在性能调优时用JMH测试过,简单的getter方法调用开销约2-3纳秒,在绝大多数场景下可以忽略不计。

5. 现代Java中的改进方案

5.1 Record类型简化

Java 14引入的record可以极大简化:

public record Point(double x, double y) {}

会自动生成:

  • 私有final字段
  • 公开getter(方法名就是x()、y())
  • equals/hashCode/toString

5.2 Lombok注解方案

@Getter @Setter public class Point { private double x; private double y; }

使用建议:新项目推荐用record,老项目改造可以用Lombok。我在微服务架构中更倾向record,因为它生成的代码更可控。

6. 设计模式实践

6.1 代理模式保护敏感操作

public class ProtectedPoint extends Point { @Override public void setX(double x) { if (!checkPermission()) { throw new SecurityException(); } super.setX(x); } }

6.2 观察者模式实现坐标变更通知

public void setX(double x) { this.x = x; notifyObservers(); }

在图形编辑器项目中,这种设计让视图能实时响应模型变化。

7. 测试要点

7.1 单元测试示例(JUnit5)

@Test void shouldThrowWhenSetNaN() { Point p = new Point(); assertThrows(IllegalArgumentException.class, () -> p.setX(Double.NaN)); } @Test void shouldAllowNormalValue() { Point p = new Point(); p.setX(10.5); assertEquals(10.5, p.getX(), 1e-9); }

7.2 边界值测试案例

  • 最大/最小双精度值
  • 正负零(+0.0 vs -0.0)
  • 接近Double.MAX_VALUE的值

8. 跨语言实践

8.1 Python属性装饰器

class Point: @property def x(self): return self._x @x.setter def x(self, value): if not isinstance(value, (int, float)): raise ValueError self._x = value

8.2 C#属性语法

public class Point { private double _x; public double X { get => _x; set => _x = value; } }

9. 性能优化技巧

  1. 缓存哈希值:对于频繁参与计算的Point
private int hash; // 缓存字段 @Override public int hashCode() { if (hash == 0) { hash = Objects.hash(x, y); } return hash; }
  1. 对象池技术:对于频繁创建的临时Point
private static final Pool<Point> POOL = new Pool<>(); public static Point obtain(double x, double y) { Point p = POOL.acquire(); p.set(x, y); return p; }

在游戏开发中,这种优化可以减少90%的对象创建开销。

10. 架构层面的思考

在DDD中,Point可以作为值对象(Value Object):

  • 实现equals/hashCode基于值而非引用
  • 设计为不可变
  • 包含业务方法如distanceTo()

在微服务中,需要考虑:

  • 序列化方案(JSON字段命名)
  • 精度问题(BigDecimal vs double)
  • 传输优化(protobuf压缩)

我在物联网项目中就遇到过GPS坐标使用float导致精度丢失的问题,最终改用定点数表示。

← 返回列表