Nâng cấp đối tượng của bạn

Trong bài này chúng ta sẽ giới thiệu về việc nâng cấp đối tượng trong Java

Giới thiệu về việc nâng cấp đối tượng của bạn

Bây giờ thì Adult của ta đã khá hữu ích, nhưng chưa thực sự hữu ích như nó cần phải có. Trong phần này, chúng ta sẽ nâng cấp đối tượng khiến nó dễ sử dụng hơn và cũng hữu ích hơn. Công việc bao gồm:

  • Tạo ra vài hàm tạo hữu ích.
  • Nạp chồng một vài phương thức để tạo ra một giao diện công cộng thuận tiện hơn
  • Thêm mã lệnh để hỗ trợ so sánh các Adult s
  • Thêm mã lệnh để dễ dàng gỡ lỗi khi sử dụng Adult s

Đồng thời, chúng ta sẽ tìm hiểu về các kỹ thuật tái cấu trúc mã và xem xem có thể xử lý những sai sót mà ta gặp trong khi chạy mã lệnh như thế nào.

Xây dựng các hàm tạo (constructors)

Trước đây chúng ta đã đề cập đến các hàm tạo. Bạn có thể nhớ rằng tất cả các đối tượng trong mã lệnh Java đều có sẵn một hàm tạo không tham số mặc định. Bạn không phải định nghĩa nó, và bạn sẽ không thấy nó xuất hiện trong mã lệnh của mình. Thực tế, chúng ta đã dùng lợi thế ấy trong lớp Adult. Bạn không thấy có sự xuất hiện của một hàm tạo trong lớp này.

Tuy nhiên, thực tế là bạn nên định nghĩa hàm tạo của riêng mình. Khi bạn làm vậy, bạn có thể hoàn toàn yên tâm là ai đó khi khảo sát lớp của bạn sẽ biết cách xây dựng nó theo cách mà bạn muốn. Bởi vậy hãy định nghĩa một hàm tạo không có tham số riêng của mình. Ta nhắc lại cấu trúc cơ bản của một hàm tạo:

 accessSpecifier ClassName(  arguments ) {

           constructor statement(s)

}

Định nghĩa một hàm tạo không tham số cho Adult thật là đơn giản:

public Adult {

}

Xong rồi. Hàm tạo không có tham số của chúng ta chẳng làm gì cả, thực thế, ngoại trừ việc tạo ra một Adult. Bây giờ khi ta gọi new để sinh một Adult, chúng ta sẽ dùng hàm tạo không tham số của chúng ta để thay thế cho hàm tạo mặc định. Nhưng điều gì sẽ xảy ra nếu ta muốn hàm tạo do ta xây dựng thực hiện một số việc? Trong trường hợp của Adult, sẽ tiện lợi hơn nhiều nếu có thể chuyển thêm tên và họ dưới dạng String, và yêu cầu hàm tạo thiết đặt các biến cá thể với các giá trị khởi tạo đó. Điều này cũng được làm đơn giản như sau:

public Adult(String aFirstname, String aLastname) {

          firstname = aFirstname;

          lastname = aLastname;

}

Hàm tạo này nhận hai tham số và sẽ gán chúng cho các biến cá thể. Hiện tại chúng ta có hai hàm tạo. Chúng ta thực sự không cần hàm tạo đầu tiên nữa, nhưng không hề gì nếu giữ nó lại. Nó mang lại cho người dùng lớp này một tùy chọn. Họ có thể tạo một đối tượng Adult với tên mặc định hoặc tạo một đối tượng Adult có tên xác định mà họ đưa vào.

Những gì ta vừa làm, thậm chí là bạn có lẽ còn chưa biết, được gọi là nạp chồng (overload) phương phức. Chúng ta sẽ thảo luận về khái niệm này chi tiết hơn trong phần tiếp theo.

Nạp chồng phương thức (Overloading)

Khi bạn tạo hai phương thức có cùng tên, nhưng số lượng tham số khác nhau (hoặc kiểu của tham số khác nhau), thì chính là bạn đang nạp chồng phương thức đó. Đây là một mặt mạnh của đối tượng. Môi trường chạy thi hành của Java sẽ quyết định phiên bản nào của phương thức được gọi, dựa trên những thứ mà bạn truyền vào. Trong trường hợp các hàm tạo của chúng ta, nếu bạn không truyền vào bất cứ tham số nào thì JRE sẽ dùng hàm tạo không tham số. Nếu ta truyền vào hai đối tượng kiểu String thì môi trường chạy thi hành sẽ dùng phiên bản nhận hai tham số String. Nếu ta truyền vào các tham số có kiểu khác (hoặc là chỉ một String) thì môi trường chạy thi hành sẽ nhắc rằng không có hàm tạo nào nhận những tham số kiểu đó.

Bạn có thể nạp chồng bất cứ phương thức nào chứ không phải chỉ hàm tạo, việc này khiến cho việc tạo các giao diện thuận tiện cho người dùng sử dụng lớp của bạn trở nên dễ dàng. Hãy thử bằng cách bổ sung thêm phiên bản khác của phương thức addMoney() của bạn. Lúc này, phương thức đó nhận một tham số kiểu int. Tốt thôi, nhưng điều gì sẽ xảy ra nếu chúng ta muốn nạp thêm 100$ vào quỹ của Adult? Chúng ta phải gọi đi gọi lại phương thức này để thêm vào một loạt tờ giấy bạc có tổng giá trị là 100$. Thật là vô cùng bất tiện. Sẽ hay hơn nhiều nếu có thể truyền vào một mảng các phần tử int biểu thị cho một tập nhiều tờ giấy bạc. Ta hãy nạp chồng phương thức này để nhận tham số là một mảng. Đây là phương thức mà ta có:

public void addMoney(int bill) {

          Integer boxedBill = new Integer(bill);

          wallet.add(boxedBill);            

}

 

Còn đây là phiên bản nạp chồng:

public void addMoney(int[] bills) {

          for (int i = 0; i < bills.length; i++) {

                   int bill = bills[i];

                   Integer boxedBill = new Integer(bill);

                   wallet.add(boxedBill);                      

          }

}

Phương thức này trông rất giống với một phương thức addMoney() khác của chúng ta, nhưng nó nhận tham số là một mảng. Ta thử dùng phương thức này bằng cách biến đổi phương thức main() của Adult giống như sau:

public static void main(String[] args) {

          Adult myAdult = new Adult();

          myAdult.addMoney(new int[] { 1, 5, 10 });

          System.out.println(myAdult);

}

Khi chạy mã lệnh này, ta có thể thấy Adult có một wallet bên trong có 16$. Đây là giao diện tốt hơn nhiều. Nhưng chưa xong. Hãy nhớ rằng chúng ta là những lập trình viên chuyên nghiệp, và ta muốn giữ cho mã lệnh của mình được sáng sủa. Bạn có thấy sự trùng lặp mã lệnh nào trong hai phương thức của chúng ta chưa? Hai dòng trong phiên bản đầu tiên xuất hiện nguyên văn trong phiên bản thứ hai. Nếu ta muốn thay đổi những gì ta làm khi thêm tiền vào, ta phải biến đổi mã lệnh ở hai nơi, đó là ý tưởng kém. Nếu ta bổ sung phiên bản khác của phương thức này để nó nhận tham số là ArrayList thay vì nhận một mảng, chúng ta phải biến đổi mã lệnh ở ba nơi. Điều đó nhanh chóng trở nên không thể chấp nhận nổi. Thay vào đó, chúng ta có thể tái cấu trúc (refactor) mã lệnh để loại bỏ sự trùng lặp. Ở phần tiếp theo, chúng ta sẽ thực hiện một thao tác tái cấu trúc gọi là trích xuất phương thức (Extract Method) để hoàn thành việc này.

Tái cấu trúc khi nâng cao

Tái cấu trúc (refactoring) là quá trình thay đổi cấu trúc của mã lệnh hiện có mà không làm biến đổi chức năng của nó. Ứng dụng của bạn phải sản sinh cùng một kết quả đầu ra như cũ sau quá trình tái cấu trúc, nhưng mã lệnh của bạn sẽ trở nên rõ ràng hơn, sáng sủa hơn, và ít trùng lặp. Thường thuận lợi hơn để làm tái cấu trúc mã lệnh trước khi thêm một đặc tính (để bổ sung vào dễ hơn hoặc làm rõ hơn cần bổ sung thêm vào đâu), và sau khi thêm một đặc tính (để làm sạch sẽ những gì đã làm khi bổ sung vào). Trong trường hợp này, chúng ta đã thêm vào một phương thức mới và ta thấy một số mã lệnh trùng lặp. Chính là lúc để tái cấu trúc !

Đầu tiên, chúng ta cần tạo ra một phương thức nắm giữ hai dòng mã lệnh trùng lặp. Chúng ta gọi phương thức đó là addToWallet():

protected void addToWallet(int bill) {

          Integer boxedBill = new Integer(bill);

          wallet.add(boxedBill);            

}

Chúng ta đặt chế độ truy nhập cho phương thức này là protected vì nó thực sự là phương thức phụ trợ nội tại của riêng chúng ta, chứ không phải là một phần của giao diện công cộng của lớp do chúng ta xây dựng. Bây giờ hãy thay các dòng mã lệnh trong phương thức bằng lời gọi đến phương thức mới:

public void addMoney(int bill) {

          addToWallet(bill);

}

Đây là phiên bản được nạp chồng:

public void addMoney(int[] bills) {

          for (int i = 0; i < bills.length; i++) {

                   int bill = bills[i];

                   addToWallet(bill);

          }

}

Nếu bạn chạy lại mã lệnh, bạn sẽ thấy cùng một kết quả. Kiểu tái cấu trúc này nên trở thành một thói quen, và Eclipse sẽ khiến nó trở nên dễ dàng hơn cho bạn bằng cách đưa vào thêm nhiều công cụ tái cấu trúc tự động. Việc đi sâu tìm hiểu chi tiết về chúng nằm ngoài phạm vi của tài liệu hướng dẫn này, nhưng bạn có thể thử nghiệm chúng. Nếu chúng ta chọn hai dòng mã lệnh trùng lặp trong phiên bản đầu của addMoney(), chúng ta có thể nhấn chuột phải vào mã lệnh đã chọn và chọn Refactor>Extract Method. Eclipse sẽ từng bước dẫn dắt chúng ta qua quá trình tái cấu trúc. Đây là một trong những đặc tính mạnh nhất của IDE này.

Các thành phần của lớp

Các biến và phương thức mà chúng ta có trong Adult là các biến cá thể và phương thức cá thể. Mỗi đối tượng sẽ có các biến và phương thức cá thể như thế.

Bản thân các lớp cũng có các biến và phương thức. Chúng được gọi chung là các thành phần của lớp, và bạn khai báo chúng bằng từ khóa static. Sự khác nhau giữa các thành phần của lớp và các biến cá thể là:

  • Tất cả các cá thể của một lớp sẽ chia sẻ chung một bản sao đơn lẻ của biến lớp (class variable).
  • Bạn có thể gọi các phương thức lớp (class method) ngay trên bản thân lớp đó mà không cần có một cá thể của lớp.
  • Các phương thức của cá thể có thể truy cập các biến lớp, nhưng các phương thức lớp không thể truy cập vào biến cá thể
  • Các phương thức lớp chỉ có thể truy cập biến lớp.

Khi nào thì việc thêm các biến lớp hay phương thức lớp trở nên có ý nghĩa? Quy tắc xuyên suốt là hiếm khi làm điều đó, để bạn không lạm dụng chúng. Một số cách dùng thông thường là:

  • Để khai báo các hằng số mà bất cứ cá thể nào của lớp cũng có thể sử dụng được
  • Để theo vết “bộ đếm” các cá thể của lớp.
  • Trên một lớp với các phương thức tiện ích mà không bao giờ cần đến một cá thể, vẫn giúp ích được (như là phương thức Collections.sort())

Các biến lớp

Để tạo một biến lớp, ta dùng từ khóa static khi khai báo:

 accessSpecifier static  variableName        

[=  initialValue ];

JRE tạo một bản sao của các biến cá thể của lớp cho mọi cá thể của lớp đó. JRE chỉ sinh duy nhất một bản sao cho mỗi biến lớp, không phụ thuộc vào số lượng cá thể, khi lần đầu tiên nó gặp lời gọi lớp trong chương trình. Tất cả các cá thể sẽ chia sẻ chung (và có thể sửa đổi) bản sao riêng lẻ này. Điều này làm cho các biến lớp trở thành một lựa chọn tốt để chứa các hằng số mà tất cả các cá thể đều có thể sử dụng.

Ví dụ, chúng ta đang dùng các số nguyên để mô tả “tờ giấy bạc” trong wallet của Adult. Điều đó hoàn toàn chấp nhận được, nhưng sẽ thật tuyệt nếu ta đặt tên cho các giá trị nguyên này để chúng ta có thể dễ dàng hiểu con số đó biểu thị cho cái gì khi ta đọc mã lệnh. Hãy khai báo một vài hằng số để làm điều này, ở chính ngay nơi ta khai báo các biến cá thể trong lớp của mình:

protected static final int ONE_DOLLAR_BILL = 1;

protected static final int FIVE_DOLLAR_BILL = 5;

protected static final int TEN_DOLLAR_BILL = 10;

protected static final int TWENTY_DOLLAR_BILL = 20;

protected static final int FIFTY_DOLLAR_BILL = 50;

protected static final int ONE_HUNDRED_DOLLAR_BILL = 100;

Theo quy ước, các hằng số của lớp đều được viết bằng chữ in hoa, các từ nối với nhau bằng dấu gạch dưới. Ta dùng từ khóa static để khai báo chúng như là các biến lớp, và ta thêm từ khóa final vào để đảm bảo là không một cá thể nào có thể thay đổi chúng được (nghĩa là biến chúng trở thành hằng số). Bây giờ ta có thể biến đổi main() để thêm một ít tiền cho Adult của ta, sử dụng các hằng được đặt tên mới:

public static void main(String[] args) {

          Adult myAdult = new Adult();                  

          myAdult.addMoney(new int[] { Adult.ONE_DOLLAR_BILL, Adult.FIVE_DOLLAR_BILL });

          System.out.println(myAdult);

}

Đọc đoạn mã này sẽ giúp làm sáng tỏ những gì ta bổ sung vào wallet wallet

Các phương thức lớp

Như ta đã biết, ta gọi một phương thức cá thể như sau:

variableWithInstance.methodName();

Chúng ta đã gọi phương thức trên một biến có tên, biến đó chứa một cá thể của lớp. Khi bạn gọi một phương thức lớp, bạn sẽ gọi như sau:

ClassName.methodName();

Chúng ta không cần đến một cá thể để gọi phương thức này. Chúng ta đã gọi nó thông qua chính bản thân lớp. Phương thức main() mà ta đang dùng chính là một phương thức lớp. Hãy nhìn chữ ký của nó. Chú ý rằng nó được khai báo với từ khóa public static. Chúng ta đã biết định tố truy nhập này từ trước đây. Còn từ khóa static chỉ ra rằng đây là một phương thức lớp, đây chính là lý do mà các phương thức kiểu này đôi khi được gọi là cácphương thức static. Chúng ta không cần có một cá thể của Adult để gọi phương thức main().

Chúng ta có thể xây dựng các phương thức lớp cho Adult nếu ta muốn, mặc dù thực sự không có lý do để làm điều đó trong trường hợp này. Tuy nhiên, để minh họa cách làm, ta sẽ bổ sung thêm một phương thức lớp thông thường:

public static void doSomething() {

          System.out.println("Did something");

}

Thêm dấu chú thích vào các dòng lệnh hiện có của main() để loại bỏ chúng và bổ sung thêm dòng sau:

Adult.doSomething();

Adult myAdult = new Adult();

myAdult.doSomething();

Khi bạn chạy mã lệnh này, bạn sẽ thấy thông điệp tương ứng trên màn hình hai lần. Lời gọi thứ nhất gọi doSomething() theo cách điển hình khi gọi một phương thức lớp. Bạn cũng có thể gọi chúng thông qua một cá thể của lớp, như ở dòng thứ ba của mã lệnh. Nhưng đó không phải là cách hay. Eclipse sẽ cảnh báo cho bạn biết bằng cách dùng dòng gạch chân dạng sóng màu vàng và đề nghị bạn nên truy cập phương thức này theo “cách tĩnh” (static way), nghĩa là trên lớp chứ không phải là trên cá thể.

So sánh các đối tượng với toán tử ==

Có hai cách để so sánh các đối tượng trong ngôn ngữ Java:

Toán tử ==

Toán tử equals()

Cách đầu tiên, và là cách cơ bản nhất, so sánh các đối tượng theo tiêu chí ngang bằng đối tượng (object equality). Nói cách khác, câu lệnh:

a == b

sẽ trả lại giá trị true nếu và chỉ nếu a và b trỏ tới chính xác cùng một cá thể của một lớp (tức là cùng một đối tượng). Các kiểu nguyên thủy có ngoại lệ riêng. Khi ta so sánh hai kiểu nguyên thủy bằng toán tử ==, môi trường chạy thi hành của Java sẽ so sánh các giá trị của chúng (hãy nhớ rằng dù gì thì chúng cũng không phải là đối tượng thực sự). Hãy thử ví dụ này trong main() và xem kết quả trên màn hình.

int int1 = 1;

int int2 = 1;

Integer integer1 = new Integer(1);

Integer integer2 = new Integer(1);

Adult adult1 = new Adult();

Adult adult2 = new Adult();

System.out.println(int1 == int2);

System.out.println(integer1 == integer2);

integer2 = integer1;

System.out.println(integer1 == integer2);

System.out.println(adult1 == adult2);

Phép so sánh đầu tiên trả lại giá trị true, vì ta đang so sánh hai kiểu nguyên thủy có cùng giá trị. Phép so sánh thứ hai trả lại giá trị false, vì hai biến không tham chiếu đến cùng một đối tượng cá thể. Phép so sánh thứ ba trả lại giá trị true, vì bây giờ hai biến trỏ đến cùng một cá thể. Hãy thử với lớp của chúng ta, ta cũng nhận được giá trị false vì adult1 và adult2 không chỉ đến cùng một cá thể.

Bạn gọi phương thức equals() trên một đối tượng như sau:

a.equals(b);

Phương thức equals() là một phương thức của lớp Object, vốn là lớp cha của mọi lớp trong ngôn ngữ Java. Điều đó có nghĩa là bất cứ lớp nào bạn xây dựng nên cũng sẽ thừa kế hành vi cơ sở equals() từ lớp Object. Hành vi cơ sở này không khác so với toán tử ==. Nói cách khác, mặc định là hai câu lệnh này cùng sử dụng toán tử == và trả lại giá trị false:

a == b;

a.equals(b);

Hãy nhìn lại phương thức spendMoney() của lớp Adult. Chuyện gì xảy ra đằng sau khi ta gọi phương thức contains()của đối tượng wallet của ta? Ngôn ngữ Java sử dụng toán tử == để so sánh các đối tượng trong danh sách với một đối tượng mà ta yêu cầu. Nếu Java thấy khớp, phương thức sẽ trả lại giá trị true; các trường hợp khác trả lại giá trị false. Bởi vì ta đang so sánh các kiểu nguyên thủy, Java có thể thấy sự trùng khớp dựa theo giá trị của các số nguyên (hãy nhớ rằng toán tử == so sánh các kiểu nguyên thủy dựa trên giá trị của chúng).

Thật tuyệt vời đối với các kiểu nguyên thủy, nhưng liệu sẽ thế nào nếu ta so sánh nội dung của các đối tượng? Toán tử == không thể làm việc này. Để so sánh nội dung của các đối tượng, chúng ta phải đè chồng phương thức equals() của lớp mà a là cá thể của lớp đó. Điều đó có nghĩa là bạn tạo ra một phương thức có cùng chữ ký chính xác như chữ ký của phương thức của một trong các lớp cha (superclasses), nhưng bạn sẽ triển khai thực hiện phương thức này khác với phương thức của lớp bậc trên. Nếu làm như vậy, bạn có thể so sánh nội dung của hai đối tượng để xem liệu chúng có giống nhau không chứ không phải là chỉ kiểm tra xem liệu hai biến đó có trỏ tới cùng một cá thể không.

Hãy thử ví dụ này trong main(), và xem kết quả trên màn hình:

Adult adult1 = new Adult();

Adult adult2 = new Adult();

System.out.println(adult1 == adult2);

System.out.println(adult1.equals(adult2));

Integer integer1 = new Integer(1);

Integer integer2 = new Integer(1);

System.out.println(integer1 == integer2);

System.out.println(integer1.equals(integer2));

Phép so sánh đầu tiên trả lại giá trị false vì adult1 và adult2 trỏ đến các cá thể khác nhau của lớp Adult. Phép so sánh thứ hai cũng trả lại giá trị false vì triển khai mặc định của equals() đơn giản là so sánh hai biến để xem liệu chúng có trỏ tới cùng một cá thể không. Nhưng hành vi mặc định này của equals() thường không phải là cái ta mong muốn. Chúng ta muốn so sánh nội dung của hai Adult để xem liệu chúng có giống nhau không. Ta có thể đè chồng phương thức equals() để làm điều này. Như bạn thấy kết quả của hai phép so sánh cuối cùng trong ví dụ trên, lớp Integer đè chồng lên phương thức này sao cho toán tử ==trả lại giá trị false, nhưng equals() lại so sánh các giá trị int đã bao bọc để xem có bằng nhau không. Chúng ta sẽ làm tương tự với Adult trong phần tiếp theo.

Đè chồng (Overriding) phương thức equals()

Để đè chồng phương thức equals() nhằm so sánh các đối tượng thì thực tế chúng ta phải đè chồng hai phương thức:

public boolean equals(Object other) {

          if (this == other)

                   return true;

          if ( !(other instanceof Adult) )

                   return false;

          Adult otherAdult = (Adult)other;

          if (this.getAge() == otherAdult.getAge() &&

                   this.getName().equals(otherAdult.getName()) &&

                   this.getRace().equals(otherAdult.getRace()) &&

                   this.getGender().equals(otherAdult.getGender()) &&

                   this.getProgress() == otherAdult.getProgress() &&

                   this.getWallet().equals(otherAdult.getWallet()))

                   return true;

          else

                  return false;

}

public int hashCode() {

          return firstname.hashCode() + lastname.hashCode();

}

Chúng ta đè chồng phương thức equals() theo cách sau, là cách diễn đạt tiêu biểu của Java:

  • Nếu đối tượng được so sánh chính là đối tượng so sánh thì hai đối tượng này rõ ràng là bằng nhau, bởi vậy ta trả lại giá trị true
  • Chúng ta kiểm tra để chắc chắn rằng đối tượng mà chúng ta sẽ đem so sánh là một cá thể của lớp Adult (nếu không thì hai đối tượng này không thể như nhau được)
  •  Chúng ta ép kiểu đối tượng được gửi đến thành một Adult để có thể gọi các phương thức phù hợp của nó
  •  Chúng ta so sánh hai Adult, chúng sẽ phải giống nhau nếu hai đối tượng là “bằng nhau” (dù theo bất cứ định nghĩa nào về phép bằng mà chúng ta sử dụng)
  • Nếu bất cứ mảnh nào không bằng nhau thì chúng ta sẽ trả về giá trị là false; ngược lại trả về giá trị true

Lưu ý rằng chúng ta có thể so sánh age bằng toán tử == vì age là giá trị nguyên thủy. Chúng ta dùng phương thức equals() để so sánh các String, vì lớp đó đè chồng phương thức equals() để so sánh nội dung của các String (nếu ta dùng toán tử ==, chúng ta sẽ luôn nhận được kết quả trả về là false, vì hai String không bao giờ cùng là một đối tượng). Chúng ta làm tương tự với ArrayList, vì nó đè chồng phương thức equals() để kiểm tra xem hai danh sách có cùng các phần tử theo cùng thứ tự hay không, như thế là đủ cho ví dụ đơn giản của chúng ta.

Bất cứ khi nào bạn đè chồng phương thức equals(), bạn cũng nên viết đè chồng cả phương thức hashCode() nữa. Lý do vì sao lại như thế nằm ngoài phạm vi của tài liệu hướng dẫn này, nhưng hiện giờ, chỉ cần biết rằng ngôn ngữ Java dùng các giá trị được trả về từ phương thức hashCode() này để đặt các cá thể của lớp của bạn vào các sưu tập, các sưu tập này lại dùng thuật toán băm để sắp đặt các đối tượng (như HashMap). Quy tắc nghiêm ngặt và nhanh chóng để quyết định hashCode() phải trả lại giá trị gì (ngoài việc nó phải trả lại một số nguyên) là nó phải trả về:

  • Cùng giá trị giống nhau cho cùng một đối tượng vào mọi thời điểm.
  • Các giá trị bằng nhau đối với các đối tượng bằng nhau.

Thông thường, việc trả về giá trị mã băm của một vài hoặc toàn bộ các biến cá thể của một đối tượng là một cách thích hợp để tính toán ra mã băm. Một lựa chọn khác là chuyển đổi các biến thành String, nối chúng lại và sau đó trả về mã băm của String kết quả. Một lựa chọn khác nữa là để một hoặc một vài biến kiểu số với một hằng số nào đó để làm cho kết quả trở nên có tính duy nhất hơn nữa, nhưng điều này vượt quá mức cần thiết.

Đè chồng phương thức toString()

Lớp Object có một phương thức toString(), mọi lớp sau này do bạn tạo ra sẽ thừa kế nó. Nó trả về một đại diện dạng String của đối tượng của bạn và rất hữu dụng cho việc gỡ lỗi. Để xem phiên bản triển khai thực hiện mặc định của phương thức toString() làm gì thì ta hãy thử ví dụ sau trong main():

public static void main(String[] args) {

          Adult myAdult = new Adult();

          myAdult.addMoney(1);

          myAdult.addmoney(5); 

          System.out.println(myAdult)

}

Kết quả ta nhận được trên màn hình sẽ như sau:

 intro.core.Adult@b108475c

Phương thức println() gọi phương thức toString() của đối tượng đã truyền đến nó. Vì chúng ta còn chưa đè chồng phương thức toString() nên chúng ta sẽ nhận được kết quả đầu ra mặc định, đó là ID của đối tượng. Tất cả đối tượng đều có ID nhưng chúng không cho bạn biết gì nhiều về đối tượng. Sẽ tốt hơn khi bạn đè chồng lên phương thức toString() để đưa ra cho chúng ta một bức tranh được định dạng đẹp đẽ của các nội dung của đối tượng Adult():

public String toString() {

          StringBuffer buffer = new StringBuffer();

          buffer.append("And Adult with: " + "\n");

          buffer.append("Age: " + age + "\n");

          buffer.append("Name: " + getName() + "\n");

          buffer.append("Race: " + getRace() + "\n");

          buffer.append("Gender: " + getGender() + "\n");

          buffer.append("Progress: " + getProgress() + "\n");

          buffer.append("Wallet: " + getWallet());

          return buffer.toString();

}

Chúng ta tạo ra một StringBuffer để xây dựng một biểu diễn dạng String của đối tượng của chúng ta, sau đó trả về String này. Khi bạn chạy lại thì màn hình sẽ cho ta kết quả xuất ra đẹp đẽ như sau:

An Adult with:

Age: 25

Name: firstname lastname

Race: inuit

Gender: male

Progress: 0

Wallet: [1, 5]

Thế này rõ ràng là thuận tiện và có ích hơn là một ID đối tượng khó hiểu.

Các lỗi (Exceptions)

Thật tuyệt nếu như mã lệnh của chúng ta không bao giờ có bất kỳ sai sót nào nhưng điều này là phi thực tế. Đôi khi mọi thứ không xuôi chèo mát mái như ta muốn, và có những khi vấn đề xảy ra còn tệ hơn là việc sản sinh ra những kết quả không mong muốn. Khi điều đó xảy ra, JRE sẽ đưa ra một lỗi ngoại lệ (throws an exception). Ngôn ngữ này có bao gồm những câu lệnh đặc biệt cho phép bạn bắt lỗi và quản lý lỗi một cách thích hợp. Sau đây là khuôn dạng chung của những câu lệnh này:

try {

           statement(s)

} catch ( exceptionType name ) {

           statement(s)

} finally {

           statement(s)

}

Lệnh try bao bọc đoạn mã lệnh có thể gây ra lỗi. Nếu có lỗi, việc thi hành sẽ lập tức nhảy tới khối catch, cũng gọi là trình xử lý lỗi. Khi đã qua khối try và khối catch, việc thi hành sẽ tiếp tục đến khối finally, bất chấp việc liệu có lỗi xảy ra hay không. Khi bạn bắt được lỗi, bạn có thể thử sửa lỗi hoặc bạn có thể thoát ra khỏi chương trình (hay phương thức) một cách nhẹ nhàng.

Xử lý lỗi

Thử ví dụ sau trong main():

public static void main(String[] args) {

          Adult myAdult = new Adult();

          myAdult.addMoney(1);

          String wontWork = (String) myAdult.getWallet().get(0);

}

Khi chúng ta chạy mã lệnh này, chúng ta sẽ nhận được báo lỗi. Màn hình sẽ hiển thị như sau:

java.lang.ClassCastException

          at intro.core.Adult.main(Adult.java:19)

Exception in thread "main" 

Lưu vết của ngăn xếp (stack trace) sẽ báo cho biết kiểu của lỗi và số hiệu của dòng xuất hiện lỗi. Hãy nhớ rằng chúng ta phải ép kiểu (cast) khi gỡ bỏ một Object khỏi collection. Chúng ta có sưu tập các đối tượng Integer nhưng chúng ta đã thử lấy đối tượng thứ nhất bằng lệnh get(0) (trong đó 0 là chỉ số của phần tử đầu tiên trong danh sách vì danh sách bắt đầu từ 0, cũng như mảng) và ép kiểu nó thành String. Môi trường chạy thi hành của Java sẽ xuất ra lỗi này. Lúc đó thì chương trình sẽ ngừng. Hãy làm sao để nó chấm dứt 'nhẹ nhàng hơn' bằng cách xử lý lỗi này:

try {

          String wontWork = (String) myAdult.getWallet().get(0);

} catch (ClassCastException e) {

          System.out.println("You can't cast that way.");

}

Tại đây chúng ta bắt lỗi và in ra một thông báo lịch sự. Một cách khác là ta có thể không làm gì trong khối catch, in ra thông báo lịch sự trong khối finally, nhưng điều đó không cần thiết. Trong một vài trường hợp, đối tượng lỗi (thường có tên khởi đầu bằng e hoặc ex, nhưng không nhất thiết phải thế) có thể cung cấp cho bạn nhiều thông tin hơn về lỗi, chúng có thể giúp bạn nắm được thông tin tốt hơn hoặc sửa chữa lỗi một cách dễ dàng.

Hệ phân cấp lỗi

Ngôn ngữ Java tích hợp chặt chẽ trọn vẹn một hệ phân cấp lỗi, điều đó có nghĩa là có rất nhiều kiểu lỗi. Ở mức cao nhất, một số lỗi được kiểm tra nhờ trình biên dịch, và một số lỗi khác, được gọi là RuntimeException, thì trình biên dịch không kiểm tra được. Quy tắc của Java là bạn phải bắt lỗi hoặc xác định rõ lỗi của mình. Nếu một phương thức có thể đưa ra một lỗi không phải RuntimeException, phương thức đó hoặc là phải xử lý lỗi, hoặc là phải chỉ rõ rằng phương thức gọi nó phải làm việc này. Bạn làm việc này với biểu thức throws trong chữ ký của phương thức. Ví dụ:

protected void someMethod() throws IOException

Trong mã lệnh của bạn, nếu bạn gọi một phương thức mà phương thức này chỉ rõ rằng nó đưa ra một hoặc các kiểu lỗi, bạn phải xử lý nó bằng một cách nào đó, hoặc bổ sung thêm một mệnh đề throws vào chữ ký của phương thức của bạn để chuyển tiếp đến ngăn xếp các lời gọi phương thức đã được gọi trong mã lệnh của bạn. Trong trường hợp xảy ra sự kiện lỗi, môi trường chạy thi hành của ngôn ngữ Java sẽ tìm trình xử lý lỗi ở đâu đó, tới tận ngăn xếp nếu không có trình xử lý nào ở nơi mà lỗi phát sinh ra. Nếu không tìm thấy trình xử lý lỗi cho đến khi truy đến đỉnh ngăn xếp thì môi trường chạy thi hành của Java sẽ lập tức dừng chương trình lại.

Một tin tốt lành là hầu hết các IDE (Eclipse hiển nhiên nằm trong số này) sẽ thông báo cho bạn nếu mã lệnh của bạn cần phải bẫy lỗi có thể được đưa ra bởi phương thức mà bạn gọi. Sau đó bạn có thể quyết định sẽ làm gì với nó.

Còn nhiều điều để nói về xử lý lỗi, dĩ nhiên là thế, nhưng lại quá nhiều để trình bày trong tài liệu này. Hy vọng những gì chúng ta đã bàn đến ở đây sẽ giúp bạn hiểu điều gì đang đợi bạn.

Bài tiếp theo chúng ta sẽ đi tìm hiểu các ứng dụng trong Java

Theo dõi http://laptrinhtot.com để tiếp tục theo dõi các loạt bài mới nhất về Java nhé !

Tags: