带你快速看完9.8分神作《Effective Java》—— 通用编程篇(一)

简介: 57 最小化局部变量的作用域58 for-each循环优先于传统的for循环59 了解并使用类库60 若需要精确答案就应避免使用float 和double 类型61 基本类型优先于包装基本类型62 如果其他类型更合适,尽量避免使用字符串63 当心字符串连接的性能问题64 通过接口引用对象65 接口优于反射66 谨慎使用本地方法67 谨慎地进行优化68 遵守被广泛认可的命名约定

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


要使局部变量的作用域最小化,最好的方法是在首次使用的地方声明它。过早地声明局部变量可能导致其作用域不仅过早开始而且结束太晚。



每个局部变量声明都应该包含一个初始化表达式。这个规则的一个例外是try-catch语句。如果该值必须在try块之外使用,那么它必须在try块之前声明,此时它还不能被「合理地初始化」。



循环允许声明循环变量,将其作用域限制在需要它们的确切区域。如果循环终止后不需要循环变量的内容,那么优先选择for循环而不是while循环。


for (Element e : c) {
  ... // Do Something with e
}

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


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


要了解为什么这些for循环优于while循环,考虑以下代码片段:


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,不幸的是,它仍在作用域范围内。生成的代码编译时没有错误,并且在不抛出异常的情况下运行,但是它的逻辑已经错了。



如果将类似的复制粘贴错误与for循环(for-each循环或传统循环)结合使用,则生成的代码就无法编译。


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循环比while循环还有一个优点:它更短,增强了可读性。


下面是另一种对局部变量的作用域最小化的循环做法:


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


它有两个循环变量,i和n,它们都具有完全相同的作用域。第二个变量n用于存储第一个变量的限定值,从而避免了每次迭代中冗余计算的代价。



最后一种“最小化局部变量作用域”的最终技术是保持方法小而集中。如果把两个操作(activities)合并到同一个方法中,与其中一个操作相关的局部变量就有可能会出现在执行另一个操作的代码范围之内。为了防止这种情况发生,只需将方法分为两个:每个操作用一个方法完成。



58 for-each循环优先于传统的for循环


下面是一个传统的for循环来遍历一个集合:

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


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


for (int i = 0; i < a.length; i++) {
  ... // Do something with a[i]
}


它们并不完美。迭代器和索引变量都很混乱,好在for-each循环解决了所有这些问题。它通过隐藏迭代器或索引变量来消除

混乱和出错的机会:


for (Element e : elements) {
  ... // Do something with e
}


此外,使用for-each循环也不会降低性能


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

常犯的一个错误:


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


问题是,对于外部集合(suit),i.next会被调用很多次


下面的代码本意是要打印一对骰子的所有可能的组合:


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个组合。

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


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


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


- 解构过滤

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


- 转换

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


- 并行迭代

如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步进行



for-each循环还允许遍历实现Iterable接口的任何对象


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


59 了解并使用类库


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


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


这个方法有三个缺点:

1. 如果n是较小的2的平方数,随机数序列会在短的时间内重复

2. 如果n不是2的幂,平均而言,一些数字将比其他数字更高概率返回


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


如果运行它,你将发现它输出一个接近666666的数字。随机方法生成的数字中有三分之二落在其范围的前半部分


3. 在极少数情况下会返回超出指定范围的数字,这是灾难性的结果


如果nextInt()返回Integer.MIN_VALUE、Math.abs也会因为越界而返回Integer.MIN_VALUE。假设n不是2的幂,那么求模运算符 (%) 将返回一个负数。


幸运的是,已经有现成的成果可以直接使用:Random.nextInt(int)



通过使用标准库,你可以利用编写它的专家的知识和以前使用它的人的经验。



从Java 7开始,就不应该再使用Random,而是用ThreadLocalRandom,原因有以下几点:


它能产生更高质量的随机数,而且速度非常快


不必浪费时间为那些与你的工作无关的问题编写专⻔的解决方案


随着时间的推移,它们的性能会不断提高


随着时间的推移,它们往往会获得新功能


可以让自己的代码融入主流。这样的代码更容易被开发人员阅读、维护和重用



在每个主要版本中,都会向库中添加许多特性,了解这些新增特性是值得的



每个程序员都应该熟悉java.lang、java.util和java.io的基础知识及其子包


如果你在Java平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源Guava库


60 若需要精确答案就应避免使用float 和double 类型


float和double类型特别不适合进行货币计算,因为不可能将0.1(或10的任意负次幂)精确地表示为float或double。


例如,假设你口袋里有1.03美元,你消费了42美分。你还剩下多少钱?


System.out.println(1.03 - 0.42);

1

你可能认为,只需在打印之前将结果四舍五入就可以解决这个问题,但不幸的是,这种方法并不总是有效。例如,假设你口袋里有一美元,你看到一个架子上有一排好吃的糖果,它们的价格仅仅是10美分,20美分,30美分,以此类推,直到1美元。你每买一颗糖,从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);
}


如果你运行这个程序,你会发现你可以买得起三块糖,你还有0.399999999999999999美元。这是错误的

答案。解决这个问题的正确方法是使用BigDecimal、int或long进行货币计算。



这里是前一个程序的一个简单改版,使用BigDecimal类型代替double。注意,使用BigDecimal的String构造函数而不是它的double构造函数。这是为了避免在计算中引入不准确的值


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


使用BigDecimal有两个缺点:

  1. 与原始算术类型相比很不方便
  2. 速度慢得多


除了使用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");
}

使用BigDecimal的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从8种舍入模式中进行选择。


61 基本类型优先于包装基本类型


自动装箱减少了使用包装类型的繁琐性,但没有减少它的风险。


Java 每个基本类型都有一个对应的引用类型,称为包装类型。与int、double和boolean对应的包装类是Integer、Double和Boolean。


基本类型和包装类型之间有三个主要区别:


两个包装类型实例可以具有相同的值,但这两个实例却是不一样的

包装类型可以是null

基本类型比包装类型更节省时间和空间


考虑下面的比较器,它的设计目的是表示Integer值上的升序数字排序。

Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

这个比较器存在严重缺陷,对于


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


两个Integer实例都表示相同的值(42),所以这个表达式的值应该是0,但它是1,这表明第一个Integer值大于第二个。


i==j表达式对两个对象引用执行比较。如果i和j引用表示相同int值的不同Integer实例,这个比较将返回false,所以将==操作符应用于包装类型几乎都是错误的。



可以通过添加两个局部变量来存储基本类型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");
    }
}

它在计算表达式 i==42 时抛出NullPointerException。原因是在操作中混合使用基本类型和包装类型时,包装类型就会自动拆箱,如果一个空对象引用自动拆箱,那么你将得到一个NullPointerException。


修复这个问题非常简单,只需将i声明为int:


最后在考虑第6条里曾经出现过的代码:


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,变量被反复装箱和拆箱,导致产生明显的性能下降。



什么时候应该使用包装类型呢?


作为集合中的元素、键和值

不能将基本类型放在集合中,因此必须使用包装类型


在参数化类型和方法中,必须使用包装类型作为类型参数

例如,不能将变量声明为ThreadLocal类型,因此必须使用ThreadLocal


在进行反射方法调用时,必须使用包装类型


62 如果其他类型更合适,尽量避免使用字符串


本条目讨论了一些不应该使用字符串的场景:


1. 字符串不适合替代其他值类型

当一段数据从文件、网络或键盘输入到程序时,它通常是字符串形式的。但是这种倾向只有在数据本质上是文本的情况下才合理。


2. 字符串不适合替代枚举类型

如第34条,枚举类型比字符串更适合表示枚举类型的常量。


3. 字符串不适合替代聚合类型

如果一个实体有多个组件,将其表示为单个字符串通常是很不好的。例如:


String compoundKey = className + "#" + i.next();


这种方法有很多缺点。如果用于分隔字段的字符出现在其中一个字段中,可能会导致混乱。要访问各个字段,必须解析字符串,这是缓慢的、冗⻓的、容易出错的过程。


更好的方法是编写一个类来表示聚合,通常是一个私有静态成员类。


4. 字符串不能很好地替代capabilities

例如,考虑线程局部变量机制的设计。这样的机制提供的变量在每个线程中都有自己的值。许多年前,当面临设计这样一个机制的任务时,有人提出了相同的设计,其中客户端提供的字符串键,用于标识每个线程本地变量:


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可以通过用一个不可伪造的键(有时称为capability)替换字符串来修复:


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中基于字符串的两个问题,但是你可以做得更好。


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


通过将ThreadLocal类泛型化,使这个API变成类型安全的:


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



相关文章
|
27天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
28天前
|
安全 Java UED
深入浅出Java多线程编程
【10月更文挑战第40天】在Java的世界中,多线程是提升应用性能和响应能力的关键。本文将通过浅显易懂的方式介绍Java中的多线程编程,从基础概念到高级特性,再到实际应用案例,带你一步步深入了解如何在Java中高效地使用多线程。文章不仅涵盖了理论知识,还提供了实用的代码示例,帮助你在实际开发中更好地应用多线程技术。
43 5
|
1月前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
123 3
|
14天前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
18天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
11天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
11天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
32 3
|
16天前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
34 2
|
21天前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界里,异常是程序运行中不可忽视的“惊喜”。它们可能突如其来,也可能悄无声息地潜伏。掌握异常处理的艺术,意味着你能够优雅地面对程序的不完美,并确保它即使在风雨飘摇中也能继续航行。本文将引导你理解Java异常的本质,探索捕获和处理这些异常的方法,并最终学会如何利用自定义异常为你的代码增添力量。