1. 程式人生 > >Stack Overflow 上 250W 瀏覽量的一個問題:你物件丟了

Stack Overflow 上 250W 瀏覽量的一個問題:你物件丟了

在逛 Stack Overflow 的時候,發現最火的問題竟然是:什麼是 NullPointerException(java.lang.NullPointerException),它是由什麼原因導致的,有沒有好的方法或者工具可以追蹤它發生的原因?

真沒想到,這個問題瀏覽的次數多達 250 萬次!所以,我想是時候把最高讚的回答整理一下分享出來了。請隨我來。

宣告引用變數(即物件)時,實際上是建立了一個指向物件的指標。請看以下程式碼:

int x;
x = 10;

第一行程式碼聲明瞭一個名為 x 的變數(int 型別),Java 會把它初始化為 0。第二行程式碼把 x 賦值為 10,意味著 10 將被寫入到 x 所指向的記憶體位置上。

但是呢,當我們嘗試宣告一個引用型別時,情況將會有所不同。

Integer num;
num = new Integer(10);

第一行程式碼聲明瞭一個名為 num 的變數(Integer 型別),Java 把它初始化為 null,表示“什麼都沒有指向 ”。

第二行程式碼中,new 關鍵字建立了一個 Integer 型別的物件,並將變數 num 指向該物件。

當我們聲明瞭一個變數,卻沒有將該變數指向任何建立的物件,然後就使用它的時候,NullPointerException 就發生了。大多數情況下,編譯器會發現這個問題,並且提醒我們“xxxx may not have been initialized”。

假如有這樣一段程式碼:

public void doSomething(SomeObject obj) {
   //do something to obj
}

在這種情況下,我們沒有建立物件 obj,而是假設它在 doSomething() 方法被呼叫之前就建立了。

現在假設在此之前它沒有建立。我們這樣呼叫 doSomething() 方法:

doSomething(null);

這就意味著 doSomething() 方法的引數 obj 為 null。如果該方法還要使用 obj 繼續做點什麼,最好提前丟擲 NullPointerException

,因為開發者需要該資訊來進行除錯。

還有另外一種替代方法,判斷 obj 是不是 null,如果是,就小心行事,做某些不會引起 NullPointerException 的事情;如果不是,就放心大膽地做該做的事情。

/**
  * @param obj An optional foo for ____. May be null, in which case 
  *  the result will be ____.
  */
public void doSomething(SomeObject obj) {
    if(obj != null) {
       //do something
    } else {
       //do something else
    }
}

那假如程式真的出現了 NullPointerException,該怎麼追蹤堆疊資訊,找到錯誤的根源呢?

簡單來說,堆疊資訊是應用程式在引發 Exception 時呼叫的方法列表,可以準確地定位到錯誤發生的根源。就像下面這樣。

Exception in thread "main" java.lang.NullPointerException
        at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

就上面這個堆疊資訊來說,錯誤發生在“at …”列表處,第一個“at 處”就是錯誤最初發生的位置。

at com.example.myproject.Book.getTitle(Book.java:16)

為了除錯,我們可以開啟 Book.java 類的第 16 行,它可能是:

15   public String getTitle() {
16      System.out.println(title.toString());
17      return title;
18   }

從這段程式碼中可以看得出,錯誤的原因很可能是因為 title 為 null。

有時候,應用程式會捕獲一個異常,然後把它作為另外一種型別的異常丟擲。就像下面這樣:

34   public void getBookIds(int id) {
35      try {
36         book.getId(id);    // 這裡可能會引發 NullPointerException
37      } catch (NullPointerException e) {
38         throw new IllegalStateException("A book has a null property", e)
39      }
40   }

此時的堆疊資訊可能是下面這樣的:

Exception in thread "main" java.lang.IllegalStateException: A book has a null property
        at com.example.myproject.Author.getBookIds(Author.java:38)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
        at com.example.myproject.Book.getId(Book.java:22)
        at com.example.myproject.Author.getBookIds(Author.java:36)
        ... 1 more

和之前堆疊資訊有所不同的是,這裡多了一個“Caused by”;有時候還會有更多的“Caused by”。在這種情況下,我們通常需要追本溯源,找到最深層次的那個“cause”——它就是堆疊資訊中最下面的那個。

Caused by: java.lang.NullPointerException <-- 根本原因
        at com.example.myproject.Book.getId(Book.java:22) 

同樣,我們需要檢視一下 Book.java 的第 22 行,找到可能引發 NullPointerException 的原因。

有時候,堆疊資訊要比上面的例子凌亂得多。參考下面這個。

javax.servlet.ServletException: Something bad happened
    at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:60)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.example.myproject.ExceptionHandlerFilter.doFilter(ExceptionHandlerFilter.java:28)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.example.myproject.OutputBufferFilter.doFilter(OutputBufferFilter.java:33)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
    at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
    at org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
Caused by: com.example.myproject.MyProjectServletException
    at com.example.myproject.MyServlet.doPost(MyServlet.java:169)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2822)
    at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:71)
    at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
    at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:321)
    at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
    at $Proxy19.save(Unknown Source)
    at com.example.myproject.MyEntityService.save(MyEntityService.java:59) <-- relevant call (see notes below)
    at com.example.myproject.MyServlet.doPost(MyServlet.java:164)
    ... 32 more
Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
    at org.hsqldb.jdbc.Util.throwError(Unknown Source)
    at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
    at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
    at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:57)
    ... 54 more

這個例子當中的堆疊資訊實在是太多了,令人眼花繚亂。如果按照之前提供的方法(堆疊資訊中最下面的那個)找最深層次的那個“cause”,它就是:

Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
    at org.hsqldb.jdbc.Util.throwError(Unknown Source)
    at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
    at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
    at org.hibernate.id.insert.AbstractSelec

但其實它並不是的,因為丟擲這個異常的方法呼叫者屬於類庫程式碼(c3p0 類庫),所以我們需要往上找異常發生的原因,並且這個異常很可能是由我們自己編寫的程式碼(com.example.myproject 包下)引發的,於是我們找到了這樣一段異常資訊。

at com.example.myproject.MyEntityService.save(MyEntityService.java:59)

順藤摸瓜,看看 MyEntityService.java 的第 59 行,它就是引發錯誤的根本原因。

謝謝大家的閱讀,原創不易,喜歡就點個贊,這將是我最強的寫作動力。如果你覺得文章對你有所幫助,也蠻有趣的,就關注一下我的公眾號,謝謝。

&n