基于字符串String在java中的地位,关于String的常识性知识就很少作介绍了,咱们先来看一段代码java
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呢?git
根据这些问题,咱们就经过对String的解读来一步一步的了解。github
明白这个问题须要对JVM的内存结构有必定的了解,说是了解也不须要太多,可以get到下图的知识点就好了。面试
ps:本文中全部的图示均是为了方便理解,画出来的大体样子,若是想要了解的更加清楚,请自行研究虚拟机原理。数组
java语法设计的时候针对String,提供了两种建立方式和一种特殊的存储机制(String intern pool )。缓存
两种建立字符串对象的方式:安全
- 字面值的方式赋值
- 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的源码有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对象自己容许二次修改的话,我有一个字符串“abc”同时被100个变量引用,其中一个引用修改了String对象,那么将会影响到其余99个引用该对象的变量,这样会对其余变量形成不可控的影响。
字符串不可变安全性的考虑处于两个方面,数据安全和线程安全。
数据安全,你们能够回忆一下,咱们都在哪些地方大量的使用了字符串?网络数据传输,文件IO等,也就是说当咱们在传参的时候,使用不可变类不须要去考虑谁可能会修改其内部的值,若是使用可变类的话,可能须要每次记得从新拷贝出里面的值,性能会有必定的损失。
线程安全,由于字符串是不可变的,因此是多线程安全的,同一个字符串实例能够被多个线程共享,这样便不用由于线程安全问题而使用同步。
关于性能效率一方面是复用,另外一方面呢须要从hash值的缓存方向来讲起了。
String的Hash值在不少的地方都会被使用到,若是保证了String的不可变性,也就可以保证Hash值始终也是不可变的,这样就不须要在每次使用的时候从新计算hash值了。
经过对属性私有化,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关键字建立String的时候,即使串池中存在相同String,仍然会再次在堆内存中建立对象,会浪费内存,另外一方面对象的建立相较于从串池中取效率也更低下。
关于三者的区别,在面试题中常常的出现,String对象不可变,所以在进行任何内容上的修改时都会建立新的字符串对象,一旦修改操做太多就会形成大量的资源浪费。
StringBuffer和StringBuilder在进行字符串拼接的时候不会建立新的对象,而是在原对象上修改,不一样之处在于StringBuffer线程安全,StringBuilder线程不安全。因此在进行字符串拼接的时候推荐使用StringBuffer或者StringBuilder。
我不能保证每个地方都是对的,可是能够保证每一句话,每一行代码都是通过推敲和斟酌的。但愿每一篇文章背后都是本身追求纯粹技术人生的态度。
永远相信美好的事情即将发生。