Category Archives: Java

Phụ lục D: Gỡ lỗi

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

Chiến thuật gỡ lỗi hay nhất còn tuỳ thuộc vào loại lỗi bạn mắc phải:

  • Lỗi cú pháp tạo ra bởi trình biên dịch, nhằm chỉ định có trục trặc trong cú pháp của chương trình. Chẳng hạn: bỏ mất dấu chấm phẩy ở cuối câu lệnh.
  • Các biệt lệ được tạo ra nếu có điều gì trục trặc khi chương trình đang chạy. Chẳng hạn: một vòng đệ quy vô hạn cuối cùng sẽ gây nên biệt lệ StackOverflowException.
  • Lỗi logic khiến cho chương trình thực hiện việc làm sai. Chẳng hạn, một biểu thức có thể không được tính toán đúng theo trình tự mà bạn định liệu, cho ra kết quả không lường trước.

Tiếp tục đọc

16 bình luận

Filed under Think Java

Phụ lục C: Phát triển chương trình

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

C.1  Các chiến lược

Trong cuốn sách tôi đã trình bày những chiến lược khác nhau để phát triển chương trình, bởi vậy giờ đây tôi muốn tập hợp chúng lại. Nến tảng của tất cả chiến lược này cùng là phát triển tăng dần, vốn như sau:

  1. Lấy điểm khởi đầu là chương trình chạy được, chỉ thực hiện một động tác dễ thấy, chẳng hạn in dữ liệu nào đó.
  2. Mỗi lúc chỉ bổ sung thêm một ít dòng lệnh, và cứ thay đổi một lần lại phải kiểm tra chương trình.
  3. Lặp lại công đoạn đến khi chương trình thực hiện được việc dự kiến.

Tiếp tục đọc

1 bình luận

Filed under Think Java

Phụ lục B: Đầu vào và đầu ra trong Java

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

B.1  Đối tượng System

Lớp System cung cấp các phương thức và đối tượng thu nhận đầu vào từ bàn phím, in dòng chữ lên màn hình, và thực hiện vào ra (input/output, I/O) đối với file. System.out là đối tượng để hiển thị lên màn hình. Khi bạn kích hoạt print và println, bạn đã kích hoạt chúng từ System.out.

Thậm chí bạn có thể dùng chính System.out để in ra System.out:

    System.out.println(System.out);

Tiếp tục đọc

1 bình luận

Filed under Think Java

Phụ lục A: Đồ họa

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

A.1  Đồ họa Java 2 chiều

Phụ lục này đưa ra các ví dụ và bài tập minh họa cho tính năng đồ họa trong Java. Có một số cách tạo nên đồ họa trong Java; cách đơn giản nhất là dùng java.awt.Graphics. Sau đây là một ví dụ hoàn chỉnh: Tiếp tục đọc

6 bình luận

Filed under Think Java

Chương 16: GridWorld, phần 3

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

Nếu bạn chưa làm bài tập trong các Chương 5 và 10, bạn hãy nên làm đi trước khi đọc chương này. Xin được nhắc lại, bạn có thể tìm tài liệu về các lớp GridWorld ở http://www.greenteapress.com/thinkapjava/javadoc/gridworld/.

Phần 3 của cuốn Hướng dẫn sinh viên về GridWorld trình bày những lớp cấu thành GridWorld và các mối tương tác giữa chúng. Đây là một ví dụ về thiết kế hương đối tượng và làm một cơ hổi để ta bàn luận những vấn đề thiết kế hướng đối tượng.

Nhưng trước khi bạn đọc cuốn Hướng dẫn sinh viên, sau đây có thêm một số điều mà bạn cần biết. Tiếp tục đọc

1 bình luận

Filed under Think Java

Chương 15: Lập trình hướng đối tượng

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

15.1  Các ngôn ngữ và phong cách lập trình

Có nhiều ngôn ngữ lập trình khác nhau, và không kém mấy về số lượng là các phong cách lập trình (còn gọi là mẫu hình). Những chương trình mà ta đã viết đến giờ đều thuộc phong cách thủ tục, bởi chú ý được dồn vào việc quy định các thủ tục tính toán. Tiếp tục đọc

1 bình luận

Filed under Think Java

Chương 14: Đối tượng chứa các mảng

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

CẢNH BÁO: Trong chương này, ta tiến thêm một bước nữa về lập hướng đối tượng nhưng vẫn chưa hẳn đến được đó. Bởi vậy, nhiều ví dụ vẫn chưa đúng giọng Java, nghĩa là chưa phải mã lệnh Java chuẩn. Hình thức trung chuyển này (hi vọng rằng) sẽ giúp bạn học, nhưng thực tế tôi không viết mã lệnh như thế này.

Bạn có thể tải về mã lệnh cho chương này từ: http://thinkapjava.com/code/Card2.java.

14.1  Lớp Deck

Ở chương trước, ta đã làm việc với một mảng các đối tượng, nhưng cũng đề cập rằng hoàn toàn có thể có đối tượng có chứa biến thực thể là mảng. Trong chương này, ta tạo ra một đối tượng Deck có chứa một mảng những đối tượng Card. Tiếp tục đọc

7 bình luận

Filed under Think Java

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.

Tiếp tục đọc

5 bình luận

Filed under Think Java

Chương 12: Mảng

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

Mảng là một tập hợp các giá trị trong đó mỗi giá trị được xác định bởi một chỉ số. Bạn có thể lập nên các mảng int, mảng double, hay mảng chứa bất kì kiểu dữ liệu nào khác, nhưng các giá trị trong cùng một mảng phải có kiểu giống nhau.

Về mặt cú pháp, các kiểu mảng trông giống như các kiểu dữ liệu khác trong Java chỉ trừ đặc điểm: theo sau là []. Chẳng hạn, int[] là kiểu “mảng các số nguyên” còn double[] là kiểu “mảng các số phẩy động.”

Bạn có thể khai báo các biến với những kiểu như vậy theo cách thông thường:

    int[] count; 
    double[] values;

Trước khi bạn khởi tạo các biến này, chúng được đặt về null. Để tự tay tạo các mảng, hãy dùng new.

    count = new int[4]; 
    values = new double[size];

Lệnh gán thứ nhất khiến cho count tham chiếu đến một mảng gồm 4 số nguyên; lệnh thứ hai tham chiếu khiến values tham chiếu đến một mảng các double. Số phần tử trong values phụ thuộc vào size. Bạn có thể dùng bất kì biểu thức nguyên nào để làm kích thước mảng.

Hình vẽ sau cho thấy cách biểu diễn mảng trong sơ đồ trạng thái:

Các số lớn ghi bên trong các ô là những phần tử của mảng. Các con số nhỏ bên ngoài hộp là những chỉ số dùng để xác định từng ô. Khi bạn huy động một mảng các int, những phần tử của chúng đều được khởi tạo bằng không.

12.1  Truy cập các phần tử

Để lưu các giá trị trong mảng, hãy dùng toán tử [] operator. Chẳng hạn, count[0] tham chiếu đến phần tử “thứ không” của mảng, còn count[1] tham chiếu đến phần tử “thứ một”. Bạn có thể dùng toán tử [] bất cứ đâu trong một biểu thức:

    count[0] = 7; 
    count[1] = count[0] * 2; 
    count[2]++; 
    count[3] -= 60;

Tất cả đó đều là những phép gán hợp lệ. Sau đây là kết quả của đoạn mã trên:

Những phần tử của mảng được đánh số từ 0 tới 3, nghĩa là không có phần tử nào mang chỉ số 4. Điều này rất quen thuộc, bởi ta đã thấy điều tương tự trong chỉ số của String. Dù vậy, việc vượt quá phạm vi của mảng vẫn là kiểu lỗi thường gặp, bằng cách đó phát ra biệt lệ ArrayOutOfBoundsException.

Bạn có thể dùng bất kì biểu thức nào làm chỉ số cũng được, miễn là nó có kiểu int. Một trong những cách thông dụng nhất để đánh chỉ số của mảng là dùng biến vòng lặp. Chẳng hạn:

    int i = 0; 
    while (i < 4) { 
      System.out.println(count[i]); i++; 
    }

Đây là một vòng lặp while tiêu chuẩn để đếm từ 0 lên 4, và khi biến lặp i bằng 4, điều kiện lặp sẽ không thỏa mãn và vòng lặp kết thúc. Như vậy, phần thân vòng lặp chỉ được thực thi khi i là 0, 1, 2 và 3.

Mỗi lần qua vòng lặp ta dùng i làm chỉ số trong mảng, để in ra phần tử thứ i. Hình thức duyệt mảng này rất thông dụng.

12.2  Sao chép mảng

Khi bạn sao chép một biến mảng, hãy nhớ rằng bạn đang sao chép tham chiếu tới mảng. Ví dụ:

    double[] a = new double [3]; 
    double[] b = a;

Đoạn mã lệnh này tạo nên một mảng ba số double, rồi đặt hai biến khác nhau để tham chiếu tới nó. Trường hợp này cũng là một dạng trùng tên (aliasing).

Bất kì thay đổi nào đối với một trong hai mảng đều được phản ánh trên mảng còn lại. Thường thì đây không phải là điều bạn muốn; mà bạn muốn huy động một mảng mới rồi sao chép các phần tử từ mảng này sang mảng kia.

    double[] b = new double [3]; 
    int i = 0; 
    while (i < 4) { 
      b[i] = a[i]; i++; 
    }

12.3  Mảng và đối tượng

Mảng giống với đối tượng ở nhiều điểm:

  • Khi khai báo một biến mảng, bạn nhận được tham chiếu đến mảng.
  • Bạn phải dùng new để tự tạo ra mảng.
  • Khi truyền mảng làm đối số, bạn truyền một tham chiếu, nghĩa là phương thức được kích hoạt có thể thay đổi nội dung của mảng.

Một số đối tượng mà ta đã xét, như Rectangle, tương đồng với mảng ở chỗ chúng cũng là tập hợp các giá trị. Vậy nảy sinh câu hỏi, “Mảng bốn số nguyên thì khác một đối tượng Rectangle ở chỗ nào?”

Nếu bạn quay về định nghĩa của “mảng” từ đầu chương, bạn sẽ thấy một khác biệt: các phần tử của mảng được xác định bằng chỉ số, còn các phần tử của đối tượng xác định bằng tên.

Một khác biệt nữa là các phần tử trong mảng phải có cùng kiểu. Còn đối tượng có thể chứa những biến thực thể khác kiểu nhau.

12.4 Vòng lặp for

Các vòng lặp mà ta đã dùng đều có một số điểm chung. Chúng đều bắt đầu bằng việc khởi tạo một biến; chúng đều có một phép kiểm tra, hay điều kiện, phụ thuộc vào biến đó; và bên trong vòng lặp thì chúng thực hiện tác động nhất định đến biến đó, như tăng giá trị.

Dạng vòng lặp này thông dụng đến nỗi còn một lệnh lặp khác, gọi là for, để diễn đạt một cách gọn gàng hơn. Cú pháp chung của nó như sau:

    for (KHỞI TẠO; ĐIỀU KIỆN; GIA TĂNG) { 
      PHẦN THÂN 
    }

Lệnh này tương đương với

    KHỞI TẠO; 
    while (ĐIỀU KIỆN) { 
      PHẦN THÂN 
      GIA TĂNG 
    }

ngoại trừ nó gọn gàng hơn vì đã đặt tất cả những câu lệnh liên quan đến lặp vào một chỗ, và do đó dễ đọc hơn. Chẳng hạn:

    for (int i = 0; i < 4; i++) { 
      System.out.println(count[i]); 
    }

thì tương đương với

    int i = 0; 
    while (i < 4) { 
      System.out.println(count[i]); 
      i++; 
    }

12.5  Chiều dài của mảng

Tất cả mảng đều có một biến thực thể tên là length. Chẳng cần nói thì bạn cũng biết, biến này chứa chiều dài của mảng (số phần tử). Nên lấy giá trị này làm giới hạn trên của vòng lặp thay vì một giá trị cố định. Làm như vậy, nếu như kích thước của mảng thay đổi thì bạn sẽ không phải dò lại cả chương trình để thay đổi các vòng lặp; chương trình sẽ chạy được đúng với mọi kích cỡ mảng khác nhau.

    for (int i = 0; i < a.length; i++) { 
      b[i] = a[i]; 
    }

Lần cuối cùng mà phần thân của vòng lặp được thực thi, i sẽ là a.length - 1, chỉ số của phần tử cuối. Khi i bằng với a.length, điều kiện sẽ không thỏa mãn và phần thần sẽ không được thực thi. Đây là điều tốt, vì sẽ có biệt lệ được phát ra. Đoạn mã này giả thiết rằng mảng b phải có bằng số phần tử, hoặc nhiều hơn so với a.

12.6  Số ngẫu nhiên

Đa số các chương trình máy tính đều làm cùng một công việc mỗi khi nó được thực thi; chương trình như vậy được gọi là có tính tất định. Thông thường, tất định là tính chất tốt, vì ta luôn trông đợi cùng một phép tính sẽ chỉ cho một kết quả. Song có những chương trình ứng dụng mà ta muốn kết quả phải không đoán trước được. Một ví dụ hiển nhiên là các trò chơi điện tử, song cũng có những ứng dụng khác nữa.

Để một chương trình thực sự phi tất định hóa ra lại không dễ chút nào, song ít nhất vẫn có những cách làm chương trình có vẻ như phi tất định. Một cách làm trong số đó là việc phát sinh những số ngẫu nhiên và dùng nó để quy định kết quả của chương trình. Java có một phương thức để phát sinh ra các số giả ngẫu nhiên, vốn không thực sự ngẫu nhiên, nhưng sẽ dùng được cho mục đích ta cần.

Hãy đọc tài liệu về phương thức random trong lớp Math. Giá trị trả lại là một doublenằm giữa 0.0 và 1.0. Chính xác là, nó lớn hơn hoặc bằng 0.0 và nhỏ hơn 1.0. Mỗi lần kích hoạt random bạn sẽ nhận được con số tiếp theo trong dãy số giả ngẫu nhiên. Để thấy được một mẫu của dãy ngẫu nhiên, hãy chạy vòng lặp sau:

    for (int i = 0; i < 10; i++) { 
      double x = Math.random(); 
      System.out.println(x); 
    }

Để phát sinh một số double giữa 0.0 và một giới hạn trên như high, bạn có thể nhân x với high.

12.7  Mảng các số ngẫu nhiên

Bằng cách nào để phát sinh một số nguyên ngẫu nhiên giữa low và high? Nếu phương thức randomInt bạn viết đã chính xác, thì mỗi giá trị trong khoảng từ low lên đến high-1 phải có cùng xác suất xuất hiện. Nếu bạn phát sinh một dãy số rất dài, thì mỗi giá trị phải xuất hiện ít nhất là có số lần xấp xỉ nhau.

Một cách kiểm tra phương thức vừa viết là phát inh rất nhiều số ngẫu nhiên, lưu trữ chúng vào một mảng, rồi đếm số lần từng giá trị xuất hiện.

Phương thức sau nhận một đối số duy nhất là kích thước của mảng. Phương thức có nhiệm vụ huy động một mảng số nguyên mới, điền vào những giá trị ngẫu nhiên, rồi trả lại tham chiếu đến mảng mới điền.

  public static int[] randomArray(int n) { 
    int[] a = new int[n]; 
    for (int i = 0; i<a.length; i++) { 
      a[i] = randomInt(0, 100); 
    } 
    return a; 
  }

Kiểu trả lại là int[], nghĩa là phương thức này trả lại một mảng các số nguyên. Để kiểm tra phương thức này, thật tiện nếu có một phương thức để in ra nội dung của mảng.

  public static void printArray(int[] a) { 
    for (int i = 0; i<a.length; i++) { 
      System.out.println(a[i]); 
    } 
  }

Đoạn mã sau đây phát sinh một mảng rồi in nó ra:

  int numValues = 8; 
  int[] array = randomArray(numValues); 
  printArray(array);

Trên máy tính của tôi, kết quả là

27
6
54
62
54
2
44
81

trông thật là ngẫu nhiên. Kết quả của bạn có thể sẽ khác đi.

Nếu đây là những điểm thi (và nếu vậy thì điểm thật tệ), giáo viên có thể biểu diễn kết quả trước lớp dưới dạng một histogram, vốn là một tập hợp những biến đếm để theo dõi số lần mỗi giá trị xuất hiện.

Với điểm thi, có thể ta dành ra 10 biến đếm để theo dõi bao nhiêu học sinh đạt điểm đầu 9 (90 – 99), bao nhiêu đạt điểm đầu 8, v.v. Một số mục tiếp theo sẽ dành cho việc phát triển mã lệnh tạo ra histogram.

12.8  Đếm

Một cách tiếp cận hay đến những bài toán như thế này là nghĩ về những phương thức đơn giản, dễ viết, rồi kết hợp chúng lại thành lời giải. Quá trình này được gọi là phát triển từ dưới lên. Xem http://en.wikipedia.org/wiki/Top-down_and_bottom-up_design.

Thật không dễ thấy điểm khởi đầu của quá trình, nhưng một cách hợp lý là tìm kiếm những bài toán nhỏ khớp với một dạng mẫu mà bạn đã biết trước.

Ở Mục 8.7 ta đã thấy một vòng lặp duyệt qua một chuỗi rồi đếm số lần xuất hiện một chữ cái cho trước. Bạn có thể coi chương trình này như một ví dụ về một mẫu có tên gọi “duyệt và đếm.” Những yếu tố tạo nên dạng mẫu này là:

  • Một tập hợp hoặc tập dữ liệu có thể duyệt được, như một mảng hoặc chuỗi.
  • Một phép thử mà bạn có thể áp dụng cho từng phần tử trong tập đó.
  • Một con trỏ để theo dõi xem có bao nhiêu phần tử đạt được phép thử này.

Trong trường hợp đang xét, tập hợp là một mảng các số nguyên. Phép thử là liệu rằng một điểm số cho trước có rơi vào một khoảng giá trị cho trước hay không.

Sau đây là một phương thức có tên inRange để đếm số phần tử trong mảng rơi vào một khoảng cho trước. Các tham số bao gồm mảng và hai số nguyên để quy định giới hạn dưới và trên của khoảng này.

  public static int inRange(int[] a, int low, int high) { 
    int count = 0; 
    for (int i = 0; i < a.length; i++) { 
      if (a[i] >= low && a[i] < high) 
        count++; 
    } 
    return count; 
  }

Tôi đã không cụ thể hóa rằng liệu việc giá trị nào đó đúng bằng low hoặc high thì sẽ được coi là rơi vào khoảng hay không, nhưng từ mã lệnh bạn có thể thấy rằng low được coi là rơi vào trong còn high thì không. Điều này giúp ta tránh được việc đếm phần tử hai lần.

Bây giờ ta có thể đếm số điểm trong những khoảng cần quan tâm:

    int[] scores = randomArray(30); 
    int a = inRange(scores, 90, 100); 
    int b = inRange(scores, 80, 90); 
    int c = inRange(scores, 70, 80); 
    int d = inRange(scores, 60, 70); 
    int f = inRange(scores, 0, 60);

12.9  Histogram

Mã lệnh này có sự lặp lại, nhưng cũng chấp nhận được khi có ít khoảng khác nhau. Nhưng thử tưởng tượng nếu ta muốn theo dõi số lần xuất hiện của từng điểm số, nghĩa là 100 giá trị có thể. Lúc đó liệu bạn còn muốn viết mã lệnh nữa không?

    int count0 = inRange(scores, 0, 1); 
    int count1 = inRange(scores, 1, 2); 
    int count2 = inRange(scores, 2, 3); 
    ... 
    int count3 = inRange(scores, 99, 100);

Tôi không nghĩ vậy. Điều mà ta thực sự mong muốn là cách để lưu trữ 100 số nguyên, tốt nhất là cách mà ta dùng được chỉ số để truy cập đến từng giá trị. Gợi ý: dùng mảng.

Dạng mẫu đếm cũng tương tự bất kể việc ta dùng một biến đếm hay một mảng các biến đếm. Trong trường hợp sau này, ta khởi tạo mảng bên ngoài vòng lặp. Sau đó, trong vòng lặp, ta kích hoạt inRange và lưu lại giá trị:

    int[] counts = new int[100]; 
    for (int i = 0; i < counts.length; i++) { 
      counts[i] = inRange(scores, i, i+1); 
    }

Ở đây chỉ có một điều mẹo mực: chúng ta dùng biến lặp với hai tác dụng: làm chỉ số bên trong mảng, và làm tham số cho inRange.

12.10  Lời giải “một lượt”

Mã lệnh nói trên hoạt động được, song không hiệu quả như khả năng mà lẽ ra nó phải làm được. Mỗi lần đoạn chương trình kích hoạt inRange, nó duyệt toàn bộ mảng. Khi số các khoảng giá trị nhiều lên, sẽ có rất nhiều lần duyệt.

Sẽ tốt hơn nếu chỉ chạy một lượt qua mảng, và với mỗi giá trị, ta đi tính xem nó rơi vào khoảng nào. Tiếp theo ta có thể tăng biến đếm thích hợp. Ở ví dụ này, phép tính đó là nhỏ nhặt, bởi vì ta có thể dùng bản thân giá trị đó làm chỉ số cho mảng các biến đếm.

Sau đây là đoạn mã để duyệt một mảng các điểm số và phát sinh ra histogram.

    int[] counts = new int[100]; 
    for (int i = 0; i < scores.length; i++) { 
      int index = scores[i]; 
      counts[index]++; 
    }

12.11  Thuật ngữ

mảng:
Một tập hợp các giá trị, trong đó những giá trị này phải cùng kiểu, và mỗi giá trị được xác định bằng một chỉ số.
phần tử:
Một trong số các giá trị thuộc mảng. Toán tử [] được dùng để lựa chọn phần tử.
chỉ số:
Một biến nguyên hoặc giá trị nguyên để chỉ định một phần tử của mảng.
tất định:
Một chương trình thực hiện đúng một công việc mỗi khi nó được kích hoạt.
giả ngẫu nhiên:
Một dãy con số trông có vẻ ngẫu nhiên, song thực ra là sản phẩm của những phép tính tất định.
histogram:
Một mảng các số nguyên trong đó từng số nguyên để đếm số các giá trị rơi vào một khoảng nhất định.

12.12  Bài tập

Bài tập 1   Hãy viết một phương thức có tên cloneArray để nhận vào tham số là một mảng các số nguyên, tạo ra một mảng mới cùng kích thước, sao chép các phần tử từ mảng đầu sang mảng mới tạo, rồi trả lại một tham chiếu đến mảng mới.
Bài tập 2   Viết một phương thức có tên randomDouble nhận vào hai số phẩy động, low và high, rồi trả lại một số phẩy động ngẫu nhiên, x, sao cho low ≤ x < high.
Bài tập 3   Viết một phương thức có tên randomInt nhận vào hai đối số, low và high, rồi trả lại một số nguyên ngẫu nhiên từ low đến high, nhưng không kể high.
Bài tập 4   Bao bọc mã lệnh trong Mục 12.10 vào một phương thức có tên makeHist để nhận một mảng các điểm số rồi trả lại một histogram các giá trị trong mảng.
Bài tập 5   Viết một phương thức có tên areFactors để nhận vào một số nguyên, n, và một mảng các số nguyên, và trả lại true nếu các số trong mảng đều là ước số của n (nghĩa là n chia hết cho tất cả những phần tử này). GỢI Ý: Xem bài tập 8.1.
Bài tập 6   Viết một phương thức nhận tham số gồm một mảng những số nguyên và một số nguyên tên là target, rồi trả lại chỉ số đầu tiên nơi mà target xuất hiện trong mảng, nếu có, hoặc -1 nếu không.
Bài tập 7   Có những lập trình viên phản đối quy tắc chung rằng các biến và phương thức phải được đặt tên có nghĩa. Thay vào đó, họ nghĩ rằng các biến và phương thức phải đặt tên là các loại hoa quả. Với từng phương thức sau đây, hãy viết một câu mô tả ý tưởng, nhiệm vụ của phương thức. Với mỗi biến, hãy xác định vai trò của nó.

  public static int banana(int[] a) { 
    int grape = 0; 
    int i = 0; 
    while (i < a.length) { 
      grape = grape + a[i]; 
      i++; 
    } 
    return grape; 
  } 
  public static int apple(int[] a, int p) { 
    int i = 0; 
    int pear = 0; 
    while (i < a.length) { 
      if (a[i] == p) 
        pear++; 
      i++; 
    } 
    return pear; 
  } 
  public static int grapefruit(int[] a, int p) { 
    for (int i = 0; i<a.length; i++) { 
      if (a[i] == p) 
        return i; 
      } 
    return -1; 
  }

Mục đích của bài tập này là thực hành đọc mã lệnh và nhận ra những dạng mẫu tính toán mà ta đã gặp.

Bài tập 8

  1. Kết quả của chương trình sau là gì?
  2. Hãy vẽ biểu đồ ngăn xếp để cho thấy trạng thái chương trình ngay trước khi mus trả về.
  3. Diễn đạt bằng lời một cách ngắn gọn nhiệm vụ của mus.
  public static int[] make(int n) { 
    int[] a = new int[n]; 
    for (int i = 0; i < n; i++) { 
      a[i] = i+1; 
    } 
    return a; 
  } 
  public static void dub(int[] jub) { 
    for (int i = 0; i < jub.length; i++) { 
      jub[i] *= 2; 
    } 
  } 
  public static int mus(int[] zoo) { 
    int fus = 0; 
    for (int i = 0; i < zoo.length; i++) { 
      fus = fus + zoo[i]; 
    } 
    return fus; 
  } 
  public static void main(String[] args) { 
    int[] bob = make(5); 
    dub(bob); 
    System.out.println(mus(bob)); 
  }
Bài tập 9   Nhiều dạng mẫu để duyệt mảng mà ta đã gặp cũng có thể được viết theo cách đệ quy. Đó không phải là cách thường dùng, nhưng là một bài tập hữu ích.

  1. Hãy viết một phương thức có tên maxInRange, nhận vào một mảng số nguyên mà một khoảng chỉ số (lowIndex và highIndex), rồi tìm giá trị lớn nhất trong mảng, nhưng chỉ xét những phần tử giữa lowIndex và highIndex, kể cả hai đầu này. Phương thức phải được viết theo cách đệ quy. Nếu chiều dài của khoảng bằng 1, nghĩa là nếu lowIndex == highIndex, thì ta biết ngay rằng phần tử duy nhất trong khoảng phải là giá trị lớn nhất. Do đó đây là trường hợp cơ sở. Nếu có nhiều phần tử trong khoảng, thì ta có thể chia mảng làm đôi, tìm cực đại trên mỗi phần, rồi sau đó lấy giá trị lớn hơn trong số hai cực đại tìm được.
  2. Các phương thức như maxInRange có thể gây lúng túng khi dùng. Để tìm phần tử lớn nhất trong mảng, ta phải cung cấp một khoảng bao gồm toàn bộ mảng đó.
    double max = maxInRange(array, 0, a.length-1);

    Hãy viết một phương thức có tên max nhận tham số là một mảng rồi dùng maxInRange để tìm và trả lại giá trị lớn nhất. Các phương thức như max đôi khi còn được gọi là phương thức gói bọc vì chúng cung cấp một lớp khái niệm xung quanh một phương thức lủng củng và giúp nó dễ dùng. Phương thức mà thực sự thực hiện tính toán được gọi là phương thức trợ giúp.

  3. Hãy viết một phiên bản find theo cách đệ quy và dùng đến dạng mẫu gói bọc-trợ giúp. find cần phải nhận một mảng các số nguyên và một số nguyên mục tiêu. Nó cần phải trả lại chỉ số của vị trí đầu tiên tại đó xuất hiện số nguyên mục tiêu, hoặc trả lại -1 nếu không xuất hiện.
Bài tập 10   Một cách không hiệu quả lắm để sắp xếp các phần tử trong mảng là tìm phần tử lớn nhất rồi đổi chỗ nó cho phần tử thứ nhất, sau đó tìm phần tử lớn thứ hai rồi đổi chỗ với phần tử thứ hai, và cứ như vậy. Cách này gọi là sắp xếp chọn (xem http://vi.wikipedia.org/wiki/Sắp_xếp_chọn).

  1. Hãy viết một phương thức mang tên indexOfMaxInRange nhận vào một mảng số nguyên, tìm phần tử lớn nhất trong khoảng cho trước, rồi trả lại chỉ số của nó. Bạn có thể sửa lại phiên bản maxInRange hay bạn có thể viết từ đầu một phiên bản tương tác với máy.
  2. Viết một phương thức có tên swapElement nhận một mảng số nguyên cùng hai chỉ số, rồi đổi chỗ hai phần tử tại các chỉ số đó.
  3. Viết một phương thức có tên selectionSort nhận vào một mảng các số nguyên và trong đó dùng indexOfMaxInRange cùng swapElement để xếp mảng từ nhỏ đến lớn.
Bài tập 11   Viết một phương thức có tên letterHist nhận một chuỗi làm tham số rồi trả lại histogram của các chữ cái trong chuỗi. Phần tử thứ không của histogram cần phải chứa số chữ a trong chuỗi (cả chữ in và thường); phần tử thứ 25 cần phải chứa số chữ z. Lời giải của bạn chỉ được duyệt chuỗi này đúng một lần.
Bài tập 12   Một từ được gọi là “doubloon” nếu trong từ đó, mỗi chữ cái xuất hiện đúng hai lần. Chẳng hạn, các từ sau đây là doubloon mà tôi đã tìm thấy trong cuốn từ điển.

Abba, Anna, appall, appearer, appeases, arraigning, beriberi, bilabial, boob, Caucasus, coco, Dada, deed, Emmett, Hannah, horseshoer, intestines, Isis, mama, Mimi, murmur, noon, Otto, papa, peep, reappear, redder, sees, Shanghaiings, Toto

Hãy viết một phương thức có tên isDoubloon để trả lại true nếu từ đã cho là một doubloon và false nếu không phải.

Bài tập 13   Hai từ là từ đảo (anagram) nếu như chúng có chứa cùng những chữ cái (đồng thời cùng số lượng từng chữ). Chẳng hạn, “stop” là từ đảo của “pots” còn “allen downey” là cụm từ đảo của “well annoyed.” Hãy viết một phương thức nhận vào hai String rồi trả lại true nếu như các String là từ đảo của nhau. Thêm phần thử thách: bạn chỉ được đọc các chữ cái của những String này đúng một lần.
Bài tập 14   Trong trò chơi Scrabble, mỗi người chơi có một tập hợp các miếng vuông với các chữ cái ghi trên đó, và mục tiêu của chò trơi là dùng những chữ cái đó ghép thành từ có nghĩa. Hệ thống tính điểm khá phức tạp, song thường thì các từ dài có giá trị cao hơn các từ ngắn. Giả dụ rằng bạn được cho trước các chữ cái dưới dạng một chuỗi, như "quijibo" và bạn nhận được một chuỗi khác để kiểm tra, như "jib". Hãy viết một phương thức có tên canSpell nhận vào hai chuỗi rồi trả lại true nếu tập hợp các miếng vuông xếp được thành từ có nghĩa. Bạn có thể có nhiều miếng ghi chữ giống nhau, nhưng chỉ được dùng mỗi miếng một lần. Thêm phần thử thách: bạn chỉ được đọc các chữ cái của những String này đúng một lần.
Bài tập 15   Thực ra trong Scrabble, còn những miếng vuông trắng có thể được dùng để biểu diễn chữ cái tùy ý. Hãy suy nghĩ một thuật toán cho canSpell xử lý được trường hợp chữ tùy ý như vậy. Đừng bận tâm đến những chi tiết thực hiện như bằng cách nào có thể biểu diễn những chữ tùy ý đó. Chỉ cần diễn đạt thuật toán bằng lời, bằng giả mã, hoặc bằng Java.

24 bình luận

Filed under Think Java

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 bình luận

Filed under Think Java