• Java内存管理-探索Java中字符串String(十二)


    做一个积极的人

    编码、改bug、提升自己

    我有一个乐园,面向编程,春暖花开!

    一、初识String类

    首先JDK API的介绍:

    public final class String extends Object 
    implements Serializable, Comparable<String>, CharSequence
    

    String类代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。

    字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。例如:

     String str = "abc";
    

    等效于:

     char data[] = {'a', 'b', 'c'};
     String str = new String(data);
    

    从JDK API中可以看出:

    • String类是final类,那么String类是不能被继承的。
    • 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
    • 实现了Serializable接口,支持序列化,也就意味了String能够通过序列化传输。

    二、字符串的不可变性

    从上面的介绍中发现:字符串是常量,它们的值在创建之后不能更改。为什么会这样呢?要了解其原因,简单看一下String类的源码实现。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
        public String concat(String str) {
            int otherLen = str.length();
            if (otherLen == 0) {
                return this;
            }
            int len = value.length;
            char buf[] = Arrays.copyOf(value, len + otherLen);
            str.getChars(buf, len);
            // 重新创建一个新的字符串
            return new String(buf, true);
        }
    
        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类其实是通过char数组来保存字符串的,注意修饰这个char前面的关键字 final。final修饰的字段创建以后就不可改变。

    注意private final char value[]; 这里虽然value是不可变,也就是说value这个引用地址不可变。但是因为其是数组类型,根据之前学过的内容,value这个引用地址其实是在栈上分配 ,而其对应的数据结构是在堆上分配保存。那也就是说栈里的这个value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子,

    final int[] value={1,2,3}
    int[] another={4,5,6};
    value=another;    //编译器报错,final不可变
    

    value用final修饰,编译器不允许我把value指向栈区另一个地址。但如果直接对数组元素进行修改,分分钟搞定。

    final int[] value={1,2,3};
    value[2]=100;  //这时候数组里已经是{1,2,100}
    

    所以String是不可变的关键都在底层的实现,而不是一个final。

    也可以通过上面的concat(String str) 和replace(char oldChar, char newChar)方法简单进行了解,所有的操作都不是在原有的value[]数组中进行操作的,而是重新生成了一个新数组buf[]。也就是说进行这些操作后,最原始的字符串并没有被改变。

    如果面试有问到的话要修改String中value[] 数组的内容,要怎么做,那么可以通过反射进行修改!实际使用中没有人会去这么做。

    三、字符串常量池和 intern 方法

    Java中有字符串常量池,用来存储字符串字面量! 由于JDK版本的不同,常量池的位置也不同,根据网上的一些资料:

    jdk1.6及以下版本字符串常量池是在永久区中。

    jdk1.7、1.8下字符串常量池已经转移到堆中了。(JDK1.8已经没有去掉永久区)

    因为字符串常量池发生了变化,在String内对intern()进行了一些修改:

    jDK1.6版本中执行intern()方法,首先判断字符串常量池中是否存在该字面量,如果不存在则拷贝一份字面量放入常量池,最后返回字面量的唯一引用。如果发现字符串常量池中已经存在,则直接返回字面量的唯一引用。

    jdk1.7以后执行intern()方法,如果字符串常量池中不存在该字面量,则不会再拷贝一份字面量,而是拷贝字面量对应堆中一个引用,然后返回这个引用。

    String 类型的常量池比较特殊。它的主要使用方法有两种:

    • 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
    • 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。不同版本的intern 表现看上面介绍。

    说明:直接使用new String() 创建出的String对象会直接存储在堆上


    通过一个栗子,看一下上面说的内容:

    String str1 = "aflyun";
    String str2 = new String("aflyun");
    System.out.println(str1 == str2);
    
    String str3 = str2.intern();
    
    System.out.println(str1 ==str3);
    

    使用JDK1.8版本运行输出的结果: false 和 true 。

    先上面示例的示意图:

    str1直接创建在字符串常量池中,str2使用new关键字,对象创建在堆上。所以str1 == str2 为false。

    str3str2.intern(),根据上面的介绍,在jdk1.8首先在常量池中判断字符串aflyun是否存在,如果存在的话,直接返回常量池中字符串的引用,也就是str1的引用。所以str1 ==str3为true。

    如果你理解了上面的内容,可以在看一下下面的栗子,运行结果是在JDK1.8环境:

    栗子1:

    String str1 = "hello";
    String str2 = "world";
    //常量池中的对象
    String str3 = "hello" + "world";
    //在堆上创建的新的对象
    String str4 = str1 + str2; 
    //常量池中的对象
    String str5 = "helloworld";
    System.out.println(str3 == str4);//false
    System.out.println(str3 == str5);//true
    System.out.println(str4 == str5);//false
    

    栗子2:

    //同时会生成堆中的对象以及常量池中hello的对象,此时str1是指向堆中的对象的
    String str1 = new String("hello");
    // 常量池中的已经存在hello
    str1.intern();
    //常量池中的对象,此时str2是指向常量池中的对象的
    String str2 = "hello";
    System.out.println(str1 == str2); // false
    
    // 此时生成了四个对象 常量池中的"world" + 2个堆中的"world" +s3指向的堆中的对象(注此时常量池不会生成"worldworld")
    String str3 = new String("world") + new String("world");
    //常量池没有“worldworld”,会直接将str3的地址存储在常量池内
    str3.intern(); 
    // 创建str4的时候,发现字符串常量池已经存在一个指向堆中该字面量的引用,则返回这个引用,而这个引用就是str3
    String str4 = "worldworld"; 
    System.out.println(str3 == str4); //true
    

    栗子3:涉及到final关键字,可以试着理解一下

    // str1指的是字符串常量池中的 java6
    String str1 = "java6";
    // str2是 final 修饰的,编译时候就已经确定了它的确定值,编译期常量
    final String str2 = "java";
    // str3是指向常量池中 java
    String str3 = "java";
    
    //str2编译的时候已经知道是常量,"6"也是常量,所以计算str4的时候,直接相当于使用 str2 的原始值(java)来进行计算.
    // 则str4 生成的也是一个常量,。str1和str4都对应 常量池中只生成唯一的一个 java6 字符串。
    String str4 = str2 + "6";
    
    // 计算 str5 的时候,str3不是final修饰,不会提前知道 str3的值是什么,只有在运行通过链接来访问,这种计算会在堆上生成 java6
    String str5 = str3 + "6";
    System.out.println((str1 == str4));//true
    System.out.println((str1 == str5));//false
    

    总结

    1. 直接定义字符串变量的时候赋值,如果表达式右边只有字符串常量,那么就是把变量存放在常量池里。

    2. new出来的字符串是存放在堆里面。

    3. 对字符串进行拼接操作,也就是做"+"运算的时候,分2中情况:

    • 表达式右边是纯字符串常量,那么存放在字符串常量池里面。

    • 表达式右边如果存在字符串引用,也就是字符串对象的句柄,那么就存放在堆里面。:

    四、面试题

    1、 String s1 = new String("hello");这句话创建了几个字符串对象?

    情况1:

    String s1 = new String("hello");// 堆内存的地址值
    String s2 = "hello";
    System.out.println(s1 == s2);// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
    System.out.println(s1.equals(s2));// 输出true
    

    如果上面代码的话,这种情况总共创建2个字符串对象。常量池中没有字符串"hello" 的话,一个是new String 创建的一个新的对象,一个是常量“hello”对象的内容创建出的一个新的String对象。

    情况2:

    ```java String s2 = "hello"; String s1 = new String("hello");

    String s1 = new String("hello"); 此时就创建一个对象,而常量“hello”则是从字符串常量池中取出来的。
    
    
    
    ### 2、有时候在面试的时候会遇到这样的问题:**都说String是不可变的,为什么我可以这样做呢,String a = "1";a = "2";**
    

    java public class StringTest {

    public static void main(String[] args) {
        String s = "aflyun";
        System.out.println("s1.hashCode() = " + s.hashCode() + "--" + s);
        s = "hello aflyun";
        System.out.println("s2.hashCode() = " + s.hashCode() + "--" + s);
        //运行后输出的结果不同,两个值的hascode也不一致,
        //说明设置的值在内存中存储在不同的位置,也就是创建了新的对象
    }
    

    }

    s1.hashCode() = -1420403061--aflyun s2.hashCode() = -855605863--hello aflyun ```

    【首先创建一个String对象s,然后让s的值为“aflyun”, 然后又让s的值为“hello aflyun”。 从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?】

    其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

    也就是说,s只是一个引用,它指向了一个具体的对象,当s=“hello aflyun”; 这句代码执行过之后,又创建了一个新的对象““hello aflyun”, 而引用s重新指向了这个新的对象,原来的对象“aflyun”还在内存中存在,并没有改变。内存结构如下图所示:

    类似的一张图:

    总结一下:“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的对象”

    参考资料


    java的线程安全、单例模式、JVM内存结构等知识学习和整理

    Java-String.intern的深入研究

    深入理解Java中的String

    备注: 由于本人能力有限,文中若有错误之处,欢迎指正。


    谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


    不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

    博客首页 : https://aflyun.blog.csdn.net/

    愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人

    © 每天都在变得更好的阿飞云

  • 相关阅读:
    移动端测试作业小集合 (6)
    移动端测试——手机常见操作的API (5)
    移动端测试——APP元素信息、事件操作、模拟手势API(4)
    移动端测试——APP元素定位操作 (3)
    移动端测试——App基础操作(2)
    移动端测试基础 (1)
    Python进阶-一切皆对象及type-object-class间的关系
    故障-解决pip安装mysqlclient、gevent报找不到cc或gcc错误问题
    Linux环境上部署Flask
    解决多版本共存时,python/pip等命令失效
  • 原文地址:https://www.cnblogs.com/aflyun/p/10810396.html
Copyright © 2020-2023  润新知