Lecture 4: Programming, Decisively
Overview
The boolean type - values representing choices
Methods that make decisions
Making more than one decision
Combining comparisons
4.1 Beyond Arithmetic and Concatenation
So far, we’ve used Java to do things a calculator could do, with the notable exception of Strings. We introduced methods mainly as a way to enhance our calculator-like behavior. In this lecture, we’ll go further, and start doing work that’s unique to computation – making decisions based on data.
4.1.1 booleans and Comparing Numbers
We’ve used +, -, /, and * to perform arithmetic on numbers (and in the case of +, append Strings). Mathematically, there’s a natural next set of operations to try on numbers – comparing them! Operators like < have meaning in math, and they also have meaning in Java. We can try them out:
class ExamplesLec { boolean fourIsLessThanFive = 4 < 5; boolean fiveIsLessThanFour = 5 < 4; }
Here, we’re using the < operator to compare 5 and 4. If we run it, we get:
ExamplesLec: |
--------------- |
|
new ExamplesLec:1( |
this.fourIsLessThanFive = true, |
this.fiveIsLessThanFour = false) |
The result says that fourIsLessThanFive has the value true, and fiveIsLessThanFour has the value false. This seems quite reasonable; the first is, after all, a true statement, while the second isn’t. These two values, true and false, have the type boolean; in fact, they are the only two values that have the type boolean. They represent computations that ask a question, where the answer can be yes (true) or no (false).
4.1.2 Making Decisions
To see how these new values are particularly useful in action, let’s go back to an example we’ve worked on before – calculating weekly pay with overtime. The past times we’ve addressed the problem, we’ve always made the assumption that the number of hours worked is greater than 40, because the calculation would produce nonsense for smaller numbers of hours. Armed with the ability to ask questions, along with one more piece we’ll add along the way, we’ll be able to make progress on this problem now. Let’s clearly state the problem:
Use the design recipe to write a program that, given an employee’s weekly hours worked and hourly wage, calculates their weekly pay. Any hours worked over 40 should count at double their hourly rate. An employee may work less than 40 hours a week.
First, we need to write the method header, which is the same as before:
int weekly(int hours, int rate)
We can add documentation:
// Calculate weekly pay at the given rate and number of hours worked. // If hours is above 40, pay at double the rate for hours beyond 40. Pay at // rate for the first 40 hours. If the number of hours is less than 40, don't // add any overtime. int weekly(int hours, int rate)
And it’s good to next think through some examples:
// Calculate weekly pay at the given rate and number of hours worked. // If hours is above 40, pay at double the rate for hours beyond 40. Pay at // rate for the first 40 hours. If the number of hours is less than 40, don't // add any overtime. int weekly(int hours, int rate) { } int exactly40 = this.weekly(40, 10); // Should be 400 int someOvertime = this.weekly(45, 10); // Should be 400 + 100, total 500 int lessThan40 = this.weekly(30, 20); // Should be 600
With that, we just need to work on the method body. We know now that we can compare numbers, but that on its own isn’t enough to solve the problem. We need to have the program make a decision based on the result of the comparison. To do this, we’re going to introduce a new kind of syntax that we can use in method bodies: the if statement. An if statement takes the form
if(someBooleanCalculation) { ... someResultIfTrue ... } else { ... someResultIfFalse ... }
In our case, the boolean calculation we need to check is if the number of hours is greater or less than 40, which we get from the problem description. So we can start the method body with:
// Calculate weekly pay at the given rate and number of hours worked. // If hours is above 40, pay at double the rate for hours beyond 40. Pay at // rate for the first 40 hours. If the number of hours is less than 40, don't // add any overtime. int weekly(int hours, int rate) { if(hours > 40) { // ... answer if greater than 40 ... } else { // ... answer if less than or equal to 40 ... } } int exactly40 = this.weekly(40, 10); // Should be 400 int someOvertime = this.weekly(45, 10); // Should be 400 + 100, total 500 int lessThan40 = this.weekly(30, 20); // Should be 600
Now, we can use what we know about getting results from methods by using return to fill in the two comments above:
// Calculate weekly pay at the given rate and number of hours worked. // If hours is above 40, pay at double the rate for hours beyond 40. Pay at // rate for the first 40 hours. If the number of hours is less than 40, don't // add any overtime. int weekly(int hours, int rate) { if(hours > 40) { return 40 * rate + ((hours - 40) * (rate * 2)); } else { return hours * rate; } } int lessThan40 = this.weekly(30, 25); // Should be 750 int exactly40 = this.weekly(40, 10); // Should be 400 int someOvertime = this.weekly(45, 10); // Should be 400 + 100, total 500
We’re going to introduce names for the different parts of the if statement. We call the expression in between the parentheses (in the above example, it’s hours > 40) the conditional part. The two parts between the curly braces we call the branches or cases of the if statement. The first is called the then branch, and the second is called the else branch. This helps us say what happens in English – The if statement evaluates the then branch if the condition is true, and the else branch if the condition is false.
Let’s see that definition in action. When we introduced methods, we said that to evaluate a method call we need to do the calculation in the body of the method with the parameters replaced with the argument values. So in the first example above, hours would be replaced with 30, and rate would be replaced with 25. That would look like:
if(30 > 40) { return 30 * 25; } else { return 40 * 25 + ((40 - 30) * 25); }
The condition evaluates to false, because 30 isn’t greater than 40. That means Java will evaluate the else branch, and ignore the then branch. This makes the result (or return value) for this method call be 750, the result of 30 * 25.
4.2 More Examples of Decision-Making Programs
There are lots of useful programs that make decisions based on the values of numbers. For example, one handy function is max, which takes in two numbers and returns the larger one. It’s method header and documentation are:
// Returns the larger of num1 and num2 int max(int num1, int num2) { // need to fill this in } int maxLeft = this.max(5, 4); // should be 5 int maxRight = this.max(6, 7); // should be 7 int maxSame = this.max(3, 3); // should be 3
Here, the method body has a similar shape to the weekly pay method above, but instead of comparing the numbers to a constant like 40, we need to compare the two parameters to see which one is larger. So max looks like:
// Returns the larger of num1 and num2 int max(int num1, int num2) { if(num1 > num2) { return num1; } else { return num2; } }
Exercise
Implement absolute, which takes a number and returns its absolute value.
4.2.1 More Conditions
Sometimes we can’t capture everything we need to know with a single comparison, like in max and weekly. For example, consider the program gradeForNumber, which takes a number representing a score out of 100, and returns a letter from "A" to "F" representing a grade. Let’s set it up using the design recipe:
// Returns A if the score is over 90, B if 80-89, C if 70-79, D if 60-69, F if below 60 String gradeForNumber(int score) { // Fill in here } String aGrade1 = this.gradeForNumber(95); // Should be "A" String aGrade2 = this.gradeForNumber(90); // Should be "A" String aGrade3 = this.gradeForNumber(100); // Should be "A" String bGrade1 = this.gradeForNumber(80); // Should be "B" String cGrade1 = this.gradeForNumber(72); // Should be "C" String fGrade1 = this.gradeForNumber(55); // Should be "F"
If we try to use the structure above, we can easily figure out how to return "A", but it’s less clear what to do in the else branch:
String gradeForNumber(int score) { if(score >= 90) { return "A"; } else { // What to do here? } }
It turns out we can write more than just two branches for an if statement. Instead of just having an else branch, we can add more branches, each with its own condition:
String gradeForNumber(int score) { if(score >= 90) { return "A"; } else if(score >= 80) { return "B"; } else if(score >= 70) { return "C"; } else if(score >= 60) { return "D"; } else { return "F"; } }
There are a few things to note here:
The way this extended kind of if statement evaluates is by checking each condition in order, and evaluating the branch corresponding to the first branch that is true. So for the input 72, first the value 72 is compared to 90, then to 80, then finally to 70, where the comparison returns true, so return "C" is evaluated. Note that even though the condition score >= 60 is also true here, Java doesn’t return "D", because that comparison came after.
Java requires that we always make it obvious that something of the right type is being returned. If we change the program slightly:
String gradeForNumber(int score) { if(score >= 90) { return "A"; } else if(score >= 80) { return "B"; } else if(score >= 70) { return "C"; } else if(score >= 60) { return "D"; } else if(score < 60) { return "F"; } } we get an errror:
javac -cp ../../../lib/tester.jar:. ExamplesLec.java
ExamplesLec.java:19: error: missing return statement
}
This is because Java can’t figure out that the method will always return some String here just by looking at the method body. So, it complains that it can’t find a return. If we use just else, like we did above in the first definition, Java can rely on the fact that the else branch runs if no condition evaluated to true, so something will for sure be returned.
Exercise
Change the program so that the condition and branch for the "D" case comes before the condition and branch for the "C" case. With this change, is there any argument that could be supplied for score that would make the method return "C"?
Exercise
Write this method in the “opposite” order, where the check for the "F" case comes first, and the check for the "A" case comes last?
4.2.2 More Comparisons
Sometimes, we need to combine more than one comparison result. For example, in this class (and many classes) you need a passing average and over half the credit on the final. To write a function that calculates whether a student passed or not, we need to consider both of these criteria. There are two operators we can use for combining booleans, && and ||. The && operator, pronounced “and”, evaluates to true if both operands are true, and false otherwise. In contrast, ||, pronounced “or”, evaluates to true if either or both operand(s) is true, and false otherwise. Here are examples of their use:
boolean andExample1 = true && true; // evaluates to true boolean andExample2 = true && false; // evaluates to false boolean andExample3 = false && true; // evaluates to false boolean andExample4 = false && false; // evaluates to false boolean orExample1 = true || true; // evaluates to true boolean orExample2 = true || false; // evaluates to true boolean orExample3 = false || true; // evaluates to true boolean orExample4 = false || false; // evaluates to false
With this in mind, we can implement a method passing that takes two numbers, one representing an overall average and another representing the score on the final, and returns whether or not they make up a passing performance.
// Returns true if the grades are sufficient to pass, false otherwise boolean passing(int overallAverage, int finalScore) { return (overallAverage >= 60) && (finalScore >= 50); } boolean goodGradeBadFinal = this.passing(80, 40); // should be false boolean goodGradeGoodFinal = this.passing(80, 60); // should be true boolean badGradeGoodFinal = this.passing(40, 80); // should be false boolean badGradeBadFinal = this.passing(40, 40); // should be false
Note here that the method is returning a boolean value. This can be useful for representing programs that give the answer to a yes/no question. This method doesn’t use if, since the yes/no answer is represented directly by the boolean value returned.
4.3 In Summary
We introduced several new ideas in this lecture:
The boolean type – there are two values true and false that have the type boolean, and are useful for representing computations that ask yes/no questions.
Comparison Operators – <, >, ==, <=, >= are all operators that take two numbers and return true or false depending on if the relationship holds.
if statements, which are comprised of conditions and branches. They use booleans to select which of several expressions to evaluate.
Boolean Operators –&&, || are pronounced “and”, “or”, and combine booleans according to specific rules. These let us express conditions that use combine several different answers.