
本文探讨java中可变对象(如date类)在对象之间传递时可能引发的“隐私泄露”问题,即内部状态被外部意外修改。针对此问题,将详细介绍两种核心解决方案:防御性复制(在构造器和访问器中创建副本)和不可变对象设计(将类及其字段声明为final并移除setter方法),以确保数据完整性和代码可预测性。
理解Java中的“隐私泄露”问题
在Java中,当我们将一个对象作为参数传递或从方法中返回时,实际上是传递了该对象的引用。如果这个对象是可变的(mutable),并且其引用被多个地方持有,那么通过其中一个引用对对象状态的修改,会影响到所有持有该引用的地方。这在面向对象设计中被称为“隐私泄露”或“别名问题”,因为它可能导致内部状态被外部意外地修改,从而破坏封装性和数据完整性。
例如,考虑一个Date类和一个Order类。如果Order对象在其构造器中接收一个可变的Date对象,并将其直接存储为内部字段,那么外部代码仍然可以通过原始的Date引用来修改日期,即使Order对象已经创建。这会导致Order内部的日期状态在不被Order类本身控制的情况下发生变化,如下面的junit测试所示:
// 假设Date类是可变的,并包含setDay方法 public class Date { private int month; private int day; private int year; public Date(int month, int day, int year) { // 参数校验,后文会优化 this.month = month; this.day = day; this.year = year; } public void setDay(int day) { // 可变方法 if (day >= 1 && day <= 31) { this.day = day; } } public int getDay() { return day; } // ... 其他getter/setter } // 假设Order类直接存储传入的Date引用 public class Order { private Date orderDate; // ... 其他字段 public Order(Date orderDate) { this.orderDate = orderDate; // 直接存储引用 } public Date getOrderDate() { return orderDate; // 直接返回内部引用 } } @Test public void OrderDatePrivacyLeaks() { Date d = new Date(6, 12, 2017); Order b = new Order(d); // Order内部持有d的引用 d.setDay(10); // 外部修改了原始Date对象 Date billDate = b.getOrderDate(); // 获取Order内部的Date对象 assertEquals(12, billDate.getDay()); // 期望是12,但实际会得到10 }
上述测试中,尽管我们期望Order对象内部的日期保持为12日,但由于Order直接持有了外部传入的Date对象的引用,并且Date对象是可变的,外部对d的修改直接影响了Order内部的orderDate,导致测试失败。
解决方案一:防御性复制(Defensive copying)
防御性复制的核心思想是在对象边界(即构造器、setter方法接收参数时,以及getter方法返回内部对象时)创建可变对象的副本,而不是直接传递或返回其引用。这样可以确保外部代码无法通过原始引用或返回的引用来修改内部状态。
立即学习“Java免费学习笔记(深入)”;
要实现防御性复制,需要满足以下条件:
- 可变类需提供复制机制:被复制的类(如Date)需要提供一个复制构造器(Copy constructor)或clone()方法。
- 在接收时复制:当一个类(如Order)在其构造器或setter方法中接收一个可变对象(如Date)时,不应直接存储传入的引用,而应创建一个新的副本并存储该副本。
- 在返回时复制:当一个类通过getter方法返回其内部的可变对象时,不应直接返回内部引用,而应返回该对象的副本。
以下是Order类如何通过防御性复制来保护其Date字段的示例:
// 首先,Date类需要一个复制构造器 public class Date { private int month; private int day; private int year; // 主构造器,包含参数校验 public Date(int month, int day, int year) { if (day < 1 || day > 31) { throw new IllegalArgumentException("Invalid day: " + day); } if (month < 1 || month > 12) { throw new IllegalArgumentException("Invalid month: " + month); } if (year < 2014 || year > 2024) { // 示例范围 throw new IllegalArgumentException("Invalid year: " + year); } this.month = month; this.day = day; this.year = year; } // 复制构造器:用于创建Date对象的副本 public Date(Date other) { // 调用主构造器进行复制和校验,确保新对象也是有效的 this(other.month, other.day, other.year); } // 可变方法(如果Date仍需可变) public void setDay(int day) { if (day >= 1 && day <= 31) { this.day = day; } else { throw new IllegalArgumentException("Invalid day: " + day); } } public int getMonth() { return month; } public int getDay() { return day; } public int getYear() { return year; } // ... 其他方法 } // Order类使用防御性复制来保护其Date字段 public class Order { private Date orderDate; // ... 其他字段 // 构造器中对传入的Date对象进行防御性复制 public Order(Date orderDate) { if (orderDate == null) { throw new IllegalArgumentException("Order date cannot be null."); } this.orderDate = new Date(orderDate); // 创建副本并存储 // ... 其他字段初始化 } // Getter方法中返回内部Date对象的副本 public Date getOrderDate() { return new Date(this.orderDate); // 返回副本,防止外部通过getter修改内部状态 } // Setter方法(如果允许修改日期),也需要防御性复制 public void setOrderDate(Date newDate) { if (newDate == null) { throw new IllegalArgumentException("New order date cannot be null."); } this.orderDate = new Date(newDate); // 存储副本 } }
通过这种方式,即使外部修改了原始的Date对象,或者尝试通过getOrderDate()获取到的引用来修改日期,Order对象内部的orderDate字段仍然是安全的,不会


