Chương 6: Phương thức trả lại giá trị

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

6.1  Những giá trị được trả lại

Một số phương thức mà ta đã dùng, như các hàm toán học, có trả lại kết quả. Nghĩa là, hiệu ứng từ việc kích hoạt phương thức là tạo ra một giá trị mới mà ta thường gán nó cho một biến hoặc dùng như một phần hợp nên một biểu thức lớn hơn. Chẳng hạn:

    double e = Math.exp(1.0); 
    double height = radius * Math.sin(angle);

Nhưng cho đến giờ tất cả những phương thức mà tự tay viết đều là phương thức rỗng; theo nghĩa những phương thức này không trả lại giá trị nào. Khi bạn kích hoạt một phương thức rống, nó thường chỉ tự được đặt trên một dòng mà không có lệnh gán nào cả:

    countdown(3); 
    nLines(3);

Trong chương này ta viết những phương thức trả lại thông tin, mà tôi gọi là phương thức trả lại giá trị. Ví dụ đầu tiên là area, một phương thức nhận vào tham số là một double, rồi trả lại diện tích của một hình tròn với bán kính cho trước:

  public static double area(double radius) { 
    double area = Math.PI * radius * radius; 
    return area; 
  }

Điều đầu tiên mà ta nhận thấy là đoạn đầu của định nghĩa phương thức đã khác đi. Thay vì public static void, vốn để chỉ một phương thức rỗng, ta thấy public static double, có nghĩa là giá trị trả về từ phương thức này là một double. Tôi vẫn chưa giải thích ý nghĩa của public static, song bạn hãy kiên nhẫn.

Dòng cuối là một dạng mới của câu lệnh return trong đó bao gồm một giá trị trả lại. Câu lệnh này có nghĩa là “từ phương thức này hãy lập tức trở về và dùng biểu thức kèm theo đây làm giá trị trả lại.” Biểu thức mà bạn đặt ra có thể phức tạp tùy ý, vì vậy ta có thể viết phương thức sau một cách gọn hơn:

  public static double area(double radius) { 
    return Math.PI * radius * radius; 
  }

Mặt khác, những biến tạm thời như area thường giúp cho việc gỡ lỗi được dễ dàng hơn. Trong cải hai trường hợp, kiểu của biểu thức trong lệnh return phải khớp với kiểu của phương thức. Nói cách khác, khi bạn khai báo rằng kiểu trả lại là double, bạn đã cam kết rằng phương thức này cuối cùng sẽ tạo ra một double. Nếu bạn thử return mà không kèm theo biểu thức nào, hoặc kèm theo biểu thức nhưng sai kiểu, thì trình biên dịch sẽ rầy la bạn.

Đôi khi cần phải có nhiều lệnh return, mỗi lệnh đặt ở một nhánh của lệnh điều kiện:

  public static double absoluteValue(double x) { 
    if (x < 0) { 
      return -x; 
    } else { 
      return x; 
    } 
  }

Vì những lệnh return này ở cấu trúc điều kiện lựa chọn, cho nên chỉ có một lệnh được thực thi. Dù rằng hoàn toàn hợp lệ nếu bạn có nhiều lệnh return trong cùng một phương thức, song bạn cần ghi nhớ rằng ngay khi một lệnh return được thực hiện, phương thức sẽ kết thúc mà không thực hiện bất cứ lệnh nào tiếp sau nó.

Mã lệnh xuất hiện sau dòng lệnh return, hay nói chung, trong bất cứ chỗ nào khác của chương trình mà không nằm trong luồng thực hiện thì được gọi là mã lệnh chết. Một số trình biên dịch sẽ cảnh báo nếu có đoạn lệnh chết trong mã lệnh bạn viết nên.

Nếu bạn đặt lệnh return trong cấu trúc điều kiện, thì phải đảm bảo được rằng mỗi luồng thực hiện khả dĩ đều dẫn tới một lệnh return. Chẳng hạn:

  public static double absoluteValue(double x) { 
    if (x < 0) { 
      return -x; 
    } else if (x > 0) { 
      return x; 
    } // SAI!! 
  }

Chương trình này không hợp lệ vì nếu x bằng 0, thì cả hai điều kiện không có điều kiện nào được thoả mãn, và hàm sẽ kết thúc mà không gặp phải lệnh return nào. Trình biên dịch thường sẽ đưa ra thông báo kiểu như “return statement required in absoluteValue”  (yêu cầu phải có lệnh return trong absoluteValue); lời thông báo này dễ gây nhầm lẫn vì trong đó bạn đã viết hai lệnh return rồi.

6.2  Phát triển chương trình

Lúc này bạn đã có thể nhìn vào toàn bộ phương thức Java rồi cho biết chúng có nhiệm vụ gì. Nhưng chưa chắc bạn đã biết cách viết nên chúng. Tôi sẽ đề xuất một phương pháp gọi là phát triển tăng dần

Ở ví dụ này, giả dụ bạn cần tìm khoảng cách giữa hai điểm cho bởi các toạ độ (x1, y1) và (x2, y2). Theo định nghĩa thông thường, khoảng cách (distance) sẽ là:

distance =
————————————
(x2 − x1)2 +(y2 − y1)2

Bước đầu tiên là cân nhắc xem một hàm distance trong Java sẽ trông như thế nào. Nói cách khác, các số liệu đầu vào (tham số) và kết quả (giá trị trả lại) là gì?

Trong trường hợp này, số liệu đầu vào mô tả hai điểm; ta có thể biểu thị chúng bằng bốn số double, dù rằng sau này ta sẽ thấy Java có kiểu đối tượng  Point mà ta có thể tận dụng.  Giá trị cần trả về là khoảng cách, tức là sẽ thuộc kiểu double.

Ta đã có thể phác thảo ngay ra hàm như sau:

  public static double distance (double x1, double y1, double x2, double y2) { 
    return 0.0; 
  }

Câu lệnh return 0.0; đóng vai trò giữ chỗ cần thiết cho việc biên dịch chương trình. Đương nhiên, vào lúc này nó chưa phát huy tác dụng, song vẫn đáng để ta thử biên dịch nhằm phát hiện ra lỗi cú pháp, nếu có, trước khi viết thêm mã lệnh.

Để kiểm tra phương thức mới viết này, ta phải kích hoạt nó bằng các giá trị mẫu. Đâu đó ở trong main, tôi sẽ phải viết lệnh:

    double dist = distance(1.0, 2.0, 4.0, 6.0);

Sở dĩ tôi chọn các tham số này vì khoảng cách ngang sẽ là 3 và khoảng cách dọc là 4, theo đó thì kết quả sẽ bằng 5 (cạnh huyền của một tam giác có các cạnh là 3-4-5). Khi thử nghiệm một hàm, bạn nên biết trước kết quả đúng.

Một khi đã kiểm tra xong cú pháp của lời định nghĩa hàm, ta có thể bắt tay vào thêm mã lệnh vào phần thân. Sau mỗi lần thay đổi tăng dần, ta biên dịch lại và chạy chương trình. Nếu có lỗi ở bất kì bước thay đổi nào, ta sẽ biết ngay rằng phải nhìn vào đâu: chính là vào dòng lệnh mà ta vừa mới bổ sung.

Một bước làm hợp lí tiếp theo là tính các hiệu số x2 − x1 và y2 − y1. Tôi lưu trữ các giá trị trên vào những biến tạm thời có tên dx và dy.

  public static double distance (double x1, double y1, double x2, double y2) { 
    double dx = x2 - x1; 
    double dy = y2 - y1; 
    System.out.println("dx is " + dx); 
    System.out.println("dy is " + dy); 
    return 0.0; 
  }

Tôi đã bổ sung hai lệnh in vào sau đó để ta kiểm tra được những giá trị trung gian trước khi tiếp tục. Những giá trị này phải bằng 3.0 và 4.0.

Một khi đã viết xong phương thức rồi thì ta cần phải bỏ những lệnh in này đi. Các câu lệnh như vậy còn có tên là dàn giáo vì nó có ích cho việc xây dựng chương trình nhưng lại không phải là một phần trong sản phẩm cuối cùng.

Tiếp theo chúng ta tính các bình phương của dx và dy. Ta đã có thể dùng phương thức Math.pow, nhưng đem nhân từng số với chính nó sẽ đơn giản hơn.

  public static double distance (double x1, double y1, double x2, double y2) { 
    double dx = x2 - x1; 
    double dy = y2 - y1; 
    double dsquared = dx*dx + dy*dy; 
    System.out.println("dsquared is " + dsquared); 
    return 0.0; 
  }

Một lần nữa, tôi biên dịch rồi chạy chương trình ở giai đoạn này và kiểm tra giá trị trung gian (vốn phải bằng 25.0).

Sau cùng, ta có thể dùng Math.sqrt để tính rồi trả lại kết quả.

  public static double distance (double x1, double y1, double x2, double y2) { 
    double dx = x2 - x1; 
    double dy = y2 - y1; 
    double dsquared = dx*dx + dy*dy; 
    double result = Math.sqrt(dsquared); 
    return result; 
  }

Từ main, ta có thể in và kiểm tra giá trị của kết quả.

Sau này khi đã có kinh nghiệm, bạn sẽ viết và gỡ lỗi nhiều dòng lệnh cùng lúc. Song dù sao đi nữa, việc phát triển tăng dần sẽ giúp bạn tiết kiệm nhiều thời gian. Các điểm cơ bản của quy trình này là:

  • Bắt đầu với một chương trình chạy được và thêm vào những thay đổi nhỏ. Bất cứ lúc nào khi gặp lỗi, bạn sẽ phát hiện được ngay lỗi đó ở đâu.
  • Dùng các biến tạm để lưu giữ các giá trị trung gian, từ đó bạn có thể hiển thị và kiểm tra chúng.
  • Một khi chương trình đã hoạt động, bạn có thể dỡ bỏ các đoạn mã “dàn giáo”, hoặc rút gọn nhiều câu lệnh về một biểu thức phức hợp, nếu việc này không làm cho chương trình trở nên khó đọc hơn.

6.3  Kết hợp phương thức

Một khi đã định nghĩa một phương thức mới, bạn có thể dùng nó như một phần của biểu thức lớn, và bạn cũng có thể thiết lập những phương thức mới từ các phương thức sẵn có. Chẳng hạn, nếu ai đó cho bạn hai điểm: một là tâm đường tròn và một điểm trên đường tròn đó, rồi yêu cầu bạn tính diện tích hình tròn thì bạn sẽ làm thế nào?

Giả sử như toạ độ của tâm điểm được lưu trong các biến xc và yc, toạ độ điểm trên đường tròn là xp và yp. Bước đầu tiên sẽ là tìm bán kính của đường tròn, vốn là khoảng cách giữa hai điểm đó. Thật may là ta đã có một phương thức, distance, để làm việc này:

    double radius = distance(xc, yc, xp, yp);

Bước tiếp theo là tìm diện tích của một đường tròn có bán kính đó, rồi trả lại kết quả.

    double area = area(radius); return area;

Kết hợp hai bước này vào trong cùng một phương thức, ta thu được:

  public static double circleArea (double xc, double yc, double xp, double yp) { 
    double radius = distance(xc, yc, xp, yp); 
    double area = area(radius); 
    return area; 
  }

Các biến tạm thời radius và area có ích cho việc phát triển và gỡ lỗi chương trình, nhưng một khi chương trình đã hoạt động tốt, ta có thể rút gọn nó lại bằng cách kết hợp các lệnh kích hoạt phương thức:

  public static double circleArea (double xc, double yc, double xp, double yp) { 
    return area(distance(xc, yc, xp, yp)); 
  }

6.4  Quá tải toán tử

Có thể bạn đã nhận thấy rằng cả circleArea lẫn area đều thực hiện những tính năng tương tự—tìm diện tích hình tròn—nhưng nhận các tham số khác nhau. Với area, chúng ta phải cung cấp bán kính; còn với circleArea ta cung cấp hai điểm.

Nếu hai phương thức cùng làm một việc, lẽ tự nhiên là ta đặt chung một tên cho cả hai. Việc có nhiều phương thức cùng tên, vốn được gọi là quá tải (overloading), là điều hợp lệ trong Java miễn sao các dạng phương thức phải nhận những tham số khác nhau. Như vậy ta có thể đổi tên circleArea:

  public static double area (double x1, double y1, double x2, double y2) { 
    return area(distance(xc, yc, xp, yp)); 
  }

Khi bạn kích hoạt một phương thức quá tải, Java sẽ biết được rằng bạn muốn dùng dạng phương thức nào, qua việc xem xét các đối số mà bạn cung cấp. Nếu bạn viết:

    double x = area(3.0);

thì Java sẽ đi tìm một phương thức mang tên area mà nhận đối số là một double; do đó nó sẽ dùng dạng thứ nhất, tức là hiểu đối số như một bán kính. Còn nếu bạn viết:

    double x = area(1.0, 2.0, 4.0, 6.0);

thì Java sẽ dùng dạng thứ hai của area. Và lưu ý rằng thực ra dạng area thứ hai đã kích hoạt dạng thứ nhất.

Nhiều phương thức Java được qua tải, nghĩa là có nhiều dạng trong đó chấp nhận số lượng hoặc kiểu tham số khác nhau. Chẳng hạn, có những dạng print và println chấp nhận một tham số thuộc kiểu bất kì. Trong lớp Math, có một dạng abs làm việc với double, đồng thời có một dạng dành cho int.

Mặc dù quá tải là một đặc điểm hữu ích, so bạn hãy cẩn thận khi dùng. Bạn có thể thật sự cảm thấy lú lẫn nếu cố gắng gỡ lỗi một dạng phương thức trong khi bạn không chủ ý kích hoạt nó, mà là một phương thức khác cùng tên!

Và điều này làm tôi nhớ đến một quy tắc then chốt trong gỡ lỗi: hãy đảm bảo chắc rằng phiên bản chương trình bạn cần gỡ lỗi chính là phiên bản chương trình bạn đang chạy!

Một ngày nào đó có thể bạn sẽ thấy mình đang loay hoay sửa đi sửa lại chương trình, và cứ thấy kết quả vẫn y nguyên như vậy khi chạy lại. Đây là một tín hiệu cảnh báo rằng hiện bạn không chạy phiên bản chương trình như đang nghĩ. Để kiểm tra lại, bạn hãy thử thêm một câu lệnh print (chẳng quan trọng là in thứ gì) và xem chương trình có biểu hiện tương ứng hay không.

6.5  Biểu thức logic

Hầu hết các toán tử mà ta đã gặp đều tạo ra kết quả có cùng kiểu với các toán hạng trong đó. Lấy ví dụ, toán tử + nhận hai số int rồi cũng tạo ra một số int, hoặc hai số double rồi tạo thành một double, v.v.

Những ngoại lệ mà ta gặp, đó là các toán tử quan hệ, vốn để so sánh các int hoặc float rồi trả lại true hoặc falsetrue và false là những giá trị đặc biệt trong Java; hai giá trị này hợp nên một kiểu gọi là boolean. Bạn có thể nhớ lại rằng khi tôi định nghĩa một kiểu, tôi có nói rằng đó là một tập các giá trị. Đối với các số intdouble hay chuỗi String, những tập hợp như vậy đều rất lớn. Song với boolean, tập hợp này chỉ chứa hai giá trị.

Các biểu thức boolean, hay biểu thức logic, cùng các biến cũng hoạt động giống như các biểu thức và biến thuộc kiểu khác:

    boolean flag; 
    flag = true; 
    boolean testResult = false;

Ví dụ thứ nhất là một lời khai báo biến đơn giản; ví dụ thứ hai là một lệnh gán, còn ví dụ thứ ba là một lệnh khởi tạo.

Các giá trị true và false là những từ khóa trong Java, vì vậy chúng có thể xuất hiện với màu chữ khác tùy theo môi trường phát triển tích hợp mà bạn đang dùng.

Kết quả của một toán tử điều kiện là một giá trị boolean, bởi vậy bạn có thể lưu trữ kết quả của phép so sánh vào một biến:

    boolean evenFlag = (n%2 == 0); // đúng nếu n chẵn 
    boolean positiveFlag = (x > 0); // đúng nếu x dương

rồi lại dùng nó làm bộ phận của một câu lệnh điều kiện:

    if (evenFlag) { 
      System.out.println("Khi tôi kiểm tra, n là số chẵn"); 
    }

Một biến được dùng theo cách này có thể gọi là một biến dấu hiệu vì nó đánh dấu cho sự có mặt hoặc vắng mặt của một điều kiện nào đó.

6.6  Toán tử logic

Có ba toán tử logic trong Java: AND, OR và NOT, vốn được kí hiệu bởi ba dấu &&|| và !. Ý nghĩa của các toán tử này giống như nghĩa các từ tương ứng trong tiếng Anh. Chẳng hạn, x > 0 && x < 10 chỉ đúng khi x lớn hơn 0  nhỏ hơn 10.

evenFlag || n%3 == 0 chỉ đúng khi một trong hai điều kiện là đúng; nghĩa là evenFlag đúng hoặc số n chia hết cho 3.

Sau cùng, toán tử not phủ định một biểu thức Boole. Do vậy !evenFlag là đúng nếu như evenFlag là sai—tức là nếu số đã cho là lẻ.

Toán tử logic có thể làm đơn giản những câu lệnh điều kiện lồng ghép. Chẳng hạn, bạn có thể viết lại mã lệnh dưới đây bằng một câu lệnh điều kiện đơn lẻ được không?

    if (x > 0) { 
      if (x < 10) { 
        System.out.println("x là số dương gồm 1 chữ số."); 
      } 
    }

6.7  Phương thức logic

Các phương thức có thể trả lại giá trị boolean cũng như các kiểu dữ liệu khác; và điều này thường thuận tiện cho việc đem những thao tác kiểm tra cất giấu vào trong phương thức. Chẳng hạn:

  public static boolean isSingleDigit(int x) { 
    if (x >= 0 && x < 10) { 
      return true; 
    } else { 
      return false; 
    } 
  }

Phương thức này có tên là isSingleDigit. Thường thì người ta hay đặt tên phương thức logic theo kiểu như những câu hỏi đúng/sai. Kiểu dữ liệu trả lại là boolean, như vậy mỗi câu lệnh return đều phải đưa ra một biểu thức boolean.

Bản thân đoạn mã lệnh rất rõ nghĩa, mặc dù nó dài hơn mức cần thiết. Hãy nhớ rằng biểu thức x >= 0 && x < 10 có kiểu boolean, bởi vậy không có gì sai khi ta trực tiếp trả lại nó đồng thời tránh được câu lệnh if:

  public static boolean isSingleDigit(int x) { 
    return (x >= 0 && x < 10); 
  }

Từ main bạn có thể kích hoạt phương thức này theo cách thông thường:

  boolean bigFlag = !isSingleDigit(17); 
  System.out.println(isSingleDigit(2));

Dòng đầu tiên đặt bigFlag là true chỉ khi 17 không phải số có một chữ số. Dòng lệnh thứ hai in ra true bởi 2 là chỉ có một chữ số.

Cách dùng hay gặp nhất đối với phương thức boole là trong các câu lệnh điều kiện

  if (isSingleDigit(x)) { 
    System.out.println("x nhỏ"); 
  } else { 
    System.out.println("x lớn"); 
  }

6.8  Nói thêm về đệ quy

Bây giờ khi đã biết phương thức trả lại giá trị, ta có được một ngôn ngữ lập trình Turing đầy đủ; theo nghĩa là chúng ta sẽ tính được mọi thứ có thể tính toán, trong đó “có thể tính toán” được định nghĩa theo cách bất kì, miễn là hợp lý. Ý tưởng này được Alonzo Church và Alan Turing phát triển, bởi vậy nó còn mang tên luận án Church-Turing. Bạn có thể đọc thêm thông tin ở http://en.wikipedia.org/wiki/Turing_thesis.

Để cụ thể hoá tác dụng của những kiến thức lập trình mà bạn vừa được học, chúng ta hãy cùng lập một số hàm toán học theo cách đệ quy. Một định nghĩa đệ quy giống như việc định nghĩa vòng quanh; điểm tương đồng là trong phần định nghĩa lại có tham chiếu đến sự vật được định nghĩa. Nhưng cách định nghĩa vòng quanh thực sự thì không mấy có tác dụng:

đệ quy:
một tính từ để chỉ một phương thức mang tính đệ quy.

Bạn hẳn sẽ bực mình khi thấy một định nghĩa kiểu như vậy trong cuốn từ điển. Ngược lại, khi bạn xem định nghĩa về hàm giai thừa trong toán học, có thể bạn sẽ thấy:

0! = 1
n! = n ·(n−1)!

(Giai thừa thường được kí hiệu bởi dấu !, xin đừng nhầm với toán tử logic! với ý nghĩa NOT.) Định nghĩa này phát biểu rằng giai thừa của 0 là 1, và giai thừa của bất kì một giá trị nào khác, n, thì bằng n nhân với giai thừa của n - 1. Theo đó, 3! bằng 3 nhân với 2!, vốn lại bằng 2 nhân với 1!, vốn bằng 1 nhân với 0!. Gộp tất cả lại, ta có 3! bằng 3 nhân 2 nhân 1 nhân 1, tức là bằng 6.

Nếu bạn có thể phát biểu một định nghĩa có tính đệ quy cho một hàm nào đó thì bạn cũng có thể viết một phương thức Java để tính nó. Bước đầu tiên là xác định các tham số và kiểu dữ liệu của giá trị trả lại. Vì giai thừa được định nghĩa cho các số nguyên, nên phương thức cần viết sẽ nhận tham số là số nguyên rồi trả lại cũng một số nguyên:

  public static int factorial(int n) { 
  }

Nếu đối số bằng 0, chúng ta chỉ cần trả lại giá trị 1:

  public static int factorial(int n) { 
    if (n == 0) { 
      return 1; 
    } 
  }

Đó là trường hợp cơ sở.

Nếu điều đó không xảy ra (đây chính là phần hay nhất), chúng ta thực hiện lời gọi đệ quy để tính giai thừa của n - 1 và sau đó nhân nó với n.

  public static int factorial(int n) { 
    if (n == 0) { 
      return 1; 
    } else { 
      int recurse = factorial(n-1); 
      int result = n * recurse; 
      return result; 
    } 
  }

Luồng thực hiện của chương trình này cũng giống với countdown trong Mục 4.8. Nếu ta kích hoạt factorial với giá trị 3:

Vì 3 khác 0 nên ta chọn nhánh thứ hai và tính giai thừa của n-1

Vì 2 khác 0 nên ta chọn nhánh thứ hai và tính giai thừa của n-1

Vì 1 khác 0 nên ta chọn nhánh thứ hai và tính giai thừa của n-1

Vì 0 bằng 0 nên ta chọn nhánh thứ nhất và trả lại giá trị 1 và không gọi đệ quy thêm lần nào nữa.

Giá trị được trả về, 1, được nhân với n, vốn bằng 1, và kết quả được trả lại.

Giá trị được trả về (1) được nhân với n, vốn bằng 2, và kết quả được trả lại.

Giá trị được trả về (2) được nhân với n, vốn bằng 3, và kết quả, 6 trở thành giá trị trả về của hàm ứng với lúc bắt đầu gọi đệ quy.

Sau đây là nội dung của biểu đồ ngăn xếp khi một loạt các phương thức được kích hoạt:

Các giá trị trả lại như ở đây được chuyển về ngăn xếp.

Lưu ý rằng ở khung cuối cùng, các biến địa phương recurse và result đều không tồn tại, vì  khi n=0, nhánh tạo ra chúng không được thực hiện.

6.9  Niềm tin

Việc dõi theo luồng thực hiện của chương trình là một cách đọc mã lệnh, nhưng bạn sẽ nhanh chóng lạc vào mê cung. Một cách làm khác mà tôi gọi là “niềm tin” như sau. Khi bạn dò đến chỗ kích hoạt phương thức, thay vì việc đi theo luồng thực hiện, hãy coi như là phương thức đó hoạt động tốt và trả lại kết quả đúng.

Thật ra, bạn đã từng có “niềm tin” này khi dùng các phương thức của Java. Mỗi lần kích hoạt Math.cos hay System.out.println, bạn không kiểm tra nội dung bên trong các phương thức này. Bạn chỉ việc giả sử rằng chúng hoạt động được.

Cũng với lý lẽ tương tự khi bạn kích hoạt các phương thức do mình viết nên. Chẳng hạn, trong Mục 6.7, chúng ta đã viết một hàm tên là isSingleDigit để xác định xem một số có nằm trong khoảng từ 0 đến 9 hay không. Một khi chúng ta tự thuyết phục rằng phương thức này đã viết đúng—bằng cách kiểm tra và thử mã lệnh—chúng ta có thể sử dụng phương thức mà không cần phải xem lại phần mã lệnh nữa.

Điều tương tự cũng đúng với các chương trình đệ quy. Khi bạn đến điểm kích hoạt đệ quy, thay vì đi theo luồng thực hiện, bạn cần coi rằng lời gọi đệ quy hoạt động tốt (tức là cho kết quả đúng) và sau đó tự hỏi mình “Giả dụ như ta đã tìm được giai thừa của n−1, liệu ta có tính được giai thừa của n không?” Trong trường hợp này, rõ ràng là ta sẽ tính được, bằng cách nhân với n.

Dĩ nhiên là sẽ có chút kì lạ trong việc ta giả sử rằng hàm hoạt động tốt khi chưa viết xong nó, nhưng chính vì vậy mà ta gọi đó là niềm tin!

6.10  Thêm một ví dụ

Ví dụ thông dụng thứ hai để minh họa cho một hàm toán toán học đệ quy là fibonacci, với cách định nghĩa hàm như sau:

fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n−1) + fibonacci(n−2);

Chuyển sang ngôn ngữ Java, ta viết được

  public static int fibonacci(int n) { 
    if (n == 0 || n == 1) { 
      return 1; 
    } else { 
      return fibonacci(n-1) + fibonacci(n-2); 
    } 
  }

Nếu bạn thử gắng theo luồng thực hiện ở đây, ngay cả với các giá trị nhỏ của n, bạn sẽ đau đầu ngay. Nhưng bằng niềm tin, nếu bạn coi rằng cả hai lời gọi đệ quy đều hoạt động tốt, thì rõ ràng bạn sẽ thu được kết quả đúng khi cộng chúng lại với nhau.

6.11  Thuật ngữ

kiểu trả lại:

Phần của lời khai báo phương thức, trong đó quy định kiểu của giá trị mà phương thức đó sẽ trả lại.
giá trị trả lại:
Giá trị được đưa ra làm kết quả của việc kích hoạt phương thức.
đoạn mã chết:
Phần chương trình không bao giờ được thực hiện, thường là do nó xuất hiện sau một câu lệnh return.
dàn giáo:
Mã lệnh được dùng trong giai đoạn phát triển chương trình nhưng bị bỏ đi ở phiên bản chương trình cuối.
rỗng (void):
Một kiểu trả lại đặc biệt có ở phương thức rỗng; nghĩa là phương thức không trả lại giá trị nào.
quá tải:
Việc có nhiều phương thức với cùng tên gọi nhưng có các tham số khác nhau. Khi bạn kích hoạt một phương thức quá tải, Java sẽ biết được phải dùng dạng nào của phương thức, căn cứ vào những đối số mà bạn cung cấp. (Tiếng Anh: “Overloading”)
boolean:
Một kiểu biến chỉ chứa hai giá trị true và false (đúng và sai).
dấu hiệu:
Một biến (thường với kiểu boolean) để ghi lại thông tin về một điều kiện hoặc trạng thái nào đó.
toán tử điều kiện:
Một toán tử dùng để so sánh hai giá trị rồi tạo ra một giá trị boolean để chỉ định quan hệ giữa hai toán hạng nêu trên.
toán tử logic:
Một toán tử nhằm kết hợp các giá trị boolean rồi trả lại cũng giá trị boolean.

6.12  Bài tập

Bài tập 1  Hãy viết một phương thức có tên isDivisible để nhận vào hai số nguyên, n và m rồi trả lại true nếu n chia hết cho  m và trả lại false trong trường hợp còn lại.
Bài tập 2  Nhiều phép tính có thể được diễn đạt ngắn gọn bằng phép “multadd” (nhân-cộng), trong đó lấy ba toán hạng rồi đi tính a*b + c. Thậm chí có bộ vi xử lý còn tích hợp cả phép tính này đối với những số phẩy động.

  1. Hãy lập một chương trình mới có tên gọi Multadd.java.
  2. Viết một phương thức gọi là multadd để lấy tham số là ba số double rồi trả lại kết quả của phép nhân-cộng giữa chúng.
  3. Viết một phương thức main để kiểm tra multadd bằng cách kích hoạt nó với một vài tham số đơn giản như 1.0, 2.0, 3.0.
  4. Cũng trong main, hãy dùng multadd để tính các giá trị sau:
    sin
    π
    4
     +
    cos
    π
     —
    4
     —————
         2
    log10 + log20
  5. Hãy viết một phương thức có tên yikes để nhận tham số là một double rồi dùng multadd để tính
    x e−x +
    ————
    1 − e−x

    Gợi ý: để nâng e lên một số mũ, hãy dùng phương thức có tên Math.exp.

Trong câu hỏi sau cùng, bạn có cơ hội viết một phương thức để kích hoạt một phương thức mà bạn đã viết trước đó. Mỗi khi làm như vậy, bạn nên cẩn thận kiểm thử phương thức đầu trước khi viết sang phương thức thứ hai. Nếu không, có thể bạn sẽ rơi vào trường hợp phải gỡ lỗi hai phương thức cùng lúc, một công việc rất khó khăn.

Một mục đích của bài này là nhằm luyện tập cách khớp mẫu: đó là khi được cho một bài toán cụ thể, ta cần nhận dạng nó trong số một tập hợp các thể loại bài toán.

Bài tập 3  Nếu có trong tay ba que gỗ, có thể bạn sẽ có hoặc không xếp được thành hình tam giác. Chẳng hạn, nếu một que dài 12 inch còn hai que kia, mỗi que chỉ dài 1 inch, thì bạn không thể kéo hai đầu que ngắn chạm nhau ở giữa được. Với ba đoạn thẳng có dài bất kì, có một cách kiểm tra đơn giản để xem liệu chúng có xếp thành hình tam giác được không:

“Nếu có bất kì chiều dài nào trong số đó lớn hơn tổng hai chiều dài còn lại, thì bạn không thể dựng thành hình tam giác. Trường hợp còn lại, thì có thể được.”

Hãy viết một phương thức với tên gọi isTriangle, nhận vào đối số là ba số nguyên, rồi trả lại true hoặc false, tùy theo khả năng xếp thành hình tam giác bằng những que có chiều dài đã cho.

Mục đích của bài tập này là nhằm áp dụng những lệnh điều kiện để viết nên một phương thức trả lại giá trị.

Bài tập 4   Kết quả của chương trình dưới đây là gì? Mục đích của bài tập này nhằm đảm bảo rằng bạn hiểu rõ các toán tử logic và luồng thực thi thông qua các phương thức trả giá trị.

  public static void main(String[] args) { 
    boolean flag1 = isHoopy(202); 
    boolean flag2 = isFrabjuous(202); 
    System.out.println(flag1); 
    System.out.println(flag2); 
    if (flag1 && flag2) { 
      System.out.println("ping!"); 
    } 
    if (flag1 || flag2) { 
      System.out.println("pong!"); 
    } 
  } 
  public static boolean isHoopy(int x) { 
    boolean hoopyFlag; 
    if (x%2 == 0) { 
      hoopyFlag = true; 
    } else { 
      hoopyFlag = false; 
    } 
    return hoopyFlag; 
  } 
  public static boolean isFrabjuous(int x) { 
    boolean frabjuousFlag; 
    if (x > 0) { 
      frabjuousFlag = true; 
    } else { 
      frabjuousFlag = false; 
    } 
    return frabjuousFlag; 
  }
Bài tập 5   Khoảng cách giữa hai điểm (x1y1) và (x2y2) thì bằng

Distance =
 ————————————
(x2 − x1)2 +(y2 − y1)2

Hãy viết một phương thức có tên distance để nhận các tham số gồm bốn số phẩy động—x1y1x2 và y2—rồi in ra khoảng cách giữa hai điểm này. Bạn cần giả sử rằng đã có một phương thức sumSquares để tính và trả lại tổng các bình phương của đối số. Chẳng hạn dòng lệnh:

    double x = sumSquares(3.0, 4.0);

sẽ gán giá trị 25.0 cho x.

Mục đích của bài tập này là nhằm viết một phương thức mới có áp dụng phương thức sẵn có. Bạn chỉ cần viết một phương thứcdistance. Bạn không được viết sumSquares hay main và cũng không kích hoạt distance.

Bài tập 6   Mục đích của bài tập này là dùng biểu đồ ngăn xếp để hiểu được trình tự thực hiện một chương trình đệ quy.

public class Prod { 

  public static void main(String[] args) { 
    System.out.println(prod(1, 4)); 
  } 

  public static int prod(int m, int n) { 
    if (m == n) { 
      return n; 
    } else { 
      int recurse = prod(m, n-1); 
      int result = n * recurse; 
      return result; 
    } 
  } 

}
  1. Hãy vẽ một biểu đồ ngăn xếp cho thấy trạng thái của chương trình ngay trước khi thực thể cuối cùng của prod hoàn tất thực thi. Kết quả của chương trình này là gì?
  2. Giải thích ngắn gọn xem prod làm việc gì.
  3. Viết lại prod mà không dùng đến các biến tạm recurse và result.
Bài tập 7   Mục đích của bài tập này là chuyển từ một lời định nghĩa đệ quy sang một phương thức Java. Hàm Ackerman được định nghĩa cho số nguyên không âm như sau:

     
A(mn) = 



              n+1 nếu m = 0 
        A(m1, 1) nếu  m > 0  và  n = 0
A(m−1, A(mn−1)) nếu  m > 0  và  n > 0.
    (1)

Hãy viết một phương thức tên là ack để nhận tham số là hai số int rồi tính và trả lại giá trị của hàm Ackerman. Hãy kiểm tra phương thức vừa viết bằng cách kích hoạt nó từ main rồi in ra giá trị vừa trả lại.

CẢNH BÁO: giá trị được trả lại sẽ rất nhanh chóng tăng cao. Bạn chỉ nên thử chạy với các giá trị m và n nhỏ (không lớn quá 2).

Bài tập 8

  1. Hãy tạo nên một chương trình có tên Recurse.java rồi gõ vào các phương thức sau:
      // first: trả lại kí tự đầu tiên của String cho trước 
      public static char first(String s) { 
        return s.charAt(0); 
      } 
    
      // last: trả lại một String mới có chứa toàn bộ 
      // chỉ trừ kí tự đầu của String cho trước 
      public static String rest(String s) { 
        return s.substring(1, s.length()); 
      } 
    
      // length: trả lại chiều dài của String cho trước 
      public static int length(String s) { 
        return s.length(); 
      }
  2. Hãy viết vài câu lệnh trong main để kiểm tra từng phương thức trên. Đảm bảo chắc là chúng hoạt động được, và chắc chắn là bạn đã hiểu công dụng của chúng là gì.
  3. Viết một phương thức có tên printString để nhận tham số là một String đồng thời in các chữ cái trong String đó, mỗi chữ cái trên một dòng. Phương thức này phải là kiểu rỗng.
  4. Viết một phương thức có tên printBackward có công dụng gần giống printString chỉ khác là in String theo chiều ngược lại (mỗi kí tự trên một dòng riêng).
  5. Viết một phương thức có tên reverseString để nhận tham số là một String rồi trả lại giá trị là một String mới. String mới này phải có đầy đủ các chữ cái như String đã nhập làm tham số; nhưng lại xếp theo thứ tự ngược lại. Chẳng hạn, kết quả của đoạn mã lệnh sau
      String backwards = reverseString("Allen Downey"); 
      System.out.println(backwards);

    sẽ phải là

    yenwoD nellA
Bài tập 9   Hãy viết một phương thức đệ quy có tên power để nhận vào x và một số nguyên n rồi trả lại xn. Gợi ý: một định nghĩa đệ quy đối với phép tính này là xn = x · x−1. Đồng thời, cần nhớ rằng mọi số nâng lên lũy thừa bậc 0 đều bằng 1. Câu hỏi khó tự chọn: bạn có thể làm cho phương thức này hiệu quả hơn, trong trường hợp n chẵn, bằng cách dùng công thức xn = (xn/2 )2.
Bài tập 10   (Bài tập này được dựa trên trang 44 cuốn sách Structure and Interpretation of Computer Programs của Abelson và Sussman.) Kĩ thuật sau đây có tên gọi Thuật toán Euclid vì nó xuất hiện trong tập Cơ bản của Euclid (Cuốn số 7, khoảng năm 300 TCN). Có lẽ đây là thuật toán đáng kể từ lâu đời nhất1. Quy trình tính toán được dựa theo quan sát thấy, nếu r là phần dư trong phép chia a cho b, thì các ước số chung của a và b cũng bằng ước số chung của b và r. Do vậy ta có thể dùng phương trình

gcd(a, b) = gcd(b, r)

để liên tiếp rút gọn bài toán tính ước số chung (GCD) về bài toán tính GCD của các cặp số nguyên ngày càng nhỏ hơn. Chẳng hạn,

gcd(36, 20) = gcd(20, 16) = gcd(16, 4) = gcd(4, 0) = 4

ngụ ý rằng GCD của 36 và 20 thì bằng 4. Có thể thấy rằng với bất kì hai số ban đầu nào, cách liên tiếp rút gọn này cuối cùng sẽ cho ta một cặp số mà số thứ hai bằng 0. Khi đó GCD sẽ bằng số còn lại trong cặp.

Hãy viết một phương thức có tên gcd để nhận tham số là hai số nguyên rồi dùng Thuật toán Euclid để tính và trả lại ước số chung lớn nhất của hai số.

19 bình luận

Filed under Think Java

19 responses to “Chương 6: Phương thức trả lại giá trị

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

  2. Cảm ơn anh đã dịch cuốn sách này, sách này em đọc nguyên bản tiếng Anh thấy rất hay, đang đọc tới chương 5. Em nghĩ phần Overload nên để là Overload hay Quá tải (Overload) để người đọc phân biệt được, dịch Quá tải nghe không hay. Một số thuật ngữ nên giữ nguyên tiếng Anh hoặc nên để tiếng Việt và tiếng Anh trong ngoặc()

    • Cám ơn bạn. Quả thực trong tài liệu khoa học máy tính rất nhiều trường hợp cũng nên viết từ gốc tiếng Anh bên cạnh. Mình sẽ viết mở ngoặc ghi chú bên cạnh từ in đậm trong lời định nghĩa từ này. Nói thật, lẽ ra phải gọi đầy đủ là “quá tải toán tử” nhưng khổ nỗi từ này đọc lên nghe trúc trắc đến là tệ. Nên chăng cộng đồng tin học mình không chấp nhận một cách gọi tên khác dễ nghe hơn, như “chồng chất toán tử”. Việc gọi là “quá tải” chứ không phải “chồng chất” có lẽ với hàm ý là đã toán tử này đã định nghĩa rồi; nay ta thực hiện mở rộng định nghĩa toán tử mà vẫn giữ tên gọi cũ. Dù sao, “quá tải” cũng là cách gọi sát nghĩa theo từ gốc tiếng Anh.

  3. chương trình bạn nên viết cả đoạn code bao gồm cả pthuc main và tên class cho dễ hiểu chứ cái dòng này: double area = area(radius); return area; mình không biết nó ở main hay phải tạo 1 phương thức khác chứa nó

  4. if (flag1 && flag2) {
    System.out.println(“ping!”);
    }
    if (flag1 || flag2) {
    System.out.println(“pong!”);

    a cho e hỏi toán tử && và || trong đoạn code này là tnao với

    • && và || là phép and và or. Lệnh thứ nhất được thực hiện nếu cả flag1 và flag2 đều true. Lệnh thứ hai được thực hiện nếu ít nhất 1 trong hai là true. && khác với & ở chỗ cái đầu cho phép “đoản mạch” tức là chỉ cần flag1 là false thì không thực hiện câu lệnh luôn, còn & phải lượng giá cả flag1 và flag2. Tương tự || là dạng đoản mạch, chỉ cần flag1 là true thì thực hiện luôn.

  5. lugd

    “Mục đích của bài tập này là nhằm viết một phương thức mới có áp dụng phương thức sẵn có. Bạn chỉ cần viết một phương thức: distance. Bạn không được viết sumSquares hay main và cũng không kích hoạt distance.” Sao áp dụng phương thức sẵn có mà chỉ viết 1 phương thức là sao nhỉ

  6. Bài tập 8.3 đến 8.5 không có vòng lặp giải kiểu gì trời 😦 Ai cho em xin phương thức bài 8.3 với ạ, xin cảm ơn.

  7. Ẩn danh

    cảm ơn bạn rất nhiều

  8. Ẩn danh

    Cuốn sách mà anh dịch có tên là gì nhỉ.

  9. Trimen

    public static char first(String s) {
    return s.charAt(0);
    }

    // tra ve chuoi moi khi bo di ki tu dau tien
    public static String rest(String s) {
    return s.substring(1, s.length());
    }

    // tra ve do dai chuoi
    public static int length(String s) {
    return s.length();
    }

    // tra ve ki tu cuoi cua chuoi
    public static char last(String s) {
    return s.charAt(length(s) – 1);
    }

    // tra ve chuoi moi khi bo di ki tu cuoi
    public static String deletelast(String s) {
    return s.substring(0, length(s) – 1);
    }

    // in tung chu cai trong chuoi
    public static void printString(String s) {
    if (length(s) == 0) {
    // System.exit(0) ;
    return;
    }
    char c = first(s);
    if (c == ‘ ‘) {
    s = rest(s);
    printString(s);
    } else {
    System.out.println(first(s));
    s = rest(s);
    printString(s);
    }
    }

    // in tung chu cai trong chuoi theo chieu nguoc lai
    public static void printBackward(String s) {
    if (length(s) == 0) {
    return;
    } else {
    char c = last(s);
    if (c == ‘ ‘) {
    s = deletelast(s);
    printBackward(s);
    } else {
    System.out.println(c);
    s = deletelast(s);
    printBackward(s);
    }
    }
    }

    //tra ve chuoi nguoc lai
    public static void reverseString(String s){
    if(length(s)==0){
    return;
    }
    else{
    char c=last(s);
    System.out.print(c);
    s=deletelast(s);
    reverseString(s);
    }
    }
    Bài 8 em làm như vậy, em có thêm 1 vài phương thức mà trong đề không yêu cầu như phương thức trả về ký tự cuối hay phương thức trả về chuỗi sau khi xóa ký tự cuối vậy có được không vậy ạ! Em cảm ơn!

  10. Nguyen

    bạn có thể cho mình bản pdf được không? Cảm ơn rất nhiều

Bình luận về bài viết này