使用自动代码生成工具(IDE),Java工程师要注意的那些小瑕疵

译文
开发 开发工具
我在本文中将介绍几个错误,据我的感受,我们在用Java开发代码,尤其是使用集成开发环境(IDE)的工具自动生成代码时经常犯这些错误。

【51CTO.com快译】我在本文中将介绍几个错误,据我的感受,我们在用Java开发代码,尤其是使用集成开发环境(IDE)的工具自动生成代码时经常犯这些错误。

自动生成代码很棒,它让我们少敲打好多代码,有助于关注领域问题,而不是关注样本代码的细枝末节。然而,有时候,我们并不认真思考IDE为我们生成的代码,我们留下了应该修复的小瑕疵。我在本文中将逐一介绍这些错误。

公共域

IDE在默认情况下认为所有类都是公共类。所以,如果我们要求IntelliJ创建一个新的类,就会出现这种情况:

  1. public class MyClass { 

在我看来,类在默认情况下会有包作用域(package scope),即:

 

  1. public class MyClass { 

 

这样一来,它们在所属的包之外是不可见的。如果某个时候我们需要更改类的域,就要考虑原因,并且做出便利的设计决定。如果类在默认情况下是公共类,我们就直接拿来使用,从而增加代码的耦合性。

要避免这个问题,***的办法就是在我们的IDE中编辑模板。比如在IntelliJ IDEA中,这个选项在偏好设置>编辑器> 文件和代码模板> 模板>类中。

我们在默认情况下使用包作用域时的***实践之一就是,它将接口创建为公共接口,但是又不暴露其实现。这样一来,接口的客户根本不知道它们使用的实现,因为它们甚至无法访问它,青睐依赖项注入之类的实践。

公共域这方面的另一个例子是,IntelliJ在默认情况下将变量创建为公共:

  1. public static final String CONSTANT = "value"

大多数时候,变量只被拥有它们的那个类使用。请注意这个,或者更改你的模板。

不可变类

这个细节与自动生产的代码没有直接关联,但是我们在99%的时候忽视了它。通常来说,当我们想要设计某个类是不可变类时,结果就会是这样子:

  1. public class MyImmutableClass { 
  2.  
  3.     private final int id; 
  4.     private final String name; 
  5.  
  6.     public MyImmutableClass(int id, String name) { 
  7.         this.id = id; 
  8.         this.name = name; 
  9.     } 
  10.  
  11.     public int getId() { 
  12.         return id; 
  13.     } 
  14.  
  15.     public String getName() { 
  16.         return name; 
  17.     } 

 私有字段及最终字段,没有setter方法,没有构造函数初始化。OK,这很好,但是我们遗漏了什么。不妨看看如果我们这么做,会出现什么情况:

  1. public class MyImmutableDerivedClass extends MyImmutableClass{ 
  2.     public String address; 
  3.     public MyImmutableDerivedClass(int id,  
  4.                                    String name,  
  5.                                    String address) { 
  6.         super(id, name); 
  7.         this.address = address; 
  8.     } 
  9.     public String getAddress() { 
  10.         return address; 
  11.     } 
  12.     public void setAddress(String address) { 
  13.         this.address = address; 
  14.     } 

这第二个类的实例(继承自MyImmutableClass)可以充当它的代理,客户根本不注意,即:

  1. MyImmutableClass myImmutableClass  
  2.  
  3.                 = new MyImmutableDerivedClass(1, "name", "address");  

这并不理想,因为这个派生类不是不可变的! 解决办法很简单,我们不妨让不可变类成为最终类:

  1. public final class MyImmutableClass {  
  2.  
  3.     //...  
  4.  
  5. }  

 现在就不可能创建派生类了。

 

getter和setter方法

我们常常创建拥有所有字段的类,并且在自动生成的代码这股热潮下,为所有字段添加构造函数、getter方法和setter方法。

我们果真需要这么做吗?

  1. public class MyClass { 
  2.  
  3.     private Collaborator1 collaborator1; 
  4.     private Collaborator2 collaborator2; 
  5.  
  6.     public MyClass(Collaborator1 collaborator1, 
  7.                    Collaborator2 collaborator2) { 
  8.         this.collaborator1 = collaborator1; 
  9.         this.collaborator2 = collaborator2; 
  10.     } 
  11.  
  12.     public Collaborator1 getCollaborator1() { 
  13.         return collaborator1; 
  14.     } 
  15.  
  16.     public void setCollaborator1(Collaborator1 collaborator1) { 
  17.         this.collaborator1 = collaborator1; 
  18.     } 
  19.  
  20.     public Collaborator2 getCollaborator2() { 
  21.         return collaborator2; 
  22.     } 
  23.  
  24.     public void setCollaborator2(Collaborator2 collaborator2) { 
  25.         this.collaborator2 = collaborator2; 
  26.     } 

一个类应该暴露最少数量的内部细节。所以,前面说过一个类应该有包作用域,直到它需要是公共的,现在我要说除非需要,否则类不该有getter或setter方法。如果出现这种情况,就要想想这是不是***决定,或者你的设计中有没有哪里是不合适的。

equals方法

通常来说,IDE暴露一起生成equals方法和hashCode方法的选项。这有必要,因为我们通常需要这两种方法都有一种合适的、一致的实现(我在此不过多的具体解释,假设我们都知道equals和hashCode契约)。

我不是很反对IDE为hashCode生成的实现,它们通常是***的,尽管我们可以进一步对它们加以优化。不过,equals实现方面存在一个小问题:

  1. public class MyDerivedClass extends MyClass { 
  2.     private final String address; 
  3.  
  4.     public MyDerivedClass(int id, String name, String address) { 
  5.         super(id, name); 
  6.         this.address = address; 
  7.     } 
  8.  
  9.     public String getAddress() { 
  10.         return address; 
  11.     } 
  12.  
  13.     // equals and hashCode 

 这里一切看起来没问题,但是出现了严重的情况:这个类违反了里氏替换原则(Liskov Substitution Principle)。这个原则是SOLID原则的一部分;简而言之,那就是“派生类可充当基类,客户不会注意到它。”这个定义有点费劲(这个原则需要用一整篇文章来介绍),于是我会用一个例子来这个问题,那样我们可以明白为何我们的equals方法有问题。

不妨创建一个派生类:

 

  1. public class MyDerivedClass extends MyClass { 
  2.     private final String address; 
  3.     public MyDerivedClass(int id, String name, String address) { 
  4.         super(id, name); 
  5.         this.address = address; 
  6.     } 
  7.     public String getAddress() { 
  8.         return address; 
  9.     } 
  10.     // equals and hashCode 

 

我忽略了equals和hashCode实现,因为它们对这个例子来说没有必要。

 

  1. public static void main(String[] args) { 
  2.     MyClass object1 = new MyClass(1, "name"); 
  3.     MyDerivedClass object2 = new MyDerivedClass(1, "name", "address"); 
  4.     System.out.println(areEquals(object1, object2)); 
  5. public static boolean areEquals(MyClass object1, MyClass object2) { 
  6.     return object1.equals(object2); 

 

方法areEquals收到MyClass的两个实例。从含有的信息来看,这两个实例一模一样,所以我们认为areEquals会返回true。但实则不然,因为自动生成的equals方法执行这个操作:

  1. if (o == null || 
  2.     getClass() != o.getClass()) 
  3.         return false; 

getClass为***个对象返回MyClass,为第二个对象返回MyDerivedClass,于是我们的方法equals返回false。

我们可以讨论这种情况出现的频率,因为两者都是不同类的实例。但是,两个字段中所有字段的值一模一样,果真有意义吗?实际上,areEquals的一种替代实现可能是这样:

  1. public static boolean areEquals(MyClass object1, MyClass object2) { 
  2.     return object1.getId() == object2.getId() && 
  3.             object1.getName().equals(object2.getName()); 

 

在这里,结果是true。

这就是里氏替代原则的大致内容。这个原则的影响之一就是,如果我们覆盖方法,它就会添加某个行为,而不清除基类执行的任何操作。显著违反这个原则的是,覆盖一个方法让它是空的这种做法(我猜许多人干过这种事)。

于是,我们需要为方法equals提供一种更好的实现。它应该是这样子:

 

  1. @Override 
  2. public boolean equals(Object o) { 
  3.     if (this == o) 
  4.         return true; 
  5.  
  6.     if (!(o instanceof MyClass)) 
  7.         return false; 
  8.  
  9.     MyClass myClass = (MyClass) o; 
  10.  
  11.     if (id != myClass.id) 
  12.         return false; 
  13.  
  14.     if (name != null ? 
  15.             !name.equals(myClass.name) : 
  16.             myClass.name != null) 
  17.                 return false; 
  18.  
  19.     return true; 

不是使用getClass,询问对象知道其类,我们所做的而是,如果它是类的实例(equals驻留于运算符instanceof),要求对象由参数传递给equals。这个运算符为这个类及其派生类的实例返回true。现在,我们的areEquals方法终于按我们所需的方式来工作。

事实上,IntelliJ暴露了自动生成equals这个版本的一种方法,但是我们必须认真选择向导程序的选项,因为它在默认情况下未勾选,我们必须自行勾选。它是“Accept subclasses as parameters to equals() method”:

使用自动代码生成工具(IDE),要注意这些生成代码的小瑕疵对话框本身提到,实现在默认情况下违反了equals契约。Instanceof实现似乎与一些框架不兼容。这种情况下,我们应该改用***个版本,但是始终要小心由此带来的后果。所以,不妨一开始就使用正确的版本,必要时再改用B计划。

原文标题:Review your auto-generated code

作者:Raúl Ávila

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

 

责任编辑:陶家龙 来源: 51CTO
相关推荐

2009-07-16 11:40:23

ibatis自动生成abator

2023-07-27 18:39:20

低代码开发编码

2022-05-17 08:26:04

API后端

2020-03-19 15:02:53

Go语言学习

2009-07-14 17:12:26

ibatis自动代码生

2013-09-03 09:30:44

软件工程师软件工程师头衔

2021-07-30 16:34:31

前端Nodejs开发

2012-07-24 13:36:58

运维

2014-12-19 09:54:03

Java

2023-12-28 09:54:22

Java内存开发

2013-06-07 13:30:20

2021-01-27 10:35:30

漏洞

2011-08-30 10:09:37

Java

2023-09-15 08:00:20

2019-06-24 09:40:17

前端前端工程师开发工具

2019-02-20 09:35:05

爬虫工程师开发工具

2010-03-17 15:28:31

Java deadlo

2010-10-12 09:51:11

2010-04-25 15:29:58

Twitter可伸缩性

2019-06-23 16:02:12

Kubernetes集群节点高并发
点赞
收藏

51CTO技术栈公众号