字符串,顾名思义就是一串连续的字符,也就是一串连续的 char
C 语言中就是用 char
数组来表示字符串,
在java
的一切皆对象的世界中,将char
数组进行了封装,
用String
类型来表达字符串,点开 String
的源码,就会发现char
数组的身影。
// Java 8 源码 public final class string{ private final char value[]; }
char 数组被 final 关键字修饰,并且是私有成员变量,为什么 java 要这样设计呢?
这就要说到 String 的不可变性。
// Person 是可变的 Person p = new Person(18); p.setAge(20); // String 是不可变的 String s = "abc";
一个对象创建后,如果可以修改对象的属性,我们会说这个对象是可变的,反之则是不可变的。
为什么 String 不可变呢?
首先,它是被 fianl
修饰,代表它不可指向新数组,(但不代表数组本身的数据不能被改变)并且不能被继承。
真正不可变的原因是 它还被 private
修饰了,并且 String
提供和暴露任何修改字符数组的方法,都是返回新的 String
对象。
获取其底层字符数组时,都是获取一个新的数组进行返回,原数组,不会收到影响。
这样设计有什么好处呢?
首先,只有String
不可变了,字符串常量池才能发挥作用,
用字面量创建字符串时,字符串常量池会返回,已有对象的引用,
如果字符串可变,引用的值就可以被修改,这样还谈何复用呢?如何节省资源呢?
String
不可变,可以保证它的哈希码也不会被改变。
public final class String{ private int hash; // Default to 0 private int hash; public 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; } }
就可以将其缓存,再用到时,就不用在计算了。
也正是因为 哈希码 不会变,可以放心的去使用,和哈希计算相关的对象
比如 hashMap 、HashSet,如果 哈希码会改变,那就会影响这些对象的哈希计算,从而导致预期之外的结果。
比如你之前明明存储了一个String 对象。到后面你会发现找不到了。
最后,不可变对象都是线程安全的。
不必担心,当前线程使用的对象,会被其它线程修改。
正是因为String
不可变,所以导致添加新对象时,会创建一个新对象,从而导致性能低下。
for(int i = 0;i<9999;++i){ string+=i; // 大量创建新对象 } System.out.print(string);
为了解决这一问题,java
提供了StringBuilder
这个可变的字符串类型,我们简称它 为 sb
,小 sb
继承了一个 老 sb (AbstractStringBuilder)
其底层和String
一样都是用的 char 数组来表示字符串,不过老sb
的 char
数组,没有加 private final
修饰符,代表这个数组可被访问且可变。
abstract class AbstractStringBuilder{ char[] value; } public final class StringBuilder extends AbstractStringBuilder{ }
而且 sb
父子俩还提供了许多方法供我们修改字符串,比如append
方法就可以原字符串后面添加字符,这些方法修改的都是自身数据,返回的 sb
对象,
也就是它自己本身,不像String
一样返回的都是新对象。
这样一来我们频繁修改字符串就方便了许多。
所以在拼接字符串的时候,谨慎使用 String
,优先选择 sb
sb
的缺点:它是一个可变对象,那它就是线程不安全的。
为了解决这个问题 java
还推出了 sb
的兄弟,StringBuffer
,
它和 sb 不一样的是,它内部使用了 synchronized 关键字,修饰了字符串操作方法,从而保证了线程安全。
有得必有舍,正是由于它每次操作字符串时都会加锁,这回导致它性能没有 sb 高,但是比String 还是要高的。
类型 | 特点 | 适用场景 |
---|---|---|
String | 不可变,线程安全 | 操作少量数据,或不需要操作数据 |
StringBuilder | 可变,线程不安全 | 需要频繁操作数据,但是不用考虑线程安全 |
StringBuffer | 可变,线程安全,性能较低 | 需要频繁操作数据,要考虑线程安全 |
StringJoiner
拼接字符串
String[] names = {"A","B","C","D"}; StringJoiner sj = new StringJoiner(",","[","]"); // 设置分隔符,要想设置开头和结尾,在构造方法后面添加两个参数即可 for(String name:names){ sj.add(name); } System.out.println(sj); // [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(","));