Java 字符串

117 阅读6分钟

String 类的声明

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */ private final char value[];
}
  • String 类是 final -> 不可以被子类继承
  • 实现了 Serializable 接口 -> 可以序列化
  • 实现 Comparable 接口 -> 可以序列化

String 底层由 char[] 到 byte[] 的转变

  • 使得 String 的占用的内存减少一半 -> GC 次数的减少
  • 引入了编码检测(Latin-1)的开销 -> 引入 coder 字段区分不同的编码

String 类的 hashCode 方法

private int hash; // 缓存字符串的哈希码

public int hashCode() {
    int h = hash; // 从缓存中获取哈希码
    // 如果哈希码未被计算过(即为 0)且字符串不为空,则计算哈希码
    if (h == 0 && value.length > 0) {
        char val[] = value; // 获取字符串的字符数组

        // 遍历字符串的每个字符来计算哈希码
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i]; // 使用 31 作为乘法因子
        }
        hash = h; // 缓存计算后的哈希码
    }
    return h; // 返回哈希码
}

String 的方法介绍

  • substring(int beginIndex) 截取字符串
  • indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) 查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引
  • length() 用于返回字符串长度
  • isEmpty() 用于判断字符串是否为空
  • charAt() 用于返回指定索引处的字符
  • valueOf() 用于将其他类型的数据转换为字符串
  • getBytes() 用于返回字符串的字节数组
  • trim() 用于去除字符串两侧的空白字符
  • toCharArray() 用于将字符串转换为字符数组

String 的不可变性

  • String 类由 final 修饰
  • 数据存在 char[] 数组中也是由 final 修饰 -> String 对象是没有办法被修改的 -> String 类的一些方法实现最终都返回了新的字符串对象

好处

  • 保证 String 对象的安全性
  • 保证哈希值不会频繁变更
  • 可以实现字符串常量池

字符串常量池

由于字符串的使用频率实在是太高了,所以 Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一块空间——也就是字符串常量池

// 会创建两个对象
// 字符串常量池一个,堆上一个
String s = new String("二哥");

// 只创建一个对象
// 字符串常量池一个,引用变量 s 存储在栈上
String s = "三妹";

字符串常量池在内存中的位置

Java 7 之前

字符串常量池位于永久代(Permanent Generation)的内存区域中,主要用来存储一些字符串常量(静态数据的一种) ->我们创建一个字符串常量时,它会被储存在永久代的字符串常量池中。如果我们创建一个普通字符串对象,则它将被储存在堆中

Java 7

将字符串常量池从永久代中移动到堆中。这个改变也是为了更好地支持动态语言的运行时特性

Java 8

永久代(PermGen)被取消,并由元空间(Metaspace)取代。元空间是一块本机内存区域,和 JVM 内存区域是分开的。不过,元空间的作用依然和之前的永久代一样,用于存储类信息、方法信息、常量池信息等静态数据。

 String.intern() 方法

该方法会从字符串常量池中查找这个字符串是否存在,若存在,则放回字符串常量池中的对象的引用

String s1 = new String("二哥三妹");
String s2 = s1.intern();
System.out.println(s1 == s2);

// s1 的对象是存在堆区的对象
// s2是字符串常量池的应用
// 故不相同

StringBuffer和StringBuilder的区别

// StringBuffer 
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {

    public StringBuffer() {
        super(16);
    }
    
    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }

    public synchronized String toString() {
        return new String(value, 0, count);
    }

    // 其他方法
}
  • StringBuffer 操作字符串的方法加了 synchronized 关键字进行了同步,主要是考虑到多线程环境下的安全问题,所以如果在非多线程环境下,执行效率就会比较低,因为加了没必要的锁。
  • => 为增加在单线程的执行效率,Java 还提供了 StringBuilder
  • => 我们可以在 TreadLocal 中多线程的使用 StringBuilder
public final class StringBuilder extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    // ...

    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

    // ...
}
new String("二哥") + new String("三妹");
// Java 会将上述代码解释为以下代码
new StringBuilder().append("二哥").append("三妹").toString();

StringBuilder 的内部实现

/**
 * Constructs a string builder with no characters in it and an
 * initial capacity of 16 characters.
 */
public StringBuilder() {
    super(16);
}

 StringBuilder 对象创建时,会为 value 分配一定的内存空间(初始容量 16),用于存储字符串。   #Todo 其他方法

字符串相等判断:Java中的equals() 与== 的区别和用法

  • == 操作符用于比较两个对象的地址是否相等
  • .equal() 方法用于比较两个对象的内容是否相等
// String 源码的 equal 的比较方法
public boolean equals(Object anObject) {
	// 如果引用地址相同则一定相同
    if (this == anObject) {
        return true;
    }
    // 否则再判断内容是否相同
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                    : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

优雅的拼接 String 字符长

循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符。原因就在于循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能

循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符

Append() 源码

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

public AbstractStringBuilder append(String str) {
	// 判断字符串是否为 NULL
    if (str == null)
        return appendNull();
    int len = str.length();
    
    // 确保当前 AbstractStringBuilder 对象有足够的容量来容纳新添加的字符串内容
    // count 是当前字符序列的长度,加上要追加的字符串长度后,检查是否需要扩容
    ensureCapacityInternal(count + len);
    
    // 将传入的字符串 str 从索引 0 开始的 len 个字符复制到当前对象的字符数组 value 中,
    // 复制操作从当前字符序列的末尾(count)开始
    str.getChars(0, len, value, count);
    
    count += len;
    return this;
}

在 Java 中拆分字符串:详解 String 类的 split () 方法

特殊分隔符会报错

  • 反斜杠 \(ArrayIndexOutOfBoundsException)
  • 插入符号 ^(同上)
  • 美元符号 $(同上)
  • 逗点 .(同上)
  • 竖线 |(正常,没有出错)
  • 问号 ?(PatternSyntaxException)
  • 星号 *(同上)
  • 加号 +(同上)
  • 左小括号或者右小括号 ()(同上)
  • 左方括号或者右方括号 [] (同上)
  • 左大括号或者右大括号 {}(同上)

需要采用正则表达式去分离

学习正则表达式的简单方法

常用正则表达式