zoukankan      html  css  js  c++  java
  • (转)Java中的String为什么是不可变的? -- String源码分析

    背景:被问到很基础的知识点  string  自己答的很模糊

    Java中的String为什么是不可变的? -- String源码分析

    ps:最好去阅读原文

    Java中的String为什么是不可变的

    什么是不可变对象?

    众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变

    jdk1.6

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence
    {
        /** The value is used for character storage. */
        private final char value[];
     
        /** The offset is the first index of the storage that is used. */
        private final int offset;
     
        /** The count is the number of characters in the String. */
        private final int count;
     
        /** Cache the hash code for the string */
        private int hash; // Default to 0

    jdk1.7

    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

    由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。JDK6中, value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:

    value,offset和count这三个变量都是private的,并且没有提供setValue, setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了, 也不能被改变。所以可以认为String对象是不可变的了。

        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对象 的值发生改变,是因为最终都返回一个new的对象。

    String对象真的不可变吗?

    从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
    那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:

        public static void testReflection() throws Exception {
            
            //创建字符串"Hello World", 并赋给引用s
            String s = "Hello World"; 
            
            System.out.println("s = " + s);    //Hello World
            
            //获取String类中的value字段
            Field valueFieldOfString = String.class.getDeclaredField("value");
            
            //改变value属性的访问权限
            valueFieldOfString.setAccessible(true);
            
            //获取s对象上的value属性的值
            char[] value = (char[]) valueFieldOfString.get(s);
            
            //改变value所引用的数组中的第5个字符
            value[5] = '_';
            
            System.out.println("s = " + s);  //Hello_World
        }

    打印结果为:
    s = Hello World
    s = Hello_World

    在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变

    为什么String要设计成不可变的?

    为什么String要设计成不可变的?

    这是一个老生常谈的话题(This is an old yet still popular question). 在Java中将String设计成不可变的是综合考虑到各种因素的结果,想要理解这个问题,需要综合内存,同步,数据结构以及安全等方面的考虑. 在下文中,我将为各种原因做一个小结。

    1. 字符串常量池的需要

    字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

    如下面的代码所示,将会在堆内存中只创建一个实际String对象.

    String s1 = "abcd";
    String s2 = "abcd";

    示意图如下所示:

    图1

    假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段.

    请思考: 假若代码如下所示,s1和s2还会指向同一个实际的String对象吗?

    String s1= "ab" + "cd";
    String s2= "abc" + "d";

    也许这个问题违反新手的直觉, 但是考虑到现代编译器会进行常规的优化, 所以他们都会指向常量池中的同一个对象. 或者,你可以用 jd-gui 之类的工具查看一下编译后的class文件.

    2. 允许String对象缓存HashCode
    Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。

    符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 在String类的定义中有如下代码:

    private int hash;//用来缓存HashCode

    3. 安全性

    String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。

    假如有如下的代码:

    boolean connect(string s){
        if (!isSecure(s)) { 
    throw new SecurityException(); 
    }
        // 如果在其他地方可以修改String,那么此处就会引起各种预料不到的问题/错误 
        causeProblem(s);
    }

    不可变对象天生就是线程安全的

    因为不可变对象不能被改变,所以他们可以自由地在多个线程之间共享。不需要任何同步处理。

    总之,String被设计成不可变的主要目的是为了安全和高效。所以,使String是一个不可变类是一个很好的设计。

    总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面. 事实上,这也是Java面试中的许多 "为什么" 的答案。
    相关文章 :

      1. String对象不可改变的特性
      2. String is passed by “reference” in Java
      3. 十大常见Java String问题
      4. Java中Set的contains()方法



  • 相关阅读:
    React生命周期及事件详解
    系统重装后常见的环境变量配置
    Java 字符串格式化
    React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发
    java-json与js-json转化
    RN项目中缩进处理
    React Native 常用学习链接地址
    React-Native 常用组件学习资料链接
    Swift-继承、构造器、类型转换(嵌套)、类扩展、泛型、协议
    CallKit详解(来电提醒+骚扰拦截)
  • 原文地址:https://www.cnblogs.com/lixuwu/p/10817588.html
Copyright © 2011-2022 走看看