1.1 String类,底层实现
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
...
}
可以看出来两点:
- final 修饰的类,不能被继承,也就是说任何对 String 的操作方法,都不会被继承覆写;
- String 中保存数据的是一个 char 的数组,是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行set赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改。
以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性,如果你自定义类时,希望也是不可变的,也可以模仿 String 的这两点操作。
1.2 字符串乱码
由于在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致导致了中文乱码问题。
如:
String str ="nihao 你好 喬亂";
// 字符串转化成 byte 数组
byte[] bytes = str.getBytes("ISO-8859-1");
// byte 数组转化成字符串
String s2 = new String(bytes,"ISO-8859-1");
log.info(s2);
// 结果打印为: nihao ?? ??
对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8 后,打印出的结果就正常了
1.3 首字母大小写
在反射场景下面,我们也经常要使类属性的首字母小写,这时候我们一般都会这么做:
name.substring(0, 1).toLowerCase() + name.substring(1);
substring 有两个方法:
- public String substring(int beginIndex, int endIndex) beginIndex:开始位置,endIndex:
结束位置; - public String substring(int beginIndex) beginIndex:开始位置,结束位置为文本末尾。
- substring 方法的底层使用的是字符数组范围截取的方法 : Arrays.copyOfRange(字符数组, 开始 位置, 结束位置); 从字符数组中进行一段范围的拷贝。
1.4 String中相等判断equal
equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写, 近期看见一些面试题在问:如果让你写判断两个 String 相等的逻辑,应该如何写
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 待比较的对象是否是 String,如果不是 String,直接返回不相等
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;
}
比较两个对象是否相同的一种思路:我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。
1.5 替换、删除
replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:name.replace('a','b') ,后者表示替换所有字符串,如: name.replace("a","b") ,两者就是单引号和多引号的区别。
public void testReplace(){
String str ="hello word !!";
log.info("替换之前 :{}",str);
str = str.replace('l','d');
log.info("替换所有字符 :{}",str);
str = str.replaceAll("d","l");
log.info("替换全部 :{}",str);
str = str.replaceFirst("l","");
log.info("替换第一个 l :{}",str); }
//输出的结果是:
替换之前 :hello word !!
替换所有字符 :heddo word !!
替换全部 :hello worl !!
替换第一个 :helo worl !!
1.6 拆分和合并
拆分我们使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参 数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。
String s ="boo:and:foo";
// 我们对 s 进行了各种拆分,演示的代码和结果是:
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"]
但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:
List<String> list = Splitter.on(',')
.trimResults()// 去掉空格
.omitEmptyStrings()// 去掉空值
.splitToList(a);
log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list));
// 打印出的结果为: ["a","b c"]
1.7 组合使用String.join()方法
String result = String.join("-",“a”,“b”,“c”,“d”);
输出结果如下:a-b-c-d
也可使用如下方式:
String[] arr = {“a”,“b”,“c”,“d”};
String result = String.join("-",arr);
输出结果如下:a-b -c-d
如果 join 的是一个 List,无法自动过滤掉 null 值。
而 Guava 正好提供了 API,解决上述问题,我们来演示一下:
// 依次 join 多个字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
log.info("依次 join 多个字符串:{}",result);
List<String> list = Lists.newArrayList(new String[]{"hello","china",null});
log.info("自动删除 list 中空值:{}",joiner.join(list));
// 输出的结果为;
依次 join 多个字符串:hello,china
自动删除 list 中空值:hello,china
1.8 Long的缓存
Long 最被我们关注的就是 Long 的缓存问题,Long 自己实现了一种缓存机制,缓存了从 -128 到 127 内的所有 Long 值,如果是这个范围内的 Long 值,就不会初始化,而是从缓存中拿,缓存初始化源码如下:
private static class LongCache {
private LongCache(){}
// 缓存,范围从 -128 到 127,+1 是因为有个 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
// 容器初始化时,进行加载
static {
// 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
1.9 为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法
答:因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。
2.0 如何解决 String 乱码的问题
答:乱码的问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转化时字符集不匹配,
所以在 String 乱码时我们可以这么做:
- 所有可以指定字符集的地方强制指定字符集,比如 new String 和 getBytes 这两个地方;
- 我们应该使用 UTF-8 这种能完整支持复杂汉字的字符集。
2.1 为什么大家都说 String 是不可变的
答:主要是因为 String 和保存数据的 char 数组,都被 final 关键字所修饰,所以是不可变的,具体细节描述可以参考上文。