读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code
建议52:推荐使用String直接量赋值★☆☆☆☆
示例代码:
public class Client {
public static void main(String[] args) {
String s1 = "谨以书为马";
String s2 = "谨以书为马";
System.out.println(s1 == s2);
//运行结果:true
String s3 = new String("谨以书为马");
System.out.println(s1 == s3);
//运行结果:false
}
}
两种赋值的区别:
-
直接赋值
因为String字符串是程序中最常使用的类型,所以Java为了避免在一个系统中大量产生String对象,于是就设计了一个字符串池(字符串常量池)
它的机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中该对象的引用,若没有则创建之,然后放到池中,并返回新建对象的引用。
-
new String("")赋值
new String("")直接声明的String对象是不检查字符串池的,也不会把对象放到池中。
对象放到池中会不会产生线程安全?
String类是一个不可变(Immutable)对象,有两层意思:一是String类是final类,不可继承,不可能产生一个String的子类;二是在String类提供的所有方法中,如果有String返回值,就会新建一个String对象,不对原对象进行修改,这也就保证了原对象是不可改变的。
注意:字符串池比较特殊,它在编译期已经决定了其存在JVM的常量池(ConstantPool),垃圾回收器是不会对它进行回收的。
建议53:注意方法中传递的参数要求★☆☆☆☆
以replace和replaceAll方法举例说明:
public class Client {
public static void main(String[] args) {
String s1 = "谨以书为马谨以";
System.out.println(s1.replace("谨以", ""));
//运行结果:书为马
String s2 = "谨以书为马谨以";
System.out.println(s2.replaceAll("谨以", ""));
//运行结果:书为马
}
}
以上例子中,虽然使用replace与replaceAll的运行结果一致,但是他们是有区别的,源码如下:
/**
* Replaces each substring of this string that matches the literal target sequence
* with the specified literal replacement sequence ...
* 翻译:使用指定的文字替换序列 替换此字符串中与文字目标序列匹配的每个子字符串
*/
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
/**
* Replaces each substring of this string that matches the given regular expression
* with the given replacement...
* 翻译:替换此字符串中 与给定正则表达式匹配的每个子字符串 为给定的替换字符串
*/
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
原来replaceAll方法传递的第一个参数是正则表达式,但是在参数类型上只要是String就行,所以很容易让使用者误解。
replace 的参数是 char 和 CharSequence,即可以支持字符的替换,也支持字符串的替换。
replaceAll 的参数是 regex,即基于正则表达式的替换,如果是正则,执行正则替换,如果是字符串,执行字符串替换。
注意:虽然有时确实得到了预期的结果,但我们要真正了解方法的参数要求
建议54:正确使用String、StringBuffer、StringBuilder★☆☆☆☆
String类是不可改变的,String类的操作都是产生新的String对象,赋值操作表面看似值变了,不过是引用指向了新创建的字符串对象而已。
StringBuilder与StringBuffer基本相同,都是可变字符序列,StringBuffer的方法前都有synchronized关键字,所以StringBuffer是线程安全的,这也是StringBuffer在性能上远低于StringBuilder的原因。
性能比较(由低到高):String < StringBuffer < StringBuilder
三者的使用场景:
-
使用String类的场景
在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算等。
-
使用StringBuffer类的场景
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
-
使用StringBuilder类的场景
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。
建议55:注意字符串的位置★☆☆☆☆
示例代码:
public class Client {
public static void main(String[] args) {
String s1 = 1 + 2 + "谨以书为马";
System.out.println(s1);
//运行结果:3谨以书为马
String s2 = "谨以书为马" + 1 + 2;
System.out.println(s2);
//运行结果:谨以书为马12
}
}
Java对加号的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接,如果是原始数据,则直接拼接,如果是对象,则调用toString方法的返回值然后拼接。
注意在“+”表达式中,String字符串具有最高优先级。
建议56:自由选择字符串拼接方法★★☆☆☆
字符串进行拼接有三种方法:加号、concat方法及StringBuilder(StringBuffer,两者是一样的)的append方法。
三者之间的区别:append方法最快,concat方法次之,加号最慢。具体看如下示例:
- “+”方法拼接字符串,示例代码:
//“+”方法拼接字符串
String str = "a";
str += "c";
str += "d";
//内部实现与以下代码相同
str = new StringBuilder(str).append("c").toString();
str = new StringBuilder(str).append("d").toString();
它与纯粹使用StringBuilder的append方法是不同的:一是每次会创建一个StringBuilder对象,二是每次执行完毕都要调用toString方法将其转换为字符串,它的执行时间就是耗费在这里了。
注意:
String str = "a"; str += "b" + "c" ; //内部实现与以下代码相同 str = new StringBuilder(str).append("b").append("b").toString(); //所以“+”拼接字符串,尽量“一行”写,避免循环中使用“+”拼接字符串
- concat方法拼接字符串源码:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
//将原始字符串放到新增长的char数组中
char buf[] = Arrays.copyOf(value, len + otherLen);
//将新字符串添加到buf中
str.getChars(buf, len);
//返回新的字符串
return new String(buf, true);
}
其整体看上去就是一个数组拷贝,虽然在内存中的处理都是原子性操作,速度非常快,不过,注意看最后的return语句,每次的concat操作都会新创建一个String对象,这就是concat速度慢下来的真正原因。
- append方法拼接字符串,StringBuilder的append方法直接由父类AbstractStringBuilder实现,其代码如下:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//加长数组,并做copy
ensureCapacityInternal(count + len);
//将字符串复制到目标数组
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
整个append方法都在做字符数组处理,加长,然后数组拷贝,这些都是基本的数据处理,没有新建任何对象,所以速度也就最快了。
注意:StringBuilder的实现性能最优,但并不表示我们一定要使用StringBuilder,因为“+”非常符合我们的编码习惯,适合人类阅读。在大多数情况下我们都可以使用加号操作,只有在系统性能临界(如在性能“增之一分则太长”的情况下)的时候才可以考虑使用concat或append方法。而且,很多时候系统80%的性能是消耗在20%的代码上的,我们的精力应该更多的投入到算法和结构上。
建议57:推荐在复杂字符串操作中使用正则表达式★☆☆☆☆
正则表达式在字符串的查找、替换、剪切、复制、删除等方面有着非凡的作用,特别是面对大量的文本字符需要处理(如需要读取大量的LOG日志)时,使用正则表达式可以大幅地提高开发效率和系统性能,但是正则表达式是一个恶魔(Regular Expressions is evil),它会使程序难以读懂,想想看,写一个包含^、$、\A、\s、\Q、+、?、()、[]、{}等符号的正则表达式,然后告诉你这是一个“这样,这样……”的字符串查找,你是不是要崩溃了?这代码只有上帝才能看懂了!
注意:正则表达式是恶魔,威力巨大,但难以控制。
建议58:强烈建议使用UTF编码★☆☆☆☆
Java程序涉及的编码包括两部分:
- Java文件编码
如果我们使用记事本创建一个.java后缀的文件,则文件的编码格式就是操作系统默认的格式。如果是使用IDE工具创建的,则依赖于IDE的设置。 - Class文件编码
通过javac命令生成的后缀名为.class的文件是UTF-8编码的UNICODE文件,这在任何操作系统上都是一样的,只要是class文件就会是UNICODE格式。需要说明的是,UTF是UNICODE的存储和传输格式,它是为了解决UNICODE的高位占用冗余空间而产生的,使用UTF编码就标志着字符集使用的是UNICODE。
注意:为了避免在应用开发中出现乱码或者需要额外进行转码操作,最好的解决办法就是使用统一的编码格式。
建议59:对字符串排序持一种宽容的心态★☆☆☆☆
Java中一涉及中文处理就会冒出很多问题来,其中排序也是一个让人头疼的课题,中国的汉字博大精深,Java是否都能精确的排序呢?最主要的一点是汉字中有象形文字,音形分离,是不是每个汉字都能按照拼音的顺序排列好呢?
答案是:并不能。
示例代码:
public class Client {
public static void main(String[] args) {
String[] strs = {"犇(B)", "鑫(X)"};
Arrays.sort(strs, Collator.getInstance(Locale.CHINA));
int i = 0;
for (String str : strs) {
System.out.println((++i) + "、" + str);
}
//运行结果:
//1、鑫(X)
//2、犇(B)
}
}
输出结果乱了!不要责怪Java,它已经尽量为我们考虑了,只是因为我们的汉字文化太博大精深了,要做好这个排序确实有点难为它。
更深层次的原因是Java使用的是UNICODE编码,而中文UNICODE字符集是来源于GB18030的,GB18030又是从GB2312发展起来,GB2312是一个包含了7000多个字符的字符集,它是按照拼音排序,并且是连续的,之后的GBK、GB18030都是在其基础上扩充出来的,所以要让它们完整排序也就难上加难了。
注意:如果排序不是一个关键算法,使用Collator类即可。