对于输入的Defensive Copy

  在软件构造的学习中,我们已经了解了防止表示泄露的重要性,也见过不少针对返回值的防御性拷贝了,但是,防御性拷贝仅仅针对返回值生效吗?其实不然,现在让我们来见识一下对输入进行防御性拷贝的方案。

一、概念

  • 关于表示泄露(Representation Exposure)

表示泄露形容了一种类外部的代码可以直接修改类内部存储的数据的现象。通常是类外代码直接对类内的可变类型对象进行直接修改,导致了之前所提到过的引用别名现象,这种引用方式会导致我们对同一块内存进行多次引用,导致程序运行过程中的不确定性,恶意代码也可以十分简单的对ADT进行攻击,严重影响ADT的表示独立性和不变量。

  • 防御性拷贝与表示泄露

防御性拷贝是防止表示泄露的方法之一。

防御性拷贝通常用于阻止这种情况:当我们在编程中,从某个对象那的内部传出了一个内部对象的引用,导致我们这个对象内部的状态被修改,这就是一种表示泄露。

防御性拷贝是如何运作的?简单来说,我们应当在传出引用时,传出内部对象的复制的引用,这样,你对这个参数进行修改的时候跟封装类内部的相关参数无关,也就不会改变类中的参数。这就是“Defensive Copy”。

  • 关于可变类型与不可变类型

可变类型:

即可以对该类型对象中保存的元素值做修改,如列表、字典都是可变类型。

不可变类型:

即该类型对象所保存的元素值不允许修改,如果修改它,实际上就是创建了一个新的不可变类型的对象、而不是修改原对象的值,如数字、字符串、元组都是不可变类型。

       对于不可变类型,修改它是非常安全的,因为我们无论如何修改,都得到的是新的对象,原本的对象不会改变,而可变类型的所有引用对它的修改都会纤毫毕现地反馈在原来的对象上,所以我们需要进行防御性拷贝。

二、示例

       作为类的设计者,我们必须假定类的客户端会竭尽所能来破坏你的类,这样才能设计出安全的程序。例如:

 1 class Poem {
 2     public String title;
 3     public String author;
 4     private List<String> lines = new ArrayList<>();
 5     private Date date;
 6     // AF: 代表一首诗,包含四个属性:
 7     // title为诗的题目,
 8     // author为诗的作者,
 9     // lines为诗的文本行,
10     // date为诗的发表日期
11     public Poem(String t, String a, List<String> l, Date d) {
12         title = t;
13         author = a;
14         lines = l;
15         date = d;
16     }
17     public void addOneLine(String newLine) {
18         lines.add(newLine);
19     }
20     
21     //..    
22 
23     public List<String> getAllLines() {
24         return lines;
25     }
26 }

在这个ADT中实际上是存在着一些表示泄露的风险的。

在构造函数里,直接使用了lines = l; 这样的语句,本意试想把输入的List<String> l拷贝给rep里的lines属性。但是由于List是对象数据类型,所以实际上此时l与line指向的是同一个对象,一旦客户端改变l中的值,line也会跟着改变,这样是很危险的。

在构造函数里,还出现了date = d;语句。Date是对象数据类型,一旦客户端改变d中的值,date中的值也会跟着改变,这样是很危险的。

在getAllLines()方法中,直接把rep中的lines放回给了用户,这样用户可以随意修改返回的List,有可能会改变ADT中的rep,这也违反了表示独立性。

我们尝试使用defensive copy来解决这些问题:

  • List问题

对于List,我们实际想要复制的是整个List里的所有对象,而不是指向这个List的指针。

我们可以遍历原始List,再将List里的所有元素拷贝到新的List中,代码如下。

 1 class Test {
 2     
 3     public static void main(String[] args) {
 4         List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
 5         List<String> copyList = new ArrayList<>();
 6         for(int i=0; i<list.size(); i++) {
 7             copyList.add(list.get(i));
 8         }
 9         list.remove(0);
10         System.out.println("list:"+list.toString());
11         System.out.println("copylist:"+copyList.toString());
12     }
13 }

也可以通过List对象的addAll()方法,实现List中元素的拷贝(俗称“深拷贝”)。

Java中的一些其他的集合类(比如Set,Map等)也可以通过类似的方法实现防御式拷贝,进而避免外来的改变对ADT造成影响。

  • Date问题

        Date与List相比就要简单一些了,我们只需要创建一个新的Date对象,保证这个对象的值和原来的对象一样即可。

        代码:

 1 class Test {
 2     
 3     public static void main(String[] args) {
 4         Date date = new Date();
 5         Date copyDate = new Date(date.getTime());
 6         date.setTime(2000000000);
 7         System.out.println("date:"+date.toString());
 8         System.out.println("copydate:"+copyDate.toString());
 9     }
10 }

        结果:

        我们成功创建了两个完全独立的Date对象,这正是我们想要的。

三、忠告

其实,从广义的角度看,不仅仅是出现返回值时需要进行defensive copy,其实在构造函数里如果对用户输入的对象不进行有效的拷贝,用户之后的修改其实也可能对你设计的ADT产生影响,特别是你用到list、map等类连接元素时。此时设计者有必要考虑一下,我是否能够容忍这种可变性。如果你认为可能会对你的程序造成影响,那么此时对于输入参数的defensive copy就是必要的。毕竟,安全性总是比效率更重要。

posted @ 2022-05-18 20:12  WildMice  阅读(89)  评论(0)    收藏  举报