带你快速看完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();
}



相关文章
|
2天前
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?
|
4天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
3天前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
5天前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
1天前
|
存储 Java
Java编程中的对象序列化与反序列化
【9月更文挑战第12天】在Java的世界里,对象序列化与反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何通过实现Serializable接口来标记一个类的对象可以被序列化,并探索ObjectOutputStream和ObjectInputStream类的使用,以实现对象的写入和读取。我们还将讨论序列化过程中可能遇到的问题及其解决方案,确保你能够高效、安全地处理对象序列化。
|
8天前
|
Java 开发者
【Java编程新纪元】JDK 22:超级构造函数来袭,super(...) 前导语句改写编程规则!
【9月更文挑战第6天】JDK 22的超级构造函数特性是Java编程语言发展史上的一个重要里程碑。它不仅简化了代码编写,还提升了代码的可读性和维护性。我们有理由相信,在未来的Java版本中,还将有更多令人兴奋的新特性等待我们去发现和应用。让我们共同期待Java编程新纪元的到来!
|
8天前
|
Oracle Java 关系型数据库
【颠覆性升级】JDK 22:超级构造器与区域锁,重塑Java编程的两大基石!
【9月更文挑战第6天】JDK 22的发布标志着Java编程语言在性能和灵活性方面迈出了重要的一步。超级构造器和区域锁这两大基石的引入,不仅简化了代码设计,提高了开发效率,还优化了垃圾收集器的性能,降低了应用延迟。这些改进不仅展示了Oracle在Java生态系统中的持续改进和创新精神,也为广大Java开发者提供了更多的可能性和便利。我们有理由相信,在未来的Java编程中,这些新特性将发挥越来越重要的作用,推动Java技术不断向前发展。
|
5天前
|
安全 Java 数据安全/隐私保护
- 代码加密混淆工具-Java 编程安全性
在Java编程领域,保护代码安全与知识产权至关重要。本文探讨了代码加密混淆工具的重要性,并介绍了五款流行工具:ProGuard、DexGuard、Jscrambler、DashO 和 Ipa Guard。这些工具通过压缩、优化、混淆和加密等手段,提升代码安全性,保护知识产权。ProGuard 是开源工具,用于压缩和混淆Java代码;DexGuard 专为Android应用程序设计,提供强大加密功能;Jscrambler 基于云,保护Web和移动应用的JavaScript及HTML5代码;DashO 支持多种Java平台和
20 1
|
5天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
7天前
|
Java 开发者
Java中的多线程编程基础与实战
【9月更文挑战第6天】本文将通过深入浅出的方式,带领读者了解并掌握Java中的多线程编程。我们将从基础概念出发,逐步深入到代码实践,最后探讨多线程在实际应用中的优势和注意事项。无论你是初学者还是有一定经验的开发者,这篇文章都能让你对Java多线程有更全面的认识。
14 1