Loading...
墨滴

代码界的小白

2021/11/03  阅读:38  主题:全栈蓝

【Java基础】String字符串你了解多少?

引入

依稀记得小米一面的时候,面试官问了我一个这样的问题。

面试官:String类型的字符串存在哪?

本人: ... 应该是存在常连池吧

面试官:这个要分情况

本人:当时听到面试官这么跟我说的时候,说实话我也是十分迷惑了

好了,下面进入干货部分。

先来看一段代码吧。

import org.junit.Test;
public class StringStudy {
    @Test
    public void demo1(){
        String str1 = "abc";  #方式1
        String str2 = new String("abc"); #方式2
        String str3 = str2.intern(); #方式3
        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str3);
    }
}

为什么面试官会说要分情况呢,看完上面的代码,你觉得输出应该是什么呢?

正确答案请往后看,结尾有彩蛋哦!

下面进入正题,我们可以先看看String类型的源码吧。

我是用的是jdk8,在jdk8的时候是由char数组和hash构成的

上图是jdk15的,可以发现String类型的源码是发生变化了的。

String对象是如何实现的?

在 Java 语言中,Sun 公司的工程师们对 String 对象做了大量的优化,来节约内存空间,提升 String 对象在系统中的性能。一起来看看优化过程,如下图所示:

  • 在 Java6 以及之前的版本中,String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量:char 数组、偏移量 offset、字符数量 count、哈希值 hash。String 对象是通过 offset 和 count 两个属性来定位 char[] 数组,获取字符串。这么做可以高效、快速地共享数组对象,同时节省内存空间,但这种方式很有可能会导致内存泄漏。

  • 从 Java7 版本开始到 Java8 版本,Java 对 String 类做了一些改变。String 类中不再有offset 和 count 两个变量了。这样的好处是 String 对象占用的内存稍微少了些,同时,String.substring 方法也不再共享 char[],从而解决了使用该方法可能导致的内存泄漏问题。

  • 从 Java9 版本开始,工程师将 char[] 字段改为了 byte[] 字段,又维护了一个新的属性coder,它是一个编码格式的标识。

为什么这样修改呢?

我们知道一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的 String 类为了节约内存空间,于是使用了占8 位,1 个字节的 byte 数组来存放字符串。

而新属性 coder 的作用是,在计算字符串长度或者使用 indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值,0 代表 Latin-1(单字节编码),1 代表 UTF-16。如果 String 判断字符串只包含了 Latin-1,则 coder属性值为 0,反之则为 1。

String对象的不可变性

看了上面jdk8和jdk15的String源码后,我们可以发现,不管是char数组还是byte数组,前面都被final关键字修饰。

我们知道类被 final 修饰代表该类不可继承,而 char[] 被 final+private 修饰,代表了String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。

    @Test
    public void demo2(){
        String str1 = "hello";
        System.out.println(str1);
        str1 = "world";
        System.out.println(str1);
    }

首先,解释下什么是对象和对象引用。在 Java 中要比较两个对象是否相等,往往是用 ==,而要判断两个对象的值是否相等,则需要用 equals 方法来判断。

这是因为 str 只是 String 对象的引用,并不是对象本身。对象在内存中是一块内存地址,str 则是一个指向该内存地址的引用。上面的这个例子中,第一次赋值的时候,创建了一个“hello”对象,str 引用指向“hello”地址;第二次赋值的时候,又重新创建了一个对象“world”,str 引用指向了“world”,但“hello”对象依然存在于内存中。

也就是说 str 并不是对象,而只是一个对象引用。真正的对象依然还在内存中,没有被改变。

Java 这样做的好处在哪里呢?

第一,保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。

第二,保证 hash 属性值不会频繁变更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。

第三,可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。

为什么面试官说String类型存在哪要分情况呢?

还记得最一开始的代码吗,不记得的请看下面

String str1 = "abc"; #方法一
String str2 = new String("abc");#方法二

方法一:JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。

方法二:String str = new String(“abc”) 这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。

String.intern能节省内存?

还记得最开头的例子吗

    @Test
    public void demo1(){
        String str1 = "abc";
        String str2 = new String("abc");
        String str3 = str2.intern();

        System.out.println(str1==str2);
        System.out.println(str2==str3);
        System.out.println(str1==str3);
    }

你觉得str1==str3是输出false还是true呢?(留一个悬念,看结尾!)

String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从 20G 降到几百兆。

    @Test
    public void demo3(){
        String a =new String("abc").intern();
        String b = new String("abc").intern();
        if(a==b) {
            System.out.print("a==b");
        }else{
            System.out.println("a!=b");
        }
    }

输出:a==b

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。 如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

了解了原理,我们再一起看看上边的例子。

在一开始创建 a 变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创建一个字符串对象,在调用 intern 方法之后,会去常量池中查找是否有等于该字符串的对象,有就返回引用。

在创建 b 字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不再创建。调用 intern 方法则会去常量池中判断是否有等于该字符串的对象,发现有等于"abc"字符串的对象,就直接返回引用。而在堆内存中的对象,由于没有引用指向它,将会被垃圾回收。所以 a 和 b 引用的是同一个对象。

看到这里你应该知道最开始的实例的输出是什么了吧!

@Test
public void demo1(){
    String str1 = "abc";
    String str2 = new String("abc");
    String str3 = str2.intern();
    System.out.println(str1==str2);
    System.out.println(str2==str3);
    System.out.println(str1==str3);
}

输出:

false
false
true

推荐阅读

Java学习资料-电子版

Java面试宝典《阿里调优手册》,你值得拥有!

面试高频问题:Java反射你了解多少?

面试高频手撕代码:写个快排的代码吧。

Java中的基本数据类型有哪些?

代码界的小白

2021/11/03  阅读:38  主题:全栈蓝

作者介绍

代码界的小白

公众号:代码界的小白