咱们经常听人说,HashMap 的 key 建议使用不可变类,好比说 String 这种不可变类。这里说的不可变指的是类值一旦被初始化,就不能再被改变了,若是被修改,将会是新的类,咱们写个 demo 来演示一下。java
String s ="hello"; s ="world";
从代码上来看,s 的值好像被修改了,但从 debug 的日志来看,实际上是 s 的内存地址已经被修改了,也就说 s =“world” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String,debug 的截图显示内存地址已经被修改,两张截图以下:面试
图片描述图片描述咱们从源码上查看一下缘由:数组
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; }
咱们能够看出来两点:缓存
String 被 final 修饰,说明 String 类毫不可能被继承了,也就是说任何对 String 的操做方法,都不会被继承覆写;数据结构
String 中保存数据的是一个 char 的数组 value。咱们发现 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对没法修改的,并且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出能够对 value 进行赋值的方法,因此说 value 一旦产生,内存地址就根本没法被修改。app
以上两点就是 String 不变性的缘由,充分利用了 final 关键字的特性,若是你自定义类时,但愿也是不可变的,也能够模仿 String 的这两点操做。工具
由于 String 具备不变性,因此 String 的大多数操做方法,都会返回新的 String,以下面这种写法是不对的:测试
String str ="hello world !!"; // 这种写法是替换不掉的 str.replace("l","dd"); // 必须接受 replace 方法返回的参数才行 str = str.replace("l","dd");
在生活中,咱们常常碰到这样的场景,进行二进制转化操做时,本地测试的都没有问题,到其它环境机器上时,有时会出现字符串乱码的状况,这个主要是由于在二进制转化操做时,并无强制规定文件编码,而不一样的环境默认的文件编码不一致致使的。this
咱们也写了一个 demo 来模仿一下字符串乱码:编码
String str ="nihao 你好 喬亂"; // 字符串转化成 byte 数组 byte[] bytes = str.getBytes("ISO-8859-1"); // byte 数组转化成字符串 String s2 = new String(bytes); log.info(s2); // 结果打印为: nihao ?? ??
打印的结果为??,这就是常见的乱码表现形式。这时候有同窗说,是否是我把代码修改为 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 方法,该方法主要是为了截取字符串连续的一部分,substring 有两个方法:
// beginIndex:开始位置,endIndex:结束位置; public String substring(int beginIndex, int endIndex) // beginIndex:开始位置,结束位置为文本末尾。 public String substring(int beginIndex)
substring 方法的底层使用的是字符数组范围截取的方法 :Arrays.copyOfRange(字符数组, 开始位置, 结束位置); 从字符数组中进行一段范围的拷贝。
相反的,若是要修改为首字母大写,只须要修改为 name.substring(0, 1).toUpperCase() + name.substring(1) 便可。
咱们判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写,近期看见一些面试题在问:若是让你写判断两个 String 相等的逻辑,应该如何写,咱们来一块儿看下 equals 的源码,整理一下思路:
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; }
从 equals 的源码能够看出,逻辑很是清晰,彻底是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给咱们:若是有人问如何判断二者是否相等时,咱们能够从二者的底层结构出发,这样能够迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组同样,判断相等时,就挨个比较 char 数组中的字符是否相等便可。
替换在工做中也常用,有 replace 替换全部字符、replaceAll 批量替换字符串、replaceFirst 替换遇到的第一个字符串三种场景。
其中在使用 replace 时须要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换全部字符,如:name.replace('a','b'),后者表示替换全部字符串,如:name.replace("a","b"),二者就是单引号和多引号的区别。
须要注意的是, replace 并不仅是替换一个,是替换全部匹配到的字符或字符串哦。
写了一个 demo 演示一下三种场景:
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 !!
固然咱们想要删除某些字符,也可使用 replace 方法,把想删除的字符替换成 “” 便可。
拆分咱们使用 split 方法,该方法有两个入参数。第一个参数是咱们拆分的标准字符,第二个参数是一个 int 值,叫 limit,来限制咱们须要拆分红几个元素。若是 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分,咱们演示一个 demo:
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"]
从演示的结果来看,limit 对拆分的结果,是具备限制做用的,还有就是拆分结果里面不会出现被拆分的字段。
那若是字符串里面有一些空值呢,拆分的结果以下:
String a =",a,,b,"; a.split(",") 结果:["","a","","b"]
从拆分结果中,咱们能够看到,空值是拆分不掉的,仍然成为结果数组的一员,若是咱们想删除空值,只能本身拿到结果后再作操做,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,能够帮助咱们快速去掉空值,以下:
String a =",a, , b c ,"; // Splitter 是 Guava 提供的 API List<String> list = Splitter.on(',') .trimResults()// 去掉空格 .omitEmptyStrings()// 去掉空值 .splitToList(a); log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list)); // 打印出的结果为: ["a","b c"]
从打印的结果中,能够看到去掉了空格和空值,这正是咱们工做中经常指望的结果,因此推荐使用 Guava 的 API 对字符串进行分割。
合并咱们使用 join 方法,此方法是静态的,咱们能够直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,在使用的时候,咱们发现有两个不太方便的地方:
而 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
从结果中,咱们能够看到 Guava 不只仅支持多个字符串的合并,还帮助咱们去掉了 List 中的空值,这就是咱们在工做中经常须要获得的结果。
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); } }
3.1 为何使用 Long 时,你们推荐多使用 valueOf 方法,少使用 parseLong 方法
答:由于 Long 自己有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,若是命中缓存,会减小资源的开销,parseLong 方法就没有这个机制。
3.2 如何解决 String 乱码的问题
答:乱码的问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转化时字符集不匹配,因此在 String 乱码时咱们能够这么作:
全部能够指定字符集的地方强制指定字符集,好比 new String 和 getBytes 这两个地方;
咱们应该使用 UTF-8 这种能完整支持复杂汉字的字符集。
3.3 为何你们都说 String 是不可变的
答:主要是由于 String 和保存数据的 char 数组,都被 final 关键字所修饰,因此是不可变的,具体细节描述能够参考上文。