由“String不可变”引发的一些思考

字符串常量池

​ JVM为了字符串的服用,减少字符串对象的重复创建,特别维护了一个常量池。

​ jdk1.7之前的版本,常量池存放在方法区,方法区和JAVA堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态常量、JIT编译后的代码等。在java虚拟机规范中将方法区描述为堆的一个逻辑部分,也被叫做Non-Heap(非堆)。经常遇到的一个错误:java.lang.OutOfMemoryError: PermGen space,这里的PermGenspace也就是我们经常提到的永久代,方法区可以理解为java虚拟机的规范,而永久代则看作是规范的一种实现。而在jdk1.7中,常量池从永久代移到到了堆区,为移除永久代工作做准备。于是我们看到的常量池内存溢出变成了java.lang.OutOfMemoryError: Java heap space。

​ 到了jdk1.8,永久代彻底移除,元空间(Meta space)出现,可以看作是对JVM规范中方法区的另一种实现。不过元空间与永久代之间最大的区别在于:元空间并不在JVM中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,把类的元数据扔到了元空间,而常量池仍然放在java heap中。

​ 元空间取代永久代可能是因为,字符串存于永久代,占用堆区内存,太小可能导致永久代溢出,太大导致老年代溢出,为GC也带来不必要的复杂度,并且回收效率也偏低。当然以上内容仅针对HotSpot 。

String

        String str1 = "123";
        String str2 = new String("123");
        System.out.println(str1 == str2);
        System.out.println(str1.equals(str2));

​ 首先是String的两种不同的初始化写法。第一种字面量形式的写法,会直接在字符串常量池中查找是否存在值 123,若存在直接返回这个值的引用,若不存在创建一个字符串123存入字符串常量池中。而使用 new 关键字,则会直接在堆上生成一个新的 String 对象,并不会理会常量池中是否有这个值。所以本质上 str1 和 str2 指向的内存地址是不一样的,str1 指向常量池,str2指向堆,而对于“==”比较的是地址,所以第一句是返回false,而第二句equals比较值(对于没有复写equals的对象,equals仍然比较的是地址),当然是返回true。

​ String对象不可变是因为它用了final修饰。在同一个方法内,当final修饰一个基本数据类型时,表示该基本数据类型的值在初始化后便不能发生变化;如果final修饰一个引用类型,则对其初始化之后便不能再改变值,但栈上的引用可以改变。

​ 上栗子1:

        String s = "123";
        System.out.println("s = " + s);
        s = "456";
        System.out.println("s = " + s);

​ 打印结果为

​ s = 123
​ s = 456

​ s的值变了,那123这个对象改变了吗,其实“123”这个对象还是存在,只不过生成了一个新的”456“对象,并把这个s指向了“456”,如下图所示。

1

​ 在进一步讨论讨论String的不可变性之前,看下面的一个栗子:

    public static void main(String args[]) {
        String str = "hello";
        change(str);
        System.out.println(str);      
    }
    private static void change(String str) {
        str = "word";
    }

    输出结果:
        hello

​ 为什么这个执行结果是hello,而不像上一个栗子,str指向了新的对象“word”呢?这个涉及到java函数调用时的参数传递策略,java到底是值传递还是引用传递。

​ 先理解几个基础概念:

形参:用来接收调用该方法时传递的参数。只有在被调用的时候才分配内存空间,一旦调用结束,就释放内存空间。因此仅仅在方法内有效,也就是change函数里面的这个str。

实参:传递给被调用方法的值,预先创建并赋予确定值,对应main函数里面的str。

值传递:方法调用时,实参把它的副本传递给对应的形参。

引用传递:方法调用时,将实参的引用地址直接传给形参,函数接收到原参数的内存地址。

值传递和引用传递都是属于函数调用时参数的求值策略(Evaluation Strategy),这是对调用函数时,求值和传值的方式的描述,而非传递的内容的类型,引用类型和引用传递是不同方法的概念,一个描述内存分配方式,一个描述参数求值策略。

对于引用传递,在函数中可以改变原始对象,这里的改变是指将一个变量指向另一个对象,就像第一个栗子中呈现的那样,而不是仅仅是改变属性或者成员变量之类的,所以Java是值传递。

​ 而这些行为与参数类型是基本类型还是引用类型无关。对于值传递来说,无论参数是基本类型还是引用类型,都会在栈上创建一个副本,但有一点不同,对于值类型,这个副本是整个原始值的复制,而对于引用类型来说,由于对象实例在堆上,复制的只是在栈上的它的一个引用值。

​ 上例代码中,str传入change()函数时,形参str只是原引用str的一个引用副本,它们同时指向了堆中的“hello”对象,也就是同时存在两个不同的str。当执行形参str = "word";时,在内存再创建了一个“word”对象,并把形参str引用指向了这个新创建的对象,但是main函数里面的str的地址并没有改变。

2

​ 那么再来看这段代码:

    public static void main(String args[]) {
        final char[] ch = {'h', 'e', 'l', 'l', 'o'};
        change(ch);
        System.out.println(ch);
    }
    private static void change(char[] ch) {
        ch[1] = 'a';
    }

    输出结果:
        hallo

​ 因为是值传递,所以引用不可变,但是可以改变对象的属性或者成员变量。那怎么改变对象的属性或者成员变量呢?比如上述的数组,直接改变ch[i]的值,要么对象自身提供改变的方法,比如StringBuilder的append方法:

    public static void main(String args[]) {
        StringBuilder sb = new StringBuilder("hello");
        change(sb);
        System.out.println(sb.toString());
    }
    private static void change(StringBuilder sb) {
        sb.append(" world");
    }

    输出结果:
        hello world

String对象不可变

​ 在Effective Java中对不可变类的设计提出了以下五点建议:

  1. 不要提供任何会修改对象状态的方法。
  2. 保证类不会被扩展。
  3. 使所有的域都是 final 的。
  4. 使所有的域都成为私有的。
  5. 确保对于任何可变性组件的互斥访问。

​ 我们再结合jdk源码(1.8)就可以知道为什么String被称为一个不可变类了,除了在类上标识final保证类不会被扩展,其所有的域都是final且私有的,也不提供改变对象状态的方法(比如setter等)。

    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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

​ 那么String真的一定不可改变,发现String的值是用一个char[]数组存储的,结合我们上面提到的栗子是不是可以做到改变呢?

改变String

​ 计算机科学领域的任何问题都可以通过增加一个中间层来解决,那么对于java,通过反射往往可以做到一些普通方法做不到的事。通过反射获取这个value[]数组,然后改变它不就可以了吗。

    public static void main(String args[]) throws Exception {
        String str = "hello";
        change(str);
        System.out.println(str);
    }
    private static void change(String str) throws Exception {
        // 1.获取域
        Field field = String.class.getDeclaredField("value");
        // 2.value是私有域,设置为可访问
        field.setAccessible(true);
        // 3.找到对应值
        char[] value = (char[]) field.get(str);
        value[1] = 'a';
    }

    输出结果:
        hallo

​ 很显然,通过反射可以破坏 String 的不可变性。

总结

​ Java的传参方式是值传递,这个是函数调用时参数的求值策略,与参数是基本类型还是引用类型无关。另外,不可变类的设计在使用过程中不容易出错,天生的线程安全,当然也应该避免不可变设计的滥用。

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

推荐阅读更多精彩内容