8. Methods
Polonius (Aside): Though this be madness, yet there is method in ’t.
8.1. Problem: Three card poker
Gambling has held a fascination for humankind since its invention. As long as there have been mathematicians, they have studied the underlying mechanisms of probability and statistics that drive games of chance. A classic game of both statistics and strategy is poker. The problem we want to solve is programming one of the many variations of poker, called three card poker. Instead of bluffing, a player competes only with the house according to fixed rules without any room for psychology. A player is dealt three cards. If the player’s hand contains a pair or better, he or she wins a payoff greater than or equal to the money bet. If the player does not have a pair or better, he or she loses the money bet. Below is a table giving one possible set of payoffs for each possible hand.
Hand | Payoff | Hand | Payoff | |
---|---|---|---|---|
Straight Flush |
40 |
Flush |
2 |
|
Three of a Kind |
30 |
Pair |
1 |
|
Straight |
6 |
Nothing |
0 |
If you’re unfamiliar with poker rules or card games in general, here’s a quick explanation of the various hands. A traditional English or American deck of cards is made up of 52 cards, organized into four suits: spades, hearts, diamonds, and clubs. In each suit, there are 13 ranks: two, three, four, five, six, seven, eight, nine, ten, jack, queen, king, and ace.
In three card poker, a straight is when the three cards can be arranged so that their ranks (ignoring suit) are in order. For example, a hand consisting of a Four of Diamonds, a Five of Spades, and a Six of Clubs would constitute a straight. An ace can serve as either the highest rank card (coming after a king) or the lowest rank card (coming before a two) and can help form a straight in either role (but not both at the same time). A flush is when all three cards have the same suit. For example, a Three of Hearts, a Seven of Hearts, and a Jack of Hearts would constitute a flush in three card poker. A straight flush, the highest hand, is made up of cards that form both a straight (by rank ordering) and a flush (by uniformity of suit). A three of a kind occurs when all three cards have the same rank, and a pair occurs when two of the cards have the same rank. When none of these conditions hold, the hand has no special designation in three card poker, and the bet is lost.
Your task is to write a program that serves as a computerized version of the game. You must create a deck of 52 cards, thoroughly randomize them to simulate shuffling, and select the first three cards as a hand. You must then determine the highest designation that applies to the hand of cards and the corresponding winnings (if any).
Any reasonable solution to this problem uses arrays, discussed in the previous chapter. Using only that knowledge, you should be able to create a three card poker game. However, the focus of this chapter is methods, which can be used to break the solution to a problem into logical pieces. By dividing the solution in this way, we’ll be able to solve the three card poker problem in a relatively short, elegant, and easy to read way.
8.2. Concepts: Dividing work into segments
You should notice that the solutions to problems we’ve been working on have become increasingly complex as the book progresses. This progression is partly because we want to solve more interesting problems with more complex solutions but also because we’ve introduced additional Java tools that make solving these harder problems possible.
Surprisingly, the tools we introduce in this chapter do not allow you to solve a problem you couldn’t solve before. Instead, the tools in this chapter and in the next make solving a problem easier and less susceptible to errors. A relatively long, complicated list of Java statements was necessary to solve problems like the statistics program or Conway’s Game of Life. Instead of solving those problems as a single monolithic segment of code, we can break our solutions into units called static methods.
8.2.1. Reasons for static
methods
A static method is a short, named segment of code that’s been packaged up so that it can be called from other parts of the program. Whenever the task performed by this method is needed, the execution of the program jumps to the code in the method, does the work it’s supposed to, and then returns to whatever it was doing before.
The reasons for using static methods can be boiled down to three essentials:
- Modularity
-
Very little software is written by individuals. Commercial software can have tens or even hundreds of developers involved in designing, implementing, testing, and maintaining code. When the code’s divided into individual methods, those methods can be written and tested by different people with minimal interference. It’s much harder to work together on one giant block of code.
- Readability
-
Methods are named segments of code. When a method is called, its name is used. If a meaningful name is chosen, the line in the code that calls the method helps document the code by explaining what’s happening. For example, if a method called
sort()
is used, a reader instantly understands that something’s being sorted. If, instead of a separatesort()
method, the code that does sorting was pasted into the same location, a reader might have to devote a few moments to understanding the meaning of those lines of code, particularly without comments. - Reusability
-
The fact that a method can be called from anywhere in the code makes it reusable. To use the
sort()
example again, we might have to sort several different arrays in one program. Without methods, we’d have to keep copying and pasting code over and over again. With methods, the total amount of source code can be smaller. In fact, if you find yourself copying and pasting code, it’s often an indication that a method would be useful.
This idea of reusability stretches beyond individual programs. A static method can be called from an entirely different program. If you create a method that’s really useful, you’re free to use it in other programs you write. By doing so, you only have to makes changes in one place if you discover errors or want to increase its functionality.
8.2.2. Parallel to mathematical functions
We’ve been discussing the usefulness of static methods without saying exactly what they are. One way to get insight into methods is by acknowledging their similarity to functions from mathematics. In procedural (non-object oriented) languages like C, the equivalents of static methods are usually referred to as functions. Like functions in mathematics, a static method takes some number of inputs (possibly as few as zero) and usually produces some output.
You’ve already used several static methods from the Math
class, such
as Math.sqrt()
. Consider the following function.
This function mirrors the code inside of Math.sqrt()
. In math, when you apply f(x) to
a specific value, you might write y = f(5). In Java,
you might type the following.
double y = Math.sqrt(5);
In the statement f(5), the placeholder variable x takes on the value 5.
In the same way, some variable inside of
Math.sqrt()
takes on the value 5
when the method is called.
It’s important to understand this similarity between methods and mathematical functions because the designers of many programming languages have been directly influenced by the older notation. However, there are many differences between the two concepts as well. Mathematical functions can take more than one input, and so can Java methods. Java methods can also take no input, while the central idea of a mathematical function is to map some input value(s) to some output value. Java methods do not necessarily even have output values. They may just do something.
8.2.3. Control flow
We have discussed selection statements such as if
and switch
as well
as three different looping structures, for
, while
, and do
-while
.
Each of these Java language features is used for control flow. The
selection statements allow your program to choose which code to execute.
The loops allow your code to repeatedly execute certain code. Methods
also affect control flow. When a method is called, the execution of the
program jumps into the method, does all the work it needs to there, and
then returns to the code that called it. Recall the very simple example
from above.
double y = Math.sqrt(5);
The JVM has allocated a variable of type double
and is preparing to
assign a value to it. Suddenly, the execution of the JVM jumps into the
code inside of the Math.sqrt()
method. It takes some non-trivial
amount of work to compute the square root of a number. After that
computation is finished, the flow of execution returns to the assignment
that has been waiting all that time. Just like a mathematical function,
we can treat the method call Math.sqrt(5)
as if it were “magically”
replaced by a double
approximating the square root of 5.
8.3. Syntax: Methods
By now, you should have a good feel for the concepts behind calling and even creating static methods and are probably getting impatient to use them. However, we haven’t yet described all the details of Java method syntax. First, we’ll describe how you can create your own static methods, then we’ll discuss the finer points of calling static methods, and finally we’ll explain how class variables can be used from many different methods.
8.3.1. Defining methods
A very simple method that the Math
class provides is the Math.max()
method. This method selects the larger of the two values provided as
input.
int maximum = Math.max(5, 10);
In this case, the value stored into maximum
is 10
. Despite its
simplicity, we demonstrated how useful this method could be in our
solution to Conway’s Game of Life from Chapter 6. If we
wanted to write this method ourselves, the code would be as follows.
public static int max(int a, int b) {
if(a >= b)
return a;
else
return b;
}
Even in such a small method, there’s a lot of syntax to
worry about. The first line of this method is called the method
header. The public
keyword in this header is used to denote that any
code, even code from a different class, can call this method. We discuss
restricting access to methods and variables more in the later part of
this chapter. For now, assume that every method is public
.
The keyword static
indicates that this method is static. Although we
have used the term static method many times, we have not yet defined
it. A static method is linked to a whole class, not to a specific object
of that class–that is, a static method can be called without referencing
an object of the class. Again, we discuss the finer points of objects
and classes in the next chapter. For now, all methods are static
.
The third keyword in the method header is the familiar int
, giving the
return type of the method. Wherever this method is called, it can be
treated like an int
value, because that’s what it gives back. In this
case, the return type is obvious: The maximum of two int
values must
also be an int
value. Any type can be used as a return value including
all the primitive types and any reference or array types. The only
limitation is that a method can only return a single item, but since
that item can be an array, this limitation is usually not important. It’s
also possible for a method to return nothing. In that case, the
keyword void
is used for the return type.
Next in the method header is the identifier max
, which is the name of
the method. Any legal identifier that you can use for a variable name is
valid for a method name as well. It’s important to pick a name that’s
readable and gives a reader a clear idea about what the method does. A
common convention is to name a method using a verb phrase, indicating
the operation that is being done by that method (e.g., computeTax
).
Like variable names, the Java standard is to use camel notation,
starting with a lowercase letter and capitalizing the first letter of
each new word in the name.
After the name of the method is the list of parameters, separated
by commas. In this case, each parameter has the int
type. You’re free
to name your parameters whatever you want, though they should be
meaningful. You can have as few as zero parameters, but there’s an
upper limit imposed by the JVM, usually 255. The body of the method
follows the header of the method, surrounded by braces ({ }
). Unlike
if
statements and loops, the braces for methods are required.
Inside the body of a method, the usual rules for Java control flow
apply. Each line is executed one by one unless there are selection
statements or loops. Calling methods inside of methods is also allowed. In the max()
method, we use an if
-else
construction to find
the larger of a
and b
. A return
statement immediately stops
execution of the method, transfers execution back to the calling code,
and gives back the value that comes after it. In this case, the value of
a
is returned if it is equal or larger, and the value of b
is
returned otherwise. Because a return
statement immediately jumps out
of a method, we could have written the method with one fewer line of
code.
public static int max(int a, int b) {
if(a >= b)
return a;
return b;
}
The only way that the line return b;
can be reached is if a
had not
already been returned.
Beginner programmers sometimes ask what to do if a
and b
are equal. In our code it’s clear that the value of a
will be returned, but it hardly matters: two equal int
values are indistinguishable from each other. Some students imagine handling this special case by printing an error message, but that approach is seldom useful. When trying to find the larger of two numbers in programming, it’s usually the value we want, not information about which one is actually larger.
The main()
method
If some of this syntax seems eerily familiar, remember that you’ve
been coding static methods since your very first Java program. The
main()
method is just another static method, special only because the
JVM chooses to start execution there. Let’s look at the main()
method
from a standard Hello, World! program.
public static void main(String[] args) {
System.out.println("Hello, world!");
return;
}
Just like the max()
method, the header for main()
starts with
public static
. Then, the return type for main()
is void
because
the JVM is not expecting to get any answer back. The main()
method has
a single parameter, an array String
values. In this program, we do
not use the args
parameter, but it is available. For the main()
method, this declaration is fine, because main()
has to be uniform
across all programs. However, when designing your own methods, you
should not include unnecessary parameters.
The final executable line in this main()
method is a return
statement. Because main()
has a void
return type, the return
statement has no value to return. For void
methods, a return
statement is optional. You can use it to leave a method early if
desired. For a value-returning method, execution must reach a return
statement with a valid value no matter what the input of the method is.
If Java finds a way that execution could reach the end of a value
returning method without reaching a return
statement, it causes a
compiler error.
Overloaded methods
Since this declaration is in another class, it’s fine to create a max()
method even though there’s already one in the Math
class. However, it’s possible to create more than one method with the same name even in the
same class, provided that their signatures are not the same. Two
methods have the same signature if they have the same name and
parameter types.
public static int max(int a, int b, int c) {
return max(max(a, b), c);
}
In this example, we’ve created yet another max()
method, but this
one takes three parameters instead of two. This method even calls the
two parameter version of max()
. Creating more than one method with the
same name is called overloading those methods. Overloading methods is
useful because it allows you to use the same method name for similar
functionality, even when there are some underlying differences in the
implementation. For example, the Math
class provides four different,
overloaded versions of the max()
method, specialized for int
,
long
, float
, and double
values, respectively.
There are limitations on creating overloaded methods, of course. The compiler must be able to determine which method you intend to use. Thus, the signatures have to vary by type or number of parameters. A different return type is not enough.
8.3.2. Calling methods
After a method has been defined, it must be called before it does
anything. You have plenty of experience calling static methods like
Math.sqrt()
and Math.max()
. An example of the appropriate syntax was
given earlier.
int maximum = Math.max(5, 10);
Formally, the call starts with the name of the class (Math
), followed
by a dot, followed by the name of the method (max
), followed by the
list of arguments inside parentheses. These arguments are the values
you want to pass into the method. Some books use the term formal
parameters to describe the variables defined in the method signature
and actual parameters to describe the values passed into the methods,
but we stick with the simpler terms parameters and arguments.
Of course, the number of arguments must match the number of parameters
defined by the method, and the types must match as well. Java performs
automatic casting when no precision is lost. Thus, you can always supply
an int
argument for a double
parameter but not the reverse.
Arguments can be literal values, variables, or even other method calls
that return the appropriate type.
Using the max()
method defined before, we could rewrite our simple
example without a class name.
int maximum = max(5, 10);
Whenever you call a static method from code that’s inside the same class, you can leave out the class name.
Binding
Many new programmers are confused about the relationship between arguments and parameters. The process of supplying an argument to be used as a parameter is called binding. Through binding, a value or variable from the calling code is given a new name inside of a method. Consider the following method.
public static int add(int a, int b) {
return a + b;
}
This absurdly short method adds two numbers together and returns the
result, approximating the functionality of the +
operator. We could
call the method in the following context.
int x = 3;
int y = 5;
int z = add(x, y);
Inside the method, the value of x
is bound to the variable a
, and
the value of y
is bound to the variable b
. The add()
method has
its own scope. Scope means the area where a variable name is visible
(or meaningful). Thus, x
and y
do not exist inside of the add()
method, only the variables a
and b
do. Since methods have their own
scope, variables in one method can have the same names as variables in
another method without the compiler (or the programmer!) becoming
confused. Consider the following example.
int a = 3;
int b = 5;
int c = add(b, a);
Here the variables a
and b
exist in both the calling code and inside
the method, but the names are independent. The value of a
in the
calling code happens to be bound to a variable called b
inside the
method, but the JVM has no confusion about which a
is which. Herein
lies the value of methods: They are largely independent of whatever else
is going on in the code, allowing the programmer to focus on a small,
manageable task.
Another important feature of Java is that the process of binding variables is pass by value, meaning that only the value of the argument is bound to the parameter. Whenever a method is called, the method creates a new variable for each parameter and copies the value of its argument into it. In practice, this approach means that a method cannot directly change the value of an argument. Consider the following method.
public static void increment(int counter) {
counter++;
}
This method takes the value of its argument and copies it into the new
variable counter
. Then, it increments counter
, but the original
argument is unchanged. Thus, the following fragment is an infinite loop.
int i = 0;
while(i < 100)
increment(i);
The value of i
remains fixed at 0
for the entire program. The copy
of i
bound to counter
increases to 1
every time increment()
is
called, but i
remains unaffected.
This is not to say that a method cannot affect the variables outside of
itself. The primary way that it can do so is by using return
statements. We can rewrite increment()
to achieve this effect.
public static int increment(int counter) {
return counter + 1;
}
Then, we need to adjust the loop so that it stores the returned value instead of dropping it on the floor.
int i = 0;
while(i < 100)
i = increment(i);
A second way that methods can affect the values of outside variables is more indirect. In Java, every argument is passed by value, even arrays and objects. Practically, this means that, if a reference to an array is passed into a method, you cannot change which array it is pointing at. Since references are not values but names pointing at particular locations in memory, you can directly change the contents of that memory with a method, even if you can’t change which locations are being referenced. For example, the following method does not reverse the order of an array.
public static void badReverseArray(int[] array) {
int[] temp = new int[array.length];
for(int i = 0; i < array.length; i++)
temp[i] = array[array.length - i - 1];
array = temp;
}
Although this code does store a reversed version of array
in temp
,
the last line of the method is meaningless: The array passed into the
method still points to the original location in memory. We can rewrite
the method to do the reversal in place, meaning that the values of the
array are shuffled around, but the array still occupies the same memory
locations.
public static void goodReverseArray(int[] array) {
int temp;
for(int i = 0; i < array.length / 2; i++) {
temp = array[i];
array[i] = array[array.length - i - 1];
array[array.length - i - 1] = temp;
}
}
In this version of the method, we swap the first element of the array
with the last, the second with the second to last, and so on. We only go
up to the halfway point of the array, otherwise we would undo the reversal
process. The values of the array are reversed, but they still occupy the
same chunk of memory. It’s possible to write a correct method more in
the style of badReverseArray()
which creates a temporary array, copies
the original values into it, and then copies them back to the original
array in reverse order, but it’s less efficient to create the extra
array and perform two copies.
8.3.3. Class variables
According to the rules we’ve given so far, the only legal variables in
the scope of a static method are the parameters and any other local
variables declared inside the method. However, it’s possible to create
a variable that exists outside of static methods yet is visible inside
all of them. These kinds of variables are called class variables (or
sometimes static fields or global variables). These variables
persist between method calls. The syntax for creating such a variable
is to declare it outside of all methods (but inside the class) with the keyword static
and an access modifier such as public
or private
.
For example, the following class includes a method called record()
that increases the class variable counter
every time it’s called.
record()
method is called.public class Bookkeeper {
public static int counter = 0;
public static void main(String[] args) {
while(Math.random() > 0.001)
record();
System.out.println("Record was called " + counter + " times.");
}
public static void record() {
counter++;
}
}
When run, this program calls the record()
method some random number of
times, and the variable counter
keeps track of the number. Because counter
is static,
it’s accessible to both the main()
and record()
methods.
Many programmers frown on the use of
class variables precisely because they’re visible to many different
methods. The idea of a method is to isolate pieces of code so that the
complexity of a program can be divided into simple units. In the case of
a public class variable, even code in other classes can modify its
value. If many different pieces of code can modify a variable, it may
be difficult to keep that variable from being changed in unexpected
ways. For example, if another method used the counter
variable to record the number
of times it was called, the final value of counter
would be the sum of
the number of times the two methods were called. There might be some
reason to keep track of such information, but it would be impossible to
reconstruct what fraction of the value in counter
came from one method
and what fraction came from the other.
Class variables have their uses, but they should generally be avoided.
The chief exception to this rule is constants. Since a constant never changes,
a class variable is a great place to store it, making the value
available to all code. An example you’ve already used is
Math.PI
. As with static methods, a static field from another class can
be accessed by using the class name, then a dot, and then the name of the
static field. Again, when the code using the field is in the same class,
the class name can be dropped. A class constant is declared like a class
variable, but with the addition of the final
keyword.
The following class allows a user to compute the one-dimensional force due to gravity. This force F is given by the following equation.
In this equation, m1 is the mass of one object, m2 is the mass of another, r is the distance between their centers, and G is the gravitational constant, 6.673 × 10-11 N·m2·kg-2.
import java.util.Scanner;
public class Gravity {
public static final double G = 6.673e-11;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("What is the first mass?");
double m1 = in.nextDouble();
System.out.println("What is the second mass?");
double m2 = in.nextDouble();
System.out.println("What is the distance between them?");
double r = in.nextDouble();
System.out.println("The force of gravity is " + force(m1, m2, r) + " N");
}
public static double force(double m1, double m2, double r) {
return G*m1*m2/(r*r);
}
}
Use named constants whenever they increase readability. You can use the public
modifier if you want all classes to have access to your constant
(Gravity.G
is a good example). You can use the private
modifier if
you want the constant to be accessible only inside your class, if it has
no use outside, or if it contains secret information.
8.4. Examples: Defining methods
Any large problem should be broken down into methods. Because the
technique is useful in so many circumstances, it’s difficult to give a
set of examples that covers all cases. Instead, our examples are
short, easy to understand methods, focusing on Euclidean distance,
testing for palindromes, and converting a String
representation of an
int
to an int
.
The Euclidean distance between two points is the length of a straight line connecting them. It plays an important role in 3D graphics and games and is the basis for many other practical applications involving spatial relationships.
Given two points in 3D space (x1, y1, z1) and (x2, y2, z2), we can compute the Euclidean distance between them with the following equation.
The method below applies this equation directly.
public static double distance(double x1, double y1, double z1,
double x2, double y2, double z2) {
double x = x1 - x2;
double y = y1 - y2;
double z = z1 - z2;
return Math.sqrt(x*x + y*y + z*z);
}
This equation is a good candidate for a static method since it might be necessary to do this calculation many times and it does not depend on any other variables or program state.
A palindrome is a word or phrase (or even a number) that’s the same
spelled forward and backward. “Racecar,” “Madam, I’m Adam,” and
“Satan, oscillate my metallic sonatas” are examples in English.
Typically, spaces and punctuation are ignored. We’re going to write a
function that, given a String
, returns true
if it’s a palindrome
and false
otherwise. To simplify the problem, we’re not going to
ignore spaces and punctuation. Thus, with our method, “racecar” counts
as a palindrome, but neither of the other two examples would.
public static boolean isPalindrome(String text) { (1)
text = text.toLowerCase(); (2)
for(int i = 0; i < text.length() / 2; i++) (3)
if(text.charAt(i) != text.charAt(text.length() - i - 1)) (4)
return false;
return true;
}
1 | Because our method returns true or false , its return type must be
boolean . Many methods that return a boolean value have a name
starting with is , like our method. |
2 | The first line of the body of our
method changes text to lowercase. The String method toLowerCase()
creates a lowercase copy of the String it’s called on, in this case
text . Then, we point the reference variable text at that new,
lowercase String . On the outside of this function, the String passed in
does not change because the reference text is passed by value. |
3 | The loop iterates through the first half of text , comparing it to the
second half. This loop reflects the asymmetry of these kinds of tests:
You can’t be sure that text is a palindrome until you’ve checked the
entire thing, but you immediately know that it’s not if even a single
pair of characters doesn’t match. |
4 | If the test in that if statement
ever shows that the two char values aren’t equal, the return false
statement jumps out of the method without completing the loop. |
When you read in a number using an object of the Scanner
class, it
converts (or parses) the text entered by the user into the appropriate
type. For example, the nextDouble()
method reads in some text and
convert it into a double
. When you use a JOptionPane
method to read
in input, it comes in as a String
. If you want to use that data as a
double
, you must convert it using the Double.parseDouble()
static
method. Some Java programmer had to write this method. We’re going to
recreate a similar method to convert the String
representation of a
floating-point number into a double
. Our simple method ignores
scientific notation.
public static double parseDouble(String value) {
int i = 0;
boolean negative = false;
double temp = 0.0;
double fraction = 10.0;
if(value.charAt(i) == '-') { (1)
negative = true;
i++;
}
else if(value.charAt(i) == '+')
i++;
while(i < value.length() && value.charAt(i) != '.') { (2)
temp *= 10.0;
temp += value.charAt(i) - '0';
i++;
}
i++; //move past decimal point (if there) (3)
while(i < value.length()) { (4)
temp += (value.charAt(i) - '0') / fraction;
fraction *= 10.0;
i++;
}
if(negative) (5)
temp = -temp;
return temp;
}
1 | After declaring a few variables, this method first checks index 0 in
the input String value to see if it is a '-' or a ''`. If it is
a `'-'`, it sets `negative` to `true` and moves on. If it is a `'' , it
simply moves on. |
2 | Then, the method loops through value until it reaches
the end or reaches a decimal point. As it iterates, it multiplies the
current value of temp by 10.0 and adds in the next digit from value
(after subtracting '0' so that the range is is from 0 to 9). This
repetitive multiplication by 10.0 accounts for the increasing powers of 10
in the base 10 number system. Since temp starts with a value of 0.0 ,
the first multiplication has no effect, as intended. |
3 | After the first while loop, the index i is incremented once, to skip
the decimal point, if there is one. If there is no decimal point, the
loop must have exited because the end of value had been reached. |
4 | The second while loop runs to the end of value , this time adding in each
digit value divided by fraction , which is increased by a factor of 10.0
each time. Doing so allows us to add smaller and smaller fractional
digits to the total. |
5 | We set temp to its opposite if the flag
negative was set earlier and finally return temp . |
Note that the real Double.parseDouble()
method not only
accepts String
values in scientific notation but also does a great
deal of error checking. Our code either crashes or gives inaccurate
results on an empty String
, a String
containing non-numerical
characters, or a String
with more than one decimal point. Furthermore,
this code does not use the best approach for minimizing floating-point
precision errors.
8.5. Solution: Three card poker
Here we present our solution to the three card poker problem. We explain each method individually.
public class ThreeCardPoker {
public static final String[] SUITS = {"Spades", "Hearts", (1)
"Diamonds", "Clubs"};
public static final String[] RANKS = {"2", "3", "4", "5", "6",
"7", "8", "9", "10", "Jack", "Queen", "King", "Ace"};
public static final int STRAIGHT_FLUSH = 40; (2)
public static final int THREE_OF_A_KIND = 30;
public static final int STRAIGHT = 6;
public static final int FLUSH = 2;
public static final int PAIR = 1;
public static final int NOTHING = 0;
1 | Before our main() method begins, we declare a number of
class constants. Two constant arrays of String values provide us with
an easy way to represent suits and ranks. |
2 | The remaining six int
constants are used to allocate a winning payoff to each possible
outcome. |
Note that these constants can be declared anywhere inside the class, provided that they’re outside of methods. However, it’s typical (and good style) to declare them at the top of the class.
public static void main(String[] args) {
int[] deck = new int[52]; (1)
int[] hand = new int[3];
for(int i = 0; i < deck.length; i++) (2)
deck[i] = i;
shuffle(deck);
for(int i = 0; i < hand.length; i++) (3)
hand[i] = deck[i];
int winnings = score(hand); (4)
System.out.println("Hand: ");
print(hand);
if(winnings == 0) (5)
System.out.println("You win nothing.");
else
System.out.println("You win " + winnings +
" times your bet.");
}
1 | In the main() method, an array representing a deck of 52 cards is
created first, followed by an array representing the 3 cards to be
dealt. |
2 | The deck is filled sequentially and then shuffled with a method. |
3 | Next, the first 3 cards of the deck are copied into the array representing the hand of cards. |
4 | The score of the hand is determined, and then the hand is printed out. We print the hand after determining the score because the hand is sorted in the process of determining the score, making the output easier to read. |
5 | Finally, we print the appropriate output, depending on the score. |
public static void shuffle(int[] deck) {
int index, temp;
for(int i = 0; i < deck.length; i++) {
index = i + (int)((deck.length - i)*Math.random());
temp = deck[index];
deck[index] = deck[i];
deck[i] = temp;
}
}
This method shuffles the deck. Its approach is to swap the first element
in the array of cards with one of the elements that follow, chosen
randomly. Then, it swaps the second element in the array with any of the
elements that follow it, and so on. If Math.random()
truly gives us a
uniformly generated random number in the range [0,1), the
final shuffled deck should be any one of the 52! possible
decks with equal probability.
public static void print(int[] hand) { (1)
for(int i = 0; i < hand.length; i++)
System.out.println(RANKS[getRank(hand[i])] + " of "
+ SUITS[getSuit(hand[i])]);
}
public static int getRank(int value) { return value % 13; } (2)
public static int getSuit(int value) { return value / 13; } (3)
1 | The first of these methods prints out a human readable version of each card in an array (instead of 0 - 51). It does so using the second and third methods as helper methods. |
2 | Method getRank() computes the rank of
a card from its number. |
3 | Method getSuit() computes the suit of a
card from its number. |
The indexes obtained from these methods are used
to index into the RANKS
and SUITS
arrays.
In the C language, calling the equivalent of a method from a method defined earlier
requires a special declaration step called prototyping before both
methods. Java does not have this complication, and the getRank()
and
getSuit()
methods compile and function perfectly if they are written
above print()
or below it inside the class definition.
private static int score(int[] hand) {
sortByRank(hand);
if(hasStraight(hand) && hasFlush(hand))
return STRAIGHT_FLUSH;
if(hasThree(hand))
return THREE_OF_A_KIND;
if(hasStraight(hand))
return STRAIGHT;
if(hasFlush(hand))
return FLUSH;
if(hasPair(hand))
return PAIR;
return NOTHING;
}
This method computes the score by first sorting the hand and then testing progressively worse outcomes, starting with the best, a straight flush. As it moves down the list of outcomes, it calls appropriate methods to determine if a hand has a certain characteristic.
private static void sortByRank(int[] hand) {
int smallest, temp;
for(int i = 0; i < hand.length - 1; i++) {
smallest = i;
for(int j = i + 1; j < hand.length; j++) {
if(getRank(hand[j]) < getRank(hand[smallest]))
smallest = j;
}
temp = hand[smallest];
hand[smallest] = hand[i];
hand[i] = temp;
}
}
This code is an implementation of selection sort packaged into a method.
Note that this method does change the values inside of the
array hand
even though it cannot change the array that hand
points
to. The array itself is passed by value, but its contents are
effectively passed by reference.
private static boolean hasPair(int[] hand) { (1)
return getRank(hand[0]) == getRank(hand[1]) ||
getRank(hand[1]) == getRank(hand[2]);
}
private static boolean hasThree(int[] hand) { (2)
return getRank(hand[0]) == getRank(hand[1]) &&
getRank(hand[1]) == getRank(hand[2]);
}
private static boolean hasFlush(int[] hand) { (3)
return getSuit(hand[0]) == getSuit(hand[1]) &&
getSuit(hand[1]) == getSuit(hand[2]);
}
private static boolean hasStraight(int[] hand) { (4)
return (getRank(hand[0]) == 0 && getRank(hand[1]) == 1
&& getRank(hand[2]) == 12) || //ace low
(getRank(hand[1]) == getRank(hand[0]) + 1 &&
getRank(hand[2]) == getRank(hand[1]) + 1);
}
}
1 | The code in hasPair() works by
checking to see if the first and second or second and third cards have
the same rank. An extra condition would be required if the cards weren’t sorted. |
2 | The code in hasThree() checks to see if all the ranks
are the same. |
3 | The code in hasFlush() is the same as hasThree()
except that it checks for suit instead of rank. |
4 | Finally, hasStraight()
checks to see if the ranks come one after the other, with an extra
case to deal with the possibility of the ace counting as low.
This test only works because the cards were sorted previously. |
These four methods would be similar but more complex for five- or seven-card poker hands.
8.6. Concurrency: Methods
In Java, it’s impossible to have concurrency without methods. Methods
are the way we break a large program into manageable pieces but are also
part of the syntax that Java uses to create threads of execution. Each
thread of execution is associated with a Thread
object; however, creating
the object is not enough to start a new thread of execution running.
Only when the start()
method is called on the Thread
object does the
new thread start running.
Hopefully, you’ve begun to visualize the execution of Java programs as
an arrow that sits next to each line of code as it’s executed. This
arrow can jump to a choice and skip over other code using if
and
switch
statements. Using loops, the arrow can jump backward and
repeatedly execute code it’s just executed. As we discussed in
this chapter, the arrow can jump into a method, execute the code in that
method, and then return to its caller, going back right to where it left
off before the call.
When the start()
method is called on a Thread
object, however, the
arrow returns to the caller, but it also splits itself into a second
arrow that then executes the corresponding run()
method and any other
methods it calls. Note that we’re talking about a method called on a
Thread
object, not a static method called on the class as a whole.
Calling start()
is an instance method, which we discuss in
Chapter 9. Unlike the static methods in this chapter,
an instance method is tied to a particular object, but
most of what you’ve learned about methods still applies.
Methods are supposed to make programming easier by breaking programs into chunks small enough to think about. One of the only real dangers of methods is using class variables, as mentioned in Section 8.3.3. This problem becomes worse with multiple threads. With a single thread, two or more different methods can all affect the same class variable, perhaps in conflicting ways. With multiple threads, even the same method can interfere with itself.
A linear congruential generator (LCG) allows you to create a sequence of pseudorandom numbers using the equation xi = (axi-1 + b) mod m, deriving the next number from the previous one, and so on.
rand()
used in the C language.public class UnsafeRandom {
private static int next = 1;
private final static int A = 1103515245;
private final static int B = 12345;
private final static int M = 32768;
public static int nextInt() {
return next = (A*next + B) % M;
}
}
The UnsafeRandom
program listed above always generates the same
sequence of pseudorandom numbers, which can be useful for debugging
a program. However, if two or more threads call nextInt()
, they’ll
probably get different sequences. One thread will pick up some of
the numbers, and the other will pick up the missing numbers in between. If
each thread wants to generate the same sequence of numbers, the method
should be rewritten so that it takes in the previous number in the
sequence. In that way, there’s no shared state. Remember that using a
(non-final) class variable should be avoided whenever
possible.
public class SafeRandom {
private final static int A = 1103515245;
private final static int B = 12345;
private final static int M = 32768;
public static int nextInt(int previous) {
return (A*previous + B) % M;
}
}
By forcing each thread to carry its own state, we fixed the previous problem. In Chapter 14 we’ll talk about the much nastier problem of two threads executing a method at exactly the same time. When that happens, very curious effects are possible. Consider the following program.
print()
method always prints "Even"
when run with a single thread but can sometimes print "Odd"
if called repeatedly with multiple threads.public class AlwaysEven {
private static int value = 1;
public static void print() {
value++; (1)
if(value % 2 == 0)
System.out.println("Even");
else
System.out.println("Odd");
value++; (2)
}
}
1 | With a single thread running, value always goes up to an even number
before printing. |
2 | Afterward, it increments to the next odd number. |
If two or more threads call the print()
method, value
could
be changed by one right before the other executes the if
statements.
8.7. Exercises
Conceptual Problems
-
Describe three advantages of dividing long segments of code into static methods.
-
Can you think of any disadvantages of dividing code into methods? Are there situations in which using a method is unwise?
-
If you wanted to declare a static method that would compute the mean, median, and standard deviation of an input array of
double
values, how would you return those three answers? -
Consider the following method definition.
public static void twice(int i) { i = 2 * i; }
How many times does the following loop run, and why?
int x = 2; while(x < 128) twice(x);
-
Consider the following signatures of two overloaded methods.
public static int magic(int rabbit, double hat) public static int magic(double wand, int spell)
Which method would be invoked by the following call?
int x = magic(3, 16);
What about the following?
int y = magic(3.2, 16.4);
Use a compiler to check your answers.
-
The following class generates a sequence of even numbers. Each time the
next()
method is called, the next even number in the sequence is returned. What’s the design problem with using a static field to keep track of the next value in the sequence?public class EvenNumbers { private static int counter = 0; public static int next() { counter += 2; return counter; } }
Programming Practice
-
Write a static method called
cube()
that takes a singledouble
value as a parameter and returns its value cubed. Do not use theMath.pow()
method. -
Implement a static method that takes a single
int
value as a parameter and prints its digits in reverse. For example, if103
was passed into this method, it would print301
to the screen.You can find out what digit is in the ones place of a number by taking its remainder modulus 10. Then, you can remove the digit in the ones place by dividing by 10. Do not convert the
int
value into aString
. -
Write a static method that takes an array of
int
values as a parameter and returnstrue
if the array is in ascending order andfalse
otherwise. Compare each element of the array to the next element of the array. If the current element is ever larger than the next element, the array is not sorted in ascending order. Note that you can only be sure that the array is in ascending order after you have checked all neighboring pairs. -
Write a static method that finds the ⌊log2 n⌋ of an integer n. Note that if log2 n = x, it’s also true that n = 2x. In other words, the log2 operator tells you what power of 2 a number is. One way to define the log2 n is the number of times you have to divide n by 2 to get 1. Use this definition to make a loop that finds the value without using any calls to the
Math
library.Here are some examples of the return values your method should give for various input values of n.
n Return Value n Return Value 1
0
16
4
2
1
100
6
4
2
512
9
8
3
1000
9
10
3
1024
10
-
Write a method that tests palindromes like the method from Example 8.3 but also ignores punctuation and spaces. Thus,
"A man, a plan, a canal: Panama"
should be counted as a palindrome by this new method. -
Add to the
parseDouble()
method from Example 8.4 so that it can also handle numbers in standard Java scientific notation, such as7.239e-14
. Note that thee
can be uppercase or lowercase, and the exponent can begin with a minus sign (-
), a plus sign (+
), or neither. -
Re-implement the solution from Section 8.5 so that it uses a GUI constructed with
JOptionPane
to display the hand and the winnings. -
Five card poker is a much more common version of poker than the three card version we discussed in Section 8.1. Using static methods, implement a two-player game of poker in which the deck is shuffled and then dealt into two hands of five cards each. Then, state which player’s hand wins. With five cards, determining which hand wins is a more complicated process. The rankings of the various possible hands from best to worst are as follows.
- Straight Flush
-
All five cards belong to the same suit and have ranks in sequential order (with either ace high or low). If two people both have straight flushes, the higher ranked one wins. If they both have the same ranks, it is a tie.
- Four of a Kind
-
Four of the five cards have the same rank. If two people have four of a kind, the higher rank set of four wins.
- Full House
-
Three of the five cards have the same rank and the other two share another rank. If two people have a full house, the higher ranked set of three wins.
- Flush
-
All five cards have the same suit. If two people have flushes, the one with the highest card wins. If the highest card is a tie, the next highest is the tie breaker, and so on. If the two flushes have exactly the same ranks, the two flushes tie.
- Straight
-
All five cards have ranks in sequential order (with either ace high or low). If two people both have straight, the higher ranked one wins. If they both have the same ranks, it is a tie.
- Three of a Kind
-
Three of the cards have the same rank. If two people have three of a kind, the higher ranked set of three wins.
- Two Pair
-
A pair of cards have the same rank and another pair of cards share another rank. If two people both have two pairs, the higher ranked pair is a tiebreaker. If the higher ranked pair is the same, the lower ranked pair is a tiebreaker. If the lower ranked pair is the same, the final unpaired card is the tiebreaker. If all the ranks of both hands match, it is a tie.
- Pair
-
A pair of cards has the same rank. If two people have pairs, the rank of the pair is a tiebreaker. If the pairs have the same rank, the remaining cards in each hand are tiebreakers, in descending rank order.
- High Card
-
If none of the other cases hold, the high card determines the value of the hand. If two people have the same highest card, the remaining cards in each hand are used as tiebreakers, in descending rank order.
Experiments
-
In terms of time, there’s a small overhead associated with calling a method and returning a value, but it’s very hard to measure. Write a program with two
int
variables,a
andb
, wherea
starts with a value of1
andb
starts with a value of2
. Run afor
loop 100,000,000 times. On each iteration first increase the value ofa
by the value ofb
and then increase the value ofb
bya
. Time this loop withSystem.nanoTime()
, and then print out the time taken and the value ofa
. The value ofa
is not important, but the compiler will optimize away the math done witha
andb
unless we output the value. We recommend that you run this program repeatedly to get a sense of the average running time.Now, instead of using the
+
operator to adda
andb
, use the following method.public static int add(int a, int b) { return a + b; }
Again, run your program repeatedly with this modification. What’s the difference in running time between the version that uses a method and the version that does addition directly?
Depending on your JVM, it’s quite possible that there’s almost no difference. The JVM does a lot of optimizations including inlining, which replaces a call to a short method with the actual code inside the method.