SC01.Mutability & Immutability 可

作者: 小麻巧吃西瓜 | 来源:发表于2019-07-04 19:31 被阅读0次

    String是一个不可变类型,而StringBulider是一个可变类型。
    String对象总是代表同样的字符串,而StringBuilder有方法可以删除部分字符串,插入或替换字符。

    String s = "a";
    s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing
    
    StringBuilder sb = new StringBuilder("a");
    sb.append("b");
    

    通过图片可以更清晰地看出它们之间的区别。



    看起来结果似乎是一样的,那什么时候我们会需要使用StringBuilder呢?

    String s = "";
    for (int i = 0; i < n; ++i) {
        s = s + n;
    }
    

    在如上的代码中,使用不可变的String,会造成很多临时的复制,例如0在上述代码中会复制n次,而1会复制n-1次,这样简单的字符串连接操作造成的开销是O(n^2)
    这时StringBuilder就派上用场了。

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < n; ++i) {
      sb.append(String.valueOf(i));
    }
    String s = sb.toString();
    

    使用StringBuilder可以避免无用的复制操作。


    既然如此,看起来StringBuilder兼具了String的所有功能,还可以使用append()这样的便捷操作,那为什么事实上在和字符串打交道时我们经常使用的还是String呢?

    首先因为StringBuilder作为一个可变数据类型,而可变数据类型不是线程安全的。其次,String可符合我们所定义的好软件的三个性质:Safe from bugs,Easy to understand,Ready for change,可以看如下的例子加以理解。


    可能的危害1: 传递可变值

    public static int sum(List<Integer> list) {
            int sum = 0;
            for (int x : list)
                sum += x;
            return sum;
        }
        
        public static int sumAbsolute(List<Integer> list) {
            // let's reuse sum(), because DRY, so first we take absolute values
            for (int i = 0; i < list.size(); ++i)
                list.set(i, Math.abs(list.get(i)));
            return sum(list);
        }
        
        public static void main(String[] args) {
            // ...
            List<Integer> myData = Arrays.asList(-5, -3, -2);
            System.out.println(sumAbsolute(myData));
            System.out.println(sum(myData));
        }
    

    本着DRY原则(Don't repeat yourself),在sumAbsolute复用了sum方法,直接修改了list,这样或许是方便,但是造成了原始数据被修改,两行输出的结果均为10。这显然破坏了好软件的前两个准则,使得bug难以追踪,易读性也不好。


    可能的危害2: 返回可变值

    Date也是一个可变对象。
    假设下列方法用来定义一个方法,返回春天的第一天。(askGroundhog()意指使用Groundhog函数计算春天的第一天是什么时候,并没有实现)

    /** @return the first day of spring this year */
    public static Date startOfSpring() {
        return askGroundhog();
    }
    

    春天开始时举办一次party:

    // somewhere else in the code...
    public static void partyPlanning() {
        Date partyDate = startOfSpring();
        // ...
    }
    

    因为startOfSpring被反复调用,我们将它重写为仅调一次askGroundhog(),将结果缓存,以备之后再被调用。

    /** @return the first day of spring this year */
    public static Date startOfSpring() {
        if (groundhogAnswer == null) groundhogAnswer = askGroundhog();
        return groundhogAnswer;
    }
    private static Date groundhogAnswer = null;
    

    将聚会时间设置为春天来之后再延后一个月

    // somewhere else in the code...
    public static void partyPlanning() {
        // let's have a party one month after spring starts!
        Date partyDate = startOfSpring();
        partyDate.setMonth(partyDate.getMonth() + 1);
        // ... uh-oh. what just happened?
    }
    

    上面的代码会发生错误,当partyDate发生改变的时候,groundhogAnswer也发生变化。为防止这种bug我们可以或者使用不可变的类型,如使用java.time包中的类LocalDateTime , Instant,此包中的类都是线程安全的;或者可以采用防御式拷贝:

    return new Date(groundhogAnswer.getTime());
    

    但防御式拷贝存在的问题是很多时候用户调用startOfSpring方法只是为了查看,并不会修改数据,如若每一次调用都产生拷贝,则会消耗很多不必要消耗的空间。

    所以不可变类效率更高。


    综合前两种危害可能产生的原因都是因为对于一个可变对象做了多个引用。事实上,对于可变类型,如果仅在本地的一个方法里面使用它,就还相对安全。真正容易出问题的情况是对一个可变对象有多个引用,即别名


    使用建议:
    使用不可变类型,下面简介一些常用的不可变类型:

    • 原始数据类型都是不可变的。java.lang.Number的子类中的BigIntegerBigDecimal也是不可变的。
    • Java中的集合类型, List,Set,Map,包括它们的子类ArrayList,HashMap都是可变类型。使用它们的时候,可以使用它们的不可修改视图。Collections.unmodifiableList,Collections.unmodifiableSet,Collections.unmodifiableMap。注:被不可修改视图包装的可变对象仍然可以被它的引用修改,就像关键字final后的可变对象可以被修改。
    • Collections也提供了获取不可变的空集合的方法,Collections.emptyList

    总结:
    尽量使用不可变对象。


    说到可变不可变问题,就不得不讨论锁与线程安全的问题了,所以SC02中将会涉及这方面的内容。

    相关文章

      网友评论

        本文标题:SC01.Mutability & Immutability 可

        本文链接:https://www.haomeiwen.com/subject/ojsrhctx.html