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

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, 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 指向的字符数组也被修改了。就是这个样子的:

Java-String:从初始化开始的发散思考_第1张图片
字符串数组、字符串对象、字符串对象的引用

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

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

你可能感兴趣的:(Java-String:从初始化开始的发散思考)