1.不变性
概念
HashMap的键值在使用中建议使用不可变类,如String。**“不可变”**指的是类对象的值一旦被初始化就不能再被改变。假设某个变量指向一个不可变类对象的引用,如果变量引用的值被改变,则会指向一个新的类对象。
String s = "hello";
s = "world";
上面的代码中,显式的变换是变量s引用的值由"hello"改变为"world",仅仅是值上的改变。实际上,底层隐式的变换是变量s的引用从最开始的String对象"hello"转变为当前新的String对象"world"。即,变量s引用的内存地址被修改了。如果在IDE中开启DEBUG,也会看到变量s引用的内存地址是不同的。
源码
下方是String的源码,
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
......
}
String对象的数据存储在名为value的char数组中,从源码来看,String类的不变性可以体现在下面两点上,
- String类被final修饰,说明String类不能被继承,任何String类的成员方法都不可能被复写。
- 保存数据的value数组被final修饰,即value变量一旦被赋值,其内存地址无法被修改。且value变量属于私有变量,String类也没有提供对其进行修改的方法,故String类对象的数据一经初始化就无法改变。
这两点是在自定义不可变类时可以借鉴的。
因为String具有不可变性,所以对某个String对象的大部分操作方法返回的都是新的String类对象,
String str = "hello world";
str.replace("l", "dd");
第二行代码是无效代码,对String的操作返回的都是新的String类对象。而replace方法返回的对象并没有被引用,所以该操作是无效的,正确的方式是str变量显式的指向replace变量返回的String对象的引用,
str = str.replace("l", "dd");
2.字符串乱码
String类对象进行二进制转化操作时有时候会出现字符串乱码的情况,出现这种情况的原因是在二进制转化时没有强制规定文件编码,而不同的环境默认的文件编码可能是不一致的。
String str = "nihao 你好";
// 将字符串转化为byte数组
byte[] bytes = str.getBytes("ISO-8859-1");
// 再将bytes数组转化成字符串
String s2 = new String(bytes);
System.out.println(s2);
打印结果,
nihao ??
即使对上面的代码进行如下修改,在二进制转化时指定文件格式,
String s2 = new String(bytes, "ISO-8859-1");
进行这样的修改后依旧显示的是乱码,并不是这种方式本身有什么错误,而是ISO-8859-1对中文的支持有限,导致中文会显示出乱码。唯一的解决方法是在所有需要用到编码的地方统一使用UTF-8编码。对于String而言,在getBytes和new String两个方法都会使用到编码,把这两处编码指定为UTF-8时,就不会出现乱码的现象。
3.首字母大小写
String类对象存在两个方法toUpperCase和toLowerCase方法,分别是将字符串全部字符改为大写或小写。当只想对首字母进行操作,
String name = "....";
// 首字母小写
name = name.subString(0, 1).toLowerCase() + name.subString(1);
// 首字母大写
name = name.subString(0, 1).toUpperCase() + name.subString(1);
使用subString方法为了截取字符串连续的一部分,subString方法的两种调用形式,
// 形式1
public String substring(int beginIndex) {
if (beginIndex < 0) { // beginIndex取不合理的负数值
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) { // beginIndex比char数组长
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
// 形式2
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
subString方法在最后一行生成新的String对象,String类的该构造方法中调用了如下一行代码,
this.value = Arrays.copyOfRange(value, offset, offset+count);
即从字符数组中选取一段范围进行拷贝。
4.判断字符串相等
判断字符串相等的两个方法,equals和equalsIgnoreCase方法。后者判断相等时会忽略大小写,对于判断相等的方法,面试过程中可能会问到判断所用的逻辑,可参考String的equals方法,
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 对象类型是否相同,如果不同直接返回false
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;
// 一次比较String对象value数组中的字符
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
equals源码完全依据String底层结构编写,在实际开发中可以参考这种形式。
备注:equals方法中判断 n == anotherString.value.length,虽然value是私有的,但是private修饰符是类级别的,而非对象级别的。所以同一个类的不同对象在类的作用域中是可以相互访问彼此的私有属性的。
5.替换、删除
String类的替换方法有replacce、replaceAll和replaceFirst,使用上稍有区别,
@test
public void testReplace() {
String str = "hello world";
System.out.println(str);
str = str.replace('l', 'd');
System.out.println(str);
str = str.replaceAll("d", "l");
System.out.println(str);
str = str.replaceFirst("l", "");
System.out.println(str);
}
运行结果如下,
hello world
heddo wordd
hello worll
helo worll
需要明确几种调用形式,
- replace方法
调用形式 | 说明 |
---|---|
public String replace(char oldChar, char newChar) | 字符串中替换某个字符(替换全部) |
public String replace(CharSequence target, CharSequence replacement) | 字符串替换(替换全部) |
- replaceAll方法
调用形式 | 说明 |
---|---|
public String replaceAll(String regex, String replacement) | 接收正则表达式字符串,也可接收普通字符串(可以视为replace方法升级版,同样替换全部) |
- replaceFirst方法
调用形式 | 说明 |
---|---|
public String replaceFirst(String regex, String replacement) | 替换出现的第一个符合要求的字符串,接收正则表达式字符串,也可接收普通字符串 |
6.拆分和合并
拆分
String对象的split方法对字符串进行拆分,返回String数组。
两种调用形式,
调用形式 | 说明 |
---|---|
public String[] split(String regex) | 只传入拆分形式,可以是正则表达式字符串,也可以是普通字符串。将字符串尽可能拆分。 |
public String[] split(String regex, int limit) | 可以是正则表达式字符串,也可以是普通字符串。将字符串拆分成limit个元素。 |
String s = "boo:and:foo";
s.split(":"); // 结果: ["boo", "and", "foo"]
s.split(":", 2); // 结果: ["boo", "and:foo"]
s.split(":", 5); // 结果: ["boo", "and", "foo"]
s.split(":", -2); // 结果: ["boo", "and", "foo"]
s.split("o"); // 结果: ["b", "", ":and:f"]
s.split("o", 2); // 结果: ["b", "o:and:foo"]
如果字符换拆分后会出现空字符串,
String a = ",a,,b,";
a.split(","); // 结果:["", "a", "", "b"]
split方法默认会去掉数组尾部的所有空字符串,而头部和中间产生的空字符串是不会被去掉的。
拼接
String类中提供join方法用于拼装字符串,
调用形式 | 说明 |
---|---|
public static String join(CharSequence delimiter, CharSequence… elements) | 数组对象 |
public static String join(CharSequence delimiter,Iterable<? extends CharSequence> elements) | List对象 |
使用方面存在两个不方便之处,
- 无法连续拼接,String.join(",", s).join(",", s1),最终只能得到s1的拼接结果,而s的拼接结果会被覆盖。
- 如果传入的是List对象,无法自动将null值滤掉。
知识点
-
如何解决String乱码问题
乱码问题的起因主要由两个,字符集不支持复杂汉子、二进制进行转化时字符集不匹配,为避免乱码,可以进行如下操作,
1 所有可以指定字符集的地方强制指定字符集,如调用new String和getBytes方法的代码中
2 应该使用UTF-8编码能够完整支持复杂汉子 -
为什么String是不可变的
String类和保存字符串数据的char数组都是使用final关键字修饰的,所以是不可变的,具体的细节见第一小节。 -
String的一些常用操作问题,如何分割、合并、替换、截取等,具体参考上面的方法说明