文章目录
- 本文代码均基于JDK8
String简介
String定义
首先,我们来看Java官方文档中对String的定义:
The String class represents character strings. All string literals in Java programs, such as "abc", are implemented as instances of this class.
String类代表字符串,Java中所有字符串字面量,例如‘abc’,都是此类的实例。所谓字面量即指在程序中无需变量保存,可以直接表现为具体的数字或字符串的值。例如:
String str ="hello world"
此处的hello world 就是一个字符串字面量。
源代码中的String
通过查看JDk8中String的源代码,我们可以发现String的一些性质。
//String源代码节选
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;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
}
首先 String被final修饰,即不可被继承。
String 实现了Serializable、Comparable和CharSequence三个接口。其中:
-
实现Serializable接口意味着String类的对象可被序列化,即可以被转换为字节序列存储或传输。
-
实现了Comparable接口意味着String对象可以进行排序,String的compareTo()方法排序规则将下一章进行详细讨论。
-
实现了CharSequence接口意味着String是可读的char值序列,String的charAt().length(),toString()等方法都继承自该接口。
String类有四个成员变量,serialPersistentFields和serialVersionUID都用于序列化过程,其中serialVersionUID是可序列化类的版本标识,保证反序列化时发送和接受的对象可兼容。hash是String对象的hash码,其中空字符串的hash码为0。value是一个不可变的char数组,用来存储String对象的值,因此String的值在创建后不可被修改。
/**
* 空字符串的hash码
*/
System.out.println("".hashCode());//0
String常用方法
接下来,通过实验的方式了解几个String类的常见方法。
charAt()
public char charAt(int index)
charAt()方法将返回该String对象index处的值。返回值为char。
/**
* charAt()方法测试
*/
System.out.println("abc".charAt(0));//a
System.out.println("abc".charAt(-1));//抛出java.lang.StringIndexOutOfBoundsException异常
System.out.println("abc".charAt(3));//抛出java.lang.StringIndexOutOfBoundsException异常
可见:
- index索引从0开始,字符串中第一个char的index值为0,第二个为1,以此类推。
- 当index值为负数或者超出字符串长度时,抛出StringIndexOutOfBoudsException异常。
concat()
public String concat(String str)
concat()方法将指定字符串连接到该字符串的结尾。
//concat()源代码
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
从concat()方法的源代码中可以知道,a.concat(str),当拼接的字符串str的长度为0时,将返回a字符串本身。当str长度不为0时,将返回一个新字符串。
valueOf()
static String valueOf(boolean b)
static String valueOf(char c)
static String valueOf(char[] data)
static String valueOf(char[] data, int offset, int count)
static String valueOf(double d)
static String valueOf(float f)
static String valueOf(int i)
static String valueOf(long l)
static String valueOf(Object obj)
String类中的valueOf()方法有很多,作用是将传入的参数转换为字符串。valueOf()都由static修饰,是类的静态方法,其参数可以是boolean、char、char[]、double、float、int、long、Object类型。值得注意的是,当参数为一个未初始化的对象,valueOf()会返回字符串"null";但当参数为null时抛出NullPointerException异常;当参数为boolean值true或false,分别返回字符串"true"和"false"。
/**
* valueOf()方法测试
*/
Object o = null;
System.out.println(String.valueOf(o)); //输出null
System.out.println(String.valueOf(true)); //输出true
System.out.println(String.valueOf(false)); //输出false
System.out.println(String.valueOf(null)); //抛出NullPointerException异常
从源码中看,valueOf()方法实质上是调用了参数类型的toString()方法,基本数据类型则调用其包装类的toString()方法(boolean除外)。
//valueOf()源码节选
//参数为int类型的valueOf()方法
public static String valueOf(int i) {return Integer.toString(i);}
//参数为boolean类型的valueOf()方法
public static String valueOf(boolean b) {return b ? "true" : "false";}
//参数为Object类型的valueOf()方法,这也解释了测试中输出null的情况。
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();}
compareTo()
public int compareTo(String anotherString)
compareTo()方法继承自comparable接口,可以按字典顺序比较两个字符串,如果该字符串等于(equal)参数字符串则返回0,大于参数字符串则返回正数,小于参数字符串则返回负数。
我们直接到源码中去寻找compareTo()的比较规则。
//compareTo()方法源码
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
//lim为两字符串中较短串的长度,用作Unicode循环比较的次数。
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
//比较每一位的Unicode差值,不等则返回差值,相等则继续循环。
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//循环结束意味着两字符串前lim位Unicode值均相等,则返回字符串长度差值。
return len1 - len2;
}
可以发现:
- 从左至右逐位比较该位字符的Unicode值,当某位Unicode不等时,返回该位Unicode的差值(a.compareTo(b)返回a[k]-b[k],str[k]表示字符串str第k位字符的Unicode值);若Unicode相等,则继续比较下一位。
- 将两字符串中较短的字符串全部进行比较结束后,仍未返回结果,则返回两字符串长度差值(若两字符串长度相等差值为0)。
以下的实验结果也印证了上述结论。
/**
* compareTo()方法测试。
*/
System.out.println("a".compareTo("b"));//-1
System.out.println("ab".compareTo("ac"));//-1
System.out.println("cd".compareTo("ae"));//2
System.out.println("ab".compareTo(new String("ab")));//0
System.out.println("abc".compareTo("abcde"));//-2
equals()、==和intern()
public boolean equals(Object anObject)
equals()方法可能是String所有方法中最常用的一个,用来比较字符串是否与参数对象相同。Java官方文档中对该方法的介绍如下:
The result is true if and only if the argument is not null and is a String object that represents the same sequence of characters as this object.
可知equals返回true的条件有三:参数对象不可为null;参数对象是一个String对象;参数对象和本对象表示相同的字符序列。除此之外的情况会返回false。
//equals方法源码
public boolean equals(Object anObject) {
if (this == anObject) {
return 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;
}
从源码来看,
- 如果anObject与调用equals()方法的对象指向内存中的同一个对象(this == anObject),则返回true;
- 否则继续进行比较,若anObject不是一个String类的实例(anObject instanceof String)直接返回false;
- 否则继续比较两个String的长度,长度不等直接返回false;
- 否则继续比较String的内容,相同则返回true,不同则返回false。
由此可以引出那个老生常谈的问题:
String比较时,equals()和==的区别
结合源码,很容易得出这个问题的答案:
-
首先,==是操作符,而equals()是String类继承自Object类的一个方法。两者不是一个层面的概念。
-
==比较的是引用内容是是否相同,即是否在内存中指向了同一处内存空间。
-
equals()既比较了引用内容是否相同,还比较了值是否相同。
-
因此,a==b时,a.equals(b)一定成立;但a.equals(b)并不意味着a==b(a、b都是String对象)。
值得注意的是,此处的结论仅针对String对象,而不适用于其他类。实际上没有重写过的equals()方法就是对对象进行了’=='操作。
//Object类源码节选
public boolean equals(Object obj) {return (this == obj);}
intern()
public String intern()
intern()将返回字符串对象的一个规范表示(returns a canonical representation for the string object),在源码中标记为native方法:
//intern()源码
public native String intern();
intern()并不是一个非常常用的方法,并且在不同版本的JDK中有较大改变。之所以在此列出这个方法,是因为intern()与内存中的字符串常量池有关,会对equals()和==的结果产生影响。我们将在下一章节中讨论intern(),在此不进行详细叙述。
其他方法
除上述方法外,String类还提供了许多操作字符串的方法,在此列出几个常用的方法及其作用,不再展开详谈。
public int length();//返回字符串长度
public boolean contains(CharSequence s);//返回字符串中是否含有某子串
public char[] toCharArray();//将字符串转换为char数组
public String toLowerCase();//小写字符串
public String toUpperCase();//大写字符串
public static String format(String format,Object... args);//根据format参数内容格式化字符串
public String[] split(String regex);//根据regex切割字符串
public String substring(int beginIndex,int endIndex);//截取字符串(包含beginIndex处字符,不包含endIndex处字符)
true or false?内存中的String
String s1 = new String("str");
String s2 = new String ("str");
String ss = "str";
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1==s2);
System.out.println(s1==ss);
System.out.println(s1==s3);
System.out.println(ss==s3);
System.out.println(s3==s4);
String str =new String("hello");
String str2 = str.intern();
String str3 = new String("wor")+new String("ld");
String str4 = str3.intern();
System.out.println(str==str2);
System.out.println(str3==str4);
以上有十个println语句,其中哪些是false、哪些是true?在了解String对象在内存的存储前,对此感到云里雾里是正常的。而对此深入了解之后,以上的问题就都变得十分简单了。
String常量池(String Pool)
首先,对于Java虚拟机有一定了解的人都应该知道,运行时产生的Java对象基本上都存放在堆(heap)中。但是为了提高性能,节约内存,Java提供了常量池这个概念。常量池就类似一个JAVA系统级别提供的缓存。八种基本数据类型和String都有其常量池。其中八种基本数据类型的常量池是系统预设的,例如Integer常量池存储-128~127之间的整数,当我们在代码中new Integer(xx)
对象时,JVM会先检查常量池中是否存在值为xx的对象,如果有,直接返回常量池中的该对象;没有则在堆中新建对象。
而String常量池比较特殊,系统并没有预设String常量池,而是在当使用""双引号声明String对象时,将其加入常量池(并未唯一途径)。例如
String str = "hello world";
"hello world"的声明在String常量池中建立了String对象,str引用了该对象。这样以来,那道经典的题目就很好解释了:
String str = new String("hello");//这个语句创建了几个对象?
答案应该是一个或两个。如果常量池中没有值为hello的String对象,则"hello"会在常量池中创建一个对象,new String()
的部分,又在常量池外建立了一个String对象,则共两个;如果常量池中已有hello对象,则只有new String()
部分创建一个String对象。
那么,String常量池又在哪里呢?事实上,历代JDK中常量池的位置是不同的。
- 在JDK6及以前,String常量池与其他常量池都存储在运行时常量池,它是方法区的一部分。
- 在JDK7中,String Pool被转移到堆中。这是因为方法区的空间有限,String常量池占用过多内存会导致OutOfMemoryError。并且JDK7之后,String Pool存放的不是实例对象,而是String对象的引用值,实例对象存放在堆中。
- JDK8之后,方法区的概念被元空间(metaspace)取代,其他常量池也随之转移到元空间。
intern()
注意:本节为描述方便,对于JDK7以后的版本也采用了“对象加入常量池“,”常量池中的对象”等说法。实际上JDK7后String Pool存放对象的引用而不存放对象。
intern()将返回字符串对象的一个规范表示(returns a canonical representation for the string object)
了解过String Pool,再回顾之前提到过的intern()方法。intern()正是将String对象加入常量池的第二条途径。
- 在JDK6及以前,执行一个String对象的intern()方法,当String Pool中有相同的对象,则返回常量池中对象的引用,否则在常量池中建立值与该对象相同的String对象,并返回其引用。新建的对象和执行intern()方法的对象不是同一个对象。
- 在JDK7以后,执行一个String对象的intern()方法,当String Pool中有相同的对象,则返回常量池中对象的引用,否则将该String对象加入常量池中,并返回其引用。两种情况下intern()都不会新建String对象。
- 此处的相同指通过String的equals()方法判断为true(a.equals(b)=true,则a与b相同)。我们继续通过实验来感受这个方法。
//基于JDK8的intern()方法测试
String s1 = "hello";
String s2 = s1.intern();
System.out.println(s1==s2);//true
String ss = "world";
String s3 = s1+ss;
String s4 = "helloworld";
String s5 = s3.intern();
System.out.println(s3==s4);//false
System.out.println(s3==s5);//false
System.out.println(s4==s5);//true
首先,通过""声明的方式建立的s1和ss显然引用了String Pool中的对象,s2是s1.intern()的返回值,String Pool中有值为hello的对象,则直接返回了其引用。因此s1和s2都指向String Pool中引用的hello,因此s1==s2为true;s3通过s1+ss的方式建立,引用的不是String Pool中的对象,s4引用了"helloworld"声明在String Pool中建立的对象。s5引用的对象是String Pool中与s3相同的对象,与s4相同。因此s3==s4和s3==s5为false,s4==s5为true。
值得注意的是:
String s1 = "hello";
String s2 = "world";
String s3 = s1+ss;
String s4 = s3.intern();
System.out.println(s3==s4);
JDK6和JDK7中,s3==s4的结果不同,JDK6结果为false,JDK7以后结果为true。从两版本intern()方法的说明中很容易得到答案,在此不赘述。
基于对intern()方法的了解,intern()方法的作用也不难理解。在大量使用String的情景下,合理地使用intern()可以极大的节约内存空间。
true or false?
/**
* intern()方法测试
*/
String s1 = new String("str");
String s2 = new String ("str");
String ss = "str";
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1==s2);//false
System.out.println(s1==ss);//false
System.out.println(s1==s3);//false
System.out.println(ss==s3);//true
System.out.println(s3==s4);//true
String str =new String("hello");
String str2 = str.intern();
String str3 = new String("wor")+new String("ld");
String str4 = str3.intern();
System.out.println(str==str2);//false
System.out.println(str3==str4);//true
本章开始时的问题,在深入了解了String Pool和intern()等概念、方法后,应该不难解答,不再赘述。
StringBuffer和StringBuilder
从’+'说起
"+“这个符号作为一个运算符的含义应该没有人不知道。int i=1+1
此时的i值为2。但是在字符串操作中,String str="hello"+"world"
显然无法将”+“认作是加号运算符。事实上,”+“在此处作为连接符使用。作用是将+前后的字符串拼接到一起。可是我们知道,Java是一个面向对象的语言,其中的String对象都是不可变对象,那么”+"操作的实质是什么呢?通过反编译class字节码可以找到答案。
- 首先,定义一个Test类。
public class Test{
public static void main(String[] args){
String ss1 ="hello";
String ss2 = "world";
String str1 = ss1+ss2;
String str2 = "hello"+"world";
}
}
- 使用
javac Test.java
将其编译为class文件。 - 使用
javap -c Test.class
反编译class文件,得到汇编代码如下(基于JDK8)。
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: ldc #8 // String helloworld
27: astore 4
29: return
}
从第9行起,为main方法的反编译结果。从第17到23行的结果可以看出,str1=ss1+ss2的实际操作是:
str1 = new StringBuilder().append("hello").append("hello").toString();
但是,看第24行,str2="hello"+"world"
没有经过这样的操作,而是被Java虚拟机直接优化为了str2="helloword"
。
换言之,
- 对于"+“左右两侧存在String变量的”+"操作,实际上是通过StringBuilder的append()完成的。
- 而"+“左右两侧均为字符串字面量的”+"操作被JVM优化,直接创建了值为拼接后字符串的String对象。这种技术叫做常量折叠,在此不作深入讨论。
StringBuilder和StringBuffer
我们已经知道Java中字符串相加的操作实质上是利用StringBuilder完成的。现在就来了解一下StringBuilder。
A mutable sequence of characters. This class provides an API compatible with StringBuffer, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement for StringBuffer in places where the string buffer was being used by a single thread (as is generally the case). Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.
官方文档中已经对其作出了明确的解释。StringBuilder是一个可变的字符序列,其作用与StringBuffer类似,是其单线程版本。虽然不能保证线程安全,但是性能比StringBuffer强,在合适的条件下(可以理解为保证线程安全的前提下),建议优先使用此类。在文档中StringBuilder的定义结合了StringBuffer。看来StringBuffer应该比StringBuilder更早出现在Java中。事实也的确如此。在JDK1.0中就已经有了StringBuffer,而StringBuilder出现在JDK1.5。我们来看StringBuffer的定义。
A thread-safe, mutable sequence of characters. A string buffer is like a String, but can be modified. At any point in time it contains some particular sequence of characters, but the length and content of the sequence can be changed through certain method calls.
StringBuffer是一个线程安全的可变字符序列,StringBuffer对象就像一个String,但是是可变的。在特定的时间点,它的内容是确定的,但是可以通过某些方法改变其长度和内容。
事实上,两者的构造器、方法和变量都很类似,StringBuffer的线程安全是通过Synchronized锁实现的。
我们在此对两者做一下总结:
-
StringBuffer和StringBuilder在功能上是高度相似的。
-
StringBuffer是线程安全的,而StringBuilder不是。
-
StringBuilder的性能比StringBuffer强。
关于StringBuffer和StringBuilder的详细内容不在此赘述,有机会在其他文章里写一写。
为什么不用"+"?
我们已经知道,"+“连接符实际上是通过StringBuilder的append()方法完成的,那么为什么往往不推荐使用”+"来连接字符串呢?
我们再来看上文中的例子:
String ss1 = "hello";
String ss2 = "world";
String str1 = ss1+ss2;
实际上就是str1 = new StringBuilder().append("hello").append("hello").toString();
。
new StringBuilder()
这一部分应该引起我们的注意。事实上,每执行一个代有"+"连接符的语句都会新建一个StringBuilder对象,当这样的语句较多时,就会新建大量不必要的StringBuilder对象,占据大量内存空间。例如:
public class Test{
public static void main(String[] args){
String ss1 = "hello";
String ss2 = "world";
for(int i=0;i<100;i++){
ss1 += ss2;
}
}
}
反编译结果如下:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: iconst_0
7: istore_3
8: iload_3
9: bipush 100
11: if_icmpge 39
14: new #4 // class java/lang/StringBuilder
17: dup
18: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
21: aload_1
22: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: aload_2
26: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_1
33: iinc 3, 1
36: goto 8
39: return
}
将例中循环体执行100次,就会新建100个StringBuilder对象,但如果直接通过append()方法,则只需要一个StringBuilder对象,可以节约大量内存资源并减少时间消耗。因此,不建议使用"+"操作符进行字符串拼接。
其他
文章以外
本文基于JDK8,大致讨论了String相关的知识点。写作的主要目的是复习巩固、分享知识,如有漏洞、错误和不妥之处欢迎大家批评指正。
在JDK8之后,String类又有了很多新的优化内容,例如利用Byte数组存储值,而不再使用char数组;"+"操作符的实质从new StringBuilder对象变成了动态调用(InvokeDynamic)等等。本文篇幅有限不再讨论,日后有机会再展开谈一谈。有兴趣的读者可以查看其他资料。
参考
JDK源代码
Java官方文档:
JDK8:https://docs.oracle.com/javase/8/docs/api/java/lang/String.html
JDK7:https://docs.oracle.com/javase/7/docs/api/java/lang/String.html
美团技术团队文章:
https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html
博客:
http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/