带你快速看完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。


相关文章
|
11天前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
15 2
|
6天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
13天前
|
Java API Apache
Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
【10月更文挑战第29天】Java编程如何读取Word文档里的Excel表格,并在保存文本内容时保留表格的样式?
66 5
|
8天前
|
安全 Java 编译器
JDK 10中的局部变量类型推断:Java编程的简化与革新
JDK 10引入的局部变量类型推断通过`var`关键字简化了代码编写,提高了可读性。编译器根据初始化表达式自动推断变量类型,减少了冗长的类型声明。虽然带来了诸多优点,但也有一些限制,如只能用于局部变量声明,并需立即初始化。这一特性使Java更接近动态类型语言,增强了灵活性和易用性。
90 53
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
6天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
18 2
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
33 1
|
11天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####