浅谈 String

161 阅读4分钟

1.介绍

String 是字符串类,位于 java.lang 包。底层是一个字符数组(char[]),并且被 final 修饰,不可被修改。

说明:这里讨论的是 JDK1.8 版本的 String,JDK1.9 中已改为 byte 数组

原因:字符数组的存储类型是 char,需要2个字节。但是,大多数字符串对象只包含 Latin-1 字符,这些字符只需要1个字节。因此通过 byte 数组存储,可以节省内存空间。由于如 UTF-16 等编码方式需要两个字节来存储字符,因此在 String 中引入了编码标志,来指明使用的是哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    // 保存字符串中的字符
    private final char value[];
    
    // 保存字符串 hash
    private int hash;
}

2.初始化

2.1 使用

// 默认方式
String str_01 = "ab";

// char 方式
String str_02 = new String(new char[]{'a', 'b'});
String str_022 = new String(new char[]{'a', 'b','c'},0,2);

// int 方式
String str_03 = new String(new int[]{0x61, 0x62, 0x63}, 0, 2);

// byte 方式
String str_04 = new String(new byte[]{0x61, 0x62});

// 以上的结果都为 "ab"

2.2 源码简述

// 无参构造函数
public String() {
    this.value = "".value;
}
// 参数为字符串,默认方式
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// 参数为字符数组,char 方式
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
// 参数为整数数组,int 方式
public String(int[] codePoints, int offset, int count) {
    // 判断 offset 合法性,即数组中开始的位置
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    // 判断 count 合法性,即截取的元素个数
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= codePoints.length) {
            this.value = "".value;
            return;
        }
    }
    // 当 offset+count > codePoints.length 时,抛出异常
    if (offset > codePoints.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    
    final int end = offset + count;

    // Pass 1: 判断 int 所表示的 char 是否合法,并计算 [offset,end) 范围内的 int 需要用多少个 char 表示
    int n = count;
    for (int i = offset; i < end; i++) {
        int c = codePoints[i];
        // 判断是否在 Basic Multilingual Plane(BMP)中(Unicode中的一个编码区段),该区间内只需一个 char 就可表示
        if (Character.isBmpCodePoint(c))
            continue;
        // 用于判断当前输入是否是一个标准的codepoint,即是否位于 0x000000~0X10FFFF 之间;剩下区间内的 int 需要两个 char 表示
        else if (Character.isValidCodePoint(c))
            n++;
        else throw new IllegalArgumentException(Integer.toString(c));
    }

    // Pass 2: 实例化 char 数组,并填充
    final char[] v = new char[n];

    for (int i = offset, j = 0; i < end; i++, j++) {
        int c = codePoints[i];
        if (Character.isBmpCodePoint(c))
            v[j] = (char)c;
        else
            Character.toSurrogates(c, v, j++);
    }

    this.value = v;
}

// Character$toSurrogates
static void toSurrogates(int codePoint, char[] dst, int index) {
    // We write elements "backwards" to guarantee all-or-nothing
    dst[index+1] = lowSurrogate(codePoint);
    dst[index] = highSurrogate(codePoint);
}
// 参是是 byte 数组,byte 方式
public String(byte bytes[]) {
    this(bytes, 0, bytes.length);
}


public String(byte bytes[], int offset, int length) {
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

2.3 不可变性(final)

字符串 String 的不可变性由 privatefinal 共同决定。

  • final 使得数组引用一旦被赋值就不可更改,说明该数组引用只能指向最初赋值的内存区域;但是通过引用还是可以修改其所指向的内存区域的内容
    • final char[] value = new char[]{'a','b'};
      System.out.println(Arrays.toString(value));
      // 输出结果:[a, b]
      
      // 编译错误:无法将值赋给 final 变量 'value'
      // value = new char[10];
      
      value[0] = 'y';
      System.out.println(Arrays.toString(value));
      // 输出结果:[y, b]
      
  • private 将数组引用封装在类内部,导致外部对象无法对数组引用直接进行操纵,使得外部对象无法通过数组引用直接修改所指向内存区域的内容

经典面试题:

String str_01 = "abc";
String str_02 = "abc" + "def";
String str_03 = str_01 + "def";

// 一共会创建 3 个 String 对象

3.intern()

intern() 方法返回字符串对象的规范化表示形式。当调用 intern() 方法时,首先检查字符串池中是否存在与之通过 equals() 方法判断后相同的字符串,存在则直接返回字符串池中该字符串的引用;否则根据 JDK 版本进行不同的操作。

在 JDK6 和 JDK7、8 中表现形式不同:

  • JDK6
    • 如果字符串池中存在该字符串,则直接返回字符串池中的对象地址
    • 如果字符串池中不存在该字符串,则把对象复制一份(new),并返回该对象的地址
  • JDK7 & JDK8
    • 如果字符串池中存在该字符串,则直接返回字符串池中的对象地址
    • 如果字符串池中不存在该字符串,则会把对象的引用地址复制一份,放入字符串池,并返回字符串池中的引用地址

总结:

  • 采用 new 创建的字符串对象不进入字符串池;默认方式创建的字符串加入字符串池
  • 字符串相加时,都是静态字符串时,结果加入字符串池;否则不加入字符串池

例子:

String str_1 = "a";
String str_2 = "b";
String str_3 = "ab";
String str_4 = "a" + "b";
String str_5 = str_1 + "b";
String str_6 = str_1 + str_2;
String str_7 = new String("ab");
String str_8 = new String("ab");

// new 创建的字符串不保存在字符串池
System.out.println(str_7 == str_8);
System.out.println(str_8 == str_3);
// intern() 得到的是字符串池中的引用;默认方式创建的字符串被保存进字符串池
System.out.println(str_7.intern() == str_8.intern());
System.out.println(str_8.intern() == str_3);
// 两个常量通过 + 拼接得到的字符串被保存进字符串池;如果存在变量,则不保存进字符串池
System.out.println(str_3 == str_4);
System.out.println(str_3 == str_5);
System.out.println(str_3 == str_6);
System.out.println(str_5 == str_6);
System.out.println(str_5.intern() == str_6.intern());

// 运行结果:
// false
// false

// true
// true

// true
// false
// false
// false
// true
String str_1 = new String("ab");
String str_2 = new String("ab");
String str_3 = "ab";

上面代码实例化过程创建的对象:

  • 可知在创建 str_1 时创建了两个 String 引用,一个驻留在字符串常量池

String2.png

String str_4 = "ef";
String str_5 = new String("ef");
String str_6 = new String("ef");

上面代码实例化过程创建的对象:

  • 可知在 str_4 时创建了一个 String 引用,驻留在字符串常量池

String3.png

4.常用API

4.1 concat() 连接

concat() 方法是字符串连接方法,用来代替 ++ 号。

String s = new String("ab");
s = s.concat("c");
// 结果是:abc

4.2 length() 获取长度

length() 方法是获取字符串长度。底层是直接获取字符数组的长度。

String s = new String("ab");

int len = s.length();
// 结果是:2

4.3 isEmpty() 判空

isEmpty() 方法是判断字符串是否为空,底层是通过判断字符数组长度是否为 0。字符串为空,则返回 truetrue,否则为 falsefalse

String s = new String("ab");
boolean b = s.isEmpty()
// 结果是:false

String s2 = new String();
boolean b2 = s2.isEmpty()
// 结果是:true

4.4 charAt() 获取指定位置元素

charAt() 方法是根据入参索引,获取字符串指定位置的字符。索引位置从 0 开始,从左到右依次递增。如果入参索引超过字符串的索引位置,会抛出 java.lang.StringIndexOutOfBoundsException 异常。

String s = new String("ab");

char c = s.charAt(0)
// c = 'a'

char c2 = s.charAt(1)
// c2 = 'b'

4.5 codePointAt() 获取指定位置 ASCII 值

codePointAt() 方法是根据入参索引,获取指定位置的元素,并返回其 ASCII 值。如果入参索引超过字符串的索引位置,会抛出 java.lang.StringIndexOutOfBoundsException 异常。

String s = new String("ab");

int i = s.codePointAt(0);
// i = 97

int j = s.codePointAt(1);
// j = 98

4.6 getBytes() 获取字节数组

getBytes() 方法是将字符串转化为字节数组的方法。

String s = new String("ab");

byte[] bytes = s.getBytes();
// 结果为:[97, 98]

4.7 equals() 比较

equals() 方法是判断两个字符串是否相等。相等则返回 truetrue,否则为 falsefalse

String s = new String("ab");
String s2 = new String("ab");
String s3 = new String("ac");

boolean b = s.equals(s2);
// 结果为:true

boolean b2 = s.equals(s3);
// 结果为:false

4.8 equalsIgnoreCase() 忽略大小写比较

equalsIgnoreCase() 方法比较两个字符串是否相等,忽略其大小写。

String s = new String("ab");
String s2 = new String("Ab");
String s3 = new String("ac");

s.equalsIgnoreCase(s2);
// 结果为:true

s.equalsIgnoreCase(s3);
// 结果为:false

4.9 startsWith() 前缀判断

startsWith() 方法是判断字符串是否以某个前缀开头。

String s = new String("abedfgh");

boolean b = s.startsWith("ab");
// 结果为:true

boolean b2 = s.startsWith("abf");
// 结果为:false

4.10 endsWith() 后缀判断

endsWith() 方法是判断字符串是否以某个后缀结尾。

String s = new String("abedfgh");

boolean b = s.endsWith("gh");
// 结果为:true

boolean b2 = s.endsWith("bgh");
// 结果为:false

4.11 indexOf() 判断元素位置

indexOf() 方法是判断元素在字符串中的索引位置。如果字符串中有多个相同元素,则返回从左到右遇到的第一个相同的元素的位置。如果字符串中不存在该元素,则返回 1-1

String s = new String("abedfghab");

int posA = s.indexOf('a');
// posA = 0

int posH = s.indexOf('h');
// posH = 6

int posZ = s.indexOf('z');
// posZ = -1

int pos = s.indexOf("ab");
// pos = 0

int pos2 = s.indexOf("ab", 2);
// pos2 = 7

4.12 lastIndexOf() 判断元素结尾位置

lastIndexOf() 方法返回某个元素在字符串中最后出现的位置。

String s = new String("abedfghab");

int posA = s.lastIndexOf('a');
// posA = 7

int posH = s.lastIndexOf('h');
// posH = 6

int posE = s.lastIndexOf('e', 1);
// posE = -1

int pos = s.lastIndexOf("ab");
// pos = 7

4.13 substring() 截取字符串

substring() 方法是根据索引范围截取字符串。入参 fromIndexfromIndexendIndexendIndex,要在字符串的索引范围内,否则抛出 java.lang.StringIndexOutOfBoundsException 异常。

String s = new String("abc");

String sub = s.substring(0, 2);
// sub = "ab"

String sub2 = s.substring(0, 3);
// sub2 = "abc"

String sub3 = s.substring(1);
// sub3 = "bc"

4.14 split() 拆分

split() 方法是根据入参字符拆分字符串,返回一个字符串数组。

String s = new String("a-b-c-d-e");

String[] str = s.split("-");
// str = [a, b, c, d, e]

String[] str2 = s.split("-", 2);
// str2 = [a, b-c-d-e]

String[] str3 = s.split(",");
// str3 = [a-b-c-d-e]

4.15 replace() 替换

replace() 方法是替换字符串中的相应元素。

方法描述
public String replace(char oldChar, char newChar)将字符串中所有 oldChar 元素替换为 newChar 元素
public String replace(CharSequence target, CharSequence replacement)将字符串中字符串序列进行替换
public String replaceAll(String regex, String replacement)基于规则表达式进行替换
String s = new String("a1-b2-c-d-e");

String rep = s.replace('-', '.');
// rep = "a1.b2.c.d.e"

String rep2 = s.replace('.', 'o');
// rep2 = "a1-b2-c-d-e"

String rep3 = s.replaceAll("\d", "*");
// rep3 = "a*-b*-c-d-e"

4.16 toUpperCase() 转大写

toUpperCase() 方法是将字符串中的小写字母转为大写。

String s = new String("a1-b2-c-d-e");
String str = s.toUpperCase();
// str = "A1-B2-C-D-E"

4.17 toLowerCase() 转小写

toLowerCase() 方法是将字符串中的大写字母转为小写。

String s = new String("A1-B2-c-d-e");
String str = s.toLowerCase();
// str = "a1-b2-c-d-e"

4.18 toCharArray() 转数组

toCharArray() 方法是将字符串转为字符数组。

String s = new String("ab");
char[] chars = s.toCharArray();
// chars = ['a', 'b']

4.19 String.format() 创建格式化字符串

String.format() 方法用于创建格式化的字符串以及连接多个字符串对象。

转换符描述转换符描述
%s字符串类型%c字符类型
%d十进制整数%x十六进制整数
%o八进制整数%bBoolean 类型
%f浮点数%a十六进制浮点数
%e指数形式%h散列码
%%百分号%n换行
String format = String.format("%s\t%c\t%d\t%x\t%f", "st", 'b', 2, 0xFF, 2.6);
// format = "st	b	2	ff	2.600000"

4.20 String.valueOf() 转字符串

String.valueOf() 方法是将入参转为字符串。

String str = String.valueOf(22);
// str = "22"

String str2 = String.valueOf(2.2);
// str2 = "2.2"

String str3 = String.valueOf(new char[]{'a', 'b', 'c', 'd', 'e'}, 0, 2);
// str3 = "ab"

4.21 trim() 首尾去空格

trim() 方法是将字符串的开头位置和尾部位置的空格去掉。

String s = new String("  ab");
String s2 = new String("  abc     ");

String str = s.trim();
// str = "ab"
String str2 = s2.trim();
// str2 = "abc"

4.22 hashCode() 获取哈希值

hashCode() 方法是获取字符串的哈希值。

String s = new String("ab");
int hashCode = s.hashCode();
// hashCode = 3105