学Guava发现:不可变特性与防护性编程

1、面试常谈:String类与不可变特性

问:String类是可变的吗?java

答:emm……因为String类的底层是final关键字修饰,所以它是不可变的。程序员

问:它被设计为不可变的好处有哪些呢?面试

答:数据库

  • 节约内存编程

    你们都知道,编程的时候,String类是大量被使用的(试着用VisualVm等工具分析堆,你会发现永远char[]类型是占用空间最多的。巧了,String类的底层实现也正是char[])。数组

    若是像普通对象那样,每次使用都new一个,恐怕你设置的JVM 堆大小得慎重考虑一下了。缓存

    所以出现了一个叫作常量池的东西,好比String a="abc"String b="abc",那么a和b都指向常量池的"abc"这个地址。这样,多个变量,能够共用一个常量池地址,节约了内存。安全

  • 线程安全多线程

    常说实现线程安全的方法之一就是使用final关键字将变量修改成常量,那么为何不可变的常量是线程安全的呢?并发

    很简单,好比多线程并发修改同一变量,若是不加同步进行控制,必然会出现数据不一致问题。可是因为String类是不可变的,根本就不支持你修改,那怎么可能出现数据不一致问题呢?(感受像是在扯淡,o(∩_∩)o 哈哈!)

  • 数据安全

    这里的数据安全,就和下文说道的防护性编程有关系了。

    假设String类可变:

    String name1 = "张三";
    String name2 = name1;
    user.setName(name1);
    name2 = "李四";
    System.out.println(user.getName());
    
    输出:李四
    复制代码

    what?这位用户明明名字叫张三,咋个无故变成李四了?

  • 提升缓存效率

    你们都知道HashMap.put(key,value),须要对key进行hashcode运算。

    hashcode是String类型。由于String的不可变特性,就不须要担忧hashcode值被修改,能够缓存起来屡次使用,减小hashcode计算次数。

2、进阶梳理:不可变特性与防护性编程

有一个Period 类

public final class Period {

    private final Date start;
    private final Date 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 getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}
复制代码

乍一看,这个相似乎是不可变的(即类中的数据不会发生变化)。而事实上,真的是这样吗?

Date start = new Date();
Date end = new Date();

Period p = new Period(start, end);

end.setYear(78); 

System.out.println(p.getEnd().toLocalString());

输出:1978-3-2 18:38:40
复制代码

以上代码和刚刚的那个“张3、李四”的例子很像。在类实例的外部,直接修改无关变量值,最后致使类实例内部的数据也变化了。

这种状况每每不易被程序员在编码时所发现,从而因为数据的变化致使业务bug。

所以,想要把Period类设计为一个不可变的类,有这么几种方案:

  • InstantLocalDateTimeZonedDateTime来代替Date

    使用从Java 8开始,解决此问题的显而易见的方法是使用InstantLocalDateTimeZonedDateTime来代替Date。由于Instant和其余java.time包下的类是不可变的。Date已过期,不该再在新代码中使用

  • Period类重设计
public Period(Date start, Date end) {
	
	//防护性拷贝:构造一个新Date对象,这样,这个内部的start变量和外面的那个start变量将没有任何联系
    this.start = new Date(start.getTime());
    this.end   = new Date(end.getTime());

    if (this.start.compareTo(this.end) > 0)
      throw new IllegalArgumentException(this.start + " after " + this.end);
}
复制代码

上面提到了一个名词:“防护性拷贝”,很确切。除了构造新Date对象,还有深克隆的方式,可是此处不推荐使用克隆。至于为何?因为篇幅有限,你们可自行百度!

那么,这样就实现了Period类不可变了吗?

并无!因为该类内部的私有数据还提供了getter方法,所以仍然可能经过getter方法修改该类的内部数据。

所以,咱们还须要:

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

    public Date getEnd() {
        return new Date(end.getTime());
    }
复制代码

这个有点像数据库中的视图了,能够给你看,但你不能修改源!


最后总结一下,防护性编程究竟是什么呢?

防护性编程是一种比较泛化的概念,是一种细致、谨慎的编程习惯。

咱们在写代码的时候,须要时刻考虑到:代码是否正确? 代码是否正确? 代码是否正确?

例如:

  • 你能够利用不可变特性、构造时拷贝对象等方法来确保一个类的不可变
  • 不少时候,考虑使用防护性拷贝,避免直接在原始实例上进行操做
  • 接收参数时考虑参数的是否非空等
  • 是否引起性能问题、死锁问题
  • ……

3、JAVA设计:我感觉到的防护性编程

一、String、Integer等的不可变特性

缘由上面已经说明了!

二、Arrays.asList返回仅可查看的“视图”

Arrays.asList()返回一个ArrayList内部类,没有add()remove()没法改变长度等,这样设计的初衷是什么?为何不直接返回可变长的ArrayList(new ArrayList())?

和咱们刚刚的重写getter方法相似,用于保证对象安全不可改变特性!

举个例子,就是你有一个数组,怎么设计一个方法:保证既能够遍历,又不能修改呢?

返回一个继承了List接口的轻量级“视图”不失为一个好的设计方式。而直接返回数组则是不安全的选择。

三、不可变集合的各类实现

为何须要不可变集合?

不可变对象有不少优势,包括:

  • 当对象被不可信的库调用时,不可变形式是安全的;
  • 不可变对象被多个线程调用时,不存在竞态条件问题
  • 不可变集合不须要考虑变化,所以能够节省时间和空间。全部不可变的集合都比它们的可变形式有更好的内存利用率(分析和测试细节);
  • 不可变对象由于有固定不变,能够做为常量来安全使用。
  • 建立对象的不可变拷贝是一项很好的防护性编程技巧。

若是你没有修改某个集合的需求,或者但愿某个集合保持不变时,把它防护性地拷贝到不可变集合是个很好的实践。

JDK的实现

JDK的Collections类提供如下不可变集合,用于开发者的一些不可变需求:

Guava的实现

同时,Guava亦提供如下不可变集合:

相关文章
相关标签/搜索