String类相信大家都不陌生,我们写工程使用String的频率,就像写作文时使用汉字“的”的频率一样高。那么你经常使用它,是否真的“了解”它?请带着问题,一步一步的揭开它神秘的面纱,看看它到底是何许“人”也!
一、先简单了解jvm
大家都知道jvm也就是Java虚拟机,这里我们只说下JVM的内存区,运行时内存区主要可以划分为:栈 (Java Virtual Machine Stacks) 、 堆内存 (Heap Memory)、方法区 (Method Area) 、本地方法栈 (Native Method Stacks) 、和程序计数器 (Program Counter (PC) Register)。
1.栈
栈是线程私有,自动分配的连续的空间,后进先出。栈用来存放栈帧。栈帧用于存储 局部变量表、操作数栈、动态链接、方法返回等信息。 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈内存中的数据没有初始化默认值,需要手动设置,且数据执行完毕,变量会立即释放。这样可以节约内存空间。
2.堆内存
堆内存是线程共享,且是不连续的空间。堆用来存放new创建的对象和数组,与栈不同,堆内存中的实体是用来封装数据的,都是有初始化默认值的。
3.方法区
方法区和堆一样都是线程共享。方法区又被称为静态区,它用于存储已被虚拟机加载的 类信息、常量、静态变量等数据。jdk1.8已经将方法区取消,替代的是元数据区,这是一块堆外的直接内存,与jdk1.6和jdk1.7方法区不同。jdk1.6和jdk1.7方法区理解为永久区(具体可以看下面两张图)。
4.常量池
运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用如声明为final的常量值等。并且池中的字符串的内容不重复!
java7内存结构图
java8内存结构图
二、了解String
String常量,它的值是在常量池中的。JDK8 已经将常量池转移到Java堆中而不是方法区。
想要从本质上了解String类,我建议可以先看看String类的源码。
String内部是用final修饰,并且可序列化,可比较,不可变,不能被继承,实现了Serializable,Comparable,CharSequence接口。同时String类是通过char数组来保存字符串的,char又被final修饰,所以说字符串是常量,它们的值在创建之后不能更改。
String类重写了hashCode方法,String类的hash采用多项式计算得来,我们完全可以通过不相同的字符串得出同样的hash,所以两个String对象的hashCode相同,并不代表两个String是一样的。同一个String对象 hashCode 一定相同, 但是 hashCode相同 ,不一定是同一个对象。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
构造方法:通过字符数组,StringBuffer,StringBuilder进行初始化时,就要执行value数组元素的拷贝,创建新数组,防止外部对value内容的改变。
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//参数为char数组,使用java.utils包中的Arrays类复制
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//略
// 检查越界
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
//从bytes数组中的offset位置开始,将长度为length的字节,以charsetName格式编码,拷贝到value
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
//调用public String(byte bytes[], int offset, int length, String charsetName)构造函数
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
//内部构造方法 , 能修改value,外部无法访问
String(char[] value, boolean share) {
this.value = value;
}
当使用任何方式来创建一个字符串对象时,运行中JVM会拿着这个对象在String池中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串,否则,不在池中添加。
3.String类的intern()方法
上面英文注释翻译过来就是:在调用intern方法时,如果池已经包含了string等于这个string的对象(由equals(Object)方法判断),则池中的字符串返回。否则,将此String对象添加到池中和返回这个String对象的引用。
三、加深印象
如果上面只是了解了概念,那下面就来做几道题,加深印象吧!
1.下面一共创建了几个对象?
String str1 = "bbc";
String str2 = "bbc";
答案:一个对象(常量池对象),JVM先查找常量池中存不存在"bbc"这个对象,如果不存在,就在常量池中创建对象"bbc",然后把池中"bbc"对象的引用地址返给str1,这时str1指向池中"bbc"字符串对象。如果存在,则不创建任何对象,直接将常量池中的"bbc"对象的地址返回给str2。
2.再问,下面一共创建了几个对象?
String str1 = new String("bbc");
String str2 = new String("bbc");
答案:一共是三个对象(new创建的两个对象和一个常量池中创建的对象),String str1 = new String("bbc");这种方式创建字符串实际生成了两个字符串对象。首先,构造器中传入了一个字符串对象 ,“bbc”它就被放在字符串常量池中。创建str2时由于常量池中已经存在"bbc",所以不会重复创建。