编写更少的代码!使用 4 个 Apache Commons Lang 类了解代码重用的优点

 

代码重用的优点

在早期的软件开发中,都认为开发人员的生产率与他编写的代码数量成正比。在那时,有这么一个看似合理的标尺:代码最终导致一个大概能用的二进制资产,所以编写很多代码的人必定是在勤奋地工作,以实现一个可用的应用程序。这个标尺似乎也适用于其他行业。处理大量纳税申报的会计,或者做浓咖啡饮料的咖啡师一定是富有生产率的,对吗?他们都为各自的生意带来更多的收入,因为他们都按预期生产了很多的产品。

但是,不久前我们才知道,更多的代码并等不同于高生产率。大量的代码当然表明高度的积极性,但是积极性并不一定等同于进步。每天产生大量错误的纳税申报表的会计确实很积极,但是他们为客户和雇员带来的价值却很少。每天以闪电般的速度制造咖啡,但弄错了订单的咖啡师的确很积极,不过其生产率显然不高。

更多代码可能意味着更多的缺陷

幸好,软件行业普遍接受这样一个观点:太多的代码可能是一件坏事。有两项研究发现,一般的应用程序每 1,000 行代码就包含 20 到 250 个 bug(参见 参考资料)!这个度量被称作缺陷密度(defect density)。据此可得出一个重要的结论:更少的代码意味着更少的缺陷

当然,仍然需要编写代码。虽然应用程序本身不能为我们编写代码,但是现在我们可以借用很多的代码。我们还没有实现业务组件中的重用(例如,开发人员可以重用其他人的 Account 对象),但是平台中的重用已经存在。开源框架和支持代码的激增可以帮助您以尽可能少的代码编写出一个 Account 对象(举个例子)。

例如,Hibernate 和 Spring 在 Java 社区中十分普遍。以 Account 对象为例,着手于 greenfield 开发项目的团队准备构建一个在线订购应用程序(需要一个 Account 对象)。与从头开始编写一个面向对象映射(ORM)框架不同,他们可通过利用 Hibernate 或一个有竞争力的面向对象映射(ORM)框架,并且会因此大大受益。对于应用程序的其他方面也是一样的,例如单元测试(您使用 JUnit,对吗?)或依赖项注入(显然,Spring 是不错的选择)。那就是重用。只是与我们曾经想象的不一样。

通过借用或重用这些框架只需编写更少的代码,从而可以更专注于业务问题。这些框架本身有大量的代码,不过关键的是,您不需要编写或维护它。这正是成功的开源项目的妙处:其他人在为您做这些事,而且他们可能比您更专业。

 

越少越好

更少的代码意味着可以更快地将缺陷更少的软件推向市场。但是,重用很重要,因为它不仅意味着编写更少的代码,而且还意味着可以利用 “群众的智慧”。一些流行的开源框架和工具 — 例如 Hibernate、Spring、JUnit 和 Apache Web server — 正在被全球各地的人在不同的应用程序中使用。这种久经考验、经过测试的软件并不是没有缺陷,但是您可以放心地认为,即使出现问题,也可以很快发现并将其解决,而且无需付出成本。

Apache Commons 项目已经存在多年了,它非常稳定。最新的发行版包含大约 90 个类和将近 1,800 个单元测试。虽然没有公布覆盖信息(当然,有人可能会说这个项目的代码覆盖率比较低),但是数据是最好的说明。每个类基本上有 20 个测试。我敢打赌,对这个项目的代码的测试是很严格的,至少不亚于您测试自己的代码。

 

对象契约

Commons Lang 库带有一套方便的类,它们统称为 builders。在本节中,您将学习如何使用其中一个类来构建 java.lang.Object equals 方法,以帮助减少编写的代码数量。

方法实现的挑战

所有 Java 类都自动继承 java.lang.Object。您可能已经知道,Object 类有 3 个方法通常需要被覆盖:

  • equals
  • hashCode
  • toString

equalshashCode 方法的特殊之处在于,Java 平台的其他方面,例如集合甚至是持久性框架(包括 Hibernate),要依赖于这两个方法的正确实现。

如果您没有实现过 equalshashCode,那么您可能会认为这很简单 — 但是您错了。Joshua Bloch 在 Effective Java 一书(参见 参考资料)以超过 10 页的篇幅论述了实现 equals 方法的特殊之处。如果最终实现 equals 方法,那么还需要实现 hashCode 方法(因为 equals 的契约表明,两个相等的对象必须有相同的散列码)。Bloch 又以 6 页的篇幅解释了 hashCode 方法。也就是说,至少有 16 页关于适当实现两个看上去很简单的方法的详细信息。

实现 equals 方法的挑战在于该方法必须遵从的契约。equals 必须:

  • 具有反射性:
    • 对于某个对象,foo(不为 null),foo.equals(foo) 必须返回 true
  • 具有对称性:
    • 对于对象 foobar(不为 null),如果 foo.equals(bar) 返回 true,那么 bar.equals(foo) 也必须返回 true
  • 具有传递性:
    • 对于对象 foobarbaz(不为 null),如果 foo.equals(bar)truebar.equals(baz)true,那么 foo.equals(baz) 必须也返回 true
  • 具有一致性:
    • 对于对象 foobar,如果 foo.equals(bar) 返回 true,那么无论 equals 方法被调用多少次,equals 方法总是应该返回 true(假设两个对象都没有实际的变化)。
  • 能够适当地处理 null
    • foo.equals(null) 应该返回 false

读到这里或者研读过 Effective Java 之后,您将面临在 Account 对象上适当地实现 equals 方法的挑战。但是请记住前面我就生产率和积极性所说的话。

假设您要为企业构建一个在线 Web 应用程序,这个应用程序越早投入使用,您的企业就能越早赚钱。在此情况下,您还会花几个小时(或者数天)来适当地实现和测试 对象上的 equals 契约吗?— 还是重用其他人的代码?

构建 equals

当实现 equals 方法时,Commons Lang EqualsBuilder 比较有用。这个类很容易理解。实际上,您需要知道它的两个方法:appendisEqualsappend 方法带有两个属性:一个是本身对象的属性,一个是相比较的对象的同一个属性。由于 append 方法返回 EqualsBuilder 的一个实例,因此可以将随后的调用链接起来,比较一个对象所有必需的属性。最后,可以通过调用 isEquals 方法完成这个链。

例如,像清单 1 中那样创建一个 Account 对象:


清单 1. 一个简单的 Account 对象

 

我们需要的 Account 对象很简单,而且是独立的。此时,您可以运行一个快速测试,如清单 2 所示,看看是否可以信赖 equals 的默认实现:


清单 2. 测试 Account 对象的默认 equals 方法

 

在清单 2 中可以看到,我创建了两个相同的 Account 对象,每个对象有它自己的引用(因此 == 将返回 false)。当我想看看它们是否相等时,JUnit 友好地通知我返回的是 false

记住,Java 平台的许多方面都可以利用 equals 方法,包括 Java 语言的集合类。所以,有必要为这个方法实现一个有效的版本。因此,我将覆盖 equals 方法。

记住,equals 契约不适用于 null 对象。而且,两种不同类型的对象(例如 AccountPerson)不能相等。最后,在 Java 代码中,equals 方法显然有别于 == 操作符(还记得吗,如果两个对象有相同 的引用,后者将返回 true;因此,那两个对象必定相等)。两个对象可以相等(并且 equals 返回 true),但是不使用相同的引用。

因此,可以像清单 3 中这样编写 equals 方法的第一个方面:


清单 3. equals 中的快速条件

					
if (this == obj) {
 return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
 return false;
}

 

在清单 3 中,我创建了两个条件,在比较基对象和传入的 obj 参数各自的属性之前,应该验证这两个条件。

接下来,由于 equals 方法带有 Object 类型的参数,所以可以将 obj 参数转换为 Account,如清单 4 所示:


清单 4. 转换 obj 参数

					
Account account = (Account) obj;

 

假设 equals 逻辑到此为止,接下来可以利用 EqualsBuilder 对象。记住,这个对象被设计为使用 append 方法将基对象(this)的类似属性和传入 equals 方法的类型进行比较。由于这些方法可以链接起来,最终可以以 isEquals 方法完成这个链,该方法返回 truefalse。因此,实际上只需编写一行代码,如清单 5 所示:


清单 5. 重用 EqualsBuilder

					
return new EqualsBuilder()。append(this.id, account.id)
  .append(this.firstName, account.firstName)
   .append(this.lastName, account.lastName)
    .append(this.emailAddress, account.emailAddress)
     .append(this.creationDate, account.creationDate)
      .isEquals();

 

合并之后,可以产生如清单 6 所示的 equals 方法:


清单 6. 完整的 equals

现在,重新运行之前失败的测试(参见 清单 2)。应该会成功。

您没有花任何时间来编写自己的 equals。如果您仍然想知道自己如何编写一个适当的 equals 方法,那么只需知道这涉及到很多的条件。例如,清单 7 中是一个非 EqualsBuilder 实现的 equals 方法的一个小片段,它比较 creationDate 属性:


清单 7. 您自己的 equals 方法的一个片段

					
if (creationDate != null ? !creationDate.equals(
   person.creationDate) : person.creationDate != null){
 return false;
}

注意,在这种情况下,虽然可以使用一个三元操作符(ternary)使代码更精确,但是代码更加费解。关键是我本可以编写一系列的条件来比较每个对象的属性的不同方面,或者可以利用 EqualsBuilder(它也会做相同的事情)。您会选择哪种方法?

还需注意,如果您真的想优化自己的 equals 方法,并编写尽可能少的代码(这也意味着维护更少的代码),那么可以利用反射的威力,编写清单 8 中的代码:


清单 8. 使用 EqualsBuilder 的反射 API

					
public boolean equals(Object obj) {
 return EqualsBuilder.reflectionEquals(this, obj);
}

这对于减少代码是否有帮助?

清单 8 的确减少了代码。但是,EqualsBuilder 必须关闭基对象中的访问控制(以便比较 private 字段)。如果在配置 VM 时考虑了安全性,那么这可能失败。而且,清单 8 中使用的反射会影响 equals 方法的运行时性能。但是,从好的方面考虑,如果使用反射 API,当增加新的属性时,就不需要更新 equals 方法(如果不使用反射,则需要更新)。

通过 EqualsBuilder,可以利用重用的威力。它为您提供两种方法来实现 equals 方法。选择哪一种方法由具体情况决定。单行风格比较简单,但是您现在已经理解,这样做并非没有风险。

 

对象的散列

现在,您已经实现一个适当的 equals 方法,而且也没有编写太多的代码,但是别忘了还要覆盖 hashCode。本节展示如何操作。

构建 hashCode

hashCode 方法也有一个契约,但是不像 equals 的契约那样正式。然而,重要的是要理解它。和 equals 一样,结果必须一致。对于对象 foobar,如果 foo.equals(bar) 返回 true,那么 foobarhashCode 方法必须返回相同的值。如果 foobar 不相等,则不要求返回不同的散列码。但是,Javadocs 提到,如果这些对象有不同的结果,那么通常会运行得更好一些。

还需注意,和之前一样,如果没有覆盖它,hashCode 会返回一个看似随机的整数。这是因为底层平台通常会将基对象的地址位置转换成一个整数;虽然如此,但文档中提到这并不是必需的,因此可以改变。无论如何,如果最终覆盖 equals 方法,那么也有必要覆盖 hashCode 方法。(记住,虽然 hashCode 方法看上去是开箱即用的,但是 Joshua Bloch 的 Effective Java 花了 6 页的篇幅讨论如何适当地实现 hashCode 方法)。

Commons Lang 库提供一个 HashCodeBuilder 类,这个类与 EqualsBuilder 几乎是一样的。但是,它不是比较两个属性,而是附加一个属性,以生成遵从我刚才描述的契约的一个整数。

在您的 Account 对象中,覆盖 hashCode 方法,如清单 9 所示:


清单 9. 默认的 hashCode 方法

					
public int hashCode() {
 return 0;
}

由于生成一个散列码时没有什么可以比较的,因此使用 HashCodeBuilder 只需一行代码。重要的是正确地初始化 HashCodeBuilder。构造函数带有两个 int,它使用这两个参数来创建一个散列码。这两个 int 必须是奇数。append 方法带有一个属性,因此,和之前一样,这些方法可以链接起来。最后可以通过调用 toHashCode 方法完成这个链。

根据这些信息,您可以像清单 10 中那样实现一个 hashCode 方法:


清单 10. 用 HashCodeBuilder 实现一个 hashCode 方法

					
public int hashCode() {
 return new HashCodeBuilder(11, 21)。append(this.id)
  .append(this.firstName)
   .append(this.lastName)
    .append(this.emailAddress)
     .append(this.creationDate)
      .toHashCode();
}

注意,我在构造函数中传入了一个 11 和一个 21。这些完全是为该对象随机选择的奇数。打开前面的 AccountTest(参见 清单 2)。添加一个快速检查,以验证如下契约:对于这两个对象,如果 equals 返回 true,那么 hashCode 应该返回相同的数字。清单 11 显示了修改后的测试:


清单 11. 验证 hashCode 关于两个相等对象的契约

 

在清单 11 中,我验证了两个相等的对象具有相同的散列码。接下来,在清单 12 中,我还验证两个不同的 对象具有不同的散列码:


清单 12. 验证 hashCode 关于两个不同的对象的契约

					
@Test
public void verifyAccountDifferentHashCodes(){
 Date now = new Date();
 Account acct1 = new Account(1, "John", "Smith", "john@smith.com", now);
 Account acct2 = new Account(2, "Andrew", "Glover", "ajg@me.com", now);

 Assert.assertFalse(acct1.equals(acct2));
 Assert.assertTrue(acct1.hashCode() != acct2.hashCode());
}

 

如果您出于好奇想自己编写一个 hashCode 方法,应该怎么做呢?记住 hashCode 契约,您可以编写如清单 13 所示的代码:


清单 13. 实现您自己的 hashCode

					
public int hashCode() {
 int result;
 result = (int) (id ^ (id >>> 32));
 result = 31 * result + (firstName != null ? firstName.hashCode() : 0);
 result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
 result = 31 * result + (emailAddress != null ? emailAddress.hashCode() : 0);
 result = 31 * result + (creationDate != null ? creationDate.hashCode() : 0);
 return result;
}

 

不用说,这段代码也可以作为有效的 hashCode 方法,这两个 hashCode 方法您更愿意维护哪一个?哪一个更易于理解?还要注意,清单 13 中如何利用三元操作符语句来避免大量的条件逻辑。您可能会想,Commons Lang 的 HashCodeBuilder 也许可以做类似的事情 — 但更好的是 Commons Lang 的开发人员在维护和测试它。

EqualsBuilder 一样,HashCodeBuilder 有另一个利用反射的 API。如果使用该 API,就不需要手动地用 append 方法添加基对象的每个属性,这样可以得到如清单 14 所示的一个 hashCode 方法:


清单 14. 使用 HashCodeBuilder 的反射 API

					
public int hashCode() {
 return HashCodeBuilder.reflectionHashCode(this);
}

 

和之前一样,由于这个方法在幕后应用 Java 反射,因此当进行安全性调整时,可能破坏该方法的功能,而且性能下降很多。

相对的 comparable

另一个有趣的方法也有一个相当正式的契约,那就是 Comparable 接口的 compareTo 方法。如果要控制特定的对象如何排序,那么这个接口非常重要。在本节中,您将学习如何利用 Commons Lang 的 CompareToBuilder

排序输出

在过去的 Java 编程中您可能已经注意到,对于对象如何按一定的顺序排序有默认的机制,例如 Collections 类的 sort 方法。

例如,清单 15 中的 Collection 未经排序,如果不对它做任何事情,那么这个顺序将一直维持下去:


清单 15. 一个 String 列表

					
ArrayList<String> list = new ArrayList<String>();
list.add("Megan");
list.add("Zeek");
list.add("Andy");
list.add("Michelle");

然而,如果像清单 16 中那样将 list 传递给 Collectionssort 方法,那么将应用默认的排序,在这里是按字母顺序。清单 16 将清单 15 中的名称列表按字母顺序排序,并打印出排序后的结果:


清单 16. 对 String 列表排序

					
Collections.sort(list);

for(String value : list){
 System.out.println("sorted is " + value);
}

清单 17 显示输出:


清单 17. 排序后的 String 列表

					
sorted is Andy
sorted is Megan
sorted is Michelle
sorted is Zeek

当然,这样之所以行得通,是因为 Java String 类实现了 Comparable 接口,因此有 compareTo 方法的一个实现,该方法允许按字母顺序排序。实际上,Java 语言中几乎所有的核心类都实现这个接口。

如果您想允许按不同的方式对一个 Account 集合进行排序 — 例如按 id 或 last name,应该怎么做呢?

当然,首先必须实现 Comparable 接口,然后实现 compareTo 方法。这个方法实际上只能用于自然排序 — 根据对象的属性对对象排序。因此,compareTo 非常类似于 equals 方法,只是通过它可以将一个集合的 Account 按它们的属性排序,排序的顺序与使用 compareTo 方法处理属性的顺序相同。

如果阅读用于实现该方法的文档,您将发现,它非常类似于 equals;也就是说,要正确地实现它比较棘手。(Effective Java 花了 4 页的篇幅讨论这个专题)。到现在,您很可能已经想出这样的模式:利用 Commons Lang。

构建 compareTo

Commons Lang 提供一个 CompareToBuilder 类,它的功能与 EqualsBuilder 几乎一样。它包括一个可链接的 append 方法,最终可以通过 toComparison 方法返回一个 int

因此,首先必须修改 Account 类,以实现 Comparable 接口,如清单 18 所示:


清单 18. 实现 Comparable 接口

					
public class Account implements Comparable {}

接下来,必须实现 compareTo 方法,如清单 19 所示:


清单 19. compareTo 的默认实现

					
public int compareTo(Object obj) {
 return 0;
}

实现这个方法需要分两步。首先,必须将传入的参数的类型转换为需要的类型(在这里是 Account)。然后,利用 CompareToBuilder 比较对象的属性。Commons Lang 文档表明,应该像 equals 方法中那样比较相同的属性;因此,Account 对象的 compareTo 方法看上去应该如清单 20 所示:


清单 20. 使用 CompareToBuilder

					
public int compareTo(Object obj) {
 Account account = (Account) obj;
 return new CompareToBuilder()。append(this.id, account.id)
  .append(this.firstName, account.firstName)
   .append(this.lastName, account.lastName)
    .append(this.emailAddress, account.emailAddress)
     .append(this.creationDate, account.creationDate)
      .toComparison();
}

别忘了,如果您真的想减少自己编写的代码,那么可以总是利用反射风格的 CompareToBuilder API,如清单 21 所示:


清单 21. 使用 CompareToBuilder 的反射 API

					
public int compareTo(Object obj) {
 return CompareToBuilder.reflectionCompare(this, obj);
}

现在,如果需要依赖用于一个 Account 集合的自然排序,那么可以利用 Collections.sort,如清单 22 所示:


清单 22. 为一个可比较的 Account 的列表排序

					
Date now = new Date();
ArrayList<Account> list = new ArrayList<Account>();
list.add(new Account(41, "Amy", "Glover", "ajg@me.com", now));
list.add(new Account(10, "Andrew", "Glover", "ajg@me.com", now));
list.add(new Account(1, "Andrew", "Blover", "ajg@me.com", new Date()));
list.add(new Account(2, "Andrew", "Smith", "b@bb.com", now));
list.add(new Account(0, "Andrew", "Glover", "z@zell.com", new Date()));

Collections.sort(list);

for(Account acct : list){
 System.out.println(acct);
}

这段代码先后根据 id、first name 和 last name 等属性以自然顺序输出对象。因此,排序后的顺序如清单 23 所示:


清单 23. 排序后的 Account 列表

					
new Account(0, "Andrew", "Glover", "z@zell.com", new Date())
new Account(1, "Andrew", "Blover", "ajg@me.com", new Date())
new Account(2, "Andrew", "Smith", "b@bb.com", now)
new Account(10, "Andrew", "Glover", "ajg@me.com", now)
new Account(41, "Amy", "Glover", "ajg@me.com", now)

这种输出的作用则是另一回事。在下一节中,您将看到 Commons Lang 如何帮助您构建可读性更强的结果。

 

对象的字符串表示

ObjecttoString 方法的默认实现返回对象的完全限定名称,后面跟上一个 @ 字符,然后是对象的散列码的值。您可能早就明白,这对于区分不同的对象帮助不大。Commons Lang 有一个方便的 ToStringBuilder 类,这个类可帮助构建可读性更强的 toString 结果。

构建 toString

您可能已经不止一次编写过 toString 方法 — 我就是这样。这些方法并不复杂,编写起来很难出错。但是,它们也可能令人讨厌。由于您的 Account 对象已经依赖于 Commons Lang 库,让我们看看 ToStringBuilder 的实际效果。

ToStringBuilder 与我在前面谈到的其他 3 个类相似。您可以创建它的一个实例,附加一些属性,然后调用 toString。就是这样!

覆盖 toString 方法,添加清单 24 中的代码:


清单 24. 使用 ToStringBuilder

					
public String toString() {
 return new ToStringBuilder(this)。append("id", this.id)。
  .append("firstName", this.firstName)
   .append("lastName", this.lastName)
    .append("emailAddress", this.emailAddress)
     .append("creationDate", this.creationDate)
      .toString();
}

您可以总是利用反射,如清单 25 所示:


清单 25. 使用 ToStringBuilder 的反射 API

					
public String toString() {
 return ToStringBuilder.reflectionToString(this);
}

无论您选择如何使用 ToStringBuilder,调用 toString 总会产生一个可读性更强的 String。例如,看看清单 26 中的对象实例:


清单 26. 一个惟一的 Account 实例

					
new Account(10, "Andrew", "Glover", "ajg@me.com", now);

如清单 27 所示,输出的可读性很好:


清单 27. ToStringBuilder 的输出

					
com.acme.app.Account@47858e[
   id=10,firstName=Andrew,lastName=Glover,emailAddress=ajg@me.com,
   creationDate=Tue Nov 11 17:20:08 EST 2008]

如果您不喜欢对象的这种 String 表示,Commons Lang 库还有一些 helper 类可帮助定制输出。例如,使用 ToStringBuilder 可以在日志文件中一致地显示对象实例。

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值