Java法式員常犯的五個毛病。本站提示廣大學習愛好者:(Java法式員常犯的五個毛病)文章只能為提供參考,不一定能成為您想要的結果。以下是Java法式員常犯的五個毛病正文
上面針對每個毛病用文字解釋聯合代碼詳解的方法展現給年夜家,詳細內容以下:
1. Null 的過度應用
防止過度應用 null 值是一個最好理論。例如,更好的做法是讓辦法前往空的 array 或許 collection 而不是 null 值,由於如許可以避免法式拋出 NullPointerException。上面代碼片斷會從另外一個辦法取得一個聚集:
List<String> accountIds = person.getAccountIds(); for (String accountId : accountIds) { processAccount(accountId); }
當一個 person 沒有 account 的時刻,getAccountIds() 將前往 null 值,法式就會拋出 NullPointerException 異常。是以須要參加空檢討來處理這個成績。假如將前往的 null 值調換成一個空的 list,那末 NullPointerException 也不會湧現。並且,由於我們不再須要對變量 accountId 做空檢討,代碼將變得加倍簡練。
當你想防止 null 值的時刻,分歧場景能夠采用分歧做法。個中一個辦法就是應用 Optional 類型,它既可所以一個空對象,也能夠是一些值的封裝。
Optional<String> optionalString = Optional.ofNullable(nullableString); if(optionalString.isPresent()) { System.out.println(optionalString.get()); }
現實上,Java8 供給了一個更簡練的辦法:
Optional<String> optionalString = Optional.ofNullable(nullableString); optionalString.ifPresent(System.out::println);
Java 是從 Java8 版本開端支撐 Optional 類型,然則它在函數式編程世界早已廣為人知。在此之前,它曾經在 Google Guava 中針對 Java 的晚期版本被應用。
2. 疏忽異常
我們常常對異常置之度外。但是,針對初學者和有經歷的 Java 法式員,最好理論還是處置它們。異常拋出平日是帶有目標性的,是以在年夜多半情形下須要記載惹起異常的事宜。別小視這件事,假如需要的話,你可以從新拋出它,在一個對話框中將毛病信息展現給用戶或許將毛病信息記載在日記中。至多,為了讓其它開辟者知曉來龍去脈,你應當說明為何沒有處置這個異常。
selfie = person.shootASelfie(); try { selfie.show(); } catch (NullPointerException e) { // Maybe, invisible man. Who cares, anyway? }
強調某個異常不主要的一個輕便門路就是將此信息作為異常的變量名,像如許:
try { selfie.delete(); } catch (NullPointerException unimportant) { }
3. 並發修正異常
這類異常產生在聚集對象被修正,同時又沒有應用 iterator 對象供給的辦法去更新聚集中的內容。例如,這裡有一個 hats 列表,並想刪除個中一切含 ear flaps 的值:
List<IHat> hats = new ArrayList<>(); hats.add(new Ushanka()); // that one has ear flaps hats.add(new Fedora()); hats.add(new Sombrero()); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hats.remove(hat); } }
假如運轉此代碼,ConcurrentModificationException 將會被拋出,由於代碼在遍歷這個聚集的同時對其停止修正。當多個過程感化於統一列表,在個中一個過程遍歷列表時,另外一個過程試圖修正列表內容,異樣的異常也能夠會湧現。
在多線程中並發修正聚集內容長短經常見的,是以須要應用並發編程中經常使用的辦法停止處置,例好像步鎖、關於並發修正采取特別的聚集等等。Java 在單線程和多線程情形下處理這個成績有渺小的差異。
搜集對象並在另外一個輪回中刪除它們
直接的處理計劃是將帶有 ear flaps 的 hats 放進一個 list,以後用另外一個輪回刪除它。不外這須要一個額定的聚集來寄存將要被刪除的 hats。
List<IHat> hatsToRemove = new LinkedList<>(); for (IHat hat : hats) { if (hat.hasEarFlaps()) { hatsToRemove.add(hat); } } for (IHat hat : hatsToRemove) { hats.remove(hat); }
應用 Iterator.remove 辦法
這個辦法更簡略,同時其實不須要創立額定的聚集:
Iterator<IHat> hatIterator = hats.iterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); } }
應用 ListIterator 的辦法
當須要修正的聚集完成了 List 接口時,list iterator 長短常適合的選擇。完成 ListIterator 接口的 iterator 不只支撐刪除操作,還支撐 add 和 set 操作。ListIterator 接話柄現了 Iterator 接口,是以這個例子看起來和 Iterator 的 remove 辦法很像。獨一的差別是 hat iterator 的類型和我們取得 iterator 的方法——應用 listIterator() 辦法。上面的片斷展現了若何應用 ListIterator.remove 和 ListIterator.add 辦法將帶有 ear flaps 的 hat 調換成帶有sombreros 的。
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.remove(); hatIterator.add(sombrero); } }
應用 ListIterator,挪用 remove 和 add 辦法可調換為只挪用一個 set 辦法:
IHat sombrero = new Sombrero(); ListIterator<IHat> hatIterator = hats.listIterator(); while (hatIterator.hasNext()) { IHat hat = hatIterator.next(); if (hat.hasEarFlaps()) { hatIterator.set(sombrero); // set instead of remove and add } }
應用Java 8中的 stream 辦法
在 Java8 中,開辟人員可以將一個 collection 轉換為 stream,而且依據一些前提過濾 stream。這個例子講述了 stream api 是若何過濾 hats 和防止 ConcurrentModificationException 。hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))
.collect(Collectors.toCollection(ArrayList::new));
Collectors.toCollection 辦法將會創立一個新的 ArrayList,它擔任寄存被過濾失落的 hats 值。假如過濾前提過濾失落了年夜量條目,這裡將會發生一個很年夜的 ArrayList。是以,須要謹嚴應用。
應用 Java 8 中的 List.removeIf 辦法
可使用 Java 8 中另外一個更簡練清楚明了的辦法—— removeIf 辦法:
hats.removeIf(IHat::hasEarFlaps);
在底層,它應用 Iterator.remove 來完成這個操作。
應用特別的聚集
假如在一開端就決議應用 CopyOnWriteArrayList 而不是 ArrayList ,那就不會湧現成績。由於 CopyOnWriteArrayList 供給了修正的辦法(例如 set,add,remove),它不會去轉變原始聚集數組,而是創立了一個新的修正版本。這就許可遍歷本來版本聚集的同時停止修正,從而不會拋出 ConcurrentModificationException 異常。這類聚集的缺陷也異常顯著——針對每次修正都發生一個新的聚集。
還有其他實用於分歧場景的聚集,好比 CopyOnWriteSet 和 ConcurrentHashMap 。
關於另外一個能夠能夠在並發修正聚集時發生的毛病是,從一個 collection 創立了一個 stream,在遍歷 stream 的時刻,同時修正後真個 collection。針對 stream 的普通原則是,在查詢 stream 的時刻,防止修正後真個 collection。接上去的例子將展現若何准確地處置 stream:
List<IHat> filteredHats = hats.stream().peek(hat -> { if (hat.hasEarFlaps()) { hats.remove(hat); } }).collect(Collectors.toCollection(ArrayList::new));
peek 辦法搜集一切的元素,並對每個元素履行既定舉措。在這裡,舉措即為測驗考試從一個基本列表中刪除數據,這明顯是毛病的。為防止如許的操作,可以測驗考試一些下面講授的辦法。
4. 背約
有時刻,為了更好地協作,由尺度庫或許第三方供給的代碼必需遵照配合的依附原則。例如,必需遵照 hashCode 和 equals 的配合商定,從而包管 Java 聚集框架中的一系列聚集類和其它應用 hashCode 和 equals 辦法的類可以或許正常任務。不遵照商定其實不會發生 exception 或許損壞代碼編譯之類的毛病;它很陰險,由於它隨時能夠在毫無風險提醒的情形下更改運用法式行動。
毛病代碼能夠潛入臨盆情況,從而形成一年夜堆不良影響。這包含較差的 UI 體驗、毛病的數據申報、較差的運用機能、數據喪失或許更多。光榮的是,這些災害性的毛病不會常常產生。在之前曾經說起了 hashCode 和equals 商定,它湧現的場景能夠是:聚集依附於將對象停止哈希或許比擬,就像 HashMap 和 HashSet。簡略來講,這個商定有兩個原則:
假如兩個對象相等,那末 hash code 必需相等。
假如兩個對象有雷同的 hash code,那末它們能夠相等也能夠不相等。
損壞商定的第一條原則,當你試圖從一個 hashmap 中檢索數據的時刻將會招致毛病。第二個原則意味著具有雷同 hash code 的對象紛歧定相等。
上面看一下損壞第一條原則的效果:
public static class Boat { private String name; Boat(String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Boat boat = (Boat) o; return !(name != null ? !name.equals(boat.name) : boat.name != null); } @Override public int hashCode() { return (int) (Math.random() * 5000); } }
正如你所見,Boat 類重寫了 equals 和 hashCode 辦法。但是,它損壞了商定,由於 hashCode 針對每次挪用的雷同對象前往了隨機值。上面的代碼極可能在 hashset 中找不到一個名為 Enterprise 的boat,雖然現實上我們提早參加了這類類型的 boat:
public static void main(String[] args) { Set<Boat> boats = new HashSet<>(); boats.add(new Boat("Enterprise")); System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise"))); }
另外一個商定的例子是 finalize 辦法。這裡是官方 Java 文檔關於它功效描寫的援用:
finalize 的慣例商定是:當 JavaTM 虛擬機肯定任何線程都沒法再經由過程任何方法拜訪指定對象時,這個辦法會被挪用,爾後這個對象只能在某個其他(預備終止的)對象或類終結時被作為某個行動的成果。 finalize 辦法有多個功效,個中包含再次使此對象對其他線程可用;不外 finalize 的重要目標是在弗成取消地拋棄對象之前履行消除操作。例如,表現輸出/輸入銜接對象的 finalize 辦法可履行顯式 I/O 事務,以便在永遠拋棄對象之前中止銜接。
你可以決議在諸如文件處置器中應用 finalize 辦法來釋放資本,然則這類用法是很蹩腳的。因為它是在渣滓收受接管時代被挪用的,而 GC 的時光其實不肯定,是以 finalize 被挪用的時光將沒法包管。
5. 應用原始類型而不是參數化的
依據 Java 文檔描寫:原始類型要末長短參數化的,要末是類 R 的(同時也長短繼續 R 父類或許父接口的)非靜態成員。在 Java 泛型被引入之前,並沒有原始類型的替換類型。Java 從1.5版本開端支撐泛型編程,毫無疑問這是一個主要的功效晉升。但是,因為向後兼容的緣由,這裡存在一個圈套能夠會損壞全部類型體系。著眼下例:
List listOfNumbers = new ArrayList(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
這是一個由數字構成的列表被界說為原始的 ArrayList。因為它並沒有指定類型參數,是以可以給它添加任何對象。然則最初一即將其包括的元素映照為 int 類型並乘以 2,打印出翻倍以後的數據到尺度輸入。
此代碼編譯時不會失足,然則一旦運轉就會拋出運轉時毛病,由於這裡試圖將字符類型映照為整形。很明顯,假如隱蔽了需要信息,類型體系將不克不及贊助寫出平安代碼。
為懂得決這個成績,須要為存入聚集中的對象指定詳細類型:
List<Integer> listOfNumbers = new ArrayList<>(); listOfNumbers.add(10); listOfNumbers.add("Twenty"); listOfNumbers.forEach(n -> System.out.println((int) n * 2));
與之前代碼的獨一差異等於界說聚集的那一行:
List<Integer> listOfNumbers = new ArrayList<>();
修正以後的代碼編譯弗成能被經由過程,由於這裡試圖向只希冀存儲整形的聚集中添加字符串。編譯器將會顯示毛病信息,並指向試圖向列表中添加 Twenty 字符的那一行。參數化泛型類型是個不錯的主張。如許的話,編譯器就可以夠檢討一切能夠的類型,從而因為類型紛歧致而招致的運轉時異常概率將年夜年夜下降。
重要總結了以上五個Java法式員常犯的毛病,願望年夜家可以或許愛好。