java中String不是基本数据类型,String中之所以能够保存字符串是因为其中定义了一个数组。
源码:
public class StringDemo {
public static void main(String[] args) {
String strA= "abc";
String strB=new String("abc");
System.out.println(strA==strB);
}
}
结果:false
如上,字符串内容虽然相同,但是==的结果却是false,若是想要实现比较字符串内容的比较,需要用equals方法。
public class StringDemo {
public static void main(String[] args) {
String strA= "abc";
String strB=new String("abc");
System.out.println(strA.equals(strB));
}
结果:true
equals源码:
public boolean equals(Object anObject) {
//比较传入对象的地址是否相等,如果相等返回true
if (this == anObject) {
return true;
}
//如果地址不相等继续比较字符串的内容(instanceof是判断传入的参数是不是String的实例)
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中的equals方法是被重写过的,因为Object的equals方法是比较的
对象的内存地址,而String的equals方法比较的是对象的值。
●
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要
创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创
建一个String对象。
ps:这里介绍一个典型的面试题:
请解释String比较中“==”与equals()区别?
public class StringDemo {
public static void main(String[] args) {
String strA = "abc";
String strB = "abc";
System.out.println(strA == strB);
}
}
结果:true
如上,==返回结果是true,因为Java底层专门提供了一个字符串常量池,在给strB赋值时,会首先去字符串常量池中查找是否有这个值,没有的话会在常量池中加入,有的话会直接将其地址给strB。
构造方法实例化,会构造两块堆内存空间,只使用一块,匿名对象开辟的内存空间会成为垃圾空间。
String strA = new String( “abc” );
构造方法实例化String对象不会自动保存到字符串常量池中。
所以以下执行的结果为false
public class StringDemo {
public static void main(String[] args) {
String strB=new String("abc");
String strA= "abc";
System.out.println(strA==strB);
}
}
字符串字面量:
1.当你使用字符串字面量创建字符串时,Java 会检查字符串池(String Pool)。
2.如果池中已经存在相同内容的字符串,那么将会返回池中的引用,而不是创建新的对象。
3. 这是为了节省内存,避免创建多个相同内容的字符串对象。
解释:
public class StringDemo {
public static void main(String[] args) {
String strB=new String("abc").intern();
String strA= "abc";
System.out.println(strA==strB);
}
}
ps:这里再介绍一个典型的面试题:
请解释String中两种对象实例化方法的区别?
String stra="zhangsan";
String strb="zhangsan";
System.out.println(stra==strb);//true
String str1=new String("zhangsan");
String str2=new String("zhangsan");
System.out.println(str1==str2);//false
String str3=new String("zhangsan").intern();
String str4=new String("zhangsan").intern();
System.out.println(str3==str4);//true
类别 | 解释 |
---|---|
静态常量池 | 指的是程序在加载的时候会自动将此程序中保存的字符串、常量、类和方法等,全部进行分配; |
运行时常量池 | 当一个程序加载之后,里面可能有一些变量,这个时候提供的常量池。 |
像这些静态的、未加载的.class文件的数据被称为静态常量池,但经过jvm把.class文件装入内存、加载到方法区后,常量池就会变为运行时常量池
当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池存在于内存中,也就是class常量池被加载到内存之后的版本。
不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
简单来说,HotSpot VM(虚拟机)里StringTable是个哈希表,里面存的是驻留字符串的引用(而不是驻留字符串实例自身)。也就是说某些普通的字符串实例被这个StringTable引用之后就等同被赋予了“驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例里只有一份,被所有的类共享。类的运行时常量池里的CONSTANT_String类型的常量,经过解析(resolve)之后,同样存的是字符串的引用;解析的过程会去查询StringTable,以保证运行时常量池所引用的字符串与StringTable所引用的是一致的。
字符串常量池 |
---|
字符串常量池 |
本质就是一个哈希表 |
存储的是字符串实例的引用 |
在被整个JVM共享 |
在解析运行时常量池中的符号引用时,会去查询字符串常量池,确保运行时常量池中解析后的直接引用跟字符串常量池中的引用是一致的 |
为了避免频繁的创建和销毁对象而影响系统性能,实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
示例1: 常量之间的拼接会进行编译期优化
@Test
public void test() {
String s1 = "a" + "b" + "c";
String s2 = "abc";
System.out.println(s1 == s2); // true
}
@Test
public void test1() {
String s = "aabb";
final String s1 = "aa";
final String s2 = "bb";
String s3 = s1 + s2;
System.out.println(s == s3); // true
}
@Test
public void test2() {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4); // false
}
此时的结果为false,这是因为在拼接过程中实现拼接功能的实际是StringBuilder对象,先创建出一个StringBuilder对象,然后调用StringBuilder中的append方法,最后调用toString方法将其转化成一个String类型的对象。所以最后s4的地址是一个String类的对象,而s3是字符串常量池当中的引用,最终结果为false。
说明
intern() 是一种手动将字符串加入常量池中的方法,其优点是执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。但是intern()方法每次操作都需要与常量池中的数据进行比较,查看常量池中是否存在等值数据,所以其主要适用于有限值,并且这些有限值会被重复利用的场景,这样可以减少内存消耗,同时在进行比较操作时减少时耗,提高程序性能。
public native String intern();
字符串常量池池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串内容相等的字符串,则返回池中的字符串地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的地址。
如果不是用双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
new Stirng()的细节说明
下面的代码中一共创建了几个对象呢?
@Test
public void test4() {
String s = new String("hello");
}
来看一下字节码指令当中的信息:
先是创建了一个String类型的对象,然后引入了常量池中的"hello",最后执行了Stirng的构造器。所以一共有两个对象产生。
new Stirng(“xxx”) + new String(“xxx”) 的细节说明
@Test
public void test5() {
String s = new String("Hello") + new String("World");
}
字节码指令当中的细节:
实际上先是创建了一个StringBuilder类的对象,然后调用了StringBuilder的构造器,再从常量池中引入"Hello",创建出一个String类的对象,调用StringBuilder中的append方法将"Hello"加入,之后同样,引入"World",然后创建一个String类的对象,再次appen方法,最后调用StringBuilder中的toString方法。
为什么打印结果输出false呢?
public void test6() {
String s = new String("Hello") + new String("World");
String s2 = "HelloWorld";
System.out.println(s == s2);// false
}
这是因为StringBuilder中的toString()方法:
实际上调用了String类的构造法新建了一个String,而在这个String中只是将原来的char[]中的内容进行了复制,然后将复制的引用返回。所以toString()返回的是一个String类的对象引用,而不是常量池中的引用,所以最后结果是false
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
//
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
关于intern() 方法的面试题
打印结果是什么呢?为什么是这样的结果呢?
public class StringTest {
public static void main(String[] args) {
// 问题一:
String s = new String("1");
String s1 = s.intern();// 调用此方法之前,字符串常量池中已经存在了"1",所以返回"1"在常量池当中的引用
String s2 = "1";
System.out.println(s == s2);// jdk6:false jdk7/8:false
System.out.println(s1 == s2); // true
// 问题二:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";// s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
System.out.println(s3 == s4);// jdk6:false jdk7/8:true
}
}
问题一在注释中以及说明,所以重点来看问题二
首先要明白实际在内存中的细节,才能知道为什么在jdk6中是false,而jdk6之后是true
先来看jdk6中的分析:
两个new String()的相加的操作实际上是创建了一个StringBuilder对象进行append操作,最后调用toStirng方法返回一个String类型对象的引用,将其赋给了s3。
在调用了intern方法后将"11"加入到常量池中,再此之前常量池是没有"11"的,该方法返回的结果是常量池中的引用
而s4直接就是字符串常量池中的引用
最后进行比较,s3是String类型对象引用,s4是常量池中的直接引用,所以结果是false。
再来看jdk7/8中的分析:
同样两个new String()的相加的操作实际上是创建了一个StringBuilder对象进行append操作,最后调用toStirng方法返回一个String类型对象的引用,将其赋给了s3。
但是调用intern方法时会对其进行优化,发现在堆区域中已经有了"11"这个内容,于是就堆区中的String类型对象的引用在方法区中保存。
因为对字符串常量池进行了优化,所以 s3的值也是在堆中的String类型对象的引用值。
拓展
public class StringTest {
public static void main(String[] args) {
//执行完下一行代码以后,字符串常量池中,是否存在"11"呢?
String s3 = new String("1") + new String("1");//new String("11")
//在字符串常量池中生成对象"11",代码顺序换一下,实打实的在字符串常量池里有一个"11"对象
String s4 = "11";
String s5 = s3.intern();
// s3 是堆中的 "ab" ,s4 是字符串常量池中的 "ab"
System.out.println(s3 == s4);//false
// s5 是从字符串常量池中取回来的引用,当然和 s4 相等
System.out.println(s5 == s4);//true
}
}
JDK6和JDK7中该方法的功能是一致的,不同的是常量池位置的改变(JDK7将常量池放在了堆空间中),下面会具体说明。intern的方法返回字符串对象的规范表示形式。其中它做的事情是:首先去判断该字符串是否在常量池中存在,如果存在返回常量池中的字符串,如果在字符串常量池中不存在,先在字符串常量池中添加该字符串,然后返回引用地址
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
String s1 = "hello";
String s1 = "hello";
String str = new String("hello");
public class Main {
public static void main(String[] args) {
String s1 = new String("1");
String s2=s1.intern();
String s3 = "1";
System.out.println(s2 == s3);
}
}
传送门(点我)
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!