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

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

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


不要使用字符串连接操作符合并多个字符串



字符串连接操作符(+) 是将几个字符串组合成一个字符串的简便方法。


为了连接n个字符而重复地使用字符串连接操作符,需要O(n^2)的时间。因为字符串是不可变的,当两个字符串被连接在一起时,它们的内容都要被拷贝。


例如,下面代码将每个账单项目重复连接到一行来构造账单语句的字符串表示:


public String statement() {
  String result = "";
  for (int i = 0; i < numItems(); i++)
    result += lineForItem(i); // String concatenation
  return result;
}

如果项的数量很大,则该方法的执行时间就难以估算。要获得更好的性能,可以使用StringBuilder代替String


public String statement() {
  StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
  for (int i = 0; i < numItems(); i++)
    b.append(lineForItem(i));
  return b.toString();
}


第二个方法预先分配了一个足够大的StringBuilder来保存整个结果,从而消除了自动增⻓的需要。即使使用默认大小的StringBuilder,它仍然比第一个方法快很多。



64 通过接口引用对象


如果存在合适的接口类型,那么应该使用接口类型声明参数、返回值、变量和字段。惟一真正需要引用对象的类的时候是使用构造函数创建它的时候。考虑LinkedHashSet的情况,它是Set接口的一个实现:

Set<Son> sonSet = new LinkedHashSet<>();

而不是这样:


LinkedHashSet<Son> sonSet = new LinkedHashSet<>();


如果你养成了使用接口作为类型的习惯,那么你的程序将更加灵活。如果你决定要切换实现,只需在构造函数中更改类名(或使用不同的静态工厂)。例如,第一个声明可以改为:


Set<Son> sonSet = new HashSet<>();

所有的代码都会继续工作。周围的代码不知道旧的实现类型,所以它不会在意更改。



有一点值得注意:如果原实现提供了接口的通用约定不需要的一些特殊功能,并且代码依赖于该功能,那么新实现提供相同的功能就非常重要。例如,如果围绕第一个声明的代码依赖于LinkedHashSet的排序策略,那么在声明中将HashSet替换为LinkedHashSet将是不正确的,因为HashSet不保证迭代顺序。



为什么要更改实现类型呢?


因为第二个实现比原来的实现提供了更好的性能,或者因为它提供了原来的实现所缺乏的理想功能。例如,假设一个字段包含一个HashMap实例。将其更改为EnumMap将为迭代提供更好的性能和与键的自然顺序。


将HashMap更改为LinkedHashMap将提供可预测的迭代顺序,性能与HashMap相当,而不需要对键类型作出任何特殊要求。



如果没有合适的接口存在,那么用类引用对象也可以。如String和BigInteger。


值类很少在编写时考虑到多个实现。它们通常是final的,很少有相应的接口。使用这样的值类作为参数、变量、字段或返回类型非常合适。



没有合适接口类型的第二种情况是属于框架的对象,框架的基本类型是类而不是接口。如果一个对象属于这样一个基于类的框架,那么最好使用相关的基类来引用它,这通常是抽象的,而不是使用它的实现类。在java.io类中许多诸OutputStream之类的就属于这种情况。



没有合适接口类型的最后一种情况是,实现接口但同时提供接口中不存在的额外方法的类,例如,PriorityQueue有一个在Queue接口上不存在的comparator方法。


如果没有合适的接口,就使用类层次结构中提供必要功能的最小具体类来引用对象~~



65 接口优于反射


核心反射机制java.lang.reflect提供对任意类的编程访问。给定一个Class对象,你可以获得Constructor、Method和Field实例,分别代表了该Class实例所表示的类的构造器、方法和字段。


通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的字段。


然而,这种能力是有代价的:


1. 失去了编译时类型检查的优势

如果一个程序试图反射性地调用一个不存在的或不可访问的方法,它将在运行时失败


2. 执行反射访问所需的代码既笨拙又冗⻓


3. 性能降低

反射方法调用比普通方法调用慢得多



如果是非常有限的形式使用反射,则可以获得反射的许多好处,同时花费的代价很少。如果代码必须用到在编译时无法获取到的类,却在编译时存在一个适当的接口或超类来引用该类,就可以用反射方式创建实例,并通过它们的接口或超类正常地访问它们。


例如,这是一个创建Set<String>实例的程序,类由第一个命令行参数指定。程序将剩余的命令行参数插入到集合中并打印出来。注意,打印这些参数的顺序取决于第一个参数中指定的类。如果你指定java.util.HashSet,它们显然是随机排列的;如果指定了java.util.TreeSet,则是按字⺟顺序打印的


public static void main(String[] args) {
  // Translate the class name into a Class object
  Class<? extends Set<String>> cl = null;
  try {
    cl = (Class<? extends Set<String>>) // Unchecked cast!
    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);
}


这个例子反映了反射的两个缺点:


1. 该示例可以在运行时生成6个不同的异常,如果不使用反射的方式实例化,所有这些异常都将是编译时错误


2. 根据类的名称生成类的实例需要25行冗⻓的代码,而构造函数调用只需要一行


通过捕获ReflectiveOperationException(Java 7中引入的各种反射异常的超类),可以减少代码的⻓度



如果要变写一个包,它运行时必须依赖其他某个包的多个版本,这种做法可能非常有用。具体做法就是,在支持包所需要最老版本下对它进行编译,然后以反射的方式访问任何更加新的类或方法。



66 谨慎使用本地方法


Java本地接口(JNI)允许Java程序调用本地方法,它们提供了“访问特定于平台的机制”,比如注册表。本地方法还可以通过本地语言,编写应用程序中注重性能的部分,以提高性能。


使用本地方法访问特定于平台的机制是合法的,但是很少有必要:随着Java平台的成熟,它提供了对许多以前只能在宿主平台中上找到的特性。


使用本地方法来提高性能的做法也不值得提倡。原因是JVM变得越来越快了。对于大多数任务,现在可以在Java中获得类似本地方法的性能。


在Java 1.1中添加了java.math,里面的BigInteger是在一个用C编写的快速多精度运算库的基础上实现的。在当时,为了获得足够的性能这样做是必要的。在Java 3中,BigInteger则完全用Java重写了,并且进行了性能调优,新的版本比原来的版本更快。


使用本地方法有严重的缺点:


1. 本地语言是不安全的。所以使用本地方法的应用程序可能会受到内存毁坏的影响


2. 本地语言比Java更依赖于平台,因此使用本地方法的程序的可移植性较差


3. 使用本地方法的代码更难调试


4. 本地方法可能会降低性能,

因为垃圾收集器无法自动跟踪本地内存使用情况


5. 进出本地代码时会产生额外的开销


6. 本地方法需要「粘合代码」,很难阅读,并且编写起来很乏味



67 谨慎地进行优化


有三条关于优化的格言是每个人都应该知道的:


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

—William A. Wulf


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

—Donald E. Knuth


在优化方面,我们应该遵守两条规则:

规则1:不要进行优化。

规则2(仅针对专家):还是不要进行优化,也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。

—M. A. Jackson


它们告诉我们关于优化的一个深刻的事实:很容易弊大于利,尤其是不成熟的优化。


我们应该努力编写好的程序,而不是快速的程序,如果一个好的程序不够快,它的架构将允许它被优化。好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策集中在单个模块中,因此可以在不影响系统其余部分的情况下更改单个决策。


这并不意味着在程序完成之前可以忽略性能问题,而是必须在设计过程中考虑性能。



尽量避免限制性能的设计决策:设计中最难以更改的组件是那些指定组件之间以及与外部世界的交互的组件。最主要的是API、交互层协议和永久数据格式。



要考虑API设计决策的性能结果:


使public类成为可变的,可能需要大量不必要的保护性拷贝

在适合复合模式的public类里使用继承,会把该类永远绑定到它的超类,这会人为地限制子类的性能

在API中使用实现类而不是接口,会将你束缚在一个具体的实现上,即使将来可能会编写更快的实现也无法使用


以java.awt.Component中的getSize方法为例,这个注重性能的方法将返回Dimension实例,Dimension实例是可变的,这就使得该方法的任何实现在每次调用时分配一个新的Dimension实例。


存在几种API设计的替代方案:


Dimension应该是不可变的

用两个方法替换getSize,他们分别返回Dimension对象的单个基本组件


为了获得良好的性能而改变API是一个非常糟糕的想法,因为导致你改变API的性能问题,可能在平台或其他底层软件的未来版本中消失,但是改变的API和随之而来的问题将永远伴随着你。



在每次尝试优化前后都要测试性能。性能分析工具可以帮助你决定将优化工作的重点放在哪里。这些工具可以提供运行时信息;另一类工具是jmh,它是一个微基准测试框架,提供了对Java代码性能详情的预测性。


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


Java平台有一组完善的命名约定,其中许多约定包含在了《The Java Language Specification》中,这些命名大致分为两类:字面的和语法的。


1. 包名和模块名应该是分层的,组件之间用句点分隔

每个部分都包括小写字⺟,很少使用数字。包名就是公司或组织的域名颠倒,例如:edu.nju、com.google、org.eff。


包名的其余部分应该由描述包的一个或多个组件组成,通常不超过8个字符。鼓励使用有意义的缩写,例如util(utilities)。缩写词也行,例如awt。



2. 类、接口、枚举、注释名称,应该由一个或多个单词组成,每个单词的首字⺟大写

例如List或FutureTask。对于首字母缩写,到底应该全部大写还是只有首字母大写,没有统一的说法。但还是强烈推荐只有首字母大写,比如HttpUrl就比HTTPURL清晰。



3. 方法和字段名的第一个字⺟应该小写

例如remove或ensureCapacity。如果首字母缩写组成的单词是一个方法或字段名的第一个单词,它应该是小写的。



4. 常量字段的名称应该由一个或多个大写单词组成,由下划线分隔

例如VALUES或NEGATIVE_INFINITY。



5. 局部变量名允许使用缩写,也允许使用单个字符和短字符序列,它们的含义取决于它们出现的上下文

例如i、denom、houseNum



6. 类型参数名通常由单个字⺟组成

最常⻅的是以下五种类型之一:T表示任意类型,E表示集合的元素类型,K和V表示Map的键和值类型,X表示异常。函数的返回类型通常为R。任意类型的序列可以是T、U、V或T1、T2、T3。



上面所有的讨论可以总结如下表:



0.png


语法命名约定比排版约定更灵活,也更有争议。


1. 可实例化的类,包括枚举类型,通常使用一个或多个名词短语来命名

例如Thread、PriorityQueue或ChessPiece



2. 不可实例化的类通常使用复数名词来命名

例如collector或Collections



3. 接口的名称类似于类

例如集合或比较器,或者以able或ible结尾的形容词,例如Runnable、Iterable或Accessible



4. 注解类型有很多的用途,所以名词、动词、介词和形容词都很常⻅

例如BindingAnnotation、Inject、ImplementedBy或Singleton



5. 执行某些操作的方法通常用动词或动词短语(包括对象)命名

例如append或drawImage



6. 返回布尔值的方法的名称通常以单词is或has(通常很少用)开头,后面跟一个名词、一个名词短语,或者任何用作形容词的单词或短语

例如isDigit、isProbablePrime、isEmpty、isEnabled或hasSiblings



7. 返回被调用对象的非布尔函数或属性的方法通常使用名词短语、以get开头的名词或动词短语来命名

例如size、hashCode或getTime



8. 转换对象类型的实例方法通常称为toType

例如toString或toArray



9. 返回与接收对象类型不同的视图的方法通常称为asType

例如asList



10. 返回与调用它们的对象具有相同值的基本类型的方法通常称为类型值

例如intValue



11. 静态工厂的常⻅名称

from、of、valueOf、instance、getInstance、newInstance、getType和newType


字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的API包含很少的公开字段。类型为boolean的字段的名称通常类似于boolean访问器方法,省略了初始值「is」,例如initialized、composite。其他类型的字段通常用名词或名词短语来命名,如height、digits和bodyStyle。


相关文章
|
1天前
|
安全 Java 程序员
Java并发编程:理解并应用ReentrantLock
【4月更文挑战第30天】 在多线程的世界中,高效且安全地管理共享资源是至关重要的。本文深入探讨了Java中的一种强大同步工具——ReentrantLock。我们将从其设计原理出发,通过实例演示其在解决并发问题中的实际应用,以及如何比传统的synchronized关键字提供更灵活的锁定机制。文章还将讨论在使用ReentrantLock时可能遇到的一些挑战和最佳实践,帮助开发者避免常见陷阱,提高程序性能和稳定性。
|
1天前
|
缓存 Java 调度
Java并发编程:深入理解线程池
【4月更文挑战第30天】 在Java并发编程中,线程池是一种重要的工具,它可以帮助我们有效地管理线程,提高系统性能。本文将深入探讨Java线程池的工作原理,如何使用它,以及如何根据实际需求选择合适的线程池策略。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】 本文将深入探讨Java中的线程池,解析其原理、使用场景以及如何合理地利用线程池提高程序性能。我们将从线程池的基本概念出发,介绍其内部工作机制,然后通过实例演示如何创建和使用线程池。最后,我们将讨论线程池的优缺点以及在实际应用中需要注意的问题。
|
1天前
|
Java 大数据 数据库连接
java编程的优点
【4月更文挑战第30天】java编程的优点
4 0
|
1天前
|
存储 安全 Java
【亮剑】Java并发编程中的四个关键字:ThreadLocal、Volatile、Synchronized和Atomic
【4月更文挑战第30天】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制。`ThreadLocal`为每个线程提供独立变量副本;`Volatile`确保变量可见性,但不保证原子性;`Synchronized`实现同步锁,保证单线程执行;`Atomic`类利用CAS实现无锁并发控制。理解其原理有助于编写高效线程安全代码。根据业务场景选择合适机制至关重要。
|
1天前
|
安全 Java API
Java 8新特性概述及其对编程实践的影响
【4月更文挑战第30天】本文将详细讨论Java 8的新特性,包括Lambda表达式、Stream API以及Optional类等,并探讨这些新特性如何改变了Java编程的实践。我们将通过实例代码展示这些新特性的用法,并分析其对提高代码可读性和编写效率的影响。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】本文将深入探讨Java并发编程中的一个重要主题——线程池。我们将从线程池的基本概念入手,了解其工作原理和优势,然后详细介绍如何使用Java的Executor框架创建和管理线程池。最后,我们将讨论一些高级主题,如自定义线程工厂和拒绝策略。通过本文的学习,你将能够更好地理解和使用Java的线程池,提高你的并发编程能力。
|
1天前
|
存储 安全 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第30天】在Java开发中,并发编程是一个复杂而又关键的领域。它允许多个线程同时执行,从而提高程序性能和资源利用率。然而,并发编程也带来了许多挑战,如数据不一致、死锁和线程安全问题。本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化策略。我们将通过实例分析如何在保证线程安全的同时提高程序性能,为Java开发者提供实用的指导。
|
1天前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第30天】本文将深入探讨Java并发编程的核心概念,包括线程安全、同步机制、锁优化以及性能调优。我们将通过实例分析如何确保多线程环境下的数据一致性,同时介绍一些常见的并发模式和最佳实践,旨在帮助开发者在保证线程安全的同时,提升系统的性能和响应能力。