编程技能与生活中的许多其他技能一样,并且需要不断提高:如果我们不前进,那么我们将落后。静止不动是一种选择。在“编写更好的Java的技巧4种”系列的第3部分中,我们涵盖了四个重要的主题:(1)使用标准Java库提供的方法来验证自变量,(2)了解重要的Object
类,(3)通过玩游戏进行实验和学习使用jshell,以及(4)在书籍和Java本身的源代码中查找并阅读最能写得很好的代码。
其中一些技术是纯粹的编程技术,可以帮助你进行特定的调整,而其他技术则专注于Java生态系统周围的工具和环境。无论每种技术的个人性质如何,在进行勤奋和合理判断的情况下,每种技术都可以帮助改进开发人员(无论是新手还是专家)编写的Java代码。
1.使用标准方法验证参数
验证输入是任何程序的必然部分。例如,如果我们将对象作为参数传递给方法,并希望在该对象上调用方法,则必须首先验证所提供的对象不为null。此外,我们可能将此对象传递给另一个方法(可能是我们未开发的方法),并且第二个方法可能期望其参数不为null,如果传递了null参数,则会导致错误。
当无效语句的执行可能在程序执行的另一点而不是在提供对象的地方导致错误时,这种情况会更加复杂。更糟的是,在堆栈跟踪中任何地方都找不到原因的情况下,可能会发生错误。例如,如果我们创建一个存储对象的不可变类,并且在另一个线程中调用了使用该对象的方法,则 NullPointerException
在调用线程中可能会抛出(NPE),而没有关于该对象发生在何处的符号。此类的示例如下所示。
由于错误可能发生在与初始赋值完全不同的位置,因此必须在赋值位置验证参数,并且 如果提供了无效的参数,则必须快速执行。为此,我们可以添加一个空检查, 以确保如果将空参数传递到分配位置,则该参数将立即被拒绝并导致抛出NPE:如果提供的参数不为null,则不会引发任何异常,并且该类将正常运行。尽管这是解决问题的一种简单方法,但是当必须验证多个参数时,就会突出其缺陷。例如,如果我们 向构造函数提供 Engine
和 Transmission
对象 Car
,则我们的类将扩展为以下内容:
使用此标准方法,我们的代码的意图更加清楚: 如果提供的Engine
和 Transmission
对象不为null,则存储 它们。该 requireNonNull
方法还足够灵活,可以提供自定义消息,并且具有紧密的表亲 requireNonNullElse
(在JDK 9中可用),可以提供默认值。如果 requireNonNullElse
提供的对象为null,则会返回提供的默认值,而不是抛出NPE。总共,该requireNotNull
方法有三个重载,并且该 方法有两个重载 requireNotNullElse
:
-
requireNonNull(T obj)
:如果提供的对象为null,则抛出NPE -
requireNonNull(T obj, String message)
:如果提供的参数为null,则将NPE与提供的消息一起抛出 -
requireNonNull(T obj, Supplier<String> messageSupplier)
:messageSupplier
如果提供的对象为null,则将NPE与参数生成的消息一起抛出 ;该消息是在引发NPE时生成的;当异常消息的创建成本很高时,应使用此方法(因此,仅当抛出NPE时才应创建) -
requireNonNullElse(T obj, T defaultObj)
:如果提供的对象不为null,则返回提供的对象,否则返回提供的默认值 -
requireNonNullElseGet(T obj, Supplier<? extends T> supplier)
:如果提供的对象不为null或返回默认值,则返回提供的对象;否则返回它;仅在提供的对象为null时才生成默认值;当默认值的创建成本可能很高时,应使用此方法(并且仅当提供的对象为null时才应生成)
JDK 7还包括两个方法 isNull
和nonNull
,与上述方法非常相似,但是,如果提供给它们的对象为null,则分别返回 true
和 false
。每当不需要NPE时都应使用这些基于布尔的方法,并且应使用一些自定义异常或处理逻辑。请注意,Java环境中的惯常行为是,当提供的参数为null(而不是一个IllegalArgumentException
或某些自定义异常)时抛出NPE, 并且应谨慎行事并抛出其他类型的异常。
随着JDK 9的发布,引入了另外三种方法,这些方法允许开发人员检查提供的索引或一组索引是否在范围之内:
-
checkFromIndexSize(int fromIndex, int size, int length)
:如果所提供的(包括)和(不包括)的总和在to (不包括) 的范围内, 则抛出一个IndexOutOfBoundsException
( IOOBE) ;如果有效,则返回 ;此方法对于验证访问n个 元素()(包括)开始 对具有给定值的集合或数组是否有效 是有用的fromIndex
size
0
length
fromIndex
size
fromIndex
length
-
checkFromToIndex(int fromIndex, int toIndex, int length)
:如果提供的fromIndex
(包含的)到提供的toIndex
(排除的)在(排除)的范围内0
, 则抛出IOOBE ;如果有效,则length
返回fromIndex
;此方法对于验证某个范围(包括从fromIndex
到toIndex
)是否有效对给定的集合或数组有效length
-
checkIndex(int index, int length)
:如果提供的index
值小于0
或等于提供的值length
,则抛出IOOBEindex
;如果有效,则 返回 ;该消息对于验证给定index
对于给定的集合或数组有效 是有用的length
我们可以使用这些索引检查方法来确保提供的索引对于给定的对象集合是正确的,如下面的清单所示:
不幸的是,索引检查方法不允许提供自定义异常甚至自定义异常消息。在某些情况下,IOOBE的低抽象级别不适用于应用程序,因此需要更高级别的异常。例如,根据上下文,我们可能不希望Garage
该类的客户端 知道我们将Car
对象存储 在列表中(而不是数据库或某些远程服务),因此,抛出IOOBE可能会显示过多信息或束缚我们的界面与其实现过于紧密。取而代之的是,a NoSuchElementException
可能更合适(如果需要, 可以自定义例外)。
考虑到这些缺点,我们可以针对方法(包括构造函数)参数的空值检查和索引检查设计以下规则:
尽可能使用JDK标准的空检查和索引检查方法。切记,标准索引检查方法抛出的异常的抽象级别可能是不合适的。
2.了解对象类
Java中面向对象的最常见的第一天课程之一是所有类的默认超类:Object
。此类构成整个Java类型层次结构的根,并包括所有Java类型(用户定义的和标准Java库中包含的那些方法)之间共有的方法。尽管这些基础知识在Java开发人员中几乎是通用的,但其中许多细节都是存在的。实际上,即使对于中级和高级Java开发人员来说,许多细节也是无法学习的。
Object
该类总共 有11种方法,这些方法被Java环境中的所有类继承。尽管其中的某些方法(例如)已finalize
被弃用,并且不应被重写或显式调用,但其他方法(例如 equals
和) hashCode
对于Java的日常编程而言却是必不可少的。尽管Object
该类的复杂性 超出了本文的讨论范围,但我们将专注于该最终类中的两个最重要的方法: equals
和 hashCode
。
等于
该 equals
方法在理论上是一种简单的方法,而在实践中则是更为细微的一种。此方法允许两个对象之间的相等性比较,true
如果对象相等false
则返回 , 否则返回。尽管这个概念听起来很简单,但实际上离它还很遥远。例如,不同类型的两个对象可以相等吗?如果两个对象的状态相同,那么它们存储在内存中不同位置(即不同实例)的两个对象是否可以相等?平等如何影响Object
班级的其他方法和特点 ?
默认情况下,true
如果两个实例相等,则equals方法返回 。如果我们看一下该Object#equals
方法的JDK 9实现,这是显而易见的 :
尽管此定义非常简单,但它隐藏了equals方法的一些重要特征。通常,整个Java环境对关于如何对任何 类(包括用户定义的类)实现equals方法进行五个基本假设,并将这些假设记录在Object
该类的文档中 。从上述文档中引用的这些假设如下:
- 这是自反的:对于任何非null的参考值
x
,x.equals(x)
应返回true
。 - 它是对称的:对于任何非null的引用值
x
和y
, 当且仅当x.equals(y)
return时应 返回。true
y.equals(x)
true
- 传递性:对于任何非空的参考值
x
,y
以及z
,如果x.equals(y)
回报率true
和y.equals(z)
回报率true
,那么x.equals(z)
应该返回true
。 - 它是一致的:对于任何非null的引用值
x
和,只要不修改对象的equals比较中使用的信息y
,就可以多次调用x.equals(y)
一致返回true
或一致返回false
。 - 对于任何非null的参考值
x
,x.equals(null)
应返回false
。
应该注意的是,这些限制与true
等值目的混合在一起: 如果两个对象被视为等值, false
否则返回。在大多数情况下,默认的equals实现就足够了,但是在某些情况下,需要更精细的实现。例如,如果我们创建一个不可变的类,则如果该类的两个对象的所有字段都相等,则它们应该相等。实际上,重写equals方法会导致以下实现结构:
- 检查提供的对象是否是此对象
- 检查提供的对象是否与此对象具有相同的类型
- 检查提供的对象的字段是否与此对象的字段相同
例如,如果我们要创建一个不可变的 Exam
班级来记录学生在特定考试中获得的成绩,则可以如下定义班级及其 equals
方法:此实现可确保获得以下结果:
该 equals
方法不仅仅具有令人眼前一亮的功能,中级和高级Java开发人员应通过阅读其官方文档来熟悉此重要方法。可以在Joshua Bloch 第3版equals
的Effective Java(第3版)的第10条(第37-49页)中找到对该方法的深入了解 ,包括与自定义实现相关的许多特质。
hashCode
Object
串联中的第二对 是 hashCode
方法,它生成一个与对象相对应的整数哈希码。在基于哈希的数据结构(例如)中插入时,此哈希代码用作哈希摘要 HashMap
。就像 equals
方法一样,整个Java环境对hashCode
方法的行为进行了假设,但这些假设 并未通过编程方式反映出来:
- 对象的哈希码必须恒定,而分解为哈希码的数据保持不变;通常,这意味着如果对象的状态不变,则该对象的哈希码将保持不变
- 对于根据
equals
方法相等的对象,哈希码必须相等 - 如果两个对象的哈希码根据其方法不相等,则两个对象的哈希码 不必相等
equals
,尽管依赖于哈希码的算法和数据结构通常在不相等的对象导致不相等的哈希码时表现更好
哈希码通常是对象中每个字段的值的一些有序求和。通常通过求和中每个分量的相乘来实现这种排序。如有效Java第三版(pp。52)中所述,选择乘数31:
选择数字31是因为它是奇数质数。如果是偶数且乘法运算溢出,则信息将丢失,因为乘以2等于移位。使用质数的优势尚不清楚,但这是传统的。31的一个不错的特性是乘法可以用移位和减法代替,以在某些体系结构上获得更好的性能:
31 * i = (i << 5) - i
。
例如,实际上,通常使用以下一系列计算来计算一些任意字段集的哈希码:
在代码中,此结果 hashCode
定义类似于以下内容:
为了减少hashCode
为具有多个字段的Objects
类实现该方法的乏味 , 该类包括一个静态方法hash
,该方法 允许将任意数量的值散列在一起:
虽然 Objects#hash
方法减少了杂乱 hashCode
米 ethod并提高其可读性,它并没有提出一个价格:由于该 hash
方法使用可变参数,Java虚拟机(JVM)创建一个数组来保存它的参数和要求的参数拳击比赛中是原始类型。hashCode
综上所述,哈希方法是重写此方法时的理想默认 方法,但是如果需要更好的性能,则应实施手动乘和求和操作或缓存哈希码。最后,由于equals
和 hashCode
方法的共同约束 (即,如果equals
方法返回true
,则哈希码非常相等 ),每当其中一个方法被覆盖时,另一个方法也应被覆盖。
尽管本节涵盖了该Object
班级的许多主要方面 ,但只是在这个重要班级的细微差别上打了水漂。有关更多信息,请查阅官方的Object类文档。总之,应遵守以下规则:
了解
Object
该类:每个类都继承自该类,并且Java环境对其方法怀有很高的期望。覆盖其中一个equals
或多个 时,请务必遵循这些规则,hashCode
并确保不要覆盖一个而不覆盖另一个。
3.使用jshell进行实验
有无数次开发人员对语句或类如何在运行时在其应用程序中工作而感到好奇,而又不想在实际应用程序中尝试它的情况。例如,如果我们使用这些 索引循环,那么该 循环将执行多少次?或者,如果我们使用此 条件, 是否会执行此逻辑?有时,好奇心可能更笼统,例如想知道单个语句的返回值是什么,或者质疑语言功能在实际中的外观(即,如果我为null提供值会 Objects#requireNonNull
怎样?)。
伴随JDK 9中的许多其他重要功能,引入了称为jshell的读取-评估-打印循环(REPL)工具。jshell是一个命令行工具,它允许执行和评估Java语句,并显示语句的结果。要启动jshell(假设 bin/
JDK 9安装目录位于操作系统路径上),只需执行以下 jshell
命令(版本号将取决于计算机上安装的JDK的版本):
一旦 jshell>
提示显示,我们可以在任何可执行的Java语句类型,看看它的评价。请注意,尽管可以根据需要包含单条语句,但不需要尾部分号。例如,我们可以使用jshell中的以下命令查看Java如何将4和5相加:
尽管这可能很简单,但重要的是要认识到我们能够执行Java代码,而无需创建全新的项目和编写样板 public static void main
方法。而且,我们还可以使用jshell执行相对复杂的逻辑,如下所示。
由于jshell是创建整个项目的快速替代方案,因此评估小段代码(编写该代码需要花费几秒钟,而创建可运行项目需要几分钟)变得不再麻烦。例如,如果我们想知道这些Objects#requireNonNull
方法将如何 响应各种参数(技术1),可以尝试使用jshell来查看它们的实际结果,如下所示。
重要的是要注意,尽管我们可以在不使用尾部分号的情况下执行单个语句,但是具有范围的语句(即那些用花括号,单行条件主体等包围的语句)必须包括尾部分号。例如,在类定义的中间省略尾部分号会导致jshell中的语法错误:
尽管jshell的功能远胜于本节中的示例,但其功能的完整介绍不在本文讨论范围之内。好奇的读者可以在《Java Shell用户指南》中找到大量信息。尽管本节中的示例很简单,但是jshell的易用性和强大功能为我们提供了一种方便的技术,可以改善我们用Java开发应用程序的方式:
使用jshell可以评估Java语句或语句组的运行时行为。别害羞:花几秒钟的时间查看语句的 实际 评估方式可以节省数分钟或数小时的时间。
4.阅读书面代码
成为熟练工匠的最好方法之一就是观看工作中经验更丰富的工匠。例如,为了成为一名更好的画家,有抱负的画家可以在视频中观看专业画家的作品(例如Bob Ross 的《绘画的喜悦》),甚至可以研究伦勃朗或莫奈等大师的现有绘画。同样,曲棍球运动员可以学习有关最佳国家曲棍球联盟(NHL)运动员在比赛中如何滑冰或仍如何操作的视频,或者雇用有经验的运动员作为教练。
编程没有什么不同。容易忘记,编程是一种必须磨练的技能,而提高这种技能的最佳方法之一就是着眼于历史上最优秀的程序员。在Java领域,这意味着调查语言的原始设计者如何使用该语言。例如,如果我们希望知道如何编写简洁的代码,则可以查看JDK源代码,并找出Java的发明者如何编写Java代码。(请注意,可以lib/src.zip
在Windows中的标准JDK安装下找到JDK源代码,也可以在任何操作系统上从OpenJDK下载JDK源代码 。)
通过查看特定类的实现,我们还可以获得有关特定类如何工作的大量信息。例如,假设我们担心a Collection
使用该AbstractCollection#remove(Object)
方法如何 删除元素 。无需猜测实现,我们可以直接从源代码中查看实现(如下所示)。
通过简单地看这个方法的源代码,我们可以看到,如果一个空的Object
我已经过去了,以这种方法,第一个空的发现 Collection
(使用 Iterator
的 Collection
)被删除。否则,该 equals
方法将用于查找匹配的元素,如果存在,则将其从中删除Collection
。如果对进行了任何更改Collection
,true
则返回;否则 , 返回。否则, false
返回。虽然我们可以理解什么 的方法和与之相关的JavaDoc做,我们可以看到如何 它是通过直接在源的方法,寻找实现。
除了了解特定方法的工作方式之外,我们还可以了解一些最有经验的Java开发人员如何编写他们的代码。例如,通过查看AbstractCollection#toString
方法,我们可以看到如何有效地连接字符串 :
许多新的Java开发人员可能使用了简单的字符串连接,但是AbstractCollection#toString
(恰好是原始Java先行者之一)的开发人员 决定使用StringBuilder
。这至少应该提出一个问题:为什么?该开发人员是否知道我们不知道的东西?(这很可能是因为在JDK源代码中发现错误或错别字不太普遍。)
但是,应该指出的是,仅仅因为在JDK中以某种方式编写了代码,并不一定意味着在大多数Java应用程序中都以这种方式编写了代码。很多时候,成语被许多Java开发人员所使用,但JDK中却没有(很多JDK代码已经写很久了)。同样,JDK的开发人员可能没有做出正确的决定(甚至有些原始的Java开发人员也承认某些原始的实现是一个错误,但是这些实现在太多不同的应用程序中使用过,无法返回并更改它们)。明智的做法是不要重复这些错误,而应该从中学习。
作为JDK源代码的补充,经验丰富的Java开发人员应阅读尽可能多的著名Java开发人员编写的代码。例如,阅读Martin Fowler在Refactoring中编写的代码可能会让人大开眼界。也许有些编写代码的方法是我们从未想到过的,但对于经验最丰富的从业者来说是常见的。尽管几乎不可能设计出一本完整的清单,其中包含编写最多的代码的书(写得好 是主观的),但一些最著名的书如下:
- 有效的Java
- 清洁代码
- 实用程序员
- 放开!
- 实践中的Java并发
尽管还有无数其他书籍,但这些书籍为基础提供了良好的基础,涵盖了历史上一些最丰富的Java开发人员。就像JDK源代码一样,以上每本书中编写的代码都是按照其作者的特定风格编写的。每个开发人员都是个人,并且每个人都有自己的风格(例如,通过在一行的结尾处打开大括号来发誓,而其他人则要求他们自己全部放在一个新的行上),但重点并不是要在细节上陷入困境。相反,我们应该学习一些最佳的Java开发人员如何编写他们的代码,并渴望编写既简单又可读的代码。
总而言之,可以将该技术简化为以下内容:
尽可能多地阅读由经验丰富的Java开发人员编写的代码。每个开发人员都有自己的风格,每个人都是人,可以做出错误的选择,但是总的来说,开发人员应该模仿许多Java原始作者及其许多多产的实践者编写的代码。
结论
有无数种技术可以提高开发人员的技能水平以及开发人员的代码水平。在“ 编写更好的Java的其他4种技巧”系列的第3期安装中 ,我们介绍了使用Objects
类提供的标准方法来验证方法参数 ,理解 Object
类,尝试jshell以及使用编写最佳的代码来保持对资源的不满足需求。使用这些技术,再加上良好的判断力,可以为从新手到专家的任何水平的开发人员带来更好的Java。