On this page:
Overview
3.1 Capturing Repeated Work with Methods
3.2 Methods as Descriptions of Tasks
3.3 With Great Power Comes More Opportunity for Mistakes
3.3.1 The Argument Count must Match the Parameter Count
3.3.2 Types Must be Consistent
3.3.3 Types Must be Consistent, Round II
6.8

Lecture 3: Programming, Methodically

There are no related files for lecture

Overview
3.1 Capturing Repeated Work with Methods

In the past few sections, we’ve used Java as a calculator. For example, we calculated the distance of a falling object at several different points in time:

class ExamplesLec {
int gravity = 10;
int distAfter2sec = (this.gravity / 2) * (2 * 2);
int distAfter4sec = (this.gravity / 2) * (4 * 4);
int distAfter6sec = (this.gravity / 2) * (6 * 6);
}

We noted that it was useful to store the value of gravity in a field, and use this.gravity in its place. However, there’s still something frustrating about this program. In particular, we are repeating the shape of the computation over and over again. This shape is repeated for each of the three distAfter____ fields:

(this.gravity / 2) * (__ * __)

In all three cases, the this.gravity and the 2 part is the same, as is the arrangment of the / and * operators. The only thing that changes is the number of elapsed seconds. It would be useful if we could somehow capture the idea of this distance-travelled calculation and re-use that instead of repeating the multiplication-and-division pattern over and over.

Java (and every other modern programming language) has a facility for doing this. In Java, the programming feature is called a method. We can think of simple methods as calculations with holes in them, where the holes can be filled in with different values to complete the calculation, just like in a mathematical function. Here’s what it looks like to define a method for the distance-travelled calculation:

class ExamplesLec {
int gravity = 10;
 
int distanceAfter(int seconds) {
return (this.gravity / 2) * (seconds * seconds);
}
int distAfter2sec = this.distanceAfter(2);
int distAfter4sec = this.distanceAfter(4);
int distAfter6sec = this.distanceAfter(6);
}

The distanceAfter method has a few components.

In reality, there are a few other components we could include in a method definition, but they aren’t relevant yet.

These four components—name, parameter(s), method body, and return type—make up a method definition. We’ll see many more examples of methods over the rest of the quarter, and starting in this section.

In addition to the method definition being added, the example above has another change. Each of the distAfter____ field definitions has its right-hand side changed to a use of the this.distanceAfter method name, followed by the number of elapsed seconds in parentheses. We call this type of expression a method call.

We can run this program, and see that we get the same answer as before:

ExamplesLec:

---------------

 

 new ExamplesLec:1(

  this.gravity = 10

  this.distAfter2sec = 20

  this.distAfter4sec = 80

  this.distAfter6sec = 180)

---------------

This is a simple explanation of how method calls work that will be useful for now. We’ll need to refine it in future lectures.

So what’s happening to make this work? The key is the interaction between the method definition and the method call. When Java evaluates the method call, we can think of it as performing the calculation after return in the method body, with the values given in the method call used in place of the parameter name. So in the case of the first method call:

this.distanceAfter(2);

The calculation that is performed is actually:

(this.gravity / 2) * (2 * 2);

Which is the method body where all uses of seconds have been replaced by 2. We say that we used the value 2 as an argument, which became the value of the parameter seconds.

The same rule is used to evaluate the next two method calls, which are

this.distanceAfter(4);
this.distanceAfter(6);

and perform the calculations:

(this.gravity / 2) * (4 * 4);
(this.gravity / 2) * (6 * 6);

There are several reasons it is useful to write this program with a method definition and method calls rather than doing three different calculations directly (as in the original version).

Do Now!

Change the distanceAfter method to calculate the answer using the cube of seconds, rather than the square of seconds.

3.2 Methods as Descriptions of Tasks

Methods (and functions, a similar construct in other languages) are fundamental to programming. As a result, a lot of the programs we write for the rest of the quarter will be specified by writing one or more methods. We’ll develop ways of describing methods, and workflows for defining and testing them to make sure they work the way we expect. One of the important things methods do is describe the input(s) and output of some computation. This includes the types of inputs and outputs; above, the distanceAfter method had int inputs (seconds) and a int output (the value returned). This helps us use methods as a way to take a concrete problem statement, and turn it into a program.

Mad Libs are a word game that I enjoy.

Let’s take another example, that of building up a “Mad Lib”. We might have the following English description of what we want to do:

Write a program that takes in an adjective and a number, and fills them into the template: “The professor’s explanation was <adjective>, probably because he’s been teaching for <number> years.”

We can take this description, and use it to derive a method that accomplishes the goal it sets out. First, we need to figure out the inputs and outputs from the problem description. The problem is expecting two inputs: an adjective, which can be represented as String data, and a number which can be represented as an int. The result of the method ought to be a String – the result of combining the template string with the two input values. This tells us that the method should have a return type of String, and two parameters, one with type String and one with type int. Along with a name for the method, which we’ll call fillIn (since we’re filling in a sentence), that gives us the first part of the method definition:

String fillIn(String adjective, int number) {
// Still need to fill in this method body }

Here I also picked names for the parameters, adjective and number, which were straightforward to pick given the problem statement. We call this part of a method definition—the part with the return type, the name, and the parameters—the method header. It describes the behavior of the method in terms of types. Finally, this is an example of defining a method that has more than one parameter, in this case two. The two parameters, adjective and number, are separated with a comma, and both written with the corresponding type in front of them.

It’s also useful to put a comment before the method describing the result that will be produced:

/* Places the given adjective and number into the sentence template to create a full sentence. */
String fillIn(String adjective, int number) {
// Still need to fill in this method body }

Next, it’s useful to come up with some examples of using the method before filling in the method body. This helps us double-check our work once we implement the body, because we’ll have some tests ready to run. We should pick meaningful inputs that match the types of the parameters:

class ExamplesLec {
/* Places the given adjective and number into the sentence template to create a full sentence. */
String fillIn(String adjective, int number) {
// Still need to fill in this method body }
 
String example1 = this.fillIn("useless", 2);
String example2 = this.fillIn("relevant", 3);
String example3 = this.fillIn("wise", 22);
 
}

Now, with all this in place, we can fill in the method body, using the names adjective and number for the different values that will be filled in:

class ExamplesLec {
/* Places the given adjective and number into the sentence template to create a full sentence. */
String fillIn(String adjective, int number) {
return "The professor's explanation was " + adjective +
", probably because he's been teaching for " + number + " years.";
}
 
String example1 = this.fillIn("useless", 2);
String example2 = this.fillIn("relevant", 3);
String example3 = this.fillIn("wise", 22);
 
}

If we run this, we see the examples print out as expected:

ExamplesLec:

---------------

 new ExamplesLec:1(

  this.example1 =  "The professor's explanation was useless, probably because he's been teaching for 2 years."

  this.example2 =  "The professor's explanation was relevant, probably because he's been teaching for 3 years."

  this.example3 =  "The professor's explanation was wise, probably because he's been teaching for 22 years.")

We just went through a several-step process for thinking through a method definition:

In this case, we got it right on the first try. Often, the last step won’t give us the result we expect. We might get a type or syntax error, or the method might produce an answer we didn’t intend on. When this happens, we need to not panic, and go back through the steps and check if we had a wrong expectation, or if the method body has a mistake, or if we misunderstood the problem.

The books How to Design Programs and How to Design Classes describe several different design recipes in great detail.

This is a useful design recipe for methods that work with simple data. You should use this design recipe when you’re writing methods that just manipulate Strings, ints, and other simple values.

Exercise

Use the design recipe to write a method for the following problem: Calculate the weekly pay for an employee given their hours worked and their hourly rate, given that any hours over 40 are counted at double the hourly rate. Assume that the employee worked over 40 hours.

This is a useful method because if written correctly and used for all the examples, it will ensure that the textbook doesn’t have any arithmetic mistakes in the examples!

Exercise

Use the design recipe to write a method for the following problem: Imagine you’re writing a program to help write a textbook for math. Write a method that takes in two numbers, and produces a string that shows the multiplication expression and the result separated by an equals sign. For example, for the inputs 4 and 5, the output should be “4 * 5 = 20”.

3.3 With Great Power Comes More Opportunity for Mistakes

In the first lecture, we saw how we needed to be careful with syntax to avoid syntactic mistakes. In the second, we saw that types could be mismatched, causing errors. Now that we’ve introduced methods, we have several new errors that can come up. It’s useful to see them in a controlled environment and understand their cause.

3.3.1 The Argument Count must Match the Parameter Count

In the fillIn example, we always called the fillIn method with two arguments. What if we don’t, and instead call it with just one?

class ExamplesLec {
/* Places the given adjective and number into the sentence template to create a full sentence. */
String fillIn(String adjective, int number) {
return "The professor's explanation was " + adjective +
", probably because he's been teaching for " + number + " years.";
}
 
// Instead of String example1 = this.fillIn("useless", 2); try this: String example1 = this.fillIn("useless");
 
}

If we run this, we get the following error:

ExamplesLec.java:12: error: method fillIn in class ExamplesLec cannot be applied to given types;

  String example1 = this.fillIn("useless");

                        ^

  required: String,int

  found: String

  reason: actual and formal argument lists differ in length

1 error

The key part of this error is the line starting with “required” which is telling us that the fillIn method was specified, in its method header, to take two parameters (what Java calls “formal arguments”). In the method call, only one argument (what Java calls “actual arguments”) was provided. Java will refuse to run this program, because it is nonsense to use a method with the wrong number of arguments.

Do Now!

Change the example so the method call uses three arguments instead of one. What error do you get?

3.3.2 Types Must be Consistent

We saw in the last lecture that Java will complain if we try to mix values of different types in certain ways. This comes up again in the context of method definitions and method calls. For instance, if we have a mismatch between the method body and the return type, we can see an error occur:

class ExamplesLec {
/* Places the given adjective and number into the sentence template to create a full sentence. */
// NOTE -- I changed String to int in the return type below int fillIn(String adjective, int number) {
return "The professor's explanation was " + adjective +
", probably because he's been teaching for " + number + " years.";
}
 
 
}

This gives the following error when run:

ExamplesLec.java:9: error: incompatible types: String cannot be converted to int

        ", probably because he's been teaching for " + number + " years.";

                                                              ^

1 error

This is a somewhat confusing place for the error to show up! We might be misled into trying to change something about the use of + or the body of the method here. However, the error is different. This is an important lesson: The location that Java reports in the error message is not always the location that needs to be fixed.

Let’s repeat that, because it’s very important:

The location that Java reports in the error message is not always the location that needs to be fixed.

No, seriously, let’s really make sure that we don’t gloss over this important point.

The location that Java reports in the error message is not always the location that needs to be fixed.

I’m not kidding:

In fact, we could make this a bit more general by saying Every programming language will sometimes give error messages that are misleading at best.

The actual error here is that the body of the method is returning a String, and the return type says that the method must return an int. Java reports an error because of this mismatch, but it doesn’t have any information about whether we made a mistake in the method body, or if we wrote down the wrong return type. It’s simply reporting that there’s an inconsistency.

This means it is crucial that when we see an error message, we carefully read the area that’s pointed to by the error, but also that we keep in mind that Java, when reporting errors, is not particularly sophisticated. Recall that the error we introduced here is a mismatch between the return type and the method body, and the fix is to change the return type back to String. You might think “that’s obvious, we just made that small change.” However, it’s quite easy to make a small mistake or typo, and then be led astray by the error messages. This is why it’s very important to have tools like the design recipe, so we can go back and check our work with our own process, rather than relying on error messages that aren’t always reliable indicators of our mistake.

3.3.3 Types Must be Consistent, Round II

In addition to checking that the return type is consistent with the method body, Java will also check that any arguments passed to the method are consistent with the types of the parameters. So if we, for example, try to pass a int value in place of a String when calling fillIn, we will also see an error:

class ExamplesLec {
/* Places the given adjective and number into the sentence template to create a full sentence. */
String fillIn(String adjective, int number) {
return "The professor's explanation was " + adjective +
", probably because he's been teaching for " + number + " years.";
}
 
// Instead of String example1 = this.fillIn("useless", 2); try this: String example1 = this.fillIn(1, 2);
 
}

This produces the error:

ExamplesLec.java:12: error: incompatible types: int cannot be converted to String

  String example1 = this.fillIn(1, 2);

                                ^

This is because the first parameter of fillIn, adjective, is specified to have type String. The value 1 doesn’t match that (it’s a int), so an error is reported. In this case, Java is providing a helpful error message (rejoice!). Just remember that this won’t always be the case (and it’s not just Java’s fault; no language gives perfect error messages in all cases).

Exercise

Are there any other new errors you can think of that come up due to method definitions and method calls? What if you wrote a parameter type incorrectly? What if you mis-type a method name? Try to break a known-good program in small ways, and understand the messages that result.