Suggested reading: sections 6.1-6.6

Basics of methods

We have already seen that the Java Math class contains some useful static methods used to compute mathematical functions such as Math.sin, Math.sqrt, etc. In chapter 6 we are going to learn how to construct our own static methods and when it is useful and appropriate to do so.

We start with a basic example from the text. The example begins with several loops that compute similar things, sums of integers in certain ranges:

public class RangeSums {
  public static void main(String[] args) {
    int sum = 0;
    for(int i = 1; i <= 10; i++)
      sum += i;
    System.out.println("Sum from 1 to 10 is "+sum);

    sum = 0;
    for(int i = 20; i <= 37; i++)
      sum += i;
    System.out.println("Sum from 20 to 37 is "+sum);

    sum = 0;
    for(int i = 35; i <= 49; i++)
      sum += i;
    System.out.println("Sum from 35 to 49 is "+sum);
  }
}

This code contains some obvious redundancies - each time we want to compute a sum for a particular range we have to construct a loop to do so, and all three loops in the example above are similar except for the details of the range involved.

Methods are an excellent way to remove this kind of redundancy. What we do is construct a method that does the computation, and we parameterize that method by the quantities that change each time we do the computation. In this case, the quantities that change each time we compute a sum are the starting and ending values for the range.

Here is the code above rewritten to make use of a method:

public class RangeSums {
  public static int sum(int i1,int i2) {
    int result = 0;
    for(int i = i1;i <= i2; i++)
      result += i;

    return result;
  }

  public static void main(String[] args) {
    System.out.println("Sum from 1 to 10 is "+sum(1,10));
    System.out.println("Sum from 20 to 37 is "+sum(20,37));
    System.out.println("Sum from 35 to 49 is "+sum(35,49));
  }
}

This class now contains two method definitions: the definition of a sum method we will call to help us compute range sums, and a definition for a main method.

Method definitions follow a set of structure rules.

Once you have written a method definition, you can call the method in your code following a syntax something like the following:

mySum = sum(1,100);

Note that when you call a method you have to provide as many parameters as the method definition set up in its parameter list. The sum method uses two parameters that indicate the range of integers we want to sum over, so we have to provide those two values when we call the method.

Note that the values that a method requires as its inputs can also come from variables:

int a = 1;
int b = 50;
int total = sum(a,b);

The variables you supply when you call a method are called actual parameters. The variables that appear in the parameter list in the method definition are called formal parameters. When you call a function, Java will automatically associate the actual parameters with the formal parameters by copying the values of each of the actual parameters into the formal parameters. In the example above, the value of a, 1, gets copied into the parameter i1 and the value of b, 50, gets copied into the parameter i2. Once the parameters have values, the code in the body of the method definition can go to work and do the desired computation.

Converting a program to use methods

In the lecture on files I showed this example. This is a program that computes and prints all of the prime numbers from 1 to 1000.

public static void main(String[] args) {
    PrintWriter pw = null;
    try {
        pw = new PrintWriter(new File("primes.txt"));
    } catch (Exception ex) {
        System.out.println("Can not open file for writing.");
        System.exit(0);
    }

    int n = 3;
    while (n < 1000) {
        int d = n - 1;
        while (d > 1) {
            if (n % d == 0) {
                break;
            }
            d--;
        }

        if (d == 1) {
            pw.println(n);
        }

        n = n + 2;
    }
    pw.close();
}

This program is moderately complex because it requires a nested loop structure. We need an outer loop to iterate over all of the integers from 1 to 1000, and an inner loop that checks whether or not a given integer is a prime. Nested loop structures are difficult for beginning programmers to understand, so this code could stand to be made simpler.

One of the best strategies you can use to simplify a nested loop is to write a method to do the work of the inner loop logic. By rewriting the code to call this method you can simplify the structure of the nested loop. Here is this example rewritten to follow this strategy.

public static boolean isPrime(int n) {
    int d = n - 1;
    while (d > 1) {
        if (n % d == 0) {
            break;
        }
        d--;
    }

    if (d == 1)
        return true;
    else
        return false;
}

public static void main(String[] args) {
    PrintWriter pw = null;
    try {
        pw = new PrintWriter(new File("primes.txt"));
    } catch (Exception ex) {
        System.out.println("Can not open file for writing.");
        System.exit(0);
    }

    for (int n = 3; n < 1000; n = n + 2) {
        if (isPrime(n)) {
            pw.println(n);
        }
    }
    pw.close();
}

This code makes use of a function called a boolean function. A boolean function is a function designed to answer a true/false question. Boolean functions are typically used in the tests of if statements or loops. A boolean function has a return type of boolean, which is the Java type used to store true/false values.

Here another example program that can benefit from the use of methods to simplify and clean up the code. I showed this example in the lecture on if-else statements. The program implements the rules for a moderately complex card game.

public class CardGame {

    public static void main(String[] args) {
        int dealerCard1, dealerCard2, dealerCard3;
        String dealerCardName1, dealerCardName2, dealerCardName3;
        int playerCard1, playerCard2, playerCard3;
        String playerCardName1, playerCardName2, playerCardName3;
        Scanner input = new Scanner(System.in);

        // Initialization
        int dealerPoints = 0;
        int playerPoints = 0;

        // First round - deal two cards to each player.
        dealerCard1 = (int) Math.floor(Math.random() * 13) + 1;
        if (dealerCard1 >= 10) {
            dealerPoints = dealerPoints + 10;
        } else {
            dealerPoints = dealerPoints + dealerCard1;
        }
        if (dealerCard1 == 1) {
            dealerCardName1 = "Ace";
        } else if (dealerCard1 == 11) {
            dealerCardName1 = "Jack";
        } else if (dealerCard1 == 12) {
            dealerCardName1 = "Queen";
        } else if (dealerCard1 == 13) {
            dealerCardName1 = "King";
        } else {
            dealerCardName1 = Integer.toString(dealerCard1);
        }

        dealerCard2 = (int) Math.floor(Math.random() * 13) + 1;
        if (dealerCard2 >= 10) {
            dealerPoints = dealerPoints + 10;
        } else {
            dealerPoints = dealerPoints + dealerCard2;
        }
        if (dealerCard2 == 1) {
            dealerCardName2 = "Ace";
        } else if (dealerCard2 == 11) {
            dealerCardName2 = "Jack";
        } else if (dealerCard2 == 12) {
            dealerCardName2 = "Queen";
        } else if (dealerCard2 == 13) {
            dealerCardName2 = "King";
        } else {
            dealerCardName2 = Integer.toString(dealerCard2);
        }

        playerCard1 = (int) Math.floor(Math.random() * 13) + 1;
        if (playerCard1 >= 10) {
            playerPoints = playerPoints + 10;
        } else {
            playerPoints = playerPoints + playerCard1;
        }
        if (playerCard1 == 1) {
            playerCardName1 = "Ace";
        } else if (playerCard1 == 11) {
            playerCardName1 = "Jack";
        } else if (playerCard1 == 12) {
            playerCardName1 = "Queen";
        } else if (playerCard1 == 13) {
            playerCardName1 = "King";
        } else {
            playerCardName1 = Integer.toString(playerCard1);
        }

        playerCard2 = (int) Math.floor(Math.random() * 13) + 1;
        if (playerCard2 >= 10) {
            playerPoints = playerPoints + 10;
        } else {
            playerPoints = playerPoints + playerCard2;
        }
        if (playerCard2 == 1) {
            playerCardName2 = "Ace";
        } else if (playerCard2 == 11) {
            playerCardName2 = "Jack";
        } else if (playerCard2 == 12) {
            playerCardName2 = "Queen";
        } else if (playerCard2 == 13) {
            playerCardName2 = "King";
        } else {
            playerCardName2 = Integer.toString(playerCard2);
        }

        // To remove clutter below, go ahead and generatate potential
        // second round cards.
        playerCard3 = (int) Math.floor(Math.random() * 13) + 1;
        if (playerCard3 == 1) {
            playerCardName3 = "Ace";
        } else if (playerCard3 == 11) {
            playerCardName3 = "Jack";
        } else if (playerCard3 == 12) {
            playerCardName3 = "Queen";
        } else if (playerCard3 == 13) {
            playerCardName3 = "King";
        } else {
            playerCardName3 = Integer.toString(playerCard3);
        }

        dealerCard3 = (int) Math.floor(Math.random() * 13) + 1;
        if (dealerCard3 == 1) {
            dealerCardName3 = "Ace";
        } else if (dealerCard3 == 11) {
            dealerCardName3 = "Jack";
        } else if (dealerCard3 == 12) {
            dealerCardName3 = "Queen";
        } else if (dealerCard3 == 13) {
            dealerCardName3 = "King";
        } else {
            dealerCardName3 = Integer.toString(dealerCard3);
        }

        // Print the results of round 1
        System.out.println("Dealer has " + dealerCardName1 + " and " + dealerCardName2 + " for a total of " + dealerPoints + " points.");
        System.out.println("You have " + playerCardName1 + " and " + playerCardName2 + " for a total of " + playerPoints + " points.");
        if (dealerPoints > 17 && playerPoints > 17) {
            System.out.println("Both players bust. Game ends in a tie.");
        } else if (dealerPoints > 17) {
            System.out.println("Dealer busts. You win.");
        } else if (playerPoints > 17) {
            System.out.println("You have gone bust. You lose.");
        } else {
            // Logic for round two
            System.out.println("Game advances to second round.");

            System.out.print("What do you want to do, hit or stay? ");
            String playerChoice = input.next();
            if (playerChoice.equalsIgnoreCase("hit")) {
                if (playerCard3 >= 10) {
                    playerPoints = playerPoints + 10;
                } else {
                    playerPoints = playerPoints + playerCard3;
                }

                System.out.println("Your next card is " + playerCardName3 + ".");
                System.out.println("You now have " + playerPoints + " points.");
            }

            if ((playerPoints <= 17) && (dealerPoints < 14 || dealerPoints < playerPoints)) {
                System.out.println("Dealer takes another card.");

                if (dealerCard3 >= 10) {
                    dealerPoints = dealerPoints + 10;
                } else {
                    dealerPoints = dealerPoints + dealerCard3;
                }

                System.out.println("Dealer's next card is " + dealerCardName3 + ".");
                System.out.println("Dealer now has " + dealerPoints + ".");
            } else if (playerPoints <= 17) {
                System.out.println("Dealer stays.");
            }

            // Print the results
            if (playerPoints > 17) {
                System.out.println("You have gone bust. You lose.");
            } else if (dealerPoints > 17) {
                System.out.println("Dealer busts. You win.");
            } else {
                System.out.println("Dealer has " + dealerPoints + " and you have " + playerPoints + ".");
                if (dealerPoints > playerPoints) {
                    System.out.println("Dealer wins on points.");
                } else if (dealerPoints == playerPoints) {
                    System.out.println("Game ties on points.");
                } else {
                    System.out.println("You win on points.");
                }
            }
        }
    }
}

A quick examination of this code should convince you that there is a fair degree of redundancy here. In particular, the logic needed to compute how many points a card is worth and the logic needed to determine the name of a card gets used over and over in the program. This suggests that we should construct a couple of methods whose job it is to do these computations:

public class CardGame {

    public static int cardPoints(int cardNumber) {
        int points = cardNumber;
        if(points > 10)
            points = 10;
        return points;
    }

    public static String cardName(int cardNumber) {
        String name;
         if(cardNumber == 1)
            name = "Ace";
        else if(cardNumber == 11)
            name = "Jack";
        else if(cardNumber == 12)
            name = "Queen";
        else if(cardNumber == 13)
            name = "King";
        else
            name = Integer.toString(cardNumber);
         return name;
    }

    public static void main(String[] args) {
        int dealerCard1, dealerCard2, dealerCard3;
        String dealerCardName1, dealerCardName2, dealerCardName3;
        int playerCard1, playerCard2, playerCard3;
        String playerCardName1, playerCardName2, playerCardName3;
        Scanner input = new Scanner(System.in);

        // Initialization
        int dealerPoints = 0;
        int playerPoints = 0;

        // First round - deal two cards to each player.
        dealerCard1 = (int) Math.floor(Math.random() * 13) + 1;
        dealerPoints = cardPoints(dealerCard1);
        dealerCardName1 = cardName(dealerCard1);

        dealerCard2 = (int) Math.floor(Math.random() * 13) + 1;
        dealerPoints = dealerPoints + cardPoints(dealerCard2);
        dealerCardName2 = cardName(dealerCard2);

        playerCard1 = (int) Math.floor(Math.random() * 13) + 1;
        playerPoints = cardPoints(playerCard1);
        playerCardName1 = cardName(playerCard1);

        playerCard2 = (int) Math.floor(Math.random() * 13) + 1;
        playerPoints = playerPoints + cardPoints(playerCard2);
        playerCardName2 = cardName(playerCard2);

        // To remove clutter below, go ahead and generatate potential
        // second round cards.
        playerCard3 = (int) Math.floor(Math.random() * 13) + 1;
        playerCardName3 = cardName(playerCard3);

        dealerCard3 = (int) Math.floor(Math.random() * 13) + 1;
        dealerCardName3 = cardName(dealerCard3);

        // Print the results of round 1
        System.out.println("Dealer has " + dealerCardName1 + " and " + dealerCardName2 + " for a total of " + dealerPoints + " points.");
        System.out.println("You have " + playerCardName1 + " and " + playerCardName2 + " for a total of " + playerPoints + " points.");
        if (dealerPoints > 17 && playerPoints > 17) {
            System.out.println("Both players bust. Game ends in a tie.");
        } else if (dealerPoints > 17) {
            System.out.println("Dealer busts. You win.");
        } else if (playerPoints > 17) {
            System.out.println("You have gone bust. You lose.");
        } else {
            // Logic for round two
            System.out.println("Game advances to second round.");

            System.out.print("What do you want to do, hit or stay? ");
            String playerChoice = input.next();
            if (playerChoice.equalsIgnoreCase("hit")) {
                playerPoints = playerPoints + cardPoints(playerCard3);

                System.out.println("Your next card is " + playerCardName3 + ".");
                System.out.println("You now have " + playerPoints + " points.");
            }

            if ((playerPoints <= 17) && (dealerPoints < 14 || dealerPoints < playerPoints)) {
                System.out.println("Dealer takes another card.");

                dealerPoints = dealerPoints + cardPoints(dealerCard3);

                System.out.println("Dealer's next card is " + dealerCardName3 + ".");
                System.out.println("Dealer now has " + dealerPoints + ".");
            } else if (playerPoints <= 17) {
                System.out.println("Dealer stays.");
            }

            // Print the results
            if (playerPoints > 17) {
                System.out.println("You have gone bust. You lose.");
            } else if (dealerPoints > 17) {
                System.out.println("Dealer busts. You win.");
            } else {
                System.out.println("Dealer has " + dealerPoints + " and you have " + playerPoints + ".");
                if (dealerPoints > playerPoints) {
                    System.out.println("Dealer wins on points.");
                } else if (dealerPoints == playerPoints) {
                    System.out.println("Game ties on points.");
                } else {
                    System.out.println("You win on points.");
                }
            }
        }
    }
}

This is still fairly lengthy, but is less verbose and redundant than the original. The original program had 177 lines of code - the new version has 115.

Extended example: computing the complexity of text

The following is an extended example of a program that demonstrates how we can use methods to help us structure the solution to a moderately complex problem. The problem in question is based on a programming problem that appears Big Java, Second Edition by Cay Horstmann.

The problem

The Flesch Readability Index is a simple calculation that can provide a measure of the complexity of some English text.

Np is the number of sentences in the text, Nw is the total number of words, and Ns is the total number of syllables in the text. For typical examples of english text, the index falls between 0 and 100. The index corresponds roughly to a grade level.

IndexGrade Level
91 - 1005th grade
81 - 906th grade
71 - 807th grade
66 - 708th grade
61 - 659th grade
51 - 60high school
31 - 50college
0 - 30college graduate
less than 0law school graduate

We are going to write a program that can take a sample of text stored in a file and count how many sentences, words, and syllables are in the text. We will then use the formula above to compute the readability index for the text.

Basic mechanics of reading words from a text file

We have already seen that you can use a Scanner to read input data from a file. The next example shows how to read one word at a time from a text file.

package testScanner;

import java.io.File;
import java.util.Scanner;

public class EchoWords {

  public static void main(String[] args) {
    Scanner input = null;
    try {
      input = new Scanner(new File("text.txt"));
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(0);
    }

    while (input.hasNext()) {
      String word = input.next();
      System.out.println(word);
    }

  }
}

Calling the next() method on a Scanner returns the next word in the text. The example above uses that technique to read the individual words from a text file and then echo them to the output window.

There is one glitch in using this method that we will have to address. The Scanner breaks the text in the file into words by looking for obvious separator characters, called delimiters. By default, the Scanner class counts only spaces, tabs, and new line characters as delimiters. In most cases this leads to the correct behavior, but in this case it produces an unwanted side effect. Some of the 'words' that this code ends up printing include attached punctuation characters such as commas, quotation marks, and dashes.

If we want to print all of the words in the text while also filtering out punctuation marks and numbers, we have to tell the Scanner to change the set of characters it uses as delimiters. To do this, we call the Scanner's useDelimiter method and pass it a list of characters we want to use as delimiters:

input.useDelimiter("[ \t\n\r0-9,.\"\\-]");

In addition to this, we also have to put in some extra logic that does not echo words of length 0. The Scanner may occasionally produce 0 length words when it encounters a sequence of delimiters.

Here now is a version of the program than can successfully break text into individual words while stripping out the punctuation and other unwanted characters.

package testScanner;

import java.io.File;
import java.util.Scanner;

public class EchoOnlyWords {

  public static void main(String[] args) {
    Scanner input = null;
    try {
      input = new Scanner(new File("text.txt"));
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(0);
    }

    input.useDelimiter("[ \t\n\r0-9,.\"\\-]");

    while (input.hasNext()) {
      String word = input.next();
      if (word.length() > 0) {
        System.out.println(word);
      }
    }

  }
}

A useful side effect of learning how to strip out unwanted characters is that we now also know how to solve the problem of counting the sentences in a sample of text. All we have to do is to change the delimiter set to only those characters that typically appear at the end of a sentence:

input.useDelimiter("[.!?]");

Doing this will cause the Scanner's next() method to return an entire sentence at a time instead of the usual one word at a time.

Counting syllables

The hardest part of this problem is counting syllables. Here is the outline of a strategy for counting syllables in a word

  1. Each group of adjacent vowels counts as one syllable. (Note that 'y' is considered a vowel in this context.)
  2. If a word has more than one syllable, a lone 'e' at the end of a word does not count as a distinct syllable. (For example, 'time' and 'sale' are both one syllable words.)
  3. An exception to rule 2 is the ending 'le'. When 'le' follows a consonant at the end of a word, it always counts as a syllable. (For example, the words 'able' and 'eagle' are both two syllable words.)

To implement this strategy, we are going to have to know how to access individual characters in a word. We will be storing individual words in Strings, which are a type of object used to store text. The Java String class includes a method named charAt() which can be used to extract individual characters from a String. charAt() uses an indexing system in which the first letter of the String is index 0. To determine the length of a String, we can use the String class's length() method. Thus, for example, to check whether the last letter in a word is an 'e' we can use this code:

int index = word.length()-1; // index of the last letter
if(word.charAt(index) == 'e')
  // Word ends in e
else
  // Word does not end in e.

Another useful method we will need is the toLowerCase method, which converts all of the letters in a String to lower case.

The structure of the solution

The next step is to sketch out an appropriate structure for the solution to our problem. One way to do this is to follow a top-down programming model. In this model we start out by writing the code for our application's main method. This will help us to identify the most important tasks to be performed. Rather than dive into the minutia of those tasks, we will delegate that work to methods we will write later.

Here is the code for the main method of our text analysis program.

public static void main(String[] args) throws Exception {
    int sentenceCount;
    int wordCount;
    int syllableCount;

    // Set up the scanner
    Scanner input = null;
    try {
      input = new Scanner(new File("text.txt"));
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(0);
    }
    input.useDelimiter("[.!?]");

    // Initialize the counters
    sentenceCount = 0;
    wordCount = 0;
    syllableCount = 0;

    // Scan the input file
    while (input.hasNext()) {
      String sentence = input.next();
      sentenceCount++;
      wordCount = wordCount + countWordsInSentence(sentence);
      syllableCount = syllableCount + countSyllablesInSentence(sentence);
    }

    // Compute the Flesch Readability Index
    double fleschIndex = 206.835
            - (84.6 * syllableCount) / wordCount
            - (1.015 * wordCount) / sentenceCount;

    // Print results
    System.out.println("This text has an index of " + fleschIndex);
    System.out.println("Syllables per word average " + (1.0 * syllableCount) / wordCount);
    System.out.println("Words per word sentence average " + (1.0 * wordCount) / sentenceCount);
  }

This code sets up the basic structure of the computation. It delegates the more difficult work of counting the words and syllables in a sentence to two other methods, countWordsInSentence and countSyllablesInSentence.

Writing the other methods

The next step is to write the code for the word and syllable counting methods.

public static int countWordsInSentence(String text) {
    int wordCount = 0;
    Scanner s = new Scanner(text);
    s.useDelimiter("[ \t\n\r0-9,.;\"\\-]");
    while (s.hasNext()) {
      String word = s.next();
      if (word.length() > 0) {
        wordCount++;
      }
    }
    return wordCount;
  }

 public static int countSyllablesInSentence(String text) {
    int syllableCount = 0;
    Scanner s = new Scanner(text);
    s.useDelimiter("[ \t\n\r0-9,.;\"\\-]");
    while (s.hasNext()) {
      String word = s.next();
      if (word.length() > 0) {
        word = word.toLowerCase();
        syllableCount = syllableCount + countSyllablesInWord(word);
      }
    }
    return syllableCount;
  }

Both of these methods use the same basic strategy. In both cases we open a Scanner that will scan across the contents of the sentence String we have been given. We equip that Scanner with a delimiter set that guarantees that next() will give us a word each time we call it, and then set up a loop that scans through the words in the sentence.

The countSyllablesInSentence method in turn delegates some of the work it needs to do to another method, countSyllablesInWord. Here is the code for that method.

public static int countSyllablesInWord(String word) {
    int count = 0;
    int index;

    // Count the vowel groups
    if (isVowel(word.charAt(0))) {
      count++;
    }
    for (index = 1; index < word.length(); index++) {
      // Start by filtering out a special case
      if(word.charAt(index) == 'a' && word.charAt(index-1) == 'i')
          count++;
      else if (isVowel(word.charAt(index)) && !isVowel(word.charAt(index - 1))) {
        count++;
      }
    }

    if (count > 1) {
      // Check for the terminal 'e' exception
      int lastIndex = word.length() - 1;
      if (word.charAt(lastIndex) == 'e') {
        // Check for the 'le' exception
        if (word.charAt(lastIndex - 1) != 'l' || isVowel(word.charAt(lastIndex - 2))) {
          count--;
        }
      }
    }
    return count;
  }

This method in turn needs some further assistance from a final method, isVowel, which examines a single character and determines whether or not it is a vowel.

public static boolean isVowel(char ch) {
    if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u' || ch == 'y') {
      return true;
    }
    return false;
  }

isVowel is an example of what is known as a boolean function. This type of function is designed to provide a true-or-false answer to a question for use in a test. The countSyllablesInWord method uses isVowel extensively to ask questions about the characters that make up the word we are working with.

Click the button below to download an archive containing the complete code for this example.

Project Folder