美文网首页Java基础知识
Java-String:从初始化开始的发散思考

Java-String:从初始化开始的发散思考

作者: 李眼镜 | 来源:发表于2017-11-23 02:08 被阅读0次

    String 的创建

    一般来说,Java 创建 String 对象有2种方式:

    1. 字面值创建。String s1 = "hello";
    2. new创建。String s2 = new String("hello");

    问题来了:这两种方式创建 String 对象有什么区别吗?
    比较一下好了=>

    比较 String

    比较 String 有两种方法:==equals

    1. == 比较的是两个对象的引用是否指向同一内存地址。
    2. equals 比较的是两个字符串对象的引用指向的内存地址所存储的字面值,不关心是否指向同一内存地址。
    • java Object 对象的equals方法,实际上就是用 == 比较两个对象的引用。
    • 而 String 重写了 Object 的 equals 方法,先用 == 比较对象引用,若引用相同则两个对象字面值一定相同;若引用不同,再比较字面值。
    
        public static void demo1(){
            // 1个字面值创建,1个 new 创建
            System.out.println("demo1-----------------");
            String s1 = "hello";
            String s2 = new String("hello");
            System.out.println(s1 == s2); // 结果:false,指向不同的内存地址
            System.out.println(s1.equals(s2));// 结果:true
        }
    
    
        public static void demo2(){
            // 1个 new 创建,1个字面值创建
            System.out.println("demo2-----------------");
            String s1 = new String("hello");
            String s2 = "hello";
            System.out.println(s1 == s2); // 结果:false,指向不同的内存地址
            System.out.println(s1.equals(s2));// 结果:true
        }
    
        public static void demo3(){
            // 2个都是 new 创建
            System.out.println("demo3-----------------");
            String s1 = new String("hello");
            String s2 = new String("hello");
            System.out.println(s1 == s2); // 结果:false,指向不同的内存地址
            System.out.println(s1.equals(s2));// 结果:true
        }
    
        public static void demo4(){
            // 2个都是字面值创建
            System.out.println("demo4-----------------");
            String s1 = "hello";
            String s2 = "hello";
            System.out.println(s1 == s2); // 结果:true,指向同一内存地址
            System.out.println(s1.equals(s2));// 结果:true
        }
    

    上面的例子中,可以看出,仅当两个字符串都是使用字面值创建时,它们才会指向同一个内存地址,为什么呢?

    这就涉及到了 java String 的内存管理。

    内存模型

    jvm 数据存储主要分布在两大区:

    • stack,存储基本类型、对象的引用,以及线程中的方法调用记录。
    • heap,存放由用户通过 new 操作创建的对象。

    字面值初始化 String 的内存分配

    堆中有一块名叫 “String Constant Pool” 的字符串常量池,专门用于存储字符串常量,一个字符串常量在这个字符串常量池中只存储一份。

    于是,当你使用字面值创建字符串常量“hello”时,JVM 会先去 “String Constant Pool” 查一遍有没有“hello”这个字符串常量,若查到了,会直接把引用指向该内存地址;如果没有查到,就在常量池中申请新的空间,把“hello”放进去。

    也就是说,当使用 String s= “hello”; 定义变量 s 的时候, “hello” 存储在堆区,s 实际上是字符串常量 “hello” 的引用,指向 “String Constant Pool” 中 “hello” 所在内存的地址,s 本身则存储在栈区。

    new String() 的内存分配

    由 new 创建的 String 对象,也会被分配在堆区,但不是 “String Constant Pool” 。

    new 一个新的 String 对象时,JVM 会做一下两件事:

    1. 在堆区创建该 String 对象,并让栈区的对象引用指向它;
    2. 在常量池中查询是否已存在相同的字符串:
      • 如果有,就将堆区的空间和常量池中的空间通过 String.inter() 关联起来;
      • 如果没有,则在常量池中申请空间存放该字符串对象,再做关联。

    String 不可变

    如前所述,在Java中,new 出来的对象是存在堆区的,而对象变量仅仅是一个引用,存在栈区。
    即:对 Object obj = new XXX(); 这行代码,new XXX() 出来的结果是存在堆区的,但 obj 是存在栈区的,它指向堆区中 new XXX() 对象所在的内存地址。

    所以,当你进行 obj = obj1 这样的操作给 obj 赋值时,实际上只是改变了 obj 的引用,使它指向 obj1 所指向的内存地址。

    String 也是 Object,因此同样具有上述特性。

    示例:

        public static void demo5(){
            // 2个都是字面值创建,对其中一个赋新值,再改回来
            System.out.println("demo5-----------------");
    
            String s1="hello";
            String s2="hello";
    
            System.out.println(s1==s2);// 结果:true
    
            s2= "World?";
            System.out.println(s1==s2);// 结果:false,s2指向了一个新的字符常量所在内存地址
    
            s2="hello";
            System.out.println(s1==s2);// 结果:true,又重新指回去了
        }
    
        public static void demo6(){
            // 2个都是 new 创建,将其中一个 赋值给 另一个
            System.out.println("demo6-----------------");
            String s1 = new String("hello");
            String s2 = new String("hello");
            System.out.println(s1 == s2); // 结果:false
            s1 = s2;
            System.out.println(s1 == s2); // 结果:true,s1 = s2使得 s1 指向了 s2 所指的内存地址
            System.out.println(s1.equals(s2));// 结果:true
        }
    

    从上面的例子上看,每次对 String 对象做赋值操作的时候,都仅仅是改变了引用的指向,原字符串本身并没有改变。

    除此之外,String 类定义中没有对外暴露任何改变对象状态的入口,因此在 String 类的外部,也无法通过类似 setXXX() 这样的方法进行对象内容的修改。

    那这些很明显对字符串做了修改的方法,包括substring, replace, replaceAll, toLowerCase等,到底是怎么回事呢?

    replace 为例看一下=>

    java String 替换

        public static void demo7(){
            // replace操作
            System.out.println("demo7-----------------");
            String s1 = "hello";
            System.out.println(s1); //结果:hello
            System.out.println(s1.replace('h','H'));//结果:Hello
            System.out.println(s1);//结果:hello
    
            String s2 = new String("hello");
            System.out.println(s2); //结果:hello
            System.out.println(s2.replace('h','H'));//结果:Hello
            System.out.println(s2);//结果:hello
        }
    

    replace执行结果上看,replace 只是将替换后的结果返了回来,s 及 “hello World” 本身并没有发生改变。

    replace 实现上看,当需要做替换操作的时候,replace 其实是创建并返回了一个新的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 真的是完全不可变的吗?

    String 强行修改

    先看一下 String 类的定义:

    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;
    ......
    

    Java 的 String 实际上是对字符数组的封装,数组也是一个引用。由于value[]被声明为 private final ,所以 String 对象一旦被初始化,它的指向就不能改变,只能指向它最开始指向的数组,而不能指向其他数组;那么问题来了:

    1. 字符数组的引用能不能修改呢?
    2. 如何访问 private 对象?

    用反射:

        private static void demo8() throws Exception{
            // 反射修改 String 中的 value
            System.out.println("demo8-----------------");
    
            String s = "hello";
            System.out.println("s = "+s); //结果:hello
    
            //获取String类中的value字段
            Field valueFieldOfString = String.class.getDeclaredField("value");
    
            //改变value字段的访问权限
            valueFieldOfString.setAccessible(true);
    
            //获取s对象上的value属性的值
            char[] value = (char[]) valueFieldOfString.get(s);
    
            //改变value所引用的数组中的第0个字符
            value[0] = 'H';
    
            System.out.println("s = "+s); //结果:Hello
        }
    

    运行结果:

    s = hello
    s = Hello
    

    通过字面值初始化 s,s指向“hello”,然后再通过反射获得 value的访问权限,对value做修改。
    从结果上看,value指向的值确实被修改了,猜测修改的是“hello”字符串本身,为了印证这个猜测,做一下测试:

        private static void demo9() throws Exception {
            // 2个都是字面值创建,比较 String 引用,和 value 引用;修改其中一个的 value值。
            System.out.println("demo9-----------------");
            String s1 = "hello";
            String s2 = "hello";
    
            System.out.println(s1 == s2);// 结果:true
    
            //获取String类中的value字段
            Field valueFieldOfString = String.class.getDeclaredField("value");
    
            //改变value字段的访问权限
            valueFieldOfString.setAccessible(true);
    
            //获取s对象上的value属性的值
            char[] value_s1 = (char[]) valueFieldOfString.get(s1);
            char[] value_s2 = (char[]) valueFieldOfString.get(s2);
    
            System.out.println(value_s1 == value_s2);//结果:true,s1 和 s2 的引用也是相同的
    
            value_s1[0]='H';
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);// 结果:true
        }
    

    可以看到,s1 和 s2 指向同一个字符串数组,当 s1 通过value 引用修改字符数组时,s2 指向的字符数组也被修改了。就是这个样子的:

    字符串数组、字符串对象、字符串对象的引用

    上面的例子说明了一个问题:如果一个对象,它组合/包含的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。

    例如:尽管 String 对外隐藏了字符串对象,并将其设为 final private,但我们仍然有办法对它进行访问和修改。

    相关文章

      网友评论

        本文标题:Java-String:从初始化开始的发散思考

        本文链接:https://www.haomeiwen.com/subject/yzulvxtx.html