Java中這麼溜的操作,看看就好,別實戰


來源 | https://urlify.cn/ieuuuy

首先需要提一下最近幾年在開發界大的鏈式調用,鏈式調用非常利於編寫,更利於閱讀,那麼,什麼是鏈式調用呢,為了掃盲,舉個例子。

示例代碼如下,如果我們想創建一個Dialog,不依賴鏈式調用的話,將會產生如下代碼:

<code>    NRStandardDialog.Builder nrStandardDialogBuilder = NRDialog.standard();     nrStandardDialogBuilder.setTitle("Test");     nrStandardDialogBuilder.setMessage("Test");     nrStandardDialogBuilder.setNegativeTitle("Test");     nrStandardDialogBuilder.setPositiveTitle("Test");     nrStandardDialogBuilder.setNeutralTitle("Test");     nrStandardDialogBuilder.show(this); /<code>

上述代碼共計372個字符,當然,通過依賴強大的IDEA,我們站在巨人的肩膀上操作,通過智能提示和自動補全,極大降低了開發成本。

那麼,現在來估算一下操作成本,通過FreeKey記錄鍵盤輸入,我們的操作路徑大概是

<code>“NRdia .sta ; nr .set "Test; nr .setM "Test; nr .setnet ""Test; nr .setPo ""Test; nr .setneu "Test; nr .sh thi ;” /<code>

共計需要輸入126個字符,What?這樣看好像其實也沒有比全部輸入少很多。

那麼,我們將這個Dialog改為鏈式調用模式創建,會產生如下代碼。

<code> NRDialog.standard() .setTitle("Test") .setMessage("Test") .setNegativeTitle("Test") .setPositiveTitle("Test") .setNeutralTitle("Test") .show(this); /<code>

上述代碼共計223個字符,減少了149個字符。怎麼樣,代碼數量是不是一下子掉下來了很多。

那麼,我們來統計一下鏈式調用的操作路徑成本,再次通過FreeKey記錄鍵盤輸入,我們的操作路徑大概是:

<code>NRD .st .sett "Test.setme "Test.setneg "Test.setpo "Test.setNe "Test.sho thi ;/<code>

共計需要輸入94個字符,通過鏈式調用,我們節省了1.5倍的開發時間成本,並且隱形的節省了代碼的閱讀成本。

方法鏈的優秀案例

一些流行的開源庫的方法鏈優秀使用案例如下:

RxJava

<code>    Flowable.range(1, 10)       .observeOn(Schedulers.computation())       .map(v -> v * v)       .blockingSubscribe(System.out::println);/<code>

EventBus

<code>    EventBus.builder()             .eventInheritance(false)             .logSubscriberExceptions(false)             .build()             .register(this);/<code>

靜態方法的鏈式調用

有些時候,我們會遇到一些需求,考慮想把靜態方法進行封裝一下,來實現優美的鏈式調用,Like this;

<code>public class Test {     public static Test doSth(){         // dosth.         return this;     } }/<code>

如果是非靜態方法的話,我們可以從容的返回this,如果是靜態方法,這麼操作,真的就是想太多。

或者我們這麼操作?

這麼操作?

R U OK?

一頓操作後,我們還是沒法實現靜態方法的鏈式調用,因為他必須返回一個自身的實例。

好吧,最終屈服於IDEA,你反悔了這個靜態方法的實例。

創建了一個Test類的實例,然後愉快地實現了鏈式調用

<code>public class MainTest { public void test(){ Test.doSth().doSth2(); } } /<code>

可是問題來了,為了實現鏈式調用,我們必須為每個對象開闢內存空間嗎?

當然不!

姿勢來了,其實我們可以這樣處理:

<code>public class Test {     public static Test doSth(){         return null;     }     public static Test doSth2(){         return null;     } } /<code>

定義兩個靜態方法,返回為空

<code>public class MainTest {     public void test(){         Test.doSth().doSth2();     } } /<code>

完美實現鏈式調用,而且不會報空指針,這是為什麼?

原理分析

我們對上述調用代碼的示例進行編譯與反編譯:

<code>    public void test(){         Test.doSth().doSth2();     } /<code>

對應的反編譯後的字節碼如下:

<code>  public void test();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=1, args_size=1          0: invokestatic  #2                  // Method com/Test.doSth:()Lcom/Test;          3: pop          4: invokestatic  #3                  // Method com//Test.doSth2:()Lcom/Test;          7: pop          8: return       LineNumberTable:         line 9: 0         line 10: 8 /<code>

invokestatic調用靜態方法doSth和doSth2,可以直接找到索引2和索引4的方法,所以並沒有依賴Test實例。

而對於正常的對象調用,如果使用Null調用為什麼會報空指針呢?我們再來看一段示例代碼和其反編譯的字節碼。

聲明mainTest為空並調用其test方法。

<code>public void test2(){     MainTest mainTest = null;     mainTest.test(); } /<code>

反編譯後的字節碼如下,invokevirtual為調用實例方法,動態綁定,invokevirtual #4代表會調用索引4的方法,即test()。

<code>  public void test2();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=2, args_size=1          0: aconst_null  // 將null入棧          1: astore_1     // 將引用存入局部變量表1中          2: aload_1      // 將局部變量表1的對象引用壓入棧頂          3: invokevirtual #4     // Method com/MainTest.test:()V          6: return       LineNumberTable:         line 25: 0         line 26: 2         line 27: 6 /<code>

而invokevirtual則負責找到操作棧棧頂元素所指向的對象並查找和調用其相關方法,但此時棧頂為空,自然就無法找到相關方法,拋出空指針。

而這裡就涉及到了靜態綁定和動態綁定。綁定過程,即指方法的調用與其類的關聯過程。

其中的靜態綁定,是在方法執行前,已經被關聯,而動態綁定則是在運行時綁定。

最後

最後,在項目代碼中還是並不推薦這種方式,團隊開發,難免會有小夥伴們產生誤解,會認為該對象已經生成了實例了,而創建非靜態方法調用導致空指針。