Java字符串知识点总结

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(or StringBuffer) class and its append method. String conversions are implemented through the method toString, defined by Object 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

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 the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

简单地说,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

StringBuilderStringbuffer 都是 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;
    }
 }

从上述的源码中可以看到 StringBufferStringBuilde 都是调用其父类的 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

  1. JDK 8 String
  2. 专题整理之—String的字符串常量池
  3. 专题整理之—不可变对象与String的不可变
  4. [Java核心技术·卷 I(原书第10版)](
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容