第57条 将局部变量的作用域最小化
- 将局部变量作用域最小化,可以增强代码可读性和可维护性,并降低出错的可能性
57.1 具体做法
- 在第一次使用它的地方进行声明
- 过早声明变量会导致代码难以读懂,等使用变量时,读者可能已经记不起该变量的类型和初始值了
- 过早声明局部变量还会导致扩大整个作用域,如果变量在"应该使用它的块"外声明,会导致这个变量在这个目标区域之前或之后使用,后果将是灾难的
- 每一个局部变量声明都应该包含一个初始化表达式
- 如果没有足够信息对变量进行有意义的初始化,应该推迟声明的位置,直到可以初始化
- 此规则有例外情况,就是如果一个变量被try块内的代码初始化(因为初始化的过程可能产生受检异常),且这个变量需要在try块外使用,那么这个变量就必须在try块前声明,但很明显,在try块之前,该变量并不能被有意义地初始化
- for循环和for-each循环允许声明循环变量,它们的作用域被限制在了循环体+for()内,保证了变量的作用域最小化,因此如果在循环终止后,不再需要循环变量的内容,且代码需要更强的可读性时,for循环优先于while循环
//1. 遍历集合的首选做法
for (Element e : c) {
... // Do Something with e
}
//2. 如果需要访问迭代器,调用其remove方法时的做法,for循环代替for-each循环
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
//3. 使用while循环的问题,在于下面这种情况,由于第二段代码是复制的第一段代码,如果忘记修改while中的i成i2,也能正常编译,但结果却有问题
//这个问题本质上讲,是因为while导致循环变量作用域扩大而产生的
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
doSomethingElse(i2.next());
}
//4. 使用for循环改造后,第二段代码,使用了i.hasNext,根本无法通过编译,因为在下面for循环中,根本无法读取到i这个变量
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
...
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
Element e2 = i2.next();
... // Do something with e2 and i2
}
- for循环的最佳实践
//如果将n放在for循环之上,导致n作用域变大
//如果将n放在for循环内,每次都需要重新计算一次n的值,浪费系统性能
//将n直接放在for的括号内,其作用域只在for的括号内+循环体内,且只执行一次
//适用于循环内涉及方法调用,且n对于每次循环,该方法调用返回值固定不变的情况
for (int i = 0, n = expensiveComputation(); i < n; i++) {
... // Do something with i;
}
- 使方法小而集中
- 如果将两个操作合并到一个方法中,那么第二个操作就可以读到第一个操作的局部变量,可能会产生问题
- 只需将这一个方法分开成为两个,每个操作用一个方法完成,就能保证将局部变量的作用域最小化
第58条 for-each循环优先于传统的for循环
58.1 传统for循环的缺点
- 容易写错,难以不改动客户端前提下,修改容器类型
- 对于第一个方法,表示iterator对象的i出现了3次,第二个方法表示元素索引的i出现4次,很容易出现写错变量的情况,如果写错了很有可能编译器还无法发现
- 两个循环完全不同,容器的类型的不同,转移了注意力,并且一旦容器类型变化,客户端代码难以修改,需要将整个循环方式都改动
//循环集合
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
//循环数组
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
- 平行迭代问题
//这段代码会造成平行迭代,不是我们想象的先由suits提供一个固定值,然后由这个固定值和ranks中所有元素进行组合
//由于每次都使用了i的next方法和j的next方法,导致每次都从suits和ranks中分别取一个新值出来,组成一个Card
//当Suit用尽,循环就会抛出NoSuchElementException
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
- 平行迭代时,如果外部集合大小为内部集合大小的整数倍(比如外部集合和内部集合是同一个,那么就是1倍的情况),问题更严重,循环会正常结束,但返回的结果和你预期的不同
//1. 该方法会正常结束,但会打印ONE ONE,TWO TWO,一直到SIX SIX
//2. 当外部集合是内部集合的整数倍时,比如外部集合30个元素,内部集合5个元素,那么会外部集合每五个元素陪内部集合的五个元素进行一次循环
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
- 传统for循环中修正平行迭代
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(suit, j.next()));
}
- for-each修正平行迭代
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
58.2 无法使用for-each循环的情况
- 解构过滤(破坏性过滤,所谓破坏可能就是指会破坏原有的集合、解除结构):需要使用显示迭代器的remove方法删除集合中元素。但这种情况在Java8中可以通过使用Collection接口新提供的removeIf(Predicate filter)方法完成同样的功能,从而避免使用传统for循环
- 转换:需要遍历集合或数组,并且修改其中的某些值,需要list的iterator或数组的索引,来完成这个工作
- 平行迭代
58.3 for-each循环可以处理的元素类型
- 可以使用for-each来遍历集合和数组
- 也可以遍历实现Iterable接口的对象
- 因此当你正在写一个表示一组元素的类,且需要遍历其内所有元素时,让他实现Iterable接口比实现Collection接口更重要,这样就可以对这个类的对象,使用for-each进行遍历
58.4 最佳实践
- for-each更简洁、灵活、不容易出错,且没有性能问题
- 除了58.2中说明的几种情况,都应该使用for-each替代传统for循环
59 了解并使用类库
59.1 使用标准类库的优点
59.1.1 通过使用标准类库,可以充分利用编写标准类库的专家的知识,以及在你之前其他人的使用经验
- 如果你只了解Random的nextInt()这一个方法,那么当你想设计一个方法(random),可以返回0到传入的int类型值(n)之间的随机整数,可能会编写如下方法
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
- 如果n为一个比较小的2的乘方,那么随机数就是那么几个,多次取随机数结果会重复,虽然这可能算不上一个毛病
- 如果n不是2的整数次方,那么该方法结果中某些数会比其他数出现的更频繁,尤其n越大,这个问题越严重
//该结果产生的数字,有2/3落在0到n的前半部分
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for (int i = 0; i < 1000000; i++)
if (random(n) < n / 2)
low++;
System.out.println(low);
- 由于Math.abs正常应该返回传入的int型数的绝对值,但该方法有一个问题,如果这个方法其内传入的值如果恰好为Integer.MIN_VALUE,而Integer.MIN_VALUE的绝对值已经超过了int的范围,所以该方法无法返回Integer.MIN_VALUE的正整数,而是会返回Integer.MIN_VALUE这个负数本身,此时如果n不是2的整数次方,无法整除,取模操作会返回一个负数
- 如果自己想编写一个修正这三个缺点的random方法,非常复杂,API中实际上已经提供了Random.nextInt(int)方法,完成该功能
- Java7开始后,选择随机数生成器时,大多使用ThreadLocalRandom,在作者机器上,是Random效率的3.6倍。对于Fork Join Pool和并行Stream,使用SplittableRandom
59.1.2 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案
应该把时间花费在应用程序上,而不是底层的细节上
59.1.3 标准类库提供的方法,会随时间推移,性能不断提高
提供这些标准类库的组织,会不断完善这些方法的实现细节,从而提升性能,比如当jdk推出新版本,你去使用,相当于就是用了新的实现,而你的客户端不需要变动,性能就会提升
59.1.4 会随着时间的推移增加新的功能
类库中如果漏掉了某些功能,开发者社区会把这些缺点公示出来,漏掉的功能就会添加到后续的发行版本中
59.1.5 可以使自己编写的代码融入主流,可读性、可维护性、可重用性更强
59.2 应如何学习标准类库
- 每个重要发行版本,都会有很多新的特性被加入到类库,应该同步地了解这些新特性
- 可以通过阅读新特性的说明网页(Java8-feat、Java9-feat),来了解这些新特性
- 标准类库太庞大,无法学完所有的文档,但一般程序员应该熟悉java.lang、java.util、java.io及其子包下的所有内容,其他类库的知识可以根据需要随时学习
- 尤其是Collections Framework、Stream类库、java.util.concurrent包,这些内容尤其重要
- 如果标准类库中没有功能满足你的需求,也不要着急自己实现,而是应该考虑使用比较高级的第三方类库,比如Google的Guava类库
59.3 最佳实践
- 不要重复发明轮子(reinvent the wheel)
- 所谓发明轮子,而不是制造轮子,因为轮子形状应该就是圆的,已经公认没有更好的形状替代它,尝试使用发明新的形状的轮子,就叫发明轮子
- 而制造轮子是被赞成的,制造轮子是指制造不同种类的轮子,比如雪地轮胎、越野轮胎
- 实现功能时,优先使用标准类库,如果不存在是否有满足功能的类,就去查一查
- 标准类库中代码比起自己写的代码质量更高,并且可以随着时间推移,不断改进
60 如果需要精确的答案,避免使用float和double
60.1 不同方式的计算
- float和double不适用于货币计算,因为他们无法准确的表示0.1
//打印0.6100000000000001,而不是0.61
System.out.println(1.03-0.42);
//打印0.09999999999999998,而不是0.1
System.out.println(1.00 - 9 * 0.10);
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
- 使用BigDecimal:BigDecimal与基本类型运算相比,编程复杂,且效率变低
public static void main(String[] args) {
//使用BigDecimal的String构造器,而不是double构造器,避免不正确的值引入到计算
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;
funds.compareTo(price) >= 0;
price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
- 使用int、long进行货币的计算:需要自己处理十进制小数点,比如本例中,不再使用元作为单位计算,而是使用分为单位
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;
funds.compareTo(price) >= 0;
price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
60.2 最佳实践
- 对于需要精确答案的计算任务,不要使用float和double
- 如果想让系统处理十进制小数点,且不介意因为不使用基本类型而带来的不变,就可以使用BigDecimal,而且BigDecimal API提供不同的舍入方式,方便使用
- 如果不介意自己处理十进制小数点,且涉及的数值不太大,可以使用int和long,long最多18位,如果数值超过18位,就必须使用BigDecimal
61 基本类型优于装箱的基本类型
61.1 基本类型与装箱基本类型区别
基本类型:primitive
装箱基本类型:boxed primitive
- 两个基本类型值相等,就完全相同,而两个装箱基本类型,可能值相等,但对象本身不相等,因此对于装箱基本类型使用==判断是否相等,基本都是有问题的
//false
System.out.println(new Float(123)==new Float(123));
//true
System.out.println(123f==123f);
- 装箱基本类型可能为null,而基本类型不能
- 基本类型比装箱基本类型更节省时间和空间
61.2 装箱基本类型引发的问题
- 问题一:装箱基本类型间,不应使用==比较相等
Comparator<Integer> naturalOrder =
(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
int value = naturalOrder.compare(new Integer(1),new Integer(1));
//值为1,因为虽然两个装箱基本类型值相同,但==是判断对象地址,是不同的
System.out.println(value);
- 如果只想实现自然排序,可以使用Comparator.naturalOrder()来构造Comparator对象,这个由Comparator提供的方法,已经实现了自然排序
- 如果想自己实现该功能
//方案一
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed; // Auto-unboxing
return i < j ? -1 : (i == j ? 0 : 1);
};
//方案二
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
return Integer.compare(iBoxed,jBoxed);
};
- 问题二:装箱基本类型与基本类型比较时,会自动拆箱
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
//会抛出NullPointerException,因为i为Integer,Integer与int比较时,会自动拆箱
//自动拆箱相当于调用了Integer的intValue()方法,由于Integer对象为null,因此抛出异常
if (i == 42)
System.out.println("Unbelievable");
}
}
- 问题三:循环中使用装箱基本类型,引发性能问题
//sum为Long型,循环中反复的装箱与拆箱,引发性能问题
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
61.3 使用装箱基本类型的场景
- 对象需要作为集合中的元素、键、值的情况
- 因为无法将基本类型放在集合中
- 无法用基本类型作为泛型
- 例如ThreadLocal<int>无法编译通过,需要写为ThreadLocal<Integer>
- 利用反射调用对象的方法时,无法反射生成int类型的对象,只能生成Integer的
61.4 最佳实践
- 基本类型优先于装箱基本类型
- 自动装箱功能降低了使用装箱基本类型的繁琐性,但提升了使用它的风险
- 不要使用==判断两个装箱基本类型
- 混用基本类型和装箱基本类型时,会涉及到装箱基本类型的拆箱操作,可能会抛出NullPointerException
- 使用装箱基本类型会导致较高的资源消耗,和不必要的对象创建
62 如果其他类型更适合,则尽量避免使用字符串
62.1 不适合用字符串的场景
- 字符串不适合代替其他的值类型
- 当一段数据从文件、网络、键盘设备,进入程序后,它通常以字符串的形式存在
- 因此很多人认为应该让这段数据以String存在,但如果这段数据本质上并不是文本信息时,应该以其适当的值类型对其进行存放
- 如果是数值,应转为int、float、BigInteger,如果是yes-or-no问题的答案,应以boolean存放,如果没有适合的类型表示这段数据,就应编写一个新类型表示
- 字符串不适合替代枚举类型(item 34)
- 字符串不适合替代聚合类型
- 如果用来表示分隔的字符#,如果需要表示属性的值,例如className叫做Test#Wu,那么会出现混乱,不知道到底哪段字符串对应哪个属性,例如是Test是类名,还是Test#Wu是类名
- 如果想访问单独某一个属性,必须解析整个compoundKey字符串,非常繁琐和易错
- 没法为compoundKey属性提供符合逻辑的equals、toString、compareTo方法,只能使用String类默认的
- 更好的做法是简单的写一个类描述这个数据集,通常是一个静态成员类(item 24)
//compoundKey就是一个聚合类型,因为它用一个属性表示了两个不同的属性,className与i.next(),不同属性间使用#分隔
String compoundKey = className + "#" + i.next();
//修改方案
private static class CompoundKey{
String className;
String iNext;
}
- 字符串不适合替代capabilities
- capabilities(能力的复数):这里指根据不同String对象来表示具有不同的能力的做法,并不好
- 例如自定义ThreadLocal,保证不同线程,使用同一个ThreadLocal对象(或类,也就是static的get方法)的get方法时,可以获取不同的值。使用方案如果是,对于每个客户端持有不同的String,根据这个String来获取客户端独有的值,就叫作用String当做能力表
- Java1.2之前,还没有ThreadLocal,需要自定义
//使用String当做能力表,ThreadLocal的get方法,对于同一个字符串,可以返回唯一值,此时只要让不同线程持有不同的字符串,就能实现不同线程通过ThreadLocal获取不同变量值的功能
//如果不同线程,决定使用同一个字符串,就会导致第二个线程获取到第一个线程中的thread-local variable,可能会有恶意的客户端伪造键,从而访问其他线程中的那个thread-local variable
public class ThreadLocal {
private ThreadLocal() { }
public static void set(String key, Object value);
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);
}
//改造二:可以去掉静态方法,使用key的实例方法替代这个构造方法,此时key不再是key,而是完全可以替代ThreadLocal的功能
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
public static class Key { // (Capability)
Key() { }
//可以使用如下两个Key类中的实例方法,替代原有的两个静态方法,由于实例方法本身一定持有当前Key的引用
//在方法内使用this关键字就可以获取Key对象,因此Key不再需要作为参数传入
public void set(Object value);
public Object get();
}
// Generates a unique, unforgeable key
public static Key getKey() {
return new Key();
}
}
//改造三:外层ThreadLocal已经没有任何作用,因为既没有对外的构造器对其初始化,又不提供具体方法,所以可以去掉,同时原来的Key类,完全具备了ThreadLocal的功能,所以将原有Key类改名为ThreadLocal
//此处使用final修饰,可能是为了防止通过继承修改其功能
public final class ThreadLocal {
public ThreadLocal();
//可以使用如下两个Key类中的实例方法,替代原有的两个静态方法,由于实例方法本身一定持有当前Key的引用
//在方法内使用this关键字就可以获取Key对象,因此Key不再需要作为参数传入
public void set(Object value);
public Object get();
}
//改造四:改造三种的方法类型不安全,因为使用get方法的客户端,只能获取到一个Object对象,必须从Object转换为其真正值,转换过程中可能转错(转错时,编译通过,执行报错),所以叫做类型不安全,通过将ThreadLocal泛型化,来解决类型不安全问题
//1. 一定要注意,对于之前String和Key的例子,是没办法做到通过将ThreadLocal泛型化,来解决类型不安全问题的,因为类上的泛型信息,没法用在static方法内,比如如下写法编译是无法通过的
//public class ThreadLocal<T> {
// 编译报错,找不到T
// public static T get(String key){
// return null;
// }
//}
//2. 这就是简略版的java提供的ThreadLocal的API,不同的线程持有不同的Key(已改为ThreadLocal),从而获取不同的值
//此处省略了具体的set、get实现,同时也省略了为每个线程,持有不同的Key的做法
public final class ThreadLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}
62.2 最佳实践
- 当可以使用更加适合的数据类型,或可以编写恰当的数据类型时,就不用String对象表示该数据
- 如果使用不当,字符串会比其他类型更笨拙、更不灵活、速度更慢、更容易出错
- 基本类型、枚举类型、聚合类型一般都不应该使用String表示
63 了解字符串连接的性能
63.1 String的+的性能问题
- 字符串连接符(+)不适用于大规模字符串连接,使用字符串连接符连接n个字符串,时间复杂度为n的平方
- 这是由于字符串的不可变性导致的,两个字符串连接在一起时,它们的内容都需要拷贝一份
//使用字符串连接符
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++)
result += lineForItem(i); // String concatenation
return result;
}
//使用StringBuilder替代String,同时用其append方法进行字符序列的连接
//当进行100次(numItems)连接,每次连接的字符串长度为80(lineForItem(i)),在作者机器上,StringBuilder方法比String的+,要快6.5倍
public String statement() {
//此处为StringBuilder预先分配了长度,这个长度足以容纳最后的结果,所以连接过程中,StringBuilder不需要自动扩展,性能更快,即使不预先分配长度,StringBuilder仍是String效率的5.5倍
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++)
b.append(lineForItem(i));
return b.toString();
}
63.2 最佳实践
- 应该使用StringBuilder的append替代String的+,对多个字符串进行连接,除非不在乎性能
- 也可以使用字符数组来提升性能,甚至就不将String进行连接,而是每次处理一个String
64 通过接口引用对象
64.1 接口引用对象的好处
- item 54中说明了应该使用接口,而不是具体的类,来作为参数的类型,实际上无论是参数、返回值、变量、成员,只要有合适的接口类型存在,都应使用接口类型进行声明,而不使用具体类型
- 只有在构造方法中,才使用具体类型,来返回创建的对象
//正确做法
Set<Son> sonSet = new LinkedHashSet<>();
//错误做法
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
- 用接口作为类型,使程序更加灵活,具体实现修改,不影响其他客户端代码
//当决定更换实现时,只需修改调用的构造器(如果用的是静态工厂方法,那就直接换一个返回新的类型的工厂方法就行)
Set<Son> sonSet = new HashSet<>();
- 需要注意更换实现时,如果原来的实现提供了某种特殊的功能(不是提供了新的方法,而是功能),那么用于替换的新的实现,也必须提供这个功能
- 例如如果原来使用的是LinkedHashSet,同时客户端又依赖于它的排序功能,才能正确的执行,那么就不用用一个HashSet替换它
- 改变具体实现带来的好处:新实现拥有更好的性能和新的功能
- 例如使用EnumMap替换HashMap,性能提升,且保证其内元素有序。但注意,只有key是Enum类型时,才能用EnumMap
- 又比如LinkedHashMap替代HashMap,可以提供可预见的迭代顺序,同时也没对key值做出什么特殊要求(不像EnumMap)
- 使用具体类型声明变量的缺点
- 虽然可以同时修改声明的类型和具体实现类型,从而改为新的实现
- 但如果新类型不具有原类型中的某些方法,会导致编译报错
64.2 不必使用接口替代具体类型的情况
- 没有合适的接口存在:比如值类(value class),值类指的是仅仅用于表示值的类,例如String、BigInteger,值类很少用多个实现进行编写,而且是final的,所以值类可以直接用于引用对象(引用对象就是指定义一个值类的引用,指向值类的对象)
- 对象属于一个框架,而框架的基本类型时类,这种框架叫做基于类的框架(class-based framework),就应该使用相关的基类(base class 往往是抽象类),来引用这个对象,也不是使用具体实现。比如OutputStream就属于这种情形
- 具体的实现类提供了接口额外的方法,而且应用程序依赖于这个额外的方法。例如PriorityQueue相比Queue提供了一个compare方法,如果客户端还用了这个方法,那么就不应该用Queue来引用具体实现
65 接口优先于反射机制
65.1 反射的功能
- 核心的反射工具,java.lang.reflect,提供了编程访问任意类的能力
- 反射机制允许一个类使用另一个编译时还不存在的类
65.2 反射的问题
- 损失了编译时,类型检查、异常检查的优势
- 执行反射访问所需要的代码笨拙且冗长,可读性差
- 性能损失:在作者机器上,使用反射调用一个以int作为返回值,无参的方法,比普通的方法调用慢11倍
65.3 需要使用反射的情况
- 如果需要用到的类在编译时并不存在,就可以用反射的方式创建其实例,然后通过它们的接口或超类,以正常的方式访问这些实例
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Set;
//1. 本例中,产生了6个运行时异常,需要在代码中try catch人工处理,如果不使用反射,这些异常实际上都是编译时异常(编译能通过,运行时就不会有问题,也就不用人为try catch了)
//2. 为了创建出Set实例,花费了大量的代码
//3. 程序的长度可以通过直接捕捉Java7引入的ReflectiveOperationException异常来减少,因为它是所有反射产生的异常的父类
//4. 限定只有用到的类在编译时并不存在,才使用反射,除了实例化Set对象的代码受到反射影响,其他代码都和原来一样
public class TestNew {
public static void main(String[] args) {
Class<? extends Set<String>> cl = null;
try {
//5. 此处会产生一个unchecked cast警告
cl = (Class<? extends Set<String>>)
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
}
65.4 最佳实践
- 如果要使用编译时不存在的类,就应该使用反射创建类对象,再通过这个类的、编译时存在的父类或接口,来访问(引用)这个对象
//通过反射,newInstance来创建对象,但后续使用已存在的Set接口,来引用这个对象
Set<String> s = cons.newInstance();
66 谨慎地使用本地方法
66.1 本地方法的缺陷
- 本地方法不安全,所以使用本机方法的应用程序不再对内存损坏错误免疫
- 本地方法与平台相关,所以使用本地方法的应用程序,可移植性会变差
- 本地方法很难调试(debug)
- 使用本地方法时,如果不够小心,还会降低系统性能,因为本地方法的垃圾收集不是自动的,甚至无法追踪本地内存的使用情况,而且在进入和退出本地代码时,需要额外的开销
- 本地方法需要"glue code"(将代码拼在一起),写起来很乏味,读起来也困难
66.2 最佳实践
- 使用本地代码时要三思,很少有必须使用本地方法来提升性能的地方
- 如果必须使用本地方法访问底层资源、当地库(native libraries),也要尽可能缩小使用本地方法的代码,并且进行全面的测试
- 本地代码中只要有一个bug,就能破坏整个应用程序
67 谨慎地进行优化
67.1 性能相关问题
- 要努力编写好的程序,而不是快的
- 必须在设计过程中,就考虑到性能问题
- 使公有的类可变,导致大量不必要的保护性拷贝
- 不使用组合而使用继承,会把子类和超类绑定,限制了子类的性能
- 在API中使用实现类型,而不是接口类型,将客户端与具体实现绑定,将来出现更快的实现也无法使用
- 要努力避免那些限制性能的设计
- 不要为了提升性能而对API进行包装,因为在平台未来发行版本中,或在将来底层软件中不复存在,但被包装的API以及由它引起的问题,会永远困扰着你
- 尽量不要优化
- 每次试图做优化的前后,要对性能进行测量,因为性能可能变得更差了,因为很难猜出系统把性能花费在哪些地方
- 性能剖析工具有助于决定将优化的重心放在哪,通常可以使用JMH来定位性能问题位置
- Java没有很强的性能模型,即各种操作基本开销没有明确定义,因此很难可靠地预测优化后的性能,大量关于性能的说法最终都被证明为半真半假
- 不同JVM实现不同,性能优化时,还要考虑不同JVM、不同硬件平台上的性能间的平衡
67.2 最佳实践
- 不要费力编写快的程序,应该努力编写好的程序,速度会随之而来
- 设计系统时,尤其设计API、交互协议、永久数据格式时,一定要考虑性能问题
- 构建完整个系统后,应测量其性能,如果不够快,使用性能剖析器来定位性能问题所在,然后只对指定部分进行优化
- 先检查使用的算法,再多的底层优化,也无法弥补算法的选择不当
- 每次优化后,重新测量性能,直到满意为止
68 遵守普遍接受的命名规范
很多命名规范写在了The Java Language Specification中,可以大致将命名规范分为两类,字面规范和语法规范
68.1 字面规范
- API如果违反字面规范,会导致使用困难。具体实现违反字面规范,会导致难以维护
- 包和模块的名称
- 层次状,用句号分隔每个部分
- 每个部分都应该由小写字母组成,很少情况下可能还会有数字夹杂其中
- 如果组织编写的包,最后要在组织外使用,那么包名的开头应该是组织的Internet域名,并且顶级域名应该放在前面,例如edu.com、com.google、org.eff
- 标准类库一般不遵守上面的这个规定,一般以java或javax开头
- 用户自己创建包时不应该使用java和javax作为包的开头
- 包的其余部分的命名,一般由一个或多个组件组成,它们能够描述这个包,每一组件一般是一个不超过8个字母的单词或缩写
- 一般鼓励使用有意义的缩写,比如util,而不是utilities
- 或者取首字母的缩写,例如awt
- 大部分包只需要域名+一个组件来命名就可以了,如果包的功能非常大,大到可能需要将功能按包名分层,那么就需要多个组件一同命名
- 例如javax.util.concurrent.atomic是javax.util的子包
- 类、接口、枚举、注解的名称
- 由一个或多个单词组成,每个单词首字母大写
- 尽量避免缩写,除非单词本身是首字母缩略词(例如AIDS就表示艾滋),或者通用缩写(MIN、MAX)
- 对于使用首字母缩略词时,到底是首字母缩写,还是全部缩写,一直有争议,不过只首字母缩写,可以在连续出现多个首字母缩略词时,方便地分辨出一个单词的起始处与结束处,比如HTTPURL和HttpUrl,明显后者可读性更强,马上能知道是Http+Url
- 方法和属性的命名
- 与类的命名规范相同,只不过第一个字母应小写
- 如果首字母缩略词作为方法或属性名中的第一个词,那么这个首字母缩略词,整体都应该小写
- “常量域"应该包含一个或多个大写单词,中间由”_"隔开,例如VALUES、NEGATIVE_INFINITY
- 局部变量
- 和方法、属性相似,也允许缩写,具体如何命名和上下文有关,例如i、denom、houseNum
- 类型参数
- 一般由一个字母组成
- T:表示任意类型
- E:表示集合中的一个元素
- K和V:表示map中的key和value
- X:表示异常
- R:表示方法的返回值
- 表示几个连续的类型参数,可以用T, U, V或T1, T2, T3
68.2 语法规范
- 包:无
- 可被实例化的类、枚举类
- 名词或名词短语命名,例如Thread、PriorityQueue、ChessPiece
- 不可实例化的工具类
- 使用复数名词命名,例如Collectors、Collections
- 接口
- 名词或名词短语
- 或以able、ible结尾的形容词,例如Runnable、Iterable、Accessible
- 注解
- 由于注解的用处特别多,没要求必须使用哪种词性命名
- 执行某个动作的方法
- 动词或动词短语,例如append、drawImage
- 返回boolean的方法
- is开头,少数has开头+名词/名词短语/具有形容词功能的单词或短语,例如isDigit,、isProbablePrime、isEmpty,、isEnabled、hasSiblings
- 返回非boolean或调用者某个属性的方法
- 名词/名词短语,例如size、hashCode
- get开头的动词短语,例如getTime
- 转换对象类型到另一个类型的方法
- toType,例如toString、toArray
- 转换对象的类型成一个视图类型的方法
- asType,例如asList
- 返回包装类型对应的java基本类型的方法
- typeValue,例如intValue
- 静态工厂方法
- item1中已经介绍
- 属性名
- 没有很完善的语法规范,因为设计良好的类,很少会将属性暴露出来
- boolean类型的属性的命名和返回值为boolean的方法命名相似,但省略了开头的is,例如initialized、composite
- 其它类型的属性,一般用名词或名词短语命名,例如height、digits、bodyStyle
- 局部变量与属性语法规范相似,但更弱
68.3 最佳实践
- 应遵守命名规范
- 但如果长期以来养成的习惯用法不符合命名规范,也不要盲目尊崇这些命名规范,还是使用大家公认的做法即可