Java編程中忽略這些細節,Bug肯定找上你
Java語言構建的各類應用程序,在人類的日常生活中占用非常重要的地位,各大IT廠商幾乎都會使用它來構建自己的產品,為客戶提供服務。作為一個企業級應用開發語言,穩定和高效的運行,至關重要。在Java語言的日常編程中,也存在著容易被忽略的細節,這些細節可能會導致程序出現各種Bug,下面就對這些細節進行一些總結:
1 相等判斷中的==和equals
在很多場景中,我們都需要判斷兩個對象是否相等,一般來說,判定兩個對象的是否相等,都是依據其值是否相等,如兩個字符串a和b的值都為"java",則我們認為二者相等。在Java中,有兩個操作可以判斷是否相當,即==和equals,但二者是有區別的,不可混用。下面給出示例:
- String a = "java";
- String b = new String("java");
- System.out.println(a == b);//false
- System.out.println(a.equals(b));//true
字符串a和b的字面值都為"java",用a == b判斷則輸出false,即不相等,而a.equals(b)則輸出true,即相等。這是為什么呢?在Java中,String是一個不可變的類型,一般來說,如果兩個String的值相等,默認情況下,會指定同一個內存地址,但這里字符串String b用new String方法強制生成一個新的String對象,因此,二者內存地址不一致。由于 == 需要判斷對象的內存地址是否一致,因此返回false,而equals默認(override后可能不一定)是根據字面值來判斷,即相等。
下面再給出一個示例:
- //integer -128 to 127
- Integer i1 = 100;
- Integer i2 = 100;
- System.out.println(i1 == i2);//true
- i1 = 300;
- i2 = 300;
- System.out.println(i1 == i2);//false
- System.out.println(i1.equals(i2));//true
這是由于Java中的Integer數值的范圍為-128到127,因此在這范圍內的對象的內存地址是一致的,而超過這個范圍的數值對象的內存地址是不一致的,因此300這個數值在 == 比較下,返回false,但在equals比較下返回true。
2 switch語句中丟失了break
在很多場景中,我們需要根據輸入參數的范圍來分別進行處理,這里除了可以使用if ... else ...語句外,還可以使用switch語句。在switch語句中,會羅列出多個分支條件,并進行分別處理,但如果稍有不注意,就可能丟失關鍵字break語句,從而出現預期外的值。下面給出示例:
- //缺少break關鍵字
- public static void switchBugs(int v ) {
- switch (v) {
- case 0:
- System.out.println("0");
- //break
- case 1:
- System.out.println("1");
- break;
- case 2:
- System.out.println("2");
- break;
- default:
- System.out.println("other");
- }
- }
如果我們使用如下語句進行調用:
- switchBugs(0);
則我們預期返回"0",但是卻返回"0" "1"。這是由于case 0 分支下缺少break關鍵字,則雖然程序匹配了此分支,但是卻能穿透到下一個分支,即case 1分支,然后遇到break后返回值。
3 大量的垃圾回收,效率低下
字符串的拼接操作,是非常高頻的操作,但是如果涉及的拼接量很大,則如果直接用 + 符號進行字符串拼接,則效率非常低下,程序運行的速度很慢。下面給出示例:
- private static void stringWhile(){
- //獲取開始時間
- long start = System.currentTimeMillis();
- String strV = "";
- for (int i = 0; i < 100000; i++) {
- strV = strV + "$";
- }
- //strings are immutable. So, on each iteration a new string is created.
- // To address this we should use a mutable StringBuilder:
- System.out.println(strV.length());
- long end = System.currentTimeMillis(); //獲取結束時間
- System.out.println("程序運行時間: "+(end-start)+"ms");
- start = System.currentTimeMillis();
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 100000; i++) {
- sb.append("$");
- }
- System.out.println(strV.length());
- end = System.currentTimeMillis();
- System.out.println("程序運行時間: "+(end-start)+"ms");
- }
上述示例分別在循環體中用 + 和 StringBuilder進行字符串拼接,并統計了運行的時間(毫秒),下面給出模擬電腦上的運行結果:
- //+ 操作
- 100000
- 程序運行時間: 6078ms
- StringBuilder操作
- 100000
- 程序運行時間: 2ms
由此可見,使用StringBuilder構建字符串速度相比于 + 拼接,效率上高出太多。究其原因,就是因為Java語言中的字符串類型是不可變的,因此 + 操作后會創建一個新的字符串,這樣會涉及到大量的對象創建工作,也涉及到垃圾回收機制的介入,因此非常耗時。
4 循環時刪除元素
有些情況下,我們需要從一個集合對象中刪除掉特定的元素,如從一個編程語言列表中刪除java語言,則就會涉及到此種場景,但是如果處理不當,則會拋出
ConcurrentModificationException異常。下面給出示例:
- private static void removeList() {
- List<String> lists = new ArrayList<>();
- lists.add("java");
- lists.add("csharp");
- lists.add("fsharp");
- for (String item : lists) {
- if (item.contains("java")) {
- lists.remove(item);
- }
- }
- }
運行上述方法,會拋出錯誤,此時可以用如下方法進行解決,即用迭代器iterator,具體如下所示:
- private static void removeListOk() {
- List<String> lists = new ArrayList<>();
- lists.add("java");
- lists.add("csharp");
- lists.add("fsharp");
- Iterator<String> hatIterator = lists.iterator();
- while (hatIterator.hasNext()) {
- String item = hatIterator.next();
- if (item.contains("java")) {
- hatIterator.remove();
- }
- }
- System.out.println(lists);//[csharp, fsharp]
- }
5 null引用
在方法中,首先應該對參數的合法性進行驗證,第一需要驗證參數是否為null,然后再判斷參數是否是預期范圍的值。如果不首先進行null判斷,直接進行參數的比較或者方法的調用,則可能出現null引用的異常。下面給出示例:
- private static void nullref(String words) {
- //NullPointerException
- if (words.equals("java")){
- System.out.println("java");
- }else{
- System.out.println("not java");
- }
- }
如果此時我們用如下方法進行調用,則拋出異常:
- nullref(null)
這是由于假設了words不為null,則可以調用String對象的equals方法。下面可以稍微進行一些修改,如下所示:
- private static void nullref2(String words) {
- if ("java".equals(words)){
- System.out.println("java");
- }else{
- System.out.println("not java");
- }
- }
則此時執行則可以正確運行:
- nullref2(null)
6 hashCode對equals的影響
前面提到,equals方法可以從字面值上來判斷兩個對象是否相等。一般來說,如果兩個對象相等,則其hash code相等,但是如果hash code相等,則兩個對象可能相等,也可能不相等。這是由于Object的equals方法和hashCode方法可以被Override。下面給出示例:
- package com.jyd;
- import java.util.Objects;
- public class MySchool {
- private String name;
- MySchool(String name) {
- this.name = name;
- }
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- MySchool _obj = (MySchool) o;
- return Objects.equals(name, _obj.name);
- }
- @Override
- public int hashCode() {
- int code = this.name.hashCode();
- System.out.println(code);
- //return code; //true
- //隨機數
- return (int) (Math.random() * 1000);//false
- }
- }
- Set<MySchool> mysets = new HashSet<>();
- mysets.add(new MySchool("CUMT"));
- MySchool obj = new MySchool("CUMT");
- System.out.println(mysets.contains(obj));
執行上述代碼,由于hashCode方法被Override,每次返回隨機的hash Code值,則意味著兩個對象的hash code不一致,那么equals判斷則返回false,雖然二者的字面值都為"CUMT"。
7 內存泄漏
我們知道,計算機的內存是有限的,如果Java創建的對象一直不能進行釋放,則新創建的對象會不斷占用剩余的內存空間,最終導致內存空間不足,拋出內存溢出的異常。內存異?;镜膯卧獪y試不容易發現,往往都是上線運行一定時間后才發現的。下面給出示例:
- package com.jyd;
- import java.util.Properties;
- //內存泄漏模擬
- public class MemoryLeakDemo {
- public final String key;
- public MemoryLeakDemo(String key) {
- this.key =key;
- }
- public static void main(String args[]) {
- try {
- Properties properties = System.getProperties();
- for(;;) {
- properties.put(new MemoryLeakDemo("key"), "value");
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- /*
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- MemoryLeakDemo that = (MemoryLeakDemo) o;
- return Objects.equals(key, that.key);
- }
- @Override
- public int hashCode() {
- return Objects.hash(key);
- }
- */
- }
此示例中,有一個for無限循環,它會一直創建一個對象,并添加到properties容器中,如果MemoryLeakDemo類未給出自己的equals方法和hashCode方法,那么這個對象會被一直添加到properties容器中,最終內存泄漏。但是如果定義了自己的equals方法和hashCode方法(被注釋的部分),那么新創建的MemoryLeakDemo實例,由于key值一致,則判定為已存在,則不會重復添加,此時則不會出現內存溢出。