干货,聊聊Java中String类!!!

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」。

java.lang.String 类可能是大家日常用的最多的类,但是对于它是怎么实现的,你真的明白吗? 认真阅读这篇文章,包你一看就明白了。

String 类定义

public final class String implements 
java.io.Serializable, Comparable<String>, CharSequence {}
复制代码

从源码可以看出,String 是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建,包含在这个对象中的字符序列是不可改变的,包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)。接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),后面会有具体方法实现;最后实现了 CharSequence 接口,表示是一个有序字符的集合,相应的方法后面也会介绍。

字段属性

/**用来存储字符串  */
private final char value[];

/** 缓存字符串的哈希码 */
private int hash; // Default to 0

/** 实现序列化的标识 */
private static final long serialVersionUID = -6849794470754667710L;
复制代码

一个 String 字符串实际上是一个 char 数组。

构造方法

String 类的构造方法很多。可以通过初始化一个字符串,或者字符数组,或者字节数组等等来创建一个 String 对象。   

image-20211119100141712

String str1 = "abc";//注意这种字面量声明的区别,文末会详细介绍
String str2 = new String("abc");
String str3 = new String(new char[]{'a','b','c'});
复制代码

equals(Object anObject) 方法

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}
复制代码

String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。

hashCode() 方法

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
            : StringUTF16.hashCode(value);
    }
    return h;
}
复制代码

String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:

1、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。

2、31可以被 JVM 优化,31 * i = (i « 5) - i。因为移位运算比乘法运行更快更省性能。

charAt(int index) 方法

public char charAt(int index) {
    if (isLatin1()) {
        return StringLatin1.charAt(value, index);
    } else {
        return StringUTF16.charAt(value, index);
    }
}
复制代码

我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。

intern() 方法

这是一个本地方法:

public native String intern();

当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。

这句话什么意思呢?就是说调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中);如果没有,则将该对象添加到池中,并返回池中的引用。

String str1 = "hello";//字面量 只会在常量池中创建对象
String str2 = str1.intern();
System.out.println(str1==str2);//true

String str3 = new String("world");//new 关键字只会在堆中创建对象
String str4 = str3.intern();
System.out.println(str3 == str4);//false

String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象
String str6 = str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象
System.out.println(str5 == str6);//true

String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
String str8 = str7.intern();
System.out.println(str7 == str8);//true
复制代码

关于String类里面的众多方法,这里不一一介绍了,下面我们来深入了解一下,String 类不可变型。

面试精选

分析一道经典的面试题:

public static void main(String[] args) {
    String A = "abc";
    String B = "abc";
    String C = new String("abc");
    System.out.println(A==B);
    System.out.println(A.equals(B));
    System.out.println(A==C);
    System.out.println(A.equals(C));
}
复制代码

答案是:true、true、false、true

对于上面的题目,我们可以先来看一张图,如下:

image-20211119101936381

首先 String A= “abc”,会先到常量池中检查是否有“abc”的存在,发现是没有的,于是在常量池中创建“abc”对象,并将常量池中的引用赋值给A;第二个字面量 String B= “abc”,在常量池中检测到该对象了,直接将引用赋值给B;第三个是通过new关键字创建的对象,常量池中有了该对象了,不用在常量池中创建,然后在堆中创建该对象后,将堆中对象的引用赋值给C,再将该对象指向常量池。

需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么A.equals(C)返回true的原因了。

注意:看上图红色的箭头,通过 new 关键字创建的字符串对象,如果常量池中存在了,会将堆中创建的对象指向常量池的引用。

再来看一道题目,使用包含变量表达式创建对象:

String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)
String str4 = "hello"+"world";//编译器确定为常量,直接到常量池中引用

System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
System.out.println(str3==str4);//fasle
复制代码

str3 由于含有变量str1,编译器不能确定是常量,会在堆区中创建一个String对象。而str4是两个常量相加,直接引用常量池中的对象即可。

image-20211120095858404

String 不可变性

String类是Java中的一个不可变类(immutable class)。

简单来说,不可变类就是实例在被创建之后不可修改。

String不可变这个话题应该是老生长谈了,String自打娘胎一出生就跟他们的兄弟姐妹不一样,好好的娃被戴了一个final的帽子,

以至于byte,int,short,long等基本类型的小伙们都不带它玩。

如果你仔细阅读源码注释,你会发现这样一句话:

image-20211118194406592

大致意思就是String是个常量,从一出生就注定不可变。

首先需要补充一个容易混淆的知识点:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。

String 类是用 final 关键字修饰的,所以我们认为其是不可变对象。但是真的不可变吗?

每个字符串都是由许多单个字符组成的,我们知道其源码是由 char[] value 字符数组构成。

/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0
复制代码

value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。

public static void main(String[] args) throws Exception {
        String str = "Hello World";
        System.out.println("修改前的str:" + str);
        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
        // 获取String类中的value字段
        Field valueField = String.class.getDeclaredField("value");
        // 改变value属性的访问权限
        valueField.setAccessible(true);
        // 获取str对象上value属性的值
        char[] value = (char[]) valueField.get(str);
        // 改变value所引用的数组中的字符
        value[3] = '?';
        System.out.println("修改后的str:" + str);
        System.out.println("修改前的str的内存地址" + System.identityHashCode(str));
    }
复制代码
修改前的str:Hello World
修改前的str的内存地址1746572565
修改后的str:Hel?o World
修改前的str的内存地址1746572565
复制代码

通过前后两次打印的结果,我们可以看到 str 值被改变了,但是str的内存地址还是没有改变。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。

不可变的好处

首先,我们应该站在设计者的角度思考问题,而不是觉得这不好,那不合理:

  • 可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。
  • 我们的程序中大量使用了String字符串,有可能是出于安全性考虑。
  • 当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

小结

有兴趣的小伙伴也可以去阅读下String的源码,浩浩荡荡的3000+。

String 被new时是要创建对象的,+ 号拼接同理,程序中尽量不要使用 + 拼接,推荐使用StringBuffer或者StringBuilder。

感谢的阅读,希望看完三连一波呀,谢谢啦~~~

Guess you like

Origin juejin.im/post/7032474757874483207