Java表示泄露可能出现的原因及预防方法
- 表示泄露的概念
表示泄露就是指用户不通过我们ADT提供的方法,就可以修改ADT的内容,出现不必要的麻烦。这种情况下,内部的数据结构就被泄露出去了,外部client可以看到或直接对我们的数据进行操作。这种风险即影响ADT的不变量RI(因为用户修改属性的值可能不再满足RI),也会影响到表示独立性(外部用户的使用已经与内部的实现产生了依赖),所以会出现许多难以处理的麻烦。所以对于一个ADT来说,最重要的就是RI和避免表示泄露。下面来总结一下可能出现表示泄露的原因以及如何预防表示泄露。
- 表示泄露可能出现的原因
- 属性和方法的可见度过高,没有实现较好的封装
这部分已经在之前的封装部分总结中进行了说明,此处再复述一次该部分的内容。
首先编写一个Person类,用来表示人这个类,此处我们只为这个类设计姓名和年龄两个成员变量:
这时我们在main方法中就可以创建一个人的对象,并设置这个对象的name和age,如图所示:
这时我们就可以得到这个人的姓名和年龄信息。
但是这时可能会出现一个问题:一般一个人(也就是一个对象)是不会改名的,但是按照我们上面的这种方案,这个对象的名字是可以被任意更改的,如图所示:
我们可以看到,这个对象的名字是可以被用户随意修改的,那么我们这个系统就存在一个巨大的漏洞:我们的类的属性是暴露给用户的,用户可以对对象的属性进行任意的更改。
上面这个漏洞看起来并不致命:有人说一个人本来也有可能改名字啊,那我允许用户修改名字也没有什么问题啊!
那我们在考虑一种情况,比如支付宝:
那按照现在的这一种设计模式,我可以修改这个对象的余额,如下所示:
我们发现我是可以修改这个对象的余额的,这就是一个非常致命的漏洞了,如果阿里支付宝是这么设计的那不就出大问题了!(如果真是这样设计的不就都不用上班了doge),所以我们需要对我们设计的这个类进行封装。封装的具体方法在下一部分预防的内容讲述。
- 在方法传入的参数中传入了mutable类型的参数
我们以下面这个例子为例说明该做法可能出现的问题:
如这个例子所示,我们有两个方法,一个是计算数组中的元素和,另一个是计算数组中元素绝对值的和,两个方法传入的参数都是mutable类型的List。单独观察这两个方法,它们似乎都可以实现它们spec中规定的功能。但是如果执行main函数,我们发现构建一个-5,-3,-2的List后先计算它们绝对值的和可以得到正确结果10,但是再想计算这个List元素的和时,本来应该为-10,但是结果也变为了10.
这是为什么呢?
我们仔细观察计算绝对值之和的函数sumAbsolute,发现这个方法中是这样计算绝对值的:把List中每一个位置的数改变成它的绝对值,再把所有元素进行相加。这种实现不只是计算了绝对值的和,还修改了最初的List,这也就暴露出问题所在了。
在main执行时,在执行完sumAbsolute方法后,由于List时mutable类型其元素可以改变,在执行完后内部存储的数变成了5,3,2.这时已经不是之前的List了,而这种问题对于我们实际编程时相对较难发现,所以我们最好使用immutable类型。
- 返回了mutable类型的变量
我们来看这样的一个例子,可以说明在方法中直接返回mutable类型的变量可能出现的严重问题:
在这个例子中,方法最后直接返回了一个mutable类型的Date,由于Date是mutable类型的,指向的是同一个地址空间,那么用户就可以通过修改方法返回的值来修改ADT中属性的值,造成难以理解的错误。
这个问题还可以用另一个例子来展示:
这个ADT抽象了一个直角三角形的类。其中getAllSides返回了一个数组,其中是直角三角形的三条边,而数组是mutable类型的,假设有这样一个直角三角形两条直角边分别为1和√3,斜边为2.其中根据要求sides[2]中存储斜边2,假设sides[0]中为1,sides[1]中为√3。返回给用户sides后用户得到了mutable类型的数组,如果用户在得到的数组基础上进行操作,如sides[1]=2;这中操作是可以通过编译的,运行时也不会报错。但是用户这一个操作直接修改了最初的直角三角形对象的属性,而修改后的对象不再是一个直角三角形了,违反了RI。这就是发生表示泄露的另一种原因以及可能出现的严重后果。
那么我们如何避免表示泄露呢?
- 避免表示泄露的方法
- 尽可能把所有public都变成private
这种方法也就是之前所描述的封装方法。在修改成private之后,这个类之外就不能直接访问private的属性,想要访问属性就需要我们这个类提供的getter和setter方法。这部分在之前封装的部分已经有表述,此处不再展示。
- 在属性之前加上final
这种方法主要是为了避免内部方法在无意中修改属性。
- 使用防御式拷贝
防御式拷贝这种方法的意思是,如果需要返回mutable类型的对象,在返回之前进行一定的处理,大概可以有以下两种方式:
①对于要返回的mutable对象复制一个副本,然后在返回时返回新new的副本,对于上述最后一个示例,可以以如下方式返回:
以这种方式返回时,返回给用户的就是一个副本,与ADT中的属性sides指向的不再是一个地址空间,可以有效避免表示泄露。
②使用不可变的包装把mutable类型的对象包装起来:
不可变的包装:unmodifiableList,unmodifiableMap,unmodifiableSet。这三个不可变的封装在后续避免表示泄露部分都有着重要的应用。
在使用不可变的包装后,无法通过包装后的这个对象来修改这个地址空间内的值,可以在一定程度上防止表示暴露。
但是这种方法由于包装后与包装前的对象指向同一个地址空间,如图所示,可能出现用户可以通过为包装前的对象的方法来修改包装后对象内部的值。所以我们在返回一个unmodifiable对象后一定要防止用户对于原始的mutable对象进行操作,要把对于原始mutable对象的引用屏蔽掉,才可以达到避免表示泄露的问题。
总结:最好的方法是使用immutable类型,彻底避免表示泄露!