辉夜的博客

繁花似锦,辉夜如昼

0%

软件构造-关于表示泄露2-进阶体会

我们上次讨论了关于表示泄露的三种基本形式:访问权限、getter传递引用、constructor接收引用,这次来讨论更加细节的内容

原创性声明:每一个字都是我手敲的。

更加间接的表示泄露-来自浅拷贝

我们对一个引用类型(对象)进行拷贝时,有三种做法。

  1. 引用拷贝, 通常通过=运算符实现。两个引用变量将指向同一个内存地址。
  2. 浅拷贝,一些方法提供了浅拷贝。我们把一个对象的字段值全部拷贝到另一个对象里,然后把这个对象的引用赋值给新的引用
  3. 深拷贝,一些方法提供了深拷贝,通常需要手动实现。对于基本数据类型字段,我们把值拷贝到另一个对象;对于引用类型字段,我们
    把这个字段深拷贝到另一个对象的这个字段中,最后把新的对象的引用赋值给新的引用。

这三个概念有些抽象,我们必须结合一些例子来进行讲解。但是我们可以先注意一点:引用拷贝是一次赋值,浅拷贝对字段遍历赋值,深拷贝是对字段递归地赋值。

参考这篇文章

如果防御性拷贝只是浅拷贝,那么依然存在风险,考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class qaq{
private final List<waw> a = new LinkedList<>();
private final String name;
qaq(Stirng name){this.name = name;}
public void add(waw w){a.add(w);}
public List<waw> getA(){return new LinkedList<waw>(a);}
public String getName(){return name;}
}
class waw{
private final int label;
waw(int label){this.label = label;}
public int getLabel(){return label}
public void setLabel(int label){this.label = label;}
}

和如下的客户端程序

1
2
3
4
5
6
7
8
public static void main(String[] args) {
qaq q = new qaq("qaq");
q.add(new waw(1));
//浅拷贝
qaq.getA().get(0).setLabel(2);
System.out.println(qaq.getA().get(0).getLabel())

}

现在,q中的a字段的第零个元素的label还会是1嘛?答案是否定的。
究其原因,是因为 qaq.getA()返回的是对LinkedList类型的浅拷贝,这个类型中存放的内容是waw,一个可变引用类型。
因此,当我们对浅拷贝(对应qaq.getA())调用getter(对应get)获取一个可变引用,再对这个引用调用mutator(对应setLabel
时,就会把拷贝的母本中的对象也修改!
再次体会一下这个逻辑:母本中的内容是一个引用,我们通过浅拷贝获取了这个引用,修改了引用对应的对象,于是母本的内容虽然没有改变,但是母本的内容指向的对象却发生了改变。
这已经很抽象了!为了避免这个方法,对于任何可变类型的List,Map等,在拷贝时要特别注意深浅。如果是List,可以不用深拷贝,因为即使浅拷贝获得了母本的内容,也不能通过这个内容修改对象的值。

一些探讨-public final会导致rep exposure吗?

MIT的课件中说表示泄露“meaning that code outside the class can modify the representation directly.”,
不过我认为这是不准确的,即使不能修改ADT的表示,而仅仅能够获取ADT的内部结构,客户端也足以让ADT的抽象性泄露。
我们考虑下面这个数据结构:

1
2
3
4
5
6
7
8
9
/** Represents an immutable right triangle. */
class RightTriangle {
/*A*/ private double[] sides;

// sides[0] and sides[1] are the two legs,
// and sides[2] is the hypotenuse, so declare it to avoid having a
// magic number in the code:
/*B*/ public static final int HYPOTENUSE = 2;

其中,HYPOTENUSE被定义为是public static final int,因此它是作为一个不可变类属性。然而,其访问权限是public,这
意味着用户将有可能使用这个特殊的成员。比如说,编写一些依赖于HYPOTENUSE的代码,而一旦这个值改变,那么这些代码都需要进行修改!
要注意,“斜边存储在边数组的哪个位置”完全不是这个ADT应该暴露出来的东西,因此,尽管它是不可被修改的,但是它依然会导致表示泄露。