文章目录
- String 和 StringBuilder
- 3.1 String基本用法
- 3.2 String底层
- 3.3 不可变性
- 3.4 编码转换
- 3.5 StringBuilder基本用法
- 3.6 StringBuilder基本实现原理
- 3.7 String的+和+=运算符
String 和 StringBuilder
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
3.1 String基本用法
可以通过常量定义String
变量,也可以通过new
创建String
变量,还可以直接使用 + + +和 + = += +=运算符,如:
@Testpublic void testNewString() {String newConstant = "constant string";String newConstructor = new String("constructor new string");String addString = newConstant + " " + newConstructor;assertTrue("constant string constructor new string".equals(addString));}
String
类包括很多方法,以方便操作字符串,比如:
public boolean isEmpty() // 判断字符串是否为空
public int length() // 获取字符串长度
public String substring(int beginIndex) // 取子字符串
public String substring(int beginIndex, int endIndex) // 取子字符串
public int indexOf(int ch) // 查找字符,返回第一个找到的索引位置,没找到返回 -1
public int indexOf(String str) // 查找子串,返回第一个找到的索引位置,没找到返回 -1
public int lastIndexOf(int ch) // 从后面查找字符
public int lastIndexOf(String str) // 从后面查找子字符串
public boolean contains(CharSequence s) // 判断字符串中是否包含指定的字符序列
public boolean startsWith(String prefix) // 判断字符串是否以给定子字符串开头
public boolean endsWith(String suffix) // 判断字符串是否以给定子字符串结尾
public boolean equals(Object anObject) // 与其他字符串比较,看内容是否相同
public boolean equalsIgnoreCase(String anotherString) // 忽略大小写比较是否相同
public int compareTo(String anotherString) // 比较字符串大小
public int compareToIgnoreCase(String str) // 忽略大小写比较
public String toUpperCase() // 所有字符转换为大写字符,返回新字符串,原字符串不变
public String toLowerCase() // 所有字符转换为小写字符,返回新字符串,原字符串不变
public String concat(String str) // 字符串连接,返回当前字符串和参数字符串合并结果
public String replace(char oldChar, char newChar) // 字符串替换,替换单个字符
public String replace(CharSequence target, CharSequence replacement) // 字符串替换,替换字符序列,返回新字符串,原字符串不变
public String trim() // 删掉开头和结尾的空格,返回新字符串,原字符串不变
public String[] split(String regex) // 分隔字符串,返回分隔后的子字符串数组
3.2 String底层
String
类内部用一个字符数组表示字符串,实例变量定义为:
private final char value[];
String
中的大部分方法内部也都是操作的这个字符数组。比如:
length()
方法返回的是这个数组的长度。substring()
方法是根据参数,调用构造方法String(char value[], int offset, int count)
新建了一个字符串。indexOf()
方法查找字符或子字符串时是在这个数组中进行查找。
String
中还有一些方法,与这个char
数组有关:
public char charAt(int index) // 返回指定索引位置的 char
public char[] toCharArray() // 返回字符串对应的 char 数组, 注意,返回的是一个复制后的数组,而不是原数组
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) // 将 char 数组中指定范围的字符复制入目标数组指定位置
与Character
类似,String
也提供了一些方法,按代码点对字符串进行处理:
public int codePointAt(int index)
public int codePointBefore(int index)
public int codePointCount(int beginIndex, int endIndex)
public int offsetByCodePoints(int index, int codePointOffset)
3.3 不可变性
与包装类类似,String
类也是不可变类,即对象一旦创建,就没有办法修改了。String
类也声明为了final
,不能被继承,内部char
数组value
也是final
的,初始化后就不能再变了。String
类中提供了很多看似修改的方法,其实是通过创建新的String
对象来实现的,原来的String
对象不会被修改。比如,concat()
方法的代码:
public String concat(String str) {int otherLen = str.length();if(otherLen == 0) {return this;}int len = value.length;char buf[] = Arrays.copyOf(value, len + otherLen);str.getChars(buf, len);return new String(buf, true);}
通过Arrays.copyOf
方法创建了一块新的字符数组,复制原内容,然后通过new
创建了一个新的String
,最后一行调用的是String
的另一个构造方法。
因为字符串的不可变性,JVM
专门为字符串提供了一个常量池,凡是放在常量池中的字符串对象都可以共享,查看下面的代码:
@Testpublic void testConstantPool() {String name1 = "测试字符串常量池";String name2 = "测试字符串常量池";assertTrue(name1 == name2);}@Testpublic void testNotConstantPool() {String name1 = new String("测试字符串常量池");String name2 = new String("测试字符串常量池");assertFalse(name1 == name2);}
看上面代码,通过常量定义name1 == name2
得到的是true
,但是通过new
得到的字符串得到的结果是false
。那么,现在有一个问题,哪些方式是使用常量池中的数据呢,哪些是新建放在堆中的呢?
- 直接使用
"..."
得到的字符串对象放在常量池。 - 直接
"..."+"..."
拼接的字符串对象放在常量池。 - 两个指向
"..."
的final常量拼接结果放在常量池。 - 所有字符串对象
.intern()
方法得到的结果放在常量池。 - 除以上四种方式,其他方式得到的字符串结果都在堆中。
@Testpublic void testCreateConstantPool() {String s1 = "helloworld"; // 1、常量池中String s2 = "hello" + "world"; // 2、常量池中final String s3 = "hello";final String s4 = "world";String s5 = s3 + s4; // 3、指向"..."的 final 常量 + 指向"..."的 final 常量在常量池String s6 = new String("hello");String s7 = new String("world");String s8 = s6 + s7;String s9 = s8.intern();// 4、字符串对象.intern() 的结果都在常量池assertTrue(s1 == s2 && s1 == s5 && s1 == s9);String s10 = "hello";String s11 = "world";String s12 = s10 + s11;assertFalse(s1 == s12);String s13 = s10 + "world";assertFalse(s1 == s13);String s14 = s6 + "world";assertFalse(s1 == s14);String s15 = String.valueOf(new char[] { 'h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd' });assertFalse(s1 == s15);String s16 = "hello".concat("world");assertFalse(s1 == s16);}
3.4 编码转换
String
内部是按UTF-16BE
处理字符的,对BMP
字符,使用一个char
,两个字节,对于增补字符,使用两个char
,四个字节。我们知道有各种编码,不同编码可能用于不同的字符集,使用不同的字节数目,以及不同的二进制表示。如何处理这些不同的编码呢?这些编码与Java
内部表示之间如何相互转换呢?Java
使用Charset
类表示各种编码,它有两个常用静态方法:
public static Charset defaultCharset()
public static Charset forName(String charsetName)
第一个方法返回系统的默认编码,第二个方法返回给定编码名称的Charset
对象,其charset
名称可以是US-ASCII
、ISO-8859-1
、windows-1252
、GB2312
、GBK
、GB18030
、Big5
、UTF-8
等,比如:
Charset charset = Charset.forName("GB18030");
String
类提供了如下方法,返回字符串按给定编码的字节表示:
public byte[] getBytes()
public byte[] getBytes(String charsetName)
public byte[] getBytes(Charset charset)
第一个方法没有编码参数,使用系统默认编码;第二个方法参数为编码名称;第三个方法参数为Charset
。
String
类有如下构造方法,可以根据字节和编码创建字符串,也就是说,根据给定编码的字节表示,创建Java
的内部表示。
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], Charset charset)
3.5 StringBuilder基本用法
如果字符串修改操作比较频繁,应该采用StringBuilder
和StringBuffer
类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于StringBuffer
类是线程安全的,而StringBuilder
类不是。
StringBuilder
的基本用法很简单。使用new
创建StringBuilder
对象,通过append
方法添加字符串,然后通过toString
方法获取构建后的字符串:
@Testpublic void testStringBuilder() {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("hello");stringBuilder.append("world");assertFalse("helloworld" == stringBuilder.toString());assertTrue("helloworld".equals(stringBuilder.toString()));}
3.6 StringBuilder基本实现原理
StringBuilder
类是怎么实现的呢?我们来看下它的内部组成,以及一些主要方法的实现,代码基于Java 7
。与String
类似,StringBuilder
类也封装了一个字符数组,定义如下:
char[] value;
与String
不同,它不是final
的,可以修改。另外,与String
不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
/*** The count is the number of characters used.*/
int count;
StringBuilder
继承自AbstractStringBuilder
,它的默认构造方法是:
public StringBuilder() {super(16);}
调用父类的构造方法,父类对应的构造方法是:
AbstractStringBuilder(int capacity) {value = new char[capacity];
}
也就是说,new StringBuilder()
代码内部会创建一个长度为 16 16 16的字符数组,count
的默认值为 0 0 0。来看append
方法的代码:
public AbstractStringBuilder append(String str) {if(str == null) str = "null";int len = str.length();ensureCapacityInternal(count + len);str.getChars(0, len, value, count);count += len;return this;
}
append
会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count
体现。具体来说,ensureCapacityInternal(count+len)
会确保数组的长度足以容纳新添加的字符,str.getChars
会复制新添加的字符到字符数组中,count+=len
会增加实际使用的长度。
ensureCapacityInternal
的代码如下:
private void ensureCapacityInternal(int minimumCapacity) {//overflow-conscious codeif(minimumCapacity - value.length > 0)expandCapacity(minimumCapacity);
}
如果字符数组的长度小于需要的长度,则调用expandCapacity
进行扩展,其代码为:
void expandCapacity(int minimumCapacity) {int newCapacity = value.length * 2 + 2;if(newCapacity - minimumCapacity < 0)newCapacity = minimumCapacity;if(newCapacity < 0) {if (minimumCapacity < 0) //overflowthrow new OutOfMemoryError();newCapacity = Integer.MAX_VALUE;}value = Arrays.copyOf(value, newCapacity);
}
扩展的逻辑是:分配一个足够长度的新数组,然后将原内容复制到这个新数组中,最后让内部的字符数组指向这个新数组。这里主要看下newCapacity
是怎么算出来的。参数minimumCapacity
表示需要的最小长度,需要多少分配多少不就行了吗?不行,因为那就跟String
一样了,每append
一次,都会进行一次内存分配,效率低下。这里的扩展策略是跟当前长度相关的,当前长度乘以 2 2 2,再加上 2 2 2,如果这个长度不够最小需要的长度,才用minimumCapacity
。比如,默认长度为 16 16 16,长度不够时,会先扩展到 16 ∗ 2 + 2 16*2+2 16∗2+2即 34 34 34,然后扩展到 34 ∗ 2 + 2 34*2+2 34∗2+2即 70 70 70,然后是 70 ∗ 2 + 2 70*2+2 70∗2+2即 142 142 142,这是一种指数扩展策略。为什么要加 2 2 2?这样,在原长度为 0 0 0时也可以一样工作。
除了append
和toString
方法, StringBuilder
还有很多其他方法,包括更多构造方法、更多append
方法、插入、删除、替换、翻转、长度有关的方法。
3.7 String的+和+=运算符
Java
中,String
可以直接使用 + + +和 + = += +=运算符,这是Java
编译器提供的支持,背后,Java
编译器一般会生成StringBuilder
,+
和+=
操作会转换为append
。比如,如下代码:
String hello = "hello";
for(int i=0; i<3; i++){hello+=", world";
}
System.out.println(hello);
背后,Java编译器一般会转换为:
String hello = "hello";
for(int i=0; i<3; i++){StringBuilder sb = new StringBuilder(hello);sb.append(", world");hello = sb.toString();
}
System.out.println(hello);
既然直接使用 + + +和 + = += +=就相当于使用StringBuilder
和append
,那还有什么必要直接使用StringBuilder
呢?在简单的情况下,确实没必要。不过,在稍微复杂的情况下,Java
编译器可能没有那么智能,它可能会生成过多的StringBuilder
,尤其是在有循环的情况下,在循环内部,每一次 + = += +=操作,都会生成一个StringBuilder
。所以,对于简单的情况,可以直接使用String
的 + + +和 + = += +=,对于复杂的情况,尤其是有循环的时候,应该直接使用StringBuilder
。
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎