摘要
是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本文来聊聊Java中的。主要包括如下的五个内容:
概览
在Java中,所有类似“”的字面值,都是的实例;类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符以及对象转换为字符串提供了特殊支持,字符串对象可以使用“+”连接其他对象。的部分源码如下:
public final class String
implements java.io.Serializable, Comparable, 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
...
}
从上面的源码可以看出:
类被final关键字修饰,意味着类时不可变类,不能被继承,并且其成员value也是final的,因此字符串一旦创建就不能再修改;类实现了、、接口;实例的值是通过字符数组实现字符串存储的。“+”连接符解析“+”连接符的实现原理
Java语言为“+”连接符以及对象转换为字符串提供了特殊的支持。其中字符串连接是通过及其方法实现的,对象转换字符串是通过方法实现的,方法由类实现,并可被Java中的所有类继承。用个简单的例子来验证“+”连接符的实现原理:
// 测试代码
public class Test {
public static void main(String[] args) {
int i = 2;
String str = "abc";
System.out.println(str + i);
}
}
// 反编译后
public class Test {
public static void main(String args[]) {
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}
由反编译后的代码可以看出,Java使用“+”连接字符串对象时,JVM会创建一个对象,并调用其方法将字符串连接,最后调用对象的方法返回拼接好的字符串。所以在实际代码编写中,使用“+”来拼接字符串和使用对象的方法来拼接字符串对象是等价的。
“+”连接符的注意事项“+”的效率
使用“+”连接符时,JVM会隐式创建对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。因为大量创建在堆内存中,必然会造成效率的损失,所以这种情况建议在循环体外创建一个对象调用方法手动拼接。
字符串常量的优化
编译时可以解析为常量值还有一种特殊情况,当“+”两端均为编译器确定的字符串常量时,编译器会进行优化,直接将两个字符串拼接好。例如:
String s = "hello" + "world!";
// 反编译后
String s0 = "helloworld!";
/**
* 编译期确定
* 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
* 所以此时的"a" + s1和"a" + "b"效果是一样的。故结果为true。
*/
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println((s0 == s2)); // true
编译时不可以被解析为常量值
/**
* 编译期无法确定
* 这里面虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定
* 因此s0和s2指向的不是同一个对象,故上面程序的结果为false。
*/
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println((s0 == s2)); // false
public String getS1() {
return "b";
}
综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如”hello”+”java”; 的字符串相加,在编译期间便被优化成了””。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
字符串常量池字符串常量池介绍
在Java语言中的8种基本类型和类型,JVM都为它们提供了一种常量池的概念,常量池就类似于一个Java系统级别提供的缓存。8种基本类型的常量池都是系统协调的,类型的常量池比较特殊,它的主要使用方法有两种:
由于字符串的不可变性,常量池中一定不存在两个相同的字符串。
内存区域
在 VM中字符串常量池是通过一个类实现的,它是一个Hash表,默认值大小长度是1009;这个在每个 VM的实例中只有一份,被所有的类共享;字符串常量由一个一个字符组成,放在了上。要注意的是,如果放进 Pool的非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用.时性能会大幅下降(因为要一个一个找)。在JDK6及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中的,的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,的长度可以通过-XX:=66666参数指定。至于JDK7为什么把常量池移动到堆上实现,原因可能是由于方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。
内存的分配
在JDK6及之前版本中, Pool里放的都是字符串常量;在JDK7.0中,由于.()发生了改变,因此 Pool中也可以存放放于堆内的字符串对象的引用。请看如下代码:
String s1 = "ABC";
String s2 = "ABC";
String s3 = new String("ABC");
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1.intern() == s3.intern()); // true
由于常量池中不存在两个相同的对象,所以s1和s2都是指向JVM字符串常量池中的”ABC”对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。所以 s3 = new (“ABC”);产生了两个对象:保存在栈中的s3和保存在堆中的对象。当执行 s1 = “ABC”时,JVM首先会去字符串常量池中检查是否存在”ABC”对象,如果不存在,则在字符串常量池中创建”ABC”对象,并将”ABC”对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中”ABC”对象的地址返回给s1。由于s1,s2,s3的字符串值都是在常量池中的同一个引用,所以()方法的返回值是相等的。
.()方法解析.()方法解析
先来看一下.()方法的代码和注释:
/**
* Returns a canonical representation for the string object.
*
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
*
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
*
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* The Java™ Language Specification.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
直接使用双引号声明出来的对象会直接存储在字符串常量池中,如果不是用双引号声明的对象,可以使用提供的方法。 方法是一个方法,方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。JDK1.7的改动将常量池 从 Perm 区移动到了 Java Heap区.() 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
.()的使用
来看看使用和不使用()的执行过程,在用new (“ABC”)实例化对象的时候,如果使用了方法,那么会先去字符串常量池中去查找是否有值为”ABC”的字符串,找到了就不会创建新的”ABC”字符串,找不到才会去创建新的”ABC”字符串;如果不使用方法,则没有去常量池查找的过程,会直接创建新的”ABC”字符串。可以看出二者的区别是:
、与类图
主要区别总结
综上,我们再通过一个例子来测验以上的学习成果:
String s1 = "AB";
String s2 = new String("AB");
String s3 = "A";
String s4 = "B";
String s5 = "A" + "B";
String s6 = s3 + s4;
System.out.println(s1 == s2); // false
System.out.println(s1 == s2.intern()); // true
System.out.println(s1 == s5); // true
System.out.println(s1 == s6); // false
System.out.println(s1 == s6.intern()); // true
要理解此题目,需要搞清楚以下三点:
直接使用双引号声明出来的对象会直接存储在常量池中;对象的方法会得到字符串对象在常量池中对应的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;字符串的+操作其本质是创建了对象进行操作,然后将拼接后的对象用方法处理成对象。
看一下以上的6个对象在内存的分布情况:
【参考资料】:////8/docs/api/
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,永久会员只需109元,全站资源免费下载 点击查看详情
站 长 微 信: nanadh666