Effective Java 第九章 通用编程一

200 阅读16分钟

欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

这一章专门讨论Java语言的具体细节。讨论了局部变量、控制结构、类库、数据类型以及两种Java语言之外工具:反射和本地方法。最后,讨论了优化和命名惯例。

57. 最小化局部变量的作用域

这条目在性质上类似于条目 15,即“最小化类和成员的可访问性”。通过最小化局部变量的作用域,可以提高代码的可读性和可维护性,并降低出错的可能性。

较早的编程语言(如C)要求必须在代码块的头部声明局部变量,并且一些程序员继续习惯这样做。 这是一个值得改进的习惯。 作为提醒,Java允许你在任何合法的语句的地方声明变量(as does C, since C99)。

用于最小化局部变量作用域的最强大的技术是再首次使用的地方声明它。 如果变量在使用之前被声明,那就变得更加混乱—— 这也会对试图理解程序的读者来讲,又增加了一件分散他们注意力的事情。 到使用该变量时,读者可能不记得变量的类型或初始值。

过早地声明局部变量可能导致其作用域不仅过早开始而且结束太晚。 局部变量的作用域从声明它的位置延伸到封闭块的末尾。 如果变量在使用它的封闭块之外声明,则在程序退出该封闭块后它仍然可见。如果在其预定用途区域之前或之后意外使用变量,则后果可能是灾难性的。

几乎每个局部变量声明都应该包含一个初始化器。如果还没有足够的信息来合理地初始化一个变量,那么应该推迟声明,直到认为可以这样做。这个规则的一个例外是try-catch语句。如果一个变量被初始化为一个表达式,该表达式的计算结果可以抛出一个已检查的异常,那么该变量必须在try块中初始化(除非所包含的方法可以传播异常)。如果该值必须在try块之外使用,那么它必须在try块之前声明,此时它还不能被“合理地初始化”。例如,参照条目 65中的示例。

循环提供了一个特殊的机会来最小化变量的作用域。传统形式的for循环和for-each形式都允许声明循环变量,将其作用域限制在需要它们的确切区域。 (该区域由循环体和for关键字与正文之间的括号中的代码组成)。因此,如果循环终止后不需要循环变量的内容,那么优先选择for循环而不是while循环

例如,下面是遍历集合的首选方式(条目 58):

// Preferred idiom for iterating over a collection or array
for (Element e : c) {
    ... // Do Something with e
}

如果需要访问迭代器,也许是为了调用它的remove方法,首选的习惯用法,使用传统的for循环代替for-each循环:

// Idiom for iterating when you need the iterator
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // Do something with e and i
}

要了解为什么这些for循环优于while循环,请考虑以下代码片段,其中包含两个while循环和一个bug:

Iterator<Element> i = c.iterator();
while (i.hasNext()) {
    doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) {             // BUG!
    doSomethingElse(i2.next());
}

第二个循环包含一个复制粘贴错误:它初始化一个新的循环变量i2,但是使用旧的变量i,不幸的是,它仍在范围内。 生成的代码编译时没有错误,并且在不抛出异常的情况下运行,但它做错了。 第二个循环不是在c2上迭代,而是立即终止,给出了c2为空的错误印象。 由于程序无声地出错,因此错误可能会长时间无法被检测到。

如果将类似的复制粘贴错误与for循环(for-each循环或传统循环)结合使用,则生成的代码甚至无法编译。第一个循环中的元素(或迭代器)变量不在第二个循环中的作用域中。下面是它与传统for循环的示例:

for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // Do something with e and i
}
...

// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
    Element e2 = i2.next();
    ... // Do something with e2 and i2
}

此外,如果使用for循环,那么发送这种复制粘贴错误的可能性要小得多,因为没有必要在两个循环中使用不同的变量名。 循环是完全独立的,因此重用元素(或迭代器)变量名称没有坏处。 事实上,这样做通常很流行。

for循环比while循环还有一个优点:它更短,增强了可读性。

下面是另一种循环习惯用法,它最小化了局部变量的作用域:

for (int i = 0, n = expensiveComputation(); i < n; i++) {
    ... // Do something with i;
}

关于这个做法需要注意的重要一点是,它有两个循环变量,i和n,它们都具有完全相同的作用域。第二个变量n用于存储第一个变量的限定值,从而避免了每次迭代中冗余计算的代价。作为一个规则,如果循环测试涉及一个方法调用,并且保证在每次迭代中返回相同的结果,那么应该使用这种用法。

最小化局部变量作用域的最终技术是保持方法小而集中。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。

58. for-each循环优于传统for循环

正如在条目 45中所讨论的,一些任务最好使用Stream来完成,一些任务最好使用迭代。下面是一个传统的for循环来遍历一个集合:

// Not the best way to iterate over a collection!
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    ... // Do something with e
}

下面是迭代数组的传统for循环的实例:

// Not the best way to iterate over an array!
for (int i = 0; i < a.length; i++) {
    ... // Do something with a[i]
}

这些习惯用法比while循环更好(条目 57),但是它们并不完美。迭代器和索引变量都很混乱——你只需要元素而已。此外,它们也代表了出错的机会。迭代器在每个循环中出现三次,索引变量出现四次,这使你有很多机会使用错误的变量。如果这样做,就不能保证编译器会发现到问题。最后,这两个循环非常不同,引起了对容器类型的不必要注意,并且增加了更改该类型的小麻烦。

for-each循环(官方称为“增强的for语句”)解决了所有这些问题。它通过隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习惯用法同样适用于集合和数组,从而简化了将容器的实现类型从一种转换为另一种的过程:

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

当看到冒号(:)时,请将其读作“in”。因此,上面的循环读作“对于元素elements中的每个元素e”。“使用for-each循环不会降低性能,即使对于数组也是如此:它们生成的代码本质上与手工编写的代码相同。

当涉及到嵌套迭代时,for-each循环相对于传统for循环的优势甚至更大。下面是人们在进行嵌套迭代时经常犯的一个错误:

// Can you spot the bug?
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()));

如果没有发现这个bug,也不必感到难过。许多专业程序员都曾犯过这样或那样的错误。问题是,对于外部集合(suit),next方法在迭代器上调用了太多次。它应该从外部循环调用,因此每花色调用一次,但它是从内部循环调用的,因此每一张牌调用一次。在suit用完之后,循环抛出NoSuchElementException异常。

如果你真的不走运,外部集合的大小是内部集合大小的倍数——也许它们是相同的集合——循环将正常终止,但它不会做你想要的。 例如,考虑这种错误的尝试,打印一对骰子的所有可能的掷法:

// Same bug, different symptom!
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());

该程序不会抛出异常,但它只打印6个重复的组合(从“ONE ONE”到“SIX SIX”),而不是预期的36个组合。

要修复例子中的错误,必须在外部循环的作用域内添加一个变量来保存外部元素:

/ Fixed, but ugly - you can do better!
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循环,问题就会消失。生成的代码也尽可能地简洁:

// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
    for (Rank rank : ranks)
        deck.add(new Card(suit, rank));

但是,有三种常见的情况是你不能分别使用for-each循环的:

  • 有损过滤(Destructive filtering)——如果需要遍历集合,并删除指定选元素,则需要使用显式迭代器,以便可以调用其remove方法。 通常可以使用在Java 8中添加的Collection类中的removeIf方法,来避免显式遍历。

  • 转换——如果需要遍历一个列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来替换元素的值。

  • 并行迭代——如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步进行(正如上面错误的card和dice示例中无意中演示的那样)。

如果发现自己处于这些情况中的任何一种,请使用传统的for循环,并警惕本条目中提到的陷阱。

for-each循环不仅允许遍历集合和数组,还允许遍历实现Iterable接口的任何对象,该接口由单个方法组成。接口定义如下:

public interface Iterable<E> {
    // Returns an iterator over the elements in this iterable
    Iterator<E> iterator();
}

如果必须从头开始编写自己的Iterator实现,那么实现Iterable会有点棘手,但是如果你正在编写表示一组元素的类型,那么你应该强烈考虑让它实现Iterable接口,甚至可以选择不让它实现Collection接口。这允许用户使用for-each循环遍历类型,他们会永远感激不尽的。

总之,for-each循环在清晰度,灵活性和错误预防方面提供了超越传统for循环的令人注目的优势,而且没有性能损失。 尽可能使用for-each循环优先于for循环。

59. 熟悉并使用Java类库

假设想要生成0到某个上界之间的随机整数。对于这个常见的任务,许多程序员会编写一个类似这样的方法:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}

这个方法可能看起来不错,但它有三个缺陷。 首先,如果n是一个比较小的2的乘方,则随机数的序列将在相当短的时间段后开始重复。 第二个缺陷是,如果n不是2的乘方,平均而言,某些数字会比其他数字出现得更加频繁。 如果n很大,这种效果可能非常明显。 以下程序有力地证明了这一点,该程序在精心选择的范围内生成了100万个随机数,然后打印出有多少个数字落在范围的上半部分:

public static void main(String[] args) {
    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);
}

如果random方法正常工作,程序将打印接近50万的数字,但如果运行它,你会发现它打印的数字接近666,666。random方法生成的三分之二数字落在其范围的上半部分!

random方法的第三个缺陷是,在极少数情况下,它可能会灾难性地失败,返回超出指定范围之外的数字。 这是因为该方法尝试通过调用Math.absrnd.nextInt()返回的值映射到非负整数。 如果nextInt()返回Integer.MIN_VALUE,则Math.abs也会返回Integer.MIN_VALUE,假设n不是2的乘方,取模运算符(%)将返回负数。 这几乎肯定会导致程序失败,并且可能难以重现。

要编写一个纠正这些缺陷的random方法的版本,你必须知道关于伪随机数生成器,数论和二进制补码算法的知识。幸运的是,你不必这样做 —— 它已经为你完成了,就是Random.nextInt(int)方法。你不必关心它如何完成其​​工作的细节(,如果您很好奇,可以研究文档或源代码)。一位具有算法背景的高级工程师花费了大量时间来设计,实现和测试这种方法,然后向该领域的几位专家展示,以确保其正确性。然后,这个类库经过了beta测试,发布,并被数百万程序员广泛使用了近二十年。该方法尚未发现任何缺陷,但如果发现了缺陷,将在下一个版本中修复。通过使用标准类库,可以利用编写类库专家的知识以及在前人使用它的经验。

从Java 7开始,就不应再使用Random了。 对于大多数用途,选择的随机数生成器现在是ThreadLocalRandom。 它产生更高质量的随机数,而且速度非常快。 在我的机器上,它比Random快3.6倍。 对于fork-join池和并行流的应用,请使用SplittableRandoms

使用这些类库的第二个好处是,不必浪费时间为那些与你的工作关联不大的问题上,而去编写专门的解决方案。如果像大多数程序员一样,那么宁愿将时间花在应用程序上,而不是底层内容上。

使用标准类库的第三个优点是,它们的性能会随着时间的推移而不断提高,而你无需付出任何努力。 因为许多人使用它们并且因为它们被用于行业标准基准测试,所以提供这些类库的组织有强烈的动力使它们运行得更快。 多年来,许多Java平台类库都经过重写,有时甚至是重复编写,从而显着提升性能。

使用类库的第四个优点是它们倾向于随着时间的推移不断增加功能。 如果某个类库遗失了某些东西,开发人员社区就会知道它,并且可能会在后续版本中添加缺少的功能。

使用标准库的最后一个好处是,可以将代码放在主流中。这样的代码更容易被开发人员阅读、维护和重用。

鉴于所有这些优点,使用类库设施优先于专门实现似乎是合乎逻辑的,但许多程序员并不这样做。为什么不呢? 也许他们不知道类库工具设施的存在。 在每个主要版本中,都会向类库中添加许多特性,了解这些新增特性是非常值得的。每次有Java平台的主要版本发布时,都会发布一个web页面来描述它的新特性。这些页面非常值得一读[Java8-feat, Java9-feat]。为了强调这一点,假设你想编写一个程序来打印命令行中指定的URL的内容(这大致与Linux系统下curl命令相同)。 在Java 9之前,这段代码有点乏味,但在Java 9中,transferTo方法被添加到InputStream中。 以下是使用此新方法执行此任务的完整程序:

// Printing the contents of a URL with transferTo, added in Java 9
public static void main(String[] args) throws IOException {
    try (InputStream in = new URL(args[0]).openStream()) {
        in.transferTo(System.out);
    }
}

这些类库太大了,以至于无法学习所有文档[Java9-api],但每个程序员都应该熟悉java.langjava.utiljava.io及其子包的基础知识。 可以根据需要获取其他类库的知识。 总结类库设施超出了本条目的范围,这些设施多年来已经发展得非常庞大。

几个类库特别值得一提。 集合Collection框架和流Stream类库(条目4——-48)应该是每个程序员的基本工具包的一部分,java.util.concurrent中的并发实用程序的也应如此。 该软件既包含了用于简化多线程编程任务的高级实用程序,还包括偏底层的原语,以允许专家编写自己的高级并发抽象。 条目 80和81会讨论java.util.concurrent的高级部分。

有时,类库设施可能无法满足你的需求。需求越专门化,发生这种情况的可能性就越大。虽然第一个冲动应该是使用这些类库,但是如果已经了解了它们在某些领域提供的功能,而这些功能不能满足你的需求,那么可以使用另一种实现。任何有限的类库集所提供的功能总是存在漏洞。如果你在Java平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源Guava类库[Guava]。如果无法在任何适当的类库中找到所需的功能,可能别无选择,你只能自己实现了。

总而言之,不要重新发明轮子。 如果需要做一些似乎应该相当常见的事情,那么类库中可能已经有了一个可以满足你需求的工具。 如果有,请使用它; 如果不知道,请检查。 一般来说,类库代码可能比您自己编写的代码更好,并且可能会随着时间的推移而改进。 这并不反映你作为程序员的能力。 规模经济决定了类库代码得到的关注远远超过大多数开发人员可以承担的相同的功能。

60. 需要精确的结果时避免使用float和double类型

float和double类型主要用于科学和工程计算。 它们执行二进制浮点运算,经过精心设计,可在很宽的范围内快速提供准确的近似值。 但是,它们不能提供准确的结果,不应在需要确切结果的地方使用。 float和double类型特别不适合进行货币计算,因为不可能将0.1(或任何其他10的负次方)精确地表示为float或double。

例如,假设你的口袋里有1.03美元,花了42美分。 你还剩多少钱? 以下是试图回答这个问题的天真的代码片段:

System.out.println(1.03 - 0.42);

不幸的是,它输出了0.6100000000000001。这不是个例。假设你口袋里有一美元,你买了9垫圈,每个10美分。你还剩多少零钱?

System.out.println(1.00 - 9 * 0.10);

根据这个程序片段,可以得到0.0999999999999999998美元。

你可能认为,只需在打印之前将结果四舍五入就可以解决这个问题,但不幸的是,这种方法并不总是有效。例如,假设你口袋里有一美元,你看到一个货架上有一排好吃的糖果,它们的价格仅仅是10美分,20美分,30美分,以此类推,直到1美元。你每买一颗糖,从10美分的那颗开始,直到你买不起货架上的下一颗糖。你买了多少糖果,换了多少零钱?这里有一个简单的程序设计来解决这个问题:

// Broken - uses floating point for monetary calculation!
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);
}

如果你运行该程序,会发现你可以买三块糖果,剩下0.3999999999999999美元。 这是错误的答案! 解决此问题的正确方法是使用BigDecimal,int或long进行货币计算

这里是对上面程序的直接转换,使用BigDecimal类型代替double。 请注意,使用BigDecimal的String类型的构造方法,而不是其double类型构造方法。 这是必要的,以避免在计算中引入不准确的值[Bloch05,Puzzle 2]:

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);
}

如果你运行修改后的程序,你会发现可以买到四块糖果,剩下0.00美元。 这是正确的答案。

但是,使用BigDecimal有两个缺点:它没有比使用基本算术类型方便,而且速度要慢得多。 如果你只解决一个简单的问题,后一种缺点是无关紧要的,但前者可能会让你烦恼。

除了使用BigDecimal以外,还可以使用int或long类型,具体取决于所涉及的数量,并自己控制十进制小数点。 在这个例子中,最明显的方法是用美分而不是美元来计算。下面是采用这种方法的简单转换:

public static void main(String[] args) {
    int itemsBought = 0;
    int funds = 100;
    for (int price = 10; funds >= price; price += 10) {
        funds -= price;
        itemsBought++;
    }

    System.out.println(itemsBought + " items bought.");
    System.out.println("Cash left over: " + funds + " cents");
}

总之,对于任何需要精确答案的计算,不要使用float或double。如果希望系统控制十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用BigDecimal。使用BigDecimal的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么不介意自己控制十进制小数点,而且数量不是太大,可以使用int或long。如果数量不超过9位小数,可以使用int;如果不超过18位,可以使用long。如果数量可能超过18位,则使用BigDecimal。

61. 基本类型优于装箱的基本类型

Java是一个由两部分类型组成的系统,一部分由基本类型组成,如int,double和boolean,还有一部分是引用类型,如String和List。 每个基本类型都有一个相应的引用类型,称为装箱基本类型。 对应于int,double和boolean的包装基本类型是Integer,Double和Boolean。

正如条目6中提到的,自动装箱和自动拆箱模糊了基本类型和装箱基本类型之间的区别,但不会消除它们。这两者之间有真正的区别,重要的是要始终意识到你正在使用的是哪一种,并在它们之间仔细选择。

基本类型和包装基本类型之间有三个主要区别。首先,基本类型只有它们的值,而包装基本类型具有与其值不同的标识。换句话说,两个包装基本类型实例可以具有相同的值但不同的引用标识。第二,基本类型只有功能的值(functional value),而每个包装基本类型类型除了对应的基本类型的功能值外,还有一个非功能值,即null。最后,基本类型比包装的基本类型更节省时间和空间。如果你不小心的话,这三种差异都会给你带来真正的麻烦。

考虑下面的比较器,它的设计目的是表示Integer值的升序数字顺序。(回想一下,比较器的compare方法返回一个负数、零或正数,这取决于它的第一个参数是小于、等于还是大于第二个参数)。你不需要在实践中编写这个比较器,因为它实现了Integer的自然排序,但它提供了一个有趣的例子:

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

这个比较器看起来应该工作,也能通过很多测试。 例如,它可以与Collections.sort方法一起使用,以正确排序百万个元素列表,无论列表是否包含重复元素。 但这个比较器存在严重缺陷。 为了说服自己,只需打印naturalOrder.compare(new Integer(42),new Integer(42))的值。 两个Integer实例都表示相同的值(42),因此该表达式的值应为0,但它为1,表示第一个Integer值大于第二个值!

那么问题出在哪里呢?naturalOrder中的第一个测试工作得很好。计算表达式i < j会使i和j引用的整数实例自动拆箱;也就是说,它提取它们的基本类型值。计算的目的是检查得到的第一个int值是否小于第二个int值。但假设是否定的。然后,下一个测试计算表达式i==j,该表达式对两个对象执行引用标识比较。如果i和j引用表示相同整型值的不同Integer实例,这个比较将返回false,比较器将错误地返回1,表明第一个整型值大于第二个整型值。将==操作符应用于装箱的基本类型几乎总是错误的

在实践中,如果你需要一个比较器来描述类型的自然顺序,应该简单地调用comparator . naturalorder()方法,如果自己编写一个比较器,应该使用比较器构造方法,或者对基本类型使用静态compare方法(条目 14)。也就是说,可以通过添加两个局部变量来存储与装箱Integer参数对应的原始int值,并对这些变量执行所有的比较,从而修复了损坏的比较器中的问题。这样避免了错误的引用一致性比较:

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");
    }
}

它不会打印出Unbelievable字符串——但它所做的事情几乎同样奇怪。它在计算表达式i==42时抛出NullPointerException。问题是,i是Integer类型,而不是int类型,而且像所有非常量对象引用属性一样,它的初始值为null。当程序计算表达式i==42时,它是在比较Integer和int之间的关系。 几乎在每种情况下,当在基本类型和包装基本类型进行混合操作时,包装基本类型会自动拆箱。如果对一个null对象进行自动拆箱,那么会抛出NullPointerException。正如这个程序所演示的,它几乎可以在任何地方发生。修复这个问题非常简单,只需将i声明为int而不是Integer就可以了。

最后,考虑第24页条目6中的程序:

// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

这个程序比它原本的速度慢得多,因为它意外地声明了一个局部变量(sum),它是装箱的基本类型Long,而不是基本类型long。程序在没有错误或警告的情况下编译,变量被反复装箱和拆箱,导致观察到的性能下降。

在本条目中讨论的所有三个程序中,问题都是一样的:程序员忽略了基本类型和包装基本类型之间的区别,并承担了后果。在前两个项目中,结果是彻底的失败;第三,严重的性能问题。

那么,什么时候应该使用装箱基本类型呢?它们有几个合法的用途。第一个是作为集合中的元素、键和值。不能将基本类型放在集合中,因此必须使用装箱的基本类型。这是一般情况下的特例。在参数化类型和方法(第5章)中,必须使用装箱基本类型作为类型参数,因为该语言不允许使用基本类型。例如,不能将变量声明为ThreadLocal<int>类型,因此必须使用ThreadLocal<Integer>。最后,在进行反射方法调用时,必须使用装箱基本类型(条目 65)。

总之,只要有选择,就应该优先使用基本类型,而不是装箱基本类型。基本类型更简单、更快。如果必须使用装箱基本类型,则需要小心!自动装箱减少了使用装箱基本类型的冗长,但没有降低使用的危险。当程序使用==操作符比较两个装箱的基本类型时,它会执行引用标识比较,这几乎肯定不是你想要的。当程序执行包含装箱和拆箱基本类型的混合类型计算时,它会执行拆箱,当程序执行拆箱时,会抛出NullPointerException。最后,当程序装箱了基本类型,可能会导致代价高昂且创建了不必要的对象。

62. 当有其他更合适的类型时就不用字符串

字符串被设计用来表示文本,它们在这方面做得很好。因为字符串是如此常见,并且受到开发语言的良好支持,所以很自然地倾向会将字符串用于其他目的,而不是它们设计的原本目的。这一条目讨论了一些不应该使用字符串做的事情。

字符串是其他值类型的不良替代品。当一段数据从文件、网络或键盘输入进入程序时,它通常是字符串形式的。有一种自然的倾向是这样的,但是这种倾向只有在数据本质上是文本的情况下才合理。如果是数值类型,则应将其转换为适当的数值类型,如int、float或BigInteger。如果是“是”或“否”问题的答案,则应将其转换为适当的枚举类型或boolean值。更通常地说,如果有合适的值类型,无论是基本值还是对象引用,都应该使用它;如果没有,你应该编写一个。虽然这条建议似乎很明显,但经常被违反。

字符串是枚举类型的不良替代品。 正如条目 34中所讨论的,枚举使得枚举类型常量比字符串好得多。

字符串是聚合类型的不良替代品。 如果实体具有多个组件,则将其表示为单个字符串通常是个坏主意。 例如,这里是来自真实系统的一行代码——标识符名称已被更改:

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

这种方法有许多缺点。 如果用于分隔属性的字符出现在某个属性中,结果可能会产生混乱。 要访问单个属性,必须解析字符串,这很慢,很乏味且容易出错。 不能提供equals,toString或compareTo方法,但必须接受String类提供的行为。 更好的方法是编写一个类来表示聚合,通常是私有静态成员类(条目 24)。

字符串是功能的不良替代品。 有时,字符串用于授予对某些功能的访问权限。 例如,考虑ThreadLocal的设计。 这样的工具提供了每个线程都有自己值的变量。 从版本1.2开始,Javal类库就有了一个ThreadLocal工具,但在此之前,程序员必须自己动手来实现。 当多年前遇到设计这样一个工具的任务时,几个人独立地想出了相同的设计,其中客户提供的字符串键用于识别每个线程局部变量:

// 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);
}

这种方法的问题是,字符串键表示线程本地变量的共享全局命名空间。为了使这种方法有效,客户端提供的字符串键必须是惟一的;如果两个客户端各自决定为它们的线程本地变量使用相同的名称,它们无意中共享一个变量,这通常会导致两个客户端都失败。而且,安全性很差。恶意客户端可以故意使用与另一个客户端相同的字符串密钥来非法访问另一个客户机端数据。

可以通过用一个不可伪造的键(有时称为功能)替换字符串来修复这个API:

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);
}

虽然这解决了基于字符串的API的这两个问题,但是可以做得更好。不再真正需要静态方法。它们可以变成键上的实例方法,此时不再是线程局部变量的键:而是线程局部变量。此时,顶层类不再做任何事情,可以删除它,并将嵌套类重命名为ThreadLocal:

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

此API不是类型安全的,因为当从线程局部变量中检索它时,必须将值从Object转换为其实际类型。原始的基于字符串的API类型安全是不可能实现的,基于键的API类型安全也是很难实现的,但通过使ThreadLocal成为参数化类(第29项)来使这种API类型安全是一件简单的事情:

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

粗略地说,这是java.lang.ThreadLocal提供的API。 除了解决基于字符串的API的问题之外,它还比任何基于键的API更快,更优雅。

总而言之,当存在或可以编写更好的数据类型时,避免将对象表示为字符串的自然倾向。 使用不当,字符串比其他类型更麻烦,更灵活更差,速度更慢,更容易出错。 字符串通常被滥用的类型包括基本类型,枚举类型和聚合类型。

转自:www.cnblogs.com/IcanFixIt/p…