前言
基于字符串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:本文中所有的图示均是为了方便理解,画出来的大致样子,如果想要了解的更加清楚,请自行研究虚拟机原理。
java语法设计的时候针对String,提供了两种创建方式和一种特殊的存储机制(String intern pool )。
两种创建字符串对象的方式:
1.字面值的方式赋值
2.new关键字新建一个字符串对象
这两种方法在性能和内存占用方面存在这差异
String Pool串池:是在内存堆中专门划分一块空间,用来保存所有String对象数据,当构造一个新字符串String对象时(通过字面量赋值的方法),Java编译机制会优先在这个池子里查找是否已经存在能满足需要的String对象,如果有的话就直接返回该对象的地址引用(没有的话就正常的构造一个新对象,丢进去存起来),这样下次再使用同一个String的时候,就可以直接从串池中取,不需要再次创建对象,也就避免了很多不必要的空间开销。
根据以上的概念,我们再来看前言中的代码,当JVM执行到String a = "abc";的时候,会先看常量池里有没有字符串刚好是“abc”这个对象,如果没有,在常量池里创建初始化该对象,并把引用指向它,如下图。
当执行到String b = "abc";
时,发现常量池已经有了abc这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:
继续执行到 String c = new String("abc");
这时候我们加了一个new关键字,这个关键字呢就是告诉JVM,你直接在堆内存里给我开辟一块新的内存,如下图所示:
这时候我们执行四个打印语句,我们需要知道==比较的是地址,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出来一个。最终的结果如下图:
那么这时候如果再有一个String c2 = new String("abc");
呢?如图
关于这一点我们通过IDEA的debug功能也能够看到,你会发现,c和c2其中的char数组的地址是相同的。足以说明在创建c和c2的时候使用的是同一个数组。
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。