Java 字符串就是 Unicode 字符序列,Java 没有内置的字符串类型,而是在 Java 类库中提供了一个预定义类 String,每个用双引号括起来的字符串都是 String 类的一个实例。
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared. For examples:[1]
String str = "abc";
char data[] = {'a', 'b', 'c'};
String str = new String(data);
The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. String concatenation is implemented through the
StringBuilder
(orStringBuffer
) class and itsappend
method. String conversions are implemented through the methodtoString
, defined byObject
and inherited by all classes in Java. For additional information on string concatenation and conversion, see Gosling, Joy, and Steele, The Java Language Specification.[1]
上面引用JDK 8 API,主要表明 Java 支持 + 进行拼接字符串, Java 8中编译器会调用 StringBuilder 或者 StringBuffer 中的 append 方法来进行字符串拼接,然后通过 toString 方法转化为字符串。
源码实现
JDK 8
String 类申明为 final ,不能有子类继承,内部定义了 char 数组进行存储字符串的值,并且用 final 进行修饰,表明 String 是不可变的。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
// code
}
JDK 9
Java 9 中,String 类用 byte[] 数组进行存储字符串, 并且添加了 coder 标识编码方式。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;
/**
* The identifier of the encoding used to encode the bytes in
* {@code value}. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;
}
底层用 byte 数组实现后,最大的好处就是可以省空间,因为很多字符的范围都在 u00 - uFF 之间,只需要一个 byte 就能存储,之前 char 数组 需要两个 byte 才能存储。
字符串不可变
通过查看源码我们可以知道 Java 中字符串是不可变的,具体的工作方式是 Java 语言的设计者将字符串放在一个公共的存储池,字符串变量都指向性存储池中相应的位置, 这样共享字符串常量可以提高效率,并且设计者认为这种共享带来的高效率胜于提取,拼接字符串带来的低效率。
String Pool
- 字符串类在 Java 堆内存中维护了一个字符串常量池,字符串常量池保存着所有字符串字面量(literal strings),目的是为了减少在jvm中创建的字符串的数量,这些字面量在编译时期就确定,在运行时可以通过
intern()
方法将字符串添加到 String Pool中 。 - 当创建 String 对象时,jvm 会先检查 String Pool 中是否存在相同的字符串,如果有则返回其引用,如果没有就创建一个相应的字符串放入String Pool 中(此过程为intern),再返回对应的引用。
- 常量池:用于保存 Java 在编译期就已经确定的,已经编译的class文件中的一份数据。包括了类、方法、接口中的常量,也包括字符串常量,如String s = "a"这种声明方式。
使用 new 创建 String 时创建了几个对象:
String str = new String("hello");
两个,使用new 创建 String 时,首先创建
hello
字符串字面量(String literal)并将其放入字符串常量池中,然后在堆内存中创建 String 对象
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
并且通过查看原码得知,使用String带参构造创建字符串时,不会完全复制 value 数组的内容,只会指向同一个数组。
代码示例:
String a1 = "a";
String b1 = "a";
System.out.println(a1 == b1); // true
String a2 = new String("a");
String b2 = new String("a");
System.out.println(a2 == b2); // false
String hello = "hello";
String lo = "lo";
System.out.println(hello == "hel" + "lo"); // true
System.out.println(hello == "hel" + lo); // false
String world = "world";
final String ld = "ld";
System.out.println(world == "wor" + ld); // true
总结:
- 使用
""
创建字符串时,在编译期将字符串放入String Pool中,字符串对象引用 String Pool 中的对象。 - 使用
new
创建字符串时,会在堆内存中新创建 String 对象,在运行时创建。 - 使用
String s = "hel" + "lo"
创建的字符串指向 String Pool 中的字符串常量 "hello", 常量池中不会有 "hel" 和 "lo"。 - 使用包含变量的字符串连接符如
"hel" + lo
创建的对象会存储在堆中,运行时期才创建;只要lo
是变量,不论lo
指向池常量池中的字符串对象还是堆中的字符串对象,运行期"hel" + lo
操作实际上是编译器创建了StringBuilder
对象进行了append
操作后通过toString()
返回了一个字符串对象存在heap
上 - 对于
final String ld = "ld"
是一个用 final 修饰的变量,在编译期就已经确定了,所以"wor" + ld
相当于"wor" + "ld"
, 也指向 常量池中的 "world"。
intern()方法
When the intern method is invoked, if the pool already contains a string equal to this
String
object as determined by theequals(Object)
method, then the string from the pool is returned. Otherwise, thisString
object is added to the pool and a reference to thisString
object is returned.It follows that for any two strings
s
andt
,s.intern() == t.intern()
istrue
if and only ifs.equals(t)
istrue
.
简单地说,intern 方法可以将当前字符串对象添加到 String Pool 中(如果String Pool中不存在通过equals判断相同的字符串),并返回其引用,示例代码如下:
String str = "java";
String str2 = new String("java");
String str3 = str2.intern();
System.out.println(str == str2); // false
System.out.println(str == str3); // true
不可变(immutable)的好处
String 设计为不可变主要是从性能和安全方面进行考虑
首先只有当字符串是不可变时才能实现字符串常量池,从而节约 JVM 内存,提高性能。
缓存 hashcode
/** Cache the hash code for the string */
private int hash; // Default to 0
阅读源码可知, String 不可变就可以缓存 hashcdoe 对应的 hashcode, 以后每次使用该对象的 hashcode 时无需重新计算,直接返回即可,这使得字符串很适合作为 HashMap 的 Key,提高效率。
安全性
由于字符串是不可变的,所以用户名,密码之类的信息不能被修改,可以确保安全。同时也不会存在多线程安全问题。
字符串拼接
+ 操作符
使用 +
进行字符串拼接效率较低,执行一次 String s += "hello";
操作,相当于
StringBuilder sb = new StringBuilder();
sb.append(str);
sb.appedn("hello");
s = sb.toString();
每次连接字符串都会构建一个新的 StringBuilder 对象 ,既费时,又耗空间,多次操作不推荐。
concat
// JDK 11
public String concat(String str) {
int olen = str.length();
if (olen == 0) {
return this;
}
if (coder() == str.coder()) {
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;
byte[] buf = Arrays.copyOf(val, len);
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
查看源码我们可以知道,concat
方法大致分为三步
- 创建 byte[] 数组
- 底层调用
System.arraycopy
方法进行数组拷贝 - 返回
new String(buf, coder)
;
多次调用会创建多个 byte[] 数组 以及多个 String 对象,多次调用也不推荐。
StringBuilder 和 StringBuffer
StringBuilder
和 Stringbuffer
都是 AbstractStringBuilder
的子类
// JDK 8
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
}
StringBuilder
// JDK 8
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
/** use serialVersionUID for interoperability */
static final long serialVersionUID = 4383685877147921099L;
// code ...
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
StringBuffer
// JDK 8
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
// source code ...
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
从上述的源码中可以看到 StringBuffer
和 StringBuilde
都是调用其父类的 append
方法
abstract class AbstractStringBuilder implements Appendable, CharSequence {
// source code ...
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
}
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// source code...
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
}
从源码中可以看出 append 方法主要有一下三个步骤:
- 判断入参 str 是否为 null, 如果为 null, 在 value 数组中追加 "null"
- 将 value 数组进行扩容,基本的扩容逻辑为 value 原来长度 * 2 + 2,或者 count + str.length, 或者 Integer.MAX_VALUE - 8(减去 8 是因为一些虚拟机会在数组中保留一些头信息),取其中的最小值。
- 最后调用
System.arraycopy
进行字符串拷贝
append 操作大部分操作在扩容与数组拷贝,不用进行重复的 new 创建对象操作,因此效率较高。
StringBuilder 与 StringBuffer 的区别
从源码中我们可以看到, StringBuffer 的 append 方法中多了 synchronized
关键字,因此它是线程安全的,但是效率较低, StringBuilder 线程不安全,效率较高。
格式化输出
Java 5 沿用了 C 语言函数库中的 printf 方法进行输出格式化,如
System.out.println("%8.2f", x);
// 增加分组分隔符
System.out.println("%,.2f", 10000.0 / 3.0); // 3,333.33
也可以使用静态的 String.format 方法创建一个格式化的字符串,而不打印输出
String message = String.format("Hello, %s.Next year, you'll be %d", name, age);
本篇文章同步在Github上,欢迎大家来Github多提issue。
Reference
- JDK 8 String
- 专题整理之—String的字符串常量池
- 专题整理之—不可变对象与String的不可变
- [Java核心技术·卷 I(原书第10版)](