本文深入探討了Java多米諾記憶游戲開發中常見的兩個關鍵問題:對象比較不當導致的多米諾牌無法正確匹配,以及游戲狀態(多米諾牌揭示狀態)未及時更新導致游戲無法結束。通過詳細解析 equals() 和 hashCode() 方法的正確覆寫,以及在游戲邏輯中有效管理對象狀態,本教程旨在幫助開發者構建功能完善、邏輯嚴謹的Java記憶游戲。
1. 問題分析與根源
在開發基于對象的記憶游戲時,兩個常見的問題可能導致游戲行為異常:
- 對象比較不準確: 當需要判斷兩個游戲元素(如多米諾牌)是否“相等”時,如果僅僅使用 == 運算符,它會比較對象的內存地址,而非其內部屬性值。這導致即使兩張多米諾牌的數字相同,程序也可能認為它們不匹配。此外,即使嘗試使用 equals() 方法,如果該方法在自定義類中未被正確覆寫,其默認行為通常與 == 相同,即比較引用。
- 游戲狀態未更新: 游戲元素的內部狀態(例如多米諾牌是否已被揭示)未能根據游戲規則及時更新。這會導致即使玩家猜對了,多米諾牌也不會保持翻開狀態,進而影響游戲結束條件的判斷。
針對上述問題,我們將以一個Java多米諾記憶游戲為例,詳細講解如何通過覆寫 equals() 和 hashCode() 方法以及正確管理對象狀態來解決這些問題。
2. 解決對象比較問題:覆寫 equals() 和 hashCode()
在Java中,當我們需要根據對象的實際內容(而非內存地址)來判斷它們是否相等時,必須在自定義類中覆寫 Object 類的 equals() 方法。同時,為了遵循Java約定和確保集合類(如 HashMap, HashSet)的正確行為,當覆寫 equals() 時,也必須覆寫 hashCode() 方法。
2.1 覆寫 Domino.equals() 方法
原始的 Domino 類中的 equals() 方法存在邏輯錯誤:它只檢查 top 和 bottom 是否相等,這導致只有雙面牌(如 [2][2])才會被認為是相等的,而不同位置但值相同的牌則無法匹配。
public boolean equals(Domino other) { // 錯誤的覆寫方式 if (top == bottom) // 錯誤:只檢查自身是否為雙面牌 return true; return false; }
正確的 equals() 覆寫應該比較當前 Domino 對象與傳入的 Object 對象(在類型轉換后)的 top 和 bottom 屬性。
立即學習“Java免費學習筆記(深入)”;
@Override public boolean equals(Object obj) { // 1. 檢查是否是同一個對象的引用 if (this == obj) { return true; } // 2. 檢查傳入對象是否為null或類型不匹配 if (obj == null || getClass() != obj.getClass()) { // 或者使用 !(obj instanceof Domino) return false; } // 3. 類型轉換 final Domino other = (Domino) obj; // 4. 比較關鍵屬性 if (this.getTop() != other.getTop()) { return false; } if (this.getBottom() != other.getBottom()) { return false; } return true; }
注意事項:
- @Override 注解是可選的,但強烈建議使用,它能幫助編譯器檢查你是否正確覆寫了父類方法。
- equals() 方法的參數類型必須是 Object,否則它不是真正的覆寫,而是一個重載方法。
- 在比較屬性時,考慮到 Domino 構造函數已經保證了 top
2.2 覆寫 Domino.hashCode() 方法
根據Java約定,如果兩個對象通過 equals() 方法判斷為相等,那么它們的 hashCode() 方法必須返回相同的值。覆寫 hashCode() 的目的是為相等的對象生成一致的哈希碼,這對于基于哈希的集合(如 HashMap, HashSet)的正確運行至關重要。
@Override public int hashCode() { int hash = 7; // 任意一個非零常數 hash = 59 * hash + this.getTop(); // 將屬性值納入哈希計算 hash = 59 * hash + this.getBottom(); return hash; }
注意事項:
- 哈希碼的計算應基于參與 equals() 比較的所有屬性。
- 使用質數(如59)進行乘法運算有助于減少哈希沖突。
- 一個好的 hashCode() 實現應盡可能地將不相等的對象分散到不同的哈希桶中。
3. 解決游戲狀態更新問題:更新 guess() 方法
多米諾牌在匹配成功后未能保持揭示狀態,是因為 Domino 對象的 revealed 屬性沒有被設置為 true。這個狀態更新的邏輯應該發生在 MemoryLane 類的 guess() 方法中,當判斷兩張牌匹配成功時。
原始的 guess() 方法:
public boolean guess(int i, int k) { if(board[i] == board[k]) { // 錯誤:使用 == 比較對象引用 return true; } return false; }
修正后的 guess() 方法不僅要使用 equals() 進行對象內容比較,還要在匹配成功后調用 setRevealed(true) 來更新多米諾牌的狀態。
public boolean guess(int i, int k) { // 使用覆寫后的 equals 方法進行內容比較 if (board[i].equals(board[k])) { // 如果匹配成功,設置這兩張多米諾牌為已揭示狀態 board[i].setRevealed(true); board[k].setRevealed(true); return true; } // 如果不匹配,不需要做任何狀態改變,直接返回false return false; }
4. 簡化 isRevealed() 方法
在 Domino 類中,isRevealed() 方法可以簡化,因為它本質上只是返回 revealed 字段的值。
原始的 isRevealed():
public boolean isRevealed() { if (revealed) return true; return false; }
簡化后的 isRevealed():
public boolean isRevealed() { return revealed; }
5. 完整代碼示例(修正后的關鍵類)
以下是經過上述修正后的 Domino 類和 MemoryLane 類中 guess 方法的關鍵代碼片段。MemoryLaneDriver 類保持不變,因為它已經能夠正確調用游戲邏輯。
5.1 修正后的 Domino 類
public class Domino { private int top, bottom; private boolean revealed; public Domino(int x, int y) { if (x > y) { top = y; bottom = x; } else { top = x; bottom = y; } } public int getTop() { return top; } public int getBottom() { return bottom; } public boolean isRevealed() { return revealed; // 簡化后的方法 } public void setRevealed(boolean revealed) { this.revealed = revealed; } @Override public int hashCode() { int hash = 7; hash = 59 * hash + this.getTop(); hash = 59 * hash + this.getBottom(); return hash; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final Domino other = (Domino) obj; if (this.getTop() != other.getTop()) { return false; } if (this.getBottom() != other.getBottom()) { return false; } return true; } }
5.2 修正后的 MemoryLane 類(僅展示 guess 方法)
import java.util.Arrays; import java.util.Random; public class MemoryLane { private Domino[] board; public MemoryLane(int max) { board = new Domino[(max * max) + max]; int i = 0; for (int top = 1; top <= max; top++) { for (int bot = 1; bot <= max; bot++) { if (top <= bot) { board[i] = new Domino(top, bot); i++; board[i] = new Domino(top, bot); i++; } } } shuffle(); } private void shuffle() { int index; Random random = new Random(); for (int i = board.length - 1; i > 0; i--) { index = random.nextInt(i + 1); if (index != i) { Domino temp = board[index]; board[index] = board[i]; board[i] = temp; } } } // 修正后的 guess 方法 public boolean guess(int i, int k) { if (board[i].equals(board[k])) { // 使用覆寫后的 equals 方法 board[i].setRevealed(true); // 設置為已揭示 board[k].setRevealed(true); // 設置為已揭示 return true; } return false; } public String peek(int a, int b) { String text = ""; // 可以直接初始化為空字符串 text += ("[" + board[a].getTop() + "] [" + board[b].getTop() + "]n"); text += ("[" + board[a].getBottom() + "] [" + board[b].getBottom() + "]n"); return text; } public boolean gameOver() { int count = 0; for (int i = 0; i < board.length; i++) { if (board[i].isRevealed()) { count++; } } return (count == board.length); // 當所有牌都被揭示時,游戲結束 } public String toString() { String text = ""; for (int i = 0; i < board.length; i++) { if (board[i].isRevealed()) { text += ("[" + board[i].getTop() + "] "); } else { text += ("[ ] "); } } text += ('n'); for (int i = 0; i < board.length; i++) { if (board[i].isRevealed()) { text += ("[" + board[i].getBottom() + "] "); } else { text += ("[ ] "); } } return text; } }
6. 總結
通過本次教程,我們深入理解了在Java中進行對象比較和狀態管理的重要性。關鍵點包括:
- 正確覆寫 equals() 和 hashCode(): 這是實現基于對象內容比較的基礎。務必記住,當覆寫 equals() 時,必須同時覆寫 hashCode(),以確保程序行為的正確性和一致性,尤其是在使用集合類時。
- 及時更新對象狀態: 游戲邏輯中,當滿足特定條件時,必須顯式地更新相關對象的內部狀態(例如通過調用 setRevealed(true))。這是確保游戲流程正確推進和結束條件能夠被正確判斷的關鍵。
遵循這些原則,開發者可以構建出功能更健壯、邏輯更清晰的面向對象程序,特別是在游戲開發等需要精細狀態管理和對象比較的場景中。