Effective Java 3rd (七)

目录

  1. 基本数据类型优于包装类
  2. 当使用其他类型更合适时应避免使用字符串
  3. 当心字符串连接引起的性能问题
  4. 通过接口引用对象
  5. 接口优于反射
  6. 明智审慎地本地方法
  7. 明智审慎地进行优化
  8. 遵守被广泛认可的命名约定
  9. 只针对异常的情况下才使用异常
  10. 编程错误使用运行时异常

61. 基本数据类型优于包装类

需要注意自动拆箱、装箱产生的bug

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

如果使用包装类,则会有问题

naturalOrder.compare(new Integer(42), new Integer(42))

返回1,原因是,i<j自动拆箱,显示不等于,然后再进行两个包装类的地址比较,也不等,所以返回1。
改成如下方法则能修复此bug

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
	int i = iBoxed, j = jBoxed; // Auto-unboxing
	return i < j ? -1 : (i == j ? 0 : 1);
};
public class Unbelievable {
static Integer i;
	public static void main(String[] args) {
		if (i == 42)
		System.out.println("Unbelievable");
	}
}

这段代码会返回空指针异常,因为Integer i是包装类,没赋值情况下是值为null。
那么,什么时候应该使用包装类型呢?
第一个是作为集合中的元素、键和值
第二是泛型参数

总结
首选基本类型,因为更快,减少自动拆装箱bug的风险。

62. 当使用其他类型更合适时应避免使用字符串

字符串是其他值类型的糟糕替代品
因为转换为需要的类型才能做处理,例如数值的相加。
字符串是枚举类型的糟糕替代品
正如条目 34 中所讨论的,枚举类型常量比字符串更适合于枚举类型常量
字符串是聚合类型的糟糕替代品

 // Inappropriate use of string as aggregate type
String compoundKey = className + "#" + i.next()

这个key如果遇到i.next()得到的恰好是“#”,而通过"#"分割解析文本将会出错

字符串不能很好地替代

// Broken - inappropriate use of string as capability!
public class ThreadLocal {
	private ThreadLocal() { } // Noninstantiable
	// Sets the current thread's value for the named variable.
	public static void set(String key, Object value);
	// Returns the current thread's value for the named variable.
	public static Object get(String key);
}

这段代码的问题是,如果客户端用同一个字符串,那么将会相互覆盖。
再进行优化,可以通过新建一个类来作为键

public class ThreadLocal {
	private ThreadLocal() { } // Noninstantiable
	public static class Key { // (Capability)
		Key() { }
	} 
	// Generates a unique, unforgeable key
	public static Key getKey() {
		return new Key();
	} 
	public static void set(Key key, Object value);
	public static Object get(Key key);
}

还可以进一步改进

public final class ThreadLocal {
	public ThreadLocal();
	public void set(Object value);
	public Object get();
}

但这个不是类型安全的,改为泛型

public final class ThreadLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}

这就是最终java.lang.ThreadLocal类

63. 当心字符串连接引起的性能问题

**字符串连接操作符 (+) 是将几个字符串组合成一个字符串的简便方法。对于生成单行输出或构造一个小的、固定大小的对象的字符串表示形式,它是可以的,但是它不能伸缩。使用 字符串串联运算符重复串联 n 个字符串需要n 的平方级时间。**使用StringBuilder则是线性级的,随着数量越来越大,连接操作符 (+) 的性能会越来越差。

64. 通过接口引用对象

如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。

// Good - uses interface as type
Set<Son> sonSet = new LinkedHashSet<>();
// Bad - uses class as type!
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

如果你养成了使用接口作为类型的习惯,那么你的程序将更加灵活。
但是要注意替换的类是否能满足要求,例如LinkedHashSet替换成HashSet将失去顺序排序的特性。
那么,为什么要更改实现类型呢?因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。
**如果没有合适的接口存在,那么用类引用对象是完全合适的。**如 String 和 BigInteger是final类的,不会有修改,所以不需要接口引用。
没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口,在 java.io 类中许多诸如 OutputStream 之类的就属于这种情况。
如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类

65. 接口优于反射

**核心反射机制 java.lang.reflect 提供对任意类的编程访问。**类似动态语言。
缺点
失去了编译时类型检查的所有好处。
执行反射访问所需的代码既笨拙又冗长。
性能降低。

对于许多程序,它们必须用到在编译时无法获取的类,在编译时存在一个适当的接口或超类来引用该类(详见第 64 条)。如果是这种情况,可以用反射方式创建实例,并通过它们的接口或超类正常地访问它们。

66. 明智审慎地本地方法

Java 本地接口(JNI)允许 Java 程序调用本地方法,这些方法是用 C 或 C++ 等本地编程语言编写的。
用途
1、特定于平台的设施(如注册中心)的访问
2、提供对现有本地代码库的访问,包括提供对遗留数据访问
3、本地方法可以通过本地语言编写应用程序中注重性能的部分,以提高性能
使用本地方法有严重的缺点
1、使用本地方法的应用程序不再能免受内存毁坏错误的影响
2、依赖平台,可移植性较差
3、本地方法可能会降低性能,因为垃圾收集器无法自动跟踪本地内存使用情况

67. 明智审慎地进行优化

优化格言

比起其他任何单一的原因(包括盲目的愚蠢),很多计算上的过失都被归昝于效率(不一定能实现)。
—William A. Wulf [Wulf72]

不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
—Donald E. Knuth [Knuth74]

在优化方面,我们应该遵守两条规则:
规则 1:不要进行优化。
规则 2 (仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行
优化。
—M. A. Jackson [Jackson75]

努力编写 好的程序,而不是快速的程序
如果一个好的程序不够快,它的架构将允许它被优化(可扩展、阅读性好,容易优化)。但是不是说可以忽略性能,性能不足可以在以后考虑。

尽量避免限制性能的设计决策
例如协议,通讯这些设计在中后期将难以更改

考虑API设计决策的性能结果
api的返回类型是可变的,可能需要大量不必要的防御性复制(详见第 50 条)
在一个公共类中使用继承(在这个类中组合将是合适的)将该类永远绑定到它的超类,这会人为地限制子类的性能(详见第 18 条)
API 中使用实现类而不是接口将你绑定到特定的实现,即使将来可能会编写更快的实现也无法使用(详见第 64 条)。

一旦你仔细地设计了你的程序,成了一个清晰、简洁、结构良好的实现,那么可能是时候考虑优化了,假设此时你还不满意程序的性能。

68. 遵守被广泛认可的命名约定

有利于交流与维护

Package or moduleExample
Package or moduleorg.junit.jupiter.api , com.google.common.collect
Class or InterfaceStream, FutureTask, LinkedHashMap,HttpClient
Method or Fieldremove, groupingBy, getCrc
Constant FieldMIN_VALUE, NEGATIVE_INFINITY
Local Variablei, denom, houseNum
Type ParameterT, E, K, V, X, R, U, V, T1, T2

69. 只针对异常的情况下才使用异常

/* Horrible abuse of exceptions. Don't ever do this! */
try {
	int i = 0;
	while ( true )
		range[i++].climb();
} catch ( ArrayIndexOutOfBoundsException e ) {
}

使用这种方式的错误理解:
1、认为trycatch就是用在不正常的情况。
2、把代码放在 try-catch 块中反而阻止了现代 JVM 实现本可能执行的某些特定优化
3、对数据进行遍历的标准模式并不会导致冗余的检查。有些 JVM 实现会将它们优化掉

这段代码的缺点:1、模糊意图,2、降低性能

正确的做法:
1、异常应该只用于异常的情况下;他们永远不应该用于正常的程序控制流程
2、设计良好的 API 不应该强迫它的客户端为了正常的控制流程而使用异常
例如,Iterator接口如果没有hasNext方法,那么将会这么调用

/
* Do not use this hideous code for iteration over a collection! */
try {
	Iterator<Foo> i = collection.iterator();
	while ( true )
	{
		Foo foo = i.next();
		...
	}
} catch ( NoSuchElementException e ) {
}

70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常

Java 程序设计语言提供了三种 throwable:受检异常(checked exceptions)、运行时异常(runtime exceptions)和错误(errors)。

如何选择:
1、如果期望调用者能够合理的恢复程序运行,对于这种情况就应该使用受检异常。(必须要捕获的异常)
2、用运行时异常来表明编程错误,例如客户端没有按照规定调用接口,或者数组越界问题
3、 实现的所有非受检的 throwable 都应该是 RuntimeExceptiond 子类,而不应该是Error的子类

对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复,就跑出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便协助程序恢复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值