2

摘要: String基本特性,String源码,为什么String不可变?

前言

基于字符串String在java中的地位,关于String的常识性知识就不多做介绍了,我们先来看一段代码

public class Test {
    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,有初学java的朋友肯定会纳闷,a==c为什么会是false呢?equals判断的为什么都是true呢?

根据这些问题,我们就通过对String的解读来一步一步的了解。

为什么a==c的结果是false

明白这个问题需要对JVM的内存结构有一定的了解,说是了解也不需要太多,能够get到下图的知识点就行了。

ps:本文中所有的图示均是为了方便理解,画出来的大致样子,如果想要了解的更加清楚,请自行研究虚拟机原理。

clipboard.png

java语法设计的时候针对String,提供了两种创建方式和一种特殊的存储机制(String intern pool )。

两种创建字符串对象的方式:

  1. 字面值的方式赋值
  2. new关键字新建一个字符串对象

这两种方法在性能和内存占用方面存在这差异

String Pool串池:是在内存堆中专门划分一块空间,用来保存所有String对象数据,当构造一个新字符串String对象时(通过字面量赋值的方法),Java编译机制会优先在这个池子里查找是否已经存在能满足需要的String对象,如果有的话就直接返回该对象的地址引用(没有的话就正常的构造一个新对象,丢进去存起来),这样下次再使用同一个String的时候,就可以直接从串池中取,不需要再次创建对象,也就避免了很多不必要的空间开销。

根据以上的概念,我们再来看前言中的代码,当JVM执行到String a = "abc";的时候,会先看常量池里有没有字符串刚好是“abc”这个对象,如果没有,在常量池里创建初始化该对象,并把引用指向它,如下图。

String1

当执行到String b = "abc";时,发现常量池已经有了abc这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:

String2

继续执行到 String c = new String("abc");这时候我们加了一个new关键字,这个关键字呢就是告诉JVM,你直接在堆内存里给我开辟一块新的内存,如下图所示:

String3

这时候我们执行四个打印语句,我们需要知道==比较的是地址,equals比较的是内容(String中的重写过了),abc三个变量的内容完全一样,因此equals的结果都是true,ab是一个同一个对象,因此地址一样,a和c很显然不是同一个对象,那么此时为false也是很好理解的。

String相关源码

在本文中只有String的部分源码,毕竟String的源码有3000多行,全部来写进来不那么现实,我们挑一些比较有意思的代码来做一定的分析说明。

属性

我们先来看一下String都有哪些成员变量,比较关键的属性有两个,如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    char数组
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0

从源码中我们能够看到,在String类中声明了一个char[]数组,变量名value,声明了一个int类型的变量hash(该String对象的哈希值的缓存)。也就是说java中的String类其实就是对char数组的封装。

构造方法

接下来我们通过一句代码来了解一下字符串创建的过程,String c = new String("abc");我们知道使用new关键字就会使用到构造方法,所以如下。

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

构造方法中的代码非常简单,把传进来的字符串的value值,也就是char数组赋值给当前对象,hash同样处理,那么问题来了WTF original?

在这里需要注意的是java中的一个机制,在Java中,当值被双引号引起来(如本示例中的"abc"),JVM会去先检查看一看常量池里有没有abc这个对象,如果没有,把abc初始化为对象放入常量池,如果有,直接返回常量池内容。所以也就是说在没有“abc”的基础上,执行代码会在串池中创建一个abc,也会在堆内存中再new出来一个。最终的结果如下图:

String4

那么这时候如果再有一个String c2 = new String("abc");呢?如图

String5

关于这一点我们通过IDEA的debug功能也能够看到,你会发现,c和c2其中的char数组的地址是相同的。足以说明在创建c和c2的时候使用的是同一个数组。

String6

equals方法

 public boolean equals(Object anObject) {
     //如果两个对象是同一个引用,那么直接返回true
        if (this == anObject) {
            return true;
        }
     /*
     1.判断传入的对象是不是String类型
     2.判断两个对象的char数组长度是否一致
     3.循环判断char数组中的每一个值是否相等
     以上条件均满足才会返回true
     */
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

为什么String不可变?

串池需要

为什么说是串池需要呢?在开篇的时候我们提到过,串池中的字符串会被多个变量引用,这样的机制让字符串对象得到了复用,避免了很多不必要的内存消耗。

那么大家试想一下,如果String对象本身允许二次修改的话,我有一个字符串“abc”同时被100个变量引用,其中一个引用修改了String对象,那么将会影响到其他99个引用该对象的变量,这样会对其他变量造成不可控的影响。

不可变性的优点

安全性

字符串不可变安全性的考虑处于两个方面,数据安全和线程安全。

数据安全,大家可以回忆一下,我们都在哪些地方大量的使用了字符串?网络数据传输,文件IO等,也就是说当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

线程安全,因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步。

性能效率

关于性能效率一方面是复用,另一方面呢需要从hash值的缓存方向来说起了。

String的Hash值在很多的地方都会被使用到,如果保证了String的不可变性,也就能够保证Hash值始终也是不可变的,这样就不需要在每次使用的时候重新计算hash值了。

String不可变性是如何实现的?

通过对属性私有化,final修饰,同时没有提供公开的get set方法以及其他的能够修改属性的方法,保证了在创建之后不会被从外部修改。

同时不能忘了,String也是被final修饰的,在之前的文章中我们提到过,final修饰类的结果是String类没有子类。

那么String真的不能改变吗?不是,通过反射我们可以,代码如下:

String c = new String("abc");
System.out.println(c);
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");

//改变value属性的访问权限
valueFieldOfString.setAccessible(true);

//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(c);

//改变value所引用的数组中的第5个字符
value[1] = '_';
System.out.println(c);

执行的结果是

abc
a_c

也就是说我们改变了字符串对象的值,有什么意义呢?没什么意义,我们从来不会这么做。

其他问题

不是特别需要请不要使用new关键字创建字符串

从前文我们知道使用new关键字创建String的时候,即便串池中存在相同String,仍然会再次在堆内存中创建对象,会浪费内存,另一方面对象的创建相较于从串池中取效率也更低下。

String StringBuffer StringBuilder的区别

关于三者的区别,在面试题中经常的出现,String对象不可变,因此在进行任何内容上的修改时都会创建新的字符串对象,一旦修改操作太多就会造成大量的资源浪费。

StringBuffer和StringBuilder在进行字符串拼接的时候不会创建新的对象,而是在原对象上修改,不同之处在于StringBuffer线程安全,StringBuilder线程不安全。所以在进行字符串拼接的时候推荐使用StringBuffer或者StringBuilder。


我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。

永远相信美好的事情即将发生。


鹿老师的Java笔记
111 声望34 粉丝