Effective Java读书笔记-14
如果其他类型更适合,则尽量避免使用字符串
字符串被用来表示文本,但存在一些不应该使用字符串的情形。
字符串不适合代替其他的值类型。当一段数据从文件、网络,或者键盘设备,进入程序之后,它通常以字符串的形式存在。有一种自然的倾向是让它继续保留这种形式,但是只有当这段数据本质上确实是文本信息时,这种想法才是合理的。如果它是数值,就应该被转换为适当的数值类型,比如int、float或者BigInteger类型。如果它是个"是-或-否”这种问题的答案,就应该被转换为boolean类型。如果存在适当的值类型,不管是基本类型,还是对象引用,大多应该使用这种类型;如果不存在这样的类型,就应该编写一个类型。
字符串不适合代替枚举类型。枚举类型比字符串更加适合用来表示枚举类型的常量。
字符串不适合代替聚合类型。如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。例如,下面这行代码来自于真实的系统——标识符的名称已经被修改了,以免发生纠纷:
// Inappropriate use of string as aggregate type
String compoundKey = className + "#" + i.next();
这种方法有许多缺点。如果用来分隔域的字符也出现在某个域中,结果就会出现混乱。为了访问单独的域,必须解析该字符串,这个过程非常慢,也很烦琐,还容易出错。你无法提供equals、toString或者compareTo方法,只好被迫接受String提供的行为。更好的做法是,简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类。
总而言之,如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。若使用不当,字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚合类型。
了解字符串连接的性能
字符串连接操作符(+)是把多个字符串合并为一个字符串的便利途径。但是接操作符(+)不适合运用在大规模的场景中。 为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间。这是由于字符串不可变而导致的结果。当两个字符串被连接在一起时,它们的内容都要被拷贝。
例如,下面的方法通过重复地为每个项目连接一行,构造出一个代表该账单声明的字符串:
// Inappropriate use of string concatenation-Performs poorly!
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++) {
result += lineForItem(i); // String concatenation
return result;
}
}
如果项目的数量巨大,这个方法的执行时间就难以估算。为了获得可以接受的性能,可以用StringBuilder代替String,来存储构造过程中的账单声明:
public String statement() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++) {
b.append(lineForItem(i));
}
return b.toString();
}
第二种做法预先分配了一个StringBuilder,使它大到足以容纳整个结果字符串,因此不需要自动扩展。即使使用了默认大小的StringBuilder,它也仍然比第一种做法快5.5倍。
原则很简单:不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。否则,应该使用StringBuilder的append方法。另一种做法是使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
通过接口引用对象
应该优先使用接口而不是类来引用对象(作为参数类型)。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类。以LinkedHashSet的情形为例,它是Set接口的一个实现。在声明变量的时候应该养成这样的习惯:
//Good-uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();
而不是像这样的声明:
//Bad-uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
如果养成了用接口作为类型的习惯,程序将会更加灵活。当你决定更换实现时,所要做的就只是改变构造器中类的名称(或者使用一个不同的静态工厂)。例如,第一个声明可以被改变为:
Set<Son> sonSet = new HashSet<>();
周围的所有代码都可以继续工作。周围的代码并不知道原来的实现类型,所以它们对于这种变化并不在意。
注意:如果原来的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点是,新的实现也要提供同样的功能。 例如,如果第一个声明周围的代码依赖于LinkedHashSet的同步策略,那么在声明中用HashSet代替LinkedHashSet就是不正确的,因为HashSet不能保证相关的迭代顺序。
如果没有合适的接口存在,完全可以用类而不是接口来引用对象。以值类(value class)为例,比如String和BigInteger。记住,值类很少会用多个实现编写。它们经常是final的,并且很少有对应的接口。使用这种值类作为参数、变量、域或者返回类型是再合适不过的了。
不存在适当接口类型的第二种情形是,对象属于一个框架,而框架的基本类型是类,不是接口。 如果对象属于这种基于类的框架(class-based framework),就应该用相关的基类(base class)(往往是抽象类)来引用这个对象,而不是用它的实现类。许多java.io类,比如 outputStream就属于这种情形。
不存在适当接口类型的最后一种情形是,类实现了接口但它也提供了接口中不存在的额外方法——例如PriorityQueue有一个没有出现在Queue接口中的comparator方法。如果程序依赖于这些额外的方法,这种类就应该只被用来引用它的实例,永远也不应该被用作参数类型。
以上这些例子并不全面,而只是代表了一些"适合于用类来引用对象"的情形。实际上,给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活。如果没有适合的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象。
接口优先于反射机制
核心反射机制(core reflection facility),java.lang.reflect包,提供了“通过程序来访问任意类”的能力。给定一个Class对象,可以获得Constructor、Method和Field实例,它们分别代表了该Class实例所表示的类的构造器、方法和域。这些对象提供了"通过程序来访问类的成员名称、域类型、方法签名等信息"的能力。
此外,Constructor、Method和Field实例使你能够通过反射机制操作它们的底层对等体:通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的域。例如,Method.invoke使你可以调用任何类的任何对象上的任何方法(遵从常规的安全限制)。反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而,这种能力也要付出代价:
- 损失了编译时类型检查的优势,包括异常检查。如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时它将会失败,除非采取了特别的预防措施。
- 执行反射访问所需要的代码非常笨拙和冗长。这样的代码阅读起来也很困难。
- 性能损失。反射方法调用比普通方法调用慢了许多。
总而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但它也有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
谨慎地使用本地方法
Java Native Interface(JNI)允许Java应用程序调用本地方法(native method),所谓本地方法是指用本地编程语言(比如C或者C++)来编写的方法。它们提供了"访问特定于平台的机制"的能力,比如访问注册表(registry)。它们还提供了访问本地遗留代码库的能力,从而可以访问遗留数据(legacy data)。最后,本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
使用本地方法来访问特定于平台的机制是合法的,但是几乎没有必要: 因为随着Java 平台的不断成熟,它提供了越来越多以前只有在宿主平台上才拥有的特性。
使用本地方法来提高性能的做法不值得提倡。对于大多数任务,现在用Java就可以获得与之相当的性能。
使用本地方法有一些严重的缺陷。 因为本地语言不是安全的,所以使用本地方法的应用程序也不再能免受内存毁坏错误的影响。因为本地语言是与平台相关的,使用本地方法的应用程序也不再是可自由移植的。使用本地方法的应用程序也更难调试。如果不小心,本地方法还可能降低性能,因为回收垃圾器不是自动的,甚至无法追踪本机内存(native memory)使用情况(详见第8条),而且在进入和退出本地代码时,还需要相关的开销。最后一点,需要“胶合代码”的本地方法编写起来单调乏味,并且难以阅读。
总而言之,只有在极少数情况下需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资源,或者遗留代码库,也要尽可能少用本地代码,并且要全面进行测试。本地代码中只要有一个Bug都可能破坏整个应用程序。