跳轉到內容

Java 除錯之道

來自華夏公益教科書,自由的教科書,開放的世界

程式中可能會出現幾種不同的錯誤,為了更快地跟蹤它們,將它們區分開來非常有用。

itemize

編譯時錯誤是由編譯器產生的,通常表示程式的語法存在問題。例如:在語句末尾省略分號。

執行時錯誤是由執行時系統產生的,如果程式在執行時出現問題。大多數執行時錯誤都是異常。例如:無限遞迴最終會導致 StackOverflowException。

語義錯誤是程式編譯並執行但沒有執行正確操作的問題。例如:表示式可能不會按預期順序計算,從而導致意外結果。

itemize

compile-time error
run-time error
semantic error
error!compile-time
error!run-time
error!semantic
exception

除錯的第一步是確定您正在處理哪種錯誤。雖然以下部分按錯誤型別組織,但有一些技術適用於多種情況。


編譯時錯誤

[編輯 | 編輯原始碼]

編譯器正在輸出錯誤資訊。

[編輯 | 編輯原始碼]
error messages
compiler

如果編譯器報告 100 個錯誤資訊,並不意味著您的程式中有 100 個錯誤。當編譯器遇到錯誤時,它會被暫時打亂。它嘗試恢復並在第一個錯誤之後繼續,但有時會失敗,並報告虛假的錯誤。

一般來說,只有第一個錯誤資訊是可靠的。我建議您一次只修復一個錯誤,然後重新編譯程式。您可能會發現一個分號“修復”了 100 個錯誤。當然,如果您看到多個合法的錯誤資訊,您不妨在每次編譯嘗試中修復多個錯誤。


我收到一個奇怪的編譯器資訊,它不會消失。

[編輯 | 編輯原始碼]

首先,仔細閱讀錯誤資訊。它用簡潔的術語寫成,但通常會隱藏一些資訊。

如果沒有其他,該資訊會告訴您程式中的問題出現在哪裡。實際上,它告訴您編譯器在注意到問題時在哪裡,這並不一定是錯誤所在的位置。將編譯器提供的資訊作為指導,但如果您沒有在編譯器指向的地方看到錯誤,請擴大搜索範圍。

通常錯誤會出現在錯誤資訊之前,但有些情況下它會完全出現在其他地方。例如,如果您在方法呼叫處收到錯誤資訊,實際錯誤可能是在方法定義中。

如果您正在逐步構建程式,您應該對錯誤所在的位置有一個很好的瞭解。它將是您新增的最後一行程式碼。

如果您正在從書中複製程式碼,請首先仔細比較您的程式碼和書中的程式碼。檢查每一個字元。同時,請記住這本書可能是錯誤的,因此,如果您看到類似語法錯誤的東西,它可能是錯誤的。

如果您沒有很快找到錯誤,請深呼吸,更廣泛地檢視整個程式。現在是仔細檢視整個程式並確保它正確縮排的好時機。我不會說良好的縮排使查詢語法錯誤變得容易,但糟糕的縮排確實會使它變得更難。

現在,開始檢查程式碼中常見的語法錯誤。

syntax

enumerate

檢查所有括號和方括號是否平衡並正確巢狀。所有方法定義都應巢狀在類定義中。所有程式語句都應在方法定義中。

請記住,大寫字母與小寫字母不同。

檢查語句末尾是否有分號(並且在花括號之後沒有分號)。

確保程式碼中的所有字串都有匹配的引號(並且您使用雙引號,而不是單引號)。

對於每個賦值語句,確保左側的型別與右側的型別相同。

對於每個方法呼叫,確保您提供的引數順序正確,型別正確,並且您正在呼叫方法的物件型別正確。

如果您正在呼叫一個有返回值的方法,請確保您對結果做了一些操作。如果您正在呼叫一個無返回值的方法,請確保您沒有嘗試對結果做任何操作。

如果您正在呼叫一個物件方法,請確保您正在對具有正確型別的物件呼叫該方法。如果您正在從定義該方法的類外部呼叫一個類方法,請確保您指定了類名。

在物件方法內部,您可以引用例項變數,而無需指定物件。如果您嘗試在類方法中這樣做,您將收到一條令人困惑的資訊,如“靜態引用非靜態變數”。

enumerate

如果什麼都不起作用,請繼續下一節...


無論我做什麼,都無法讓我的程式編譯。

[編輯 | 編輯原始碼]

如果編譯器說有錯誤而您沒有看到它,可能是因為您和編譯器沒有檢視相同的程式碼。檢查您的開發環境,確保您正在編輯的程式是編譯器正在編譯的程式。如果您不確定,請嘗試在程式開頭新增一個明顯的故意語法錯誤。現在重新編譯。如果編譯器沒有找到新錯誤,可能是您的專案設定方式有問題。

否則,如果您已經徹底檢查了程式碼,是時候採取極端措施了。您應該從一個可以編譯的程式重新開始,然後逐步新增您的程式碼。

itemize

將您正在處理的檔案複製一份。如果您正在處理 Fred.java,請複製一份名為 Fred.java.old 的副本。

從 Fred.java 中刪除大約一半的程式碼。再次嘗試編譯。

itemize

如果程式現在可以編譯,那麼您就知道錯誤在另一半中。將您刪除的大約一半程式碼放回,然後重複。

如果程式仍然無法編譯,則錯誤一定在這部分程式碼中。刪除大約一半的程式碼,然後重複。

itemize

找到並修復錯誤後,開始逐步恢復您刪除的程式碼。

itemize

這個過程被稱為“二分法除錯”。作為替代方案,您可以註釋掉程式碼塊,而不是刪除它們。但是,對於非常棘手的語法問題,我認為刪除更可靠——您不必擔心註釋的語法,並且透過使程式更小,您會使它更易讀。

bisection!debugging by
debugging by bisection

執行時錯誤

[編輯 | 編輯原始碼]

我的程式掛起了。

[編輯 | 編輯原始碼]
infinite loop
infinite recursion
hanging

如果一個程式停止並且似乎沒有做任何事情,我們說它“掛起了”。通常這意味著它陷入無限迴圈或無限遞迴。

itemize

如果您懷疑某個特定迴圈是問題所在,請在迴圈之前新增一個列印語句,該語句說“進入迴圈”,並在迴圈之後新增另一個語句說“退出迴圈”。

執行程式。如果您收到第一條訊息,但沒有收到第二條訊息,那麼您遇到了無限迴圈。轉到標題為“無限迴圈”的部分。

大多數情況下,無限遞迴會導致程式執行一段時間然後產生 StackOverflowException。如果發生這種情況,請轉到標題為“無限遞迴”的部分。

如果您沒有收到 StackOverflowException,但您懷疑遞迴方法存在問題,您仍然可以使用無限遞迴部分中的技術。

如果這些方法都沒有奏效,請開始測試其他迴圈和其他遞迴方法。

如果這些方法都沒有奏效,那麼您可能不瞭解程式中執行流程。轉到標題為“執行流程”的部分。

itemize


無限迴圈
[編輯 | 編輯原始碼]

如果您認為您遇到了無限迴圈,並且認為您知道是哪個迴圈導致了問題,請在迴圈末尾新增一個列印語句,列印條件中變數的值和條件的值。

例如,

逐字

   while (x > 0 && y < 0) 
       // do something to x
       // do something to y
       System.out.println ("x: " + x);
       System.out.println ("y: " + y);
       System.out.println ("condition: " + (x > 0 && y < 0));
   

逐字

現在,當您執行程式時,您將看到迴圈每次執行時的三行輸出。最後一次迴圈時,條件應該為假。如果迴圈繼續進行,您將能夠看到 x 和 y 的值,並且您可能會弄清楚為什麼它們沒有正確更新。


無限遞迴
[編輯 | 編輯原始碼]

大多數情況下,無限遞迴會導致程式執行一段時間然後產生 StackOverflowException。

如果您懷疑該方法會導致無限遞迴,請首先檢查以確保存在基本情況。換句話說,應該存在導致方法返回而不會進行遞迴呼叫的條件。如果沒有,那麼您需要重新考慮演算法並確定基本情況。

如果存在基本情況,但程式似乎沒有達到它,請在方法開頭新增一個列印語句,列印引數。現在,當您執行程式時,您將看到每次呼叫方法時的幾行輸出,並且您將看到引數。如果引數沒有朝著基本情況移動,您將獲得一些關於原因的線索。


執行流程
[編輯 | 編輯原始碼]
flow of execution

如果您不確定執行流程如何在程式中移動,請在每個方法的開頭新增列印語句,並顯示類似“進入方法 foo”的訊息,其中foo 是方法的名稱。

現在,當您執行程式時,它將列印每個方法呼叫時的跟蹤資訊。

在方法被呼叫時列印每個方法接收的引數通常很有用。當您執行程式時,請檢查引數是否合理,並檢查是否存在經典錯誤——引數順序錯誤。


當我執行程式時,我遇到了一個異常。

[編輯 | 編輯原始碼]
Exception

如果在執行時出現錯誤,Java 執行時系統會列印一條訊息,其中包含異常的名稱、發生問題的程式行以及堆疊跟蹤。

堆疊跟蹤包括當前正在執行的方法,然後是呼叫它的方法,然後是呼叫它的方法,依此類推。換句話說,它追蹤了導致您到達當前位置的方法呼叫路徑。

第一步是檢查程式中發生錯誤的位置,看看您是否能弄清楚發生了什麼。

描述

[NullPointerException:] 您嘗試訪問當前為 null 的物件的例項變數或呼叫物件的方法。您應該弄清楚哪個變數為 null,然後弄清楚它是怎麼變成這樣的。

請記住,當您用物件型別宣告變數時,它最初為 null,直到您為它分配一個值。例如,以下程式碼會導致 NullPointerException

逐字 Point blank; System.out.println (blank.x); 逐字

[ArrayIndexOutOfBoundsException:] 您用來訪問陣列的索引為負數或大於 array.length-1。如果您能找到問題所在,請在它之前新增一個列印語句,列印索引的值和陣列的長度。陣列大小是否正確?索引是否正確?

現在,從程式中向後追蹤,看看陣列和索引是從哪裡來的。找到最近的賦值語句,看看它是否按預期工作。

如果兩者都是引數,請轉到呼叫方法的位置,看看這些值是從哪裡來的。

[StackOverFlowException:] 參見“無限遞迴”。

描述


我添加了太多列印語句,導致我被輸出淹沒。

[編輯 | 編輯原始碼]
print statement
statement!print

使用列印語句進行除錯的其中一個問題是,您最終可能會被輸出淹沒。有兩種方法可以繼續:簡化輸出或簡化程式。

為了簡化輸出,您可以刪除或註釋掉沒有幫助的列印語句,或者將它們合併,或者格式化輸出以便更容易理解。

為了簡化程式,您可以做幾件事。首先,縮小程式正在處理的問題範圍。例如,如果您正在對陣列進行排序,請對一個小的陣列進行排序。如果程式從使用者那裡獲取輸入,請提供最簡單的輸入,該輸入會導致錯誤。

其次,清理程式。刪除無用程式碼並重新組織程式,使其儘可能易讀。例如,如果您懷疑錯誤位於程式的巢狀層級較深的區域,請嘗試使用更簡單的結構重寫該部分程式碼。如果您懷疑一個大型方法,請嘗試將其拆分為更小的方法並分別測試它們。

通常,找到最小測試用例的過程會引導您找到錯誤。例如,如果您發現程式在陣列具有偶數個元素時可以正常工作,但在陣列具有奇數個元素時無法正常工作,那麼這會給您關於發生情況的線索。

同樣,重寫一段程式碼可以幫助您找到細微的錯誤。如果您進行了您認為不會影響程式的更改,但它確實影響了,那麼這會提醒您。


語義錯誤

[編輯 | 編輯原始碼]

我的程式無法正常工作。

[編輯 | 編輯原始碼]

在某些方面,語義錯誤是最難的,因為編譯器和執行時系統沒有提供有關錯誤的資訊。只有您知道程式應該做什麼,也只有您知道它沒有按照預期工作。

第一步是在程式文字和您看到的行為之間建立聯絡。您需要一個關於程式實際在做什麼的假設。使這變得困難的一件事是計算機執行速度非常快。您經常希望能夠將程式速度降低到人類速度,但沒有直接的方法可以做到這一點,即使有,這也不是一個很好的除錯方法。

以下是一些需要自問的問題

itemize

程式是否應該做某事,但似乎沒有發生?找到執行該功能的程式碼部分,並確保它在您認為它應該執行的時候執行。在可疑方法的開頭新增一個列印語句。

是否發生了不應該發生的事情?找到程式中執行該功能的程式碼,看看它是否在不應該執行的時候執行。

程式碼部分是否產生了與您預期的效果不同的效果?確保您理解所討論的程式碼,尤其是當它涉及呼叫內建 Java 方法時。閱讀您呼叫的方法的文件。嘗試透過直接呼叫方法並使用簡單的測試用例來呼叫方法,並檢查結果。

itemize

為了程式設計,你需要對程式的工作原理有一個心理模型。如果你的程式沒有按預期工作,很多時候問題不在程式本身,而在於你的心理模型。

model!mental
mental model

修正心理模型的最佳方法是將程式分解成各個元件(通常是類和方法),並獨立測試每個元件。一旦你發現模型與現實之間的差異,你就可以解決問題。

當然,你應該在開發程式的同時構建和測試元件。如果你遇到問題,應該只有一小部分新程式碼是未知的。

以下是一些你可能想檢查的常見語義錯誤

itemize

如果你在 if、while 或 for 語句的條件中使用賦值運算子 =,而不是相等運算子 ==,你可能會得到一個語法上合法的表示式,但它沒有按預期執行。

當你在物件上應用相等運算子 == 時,它檢查的是淺層相等性。如果你想檢查深層相等性,你應該使用 equals 方法(或者為使用者定義的物件定義一個)。

一些 Java 庫要求使用者定義的物件定義像 equals 這樣的方法。如果你沒有自己定義它們,你將從父類繼承預設行為,這可能不是你想要的。

總的來說,繼承會導致微妙的語義錯誤,因為你可能會在沒有意識到的情況下執行繼承的程式碼。同樣,確保你理解程式中的執行流程。

itemize


我有一個很大的複雜表示式,它沒有按預期執行。

[edit | edit source]
expression!big and hairy

編寫複雜的表示式是可以的,只要它們可讀,但它們可能難以除錯。將複雜表示式分解成一系列對臨時變數的賦值通常是一個好主意。

例如

verbatim rect.setLocation (rect.getLocation().translate

                    (-rect.getWidth(), -rect.getHeight()));

逐字

可以改寫為

verbatim int dx = -rect.getWidth(); int dy = -rect.getHeight(); Point location = rect.getLocation(); Point newLocation = location.translate (dx, dy); rect.setLocation (newLocation); verbatim

顯式版本更容易閱讀,因為變數名提供了額外的文件,並且更容易除錯,因為我們可以檢查中間變數的型別並顯示它們的值。

temporary variable
variable!temporary
order of evaluation
precedence

大型表示式可能出現的另一個問題是,求值的順序可能不是你預期的。例如,如果你正在翻譯表示式

into Java, you might write

verbatim double y = x / 2 * Math.PI; verbatim

這是不正確的,因為乘法和除法具有相同的優先順序,並且從左到右求值。所以這個表示式計算的是。

調試表達式的有效方法是新增括號來使求值的順序明確。

verbatim double y = x / (2 * Math.PI); verbatim

任何時候你不確定求值的順序,都使用括號。程式不僅會是正確的(在做你想做的事情的意義上),而且對於沒有記住優先順序規則的其他人來說也更容易閱讀。


我有一個方法沒有返回我期望的值。

[edit | edit source]
return statement
statement!return

如果你有一個帶有複雜表示式的 return 語句,你沒有機會在返回之前列印返回值。同樣,你可以使用一個臨時變數。例如,而不是

verbatim public Rectangle intersection (Rectangle a, Rectangle b)

   return new Rectangle (
       Math.min (a.x, b.x),
       Math.min (a.y, b.y),
       Math.max (a.x+a.width, b.x+b.width)-Math.min (a.x, b.x)
       Math.max (a.y+a.height, b.y+b.height)-Math.min (a.y, b.y) );

逐字

你可以寫

verbatim public Rectangle intersection (Rectangle a, Rectangle b)

   int x1 = Math.min (a.x, b.x);
   int y2 = Math.min (a.y, b.y);
   int x2 = Math.max (a.x+a.width, b.x+b.width);
   int y2 = Math.max (a.y+a.height, b.y+b.height);
   Rectangle rect = new Rectangle (x1, y1, x2-x1, y2-y1);
   return rect;

逐字

現在你可以在返回之前顯示任何中間變數。


我的列印語句沒有起作用

[edit | edit source]
print statement
statement!print

如果你使用 println 方法,輸出會立即顯示,但如果你使用 print(至少在某些環境中),輸出會儲存起來,直到下一個換行符輸出才會顯示。如果程式在沒有產生換行符的情況下終止,你可能永遠看不到儲存的輸出。

如果你懷疑這種情況正在發生,嘗試將所有 print 語句更改為 println。


我真的,真的卡住了,我需要幫助

[edit | edit source]

首先,嘗試離開電腦幾分鐘。電腦會發出影響大腦的波,引起以下症狀

itemize

沮喪和/或憤怒。

迷信(“電腦討厭我”)和魔法思維(“只有當我戴著帽子倒著的時候程式才會執行”)。

隨機漫步程式設計(嘗試透過編寫所有可能的程式並選擇做正確事情的程式來進行程式設計)。

itemize

如果你發現自己患有這些症狀中的任何一種,站起來去散散步。當你冷靜下來後,思考一下程式。它在做什麼?這種行為的可能原因是什麼?你上次擁有一個可執行的程式是什麼時候,接下來你做了什麼?

有時找到一個錯誤只需要時間。我經常在遠離電腦的時候,讓我的思緒漫無目的地遊走的時候找到錯誤。找到錯誤的一些最佳地點是火車、淋浴和床上,就在你快要睡著的時候。


不,我真的需要幫助。

[edit | edit source]

這種情況很常見。即使是最優秀的程式設計師也會偶爾陷入困境。有時你會長時間地在一個程式上工作,以至於你看不到錯誤。一雙新的眼睛就是解決問題的方法。

在你請別人幫忙之前,確保你已經用盡了這裡描述的技術。你的程式應該儘可能簡單,你應該在導致錯誤的最小輸入上工作。你應該在適當的地方新增列印語句(並且它們產生的輸出應該是可以理解的)。你應該對這個問題有足夠的瞭解,能夠簡潔地描述它。

當你請別人幫忙時,一定要給他們他們需要的資訊。

itemize

是什麼型別的錯誤?編譯時、執行時還是語義錯誤?

如果錯誤發生在編譯時或執行時,錯誤訊息是什麼,它指示程式的哪一部分?

在你遇到這個錯誤之前你最後做了什麼?你最後寫的幾行程式碼是什麼,或者哪個新的測試用例失敗了?

你嘗試了什麼,你學到了什麼?

itemize

當你找到錯誤時,花點時間想想你本可以做什麼來更快地找到它。下次你看到類似的東西時,你將能夠更快地找到錯誤。

記住,在這個課程中,目標不是讓程式執行。目標是學習如何讓程式執行。程式開發計劃

如果你花費大量時間進行除錯,可能是因為你沒有一個有效的程式開發計劃。

一個典型的、糟糕的程式開發計劃是這樣的

enumerate

編寫一個完整的函式。

編寫幾個更多的函式。

嘗試編譯程式。

花費一個小時查詢語法錯誤。

花費一個小時查詢執行時錯誤。

花費三個小時查詢語義錯誤。

enumerate

當然,問題出在第一步和第二步。如果你在開始除錯過程之前編寫了多個函式,甚至一個完整的函式,你可能會編寫比你能夠除錯的更多的程式碼。

如果你發現自己身處這種情況,唯一的解決辦法是刪除程式碼,直到你再次獲得一個可執行的程式,然後逐漸重建程式。初級程式設計師往往不願意這樣做,因為他們精心編寫的程式碼對他們來說是寶貴的。為了有效地除錯,你必須毫不留情!


以下是一個更好的程式開發計劃

enumerate

從一個可執行的程式開始,它可以做一些可見的事情。

  like printing something.

一次新增少量程式碼。

  and test the program after every change.

重複,直到程式執行它應該執行的操作。

enumerate

在每次更改後,程式應該產生一些可見的效果,以演示新的程式碼。這種程式設計方法可以節省大量時間。因為你一次只新增幾行程式碼,所以很容易找到語法錯誤。此外,因為每個程式版本都產生一個可見的結果,所以你不斷地測試你對程式工作原理的心理模型。如果你的心理模型有誤,你將在編寫大量錯誤程式碼之前遇到衝突(並有機會糾正它)。

這種方法的一個問題是,通常很難找到從起點到完整且正確程式的路徑。

我將透過開發一個名為 isIn 的函式來演示,它接受一個字串和一個向量,並返回一個布林值:如果字串出現在列表中則為真,否則為假。

enumerate

第一步是編寫最短的函式,它可以編譯、執行並執行一些可見的操作

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn");
   return false;

逐字

當然,為了測試這個方法,我們必須呼叫它。在 main 函式中,或是在工作程式中的其他地方,我們需要建立一個簡單的測試用例。

我們將從字串出現在向量中的情況開始(因此我們預計結果為真)。

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("banana");
   boolean test = isIn ("banana", v);
   System.out.println (test);

逐字

如果一切按計劃進行,這段程式碼將編譯、執行並列印單詞 isIn 和值 false。當然,答案是不正確的,但在這一點上我們知道方法正在被呼叫並返回了一個值。

在我的程式設計生涯中,我浪費了太多時間除錯一個方法,最後才發現它從未被呼叫。如果我使用了這種開發計劃,這種情況就不會發生。

下一步是檢查方法接收的引數。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   return false;

逐字

第一個列印語句讓我們可以確認 isIn 正在查詢正確的單詞。第二個語句列印了向量中元素的列表。

為了使事情更有趣,我們可以向向量中新增更多元素。

verbatim public static void main (String[] args)

   Vector v = new Vector ();
   v.add ("apple");
   v.add ("banana");
   v.add ("grapefruit");
   boolean test = isIn ("banana", v);
   System.out.println (test);

逐字

現在輸出看起來像這樣

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] verbatim

列印引數可能看起來很愚蠢,因為我們知道它們應該是什麼。重點是確認它們是我們認為的。


為了遍歷向量,我們可以利用 Section vector 中的程式碼。一般來說,重用程式碼片段而不是從頭開始編寫程式碼是一個好主意。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
   
   return false;

逐字

現在當我們執行程式時,它會一次列印一個向量的元素。如果一切順利,我們可以確認迴圈檢查了向量的所有元素。


到目前為止,我們還沒有過多考慮這個方法將要做什麼。在這一點上,我們可能需要想出一個演算法。最簡單的演算法是線性搜尋,它遍歷向量並將每個元素與目標單詞進行比較。

幸運的是,我們已經編寫了遍歷向量的程式碼。像往常一樣,我們將透過一次新增幾行程式碼來繼續。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
       
   
   return false;

逐字

和往常一樣,我們使用 equals 方法來比較字串,而不是 == 運算子!

同樣,我添加了一個列印語句,以便在新的程式碼執行時產生可見的效果。

在這一點上,我們已經非常接近可工作的程式碼了。下一個更改是從方法中返回,如果我們找到了我們正在尋找的東西。

verbatim public static boolean isIn (String word, Vector v)

   System.out.println ("isIn looking for " + word);
   System.out.println ("in the vector " + v);
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           System.out.println ("found it");
           return true;
       
   
   return false;

逐字

如果我們找到了目標單詞,我們返回 true。如果我們在整個迴圈中都沒有找到它,那麼正確的返回值是 false。

如果我們在這一點上執行程式,我們應該得到

verbatim isIn looking for banana in the vector [apple, banana, grapefruit] apple banana found it true verbatim


下一步是確保其他測試用例正常工作。首先,我們應該確認如果向量中沒有這個詞,該方法返回 false。

然後我們應該檢查一些典型的麻煩製造者,比如一個空向量(大小為 0 的向量)和一個只有一個元素的向量。此外,我們還可以嘗試給方法一個空字串。

和往常一樣,這種測試可以幫助發現錯誤(如果有的話),但它不能告訴你方法是否正確。

倒數第二步是刪除或註釋掉列印語句。

verbatim public static boolean isIn (String word, Vector v)

   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

逐字

如果您認為您可能需要稍後重新訪問此方法,註釋掉列印語句是一個好主意。但如果這是該方法的最終版本,並且您確信它是正確的,您應該刪除它們。

刪除註釋可以讓您最清晰地看到程式碼,這可以幫助您發現任何剩餘的問題。

如果程式碼中有任何不清楚的地方,您應該添加註釋來解釋它。抵制逐行翻譯程式碼的誘惑。例如,沒有人需要這個

逐字

       // if word equals s, return true
       if (word.equals (s)) 
           return true;
       

逐字

您應該使用註釋來解釋非顯而易見的程式碼,警告可能導致錯誤的條件,以及記錄任何內建到程式碼中的假設。此外,在每個方法之前,最好寫一個抽象描述該方法的功能。


最後一步是檢查程式碼,看看您是否能說服自己它是正確的。

在這一點上,我們知道該方法在語法上是正確的,因為它可以編譯。

為了檢查執行時錯誤,您應該找到每個可能導致錯誤的語句,並找出導致錯誤的條件。

這個方法中可能產生執行時錯誤的語句是

tabularl l v.size() & if v is null. word.equals (s) & if word is null. (String) v.get(i) & if v is null or i is out of

                         bounds, 
                       & or the th element of v is not
                         a String.

tabular

由於我們從引數中獲得 v 和 word,因此無法避免前兩個條件。我們能做的最好的就是檢查它們。

verbatim public static boolean isIn (String word, Vector v)

   if (v == null  word == null) return false;
   for (int i=0; i<v.size(); i++) 
       System.out.println (v.get(i));
       String s = (String) v.get(i);
       if (word.equals (s)) 
           return true;
       
   
   return false;

逐字

一般來說,方法最好確保其引數是合法的。

instanceof operator
operator!instanceof

for 迴圈的結構確保 i 始終介於 0 和 v.size()-1 之間。但是,沒有辦法確保 v 的元素是字串。另一方面,我們可以一邊走一邊檢查它們。instanceof 運算子檢查物件是否屬於某個類。

逐字

   Object obj = v.get(i);
   if (obj instanceof String) 
       String s = (String) v.get(i);
   

逐字

這段程式碼從向量中獲取一個物件,並檢查它是否是一個字串。如果是,它會執行型別轉換並將字串賦給 s。

作為練習,修改 isIn,以便如果它在向量中找到一個不是字串的元素,它會跳到下一個元素。

如果我們處理了所有問題條件,我們就可以證明這個方法不會導致執行時錯誤。

我們還沒有證明該方法在語義上是正確的,但透過增量方式進行,我們已經避免了許多可能的錯誤。例如,我們已經知道該方法正在正確地接收引數,並且迴圈遍歷了整個向量。我們也知道它正在成功地比較字串,並在找到目標單詞時返回 true。最後,我們知道如果迴圈退出,目標單詞不可能在向量中。

除了正式證明之外,這可能是我們能做的最好的了。

華夏公益教科書