我在一次采访中被问到这个问题。面试官想知道如何使对象不可变。然后他问如果我序列化这个对象会怎样——它会破坏不变性吗?如果是,我该如何预防?谁能帮我理解这一点?
7 回答
不可变对象是一旦创建就无法更改的对象。您可以使用private
访问修饰符和final
关键字来创建这样的对象。
如果一个不可变对象被序列化,它的原始字节可以被修改,以便在反序列化时对象不再相同。
这是无法完全避免的。加密、校验和和 CRC 将有助于防止这种情况发生。
您应该阅读 Joshua Bloch 编写的 Effective Java。有一整章是关于与序列化相关的安全问题,并建议如何正确设计你的类。
简而言之:您应该了解 readObject 和 readResolve 方法。
更详细的答案:是的,序列化可以打破不变性。
假设您有课程 Period (这是 Joshua 书中的示例):
private final class Period implements Serializable {
private final Date start;
private final Date end;
public Period(Date start, Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end() > 0)
throw new IllegalArgumentException("sth");
}
//getters and others methods ommited
}
看起来不错。它是不可变的(初始化后不能更改开始和结束)、优雅、小巧、线程安全等。
但...
您必须记住,序列化是创建对象的另一种方式(它不使用构造函数)。对象是从字节流构建的。
考虑某人(攻击者)更改您的序列化字节数组的情况。如果他这样做,他可能会打破你关于 start < end 的条件。此外,攻击者可能会在流中(传递给反序列化方法)引用他的 Date 对象(它是可变的,并且 Period 类的不变性将被完全破坏)。
如果你不需要,最好的防御就是不使用序列化。如果您必须序列化您的课程,请使用序列化代理模式。
编辑(应 kurzbot 请求):如果您想使用序列化代理,您必须在 Period 内添加静态内部类。此类对象将用于序列化,而不是 Period 类对象。
在 Period 类中编写两个新方法:
private Object writeReplace(){
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Need proxy");
}
第一种方法用 SerializationProxy 对象替换默认的序列化 Period 对象。其次保证攻击者不会使用标准的 readObject 方法。
您应该为 SerializationProxy 编写 writeObject 方法,以便可以使用:
private Object readResolve() {
return new Period(start, end);
}
在这种情况下,您只使用公共 API 并确定 Period 类将保持不变。
当你序列化一个对同一个对象有多个引用的对象图时,序列化器会注意到这一事实,因此反序列化的对象图具有相同的结构。
例如,
int[] none = new int[0];
int[][] twoArrays = new int[] { none, none };
System.out.print(twoArrays[0] == twoArrays[1]);
将打印true
,如果您序列化和反序列化,twoArrays
那么您将得到相同的结果,而不是数组的每个元素都是不同的对象,如
int[][] twoDistinctArrays = new int[] { new int[0], new int[0] };
您可以利用此对引用共享的支持在序列化条目之后制作字节,以与私有帮助对象或数组共享引用,然后对其进行变异。
因此,不可序列化的类可以维护不变量——私有对象不会逃逸——而可序列化的类无法维护。
正如其他人所说,人们可以认为序列化会产生一个全新的对象,然后它是不可变的,所以不,序列化不会破坏它,但我认为在回答这个问题之前,我们必须考虑更大的不变性问题。
我认为真正的答案完全取决于被序列化的类,以及所需的不变性水平,但是由于面试官没有给我们源代码,我会想出我自己的。我还想指出,一旦人们开始谈论不可变性,他们就会开始抛出final
关键字——是的,这使得引用不可变,但这并不是实现不变性的唯一方法。好的,让我们看一些代码:
public class MyImmutableClass implements Serializable{
private double value;
public MyImmutableClass(double v){
value = v;
}
public double getValue(){ return value; }
}
这个类是可变的,因为我实现了Serializable
?它是可变的,因为我没有使用final
关键字吗?不可能 - 它在每个实际意义上都是不可变的,因为我不会修改源代码(即使你很好地要求我),但更重要的是,它是不可变的,因为没有外部类可以改变value
,没有使用反射将其公开,然后对其进行修改。出于这个原因,我想你可以运行一些中间的十六进制编辑器并手动修改 RAM 中的值,但这并没有使它比以前更易变。扩展类也不能修改它。当然,您可以扩展它,然后覆盖getValue()
以返回不同的东西,但这样做不会改变底层的value
.
我知道这可能会以错误的方式惹恼很多人,但我认为不变性通常是纯粹的语义 - 例如,对于从外部类调用您的代码的人来说它是不可变的,还是对于在您的主板上使用 BusPirate 的人来说它是不可变的?有很好的理由可以final
用来帮助确保不变性,但我认为它的重要性在不止几个论点中被大大夸大了。仅仅因为 JVM 被允许在后台做一些魔术以确保序列化工作并不意味着您的应用程序所需的不变性级别被破坏了。
通过将所有状态信息保持在创建对象后无法更改的形式,使其不可变。
在某些情况下,Java 不允许完美的不变性。
可序列化是您可以做的事情,但它并不完美,因为在反序列化时必须有一种方法来重新创建对象的精确副本,并且使用相同的构造函数来反序列化并在第一名。这就留下了一个洞。
一些事情要做:
- 只有私有或最终属性。
- 构造函数设置对操作至关重要的任何属性。
其他一些需要考虑的事情:
- 静态变量可能不是一个好主意,尽管静态最终常量不是问题。加载类时无法从外部设置这些,但以后不能再次设置它们。
- 如果传递给构造函数的属性之一是对象,则调用者可以保留对该对象的引用,如果它也不是不可变的,则更改该对象的某些内部状态。这有效地改变了对象的内部状态,该对象存储了该对象的副本,现在已修改。
- 从理论上讲,有人可以采用序列化形式并对其进行更改(或者只是从头开始构建序列化形式),然后使用它来反序列化,从而创建对象的修改版本。(我认为在大多数情况下这可能不值得担心。)
- 您可以编写自定义序列化/反序列化代码来签署序列化表单(或对其进行加密),以便可以检测到修改。或者您可以使用某种形式的序列化形式的传输,以保证它不会被更改。(这假设您在不传输时对序列化表单有一定的控制权。)
- 有字节码操纵器可以对对象做任何他们想做的事情。例如,将 setter 方法添加到原本不可变的对象。
简单的答案是,在大多数情况下,只需遵循此答案顶部的两条规则,这足以满足您对不变性的需求。
简单的答案是
class X implements Serializable {
private final transient String foo = "foo";
}
如果对象是新创建的,则字段 foo 将等于“foo”,但在反序列化时将为 null(如果不使用肮脏的技巧,您将无法分配它)。
您可以在 Java 中借助 SecurityManager 防止序列化或克隆
public final class ImmutableBean {
private final String name;
public ImmutableBean(String name) {
this.name = name;
//this line prevent it form serialization and reflection
System.setSecurityManager(new SecurityManager());
}
public String getName() {
return name;
}
}