02: Java review: Control flow
Welcome
Welcome to people of all ages, all colors, all cultures, abilities, sexual orientations and gender identities.
Welcome to those who identify as biracial, multi-racial, or multi-ethnic.
Welcome especially to those of you who added the course late. Please be sure to come talk to me or email me if you haven’t already!
Announcements
Are you a new add to the course? You need to get in touch with me immediately.
Java and control flow
Control flow is the order in which statements in an imperative language are executed. By default, the program starts executing at the main
method, and goes line by line, executing a sequence of statements. In Java, this default is overriden in one of several ways:
A method could be invoked. This unconditionally sends the flow of control into that method. Similarly, we can return
control from a method to the method that invoked it.
Or, the flow of control could branch conditionally. Most commonly we think of if[/then]
and if[/then]/else
, but Java also also supports the switch
statement as well. Probably you should never use the switch
statement – let us never speak of them again! (Well, not much, anyway.)
Or, we could execute a sequence of statements zero, one, or more times, as in the while
(or do/while
, which we’ll also neglect) or for
statements. Within these statements, we might break
out of the loop, or return
from the entire method.
Or, we could throw
an exception, unwinding the stack of method calls until either the exception is caught (in other words, control resumes in the exception handler’s except
clause) or the main
method is reached, in which case the JVM exits and outputs an error message. NullPointerException
anyone?
There are a few other ways control flow can by modified, but the above hits most of the highlights.
Let’s talk about each of them in turn.
Method invocation
Method invocations usually looks something like method(parameter1, parameter2)
, in other words, the name of a method followed by its parameters (either variables or literal values). If there is no preceding variable name, e.g., anObject.method()
, the method is resolved first in the context of the current object, then as a static method. Otherwise, the method is resolved in the context of the leading object name. You can also invoke static methods of other classes by prefixing with the class name; more about this in a bit.
return
, no matter where it is, exits the current method. For example, you can return
from within a loop and the loop will terminate immediately.
Conditional branches
if[/then]/else
statements evaluate an expression as a boolean, then branch one way if it is true. If there’s an else
, the other way is followed – but with no else
and an expression that evaluates to false
, the statements are just skipped over.
How does Java delimit which statements are part of the then
or else
? It executes only the following statement. “But Marc!” Yes, I know, curly braces.
If you enclose a sequence of statements in curly braces, you have created a block. In terms of control flow treats a block as a single statement, so that you can, for example, branch to a sequence of statements after an if
. Blocks also serve the purpose of defining a lexical scope, as we mentioned last class.
Speaking of, the fact you don’t need to declare a block using {}
after an if
statement leads to one of the most common kinds of bugs that we see in first- and second-year programming classes. Students often write code that looks like:
x = 5;
if (someCondition)
x = x + 10;
else
x = x - 10;
System.out.println(x);
And on its face, that’s fine. The problem is when you later realize you need to modify the stuff that happens on the basis of someCondition
to include more statements:
x = 5;
if (someCondition)
x = x + 10;
else
x = x - 10;
x = x * 2
System.out.println(x);
Do you see the problem? Just because you indent the code, and things look like a block, it doesn’t mean they are. Experienced programmers will insert the {}
always, so that if later they modify the branch, things work correctly. Many style guides (e.g., Google’s) require this, and IDEs like Eclipse can be set to enforce it (either through warnings or errors).
In-class exercise
boolean someCondition = true;
int x = 10;
if (someCondition)
x = x + 2;
else
x = x - 2;
x = x / 2;
System.out.println(x);
What’s the output?
boolean someCondition = false;
int x = 10;
if (someCondition)
x = x + 2;
else {
x = x - 2;
x = x / 2;
}
System.out.println(x);
What’s the output?
Loops
Sometimes you want a statement to run zero or more times, as the result of a conditional. You want a while
loop.
while (someCondition)
doTheThing();
Of course, in the above example, doTheThing()
had better change someCondition
at some point, or the loop will never terminate (through normal means, anyway – it could throw an exception or otherwise exit the program, say, via System.exit()
).
Also, why don’t we check if someCondition == true
? Because we don’t need to: the if statement is checking if the condition is true already; there’s no need to duplicate the code here.
Just like if
statement branches, it’s a super-good idea to always enclose loop bodies in a {}
delimited block.
Note that there are two other ways, from within a loop, that we can change control flow. If we want to, we can immediately exit a loop with a break
statement:
while (true) { // if you ever see this, you better see some way out of the loop, too
boolean amDone = doTheThing();
if (amDone) {
break;
}
}
return
also exits loops.
In-class exercise
boolean done = false;
int x = 1;
while (!done) {
x = x * 2;
if (x > 10)
break;
}
Will this loop terminate?
a. yes b. no c. not enough information to decide
In addition, you can use the continue
statement to return to the “top” of a loop (in a while
, it forces re-evaluation of the condition):
int i = 0;
while (true) {
i = i + 1;
if (i < 10) {
continue;
}
System.out.println(i);
break;
}
This will repeatedly increment i
and not reach the println
until i == 10
; then it will print i
and exit the loop. What would happen if we removed the break
?
In-class exercise
int i = 0;
while (i < 10) {
if (i % 2 == 0) {
System.out.println(i);
continue;
}
i = i + 1;
}
What will this loop do?
Finally, there’s the for
loop.
IMHO, when possible, the best way to use a for
loop is with the iterator syntax. Arrays (and more importantly, container types that implement the Iterable
interface, support the iterator protocol. If you care about the contents of an array (or any iterable), but not about the indices, then you can use the enhanced for loop.
Suppose you have an array you want to iterate over and do something to, like print its members. You can do it the “old fashioned” way:
int[] array = {1, 3, 3, 7};
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
Or you could use the enhanced for
loop:
int[] array = {1, 3, 3, 7};
for (int e : array) {
System.out.println(e);
}
Notably, the enhanced for
loop prevents you from making indexing errors – it guarantees that it will visit each element of the array exactly once, assigning it to the iterator variable you provide each time through the loop.
The other way (the “old fashioned” way) is more general: you specify an initial state, a termination condition, and an update statement. Unfortunately this is prone to errors:
void printInts(int[] theInts) {
for (int i = 0; i <= theInts.length + 1; i++) {
System.out.println(theInt[i]);
}
}
See the error(s)? Hard to follow exactly what’s going on with that termination condition, huh?
In-class exercise
int sum(int[] array) {
int total = 0;
for (int i = 0; i <= array.length; i++) {
total = total + array[i];
}
return total;
}
What will this method return?
Back to for
loops
But there are at least two major times when this style comes in handy.
The first is when you need access to the index, not just the element.
And the second (which is related) is when you need to compare different elements within the same array. For example, let’s say you wanted to write a method to check if an array of int
s was sorted. You’d need to make sure each element was less than or equal to the element after it (and make sure you didn’t check anything past the end of the array!):
boolean isSorted(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
return false;
}
}
return true;
}
In either style of for
loop, you can, just as in while
loops, modify the flow of control with break
, continue
(which performs the update and checks for termination), and return
.
There are a few common patterns of for
loops you should learn about and master, as they’ll make writing for
loops easier for you. We’ve seen some of them already, but let’s talk about patterns. Here they are:
Doing something to every element of an iterable: Suppose we have an array of Things, and we want to doSomething to each one. The enhanced for loop lets us do so in a straightforward manner:
void doAll(Thing[] array) {
for (Thing t: array) {
doSomething(t);
}
}
Testing that everything in an iterable satisfies a property:: Let’s say we want to check if every element in an array is even. The general idea here is to check each element one-by-one. As soon as one thing violates the property (that is, is odd), we can return false. If we get to the end, we can return true:
boolean allEven(int[] array) {
for (int e : array) {
if (e % 2 == 1) {
return false;
}
}
return true;
}
Another example is a linear search, where we scan every element of an iterable and do a thing if an element is found. We do a different thing if it’s not found:
boolean containsValue(int[] array, int value) {
for (eint e : array) {
if (e == value) {
return true;
}
}
return false;
}
The pattern is the same; the test and the return values differ.
Or if we wanted to see if a String contained a particular letter we can iterate over its contents. Probably the easiest way to do this is using the charAt
member method of String
, as String
s don’t directly support enhanced for loops:
boolean containsChar(String s, char c) {
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) {
return true;
}
}
return false;
}
Again, the pattern is the same; the test and the return values differ.
Comparing elements in an array for some property: We saw this already in the isSorted
method. Here, we cannot easily use the enhanced for loop, since we need access to the index (to check the current against the next value):
boolean isSorted(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
return false;
}
}
return true;
}
The pattern is similar to before, but we need to sure we access the right elements of the array, and we need to be careful with bounds. You could rewrite it as:
boolean isSorted(int[] array) {
for (int i = 1; i < array.length; i++) {
if (array[i - 1] > array[i]) {
return false;
}
}
return true;
}
but I find the former more natural.
Exceptions and other abnormal exits
We’ll cover these more later – for the first part of this course, I won’t be asking you to write code in this class that needs to throw or handle exceptions. We’ll return to them in more detail when the need arises.
End of class reminders
Homeworks generally due every class, usually posted by the end of day the previous class. Don’t forget!
There’s usually a programming assignment due Fridays, including tomorrow.
There’s a quiz Monday in discussion, don’t miss it!