Chương 13: Mảng chứa các đối tượng

Trở về Mục lục cuốn sách

13.1  Con đường phía trước

Ở ba chương kế tiếp ta sẽ phát triển các chương trình chơi bài tây và với những cỗ bài. Trước khi đi vào chi tiết, sau đây là khái quát những bước đi:

  1. Trong chương này ta sẽ định nghĩa một lớp Card rồi viết các phương thức để hoạt động với đối tượng Card và mảng chứa Card.
  2. Trong Chương 14 ta sẽ tạo lập một lớp Deck rồi viết các phương thức hoạt động với các đối tượng Deck.
  3. Trong Chương 15 tôi sẽ trình bày về lập trình hướng đối tượng (OOP) và ta sẽ chuyển đổi các lớp Card và Deck sang một phong cách giống như hướng đối tượng hơn.

Tôi nghĩ rằng tiến bước theo kiểu này khiến cho con đường đi dễ dàng hơn; song nhược điểm là ta sẽ thấy nhiều phiên bản của cùng đoạn mã lệnh, vì vậy có thể gây nhầm lẫn. Nếu được, bạn có thể tải về mã lệnh cho từng chương trong khi làm. Mã lệnh trong chương này ở đây: http://thinkapjava.com/code/Card1.java.

13.2  Các đối tượng Card

Nếu bạn chưa quen với bài tây, thì giờ là lúc thích hợp để kiểm một bộ, kẻo những gì trong chương này sẽ không có nhiều ý nghĩa. Hoặc bạn hãy đọc lấy http://en.wikipedia.org/wiki/Playing_card.

Có 52 lá bài trong một bộ, mỗi lá bài thuộc về một trong bốn chất và một trong 13 bậc. Các chất gồm Pích, Cơ, Rô, và Nhép (theo thứ tự giảm dần trong trò bridge). Các bậc gồm có A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, và K. Tùy theo trò chơi mà bạn quân A có thể cao hơn K hoặc thấp hơn 2.

Nếu bạn muốn định nghĩa một đối tượng mới để biểu diễn cho lá bài, rõ ràng các thuộc tính phải là: rank (bậc) và suit (chất). Còn việc chọn kiểu dữ liệu cho các thuộc tính lại không hiển nhiên. Một khả năng là dùng các chuỗi gồm những từ như "Spade" (Pích) cho chất và "Queen" cho bậc. Một vấn đề đặt ra với cách làm này là sẽ không dễ so sánh xem lá bài nào có bậc hoặc chất cao hơn.

Một cách khác là dùng số nguyên để đánh số cho các bậc và chất. Ở đây, “đánh số” không có nghĩa là ý mã hóa hoặc dịch thông điệp ra dạng mật mã như nhiều người thường nghĩ. Mà đối với nhà khoa học máy tính, “đánh số” nghĩa là “lập một phép ánh xạ từ con số đến dữ liệu cần biểu thị.” Chẳng hạn:

Spades (Pích) 3
Hearts (Cơ) 2
Diamonds (Rô) 1
Clubs (Nhép) 0

Mã số này giúp so sánh các lá bài dễ hơn; vì chất cao hơn được ánh xạ đến số lớn hơn, và ta có thể so sánh chất bằng cách so các mã số của chúng. Ánh xạ đối với bậc thì khá dễ thấy; mỗi bậc số thì ánh xạ đến chính số nguyên tương ứng, còn với các bậc chữ:

J 11
Q 12
K 13

Ở đây tôi dùng kí hiệu toán học để biểu diễn ánh xạ là do ánh xạ không phải là một phần của chương trình. Đó là một phần của khâu thiết kế chương trình, nhưng không xuất hiện một cách cụ thể trên mã lệnh. Lời định nghĩa lớp cho kiểu Card sẽ như sau:

class Card { 
  int suit, rank; 

  public Card() { 
    this.suit = 0; 
    this.rank = 0; 
  } 

  public Card(int suit, int rank) { 
    this.suit = suit; 
    this.rank = rank; 
  } 

}

Như thường lệ, tôi cung cấp hai constructor: một cái nhận mỗi tham số ứng với từng biến thực thể; cái kia thì không nhận tham số nào.

Để tạo nên một đối tượng biểu diễn lá bài 3 Nhép, ta kích hoạt new:

    Card threeOfClubs = new Card(0, 3);

Đối số thứ nhất, 0 biểu thị chất Nhép.

13.3  Phương thức printCard

Khi bạn tạo nên một lớp mới, bước đầu tiên là khai báo các biến thực thể và viết các constructor. Bước thứ hai là viết những phương thức tiêu chuẩn mà từng đối tượng đều nên có, gồm một phương thức để in đối tượng ra, và một hoặc hai phương thức để so sánh các đối tượng. Ta hãy bắt đầu với printCard.

Để in ra đối tượng Card theo cách mà mọi người dễ đọc, ta cần ánh xạ từ mã số đến các bậc và chất tương ứng. Một cách làm tự nhiên là dùng mảng chứa các chuỗi. Bạn có thể tạo một mảng các chuỗi theo cách giống như đã tạo ra mảng chứa những kiểu dữ liệu nguyên thuỷ:

    String[] suits = new String[4];

Sau đó ta có thể đặt giá trị của các phần tử trong mảng này.

    suits[0] = "Clubs"; 
    suits[1] = "Diamonds"; 
    suits[2] = "Hearts"; 
    suits[3] = "Spades";

Việc tạo ra một mảng và khởi tạo các phần tử trong nó là một thao tác thường gặp đến nỗi Java cung cấp luôn một cú pháp đặc biệt cho nó:

    String[] suits = { "Clubs", "Diamonds", "Hearts", "Spades" };

Câu lệnh này tương đương với các lệnh khai báo, huy động và gán. Sơ đồ trạng thái cho mảng này sẽ như sau:

Các phần tử của mảng này là những tham chiếu đến các chuỗi, thay vì là bản thân các chuỗi.

Bây giờ ta cần một mảng các chuỗi khác để giải mã các bậc của lá bài:

    String[] ranks = { "narf", "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King" };

Lý do có mặt "narf" là để đứng vào chỗ phần tử thứ không của mảng, vốn chẳng bao giờ được dùng đến (hay lẽ ra không có). Các bậc hợp lý chỉ có từ 1–13. Để tránh phần tử thường này, ta đã có thể bắt đầu từ 0, nhưng việc ánh xạ sẽ tự nhiên hơn nếu ta mã hóa 2 là 2, và 3 là 3, v.v.

Với các mảng này, ta có thể chọn được String thích hợp bằng cách dùng chỉ số là suit và rank. Trong phương thức printCard,

  public static void printCard(Card c) { 
    String[] suits = { "Clubs", "Diamonds", "Hearts", "Spades" }; 
    String[] ranks = { "narf", "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King" }; 
    System.out.println(ranks[c.rank] + " of " + suits[c.suit]); 
  }

biểu thức suits[c.suit] có nghĩa là “dùng biến thực thể suit từ đối tượng c làm chỉ số trong mảng có tên suits, rồi chọn chuỗi thích hợp.” Kết quả của đoạn mã này

    Card card = new Card(1, 11); 
    printCard(card);

là Jack of Diamonds.

13.4  Phương thức sameCard

Từ “same” (giống nhau, cùng) là một trong những hiện tượng ngôn ngữ trong tiếng Anh mà có vẻ ngoài quá rõ ràng, nhưng khi bạn suy nghĩ thì sẽ thấy còn có nhiều điều hơn bạn chờ đón ban đầu.

Chẳng hạn, nếu nói rằng “Chris và tôi có cùng (loại) xe,” thì tôi muốn nói rằng hai chiếc xe cùng nhãn hiệu, song là hai chiếc khác nhau. Còn nếu nói “Chris và tôi có cùng mẹ,” thì ý rằng mẹ cậu ta và mẹ tôi cùng là một người. Bởi vậy ý nghĩa của “cùng” thì lại khác nhau tùy theo ngữ cảnh.

Khi nói về các đối tượng, ta cũng gặp sự mập mờ tương tự. Chẳng hạn, nếu hai Card như nhau, thì liệu có nghĩa là chúng có cùng dữ liệu (bậc và chất), hay đó thực ra cùng là một đối tượng Card?

Để xem liệu có phải hai tham chiếu cùng chỉ đến một đối tượng hay không, ta dùng toán tử ==. Chẳng hạn:

    Card card1 = new Card(1, 11); 
    Card card2 = card1; 
    if (card1 == card2) { 
      System.out.println("card1 và card2 giống hệt nhau."); 
    }

Các tham chiếu đến cùng đối tượng thì giống hệt nhau. Còn các tham chiếu đến các đối tượng với dữ liệu như nhau thì sẽ tương đồng với nhau.

Để kiểm tra sự tương đồng, người ta thường viết một phương thức có tên gọi kiểu như sameCard.

  public static boolean sameCard(Card c1, Card c2) { 
    return(c1.suit == c2.suit && c1.rank == c2.rank); 
  }

Sau đây là một ví dụ để tạo nên hai đối tượng có dữ liệu giống nhau, rồi dùng sameCard để kiểm tra xem liệu chúng có tương đồng không:

    Card card1 = new Card(1, 11); 
    Card card2 = new Card(1, 11); 
    if (sameCard(card1, card2)) { 
      System.out.println("card1 and card2 tuong dong nhau."); 
    }

Nếu các tham chiếu giống nhau thì chúng tương đồng. Song nếu chúng tương đồng thì chưa chắc chúng đã giống hệt nhau.

Ở đây, card1 và card2 tương đồng nhưng không giống hệt, cho nên sơ đồ trạng thái sẽ như sau:

Sơ đồ này sẽ trông thế nào nếu card1 và card2 giống hệt nhau?

Ở Mục 8.10, tôi đã nói rằng bạn không nên dùng toán tử == đối với String vì nó sẽ không hoạt động theo ý mình. Thay vì việc so sánh nội dung của các String (so sánh tương đồng), nó lại đi kiểm tra xem hai String này có phải cùng đối tượng (giống hệt) không.

13.5  Phương thức compareCard

Với những kiểu nguyên thủy, các toán tử điều kiện so sánh hai giá trị rồi quyết định xem cái nào lớn hay nhỏ hơn các kia. Những toán tử như vậy (< và > cùng những cái khác) không hoạt động được với kiểu đối tượng. Với các chuỗi, Java cung cấp một phương thức compareTo. Còn với Cards thì ta phải tự viết phương thức riêng, mà ta sẽ gọi là compareCard. Sau này, ta sẽ dùng phương thức này để sắp xếp một cỗ bài.

Có những tập hợp được xếp thứ tự hoàn toàn, theo nghĩa là bạn có thể so sánh hai phần tử bất kì trong đó để biết được phần tử nào lớn hơn. Lại có những tập hợp không sắp xếp được, theo nghĩa là chẳng có nghĩa lý gì để nói rằng phần tử này lớn hơn phần tử kia. Các số nguyên và số phẩy động là loại thứ tự hoàn toàn. Còn các loại trái cây là không có thứ tự, vì vậy mà ta không thể so sánh táo với cam được. Trong Java, kiểu boolean là không thứ tự; ta không thể nói rằng true lớn hơn false.

Tập hợp các lá bài thì lại phần nào được xếp thứ tự, có nghĩa rằng đôi khi ta có thể so sánh lá bài và đôi khi không. Chẳng hạn, tôi biết rằng cây 3 Nhép thì cao hơn 2 Nhép và 3 Rô thì cao hơn 3 Nhép. Nhưng lá bài nào hơn, 3 Nhép hay 2 Rô? Một lá thì có bậc cao hơn, nhưng lá kia thì có chất cao hơn.

Để làm cho các lá bài so sánh được với nhau, ta phải quyết định xem thứ nào quan trọng hơn, bậc hay chất. Cách lựa chọn là tùy ý, nhưng khi bạn mua một cỗ bài mới, thì các quấn Nhép được xếp cạnh nhau, sau đó là các quân Rô, rồi cứ như vậy. Bởi thế ta hãy coi rằng chất thì quan trọng hơn.

Khi đã quyết định như vậy, ta có thể viết compareCard. Phương thức này nhận tham số là hai Card rồi trả lại 1 nếu lá bài thứ nhất hơn, -1 nếu lá bài thứ hai hơn, và 0 nếu chúng tương đồng.

Trước tiên, ta so sánh chất:

    if (c1.suit > c2.suit) return 1; 
    if (c1.suit < c2.suit) return -1;

Nếu hai câu lệnh trên chẳng có câu lệnh nào đúng, thì các chất phải bằng nhau, và ta phải so sánh bậc:

    if (c1.rank > c2.rank) return 1; 
    if (c1.rank < c2.rank) return -1;

Nếu lại chẳng có câu nào đúng, thì hai bậc phải bằng nhau, và vì vậy ta phải trả lại 0.

13.6  Mảng các lá bài

Đến giờ ta đã thấy một vài ví dụ về phép hợp (khả năng kết hợp những đặc điểm của ngôn ngữ lập trình theo nhiều cách bố trí khác nhau). Một trong những ví dụ đầu tiên ta bắt gặp là việc dùng phép kích hoạt phương thức như là một phần của biểu thức. Một ví dụ khác là cấu trúc lồng ghép gồm các câu lệnh: bạn có thể đặt một lệnh if bên trong một vòng lặp while, hay bên trong một lệnh if khác, v.v.

Khi đã biết được dạng như vậy, và đã học được về mảng và đối tượng, thì có lẽ bạn chẳng ngạc nhiên khi được biết rằng ta có thể tạo nên mảng chứa những đối tượng. Và bạn có thể định nghĩa những đối tượng có biến thực thể là các mảng; bạn có thể lập nên những mảng chứa mảng khác; bạn có thể định nghĩa đối tượng chứa đối tượng khác, v.v. Trong hai chương tiếp theo, ta sẽ thấy những ví dụ về cách kết hợp như vậy trên cơ sở các đối tượng Card.

Ví dụ này tạo nên một mảng gồm 52 quân bài:

    Card[] cards = new Card[52];

Sau đây là sơ đồ trạng thái cho đối tượng này:

Mảng hiện tại có chứa các tham chiếu đến đối tượng; nó không chứa bản thân các đối tượng Card. Những phần tử này đều được khởi tạo về null. Bạn có thể truy cập từng phần tử trong mảng theo cách thông thường:

    if (cards[0] == null) { 
      System.out.println("Chưa có quân bài nào!"); 
    }

Nhưng nếu bạn cố thử truy cấp các biến thực thể của những Card chưa tồn tại, bạn sẽ nhận được biệt lệ NullPointerException.

    cards[0].rank; // NullPointerException

Nhưng đó lại là cú pháp đúng để truy cập rank (bậc) của lá bài “thứ không” trong cỗ. Đây là một ví dụ khác của phép hợp, bằng cách kết hợp cú pháp truy cập phần tử của mảng và truy cập một biến thực thể của đối tượng.

Cách dễ nhất để điền những đối tượng Card đầy vào cỗ bài là viết những vòng lặp for lồng ghép (nghĩa là vòng lặp này đặt trong vòng lặp khác):

    int index = 0; 
    for (int suit = 0; suit <= 3; suit++) { 
      for (int rank = 1; rank <= 13; rank++) { 
        cards[index] = new Card(suit, rank); 
        index++; 
      } 
    }

Vòng lặp ngoài cùng đếm các chất từ 0 tới 3. Với từng chất, vòng lặp trong đếm các bậc từ 1 đến 13. Vì vòng lặp ngoài chạy 4 lần, và vòng lặp trong chạy 13 lần, nên phần thân được thực hiện 52 lần.

Tôi đã dùng index để theo dõi lá bài tiếp theo sẽ cần phải đặt vào đâu trong cỗ bài. Sơ đồ trạng thái sau đây cho thấy cỗ bài như thế nào sau khi hai lá bài đầu tiên được huy động:

13.7  Phương thức printDeck

Khi làm việc với mảng, cách tiện lợi là có một phương thức để in ra nội dung. Ta đã vài lần thấy đượng dạng mẫu cho việc duyệt mảng, bởi vậy phương thức sau sẽ quen thuộc đối với bạn:

  public static void printDeck(Card[] cards) { 
    for (int i = 0; i < cards.length; i++) { 
      printCard(cards[i]); 
    } 
  }

cards có kiểu là Card[], nên một phần tử của cards thì có kiểu là Card. Bởi vậy cards[i] là một đối số hợp lệ cho printCard.

13.8  Tìm kiếm

Phương thức tiếp theo mà tôi sẽ viết là findCard, để tìm kiếm trong một mảng chứa Card, xem liệu rằng mảng này có chứa một lá bài cụ thể hay không. Phương thức này cho tôi một cơ hội biểu diễn hai thuật toán: tìm kiếm tuyến tính và tìm kiếm phân đôi.

Tìm kiếm tuyến tính thật dễ hiểu; ta duyệt cả cỗ bài rồi so sánh từng lá bài với lá mà ta đang tìm. Nếu thấy, ta sẽ trả về chỉ số tại đó lá bài xuất hiện. Nếu không có trong cỗ bài, ta trả về -1.

  public static int findCard(Card[] cards, Card card) { 
    for (int i = 0; i< cards.length; i++) { 
      if (sameCard(cards[i], card)) { 
        return i; 
      } 
    } 
    return -1; 
  }

Các đối số của findCard là card và cards. Dường như thật kì quặc khi có một biến cùng tên với kiểu dữ liệu (biến card thuộc kiểu Card). Ta có thể nhận thấy sự khác biệt vì biến bắt đầu bằng chữ cái thường.

Phương thức này trả lại ngay khi nó phát hiện ra lá bài cần tìm, ncos nghĩa là ta không cần phải duyệt cả cỗ bài nếu đã tìm được lá bài ta cần. Còn nếu ta đến điểm cuối vòng lặp, ta biết rằng lá bài đó không có trong cỗ.

Nếu các quân bài trong cỗ không được sắp xếp, thì chẳng có cách tìm kiếm nào nhanh hơn cách này. Ta phải nhìn từng lá bài một, bởi nếu không ta sẽ không chắc rằng quân bài mong muốn không ở đó.

Nhưng khi bạn tra từ trong một cuốn từ điển, bạn lại không tìm tuyến tính qua từng từ một, bởi lẽ các từ đều được xếp thứ tự rồi. Do vậy, có khả năng bạn sẽ dùng một thuật toán tương tự như tìm kiếm chia đôi:

  1. Bắt đầu ở một chỗ giữa cuốn từ điển.
  2. Chọn một từ trên trang đó rồi so sánh với từ cần tra.
  3. Nếu bạn tìm thấy từ cần tra thì dừng lại.
  4. Nếu từ cần tra xếp sau từ thấy được trên trang, thì hãy lật đến một chỗ nào đó phía sau của cuốn từ điển, rồi trở lại bước 2.
  5. Nếu từ cần tra xếp trước từ thấy được trên trang, thì hãy lật đến một chỗ nào đó phía trước của cuốn từ điển, rồi trở lại bước 2.

Nếu bạn đã tìm đến chỗ mà có hai từ liền kề nhau trong một trang, và từ cần tra lại nằm giữa hai từ đó, thì có thể kết luận rằng từ cần tra không có trong cuốn từ điển.

Quay trở lại với cỗ bài, nếu ta biết rằng các lá bài đã được xếp thứ tự, thì ta có thể viết một phiên bản khác findCard, nhưng chạy nhanh hơn. Cách tốt nhất để viết phương thức tìm kiếm chia đôi là dùng cách đệ quy, bởi việc chia đôi về bản chất là mang tính đệ quy.

Một mẹo là viết một phương thức có tên findBisect trong đó nhận vào tham số là hai chỉ số, low và high, quy định đoạn trong mảng cần được tìm kiếm (bao gồm cả low và high).

  1. Để tìm kiếm trên mảng, hãy chọn một chỉ số giữa low và high (gọi nó là mid) rồi so sánh nó với lá bài cần tìm.
  2. Nếu bạn đã tìm thấy nó thì dừng lại.
  3. Nếu lá bài tại mid cao hơn lá bài cần tìm, thì tìm kiếm trong khoảng từ low đến mid-1.
  4. Nếu lá bài tại mid thấp hơn lá bài cần tìm, thì tìm kiếm trong khoảng từ mid+1 đến high.

Các bước 3 và 4 trông giống những lời gọi đệ quy đến mức đáng ngờ. Sau đây là toàn bộ ý tưởng khi chuyển thành mã lệnh Java:

  public static int findBisect(Card[] cards, Card card, int low, int high) { 
    // CẦN LÀM: một trường hợp cơ sở 
    int mid = (high + low) / 2; 
    int comp = compareCard(cards[mid], card); 
    if (comp == 0) { 
      return mid; 
    } else if (comp > 0) { 
      return findBisect(cards, card, low, mid-1); 
    } else { 
      return findBisect(cards, card, mid+1, high); 
    } 
  }

Mã lệnh này có chứa phần cốt lõi của phép tìm kiếm chia đôi, song vẫn thiếu một phần trong trọng, đó là lý do mà tôi đã ghi chú “CẦN LÀM”. Như đã viết, phương thức này sẽ lặp đệ quy mãi mãi nếu như lá bài không có trong cỗ. Ta cần một trường hợp cơ bản để xử lý tình huống này.

Nếu high nhỏ hơn low, thì không có lá bài nào giữa chúng, bởi vậy ta sẽ kết luận rằng lá bài cần tìm không có trong cỗ. Nếu ta xử lý được trường hợp đó, thì phương thức sẽ hoạt động đúng:

  public static int findBisect(Card[] cards, Card card, int low, int high) { 
    System.out.println(low + ", " + high); 
    if (high < low) return -1; 
    int mid = (high + low) / 2; 
    int comp = compareCard(cards[mid], card); 
    if (comp == 0) { 
      return mid; 
    } else if (comp > 0) { 
      return findBisect(cards, card, low, mid-1); 
    } else { 
      return findBisect(cards, card, mid+1, high); 
    } 
  }

Tôi đã bổ sung một lệnh in để có thể theo dõi được một loạt những lần kích hoạt đệ quy. Tôi đã thử đoạn mã sau:

    Card card1 = new Card(1, 11); 
    System.out.println(findBisect(cards, card1, 0, 51));

và nhận được kết quả dưới đây:

0, 51
0, 24
13, 24
19, 24
22, 24
23

Sau đó tôi lập một lá bài không có trong cỗ (15 Rô), và thử cố tìm nó. Tôi đã nhận được kết quả:

0, 51
0, 24
13, 24
13, 17
13, 14
13, 12
-1

Những phép thử này không chứng minh được rằng chương trình đúng đắn. Thực tế là bao nhiêu kiểm thử cũng không thể chứng minh được tính đúng đắn nói trên. Song qua việc xem xét một vài trường hợp và kiểm tra mã lệnh, bạn có thể tự thuyết phục bản thân.

Số lần kích hoạt đệ quy thường từ 6 đến 7, vì vậy ta chỉ kích hoạt compareCard có 6 hoặc 7 lần thôi, so với tận 52 lần nếu tìm kiếm tuyến tính. Nói chung, phép chia đôi thì nhanh hơn nhiều so với tìm kiếm tuyến tính, và cò nhanh nữa với các mảng lớn.

Có hai lỗi thường gặp trong chương trình đệ quy, đó là quên đưa vào trường hợp cơ sở và viết lời gọi đệ quy song không bao giờ dẫn đến trường hợp cơ sở. Lỗi sai nào cũng dẫn đến đệ quy vô hạn, và biệt lệ StackOverflowException sẽ được phát ra. (Hãy hình dung một sơ đồ ngăn xếp cho một phương thức đệ quy không bao giờ kết thúc.)

13.9  Cỗ bài và cỗ bài con

Sau đây là nguyên mẫu (xem Mục 8.5) của findBisect:

  public static int findBisect(Card[] deck, Card card, int low, int high)

Ta có thể coi cardslow, và high chỉ là một thông số quy định một cỗ bài con. Cách suy nghĩ này rất thông dụng, và đôi khi được gọi là tham số trừu tượng. Ở đây, “trừu tượng” có nghĩa là thứ mà đúng ra không có mặt trên mã lệnh chương trình, nhưng lại diễn tả tính năng của chương trình theo cấp độ ý tưởng cao hơn.

Chẳng hạn, khi bạn kích hoạt một phương thức rồi truyền vào một mảng cùng với các giới hạn low và high, không có gì ngăn cản ươợc phương thức đã kích hoạt khỏi truy cập phần của mảng nằm ngoài phạm vi giới hạn nói trên. Bởi vậy thật ra bạn không gửi một tập con của cỗ bài; bạn đang gửi toàn bộ cỗ bài. Nhưng miễn là bộ phận tiếp nhận (tức là phần nội dung phương thức) tuân theo luật chơi, thì ta có thể coi rằng đó chính là một cỗ bài con.

Hình thức suy nghĩ này, trong đó chương trình có hàm ý cao xa hơn là những câu mã lệnh, chính là một phần quan trọng trong tư duy nhà khoa học máy tính. Từ “trừu tượng” đã xuất hiện quá nhiều trong nhiều ngữ cảnh khác nhau và điều này khiến cho ý nghĩa của nó bị loãng đi. Mặc dù vậy, trừu tượng chính là một ý tưởng trọng tâm trong ngành khoa học máy tính (cũng như nhiều ngành khác).

Một định nghĩa khái quát hơn cho “trừu tượng” là “Quá trình mô hình hóa một hệ thống phức tạp bằng diễn giải được giản hóa, nhằm lược đi những chi tiết không liên quan đồng thời nắm bắt được những động thái mà ta cần quan tâm.”

13.10  Thuật ngữ

mã hóa:
Việc biểu diễn một tập hợp các giá trị bằng một tập hợp các giá trị khác, bằng việc thiết lập một ánh xạ giữa chúng.
giống hệt:
Sự bằng nhau giữa các tham chiếu. Hai tham chiếu chỉ đến cùng một đối tượng trong bộ nhớ.
tương đồng:
Sự bằng nhau giữa các giá trị. Hai tham chiếu chỉ đến hai đối tượng chứa dữ liệu giống nhau.
tham số trừu tượng:
Một tập hợp gồm các tham số hoạt động cùng nhau như một tham số thống nhất.
trừu tượng:
Quá trình diễn giải một chương trình (hay thứ khác) ở một cấp độ cao hơn so với những gì được viết dưới dạng mã lệnh.

13.11  Bài tập

Bài tập 1   Hãy gói bọc mã lệnh trong Mục 13.5 vào một phương thức. Sau đó chỉnh sửa nó để bậc của Át cao hơn K.
Bài tập 2   Hãy gói bọc mã lệnh thiết lập cỗ bài ở Mục 13.6 vào trong một phương thức có tên makeDeck không nhận tham số nào và trả lại một mảng đã điền đầy đủ những lá bài (Card).
Bài tập 3   Trong trò chơi Blackjack, mục tiêu là lấy được một nhóm cây bài có tổng điểm bằng 21. Điểm của nhóm bài bằng tổng các điểm trên những cây bài. Điểm cho những quân Át bằng 1, cho những quân bài mặt người bằng 10, và những quân khác thì điểm đúng bằng bậc. Chẳng hạn, nhóm ba quân bài (Ace, 10, Jack, 3) có tổng điểm là 1 + 10 + 10 + 3 = 24. Hãy viết một phương thức có tên handScore nhận vào đối óố là một mảng những lá bài rồi trả lại tổng điểm.
Bài tập 4   Trong trò chơi Poker, một “dây” (flush) là một nhóm lá bài có từ 5 lá trở lên cùng chất. Một nhóm bài có thể chứa bao nhiêu lá bài cũng được.

  1. Hãy viết một phương thức có tên suitHist nhận tham số là một mảng gồm những Card rồi trả lại một histogram các chất trong nhóm. Lời giải của bạn chỉ được duyệt mảng đúng một lần.
  2. Hãy viết một phương thức hasFlush nhận tham số là một mảng những Card rồi trả lại true nếu nhóm bài có chứa dây, và false nếu không.
Bài tập 5   Làm việc với những cây bài sẽ hay hơn nếu bạn hiển thị được chúng lên màn hình. Nếu bạn chưa từng thử những ví dụ đồ họa ở Phụ lục A, bây giờ có thể sẽ là lúc thích hợp. Trước hết, hãy tải về http://thinkapjava.com/code/CardTable.java và http://thinkapjava.com/code/cardset.zip vào cùng một thư mục. Sau đó, giải nén cardset.zip, vốn có chứa một thư mục con cardset-oxymoron với tất cả hình của những quân bài. (Lưu ý rằng biến cardset trong CardTable.main chính là tên của thư mục này.) Chạy CardTable.java và bạn có thể thấy hình ảnh một cỗ bài trải ra trên bàn màu xanh. Bạn có thể dùng lớp này để khởi đầu lập nên những trò chơi bài riêng.

3 phản hồi

Filed under Think Java

3 responses to “Chương 13: Mảng chứa các đối tượng

  1. Pingback: Think Java: Cách suy nghĩ như nhà khoa học máy tính | Blog của Chiến

  2. chỉ mình bài 1 phần bài tập với

  3. bạn ơi bài 3 phần bài bài tập mình làm như này có đúng không:
    public static void main(String[] args) {
    Card[] c=new Card[52];
    int index=0;
    for(int suit=0;suit<=3;suit++){
    for(int rank=1;rank<=13;rank++){
    c[index]=new Card(suit,rank);
    index++;
    }
    }
    System.out.println(HandScore(c));
    }
    public static int HandScore(Card[] cards){
    //chuyển giá trị của bài sang 1 và 10
    cards[1].rank=1;
    cards[11].rank=10;
    cards[12].rank=10;
    cards[13].rank=10;
    int Card1=RandomInt3(0,11);
    int Card2=RandomInt3(0,11);
    int Card3=RandomInt3(0,11);
    int Card4=RandomInt3(0,11);
    int Card5=RandomInt3(0,11);
    return Card1+Card2+Card3+Card4+Card5;
    }
    //random ra một số trong khoảng low3<x<high3
    public static int RandomInt3(int low3,int high3){
    double ran = Math.random()*(high3-low3);
    int x=(int)ran+low3;
    if(x==low3){
    x=low3+1;
    }
    return x;
    }

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s