字符串,就是一串连续的字符。在C语言中就是使用char的数组来表示字符串。
char string[] = {"D","i","o","x","i","d","e","\0"};
char string[] = "Dioxide";
而在 Java 中,为遵循 一切皆对象 的概念,将 char
数组进行了一次封装,进而用 String
类型来表达字符串。
//Java8源码
public final class String{
private final char value[];
}
在源码中 value[]
被 final
关键字修饰且为 private
私有成员变量,其设计原理即为 String
的不可变性。
一个对象创建后,如果可以修改其对象属性,则说明这个对象是可变的,反之则是不可变的。
//Person是可变的
Person p = new Person(18);
p.setAge = 20;
//String是不可变的
String s = "Dioxide.CN";
s = "Dioxide";
System.out.println(s);
对象的不可变性,其实就是指对象本身的属性、数据不会发生改变。将 String
变量重新赋值 不等同于 改变 String
对象本身的属性。而是创建了一个新的 String
对象,将新对象的 引用 赋值给了 String
对象,之前的 String
对象是不会受到影响的。
String
的不可变性不仅是因为其被 final
关键字修饰,最根本的原因是被 private
权限修饰符所修饰。被 final
修饰只能代表它不能指向新的数组,不代表数组本身的数据不会被修改。
被 private
修饰的 String
并没有暴露和提供任何修改字符数组的方法。很多字符串操作都是返回的新的 String
对象,绝对不会影响原数据。
获取其底层字符数组时都是复制一个新的字符数组进行返回,原数组也不会收到影响。
//Java8源码
public final class String {
private final char value[];
public char[] toCharArray() {
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
并且,String
还被 final
修饰为不可继承类,从而杜绝了子类覆盖父类的可能。
Error: Parse error on line 1:
flowchart LR stack([堆]) -.- pool(
--------------^
Expecting 'NEWLINE', got 'ALPHA'
首先,只有 String
不可变了,字符串常量池才能发挥作用。
用字面量创建字符串时,字符串常量池会返回已有对象的引用。如果字符串可变,那引用的值就可以随时修改 ,并能随时影响到其他的引用,从而数据会发生各种错误,这样就会导致常量池不存在复用性。
String s1 = "str";
String s2 = "str";
System.out.println(s1 == s2); //true
//如果String会改变,那么s1改变时s2也会跟着改变
System.out.println(s2);
String
不可变可以保证其哈希码也不可变,因此计算一次哈希码后即可将其储存,再用到时就无需计算哈希码了,性能更高。
//Java8源码
public final class String{
private final char value[];
//默认值为0
private int hash;
private int hashCode() {
int h = hash;
if(h == 0 && value.length > 0) {
char val[] = value;
for(int i = 0; i < value.length; i++) {
h = 31 * h + val[i]
}
//计算一次后可将哈希码储存
hash = h;
}
return h;
}
}
得益于 String
的哈希码不会变,所以能够放心地使用和哈希计算相关的对象(如:HashMap、HashSet)。
String s = "Hello World"
HashSet<String> set = new HashSet<>();
set.add(s);
//假设可变,则此时set中的"Hello World"就找不到了
s.value = "Dioxide_CN";
如果 String
的哈希码会改变则会影响到这些对象的哈希计算,从而导致预期之外的效果。
最后一个最重要的原因就是,不可变对象都是 线程安全 的,即当前线程使用的对象不会被其他线程修改。
弊端: String
对象频繁拼接时会产生大量新的 String
对象。
String s = "报数:";
for(int i = 0; i < 10; i++) {
s = s + " " + i; //大量创建新的String对象
}
//报数: 0 1 2 3 4 5 6 7 8 9
System.out.println(s);
为此 Java 推出了 StringBuilder
可变的字符串类型:
//抽象类
abstract class AbstractStringBuilder {
//AbstractStringBuilder底层与String类似
char[] value;
}
public final class StringBuilder extends AbstractStringBuilder {
}
新的 StringBuilder
和老的 AbstractStringBuilder
都提供了许多方法来修改字符串。但是其修改的数据都是本身,返回的数据也是一个自身 StringBuilder
对象,这样是为了后续更好地链式调用方法。如:
str.appen("1").append("23").append("456");
其实使用 String 拼接字符串时,其底层会自动创建 StringBuilder 对象并调用其 append()
方法完成操作。
所以频繁操作 String 对象时,应当优先使用 StringBuilder 。
StringBuilder 是一个可变对象,那么其自身自然是 线程不安全的 。
为了解决 StringBuilder 的线程不安全的问题,Java 推出了 StringBuffer 来解决线程问题。
同样的 StringBuffer 也继承了 AbstractStringBuilder 所以同样也能修改字符串。
而其与 StringBuilder 的不同点在于,StringBuffer 的方法中都使用了 synchronized
关键字来保障线程安全
public final class StringBuffer extends AbstractStringBuilder {
@Override
public synchronized StringBuffer append(String str) {
//逻辑段
return this;
}
@Override
public synchronized StringBuffer insert(int offset, String str) {
//逻辑段
return this;
}
//其他方法
}
正因为 StringBuffer 每次操作 String 时都会加锁,从而导致了它的性能低于 StringBuilder。
类型 | 特点 | 适用场景 |
---|---|---|
String | 不可变,线程安全 | 操作少量数据或不需要操作数据 |
StringBuilder | 可变,线程不安全 | 需要频繁操作数据且不考虑线程安全 |
StringBuffer | 可变,线程安全,性能低 | 需要频繁操作数据且考虑线程安全 |
String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder();
for(String name : names) {
sb.append(name).append(", ");
}
System.out.println(sb); // A, B, C, D,
上述案例的需求为:需要将数据集进行不断拼接。
但是使用 StringBuilder 进行拼接所带来的问题就是在最终拼接出来的结果末尾会多出一个 ", "
为了解决这一问题,可以在拼接完成的最后删除多余的字符,或者在拼接时进行边界判断:
String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder();
for(String name : names) {
sb.append(name).append(", ");
}
sb.delete(sb.length() - 2, sb.length()); //删除
System.out.println(sb); // A, B, C, D,
若字符串同时又需要在开头和结尾添加括号,则还需要在开头结尾再进行一次拼接:
String[] names = {"A","B","C","D"};
StringBuilder sb = new StringBuilder("["); //头
for(int i = 0; i < names.length; i++) {
sb.append(name[i]);
if(i != names.length - 1) {
sb.append(", ");
}
}
sb.appen("]"); //尾
System.out.println(sb); // A, B, C, D,
这进一步导致了代码的冗余程度。为此,Java 8 推出了 StringJoiner
来简化这些操作:
String[] names = {"A","B","C","D"};
StringJoiner sjr = new StringJoiner(", ");
for(String name : names) {
sjr.add(name);
}
System.out.println(sb); // A, B, C, D
在构造 StringJoiner 时需要传入分隔符,并在遍历过程中将 String 加入 StringJoiner 对象中。
同样的,可在实例化对象时,指定开头和结尾:
String[] names = {"A","B","C","D"};
StringJoiner sjr = new StringJoiner(", ","[","]");
for(String name : names) {
sjr.add(name);
}
System.out.println(sb); // [A, B, C, D]
在 Java 的标准库中,同样也用到了 StringJoiner,比如 String 对象的静态方法 join()
,以及 Stream 流中常用的 joining()
操作:
String[] names = {"A","B","C","D"};
String.join(", ", names);
Arrays.stream(names).collect(Collectors.joining(", "));