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.
public
is an access specifier, which can be either public
or private
. In all of the early examples we will see, this will be public
. When we start using private
as an access specifier I will describe what the distinction means.static
marks this as a static method. Methods can be either static or non-static. When we start to write non-static methods I will explain the distinction.int
sum
.return
followed by an expression. When you execute the return statement, the expression after the return
gets evaluated and its value becomes the return value of the method.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.
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.
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 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.
Index | Grade Level |
---|---|
91 - 100 | 5th grade |
81 - 90 | 6th grade |
71 - 80 | 7th grade |
66 - 70 | 8th grade |
61 - 65 | 9th grade |
51 - 60 | high school |
31 - 50 | college |
0 - 30 | college graduate |
less than 0 | law 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.
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.
The hardest part of this problem is counting syllables. Here is the outline of a strategy for counting syllables in a word
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 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
.
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.