By the end of this module, for simple programs, you will be able to:
Trace the execution, mentally and written, of programs
that have methods with parameters and return values.
Write methods to achieve the desired output.
Use method calls in expressions.
Explain the difference between basic-type
parameters, and array parameters.
Debug errors in methods.
Identify new syntactic elements related to the above.
4.0 Audio:
4.0 Why we need methods: via an example
Before we get into the details of methods
(yes, that's jargon), we're going
to get a "high altitude" view:
We'll glance over a fairly long program with no methods.
We'll briefly look at the same thing but written
with methods.
The idea now, before getting into details, is to
get a sense of why it's useful to define and use our own methods.
We'll use this application as our example:
We've already encountered the Klingon language.
The general perception is that it's a more guttural
language than English.
To quantify this, we will look through all the words
and count the number of vowels, and the number of consonants.
And we'll look at the fraction of letters that are
vowels.
The more guttural, the lower the fraction.
4.1 Exercise:
Download, compile and execute
VowelDensity.java
to see these fractions being computed for each
of English and Klingon.
You will also need
WordTool.java,
words.txt,
and
klingonwords.txt.
Try to read the code to see if you can make sense of
parts of it.
We'll now list the whole program in its entirety:
public class VowelDensity {
public static void main (String[] argv)
{
char[] vowels = {'a', 'e', 'i', 'o', 'u'};
// First, English.
String[] englishWords = WordTool.getUnixWords ();
// Set up counters:
int numVowels = 0;
int numConsonants = 0;
// Loop through all English words.
for (int i=0; i<englishWords.length; i++) {
// For each word, we'll count the number of vowels:
int v = 0;
for (int j=0; j<englishWords[i].length(); j++) {
// Compare the j-th letter with all vowels
for (int k=0; k<vowels.length; k++) {
if ( vowels[k] == englishWords[i].charAt(j) ) {
v++;
}
}
}
numVowels = numVowels + v;
numConsonants = numConsonants + (englishWords[i].length() - v);
// Note: if the word length is N, there are N-v consonants.
}
double englishFraction = (double) numVowels / (double) (numConsonants + numVowels);
System.out.println ("Fraction for English: " + englishFraction);
// Next, Klingon (without comments embedded)
String[] klingonWords = WordTool.getKlingonWords ();
numVowels = 0;
numConsonants = 0;
for (int i=0; i<klingonWords.length; i++) {
int v = 0;
for (int j=0; j<klingonWords[i].length(); j++) {
for (int k=0; k<vowels.length; k++) {
if ( vowels[k] == klingonWords[i].charAt(j) ) {
v++;
}
}
}
numVowels = numVowels + v;
numConsonants = numConsonants + (klingonWords[i].length() - v);
}
double klingonFraction = (double) numVowels / (double) (numConsonants + numVowels);
System.out.println ("Fraction for Klingon: " + klingonFraction);
// A 3rd language?
// ...
}
}
So, yes, it's long. But let's point out a few things:
Does it not seem wasteful to write more or less exactly
the same code for both languages?
The triply-nested for-loop seems a bit hard to understand,
even though we already have done vowel-counting before
(Module 3).
4.2 Exercise:
How many more lines would need to be added to the above
code to add a 3rd language?
Now let's look at the same application written with methods:
public class VowelDensity2 {
public static void main (String[] argv)
{
// First, English.
String[] englishWords = WordTool.getUnixWords ();
double englishFraction = processWords (englishWords);
System.out.println ("Fraction for English: " + englishFraction);
// Next, Klingon
String[] klingonWords = WordTool.getKlingonWords ();
double klingonFraction = processWords (klingonWords);
System.out.println ("Fraction for Klingon: " + klingonFraction);
// A 3rd language?
// ...
}
static double processWords (String[] words)
{
// This method will go through the array of words and update
// the vowel/consonant counts.
int numVowels = 0;
int numConsonants = 0;
for (int i=0; i<words.length; i++) {
int v = vowelCount (words[i]); // This method does the work.
numVowels = numVowels + v;
numConsonants = numConsonants + (words[i].length() - v);
}
double fraction = (double) numVowels / (double) (numConsonants + numVowels);
return fraction;
}
static int vowelCount (String w)
{
int numV = 0;
char[] letters = w.toCharArray ();
for (int j=0; j<letters.length; j++) {
if ( isVowel(letters[j]) ) {
// if the j-th letter is a vowel, update the count.
numV++;
}
}
return numV;
}
static boolean isVowel (char c)
{
// See if c is a vowel.
char[] vowels = {'a', 'e', 'i', 'o', 'u'};
for (int k=0; k<vowels.length; k++) {
if (c == vowels[k]) {
return true;
}
}
return false;
}
}
Without understanding the details, let's point out a few things:
It's now easy to look at the code in
main
and see that there are three lines of code per language.
There's a new reserved word we've seen for the
first time:
return
We can sort-of make sense of some of the code. For example:
static boolean isVowel (char c)
{
// See if c is a vowel.
char[] vowels = {'a', 'e', 'i', 'o', 'u'};
for (int k=0; k<vowels.length; k++) {
if (c == vowels[k]) {
return true;
}
}
return false;
}
We've seen this before.
And we can see that the method names are used
in other places.
For example,
for (int j=0; j<letters.length; j++) {
if ( isVowel(letters[j]) ) {
// if the j-th letter is a vowel, update the count.
numV++;
}
}
You may have noticed that our new methods do not
have the
public
reserved word. What this means is that other programs
can't use them. More about that later.
4.3 Exercise:
How many more lines would need to be added to the above
code to add a 3rd language?
Why methods are useful:
They improve readability.
They can be used in multiple ways
⇒ e.g., the isVowel method can be used for other purposes.
They can be designed for use by other people, if
we made them
public:
⇒ e.g., the
getKlingonWords()
method is what we've used from
WordTool.
Most importantly, they can be used in composition:
methods that call methods that call methods ... and so on.
Composition is a powerful way of using building blocks
for many purposes, and repeatedly:
For example, the Java folks wrote
System.out.println().
Now everyone uses it.
If you are working on a large project with many people,
you would be creating methods for others to use.
4.1 Methods: what we've already seen
Before continuing, it is essential to review methods as described
in Module 3
of Unit-0.
What to review:
The notion of how execution starts in one method, goes
to another, and comes back to the originating method.
Important: When we come back to the originating method, we
continue execution from where we left off.
4.4 Exercise:
Review that module. Now.
4.2 Method parameters
To see how methods work, let's start with a simple example:
4.5 Exercise:
Edit, compile and execute the above program.
Then, just before the declaration of j but
after the first invocation of incrementAndPrint in
main, print the value of i.
Let's examine a few details:
First, method names are like variable names:
usually a collection of letters and numbers.
⇒ e.g., incrementAndPrint.
The capitalization inside the name
(incrementAndPrint)
has no effect on the compilation/execution
⇒ It's merely for readability.
Next, let's distinguish between invocation
and definition:
We'll focus on as few aspects of the definition.
When you read this, say to yourself
"The method takes one parameter, an integer."
"The method returns nothing" (void).
Next, let's examine the flow of execution with
the first invocation:
At the next invocation:
What's important to remember: the value in
j gets copied into the parameter variable k.
⇒ Thus, the value in j is not affected inside
incrementAndPrint.
It's possible to use the same name for the parameter
variables, even though they are different:
Here, we say that the scope of the variables
i
and
j
in the method
incrementAndPrint
is limited to all the code inside the method.
4.6 Video:
4.7 Exercise:
Fill in the code in the method below so that the output
of the program is:
*****
***
*
4.8 Audio:
4.3 Multiple parameters
Remember Pythagoras? We know his famous result:
A Pythagorean triple is any group of three
integers like 3,4,5 where the squares of the
first two add up to the square of the third:
32 + 42 = 52.
We'll now write code to check whether a trio of numbers
is indeed a Pythagorean triple:
Here, the method checkPythagoreanTriple is
defined with three parameter variables.
⇒ All happen to identically
be int's here.
The values at the invocation are given to the
method in the order the parameters are declared:
The first value in the invocation goes to the first
parameter variable.
The second value in the invocation goes to the second
parameter variable. And so on.
A method can be invoked in various ways:
4.9 Exercise:
Fill in the code in the method below so that the output
of the program is 3 and 5, respectively.
4.10 Video:
4.4 Return values
So far, we've written methods that take values,
do things and print.
We get a whole new level of programming power, when
methods can compute something and return something
to the invoker.
Here's an example:
First, recall the meaning of power:
"2 raised to the power 5"
or 25 means 2 × 2 × 2 × 2
× 2
In this case, it evaluates to 32.
To see how the program works, let's simplify the program a little:
Initially, a has the value 0.
Then, the next thing to execute is the
invocation to power.
Only after power completes execution, does
the assignment to a occur.
After power completes execution, the return
value (8 in this case), gets assigned to a.
4.11 Exercise:
In module4.pdf,
trace the entire execution of the above program step by step,
including every iteration of the for-loop.
4.12 Exercise:
Fill in the code in the method below so that the output
of the program is 3 and 5, respectively.
4.13 Audio:
4.5 Using method calls in expressions
Methods that return values can be used and combined
in expressions, in powerful ways.
For example:
In the first statement, power gets called twice:
The first time, it is with parameter values 2 and 3.
The second time with parameters 2 and 4.
The resulting expression is evaluated and the final result
stored in a
4.14 Exercise:
Add the statement
System.out.println ("power of " + x + " to " + k)
as the first line inside the
power
method
in
ExpressionExample.java.
Then, in module4.pdf
trace through the second expression above, when
b is assigned a value.
4.15 Exercise:
In an earlier exercise, you wrote a method to find the
smallest among three values. Suppose you had
to find the smallest among five variables. Invoke the
the method (with the three variables) twice to
find the smallest among five. Fill in your code below:
4.16 Video:
4.6 Methods and arrays
When working with arrays, it's often useful to have
some computations delegated to methods.
For example:
4.17 Exercise:
Modify the above program to compute and print the mean
of the array's elements.
A method can return an array:
4.18 Exercise:
In module4.pdf,
trace through the steps of execution.
Let's point out a few things:
Notice how the return type is declared
in the method definition:
This means that, inside the method, we need to return
the same type of variable:
Each call to makeConsecutiveIntegers creates
a fresh array:
The size-5 array is "alive" until the second call
to makeConsecutiveIntegers.
After the second call returns, the variable A
refers to the newly return (3-element) array.
If we had wanted to keep both arrays around, we'd
write:
We can re-use a variable name inside a method:
Here, the scope of
A
inside
makeConsecutiveIntegers
is limited to all the code inside the method.
The two
A
variables in the program are really different variables.
4.19 Exercise:
In FindLargestInArray.java,
fill in the code needed to return the largest element of an
array
public class FindLargestInArray {
public static void main (String[] argv)
{
double[] someNumbers = {2.718, 3.141, 1.414, 1.618};
double largest = findLargest (someNumbers);
System.out.println (largest);
}
// Write your method here:
}
4.20 Video:
4.7 Multiple returns in a method
A method can have many points of return.
When a
return
is executed, then execution returns to the invoking code.
Think of multiple return's
as multiple doors to leave a method.
Consider this example:
Observe that the min method has two return's.
Only one return is executed in any one invocation
of a method, e.g.
4.21 Exercise:
In module4.pdf,
trace the execution of the above program. Why does it work
(that is, why does it find the minimum element in the
array)? How often is
the first return in min executed?
How often is the second one executed?
Here's another example with multiple return's
public class NegativeElement {
public static void main (String[] argv)
{
double[] data = {4.0, 5.0, -2.0, 1.0, 3.0};
boolean hasNegative = hasNegativeElement (data);
if (hasNegative) {
System.out.println ("Yeah");
}
else {
System.out.println ("Nah");
}
}
static boolean hasNegativeElement (double[] A)
{
for (int i=0; i<A.length; i++) {
if (A[i] < 0) {
return true;
}
}
return false;
}
}
4.22 Exercise:
In module4.pdf,
trace the execution of the above program.
How often is the second return in min executed?
Let's point out a few things regarding the above program:
First, we could "compact" the code in main
by writing:
This is possible because the return value of
hasNegativeElement is Boolean, i.e.,
will result in true or false.
4.23 Exercise:
In MyNegativeElement.java,
rewrite the hasNegativeElement
method so that there
is only one return in it.
4.24 Video:
4.8 Call-by-value vs call-by-reference
Consider the following program:
4.25 Exercise:
Before executing the program, guess the output. Then,
edit, compile and execute to see what the output is.
Use SwapExample.java
as your file name.
To explain what happened, let's simplify a little:
First, consider this simple program:
Here, the value in a is copied into
the parameter variable x.
This type of parameter behavior is called call-by-value:
⇒ The "value" is what's passed in a method "call"
Call-by-value applies to parameters of basic types like int
and double.
Arrays, however, are treated differently.
Consider the same example for arrays:
Here, a "reference" (as it's called) is passed:
A reference allows a method to modify the original.
Yes, it's a little strange.
This parameter behavior is called call-by-reference
⇒ It applies to arrays, and more generally objects.
We will learn about objects (which are a special
type of Java construct) later.
Finally, let's look at this program:
Here, we are NOT passing the array itself.
We are passing a single element of the array, an integer.
Because it's an integer, it gets copied into
the parameter variable.
Thus, a change to x has no impact on the array.
4.26 Video:
4.9 Communicating via global variables
Consider this task:
We're going to scan the letters in a string and do two things:
Count the number of spaces (an integer)
See whether or not there's an apostrophe (boolean).
And we'd like to create a single method that can "do this job".
We could start out with a method like:
static int analyze (String s)
{
char[] letters = s.toCharArray ();
int numSpaces = 0;
boolean hasApostrophe = false;
for (int i=0; i<letters.length; i++) {
if (letters[i] == ' ') {
numSpaces++;
}
if (letters[i] == '\'') {
hasApostrophe = true;
}
}
// return numSpaces or hasApostrophe?
}
But do we return the integer or the boolean?
In Java, you can only return one type of thing.
So either we declare
static int analyze (String s)
{
// return an int from somewhere in the code.
}
or
static boolean analyze (String s)
{
// return a boolean from somewhere in the code.
}
There are two ways to solve the problem of having
a method "return many things":
Put the many things in an object and return the
object.
Put the values to be returned in a place that's accessible
across all methods.
The former will have to wait until we dive into the more complex
topic of objects.
The second approach is easy to work with:
First, let's remember that we have thus far defined variables
inside methods, like:
However, what DOES work is to have variables declared outside
methods where they can be shared by all methods, as in.
public class StringAnalyzer {
static int numSpaces;
static boolean hasApostrophe;
static void analyze (String s)
{
// We can access the variables here:
numSpaces = 0;
hasApostrophe = false;
// ...
}
static void someOtherMethod ()
{
// ...
// And here:
numSpaces = numSpaces + 1;
// ...
}
}
Such shared variables are called global variables
because they are global to all the methods.
One can initialize and set values for global variables
in the same way we've done for other kinds of variables, as in:
public class StringAnalyzer {
static int a = 1;
static double b = 2.3;
static int[] A = {1,2,3,4,5};
static double[] B = new double [5];
static void someMethod (String s)
{
// ...
}
}
What we cannot do outside of methods is write
regular code with for-loops, conditionals etc. For example,
the following will NOT compile:
public class StringAnalyzer {
// This is fine to have outside
// methods because it's a variable declaration
static int a = 1;
// This is NOT:
a = a + 1;
for (int i=0; i<5; i++) {
System.out.println (i);
}
static void someMethod (String s)
{
// ...
}
}
4.27 Exercise:
In
StringAnalyzer.java
complete the full program and test with the following:
One of the most useful, intriguing and slightly forbidding
reserved words is :
null.
Forbidding, because it often occurs in a negative context
such as when an error occurs or when you seek to prevent them.
null
is NOT the same thing as zero, nor does it refer to "nothing."
Let's first distinguish between basic types and more complex types:
The basic types are things like
int's
char's
and
double's.
The more complex types are like arrays.
Once we understand objects, we'll see that
Strings's
are also the more complex kind. In fact, we have a hint
of this because of methods inside a string like
length()
and
charAt().
All variables of the more complex types need
to be properly initialized, most often with the
new
operator, as in
int[] A = new int [5];
It's when they are not initialized that
null
comes in.
The default value for these types of variables (prior
to being initialized) is
null.
And it's when we forget to initialize that we get the
infamous null-pointer exception.
Let's look at an example:
public class NullExample {
static int[] A;
public static void main (String[] argv)
{
print (A);
}
static void print (int[] B)
{
for (int i=0; i<B.length; i++) {
System.out.println (B[i]);
}
}
}
Looks harmless, right?
4.29 Exercise:
Write up, compile and execute. What you will see is an error
(exception) printed to the screen: the null pointer exception.
Let's see what happened:
Look back at the program and notice that the array
was never initialized.
This means that the variable
A
has the value
null
in it.
So,
null
gets copied into the parameter variable
B
when
print
gets called.
Then, in the for-loop, when we try to access
B.length
there is no real array.
This is what Java flags as a null-pointer exception.
Our purpose in introducing this reserved word here is to
give you a "heads up". We'll deal with this more intensively
later.
Lastly, you might be wondering what the word "pointer"
is doing here? That, as it will turn out, is a really fundamental
and powerful concept.
4.11 Methods in String
Now that we know a little more about methods, let's
go back to
String
for a moment:
Recall the
length()
method inside a string:
String s = "Hola";
int k = s.length ();
When executed
length()
method inside the string "Hola" returns an integer
value, in this case
4
Similarly, in this example,
String s = "Ciao";
char c = s.charAt (3);
the
charAt()
method inside the string "Ciao" returns
the letter 'o'.
There are many useful methods in
String.
Here are some useful ones:
We can check equality using a method:
String s = "Ciao";
String r = "Halo";
if ( s.equals(r) ) {
System.out.println ("They're equal");
}
(Nothing gets printed)
Just as useful:
String s = "Hujambo";
String r = "hujambo";
if ( s.equalsIgnoreCase(r) ) {
System.out.println ("They're equal");
}
(They are equal, ignoring whether upper or lower case).
Some other useful ones:
String s = "Ni Hau";
if ( s.startsWith("Ni") ) {
System.out.println ("This string begins with \"Ni\"");
}
String r = "Konnichiwa";
boolean rhymesWithWa = r.endsWith ("wa");
int k = r.indexOf ('h'); // k is 6
String r2 = r.toLowerCase (); // Makes a lowercase version.
String r3 = r.substring (1,3); // Returns "on"
4.12 Reading and writing
First, reading.
Consider this snippet of code:
When reading a method call, do not at first
chase down the method to look at its body.
Instead, merely ask:
What does the method take as parameters?
What does the method return?
Here, we can tell that the parameters must be double's:
Although: we probably don't know the parameter variable names.
Similarly, we can tell what the return type must be:
Let's look at a method declaration:
Here, without knowing anything about how the method works,
we can see how it needs to be called, e.g.,
Of course, the method code now must return an array:
Next, writing.
The following examples show various writing styles.
Most commonly used: one or two spaces (in the Math.sqrt
examples above).
Sometimes, spaces between parameters makes the program
more readable, as in the last call to sum above.
There are also two fundamentally different styles in
writing method definitions, e.g.,
For the most part, we will prefer the former.
There are also different styles with regard
to the opening brace:
Again, we will prefer the former.
4.30 Exercise:
Now go back and read through the second
Vowel-Density program that we started the module with.
Examine how much
easier they are to read with your newfound understanding
of methods.
4.31 Video:
4.13 When things go wrong
Below, try to identify the errors first by reading,
then compile and run to see if you were right. Add a
main() method
where needed.
4.32 Exercise:
What is wrong with the program below?
How can you fix it
(in Ex4_32.java).
4.33 Exercise:
Identify the errors in the method below:
And fix it in Ex4_33.java.
4.34 Exercise:
What is the compiler error in the method below?
Fix the error in Ex4_34.java.
4.14 Meta
Read the following program carefully:
public class WellDone {
public static void main (String[] argv)
{
System.out.println ("CONGRATULATIONS!");
}
}
Seriously, you should feel good about coming this far.
And should reward yourself.
Let's point out a few things:
These four topics
loops,
conditionals,
arrays,
and methods,
form the core of basic programming.
They are hard to master and it's astonishing how challenging
it can get when you combine all of these.
In the old days, this was more or less
ALL there was to a programming
language (FORTRAN, if you've heard of it). You spent the rest
of your time writing more and more code for complex tasks.
Do not fret if you feel "shaky" about these topics now.
You get an ever increasing sense of mastery over time,
long after you've encountered more advanced topics.
For example, when you are doing, say, Unit-8, you can
come back and look at some of the exercises here. They will
seem quite easy.
Also, this is typically a moment when many students
encounter self-doubt, as in:
"I can't really do this. This is not for me".
"Why do I find this so hard?"
"I'm clearly not wired for computer stuff".
Here's the thing ... everyone finds it hard. It's no
different an experience in learning a foreign language or a musical
instrument.
If you are used to the comfortable learning in
a definitions/facts type of course, blame that on that type of course.
Skill learning is different. So, be aware of self-doubt and
do something concrete to ignore it.
For the moment, the best thing to do is to "stick with it".
Take a break and come back to some things that you didn't quite get.
The forthcoming review should also help.