Chương 11: Tự tạo nên những đối tượng riêng

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

11.1  Lời định nghĩa lớp và các kiểu đối tượng

Trở về tận Mục 1.5 khi chúng ta định nghĩa lớp Hello, ta đồng thời tạo nên một kiểu đối tượng có tên Hello. Ta không tạo nên biến nào thuộc kiểu Hello này, và cũng không dùng new để tạo ra đối tượng Hello nào, song việc đó là hoàn toàn có thể!

Ví dụ đó chẳng có mấy tác dụng minh họa, bởi không lý gì để ta tạo ra một đối tượng Hello như vậy, và dù có tạo nên thì cũng chảng để làm gì. Trong chương này, ta sẽ xét đến những định nghĩa lớp để tạo nên các kiểu đối tượng có ích.

Sau đây là những ý tưởng quan trọng nhất trong chương:

  • Việc định nghĩa một lớp mới đồng thời cũng tạo nên một đối tượng mới cùng tên.
  • Lời định nghĩa lớp cũng giống như một bản mẫu cho các đối tượng: nó quy định những biến thực thể nào mà đối tượng đó chứa đựng, và những phương thức nào có thể hoạt động với chúng.
  • Mỗi đối tượng thuộc về một kiểu đối tượng nào đó; như vậy, nó là một thực thể của một lớp nào đó.
  • Khi bạn kích hoạt new để tạo nên một đối tượng, Java kích hoạt một phương thức đặc biệt có tên là constructor để khởi tạo các biến thực thể. Bạn cần cung cấp một hoặc nhiều constructor trong lời định nghĩa lớp.
  • Các phương thức thao tác trên một kiểu được định nghĩa trong lời định nghĩa lớp cho kiểu đó.

Sau đây là một số vấn đề về lời định nghĩa lớp:

  • Tên lớp (và do đó, tên của kiểu đối tượng) nên bắt đầu bàng một chữ in, để phân biệt chúng với các kiểu nguyên thủy và những tên biến.
  • Bạn thường đặt một lời định nghĩa lớp vào trong mỗi file, và tên của file phải giống như tên của lớp, với phần mở rộng .java. Chẳng hạn, lớp Time được định nghĩa trong file có tên Time.java.
  • Ở bất kì chương trình nào, luôn có một lớp được giao làm lớp khởi động. Lớp khởi động phải chứa một phương thức mang tên main, đó là nơi mà việc thực thi chương trình bắt đầu. Các lớp khác cũng có thể chứa phương thức cùng tên main, song phương thức đó sẽ không được thực thi từ đầu.

Khi đã nêu những vấn đề này rồi, ta hãy xét một ví dụ về lớp do người dùng định nghĩa, lớp Time.

11.2  Time

Một động lực chung cho việc tạo nên kiểu đối tượng, đó là để gói gọn những dữ liệu liên quan vào trong một đối tượng để ta có thể coi như một đơn vị duy nhất. Ta đã gặp hai kiểu như vậy, đó là Point và Rectangle.

Một ví dụ khác, mà ta sẽ tự tay lập nên, là Time, để biểu diễn giờ đồng hồ. Dữ liệu được gói trong một đối tượng Time bao gồm có số giờ, số phút, và số giây. Bởi mỗi đối tượng Time đều chứa những dữ liệu này, nên ta cần biến thực thể để lưu giữ chúng.

Bước đầu tiên là xác định xem từng biến phải thuộc kiểu gì. Dường như rõ ràng là hour (giờ) và minute (phút) đều phải là những số nguyên. Để cho vấn đề được thú vị hơn, ta hãy đặt second (giây) là một double.

Các biến thực thể được định nghĩa ở đoạn đầu của lời khai báo lớp, bên ngoài bất kì lời khai báo phương thức nào khác, như sau:

class Time { 
  int hour, minute; 
  double second; 
}

Đoạn mã này tự bản thân nó đã là lời khai báo lớp hợp lệ. Sơ đồ trạng thái cho một đối tượng Time sẽ trông như sau:

Sau khi khai báo các biến thực thể, bước tiếp theo là định nghĩa một constructor cho lớp mới này.

11.3  Constructor

Các constructor có nhiệmvụ khởi tạo các biến thực thể. Cú pháp của constructor cũng giống như của các phương thức khác, trừ ba điểm sau:

  • Tên của constructor phải giống như tên lớp.
  • Constructor không có kiểu trả về và cũng không có giá trị trả về.
  • Từ khoá static được bỏ qua.

Sau đây là một ví dụ cho lớp Time:

public Time() { 
  this.hour = 0; 
  this.minute = 0; 
  this.second = 0.0; 
}

Ở chỗ mà bạn trông đợi một kiểu trả về, giữa public and Time, lại không có gì cả. Điều đó cho thấy cách mà chúng ta (và trình biên dịch nữa) có thể phân biệt được rằng đây là một constructor.

Constructor này không nhận tham số nào. Mỗi dòng của constructor khởi tạo một biến thực thể cho một giá trị mặc định (trong trường hợp này là nửa đêm). Cái tên this là một từ khóa đặc biệt để tham chiếu tới đối tượng mà ta đang tạo nên. Bạn có thể dùng this theo cách giống như dùng tên của bất kì đối tượng nào khác. Chẳng hạn, bạn có thể đọc và ghi các biến thực thể của this, và cũng truyền được this với vai trò tham số đến những phương thức khác.

Nhưng bạn không khai báo cái this này và cũng không thể gán giá trị cho nó. this được tạo bởi hệ thống; tất cả những gì bạn phải làm đó là khởi tạo các biến thực thể của nó.

Một lỗi thường gặp khi viết ra constructor là việc đưa câu lệnh return vào cuối. Hãy kiềm chế, tránh làm việc này.

11.4  Thêm các constructor

Constructor có thể được chồng chất [xem thêm phần “Quá tải”], cũng như các phương thức khác, theo nghĩa bạn có thể có nhiều constructor với các tham số khác nhau. Java biết rõ cần phải kích hoạt constructor nào bằng cách khớp những tham số của new với các tham số của constructor.

Việc có constructor không nhận đối số (như trên) là hoàn toàn bình thường, cũng như constructor nhận một danh sách tham số giống hệt với danh sách các biến thực thể. Chẳng hạn:

public Time(int hour, int minute, double second) { 
  this.hour = hour; 
  this.minute = minute; 
  this.second = second; 
}

Các tên và kiểu của những tham số cũng giống với tên và kiểu của các biến thực thể. Tất cả những gì mà constructor này làm chỉ là sao chép thông tin từ các tham số sang các biến thực thể.

Nếu xem tài liệu về Point và Rectangle, bạn sẽ thấy rằng cả hai lớp này đều có những constructor kiểu như trên. Việc chồng chất constructor cho phép linh hoạt tạo nên đối tượng trước rồi sau đó mới điền vào phần trống, hoặc để thu thập toàn bộ thông tin trước khi lập ra đối tượng.

Điều này nghe thì có vẻ không đáng quan tâm, song thực ra thì khác. Việc viết những constructor là quá trình máy móc, buồn tẻ. Một khi bạn đã viết được hai constructor rồi, bạn sẽ thấy rằng mình có thể viết chúng nhanh chóng chỉ qua việc trông vào danh sách các biến thực thể.

11.5  Tạo nên đối tượng mới

Mặc dù trông giống như phương thức, song constructor không bao giờ được kích hoạt trực tiếp. Thay vì vậy, khi bạn kích hoạt new, hệ thống sẽ huy động dung lượng bộ nhớ cho đối tượng mới và kích hoạt constructor này.

Chương trình sau giới thiệu hai cách làm để lập thành và khởi tạo các đối tượng Time:

class Time { 
  int hour, minute; 
  double second; 
  public Time() { 
    this.hour = 0; 
    this.minute = 0; 
    this.second = 0.0; 
  } 
  public Time(int hour, int minute, double second) { 
    this.hour = hour; 
    this.minute = minute; 
    this.second = second; 
  } 
  public static void main(String[] args) { 
    // một cách lập thành và khởi tạo đối tượng Time 
    Time t1 = new Time(); 
    t1.hour = 11; 
    t1.minute = 8; 
    t1.second = 3.14159; 
    System.out.println(t1); 
    // một cách khác để thực hiện việc tương tự 
    Time t2 = new Time(11, 8, 3.14159); 
    System.out.println(t2); 
  } 
}

Trong main, lần đầu tiên kích hoạt new, ta không cấp cho đối số nào, bởi vậy Java kích hoạt constructor thứ nhất. Vài dòng phía dưới thực hiện gán giá trị cho các biến thực thể.

Lần thứ hai kích hoạt new, ta cấp các đối số khớp với các tham số của constructor thứ hai. Cách khởi tạo biến thực thể này gọn gàng hơn và hiệu quả hơn một chút, song cách làm này có thể khó đọc, bởi nó không rõ ràng là giá trị nào được gán cho biến thực thể nào.

11.6  In các đối tượng

Kết quả của chương trình nêu trên là:

Time@80cc7c0
Time@80cc807

Khi Java in giá trị của kiểu đối tượng do người dùng định nghĩa, nó sẽ in tên kiểu cùng một mã thập lục phân đặc biệt riêng của từng đối tượng. Mã này bản thân nó chẳng có ý nghĩa gì; thực tế nó khác nhau tuỳ máy tính và thậm chí tuỳ cả những lần chạy chương trình. Nhưng có thể nó giúp ích cho việc gỡ lỗi, trong trường hợp bạn muốn theo dõi từng đối tượng riêng rẽ.

Để in các đối tượng theo cách có ý nghĩa hơn đối với người dùng (chứ không phải đối với lập trình viên), bạn có thể viết một phương thức với tên gọi kiểu như printTime:

public static void printTime(Time t) { 
  System.out.println(t.hour + ":" + t.minute + ":" + t.second); 
}

Hãy so sánh phương thức này với phiên bản printTime ở Mục 3.10.

Kết quả của phương thức này, nếu ta truyền t1 hoặc t2 làm đối số, sẽ là 11:8:3.14159. Mặc dù ta có thể nhận ra đây là giờ đồng hồ, nhưng cách viết này không hề theo chuẩn quy định. Chẳng hạn, nếu số phút hoặc số giây nhỏ hơn 10, ta sẽ luôn dự kiển rằng có số 0 đi trước. Ngoài ra, có thể ta còn muốn bỏ phần thập phân của số giây đi. Nói cách khác, ta muốn kết quả kiểu như 11:08:03.

Trong đa số những ngôn ngữ lập trình, có nhiều cách đơn giản để điều khiển định dạng đầu ra cho kết quả số. Trong Java thì không có cách đơn giản nào.

Java có những công cụ mạnh dành cho việc in dữ liệu được định dạng như giờ đồng hồ và ngày tháng, đồng thời cũng có công cụ để diễn giải dữ liệu vào được định dạng. Song thật không may là những công cụ như vậy không dễ sử dụng, nên tôi sẽ bỏ qua chúng trong khuôn khổ cuốn sách này. Nếu muốn, bạn có thể xem qua tài liệu của lớp Date trong gói java.util.

11.7  Các thao tác với đối tượng

Trong một vài mục tiếp theo, tôi sẽ giới thiệu ba dạng phương thức hoạt động trên các đối tượng:

hàm thuần tuý:
Nhận các đối tượng làm tham số nhưng không thay đổi chúng. Giá trị trả lại thuộc kiểu nguyên thuỷ hoặc một đối tượng mới tạo ra bên trong phương thức này.
phương thức sửa đổi:
Nhận đối số là các đối tượng rồi sửa đổi một vài, hoặc tất cả những đối tượng đó. Thường trả lại đối tượng rỗng (void).
phương thức điền:
Một trong các đối số là đối tượng “trống trơn” sẽ được phương thức điền thông tin vào. Về mặt kĩ thuật, đây cũng chính là một dang phương thức sửa đổi.

Với một phương thức cho trước ta thường có thể viết nó dưới dạng hàm thuần túy, phương thức sửa đổi hay phương thức điền. Tôi sẽ bàn thêm về ưu nhược điểm của từng hình thức một.

11.8  Các hàm thuần túy

Một phương thức được coi là hàm thuần túy nếu như kết quả chỉ phụ thuộc vào các đối số, và phương thức này không có hiệu ứng phụ như thay đổi một đối số hoặc in ra thông tin gì. kết quả duy nhất của việc kích hoạt một hàm thuần túy, đó là giá trị trả lại.

Một ví dụ là isAfter, để so sánh hai đối tượng Time rồi trả lại một boolean để chỉ định xem liệu toán hạng thứ nhất có xếp trước toán hạng thứ hai hay không:

  public static boolean isAfter(Time time1, Time time2) { 
    if (time1.hour > time2.hour) 
      return true; 
    if (time1.hour < time2.hour) 
      return false; 
    if (time1.minute > time2.minute) 
      return true; 
    if (time1.minute < time2.minute) 
      return false; 
    if (time1.second > time2.second) 
      return true; 
    return false; 
  }

Kết quả của phương thức này sẽ là gì nếu hai thời gian đã cho bằng nhau? Liệu đó có phải là kết quả phù hợp đối với phương thức này không? Nếu bạn viết tài liệu cho phương thức này, liệu bạn có đề cập rõ đến trường hợp đó không?

Ví dụ thứ hai là addTime, phương thức tính tổng hai thời gian. Chẳng hạn, nếu bây giờ là 9:14:30, và người làm bánh cần 3 giờ 35 phút, thì bạn có thể dùng addTime để hình dung ra khi nào bánh ra lò.

Sau đây là bản sơ thảo của phương thức này; nó chưa thật đúng:

  public static Time addTime(Time t1, Time t2) { 
    Time sum = new Time(); 
    sum.hour = t1.hour + t2.hour; 
    sum.minute = t1.minute + t2.minute; 
    sum.second = t1.second + t2.second; 
    return sum; 
  }

Mặc dù phương thức này trả lại một đối tượng Time, song nó không phải là constructor. Bạn cần xem lại và so sánh cú pháp của một phương thức dạng này với cú pháp của một constructor, vì chúng dễ gây nhầm lẫn.

Sau đây là một ví dụ về cách dùng phương thức. Nếu như currentTime chứa thời gian hiện tại và breadTime chứa thời gian cần để người thợ nướng bánh, thì bạn có thể dùng addTime để hình dung ra khi nào sẽ nướng xong bánh.

    Time currentTime = new Time(9, 14, 30.0); 
    Time breadTime = new Time(3, 35, 0.0); 
    Time doneTime = addTime(currentTime, breadTime); 
    printTime(doneTime);

Kết quả của chương trình, 12:49:30.0, là đúng. Mặt khác, cũng có những trường hợp mà kết quả không đúng. Bạn có đoán được một trường hợp như vậy không?

Vấn đề là ở chỗ phương thức này không xử lý được tình huống khi số giây hoặc số phút cộng lại vượt quá 60. Trong trường hợp đó, ta phải “nhớ” số giây còn dư vào cột số phút, hoặc nhớ số phút dư vào cột giờ.

Sau đây là một dạng đúng của phương thức này.

  public static Time addTime(Time t1, Time t2) { 
    Time sum = new Time(); 
    sum.hour = t1.hour + t2.hour; 
    sum.minute = t1.minute + t2.minute; 
    sum.second = t1.second + t2.second; 
    if (sum.second >= 60.0) { 
      sum.second -= 60.0; 
      sum.minute += 1; 
    } 
    if (sum.minute >= 60) { 
      sum.minute -= 60; 
      sum.hour += 1; 
    } 
    return sum; 
  }

Mặc dù cách này đúng, song chương trình bắt đầu dài dòng. Sau này tôi sẽ gợi ý một giải pháp khác ngắn hơn nhiều.

Đoạn mã lệnh trên giới thiệu hai toán tử mà ta chưa từng gặp, += và -=. Những toán tử này cho ta viết ngắn gọn lệnh tăng hoặc giảm biến. Chúng cũng gần giống như ++ và --, chỉ khác ở chỗ (1) chúng làm việc được cả với double lẫn int, và (2) lượng tăng hoặc giảm không nhất thiết bằng 1. Câu lệnh sum.second -= 60.0; tương đương với sum.second = sum.second - 60;

11.9  Phương thức sửa đổi

Xét một ví dụ về phương thức sửa đổi, phương thức increment, nhằm tăng thêm một số giây cho trước vào một đối tượng Time. Một lần nữa, ta có bản nháp phương thức này như sau:

  public static void increment(Time time, double secs) { 
    time.second += secs; 
    if (time.second >= 60.0) { 
      time.second -= 60.0; 
      time.minute += 1; 
    } 
    if (time.minute >= 60) { 
      time.minute -= 60; 
      time.hour += 1; 
    } 
  }

Dòng đầu tiên thực hiện thao tác cơ bản; những dòng còn lại để xử lý các trường hợp ta đã xét.

Liệu phương thức này có đúng không? Điều gì sẽ xảy ra nếu đối số secs lớn hơn nhiều so với 60? Trong trường hợp như vậy, trừ đi 60 một lần là chưa đủ; ta phải tiếp tục trừ đến khi second nhỏ hơn 60. Ta có thể làm điều này bằng cách thay các lệnh if bằng các lệnh while:

  public static void increment(Time time, double secs) { 
    time.second += secs; 
    while (time.second >= 60.0) { 
      time.second -= 60.0; 
      time.minute += 1; 
    } 
    while (time.minute >= 60) { 
      time.minute -= 60; 
      time.hour += 1; 
    } 
  }

Giải pháp này đúng đắn, nhưng chưa hiệu quả lắm. Bạn có thể nghĩ ra lời giải nào không cần đến tính lặp hay không?

11.10  Các phương thức điền

Thay vì việc tạo nên đối tượng mới mỗi khi addTime được kích hoạt, ta có thể yêu cầu chương trình gọi hãy cung cấp một đối tượng nơi mà addTime lưu kết quả. Hãy so sánh đoạn mã sau với phiên bản trước:

  public static void addTimeFill(Time t1, Time t2, Time sum) { 
    sum.hour = t1.hour + t2.hour; 
    sum.minute = t1.minute + t2.minute; 
    sum.second = t1.second + t2.second; 
    if (sum.second >= 60.0) { 
      sum.second -= 60.0; 
      sum.minute += 1; 
    } 
    if (sum.minute >= 60) { 
      sum.minute -= 60; 
      sum.hour += 1; 
    } 
  }

Kết quả được lưu trong sum, nên kiểu trả về là void.

Các phương thức sửa đổi và phương thức điền đều hiệu quả vì chúng không phải tạo nên đối tượng mới. Nhưng chúng lại gây khó khăn trong việc cô lập các phần khác nhau của chương trình; trong những dự án lớn chúng có thể gây nên lỗi rất khó tìm ra.

Các hàm thuần túy giúp ta quản lý tính chất phức tạp của những dự án lớn, phần là nhờ ngăn không cho những loại lỗi nhất định không thể xảy ra. Hơn nữa, hàm thuần túy còn thích hợp với những kiểu lập trình ghép và lồng. Và vì kết quả của hàm thuần túy chỉ phụ thuộc vào tham số, ta có thể tăng tốc cho nó bằng cách lưu giữ những giá trị đã tính toán từ trước.

Tôi gợi ý rằng bạn nên viết hàm thuần túy mỗi lúc thấy được, và chỉ dùng đến phương thức sửa đổi khi thấy rõ ưu điểm vượt trội.

11.11  Lập kế hoạch và phát triển tăng dần

Trong chương trình này tôi giới thiệu một quá trình phát triển chương trình với tên gọi lập nguyên mẫu nhanh1. Với từng phương thức, tôi viết một bản sơ thảo để thực hiện tính toán cơ bản, rồi kiểm tra nó với một vài trường hợp, sửa những lỗi bắt gặp được.

Cách tiếp cận này có thể hiệu quả, song nó có thể dẫn đến mã lệnh phức tạp một cách không cần thiết—vì nó xử lý quá nhiều trường hợp đặc biệt—và cũng kém tin cậy—vì thật khó tự thuyết phục rằng bạn đã tìm thấy tất cả những lỗi trong chương trình.

Một cách khác là xem xét kĩ hơn vấn đề nhằm tìm mấu chốt có thể giúp việc lập trình dễ dàng hơn. Trong trường hợp này điểm mấu chốt bên trong là: Time thực ra là một số có ba chữ số trong hệ cơ số 60! Số giây, second, là hàng đơn vị, số phút, minute, là hàng 60, còn số giờ, hour, là hàng 3600.

Khi ta viết addTime và increment, thực chất là ta đang tính cộng ở hệ 60; đó là lý do tại sao ta phải “nhớ” từ hàng này sang hàng khác.

Một cách tiếp cận khác đối với tổng thể bài toán là chuyển Time thành double rồi lợi dụng khả năng tính toán của máy đối với double. Sau đây là một phương thức chuyển đổi Time thành double:

  public static double convertToSeconds(Time t) { 
    int minutes = t.hour * 60 + t.minute; 
    double seconds = minutes * 60 + t.second; 
    return seconds; 
  }

Bây giờ tất cả những gì ta cần là cách chuyển từ double sang đối tượng Time. Ta có thể viết một phương thức để thực hiện điều này, song có lẽ hợp lý hơn la viết một constructor thứ ba:

  public Time(double secs) { 
    this.hour =(int)(secs / 3600.0); 
    secs -= this.hour * 3600.0; 
    this.minute =(int)(secs / 60.0); 
    secs -= this.minute * 60; 
    this.second = secs; 
  }

Constructor này hơi khác những constructor khác; nó bao gồm những tính toán bên cạnh phép gán cho các biến thực thể.

Có thể bạn phải suy nghĩ để tự thuyết phục bản thân rằng kĩ thuật mà tôi dùng để chuyển từ hệ cơ số này sang cơ số kia là đúng. Nhưng một khi bạn đã bị thuyết phục rồi, ta có thể dùng những phương thức này để viết lại addTime:

  public static Time addTime(Time t1, Time t2) { 
    double seconds = convertToSeconds(t1) + convertToSeconds(t2); 
    return new Time(seconds); 
  }

Mã lệnh trên ngắn hơn phiên bản gốc, và dễ thấy hơn hẳn rằng mã lệnh này đúng đắn (với giả thiết thường lệ rằng những phương thức nó kích hoạt cũng đều đúng). Việc viết lại increment theo cách tương tự được dành cho bạn như một bài tập.

11.12  Khái quát hóa

Trong chừng mực nào đó, việc chuyển đổi qua lại giữa các hệ cơ số 60 và 10 khó hơn việc xử lý thời gian đơn thuần. Việc chuyển hệ cơ số thì trừu tượng hơn, còn trực giác của ta xử lý thời gian tốt hơn.

Nhưng nếu ta có hiểu biết sâu để coi thời gian như các số trong hệ 60, và đầu tư công sức viết những phương thức chuyển đổi (convertToSeconds và constructor thứ ba), ta sẽ thu được một chương trình ngắn hơn, dễ đọc và gỡ lỗi, đồng thời đáng tin cậy hơn.

Việc bổ sung các đặc tính sau này cũng dễ dàng hơn. Hãy tưởng tượng ta cần trừ hai đối tượng Time để tìm ra khoảng thời gian giữa chúng. Cách làmthực hiện tính trừ có nhớ. Nhưng dùng phương thức để chuyển đổi sẽ dễ hơn nhiều.

Điều trớ trêu là, đôi khi việc làm cho bài toán khó hơn (tổng quát hơn) lại khiến cho dễ dàng hơn (ít trường hợp đặc biệt, ít khả năng gây ra lỗi).

11.13  Thuật toán

Khi bạn viết một lời giải tổng quát cho một lớp các bài toán, thay vì tìm lời giải riêng cho một bài toán riêng lẻ, bạn đã viết một thuật toán. Thật không dễ định nghĩa thuật ngữ này, bởi vậy tôi sẽ cố gắng thử vài cái tiếp cận khác nhau.

Trước hết, hãy xét một số thứ không phải là thuật toán. Khi bạn học tính nhân giữa hai số, có lẽ bạn đã ghi nhớ bản cửu chương. Thật ra, bạn đã học thuộc lòng 100 lời giải cụ thể, bởi vậy kiến thức này thực sự không phải là thuật toán.

Nhưng nếu bạn “lười biếng,” có thể bạn đã học hỏi được mấy mẹo vặt. Chẳng hạn, để tính tính của một số n với 9, bạn có thể viết n−1 là chữ số thứ nhất và 10−n là chữ số thứ hai. Mẹo này là lời giải tổng quát để nhân một số dưới mười bất kì với 9. Đó chính là thuật toán!

Tương tự, những kĩ thuật bạn học để cộng có nhớ, trừ có nhớ, và phép chia số lớn đều là những thuật toán. Một trong những đặc điểm của thuật toán là chúng không cần trí thông minh để thực hiện. Chúng chỉ là những cơ chế máy móc trong đó từng bước nối tiếp nhau theo một loạt những nguyên tắc đơn giản.

Theo ý kiến của tôi, thật đáng ngại khi thấy rằng chúng ta dành quá nhiều thời gian trên lớp để học cách thực hiện những thuật toán mà, nói thẳng ra là, không cần trí thông minh gì cả. Mặt khác, quá trình thiết kế những thuật toán lại thú vị, đầy thử thách trí tuệ, và là phần trung tâm của việc mà ta gọi là lập trình.

Có những việc mà con người làm theo lẽ tự nhiên, chẳng khó khăn hay phải suy nghĩ gì, lại là những thứ khó biểu diễn bằng thuật toán nhất. Việc hiểu ngôn ngữ là một ví dụ điển hình. Chúng ta ai cũng làm vậy, nhưng đến nay chưa ai giải thích được rằng ta làm vậy bằng cách nào, ít nhất là biểu diễn dưới dạng thuật toán.

Bạn sẽ sớm có cơ hội thiết kế những thuật toán đơn giản cho nhiều bài toán khác nhau.

11.14  Thuật ngữ

lớp:
Trước đây, tôi đã định nghĩa lớp là một tập hợp các phương thức có liên quan. Trong chương này ta còn được biết rằng lời định nghĩa lớp cũng đồng thời là một bản mẫu của một kiểu đối tượng mới.
thực thể:
Thành viên của một lớp. Mỗi đối tượng đều là thực thể của một lớp nào đó.
constructor:
Một phương thức đặc biệt để khởi tạo các biến thực thể của một đối tượng mới lập nên.
lớp khởi động:
Lớp có chứa phương thức main nơi bắt đầu việc thực thi chương trình.
hàm thuần túy:
Phương thức mà kết quả chỉ phụ thuộc vào các tham số của nó, và không gây hiệu ứng phụ nào ngoài việc trả lại một giá trị.
phương thức sửa đổi:
Phương thức làm thay đổi một hay nhiều đối tượng nhận làm tham số, và thường trả lại void.
phương thức điền:
Kiểu phương thức nhận tham số là một đối tượng “trống không” và điền vào những biến thực thể của nó thay vì việc phát sinh một giá trị trả lại.
thuật toán:
Một loạt những chỉ dẫn nhằm giải một lớp các bài toán theo một quá trình máy móc.

11.15  Bài tập

Bài tập 1   Trong trò chơi trên bàn có tên Scrabble2, mỗi miếng vuông để xếp lên bàn sẽ chứa một chữ cái, để xêm nên các từ có nghĩa, và đồng thời có một điểm số; từ đó ta tính được điểm cho các từ khác nhau.

  1. Hãy viết một định nghĩa lớp có tên Tile để biểu diễn các miếng vuông Scrabble. Các biến thực thể sẽ gồm một kí tự có tên letter và một số nguyên có tên value.
  2. Hãy viết một constructor để nhận các tham số letter và value rồi khởi tạo các biến thực thể.
  3. Viết một phương thức có tên printTile để nhận tham số là một đối tượng Tile rồi in ra các biến thực thể dưới định dạng mà người thường có thể đọc được.
  4. Viết một phương thức có tên testTile để tạo nên một đối tượng Tile có chữ cái Z và giá trị 10, rồi dùng printTile để in ra trạng thái của đối tượng này.

Mục đích của bài tập này là để luyện tập phần cơ chế tạo nên một lời định nghĩa lớp và mã lệnh để kiểm tra nó.

Bài tập 2   Hãy viết một định nghĩa lớp của Date, một kiểu đối tượng bao gồm ba số nguyên là yearmonth và day. Lớp này cần phải có hai constructor. Constructor thứ nhất không nhận tham số nào. Constructor thứ hai nhận các tham số mang tên yearmonth và day, rồi dùng chúng để khởi tạo các biến thực thể. Hãy viết một phương thức main để tạo nên một đối tượng Date mới có tên birthday. Đối tượng mới này để chứa ngày sinh nhật của bạn. Có thể dùng constructor nào cũng được.

Bài tập 3  Phân số là số có thể biểu điễn được dưới dạng tỉ số giữa hai số nguyên. Chẳng hạn, 2/3 là một phân số,  và bạn cũng có thể coi 7 là một phân số với mẫu số ngầm định bằng 1. Ở bài tập này, bạn sẽ viết một lời định nghĩa lớp cho các phân số.

  1. Lập một chương trình mới có tên Rational.java để định nghĩa một lớp tên là Rational. Một đối tượng Rational phải có hai biến thực thể số nguyên để lưu trữ tử số và mẫu số.
  2. Viết một constructor không nhận tham số nào để đặt tử số bằng 0 và mẫu số bằng 1.
  3. Viết một phương thức có tên printRational để nhận vào đối số là một đối tượng Rational rồi in nó ra theo định dạng hợp lý.
  4. Viết một phương thức main để lập nên một đối tượng mới có kiểu là Rational, đặt các biến thực thể của nó bằng giá trị cụ thể, rồi in đối tượng này ra.
  5. Đến đây, bạn đã có một chương trình tối thiểu có thể chạy thử được. Hãy chạy để kiểm tra nó, và gỡ lỗi, nếu cần.
  6. Viết một constructor thứ hai cho lớp này có nhận vào hai đối số rồi sử dụng chúng để khởi tạo các biến thực thể.
  7. Hãy viết một phương thức có tên negate để đảo dấu của phân số. Phương thức này phải là một phân thức sửa đổi, và vì vậy cần phải trả lại void. Hãy viết thêm dòng lệnh trong main để kiểm tra phương thức mới này.
  8. Viết một phương thức có tên invert để nghịch đảo số bằng cách tráo đổi tử số và mẫu số. Hãy viết thêm dòng lệnh trong main để kiểm tra phương thức mới này.
  9. Viết một phương thức có tên toDouble để chuyển đổi phân số thành một số double (số dấu phẩy động) rồi trả lại kết quả. Phương thức này là một hàm thuần tuý; nó không thay đổi đối tượng. Như thường lệ, hãy kiểm tra phương thức mới viết.
  10. Viết một phương thức có tên reduce để rút gọn một phân số về dạng tối giản bằng cách tìm ước số chung lớn nhất của tử số và mẫu số rồi cùng chia cả tử lẫn mẫu cho ước chung này. Phương thức nêu trên phải là một hàm thuần tuý; nó không được phép thay đổi các biến thực thể của đối tượng mà nó được kích hoạt lên. Để tính ước số chung lớn nhất, hãy xem Bài tập 10 của Chương 6).
  11. Viết một phương thức có tên add để nhận hai đối số là hai Rational rồi trả lại một đối tượng Rational mới. Đối tượng được trả lại phải chứa tổng của các đối số. có vài cách thực hiện phép cộng này. Bạn có thể dùng bất kì cách nào, nhưng hãy đảm bảo rằng kết quả của phép tính phải được rút gọn sao cho tử và mẫu không có ước số chung nào khác (ngoài 1).

Mục đích của bài tập này là nhằm viết một lời định nghĩa hàm có chứa nhiều loại phương thức, bao gồm constructors, phương thức sửa đổi, và hàm thuần tuý.


1
Cái mà tôi gọi là “nguyên mẫu nhanh” (rapid prototyping)  ở đây rất giống với cách phát triển dựa trên kiểm thử (test-driven development, TDD); sự khác biệt là ở chỗ TDD thường dựa trên kiểm thử tự động. Xem http://en.wikipedia.org/wiki/Test-driven_development.
2
Scrabble là một nhãn hiệu đã đăng kí ở Hoa Kì và Canada, thuộc về cty Hasbro Inc., và ở các nước còn lại trên thế giới, thì thuộc về J.W. Spear & Sons Limited ở Maidenhead, Berkshire, Anh Quốc, công ty nhánh của Mattel Inc.

3 phản hồi

Filed under Think Java

3 responses to “Chương 11: Tự tạo nên những đối tượng riêng

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

  2. “Để tính ước số chung lớn nhất, hãy xem Bài tập 10 của Chương 8)”.nghiêm túc đấy chứ ?

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