不变性

我们常常听人说,HashMap 的 key 建议使用不可变类,比如说 String 这种不可变类。
这里说的不可变指的是类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类。以下 demo 演示。

  1. @Test
  2. public void test12() {
  3. String s ="hello";
  4. s ="world";
  5. }

从代码上来看,s 的值好像被修改了,但从 debug 的日志来看,其实是 s 的内存地址已经被修改了,也就是说
s = "world" 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String, debug 的截图显示内存地址已经被修改
图片.png 图片.png


我们从源码上查看一下原因:

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. // 用于字符存储
  5. private final char value[];

通过 String 类的部分源码,我们可以看出来两点:

  • String 被 final 修饰,说明 String 类绝不可能被继承,也就是说任何对 String 的操作方法,都不会被继承覆写
  • String 中保存数据的是一个 char 的数组 value。value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就无法被修改

以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性。


如果你自定义类时,希望类也是不可变的,也可以模仿 String 的这两点操作。
因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String,如下面这种写法是不对的

  1. String str ="hello world !!";
  2. // 这种写法是替换不掉的,必须接受 replace 方法返回的参数才行,
  3. str.replace("l","dd");
  4. // str = str.replace("l","dd");

字符串乱码

在代码编写过程中,经常碰到这样的场景,进行二进制转化操作时,本地测试的都没有问题,但是到其它环境机器上时,有时会出现字符串乱码的情况,这个主要是因为:在二进制转化操作时,并没有强制规定文件编码,而不同的环境默认的文件编码不一致


以下 demo 用于模仿字符串乱码:

  1. @Test
  2. public void test12() throws UnsupportedEncodingException {
  3. String str = "nihao 你好 喬亂";
  4. // 字符串转化成 byte 数组
  5. byte[] bytes = str.getBytes("ISO-8859-1");
  6. // byte 数组转化成字符串
  7. String s2 = new String(bytes);
  8. // 输出结果:nihao ?? ??
  9. System.out.println(s2);
  10. }

这就是常见的乱码表现形式。
String s2 = new String(bytes,"ISO-8859-1"); 也出现乱码。
主要是因为:ISO-8859-1 这种编码对中文的支持有限,导致中文就会显示乱码。
唯一的解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytes() 和 new String() 两个方法都会使用到编码,把这两处的编码替换成 UTF-8 后,打印出的结果就正常了。

首字母大小写

如果我们的项目被 Spring 托管的话,有时候我们会通过 applicationContext.getBean(className); 这种方式得到 SpringBean,这时 className 必须是要满足首字母小写。
除了上面场景,在反射场景下面,也经常要使类属性的首字母小写,这时候我们一般都会这么做:
name.substring(0, 1).toLowerCase() + name.substring(1);
substring() 主要是为了截取字符串连续的一部分,该方法底层使用的是字符数组范围截取的方法:Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。
相反的,如果要修改成首字母大写同理。

相等判断

我们判断相等有两种办法,equals() 和 equalsIgnoreCase()。
近期看见一些面试题在问:如果让你写判断两个 String 相等的逻辑,应该如何写,我们来看下 equals() 的源码,整理下思路:

  1. public boolean equals(Object anObject) {
  2. // 判断内存地址是否相同
  3. if (this == anObject) {
  4. return true;
  5. }
  6. // 待比较的对象是否是 String,如果不是 String,直接返回不相等
  7. if (anObject instanceof String) {
  8. String anotherString = (String)anObject;
  9. int n = value.length;
  10. // 两个字符串的长度是否相等,不等则直接返回不相等
  11. if (n == anotherString.value.length) {
  12. char v1[] = value;
  13. char v2[] = anotherString.value;
  14. int i = 0;
  15. // 依次比较每个字符是否相等,若有一个不等,直接返回不相等
  16. while (n-- != 0) {
  17. if (v1[i] != v2[i])
  18. return false;
  19. i++;
  20. }
  21. return true;
  22. }
  23. }
  24. return false;
  25. }

从 equals 的源码可以看出,完全是根据 String 底层的结构来编写出相等的代码。
这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。

替换 & 删除

替换有 replace() 替换所有字符、replaceAll() 批量替换字符串、replaceFirst() 替换遇到的第一个字符串,这三种场景。
replace() 有两个方法,一个入参是 char,一个入参是 String。
前者表示:替换所有字符,如:name.replace('a','b')
后者表示:替换所有字符串,如:name.replace("a","b")
replaceAll() 和 replaceFirst() 所用的替换参数可以是普通字符串,也可以是正则表达式,参数列表如下
(String regex, String replacement)


我们想要删除某些字符,可以使用 replace(),把想删除的字符替换成 “” 即可。

拆分 & 合并

拆分使用 split(),该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分
以下 demo 用于演示 split() 的各种情况:

  1. @Test
  2. public void test12() {
  3. String s = "boo:and:foo";
  4. System.out.println(Arrays.toString(s.split(":", 2)));
  5. // 结果:["boo","and:foo"]
  6. System.out.println(Arrays.toString(s.split(":", 5)));
  7. // 结果:["boo","and","foo"]
  8. System.out.println(Arrays.toString(s.split(":", -2)));
  9. // 结果:["boo","and","foo"]
  10. System.out.println(Arrays.toString(s.split("o")));
  11. // 结果:["b","",":and:f"]
  12. System.out.println(Arrays.toString(s.split("o", 2)));
  13. // 结果:["b","o:and:foo"]
  14. String a = ",a,,b,";
  15. System.out.println(Arrays.toString(a.split(",")));
  16. //结果:["","a","","b"]
  17. }

从拆分结果可以看到,空值是拆分不掉的,如果我们想删除空值,只能自己拿到结果后再做操作,但Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以帮助我们快速去掉空值
以下 demo 用于演示 Guava 拆分:

  1. @Test
  2. public void test13() {
  3. String a = ",a, , b c ,";
  4. // Splitter 是 Guava 提供的 API
  5. List<String> list = Splitter.on(',')
  6. .trimResults()// 去掉空格
  7. .omitEmptyStrings()// 去掉空值
  8. .splitToList(a);
  9. System.out.println(list);
  10. // 打印结果为:["a", "b c"]
  11. }

合并我们使用 join(),此方法是静态的,方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,如果 join 的是一个 List,无法自动过滤掉 null 值
以下 demo 用于演示 List 存在 null 值时,String.join() 方法的打印:

  1. @Test
  2. public void test15() {
  3. List<String> strs = Arrays.asList("hell", null, "china");
  4. String joinStr = String.join(",", strs);
  5. System.out.println(joinStr);
  6. // 打印结果为:hell,null,china
  7. }

以下 demo 用于演示 Guava 合并:

  1. @Test
  2. public void test13() {
  3. // Joiner 是 Guava 提供的 API
  4. Joiner joiner = Joiner.on(",").skipNulls();
  5. String result = joiner.join("hello", null, "china");
  6. System.out.println(result);
  7. // 打印结果为:hello,china
  8. // Lists 是 Guava 提供的类
  9. List<String> list = Lists.newArrayList(new String[]{"hello", "china", null});
  10. System.out.println(joiner.join(list));
  11. // 打印结果为:hello,china
  12. }

常见问题

如何解决 String 乱码的问题

答:乱码的问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转化时字符集不匹配,所以在 String 乱码时我们可以这么做:

  • 所有可以指定字符集的地方强制指定字符集,比如 new String()getBytes() 这两个地方
  • 我们应该使用 UTF-8 这种能完整支持复杂汉字的字符集

    ISO-8859-1编码是单字节编码,向下兼容ASCII,不支持中文 单字节,即八位二进制数。