使用自動代碼生成工具(IDE),Java工程師要注意的那些小瑕疵
譯文【51CTO.com快譯】我在本文中將介紹幾個錯誤,據(jù)我的感受,我們在用Java開發(fā)代碼,尤其是使用集成開發(fā)環(huán)境(IDE)的工具自動生成代碼時經(jīng)常犯這些錯誤。
自動生成代碼很棒,它讓我們少敲打好多代碼,有助于關注領域問題,而不是關注樣本代碼的細枝末節(jié)。然而,有時候,我們并不認真思考IDE為我們生成的代碼,我們留下了應該修復的小瑕疵。我在本文中將逐一介紹這些錯誤。
公共域
IDE在默認情況下認為所有類都是公共類。所以,如果我們要求IntelliJ創(chuàng)建一個新的類,就會出現(xiàn)這種情況:
- public class MyClass {
- }
在我看來,類在默認情況下會有包作用域(package scope),即:
- public class MyClass {
- }
這樣一來,它們在所屬的包之外是不可見的。如果某個時候我們需要更改類的域,就要考慮原因,并且做出便利的設計決定。如果類在默認情況下是公共類,我們就直接拿來使用,從而增加代碼的耦合性。
要避免這個問題,***的辦法就是在我們的IDE中編輯模板。比如在IntelliJ IDEA中,這個選項在偏好設置>編輯器> 文件和代碼模板> 模板>類中。
我們在默認情況下使用包作用域時的***實踐之一就是,它將接口創(chuàng)建為公共接口,但是又不暴露其實現(xiàn)。這樣一來,接口的客戶根本不知道它們使用的實現(xiàn),因為它們甚至無法訪問它,青睞依賴項注入之類的實踐。
公共域這方面的另一個例子是,IntelliJ在默認情況下將變量創(chuàng)建為公共:
- public static final String CONSTANT = "value";
大多數(shù)時候,變量只被擁有它們的那個類使用。請注意這個,或者更改你的模板。
不可變類
這個細節(jié)與自動生產的代碼沒有直接關聯(lián),但是我們在99%的時候忽視了它。通常來說,當我們想要設計某個類是不可變類時,結果就會是這樣子:
- public class MyImmutableClass {
- private final int id;
- private final String name;
- public MyImmutableClass(int id, String name) {
- this.id = id;
- this.name = name;
- }
- public int getId() {
- return id;
- }
- public String getName() {
- return name;
- }
- }
私有字段及最終字段,沒有setter方法,沒有構造函數(shù)初始化。OK,這很好,但是我們遺漏了什么。不妨看看如果我們這么做,會出現(xiàn)什么情況:
- public class MyImmutableDerivedClass extends MyImmutableClass{
- public String address;
- public MyImmutableDerivedClass(int id,
- String name,
- String address) {
- super(id, name);
- this.address = address;
- }
- public String getAddress() {
- return address;
- }
- public void setAddress(String address) {
- this.address = address;
- }
- }
這第二個類的實例(繼承自MyImmutableClass)可以充當它的代理,客戶根本不注意,即:
- MyImmutableClass myImmutableClass
- = new MyImmutableDerivedClass(1, "name", "address");
這并不理想,因為這個派生類不是不可變的! 解決辦法很簡單,我們不妨讓不可變類成為最終類:
- public final class MyImmutableClass {
- //...
- }
現(xiàn)在就不可能創(chuàng)建派生類了。
getter和setter方法
我們常常創(chuàng)建擁有所有字段的類,并且在自動生成的代碼這股熱潮下,為所有字段添加構造函數(shù)、getter方法和setter方法。
我們果真需要這么做嗎?
- public class MyClass {
- private Collaborator1 collaborator1;
- private Collaborator2 collaborator2;
- public MyClass(Collaborator1 collaborator1,
- Collaborator2 collaborator2) {
- this.collaborator1 = collaborator1;
- this.collaborator2 = collaborator2;
- }
- public Collaborator1 getCollaborator1() {
- return collaborator1;
- }
- public void setCollaborator1(Collaborator1 collaborator1) {
- this.collaborator1 = collaborator1;
- }
- public Collaborator2 getCollaborator2() {
- return collaborator2;
- }
- public void setCollaborator2(Collaborator2 collaborator2) {
- this.collaborator2 = collaborator2;
- }
- }
一個類應該暴露最少數(shù)量的內部細節(jié)。所以,前面說過一個類應該有包作用域,直到它需要是公共的,現(xiàn)在我要說除非需要,否則類不該有getter或setter方法。如果出現(xiàn)這種情況,就要想想這是不是***決定,或者你的設計中有沒有哪里是不合適的。
equals方法
通常來說,IDE暴露一起生成equals方法和hashCode方法的選項。這有必要,因為我們通常需要這兩種方法都有一種合適的、一致的實現(xiàn)(我在此不過多的具體解釋,假設我們都知道equals和hashCode契約)。
我不是很反對IDE為hashCode生成的實現(xiàn),它們通常是***的,盡管我們可以進一步對它們加以優(yōu)化。不過,equals實現(xiàn)方面存在一個小問題:
- public class MyDerivedClass extends MyClass {
- private final String address;
- public MyDerivedClass(int id, String name, String address) {
- super(id, name);
- this.address = address;
- }
- public String getAddress() {
- return address;
- }
- // equals and hashCode
- }
這里一切看起來沒問題,但是出現(xiàn)了嚴重的情況:這個類違反了里氏替換原則(Liskov Substitution Principle)。這個原則是SOLID原則的一部分;簡而言之,那就是“派生類可充當基類,客戶不會注意到它。”這個定義有點費勁(這個原則需要用一整篇文章來介紹),于是我會用一個例子來這個問題,那樣我們可以明白為何我們的equals方法有問題。
不妨創(chuàng)建一個派生類:
- public class MyDerivedClass extends MyClass {
- private final String address;
- public MyDerivedClass(int id, String name, String address) {
- super(id, name);
- this.address = address;
- }
- public String getAddress() {
- return address;
- }
- // equals and hashCode
- }
我忽略了equals和hashCode實現(xiàn),因為它們對這個例子來說沒有必要。
- public static void main(String[] args) {
- MyClass object1 = new MyClass(1, "name");
- MyDerivedClass object2 = new MyDerivedClass(1, "name", "address");
- System.out.println(areEquals(object1, object2));
- }
- public static boolean areEquals(MyClass object1, MyClass object2) {
- return object1.equals(object2);
- }
方法areEquals收到MyClass的兩個實例。從含有的信息來看,這兩個實例一模一樣,所以我們認為areEquals會返回true。但實則不然,因為自動生成的equals方法執(zhí)行這個操作:
- if (o == null ||
- getClass() != o.getClass())
- return false;
getClass為***個對象返回MyClass,為第二個對象返回MyDerivedClass,于是我們的方法equals返回false。
我們可以討論這種情況出現(xiàn)的頻率,因為兩者都是不同類的實例。但是,兩個字段中所有字段的值一模一樣,果真有意義嗎?實際上,areEquals的一種替代實現(xiàn)可能是這樣:
- public static boolean areEquals(MyClass object1, MyClass object2) {
- return object1.getId() == object2.getId() &&
- object1.getName().equals(object2.getName());
- }
在這里,結果是true。
這就是里氏替代原則的大致內容。這個原則的影響之一就是,如果我們覆蓋方法,它就會添加某個行為,而不清除基類執(zhí)行的任何操作。顯著違反這個原則的是,覆蓋一個方法讓它是空的這種做法(我猜許多人干過這種事)。
于是,我們需要為方法equals提供一種更好的實現(xiàn)。它應該是這樣子:
- @Override
- public boolean equals(Object o) {
- if (this == o)
- return true;
- if (!(o instanceof MyClass))
- return false;
- MyClass myClass = (MyClass) o;
- if (id != myClass.id)
- return false;
- if (name != null ?
- !name.equals(myClass.name) :
- myClass.name != null)
- return false;
- return true;
- }
不是使用getClass,詢問對象知道其類,我們所做的而是,如果它是類的實例(equals駐留于運算符instanceof),要求對象由參數(shù)傳遞給equals。這個運算符為這個類及其派生類的實例返回true?,F(xiàn)在,我們的areEquals方法終于按我們所需的方式來工作。
事實上,IntelliJ暴露了自動生成equals這個版本的一種方法,但是我們必須認真選擇向導程序的選項,因為它在默認情況下未勾選,我們必須自行勾選。它是“Accept subclasses as parameters to equals() method”:
使用自動代碼生成工具(IDE),要注意這些生成代碼的小瑕疵對話框本身提到,實現(xiàn)在默認情況下違反了equals契約。Instanceof實現(xiàn)似乎與一些框架不兼容。這種情況下,我們應該改用***個版本,但是始終要小心由此帶來的后果。所以,不妨一開始就使用正確的版本,必要時再改用B計劃。
原文標題:Review your auto-generated code
作者:Raúl Ávila
【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】