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

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

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

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

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

2. 不可变对象的优缺点

优点:

  • 构造、测试、使用简单
  • 不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制保证同步问题;因为对象的值无法改变,所以不需要锁机制来保持内存一致性。
  • 不可变对象可以被重复使用,可以将它们缓存起,就像字符串字面量和整型数字一样。可以使用静态工厂方法来提供类似于valueOf()这样的方法,从缓存中返回一个已经存在的Immutable对象。
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为什么设置成不可变对象

  • 字符串常量池的需要,这里应该说字符串常量池的设计是利用了String是不可变对象来进行的一种优化的手段;(这里讲到字符串常量池,可参考我的另一篇总结专题整理之—String的字符串常量池
  • 允许String对象缓存hashCode:在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的字符串常量池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容