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

·  阅读 1075

做一个积极的人

编码、改bug、提升自己

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

推荐阅读

第一季

0、Java的线程安全、单例模式、JVM内存结构等知识梳理

1、Java内存管理-程序运行过程(一)

2、Java内存管理-初始JVM和JVM启动流程(二)

3、Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)

4、Java内存管理-掌握虚拟机类加载机制(四)

5、Java内存管理-掌握虚拟机类加载器(五)

6、Java内存管理-类加载器的核心源码和设计模式(六)

7、Java内存管理-掌握自定义类加载器的实现(七)
第一季总结:由浅入深JAVA内存管理 Core Story

第二季

8、Java内存管理-愚人节new一个对象送给你(八)

【福利】JVM系列学习资源无套路赠送

9、Java内存管理-”一文掌握虚拟机创建对象的秘密”(九)

10、Java内存管理-你真的理解Java中的数据类型吗(十)

11、Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)

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

实战

一文学会Java死锁和CPU 100% 问题的排查技巧

分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!大家可以看看是否对自己有帮助,点击这里查看【人工智能教程】。接下来进入正文。

一、初识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版本运行输出的结果: falsetrue

先上面示例的示意图:

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:

String s2 = "hello";
String s1 = new String("hello");复制代码

String s1 = new String("hello"); 此时就创建一个对象,而常量“hello”则是从字符串常量池中取出来的。

2、有时候在面试的时候会遇到这样的问题:都说String是不可变的,为什么我可以这样做呢,String a = "1";a = "2";

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

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

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

Java编程技术乐园:一个分享编程知识的公众号。跟着老司机一起学习干货技术知识,每天进步一点点,让小的积累,带来大的改变!

扫描关注,后台回复【资源】,获取珍藏干货! 99.9%的伙伴都很喜欢

image.png | center| 747x519

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