簡體   English   中英

空檢查鏈 vs 捕獲 NullPointerException

[英]Null check chain vs catching NullPointerException

Web 服務返回一個巨大的 XML,我需要訪問它的深層嵌套字段。 例如:

return wsObject.getFoo().getBar().getBaz().getInt()

問題是getFoo()getBar()getBaz()可能返回null

但是,如果我在所有情況下都檢查null ,則代碼會變得非常冗長且難以閱讀。 此外,我可能會錯過某些字段的檢查。

if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();

可以寫嗎

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    return -1;
}

或者這會被視為反模式嗎?

捕獲NullPointerException是一件非常有問題的事情,因為它們幾乎可以在任何地方發生。 很容易從錯誤中找到一個錯誤,偶然發現它並繼續像一切正常一樣,從而隱藏真正的問題。 處理起來非常棘手,所以最好完全避免。 (例如,考慮對 null Integer自動拆箱。)

我建議您改用Optional類。 當您想要處理存在或不存在的值時,這通常是最好的方法。

使用它,您可以像這樣編寫代碼:

public Optional<Integer> m(Ws wsObject) {
    return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
        .map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
        .map(b -> b.getBaz())
        .map(b -> b.getInt());
        // Add this if you want to return null instead of an empty optional if any is null
        // .orElse(null);
        // Or this if you want to throw an exception instead
        // .orElseThrow(SomeApplicationException::new);
}

為什么可選?

對於可能不存在的值,使用Optional而不是null使這一事實對讀者非常明顯和清晰,並且類型系統將確保您不會意外忘記它。

您還可以更方便地訪問使用這些值的方法,例如maporElse


缺席是有效的還是錯誤的?

但也要考慮中間方法返回 null 是否是有效結果,或者這是否是錯誤的跡象。 如果它總是一個錯誤,那么拋出異常可能比返回特殊值或中間方法本身拋出異常更好。


也許更多的選擇?

另一方面,如果中間方法中缺少的值是有效的,也許您也可以為它們切換到Optional

然后你可以像這樣使用它們:

public Optional<Integer> mo(Ws wsObject) {
    return wsObject.getFoo()
        .flatMap(f -> f.getBar())
        .flatMap(b -> b.getBaz())
        .flatMap(b -> b.getInt());        
}

為什么不是可選的?

我能想到的不使用Optional的唯一原因是這是否在代碼的性能關鍵部分,並且垃圾收集開銷是否會成為一個問題。 這是因為每次執行代碼時都會分配一些Optional對象,VM可能無法優化掉這些對象。 在這種情況下,您的原始 if 測試可能會更好。

我建議考慮Objects.requireNonNull(T obj, String message) 您可以為每個異常構建帶有詳細消息的鏈,例如

requireNonNull(requireNonNull(requireNonNull(
    wsObject, "wsObject is null")
        .getFoo(), "getFoo() is null")
            .getBar(), "getBar() is null");

我建議你不要使用特殊的返回值,比如-1 那不是Java風格。 Java 設計了異常機制來避免這種來自 C 語言的老式方法。

拋出NullPointerException也不是最好的選擇。 您可以提供自己的異常(進行檢查以保證它將由用戶處理或取消檢查以更簡單的方式處理它)或使用來自您正在使用的 XML 解析器的特定異常。

假設類結構確實超出了我們的控制,似乎是這種情況,我認為按照問題中的建議捕獲 NPE 確實是一個合理的解決方案,除非性能是一個主要問題。 一個小的改進可能是包裝 throw/catch 邏輯以避免混亂:

static <T> T get(Supplier<T> supplier, T defaultValue) {
    try {
        return supplier.get();
    } catch (NullPointerException e) {
        return defaultValue;
    }
}

現在你可以簡單地做:

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);

正如湯姆在評論中已經指出的那樣,

以下陳述違反了得墨忒耳定律

wsObject.getFoo().getBar().getBaz().getInt()

你想要的是int ,你可以從Foo得到它。 得墨忒耳法則說,永遠不要和陌生人說話 對於您的情況,您可以隱藏FooBar引擎蓋下的實際實現。

現在,您可以在Foo創建方法以從Baz獲取int 最終, Foo將擁有Bar並且在Bar我們可以訪問Int而無需將Baz直接暴露給Foo 因此,空檢查可能會被划分到不同的類中,並且只有必需的屬性才會在類之間共享。

我的回答與@janki 幾乎在同一行,但我想對代碼片段稍作修改,如下所示:

if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null) 
   return wsObject.getFoo().getBar().getBaz().getInt();
else
   return something or throw exception;

如果該對象有可能為空,您也可以為wsObject添加空檢查。

您說某些方法“可能返回null ”,但沒有說明它們在什么情況下返回null 你說你捕獲了NullPointerException但你沒有說你為什么捕獲它。 缺乏信息表明您沒有清楚地了解例外的用途以及為什么它們優於替代方案。

考慮一個旨在執行操作的類方法,但該方法不能保證它會執行該操作,因為情況超出了它的控制范圍(實際上Java 中的所有方法都是這種情況)。 我們調用該方法並返回。 調用該方法的代碼需要知道它是否成功。 它怎么會知道? 它如何構建以應對成功或失敗的兩種可能性?

使用異常,我們可以編寫將成功作為后置條件的方法 如果方法返回,則成功。 如果它拋出異常,它就失敗了。 為清晰起見,這是一個巨大的勝利。 我們可以編寫清楚地處理正常情況、成功情況的代碼,並將所有錯誤處理代碼移動到catch子句中。 經常會發現一個方法如何或為什么不成功的細節對調用者來說並不重要,因此相同的catch子句可用於處理多種類型的失敗。 它經常發生這樣的方法並不需要在所有捕獲異常,但只是讓他們傳播到它的調用者。 由於程序錯誤導致的異常屬於后一類; 當出現錯誤時,很少有方法可以做出適當的反應。

所以,那些返回null方法。

  • null值是否表示您的代碼中存在錯誤? 如果是這樣,您根本不應該捕獲異常。 並且您的代碼不應該試圖自行猜測。 只要假設它會起作用,就寫出清晰簡潔的內容。 方法調用鏈是否清晰簡潔? 然后只需使用它們。
  • null值是否表示您的程序輸入無效? 如果是,則NullPointerException不是要拋出的合適異常,因為按照慣例,它被保留用於指示錯誤。 您可能想拋出一個從IllegalArgumentException (如果您想要未經檢查的異常)或IOException (如果您想要一個已檢查的異常)派生的自定義異常。 當輸入無效時,您的程序是否需要提供詳細的語法錯誤消息? 如果是這樣,檢查每個方法是否null返回值然后拋出適當的診斷異常是您唯一可以做的事情。 如果您的程序不需要提供詳細的診斷,將方法調用鏈接在一起,捕獲任何NullPointerException然后拋出您的自定義異常是最清晰和最簡潔的。

其中一個答案聲稱鏈式方法調用違反了迪米特法則,因此是不好的。 這種說法是錯誤的。

  • 在程序設計方面,對於什么是好什么是壞並沒有任何絕對的規則。 只有啟發式:在大部分(甚至幾乎所有)時間都是正確的規則。 編程技能的一部分是知道什么時候可以打破這些規則。 因此,“這違反規則X ”的簡潔斷言根本不是真正的答案。 這是應該打破規則的情況之一嗎?
  • 得墨忒耳定律實際上是關於 API 或類接口設計的規則。 在設計類時,具有抽象層次結構很有用。 您有使用語言原語直接執行操作並在比語言原語更高級別的抽象中表示對象的低級類。 您擁有委托給低級類的中級類,並在比低級類更高的級別上實現操作和表示。 您擁有委托給中級類的高級類,並實現更高級別的操作和抽象。 (我在這里只討論了三個抽象級別,但更多是可能的)。 這允許您的代碼根據每個級別的適當抽象來表達自己,從而隱藏復雜性。 迪米特定律的基本原理是,如果您有一系列方法調用,則表明您有一個高級類通過中級類直接處理低級細節,因此您的中級類沒有提供了高級類需要的中級抽象操作。 但似乎不是你這里的情況:你沒有設計方法調用鏈中的類,它們是一些自動生成的 XML 序列化代碼的結果(對吧?),調用鏈不是降序的通過抽象層次結構,因為反序列化的 XML 都在抽象層次結構的同一級別(對嗎?)?

為了提高可讀性,您可能需要使用多個變量,例如

Foo theFoo;
Bar theBar;
Baz theBaz;

theFoo = wsObject.getFoo();

if ( theFoo == null ) {
  // Exit.
}

theBar = theFoo.getBar();

if ( theBar == null ) {
  // Exit.
}

theBaz = theBar.getBaz();

if ( theBaz == null ) {
  // Exit.
}

return theBaz.getInt();

正如其他人所說,尊重迪米特法則絕對是解決方案的一部分。 在可能的情況下,另一部分是更改這些鏈接方法,使它們不能返回null 您可避免返回null的,而不是返回一個空String ,空Collection ,或者一些其他虛擬對象,手段或為所欲為呼叫者會用做null

NullPointerException是一個運行時異常,所以一般來說不建議捕獲它,而是避免它。

您必須在要調用該方法的任何地方捕獲異常(否則它會向上傳播堆棧)。 盡管如此,如果在您的情況下,您可以繼續使用值為 -1 的結果,並且您確定它不會傳播,因為您沒有使用任何可能為空的“部分”,那么在我看來是正確的抓住它

編輯:

我同意@xenteros 稍后的回答,最好啟動自己的異常而不是返回 -1,例如,您可以將其InvalidXMLException

不要捕捉NullPointerException 你不知道它是從哪里來的(我知道在你的情況下不太可能,但也許是其他東西扔了它)而且它很慢。 您想訪問指定的字段,為此,所有其他字段都必須不為空。 這是檢查每個字段的完美正當理由。 如果然后創建一種可讀性方法,我可能會檢查它。 正如其他人指出的那樣,已經返回 -1 是非常老派的,但我不知道您是否有理由這樣做(例如與另一個系統交談)。

public int callService() {
    ...
    if(isValid(wsObject)){
        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    return -1;
}


public boolean isValid(WsObject wsObject) {
    if(wsObject.getFoo() != null &&
        wsObject.getFoo().getBar() != null &&
        wsObject.getFoo().getBar().getBaz() != null) {
        return true;
    }
    return false;
}

編輯:如果它不遵守迪米特法則,這是有爭議的,因為 WsObject 可能只是一個數據結構(檢查https://stackoverflow.com/a/26021695/1528880 )。

如果不想重構代碼並且可以使用 Java 8,則可以使用方法引用。

先做一個簡單的演示(請原諒靜態內部類)

public class JavaApplication14 
{
    static class Baz
    {
        private final int _int;
        public Baz(int value){ _int = value; }
        public int getInt(){ return _int; }
    }
    static class Bar
    {
        private final Baz _baz;
        public Bar(Baz baz){ _baz = baz; }
        public Baz getBar(){ return _baz; }   
    }
    static class Foo
    {
        private final Bar _bar;
        public Foo(Bar bar){ _bar = bar; }
        public Bar getBar(){ return _bar; }   
    }
    static class WSObject
    {
        private final Foo _foo;
        public WSObject(Foo foo){ _foo = foo; }
        public Foo getFoo(){ return _foo; }
    }
    interface Getter<T, R>
    {
        R get(T value);
    }

    static class GetterResult<R>
    {
        public R result;
        public int lastIndex;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) 
    {
        WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
        WSObject wsObjectNull = new WSObject(new Foo(null));

        GetterResult<Integer> intResult
                = getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);

        GetterResult<Integer> intResult2
                = getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);


        System.out.println(intResult.result);
        System.out.println(intResult.lastIndex);

        System.out.println();
        System.out.println(intResult2.result);
        System.out.println(intResult2.lastIndex);

        // TODO code application logic here
    }

    public static <R, V1, V2, V3, V4> GetterResult<R>
            getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
            {
                GetterResult result = new GetterResult<>();

                Object tmp = value;


                if (tmp == null)
                    return result;
                tmp = g1.get((V1)tmp);
                result.lastIndex++;


                if (tmp == null)
                    return result;
                tmp = g2.get((V2)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g3.get((V3)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g4.get((V4)tmp);
                result.lastIndex++;


                result.result = (R)tmp;

                return result;
            }
}

輸出

241
4


2

接口Getter只是一個功能接口,您可以使用任何等效的接口。
GetterResult類,為了清晰起見,去掉了訪問器,保存了 getter 鏈的結果(如果有的話),或者最后調用的 getter 的索引。

getterChain方法是一個簡單的樣板代碼,可以自動生成(或在需要時手動生成)。
我構造了代碼,以便重復塊是不言而喻的。


這不是一個完美的解決方案,因為您仍然需要為每個 getter 數量定義一個getterChain重載。

我會重構代碼,但如果不能,並且您發現自己經常使用長 getter 鏈,您可能會考慮構建一個重載類,重載從 2 到 10 個 getter。

我想添加一個專注於error 含義的答案。 空異常本身不提供任何意義的完整錯誤。 所以我建議避免直接與他們打交道。

你的代碼可能出錯的情況有數千種:無法連接到數據庫、IO 異常、網絡錯誤……如果你一一處理它們(比如這里的空檢查),那就太麻煩了。

在代碼中:

wsObject.getFoo().getBar().getBaz().getInt();

即使您知道哪個字段為空,您也不知道出了什么問題。 也許 Bar 為空,但這是預期的嗎? 還是數據錯誤? 想想閱讀你代碼的人

就像在 xenteros 的回答中一樣,我建議使用custom unchecked exception 例如,在這種情況下:Foo 可以為 null(有效數據),但 Bar 和 Baz 永遠不應為 null(無效數據)

代碼可以改寫:

void myFunction()
{
    try 
    {
        if (wsObject.getFoo() == null)
        {
          throw new FooNotExistException();
        }

        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    catch (Exception ex)
    {
        log.error(ex.Message, ex); // Write log to track whatever exception happening
        throw new OperationFailedException("The requested operation failed")
    }
}


void Main()
{
    try
    {
        myFunction();
    }
    catch(FooNotExistException)
    {
        // Show error: "Your foo does not exist, please check"
    }
    catch(OperationFailedException)
    {
        // Show error: "Operation failed, please contact our support"
    }
}

從昨天開始一直在關注這個帖子。

我一直在評論/投票說,抓住 NPE 是不好的。 這就是我一直這樣做的原因。

package com.todelete;

public class Test {
    public static void main(String[] args) {
        Address address = new Address();
        address.setSomeCrap(null);
        Person person = new Person();
        person.setAddress(address);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                System.out.println(person.getAddress().getSomeCrap().getCrap());
            } catch (NullPointerException npe) {

            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) / 1000F);
        long startTime1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            if (person != null) {
                Address address1 = person.getAddress();
                if (address1 != null) {
                    SomeCrap someCrap2 = address1.getSomeCrap();
                    if (someCrap2 != null) {
                        System.out.println(someCrap2.getCrap());
                    }
                }
            }
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println((endTime1 - startTime1) / 1000F);
    }
}

  public class Person {
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

package com.todelete;

public class Address {
    private SomeCrap someCrap;

    public SomeCrap getSomeCrap() {
        return someCrap;
    }

    public void setSomeCrap(SomeCrap someCrap) {
        this.someCrap = someCrap;
    }
}

package com.todelete;

public class SomeCrap {
    private String crap;

    public String getCrap() {
        return crap;
    }

    public void setCrap(String crap) {
        this.crap = crap;
    }
}

輸出

3.216

0.002

我在這里看到了一個明顯的贏家。 進行 if 檢查比捕獲異常要便宜得多。 我已經看到了 Java-8 的做法。 考慮到當前 70% 的應用程序仍然在 Java-7 上運行,我添加了這個答案。

底線對於任何關鍵任務應用程序,處理 NPE 都是昂貴的。

值得考慮創建自己的異常。 我們稱之為 MyOperationFailedException。 您可以拋出它而不是返回一個值。 結果將是相同的 - 您將退出該函數,但不會返回硬編碼值 -1,這是 Java 反模式。 在 Java 中,我們使用異常。

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    throw new MyOperationFailedException();
}

編輯:

根據評論中的討論,讓我在之前的想法中添加一些內容。 在這段代碼中有兩種可能性。 一個是您接受 null,另一個是,這是一個錯誤。

如果它是一個錯誤並且發生了,當斷點不夠用時,您可以使用其他結構來調試代碼以進行調試。

如果可以接受,您就不必關心此空值出現在何處。 如果你這樣做,你絕對不應該鏈接這些請求。

如果效率是一個問題,則應考慮“捕獲”選項。 如果 'catch' 不能使用,因為它會傳播(如 'SCouto' 所述),則使用局部變量來避免多次調用方法getFoo()getBar()getBaz()

您擁有的方法很長,但非常具有可讀性。 如果我是一名新的開發人員,來到您的代碼庫,我可以很快看到您在做什么。 大多數其他答案(包括捕獲異常)似乎並沒有使事情更具可讀性,而且在我看來,有些答案使其可讀性降低。

鑒於您可能無法控制生成的源,並假設您確實只需要在這里和那里訪問一些深度嵌套的字段,那么我建議將每個深度嵌套的訪問都包裝在一個方法中。

private int getFooBarBazInt() {
    if (wsObject.getFoo() == null) return -1;
    if (wsObject.getFoo().getBar() == null) return -1;
    if (wsObject.getFoo().getBar().getBaz() == null) return -1;
    return wsObject.getFoo().getBar().getBaz().getInt();
}

如果您發現自己編寫了很多這些方法,或者您發現自己很想制作這些公共靜態方法,那么我將創建一個單獨的對象模型,按照您的意願嵌套,僅包含您關心的字段,並從網絡轉換服務對象模型到您的對象模型。

當您與遠程 Web 服務進行通信時,通常會有“遠程域”和“應用程序域”並在兩者之間切換。 遠程域通常受 Web 協議的限制(例如,您不能在純 RESTful 服務中來回發送輔助方法,深度嵌套的對象模型很常見以避免多次 API 調用),因此不適合直接用於你的客戶。

例如:

public static class MyFoo {

    private int barBazInt;

    public MyFoo(Foo foo) {
        this.barBazInt = parseBarBazInt();
    }

    public int getBarBazInt() {
        return barBazInt;
    }

    private int parseFooBarBazInt(Foo foo) {
        if (foo() == null) return -1;
        if (foo().getBar() == null) return -1;
        if (foo().getBar().getBaz() == null) return -1;
        return foo().getBar().getBaz().getInt();
    }

}
return wsObject.getFooBarBazInt();

通過應用得墨忒耳定律,

class WsObject
{
    FooObject foo;
    ..
    Integer getFooBarBazInt()
    {
        if(foo != null) return foo.getBarBazInt();
        else return null;
    }
}

class FooObject
{
    BarObject bar;
    ..
    Integer getBarBazInt()
    {
        if(bar != null) return bar.getBazInt();
        else return null;
    }
}

class BarObject
{
    BazObject baz;
    ..
    Integer getBazInt()
    {
        if(baz != null) return baz.getInt();
        else return null;
    }
}

class BazObject
{
    Integer myInt;
    ..
    Integer getInt()
    {
        return myInt;
    }
}

給出似乎與所有其他人不同的答案。

我建議您在if檢查NULL

原因:

我們不應該給我們的程序崩潰留下任何機會。 NullPointer 由系統生成。 無法預測系統生成異常的行為 當您已經有自己處理程序的方法時,您不應該將程序交給 System。 並放置異常處理機制以增加安全性。!!

為了使您的代碼易於閱讀,請嘗試使用以下方法檢查條件:

if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null) 
   return -1;
else 
   return wsObject.getFoo().getBar().getBaz().getInt();

編輯:

在這里,您需要將這些值wsObject.getFoo()wsObject.getFoo().getBar()wsObject.getFoo().getBar().getBaz()在一些變量中。 我不這樣做是因為我不知道該函數的返回類型。

任何建議將不勝感激.. !!

我寫了一個叫做Snag的類,它可以讓你定義一個路徑來瀏覽對象樹。 以下是它的使用示例:

Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();

這意味着實例ENGINE_NAME將有效地調用Car?.getEngine()?.getName()傳遞給它的實例,並返回null ,如果任何引用返回null

final String name =  ENGINE_NAME.get(firstCar);

它沒有發布在 Maven 上,但如果有人發現它有用,它就在這里(當然沒有保證!)

這有點基礎,但似乎可以勝任。 顯然,對於支持安全導航或Optional更新版本的 Java 和其他 JVM 語言,它已經過時了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM