我爱编程

专题整理之—不可变对象与String的不可变

2018-11-27  本文已影响4人  你想要怎样的未来

为了加深对Java语言的理解,加深对Java各种特性的理解与掌握,平常会自己归纳一些专题的分析和总结。基于自己的理解,感觉哪些部分适合在一起进行总结,就归纳为一个专题了。可能一个专题里面的东西也不属于一个类别,或者也比较杂乱,请见谅。
本文是前段时间对于不可变对象的学习,然后联系到了一个非常重要的不可变对象String,所以在此对这两者进行一个整理与总结。
文中部分论证方式和结论是在一些博客上学到,借鉴过来的,如有侵权,请联系删除。

1. 什么是不可变对象、不可变类

不可变对象的状态在构造后不能被修改,任何修改都应产生新的不可变对象。
不可变类的所有属性都应该是final。
不可变对象应该是final的,以限制子类来修改父类的不变性。
不可变对象必须正确构造,即对象引用在构造过程中不能泄漏。

不可变对象的类即为不可变类。如String、基本类型的包装类、BigInteger和BigDecimal等。

2. 不可变对象的优缺点

优点:

public class CacheImmutale {  
    private final String name;  
    private static CacheImmutale[] cache = new CacheImmutale[10];  
    private static int pos = 0;  
  
    public CacheImmutale(String name) {  
        super();  
        this.name = name;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public static CacheImmutale valueOf(String name) {  
        // 遍历已缓存的对象  
        for (int i = 0; i < pos; i++) {  
            // 如果已有相同实例,直接返回该缓存的实例  
            if (cache[i] != null && cache[i].getName().equals(name)) {  
                return cache[i];  
            }  
        }  
        // 如果缓冲池已满  
        if (pos == 10) {  
            // 把缓存的第一个对象覆盖  
            cache[0] = new CacheImmutale(name);  
            pos = 1;  
            return cache[0];  
        } else {  
            // 把新创建的对象缓存起来,pos加1  
            cache[pos++] = new CacheImmutale(name);  
            return cache[pos - 1];  
        }  
    }  
  
    @Override  
    public int hashCode() {  
        return name.hashCode();  
    }  
  
    @Override  
    public boolean equals(Object obj) {  
        if (obj instanceof CacheImmutale) {  
            CacheImmutale ci = (CacheImmutale) obj;  
            if (name.equals(ci.getName())) {  
                return true;  
            }  
        }  
        return false;  
    }  
  
    public static void main(String[] args) {  
        CacheImmutale c1 = CacheImmutale.valueOf("hello");  
        CacheImmutale c2 = CacheImmutale.valueOf("hello");  
        System.out.println(c1 == c2);// 输出结果为true  
    }  
}

缺点:

3. 怎样创建不可变类

  1. 将类声明为final,确保类不能被继承。(因为继承会破坏类的不可变特性,继承类覆盖父类的方法并且继承类可以改变成员变量值,那么就不能保证父类不可变)
  2. 将所有的成员声明为private和final的,这样就不允许直接访问这些成员。
  3. 如果成员属性为可变对象属性,不能共享对可变对象的引用,不要存储传给构造器的外部可变对象的引用。(因为可变对象的成员变量的引用和外部可变对象的引用指向同一块内存地址,这样可以在不可变对象之外修改可变属性的值)。
//存储传给构造器的外部可变对象的引用
public final class ImmutableTest {
    private final int[] array;
    public ImmutableTest(int[] array) {
        this.array = array; //error
    }
    public int[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1,2,3,4};
        ImmutableTest immutableTest = new ImmutableTest(arr);
        System.out.println(Arrays.toString(immutableTest.getArray()));
        //在外部改变可变对象的值
        arr[2] = 5;
        System.out.println(Arrays.toString(immutableTest.getArray()));
    }
}
//Output:
[1, 2, 3, 4]
[1, 2, 5, 4]

//使用深度拷贝的方法来复制一个对象并传入副本的引用来确保类的不可变
public final class ImmutableTest {
    private final int[] array;
    public ImmutableTest(int[] array) {
        this.array = array.clone(); //clone
    }
    public int[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1,2,3,4};
        ImmutableTest immutableTest = new ImmutableTest(arr);
        System.out.println(Arrays.toString(immutableTest.getArray()));
        arr[2] = 5;
        System.out.println(Arrays.toString(immutableTest.getArray()));
    }
}
//Output:
[1, 2, 3, 4]
[1, 2, 3, 4]
  1. 通过构造器初始化所有成员,进行深拷贝(deep copy)。
  2. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
//构造器浅拷贝
public class ImmutableTest2 {
    private final int id;
    private final String name;
    private final HashMap testMap;
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    //返回实际引用
    public HashMap getTestMap() {
        return testMap;
    }
    //浅拷贝构造器
    public ImmutableTest2(int id, String name, HashMap testMap) {
        this.id = id;
        this.name = name;
        this.testMap = testMap;
    }

    public static void main(String[] args) {
        int i = 1;
        String s = "a";
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        ImmutableTest2 immutableTest2 = new ImmutableTest2(i, s, hashMap);
        System.out.println(s == immutableTest2.getName());
        System.out.println(hashMap == immutableTest2.getTestMap());
        System.out.println("1,id=" + immutableTest2.getId());
        System.out.println("1,name=" + immutableTest2.getName());
        System.out.println("1,hashmap=" + immutableTest2.getTestMap());
        i = 2;
        s = "b";
        hashMap.put("3","c");
        System.out.println("2,id=" + immutableTest2.getId());
        System.out.println("2,name=" + immutableTest2.getName());
        System.out.println("2,hashmap=" + immutableTest2.getTestMap());
        HashMap<String,String> hashMap1 = immutableTest2.getTestMap();
        hashMap1.put("4","d");
        System.out.println("3,hashmap="+immutableTest2.getTestMap());
    }
}
//Output:
true
true
1,id=1
1,name=a
1,hashmap={1=a, 2=b}
2,id=1
2,name=a
2,hashmap={1=a, 2=b, 3=c}
3,hashmap={1=a, 2=b, 3=c, 4=d}
//可以看出,hashmap的值被更改了,这是因为构造器实现的是浅拷贝,而且在get方法中返回的是原来的引用。
//构造器深拷贝
public class ImmutableTest2 {
    private final int id;
    private final String name;
    private final HashMap testMap;
    public int getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    //拷贝
    public HashMap getTestMap() {
//        return testMap;
        return (HashMap) testMap.clone();
    }
    //浅拷贝构造器
//    public ImmutableTest2(int id, String name, HashMap testMap) {
//        this.id = id;
//        this.name = name;
//        this.testMap = testMap;
//    }
    //深拷贝构造器
    public ImmutableTest2(int id, String name, HashMap testMap) {
        this.id = id;
        this.name = name;
        HashMap<String, String> tempMap = new HashMap<>();
        String key;
        Iterator iterator = testMap.keySet().iterator();
        while (iterator.hasNext()) {
            key = iterator.next().toString();
            tempMap.put(key, testMap.get(key).toString());
        }
        this.testMap = tempMap;
    }
    public static void main(String[] args) {
        int i = 1;
        String s = "a";
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("1", "a");
        hashMap.put("2", "b");
        ImmutableTest2 immutableTest2 = new ImmutableTest2(i, s, hashMap);
        System.out.println(s == immutableTest2.getName());
        System.out.println(hashMap == immutableTest2.getTestMap());
        System.out.println("1,id=" + immutableTest2.getId());
        System.out.println("1,name=" + immutableTest2.getName());
        System.out.println("1,hashmap=" + immutableTest2.getTestMap());
        i = 2;
        s = "b";
        hashMap.put("3", "c");
        System.out.println("2,id=" + immutableTest2.getId());
        System.out.println("2,name=" + immutableTest2.getName());
        System.out.println("2,hashmap=" + immutableTest2.getTestMap());
        HashMap<String, String> hashMap1 = immutableTest2.getTestMap();
        hashMap1.put("4", "d");
        System.out.println("3,hashmap=" + immutableTest2.getTestMap());
    }
}
//Output:
true
false
1,id=1
1,name=a
1,hashmap={1=a, 2=b}
2,id=1
2,name=a
2,hashmap={1=a, 2=b}
3,hashmap={1=a, 2=b}
//这样构造,hashmap的值不会被更改

4. String的不可变是怎么实现的

这里对于初学者,可能会对于String是不可变对象存在疑惑,比如:

public static void main(String[] args){
    String str = "123";
    System.out.println("str = " + str);
    str = "abc";
    System.out.println("str = " + str);
}
//Output:
str = 123
str = abc

从打印结果来看,这里的s的值的确是变化了啊,怎么说是不可变的呢?
这里就是对象的引用问题,对象是存在于堆区,而str只是一个String对象的引用,存放了指向这个对象的地址,并不是这个对象本身,然后通过这个引用可以访问这个对象。
所以这里当str="123";执行之后,str就指向了"123"这个对象,然后str="abc";执行之后,str又指向了新创建的"abc"对象,原来的"123"对象在堆中还是存在,并且没有改变。

image.png

那么再来看String的不可变是怎么实现的:

String.java:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

JDK1.8下,主要成员变量就2个,在这里hash成员变量是String对象的哈希值的缓存,与此类题关系不大。所以与String是不可变对象有关的是value数组,java里面,数组也是对象,所以这里的value也是一个引用,指向了真正的数组对象。

image.png

在这里value变量是private的,并且没有提供set方法和其他公共方法,所以在String外部不能修改这个值;然后value变量也是final的,所以在String内部,一旦初始化后也就不能修改,所以可以认为String是不可变对象。

在这里可能还有一个疑问,在String类中,存在一些方法,调用它们之后可以得到改变后的值,substring、replace等。

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
               i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}

这里看源码可以知道,最后是在内部新创建了一个String对象,然后把这个新的对象的引用返回出去重新赋值给调用者。

5. String为什么设置成不可变对象

/** Cache the hash code for the string */
private int hash; // Default to 0

字符串的不变性保证了hash码的唯一,因此可以放心的使用缓存,这也是一种性能优化的手段。因为String对象的hash码被频繁的使用,比如在hashmap等容器中;

传递安全:因为java对象参数传的引用,所以可变的StringBuffer参数就被改变了,可以看到变量sb在appendSb之后,就变成了"aaabbb",如果String也是可变对象,就会出现这个问题。

Class Test{
    //不可变的String
    public static String appendStr(String s){
        s+="bbb";
        return s;
    }
    
    //可变的StringBuilder
    public static StringBuilder appendSb(StringBuilder sb){
        return sb.append("bbb");
    }
    
    public static void main(String[] args){
         String s = new String("aaa");
         String ns = Test.appendStr(s);
         System.out.println("String aaa>>>"+s.toString());
         
         //StringBuilder做参数
         StringBuilder sb = new StringBuilder("aaa");
         StringBuilder nsb = Test.appendSb(sb);
         System.out.println("StringBuilder aaa >>>"+sb.toString());
    }
}
//Output:
String aaa>>>aaa
StringBuilder aaa >>>aaabbb

线程安全:在并发场景下,多个线程同时读一个资源,不会引发竞争条件;只有对资源进行写操作时才会竞争,不可变对象不能被写,所以线程安全。
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。

6. String的不可变有什么好处

String不可变的好处,其实上面的String为什么要设计成不可变对象已经用了一些代码进行解答了,这里就整体归纳一下:

  • 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。
  • 如果字符串是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  • 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
  • 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。比如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
  • 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。

目前全部文章列表:
idea整合restful风格的ssm框架(一)
idea整合restful风格的ssm框架(二)
idea整合spring boot+spring mvc+mybatis框架
idea整合springboot+redis
JVM学习之—Java内存区域
JVM学习之—垃圾回收与内存分配策略
专题整理之—不可变对象与String的不可变
专题整理之—String的字符串常量池

上一篇 下一篇

猜你喜欢

热点阅读