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, 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, 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, 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版)](

你可能感兴趣的:(Java字符串知识点总结)