6.1 引言

  • 方法可以用于定义可重用的代码以及组织和简化代码

6.2 定义方法

  • 方法的定义由方法名称、参数、返回值类型以及方法体组成
  • 定义方法的语法如下所示:
    1. 修饰符 返回值类型 方法名(参数列表){
    2. //方法体;
    3. }
  • 方法头(method header)是指方法的修饰符(modifier)、返回值类型(return value)、方法名(method name)和方法的参数(parameter)。
  • 方法可以返回一个值。returnValueType是方法返回值的数据类型。有些方法只是完成某些要求的操作,而不返回值。在这种情况下,returnValueType为关键字void例如:在main方法中returnValueType就是void,在System.exit、System.out.println方法中返回值类型也是void。如果方法有返回值,则称为带返回值的方法(value-returning method),否则就称这个该方法为void方法(void method)
  • 定义在方法头的变量称为形式参数(formal parameter)或者简称为形参(parameter)。参数就像占位符。当调用方法时,就给参数传递一个值,这个值称为实际参数(actual parameter)或实参(argument)。参数列表(parameter list)指明方法中参数的类型、顺序和个数。方法名和参数列表一起构成方法签名(method signature)。参数是可选的,也就是说,方法可以不包含参数。例如:Math.random()方法就没有参数。
  • 方法体中包含一个实现该方法的语句集合。max方法的方法体使用一个if语句来判断哪个数较大,然后返回该数的值。为使带返回值的方法能返回一个结果,必须要使用带关键字return的返回语句。执行return语句时方法终止
    • 在其他某些语言中,方法称为过程(procedure)或函数(function)。这些语言中,带返回值的方法称为函数,返回值类型为void的方法称为过程
    • 在方法头中,需要对每一个参数进行单独的数据类型声明。例如:max(int num1, int num2)是正确的,而max(int num1,num2)是错误的。
    • 我们经常会说“定义方法”和“声明变量”,这里我们谈谈两者的细微差别。定义是指被定义的项是声明,而声明通常是指为被声明的项分配内存来存储数据。

6.3 调用方法

  • 方法的调用是指执行方法中的代码
  • 在方法定义中,定义方法要用于做什么,为了使用方法,必须调用(call或invoke)它。根据方法是否有返回值,调用方法有两种途径
  • 在Java中,带返回值的方法也可以当作语句调用。这种情况下,函数调用者只需忽略返回值即可。虽然很少这么做,但是如果调用者对返回值不感兴趣,这样也是允许的。
  • 当程序调用一个方法时,程序控制就转移到被调用的方法。当执行完return语句或执行到表示方法结束的右括号时,被调用的方法将程序控制返回给调用者

程序清单 6-1 TestMax.java

  1. public class TestMax {
  2. /**
  3. * Main method
  4. */
  5. public static void main(String[] args) {
  6. int i = 5;
  7. int j = 2;
  8. int k = max(i, j);
  9. System.out.println("The maximum of " + i
  10. + " and " + j + " is " + k);
  11. }
  12. /**
  13. * Return the max of two numbers
  14. */
  15. public static int max(int num1, int num2) {
  16. int result;
  17. if (num1 > num2) {
  18. result = num1;
  19. } else {
  20. result = num2;
  21. }
  22. return result;
  23. }
  24. }
  • 对带返回值的方法而言,return语句是必需的。下面图a中显示的方法在逻辑上是正确的,但它会有编译错误,因为Java编译器认为该方法有可能不会返回任何值
  • 为修正这个问题,删除图a中的if(n < 0),这样,编译器将发现不管if语句如何执行,总可以执行到return语句
  • 方法能够带来代码的共享和重用。除了可以在TestMax中调用max方法,还可以在其他类中调用它。如果创建了一个新类,可以通过使用”类名.方法名”(即TestMax.max)来调用max方法
  • 每当调用一个方法时,系统会创建一个活动记录(也称为活动框架),用于保存方法中的参数和变量。活动记录置于一个内存区域中,称为调用栈(call stack)。调用栈也称为执行栈、运行时栈,或者一个机器栈,常简称为”栈”。当一个方法调用另一个方法时,调用者的活动记录保持不变,一个新的活动记录被创建用于被调用的新方法。一个方法结束运行返回到调用这时,其相应的活动记录也被释放。
  • 调用栈以后进先出的方式来保存活动记录:最后调用的方法的活动记录最先从栈中移出。例如,假设方法m1调用了方法m2,而方法m2调用了方法m3.运行时将m1的活动记录压倒栈中,然后是m2的,再是m3的。当m3结束运行后,它的活动记录从栈中移出。当m2结束运行后,它的活动记录从栈中移出。当m1结束运行后,它的活动记录从栈中移出。
  • 理解调用栈有助于理解方法是如何调用的。程序清单6-1中main方法定义了变量i、j和k;max方法中定义了变量num1、num2和result。定义在方法签名中的变量num1和num2都是方法max的参数。它们的值通过方法调用进行传递。图6-3展示了堆栈中用于方法调用的活动记录。

6.4 void方法与返回值方法

  • void 方法不返回值

程序清单 6-2 TestVoidMethod.java

  1. public class TestVoidMethod {
  2. public static void main(String[] args) {
  3. System.out.print("The grade is ");
  4. printGrade(78.5);
  5. System.out.println("The grade is ");
  6. printGrade(59.5);
  7. }
  8. public static void printGrade(double score){
  9. if (score >= 90){
  10. System.out.println('A');
  11. }
  12. else if (score >= 80){
  13. System.out.println('B');
  14. }
  15. else if (score >= 70){
  16. System.out.println('C');
  17. }
  18. else if (score >= 60){
  19. System.out.println('D');
  20. }
  21. else {
  22. System.out.println('F');
  23. }
  24. }
  25. }

程序清单 6-3 TestReturnGradeMethod.java

  1. public class TestReturnGradeMethod {
  2. public static void main(String[] args) {
  3. System.out.print("The grade is " + getGrade(78.5));
  4. System.out.print("\nThe grade is " + getGrade(59.5));
  5. }
  6. public static char getGrade(double score) {
  7. if (score >= 90) {
  8. return 'A';
  9. } else if (score >= 80) {
  10. return 'B';
  11. } else if (score >= 70) {
  12. return 'C';
  13. } else if (score >= 60) {
  14. return 'D';
  15. } else {
  16. return 'F';
  17. }
  18. }
  19. }
  • return语句对于void方法不是必需的,但它能用于中止方法并返回到方法的调用者。它的语法是:
    1. return;
  • 这种用法很少,但是对于改变void方法中的正常流程控制是很有用的。例如:下列代码在分数是无效值时,用return语句结束方法
    1. public static void printGrade(double score){
    2. if (score < 0 || score > 100){
    3. System.out.println("Invalid score");
    4. return;
    5. }
    6. if (score >= 90.0){
    7. System.out.println('A');
    8. }
    9. else if (score >= 80.0){
    10. System.out.println('B');
    11. }
    12. else if (score >= 70.0){
    13. System.out.println('C');
    14. }
    15. else if (score >= 60.0){
    16. System.out.println('D');
    17. }
    18. else {
    19. System.out.println('F');
    20. }
    21. }

6.5 按值传参

  • 调用方法的时候是通过传值的方式将实参传给形参的
  • 方法的强大之处在于它处理参数的能力。可以使用方法println打印任意字符串,用max方法求任意两个int值的最大值。调用方法时,需要提供实参,它们必须与方法签名中所对应的形参次序相同。这称作参数顺序匹配(parameter order associatiion)。例如,下面的方法打印message信息n次:
  1. public static void nPrintln(String message, int n){
  2. for(int i = 0; i < n; i++)
  3. System.out.println(message);
  4. }
  • 实参必须与方法签名中定义的形参在次序和数量上匹配,在类型上兼容。类型兼容是指不需要经过显示的类型转换,实参的值就可以传递给形参,例如,将int型的实参值传递给double形参
  • 当调用带参数的方法时,实参的值传递给形参,这个过程称为按值传递(pass - by - value)。如果实参是变量而不是字面值,则将该变量的值传递给形参。无论形参在方法中是否改变,该变量都不受影响。如程序清单6-4所示,x(1)的值传给参数n,用以调用方法increment(第5行)。在该方法中n自增1(第10行),而x的值则不论方法做了什么都保持不变。

程序清单 6-4 Increment.java

  1. public class Increment {
  2. public static void main(String[] args) {
  3. int x = 1;
  4. System.out.println("Before the call, x is " + x);
  5. increment(x);
  6. System.out.println("After the call, x is " + x);
  7. }
  8. public static void increment(int n) {
  9. n++;
  10. System.out.println("n inside the method is " + n);
  11. }
  12. }

程序清单 6-5 TestPassByValue.java

  1. public class TestPassByValue {
  2. /**
  3. * Main method
  4. *
  5. * @param args
  6. */
  7. public static void main(String[] args) {
  8. //Declare and initialize variables
  9. int num1 = 1;
  10. int num2 = 2;
  11. System.out.println("Before invoking the swap method, num1 is " +
  12. num1 + " and num2 is " + num2);
  13. //Invoke the swap method to attempt to swap two variables
  14. swap(num1, num2);
  15. System.out.println("After invoking the swap method, num1 is " +
  16. num1 + " and num2 is " + num2);
  17. }
  18. /**
  19. * Swap two variables
  20. */
  21. public static void swap(int n1, int n2) {
  22. System.out.println("\tInside the swap method");
  23. System.out.println("\t\tBefore swapping, n1 is " + n1
  24. + " and n2 is " + n2);
  25. //Swap n1 with n2
  26. int temp = n1;
  27. n1 = n2;
  28. n2 = temp;
  29. System.out.println("\t\tAfter swapping, n1 is " + n1
  30. + " and n2 is " + n2);
  31. }
  32. }
  • 为了简便,Java程序员经常说将实参x传给形参y,实际含义是指将x的值传递给y

6.6 模块化代码

  • 模块化使得代码易于维护和调试,并且使得代码可以被重用
  • 使用反复噶可以减少冗余的代码,提高代码的复用性。方法也可以用来模块化代码,以提高程序的质量

程序清单 6-6 GreatestCommonDivisorMethod.java

  1. import java.util.Scanner;
  2. public class GreatestCommonDivisorMethod {
  3. /**
  4. * Main method
  5. * @param args
  6. */
  7. public static void main(String[] args) {
  8. //Create a Scanner
  9. Scanner input = new Scanner(System.in);
  10. //Prompt the user to enter two integers
  11. System.out.print("Enter first integer: ");
  12. int n1 = input.nextInt();
  13. System.out.print("Enter second integer: ");
  14. int n2 = input.nextInt();
  15. System.out.println("The Greatest common divisor for " + n1 +
  16. " and " + n2 + " is " + gcd(n1,n2));
  17. }
  18. /**
  19. * Return the gcd of two integers
  20. */
  21. public static int gcd(int n1,int n2){
  22. //Initial gcd is 1
  23. int gcd = 1;
  24. //Possible gcd
  25. int k = 2;
  26. while (k <= n1 && k<=n2){
  27. if (n1 % k == 0 && n2 % k == 0){
  28. //Update gcd
  29. gcd = k;
  30. }
  31. k++;
  32. }
  33. return gcd;
  34. }
  35. }

通过将求最大公约数的代码封装在一个方法中,这个程序就具备了以下几个优点:

  • 它将计算最大公约数的问题和main方法中的其他代码分隔开,这样做会使逻辑更加清晰而且程序的可读性更强
  • 将计算最大公约数的错误限定在gcd方法中,这样就缩小了调试的范围
  • 现在,其他程序可以重复使用gcd方法

程序清单 6-7 PrimeNumberMethod.java

  1. public class PrimeNumberMethod {
  2. public static void main(String[] args) {
  3. System.out.println("The first 50 prime numbers are :\n");
  4. printPrimeNumbers(50);
  5. }
  6. public static void printPrimeNumbers(int numberOfPrimes) {
  7. //Display 10 per line
  8. final int NUMBER_OF_PRIMES_PER_LINE = 10;
  9. //Count the number of prime numbers
  10. int count = 0;
  11. //A number to be tested for primeness
  12. int number = 2;
  13. //Repeatedly find prime numbers
  14. while (count < numberOfPrimes) {
  15. //Print the prime number and increase the count
  16. if (isPrime(number)) {
  17. //Increase the count
  18. count++;
  19. if (count % NUMBER_OF_PRIMES_PER_LINE == 0) {
  20. //Print the number and advance to the new line
  21. System.out.printf("%-5d\n", number);
  22. } else {
  23. System.out.printf("%-5d", number);
  24. }
  25. }
  26. //Check whether the next number is prime
  27. number++;
  28. }
  29. }
  30. public static boolean isPrime(int number) {
  31. for (int divisor = 2; divisor <= number / 2; divisor++) {
  32. if (number % divisor == 0) {
  33. //If true ,number is not prime
  34. //Number is not a prime
  35. return false;
  36. }
  37. }
  38. //Number is prime
  39. return true;
  40. }
  41. }

我们将一个大问题分成两个子问题:确定一个数字是否是素数以及打印素数。这样,新的程序会更易读,也更易于调试。而且,其他程序也可以复用方法printPrimeNumbers和isPrime

6.7 示例学习:将十六进制数转换为十进制数

  • 本节给出一个程序,将十六进制数转换位十进制数
  • 一种基于穷举的方法是将每个十六进制的字符转换为一个十进制数,即第 i 个位置的十六进制数乘以16i ,然后将所有这些项相加,就得到和该十六进制数等价的十进制数
  • 注意到:
    hn × 16n + hn-1 × 16n-1 + hn-2 × 16n-2 + …… + h1 × 161 + h0 × 160

= (……(( hn × 16n + hn-1 ) × 16 + hn-2) × 16 + …… + h1) × 16 + h0

  • 这个发现,称为霍纳算法,可以导出下面这个将十六进制字符串转换为十进制数的高效算法:
  1. int decimalValue = 0;
  2. for(int i = 0; i <hex.length(); i++){
  3. char hexChar = hex.charAt(i);
  4. decimalValue = decimalValue * 16 + hexCharToDecimal(hexChar);
  5. }
  • 下面是将算法应用于十六进制数AB8C时对程序的跟踪: | | i | 十六进制数 | 十六进制数转换为十进制数 | 十进制数 | | —- | —- | —- | —- | —- | | 循环开始之前 | | | | 0 | | 第一次迭代之后 | 0 | A | 10 | 10 | | 第二次迭代之后 | 1 | B | 11 | 10 16 +11 | | 第三次迭代之后 | 2 | 8 | 8 | (10 16 + 11) 16 + 8 | | 第四次迭代之后 | 2 | C | 12 | ((10 _ 16 + 11)_16 +8) 16 + 12 |

程序清单 6-8 Hex2Dec.java

  1. import java.util.Scanner;
  2. public class Hex2Dec {
  3. /**
  4. * Main method
  5. *
  6. * @param args
  7. */
  8. public static void main(String[] args) {
  9. //Create a Scanner
  10. Scanner input = new Scanner(System.in);
  11. //Prompt the user to enter a string
  12. System.out.print("Enter a hex number: ");
  13. String hex = input.nextLine();
  14. System.out.println("The decimal value for hex number " + hex +
  15. " is " + hexToDecimal(hex.toUpperCase()));
  16. }
  17. public static int hexToDecimal(String hex) {
  18. int decimalValue = 0;
  19. for (int i = 0; i < hex.length(); i++) {
  20. char hexChar = hex.charAt(i);
  21. decimalValue = decimalValue * 16 + hexCharToDecimal(hexChar);
  22. }
  23. return decimalValue;
  24. }
  25. public static int hexCharToDecimal(char ch) {
  26. if (ch >= 'A' && ch <= 'F') {
  27. return 10 + ch - 'A';
  28. } else {
  29. //ch is '0','1',...,or '9
  30. return ch - '0';
  31. }
  32. }
  33. }

6.8 重载方法

  • 重载方法使得你可以使用同样的名字来定义不同方法,只要它们的参数列表是不同的

程序清单 6-9 TestMethodOverloading.java

  1. public class TestMethodOverloading {
  2. /**
  3. * Main method
  4. *
  5. * @param args
  6. */
  7. public static void main(String[] args) {
  8. //Invoke the max method with int parameters
  9. System.out.println("The maximum of 3 and 4 is "
  10. + max(3, 4));
  11. //Invoke the max method with double parameters
  12. System.out.println("The maximum of 3.0 and 5.4 is "
  13. + max(3.0, 5.4));
  14. //Invoke the max method with three double parameters
  15. System.out.println("The maximum of 3.0 and 5.4 , and 10.14 is "
  16. + max(3.0, 5.4, 10.14));
  17. }
  18. /**
  19. * Return the max of two int values
  20. */
  21. public static int max(int num1, int num2) {
  22. if (num1 > num2) {
  23. return num1;
  24. } else {
  25. return num2;
  26. }
  27. }
  28. /**
  29. * Find the max of two double values
  30. */
  31. public static double max(double num1, double num2) {
  32. if (num1 > num2) {
  33. return num1;
  34. } else {
  35. return num2;
  36. }
  37. }
  38. /**
  39. * Return the max of three double values
  40. */
  41. public static double max(double num1, double num2, double num3) {
  42. return max(max(num1, num2), num3);
  43. }
  44. }
  • 重载方法可以使得程序更加清楚,以及更加具有可读性。执行同样功能但是具有不同参数类型的方法应该使用同样的名字
  • 被重载的方法必须具有不同的参数列表。不能基于不同的修饰符或返回值类型来重载方法。
  • 有时调用一个方法时,会有两个或更多可能的匹配,但是,编译器无法判断哪个是最精确的匹配。这称为歧义调用(ambiguous invocation)。歧义调用会产生一个编译错误。考虑如下代码:
  1. public class AmbiguousOverloading {
  2. public static void main(String[] args) {
  3. System.out.println(max(1,2));
  4. }
  5. public static double max(int num1, double num2){
  6. if (num1 > num2) {
  7. return num1;
  8. } else {
  9. return num2;
  10. }
  11. }
  12. public static double max(double num1, int num2){
  13. if (num1 > num2){
  14. return num1;
  15. }
  16. else {
  17. return num2;
  18. }
  19. }
  20. }

max(int ,double)和max(double, int)都有可能与max(1,2)匹配。由于两个方法谁也不比谁更精确,所以这个调用是有歧义的,它会导致一个编译错误。

6.9 变量的作用域

  • 变量的作用域(scope of a variable)是指变量可以在程序中被引用的范围。
  • 在方法中定义的变量称为局部变量(local variable)。
  • 局部变量的作用域是从变量声明的地方开始,直到包含该变量的块结束为止,局部变量都必须在使用之前进行声明和赋值
  • 参数实际上就是一个局部变量。一个方法的参数的作用域覆盖整个方法。在for循环头中初始操作部分声明的变量,其作用域只限于循环体内,从它的声明处开始,到包含该变量的块结束为止。
  • 可以在一个方法中的不同块里声明同名的局部变量,但是,不能在嵌套块中或同一块中两次声明同一个局部变量
  • 一种常见的错误是在for循环中声明一个变量,然后试图在循环外使用它。如下面代码所示,i在for循环中声明,但是在for循环外进行访问,这将导致语法错误。
  1. for(int i= 0; i < 10;i++){
  2. }
  3. //Causes a syntax error on i
  4. System.out.println(i);

6.10 示例学习: 生成随机字符

  • 字符使用整数来编码。产生一个随机字符就是产生一个随机整数
  • 每个字符都有一个唯一的Unicode,其值在十六进制数0到FFFF(即十进制的655 35)之间。生成一个随机字符就是使用下面的表达式,生成从0到65 535 之间的一个随机整数(注意:因为0 <= Math.random( ) < 1.0,必须给65 535 上加1):
  1. (int)(Math.random() * (65535 + 1))
  • 现在让我们来考虑如何生成一个随机小写字母。小写字母的Unicode是一串连续的整数,从小写字母’a’的Unicode开始,然后是’b’、’c’、…和’z’的Unicode。’a’的Unicode是:
    1. (int)'a'

    所以,(int)’a’到(int)’z’之间的随机整数是:
    正如4.3.3节中所讨论的,所有的数字操作符都可以应用到char操作数上。如果另一个操作数是数字或字符,那么char型操作数就会被转换成数字。这样,前面的表达式就可以简化为如下所示:
    这样,随机的小写字母是:
    由此,可以生成任意两个字符ch1和ch2之间的随机字符,其中ch1 < ch2,如下所示:
    这是一个简单但却很有用的发现。在程序清单6-10中创建一个名为RandomCharacter的类,它有五个重载的方法,随机获取某种特定类型的字符。可以在以后的项目中使用这些方法。

程序清单 6-10 RandomCharacter.java

  1. public class RandomCharacter {
  2. /**
  3. * Generate a random character between ch1 and ch2
  4. */
  5. public static char getRandomCharacter(char ch1, char ch2){
  6. return (char)(ch1 + Math.random() * (ch2 - ch1 + 1));
  7. }
  8. /**
  9. * Generate a random lowercase letter
  10. */
  11. public static char getRandomLowerCaseLetter(){
  12. return getRandomCharacter('a','z');
  13. }
  14. /**
  15. * Generate a random uppercase letter
  16. */
  17. public static char getRandomUpperCaseLetter(){
  18. return getRandomCharacter('A','Z');
  19. }
  20. /**
  21. * Generate a random digit character
  22. */
  23. public static char getRandomDigitCharacter(){
  24. return getRandomCharacter('0','9');
  25. }
  26. /**
  27. * Generate a random character
  28. */
  29. public static char getRandomCharacter(){
  30. return getRandomCharacter('\u0000','\uFFFF');
  31. }
  32. }

程序清单6-11 给出一个测试程序,显示175个随机的小写字母。

程序清单 6-11 TestRandomCharacter.java

  1. public class TestRandomCharacter {
  2. /**
  3. * Main method
  4. *
  5. * @param args
  6. */
  7. public static void main(String[] args) {
  8. final int NUMBER_OF_CHARS = 175;
  9. final int CHARS_PER_LINE = 25;
  10. //Print random characters between 'a' and 'z' , 25 chars per line
  11. for (int i = 0; i < NUMBER_OF_CHARS; i++) {
  12. char ch = RandomCharacter.getRandomLowerCaseLetter();
  13. if ((i + 1) % CHARS_PER_LINE == 0) {
  14. System.out.println(ch);
  15. } else {
  16. System.out.print(ch);
  17. }
  18. }
  19. }
  20. }
  • 第9行调用定义子啊RandomCharacter类中的方法getRandomLowerCaseLetter( )。注意,虽然方法getRandomLowerCaseLetter( )没有任何参数,但是在定义和调用这类方法时仍然需要使用括号。

6.11 方法抽象和逐步求精

  • 开发软件的关键在于应用抽象的概念
  • 方法抽象(method abstraction)是通过将方法的使用和它的实现分离来实现的。用户在不知道方法是如何实现的情况下,就可以使用方法。方法的实现细节封装在方法内,对使用该方法的用户来说是隐藏的。这就称为信息隐藏(information hiding)或封装(encapsulation)。如果决定改变方法的实现,只要不改变方法签名,用户的程序就不受影响。方法的实现对用户隐藏在"Black Box"中。
  • 前面已经使用过方法System.out.print来显示一个字符串,用max方法求最大数。也知道了怎样在程序中编写代码来调用这些方法。但是作为这些方法的使用者,你并不需要知道它们是怎样实现的。
  • 方法抽象的概念可以应用于程序的开发过程中。当编写一个大型程序时,可以使用"分治"(divid-and-conquer)策略,也称为逐步求精(stepwise-refinement),将大问题分解成子问题。子问题又分解成更小、更容易处理的问题

6.11.1 自顶向下的设计

  • 如何开始编写这样一个程序呢?你会立即开始编写代码吗?编程初学者常常想一开始就解决每一个细节。尽管细节对最终程序很重要,但在前期过多关注细节会阻碍解决问题的进程。为使解决问题的流程尽可能地流畅,本例先用方法抽象把细节与设计分离,到后面才实现这些细节
  • 对本例来说,先把问题拆分成两个子问题:读取用户输入和打印该月的日历。在这一阶段,应该考虑还能分解成声明子问题,而不是用什么方法来读取输入和打印整个日历。可以画一个结构图,这有助于看清楚问题的分解过程。(图6-8 结构图显示将打印日历printCalendar问题分解成两个子问题 —- 读取输入readInput 和打印日历printMonth,如图a;而将printMonth分解成两个更小的问题 —- 打印日历头printMonthTitle和打印日历头printMonthTitle和打印日历体printMonthBody,如图b)往后学会怎么在md文档中画图在来画图
  • 你可以使用Scanner来读取年和月份的输入。打印给定月份的日历问题可以分解成两个子问题:打印日历的标题和日历的主体,如图6-8b所示。月历的标题由三行组成:年月、虚线、每周七天的星期名称。需要通过表示月份的数字(例如:1)来确定该月的全程(例如:January)。这个步骤是由getMonthName来完成的(参见图6-9a)
  • 为了打印日历的主体,需要知道这个月的第一天是星期几(getStartDay),以及该月有多少天(getNumberOfDaysInMonth),如图6-9b所示。例如:2013年12月有31天,2013年12月1号是星期天。
  • 怎样才能知道一个月的第一天是星期几呢?有几种方法可以求得。这里,我们采用下面的方法。假设知道1800年1月1日是星期三(START_DAY_FOR_JAN_1_1800=3),然后计算1800年1月1日和日历月份的第一天之间相差的总天数(totalNumberOfDays)。因为每个星期有7天,所以日历上每月第一天的星期值就是(totalNumberOfDays + START_DAY_FOR_JAN_1_1800) % 7。这样getStartDay问题就可以进一步细化为getTotalNumberOfDays,如图6-10a所示。
  • 要计算总天数,需要知道该年是否是闰年以及每个月的天数。所以getTotalNumberOfDays可以进一步细化成两个子问题:isLeapYear 和 getNumberOfDaysInMonth,如图6-10b所示。完整的结构图如图6-11 所示。

6.11.2 自顶向下和自底向上的实现

  • 现在我们把注意力转移到实现上。通常,一个子问题对应于实现中的一个方法,即使某些子问题太简单,以至于都不需要方法来实现。需要决定哪些模块要用方法实现,而哪些模块要与其他方法结合完成。这种决策应该基于所做的选择是否使整个程序更易读。在本例中,子问题readInput只要在main方法中实现即可。
  • 可以采用“自顶向下”“自底向上”的方法。“自顶向下” 方法是自上而下,每次实现结构图中的一个方法。待实现的方法可以用存根方法(stub)代替,存根方法是方法的一个简单但不完整的版本。使用存根方法可以快速地构建程序的框架。首先实现main方法,然后使用printMonth方法的存根方法。例如,让printMonth的存根方法显示年份和月份,程序以下面的形式开始:
  1. import java.util.Scanner;
  2. public class PrintCalendar {
  3. /**
  4. * Main method
  5. */
  6. public static void main(String[] args) {
  7. Scanner input = new Scanner(System.in);
  8. //Prompt the user to enter year
  9. System.out.println("Enter full year (e.g.,2012): ");
  10. int year = input.nextInt();
  11. //Prompt the user to enter month
  12. System.out.println("Enter month as a number between 1 and 12: ");
  13. int month = input.nextInt();
  14. //Print calendar for the month of the year
  15. printMonth(year, month);
  16. }
  17. /**
  18. * A stub for printMonth may look like this
  19. */
  20. private static void printMonth(int year, int month) {
  21. System.out.println(month + " " + year);
  22. }
  23. /**
  24. * A stub for printMonthTitle may look like this
  25. */
  26. public static void printMonthTitle(int year, int month) {
  27. }
  28. /**
  29. * A stub for printMonthBody may look like this
  30. */
  31. public static void printMonthBody(int year, int month) {
  32. }
  33. /**
  34. * A stub for getMonthName may look like this
  35. */
  36. public static String getMonthName(int month) {
  37. //A dummy value
  38. return "January";
  39. }
  40. /**
  41. * A stub for getStartDay may look like this
  42. */
  43. public static int getStartDay(int year, int month) {
  44. //A dummy value
  45. return 1;
  46. }
  47. /**
  48. * A stub for getTotalNumberOfDays may look like this
  49. */
  50. public static int getTotalNumberOfDays(int year,int month) {
  51. //A dummy value
  52. return 10000;
  53. }
  54. /**
  55. * A stub for getNumberOfDaysInMonth may look like this
  56. */
  57. public static int getNumberOfDaysInMonth(int year,int month) {
  58. //A dummy value
  59. return 31;
  60. }
  61. /**
  62. * A stub for isLeapYear may look like this
  63. */
  64. public static Boolean isLeapYear(int year) {
  65. //A dummy value
  66. return true;
  67. }
  68. }
  • 编译和测试这个程序,然后修改所有的错误。现在,可以实现printMonth方法。对printMonth中调用的方法,可以继续使用存根方法。
  • 自底向上方法是从下向上每次实现结构图中的一个方法,对每个实现的方法都写一个测试程序(称为驱动器(driver))进行测试。自顶向下和自底向上都是不错的方法:它们都是渐近地实现方法,这有助于分离程序设计错误,使调试变得容易。这两种方法可以一起使用。

6.11.3 实现细节

  • 从3.11 节我们知道,方法isLeapYear(int year)可以使用下列代码实现:
  1. return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
  • 使用下面的事实实现getNumberOfDaysInMonth(int year, int month)方法:
    • 一三五七八十腊,三十一天永不差,四六九冬三十天,平年二月二十八
  • 要实现getTotalNumberOfDays(int year,int month)方法,需要计算1800年1月1日和该日历所属月份的第一天之间的总天数(totalNumberOfDays)。可以求出1800年到该日历所在年的总天数,然后求出在该年中日历所属月份之前的总天数。这两个总天数相加就是totalNumberOfDays。
  • 要打印日历体,首先在第一天之前填充一些空格,然后为每个星期打印一行。
  • 完整的程序见程序清单6-12

程序清单 6-12 PrintCalendar.java

  1. package Based_On_Article.The_Textbook_Source_Code.Chapter06;
  2. import java.util.Scanner;
  3. public class PrintCalendar {
  4. /**
  5. * Main method
  6. */
  7. public static void main(String[] args) {
  8. Scanner input = new Scanner(System.in);
  9. //Prompt the user to enter year
  10. System.out.println("Enter full year (e.g.,2012): ");
  11. int year = input.nextInt();
  12. //Prompt the user to enter month
  13. System.out.println("Enter month as a number between 1 and 12: ");
  14. int month = input.nextInt();
  15. //Print calendar for the month of the year
  16. printMonth(year, month);
  17. }
  18. /**
  19. * Print the calendar for a month in a year
  20. */
  21. public static void printMonth(int year, int month) {
  22. //Print the headings of the calendar
  23. printMonthTitle(year, month);
  24. //Print the body of the calendar
  25. printMonthBody(year, month);
  26. }
  27. /**
  28. * Print the month title,e.g.,March 2012
  29. */
  30. public static void printMonthTitle(int year, int month) {
  31. System.out.println(" " + getMonthName(month) +
  32. " " + year);
  33. System.out.println("--------------------------");
  34. System.out.println(" Sun Mon Tue Wed Thu Fri Sat ");
  35. }
  36. /**
  37. * Get the English name for the month
  38. */
  39. public static String getMonthName(int month) {
  40. String monthName = "";
  41. switch (month) {
  42. case 1: monthName = "January";break;
  43. case 2: monthName = "February";break;
  44. case 3: monthName = "March";break;
  45. case 4: monthName = "April";break;
  46. case 5: monthName = "May";break;
  47. case 6: monthName = "June";break;
  48. case 7: monthName = "July";break;
  49. case 8: monthName = "August";break;
  50. case 9: monthName = "September";break;
  51. case 10: monthName = "October";break;
  52. case 11: monthName = "November";break;
  53. case 12: monthName = "December";break;
  54. default:
  55. System.out.println("error");;
  56. }
  57. return monthName;
  58. }
  59. /**
  60. * Print month body
  61. */
  62. public static void printMonthBody(int year, int month) {
  63. //Get start day of the week for the first date in the month
  64. int startDay = getStartDay(year, month);
  65. //Get number of day in the month
  66. int numberOfDaysInMonth = getNumberOfDaysInMonth(year, month);
  67. //Pad space before the first day of the month
  68. int i = 0;
  69. for (i = 0; i < startDay; i++) {
  70. System.out.print(" ");
  71. }
  72. for (i = 1; i <= numberOfDaysInMonth; i++) {
  73. System.out.printf("%4d", i);
  74. if ((i + startDay) % 7 == 0) {
  75. System.out.println();
  76. }
  77. }
  78. System.out.println();
  79. }
  80. /**
  81. * Get the start day of month/1/year
  82. */
  83. public static int getStartDay(int year, int month) {
  84. final int START_DAY_FOR_JAN_1_1800 = 3;
  85. //Get total number of days from 1/1/1800 to month/1/year
  86. int totalNumberOfDays = getTotalNumberOfDays(year, month);
  87. //Return the start day for month/1/year
  88. return (totalNumberOfDays + START_DAY_FOR_JAN_1_1800) % 7;
  89. }
  90. /**
  91. * Get the total number of days since January 1, 1800
  92. */
  93. public static int getTotalNumberOfDays(int year, int month) {
  94. int total = 0;
  95. //Get the total days from 1800 to 1/1/year
  96. for (int i = 1800; i < year; i++) {
  97. if (isLeapYear(i)) {
  98. total = total + 366;
  99. } else {
  100. total = total + 365;
  101. }
  102. }
  103. //Add days from Jan to the month prior to the calendar month
  104. for (int i = 1; i < month; i++) {
  105. total = total + getNumberOfDaysInMonth(year, i);
  106. }
  107. return total;
  108. }
  109. /**
  110. * Get the number of days in a month
  111. */
  112. public static int getNumberOfDaysInMonth(int year, int month) {
  113. if (month == 1 || month == 3 || month == 5 || month == 7 ||
  114. month == 8 || month == 10 || month == 12) {
  115. return 31;
  116. }
  117. if (month == 4 || month == 6 || month == 9 || month == 11) {
  118. return 30;
  119. }
  120. if (month == 2) {
  121. return isLeapYear(year) ? 29 : 28;
  122. }
  123. //If month is incorrect
  124. return 0;
  125. }
  126. /**
  127. * Determine if it is a leap year
  128. */
  129. public static boolean isLeapYear(int year) {
  130. return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
  131. }
  132. }
  • 该程序没有检测用户输入的有效性。例如:如果用户输入的月份不在1到12之间,或者年份在1800年之前,那么程序就会显示出错误的日历。为避免出现这样的错误,可以添加一个if语句在打印日历前检测输入。
  • 该程序可以打印一个月的日历,还可以很容易地修改为打印整年的日历。尽管它现在只能处理1800年1月以后的月份,但是可以稍作修改,便能够打印1800年之前的月份。

6.11.4 逐步求精的优势

  • 逐步求精将一个大问题分解为小的易于处理的子问题。每个子问题可以使用一个方法来实现。这种方法使得问题更加易于编写、重用、调试、修改和维护
  • 优势:
    • 更简单的程序
      • 打印日历的程序比较长。逐步求精方法见将其分解为较小的方法,而不是在一个方法中写很长的语句序列。这样简化了程序,使得整个程序易于阅读和理解
    • 重用方法
      • 逐步求精提高了一个程序中的方法重用。isLeapYear方法只定义了一次,从getTotalNumberOfDays和getNumberOfDaysInMonth方法中都进行了调用。这减少了冗余的代码。
    • 易于开发、调试和测试
      • 因为每个子问题在一个方法中解决,而一个方法可以分别的开发、调试和测试。这隔离了错误,使得开发、调试和测试更加容易
      • 编写大型程序时,可以使用自顶向下或自底向上的方法。不要一次性地编写整个程序。使用这些方法似乎浪费了更多的开发时间(因为要反复编译和运行程序),但实际上,它会更节省时间并使调试更容易
    • 更方便团队合作
      • 当一个大问题分解为许多子问题,各个子问题可以分配给不同的编程人员。这更加易于编程人员进行团队工作