String不可变性
String
的不变性指的是类值一旦被初始化,就不能被改变。我们从源码出发,可以看到String
类由final
修饰,即类不能被继承,String
中的方法不能被继承重写。String通过一个char
数组value
来保存数据,同样是final
修饰的,即value
数组一旦被赋值,内存地址无法修改。String
的不变性,充分利用了final
关键字的性质。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
}
String
被设计成不可变的主要目的是为了安全和高效。
第一,安全方面。String
中有些方法比较底层,如果被继承并重写方法会有隐患;同时不可变对象一定是线程安全的,可以在线程之间共享,无需同步。
第二,设计者希望用户用到的String
就是JDK中的String
,不希望被继承和重写,确保String
可控。
第三,性能方面。基于不变性,编译器和JVM
可以对String
进行优化,提高性能。
String
是不可变的,其hashcode
值也是固定不变的。String
通过hash来缓存当前字符串的hashcode
值,保证只需要计算一次。
private int hash; // Default to 0
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;
}
方法中首先对hash
属性值做了判断,如果计算过hashCode
,结果会被缓存到hash
属性,不需要重复计算。
replaceFirst()、replaceAll()、replace()三个方法的区别
public String replaceFirst(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
}
使用给定的 replacement
替换此字符串匹配给定的正则表达式的第一个子字符串。
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
使用给定的 replacement
替换此字符串所有匹配给定的正则表达式的子字符串。
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
将字符串中目标文本字符序列(非正则表达式)替换为指定字符序列。
替换从字符串的开头到结尾,例如,在字符串“aaa”
中将“aa”
替换为“b”
将导致“ba”
而不是“ab”
。
小结:
replace
和replaceAll
都是全部替换,replace
替换的只能是字符或字符串形式,而replaceAll
是基于正则表达式替换。replaceAll
和replaceFirst
()是基于正则表达式的替换,replaceFirst
仅替换首次出现。replace
的另一个方法参数类型是char
,对字符串中所有匹配字符做替换。replace(String)
方法还可以用于对字符串中字符的删除。
"Awecoder".replace("e","") // 值为Awcodr
扩展:对于替换,特别注意转移字符’’。
反斜杠'\'
即是Java语言的转移字符,也是正则表达式的转义字符。反斜杠\
,在正则表达式表示反斜杠用\\
,而Java代码需要用\\\
\来表示。
// 输出a\b
System.out.println("a/b".replaceAll("/","\\\\"));
System.out.println("a/b".replace("/","\\"));
System.out.println("a/b".replace('/', '\\'));
如何解决String乱码问题
出现乱码主要是因为字符集不支持复杂汉字、二进制转化时字符集不匹配等原因。在出现String
乱码时,我们通常会采取以下操作。
- 在可以指定字符集的地方强制指定字符集,例如
new String
和getBytes
。 - 使用UTF-8这种能完整支持复杂汉字的字符集。
示例:
String str = "字符串";
byte[] bytes = str.getBytes("ISO-8859-1"); // 字符串转化成byte数组
String s2 = new String(bytes); // byte数组转化成字符串
System.out.println(s2);
运行结果:
???
首字母大小写转换
首字母大小写我们通常采用下面的代码来实现。
String str = "Awecoder";
str.substring(0, 1).toLowerCase() + str.substring(1) // awecoder
底层源码分析
public String substring(int beginIndex, int endIndex) {
// 边界处理
int subLen = endIndex - beginIndex;
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
// 边界处理
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
substring()方法会创建一个新的字符串,底层采用了字符数组范围截取: Arrays.copyOfRange(value, offset, offset+count)
。
String的相等判断
在String
中通常使用equals
和equalsIgnoreCase
两个方法。其中,equalsIgnoreCase
方法忽略大小写比较,参数类型是String
。equals
方法是重写方法,其参数类型是Object
。在进行相等判断设计时,equals
方法更具代表性。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
equals
的源码设计逻辑是很清晰的,分为下面四个步骤。
- 判断内存地址是否相同
- 判断对象类型是否相同
- 判断数据长度是否相同
- 挨个比较每个数据元素是否相同
字符串拆分
字符串拆分采用的是split方法,底层split方法有两个参数,分别是分隔符和分隔的最大子字符串个数。
public String[] split(String regex, int limit)
实例:
String str = "awe|coder";
str.split("|"); // ["awe","coder"]
str.split("|", 1); // ["awecoder"]
str.split("|", 2); // ["awe","coder"]
如果分隔符之间存在空值,空值是拆分不掉的,作为空串成为结果数组一部分。借助外部工具例如Guava
,可以快速并且可靠地去除空格、空值等。
String str ="|awe||coder";
str.split("|") 结果:["","awe","","coder"]