咱们经常听人说,HashMap 的 key 建议使用不可变类,好比说 String 这种不可变类。这里说的不可变指的是类值一旦被初始化,就不能再被改变了,若是被修改,将会是新的类,咱们写个 demo 来演示一下。java
String s ="hello"; s ="world";
从代码上来看,s 的值好像被修改了,但从 debug 的日志来看,实际上是 s 的内存地址已经被修改了,也就说 s =“world” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String,debug 的截图显示内存地址已经被修改,两张截图以下:面试
图片描述图片描述咱们从源码上查看一下缘由:segmentfault
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 的大多数操做方法,都会返回新的 String,以下面这种写法是不对的:数据结构
String str ="hello world !!"; // 这种写法是替换不掉的 str.replace("l","dd"); // 必须接受 replace 方法返回的参数才行 str = str.replace("l","dd");
在生活中,咱们常常碰到这样的场景,进行二进制转化操做时,本地测试的都没有问题,到其它环境机器上时,有时会出现字符串乱码的状况,这个主要是由于在二进制转化操做时,并无强制规定文件编码,而不一样的环境默认的文件编码不一致致使的。app
咱们也写了一个 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 后,打印出的结果就正常了。this
若是咱们的项目被 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 关键字所修饰,因此是不可变的,具体细节描述能够参考上文。