关于String的两种实例化方式,我们要先理解类、对象、实例的含义和它们之间的区别与联系。类是抽象化的(如狗),对象是具体化的(如京巴狗),我们常说一个对象(京巴狗)是某个类(狗)的一个实例(instance),所以对象与实例的含义等价。
我们知道,字符串操作是计算机程序设计中最常见的行为。学习String的实例化是非常有必要的。以下是String的源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}
我们可以看出String是final类型,不能被继承。它继承了3个接口,Serializable(可序列化的)、Comparable(可比较的)、CharSequence(字符序列)。
- 继承Serializable接口,可以被JVM序列化成文件。
- 继承Comparable接口,主要实现它的compareTo(T o)方法,此方法用来比较两个字符串的长度,返回一个int值。
- 继承CharSequence接口,实现的方法常用的有length()、toString()等。
顺便提醒,Java是单继承、多实现语言。
注意:
- 本文多次出现的字符串是指字符串常量,为表达意思更加贴切,遂有的地方使用字符串代替字符串常量。
- 本文多次出现的引用指对象引用(实例引用),引用和基本数据类型均为变量,在JVM中存于Java虚拟机栈中,更确信的说是局部变量表中,如果你想要深入了解请看《深入理解Java虚拟机(第二版)》或者这篇博文。
- 本文多次使用大意为“返回引用”的语句,可以理解为返回引用所指向的对象。
字符串常量池
想要了解String的实例化,要先了解字符串常量池。字符串常量池存放于Java文件编译生成的Class文件中,Class文件存放于方法区(《深入理解Java虚拟机(第二版)》第65页)。
JVM中设计字符串常量池是为了减少实例化重复的字符串,以节约新建时间和内存空间。如果字符串已经在字符串常量池存在,就不再新建,而是将新的引用指向它,可以理解为多个引用共享一个字符串。因为String为final类型,它不可改变的特点,保证其在字符串常量池的唯一性(已经存在的不新建,新建的肯定不同),这也是字符串常量池能设计出来的原因。
隐式实例化:直接赋值
public class Demo {
public static void main(String[] args) {
String s = "hello";
String s2 = "hello";
System.out.println(s == s2);
}
}
以上直接赋值,是我们常用的实例化方式。我们将采用直接赋值方式生成的字符串也称为匿名对象,当然,匿名对象也是对象。符号"=="判断两个引用是否指向同一个对象(字符串),通过输出结果为true可知,对于引用s2来说,没有重新新建一个字符串,而是将引用s2指向字符串"hello",
从JVM角度来看,JVM在编译Java文件生成Class文件时,将字符串"hello"添加到字符串常量池中,在虚拟机栈(局部变量表)中给s、s2分配一块内存,均指向字符串"hello"。如下图,我们可以将引用理解为存放它“指向”的对象(字符串)的内存地址,假设字符串"hello"的地址为0x1001。
那如果隐式实例化如下,引用与字符串是什么关系呢?
String s = "hello";
s = "world";
因为String是final类型,即一个字符串(String对象)创建后就不能被修改。所以"world"是一个新的字符串。引用s指向字符串"world",字符串常量池中的字符串"hello"由于没有了引用,会被回收。(本文是基于jdk1.8研究,字符串常量池已经移出永久代。字符串可以被回收)如下图所示:
显式实例化:使用构造函数
public class Demo {
public static void main(String[] args) {
String s = "hello";
String s2 = new String("hello");
String s3 = new String("hello");
System.out.println(s==s2);
System.out.println(s==s3);
System.out.println(s2==s3);
}
}
从输出三个false可以看出,三个对象均不相同,我们知道使用new关键字创建的实例存放于Java堆中。假设堆中存放的两个String对象的内存地址分别为0x1002和0x1003。如前面所讲,引用s所“指向”的字符串存放于字符串常量池。从JVM分析,有如下图示:
显式实例化常用于将字符数组转换为字符串,使用方式如下:
String string = "hello";
char[] array = string.toCharArray();
return new String(array);
intern()方法
你是不是觉得,两种实例化方式产生的“相等(equals())”对象总感觉能联系起来,你的直觉是对的。它们之间的联系纽带在于intern()方法。intern()方法是String类的方法,一个Native方法。一个存放在堆中的String对象调用此方法,JVM会在字符串常量池中去比较是否有“等于(==)”此String对象的字符串,如果有,返回池中代表这个字符串的String对象,没有则将此String对象包含的字符串放入字符串常量池中,再返回此String对象的引用(书中78页)。如果执行以下代码,会输出false/true/true。
public class Demo {
public static void main(String[] args) {
String s = "hello";
String s2 = new String("hello");
String s3 = new String("hello");
System.out.println(s2 == s2.intern());
System.out.println(s == s3.intern());
System.out.println(s2.intern() == s3.intern());
}
}
来分析一下结果:
第一个输出false,我们将引用s、s2、s3所指向String对象分别称为对象1(字符串"hello")、对象2和对象3,根据我上面所写的定义,s2.intern()将返回字符串常量池中的对象1,池中的对象1和堆中的对象2不是同一个对象,我们知道,符号"=="判断两个引用是否指向同一个对象,所以返回false。
如果此时池中没有对象1,即对象2包含的字符串"hello"是首次出现的。代码如下:
String s2 = new String("hello");
System.out.println(s2 == s2.intern());
s2.intern()方法将会把对象2的引用(我理解为字符串“hello”)放入字符串常量池中,并返回此引用,所以结果为true。
第二个和第三个输出true。此时对象1(字符串“hello”)存在字符串常量池中,s2.intern()和s3.intern()均返回对象1,所以两个true。
总结
根据《深入理解Java虚拟机(第二版)》书本上的理解,字符串常量池中的存放直接赋值的字符串(匿名对象)。待我自己安装后一套JDK,实际操作后,再来看看理解是否有误。只有代码不会撒谎!任何观点或定义必须用代码才能解释清楚。