概述
写在前面,工作第四年,重新把基础抓起来吧。String可以说是JDK中最基础的一个类。就记录一些日常开发中最常用的方法。
String类是非可变类,其对象一旦创建,就不可销毁。String类那些看似修改字符序列的方法实际上都是返回新创建的String对象,而不是修改自身对象。由于String对象是不可改变的,因此具有线程安全性,可以自由地实现共享。在String类内部,是使用一个字符数组(char[])来维护字符序列的。String的最大长度也就是字符数组的最大长度,理论上最大长度为int类型的最大值,即2147483647。在实际中,一般可获取的最大值小于理论最大值。
实战
UnknownFormatConversionException
代码很简单:
return String.format("#iview%s监控#\n"
+ "\n预警信息:" + msg
+ "\n\n数据异常,请及时处理", typeName);
将typeName
替换掉监控前面的%s
,表示某种类型的监控,并拼接msg
。而String
实际上的执行逻辑是:先拼接后替换。报错的原因是msg
里面也含有%s
,于是报错。解决方法:
return String.format("#iview%s监控#\n", typeName)
+ "\n预警信息:" + msg
+ "\n\n数据异常,请及时处理";
先替换后拼接。
类似的错误:stackoverflow
String&StringBuilder&StringBuffer
StringBuilder是可变的,这就意味你在创建对象之后还可以去修改它的值。
StringBuffer是同步的,意味着它是线程安全的,会比StringBuilder慢些。
用空格去分隔字符串?
用正则表达式:"\s"即表示空格,其他还有如"","\t","\r","\n"。
String[] strArray = "johnny 233".split("\\s+");
trim()
问题背景:
之前公司开发时,遇到的一个问题。docker环境下,启动镜像注册应用时输入用户名和密码,然后密码会在容器里有shell脚本去做一层加密处理,再次登录应用时,shell脚本会去解密密文,然后应用层判断一下登录时输入的密码和保存的密码(经过加密后)是否一样,一样则验证通过,准许登录应用。大致如此。现在的问题是,明明记得相当清楚密码就是一个数字1,可是怎么都验证不通过,反复两次都是这样。一开始还怀疑他人写的加密解密shell脚本的问题,或者是存储的问题(密文存储到PostgresSQL中)。后来想着把错误推到别人头上之前,先确认一下自己的问题,就增加一个logger记录:
password from FE [1], password from shell [1
]
此时才恍然大悟。在Java程序里面调用shell脚本时返回字符串包含\t这样的特殊字符,加上trim解决问题。之前一直以为trim()方法用于去掉字符串前后的空格。google之后:trim()方法去掉编码小于等于\u0020的字符,也就是在ASCII码里面十六进制20以前的字符。要保留\t,\n的要慎用trim()。
split()
两种重载形式:
public String[] split(String regex)
等价于public String[] split(String regex, int limit)
其中limit=0,limit用来限制返回数组中的元素个数
regex为空字符串时,返回包含整个字符串的单一元素数组;
“.“需要加转义字符变成”\.”;
如果在一个字符串中有多个分隔符,可以用"|“作为连字符,即”\.|\。"。(有中英文句号两个分隔符)
当字符串只包含分隔符时,返回数组没有元素;
当字符串不包含分隔符时,返回数组只包含一个元素(该字符串本身);
字符串最尾部出现的分隔符可以看成不存在,不影响字符串的分隔;
字符串最前端出现的分隔符将分隔出一个空字符串以及剩下的部分的正常分隔;
intern()
Java源码只有一行:public native String intern();
文档:
Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class. When the intern method is invoked, if the pool already contains a string equal to this object as determined by the equals method, then the string from the pool is returned. Otherwise, this object is added to the pool and a reference to this object is returned. All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java Language Specification.
@return a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
对于两个字符串s和t,s.intern() == t.intern()
当且仅当s.equals(t)
。
intern()方法的调用会先去字符串常量池中查找相应的字符串,如果字符串不存在,就会在字符串常量池中创建该字符串然后再返回。
字符串常量池是一个固定大小的HashMap,桶的数量默认是1009, 从Java7u40开始,该默认值增大到60013。在Java6当中,字符串常量池是放在Perm空间的,从Java7开始字符串常量池被移到Heap空间。
replaceAll() & replace()
需求:
拿到一个JSON字符串:{"number":5,"vendorType":1,"refresh":null}
,现在期望对JSON字符串里面的引号进行转义,得到这样的输出:{\"number\":5,\"vendorType\":1,\"refresh\":null}
。
看String的API,理所当然地使用replaceAll么,因为是想要对所有的双引号进行替换:sss.replaceAll("\"", "\\\"")
。但是这个输出并不是自己期望的。一脸懵逼。然后看到
即有两种方法实现:
sss.replace("\"", "\\\"");
sss.replaceAll("\"", "\\\\\"");
实际上,无论是replaceAll()还是replace()方法,都是使用正则表达式匹配,是全文替换的。然后replaceAll()源码文档里面写有两种情况下可能会有意想不到的情况发生,另一个字符是$
。
至于为什么是5个斜杠,参考上面的链接:
需要一个 \ 来给替换函数转义
还需要在每个 \ 前再加一个 \ 来给 Java 编译器转义
再还需要一个 \ 来给 Java 编译器转义 "
另外,带有引号的JSON字符串copy到IDEA时,IDEA会自动进行转义。
“a”+"b"会产生几个对象
String str="a"+"b";
产生几个对象?面试考察,很变态是吧。
只能从字节码分析入手:
用IDEA看class文件,看到的是"ab",反编译器是直接从class文件中取"ab",即在javac编译,已经加以优化。
javap -v AB1.class
看字节码指令只有一个对象。
拓展:
String a = "a";
String b = "b";
String result = a + b ;
产生几个对象?
使用StringBuilder进行拼接。此时有三个对象 a、b、result,以及StringBuilder对象,共四个对象。
new String("ab")
产生几个对象?
new String时会额外强制新建一个对象,共两个对象。
参考
快速重复构造一段字符串
无论是使用guava还是使用Apache的commons-lang3都可以实现:
StringUtils.repeat("johnny", 233); Strings.repeat("johnny", 233)
统计一个字符在某个字符串中出现的次数
StringUtils.countMatches("11112222", "1");
字符串用于switch表达式
从JDK7开始,可以在switch条件表达式中使用字符串:
switch (str.toLowerCase()) {
case "a":
value = 1;
break;
case "b":
value = 2;
break;
}
substring()方法
在JDK6中,这个方法只会在标识现有字符串的字符数组上 给一个窗口来表示结果字符串,但是不会创建一个新的字符串对象。如果需要创建个新字符串对象,可以这样在结果后面+一个空的字符串:
str.substring(m, n) + “”
这么写的话就会创建一个新的字符数组来表示结果字符串。同时,这么写也有一定的几率让你的代码跑的更快,因为垃圾回收器会吧没有在使用的大字符串回收而留下子字符串。
Oracle JDK7中的substring()方法会创建一个新的字符数组,而不用之前存在的。看看这张图就会明白substring()方法在JDK6和JDK7中的区别。
将时间格式的字符串转换成date对象
String str = "Sep 17, 2013";
Date date = new SimpleDateFormat("MMMM d, yy", Locale.ENGLISH).parse(str);
System.out.println(date);//Tue Sep 17 00:00:00 EDT 2013
创建字符串对象时,使用字面值和new String()构造器的区别
当使用new String构造器来创建字符串的时候,字符串的值会在堆中创建,而不会加入JVM的字符串池中。相反,使用字面值创建的String对象会被放入堆的PermGen段中。例如:String str=newString(“Test”);
这句代码创建的对象str不会放入字符串池中,需要显式调用String.intern()方法来将它放入字符串池中。仅仅当你使用字面值创建字符串时,Java才会自动将它放入字符串池中,如:String s=”Test”。将参数“Test”传入构造器的时候,这个参数是个字面值,因此它也会在字符串池中保存另外一份。这篇文章。
下图很好地解释这种差异。