2015062706 - EffactiveJava笔记 - 第39条 必要时进行保护性拷贝

    第39条 必要时进行保护性拷贝

    ----->01.保护性拷贝发生的背景

   假设类的客户端会及其所能地破坏类的约束条件,因此你必须保护性地设计程序.实际上,只有当有人试图破坏系统的安全性时,才可能发生这种情况.

   更加可能的是,对你的API产生误解的程序员,导致各种不可预期的行为,那么只好由类处理.无论何种情况,编写一些面对客户端的不良行为时仍能保持健壮性的类,这是非常值得投入时间去做的事情.

   [看了三遍,感觉这篇文章写得非常爽]

 

   ----->02.保护性拷贝反例

   没有对象帮助的时候,虽然一个不可能修改对象的内部状态,但是对象很容易在无意识情况下,提供这种帮助.例如下面的类,它声称可以表示一段不可变的周期.

import java.util.Date;

public final class Period {

     private final Date start;

     private final Date end;

    

     /**

      * @param start thebeginning of the period

      * @param end the end ofthe period; must not precede start

      * @throwsIllegalArgumentException if start is after end

      */

     public Period(Date start, Date end) {

            if (start.compareTo(end) > 0) {

                   throw new IllegalArgumentException(start + "after " + end);

            }

           

            this.start = start;

            this.end = end;

     }

 

     public Date start(){ return start; }

     public Date end(){ return end; }

}

 

   第一眼看过去,这个类似乎是不可改变的,并且强加约束条件:周期的起始时间不能在结束时间之后.然而Date对象是可以改变的,因为很容易违反此约束条件:

   Date start = new Date();

   Date end = new Date();

   Period p = newPeriod(start, end);

   end.setTime(78);

 

   为了保护Period实例内部信息避免受到这种攻击,对于每个构造器的每个可变参数进行保护性拷贝是有必要的,并且作为备份对象Period实例的组件,而不是用原始的对象.

     /**

      * @param start thebeginning of the period

      * @param end the end ofthe period; must not precede start

      * @throwsIllegalArgumentException if start is after 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(start + "after " + end);

            }

     }

   使用新的构造器,上述的攻击对于Period实例不再有效.注意,保护性拷贝是在检查参数有效性之前进行的,并且有效性检查针对的是拷贝之后的对象,而不是针对原始对象.

   为什么这么做呢? 这样做避免在危险阶段期间从另外一个线程改变类的参数,这里说的危险阶段是指检查参数有效性,直到拷贝参数之间的时间段.

 

   ------>03.保护性拷贝为什么不使用clone方法

   注意:我们没有使用Date的clone方法来进行保护性拷贝,因为Date不是final的,不能保证clone方法一定返回类为  java.util.Date的对象.它有可能返回专门处于恶意目的而设计的不可信任子类的实例.

   例如:这样的子类可以在每个实例被创建的时候,把指向该实例的引用记录到一个私有的静态列表中,并且允许攻击者访问这个列表,这使用攻击者可以自由地控制所有的实例.

   为了防止这种攻击,对于参数类型可以被不信任放子类化的参数,请不要使用clone方法进行保护性拷贝.

   [原来看这本书没看出什么好处,现在有足够的经验,感觉非常棒啊,纠正很多自己的错误经验,好爽!]

 

   --------->04.通过改变属性进行攻击

   替换掉构造方法可以避免上述攻击,但是改变Period实例仍然是可能的.因为它的访问方法提供了对其可变的内部成员方法的能力.

   Date start = new Date();

   Date end = new Date();

   Period p = newPeriod(start, end);

   p.end().setTime(78);


   为了防御上面的攻击,只需要修改这两个访问方法,使它返回可变内部域的保护性拷贝即可.

   public Date start(){ return new Date(start.getTime()); }

   public Date end(){ return new Date(end.getTime()); }

   采用新的构造方法和新的访问方法后,Period真正不可变了.绝对不会违反周期的开始时间不能大于结束时间.除了Period类自身之外,其他任何类都无法访问Period实例中的任何一个可变域.这些域被真正封装在对象的内部.

   普通方法和构造方法不同,普通方法进行保护性拷贝允许使用clone方法.为什么呢?因为我们知道,Period内部的Date对象的类是java.util.Date,而不可能是其它某个潜在的不可信的子类.[基于11条,一般情况下,使用构造方法或者静态工厂,11条还没有写笔记]

 

    ----->05.什么时候进行保护性拷贝

   参数的保护性拷贝,不仅仅针对不可变的类.每当编写方法或者构造方法时,如果它要允许客户提供的对象进入到内部数据结构,则有必要考虑一下,客户提供的对象是否有可能是可变的.如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化.如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝后的对象而不是原始对象进入到数据结构中.

   例如,如果你正在考虑使用由客户端提供的对象引用作为内部set实例的元素,或者内部Map实例的键,就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏.

   在内部组件被返回给客户端之前,对它们进行保护性拷贝是同样的道理.不管类是否为不可变的,在把一个指向内部可变组件的引用返回给客户端之前,也应该加倍认真地考虑.解决方案,应该进行保护性拷贝.记住,长度非0的数组总是可变的.因此,把内部数组返回给客户端之前,应该进行保护性拷贝.

   另外一种方案是,给客户端返回该数组的不可变视图.[13条,笔记没记录呢]

   如果有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必为保护性拷贝操心.

   在Period类中,有经验的程序员通常使用Date.getTime()返回的long基本数据作为内部的时间表示法,而不是使用Date对象引用.之所以这么做,是因为Date是可变的.

     public long start(){ return start.getTime(); }

     public long end(){ return end.getTime(); }

 

    ----->06.保护性拷贝和性能损失

    保护性拷贝未必对性能有太多的损失.如果类信任它的调用者不会修改内部的组件,可能因为类及其客户端都在同一个包的双方,那么不进行保护性拷贝是可以的.这种情况下,类的文档就必须说清楚,调用者不能修改收到影响的参数或者返回值.[防止人为影响,还是进行保护性拷贝为好]

    即使跨越包的作用范围,也并不总是适合在将可变参数整合到对象中之前,对它进行保护性拷贝.有些方法和构造器的调用,要求参数对所用的对象必须有个显式交接(handoff)过程.当客户端调用这样的方法时,它承诺以后不再直接修改该对象,如果方法或者构造器期望结果一个由客户端提供的可变对象,它就需要在文档中明确指明这一点.

   [如何显式交接呢?如果在方法内获取对象的引用,方法外部依旧有此对象的引用,如果方法外部的引用不失效,那么方法内部的引用不算是交接,因为外部依旧可以修改对象.如何实现交接呢?除了在方法外部将已有的引用设置为null,还怎么处理呢?]

   如果类所包含的方法或者构造器的调用需要移交对象的控制权,这个类就无法让自身抵御恶意的客户端.只有当类和它的客户端之间有着相互的信任,或者破坏类的约束条件不会伤害到除了客户端之外的对象时,这种类才可以接受.

   后一种情景是包装类模式(第16条,没有笔记呢),根据包装类的本质特征,客户端只需在对象被包装之后直接访问它,就可以破坏包装类的约束条件,但是,这往往只会伤害客户端自己.[没有体会到,没代码]

 

   如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件.如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改收到影响的组件,以此来代替保护性拷贝.

   [如果在一个类A引用类B,获取类B的引用就可以直接修改类B内的数据,如果在hibernate保存中,那么做关联后,那么类B内部的数据就会被改变,因为引用不改变,但是引用后的属性可以进行改变,是否需要保护性拷贝呢?]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值