Lecture 02: Java review: Control flow

Announcements

Garrett’s office hours are Wednesday at 2:30 in LGRT 220. My office hours are Wednesday 10:00–12:00 in CS 318. Please come, but don’t expect to just open your laptop and say, “my program doesn’t work, can you help me find the problem?” Be ready to tell us what you’ve tried and why you’re stuck.

If you added the course late (or you’re thinking of adding the course and want access to the Piazza and Gradescope sites during add/drop), you need to tell me. SPIRE doesn’t notify me when you add, and it doesn’t show me the add date. Come see me after class and I’ll get you set up.

Similarly, if you add late, you need to see me to be excused from the in-class exercise you missed.

Garrett graded the first in-class exercise, and the grades are up on Moodle. A couple notes:

  • Please write your name clearly at the top of the page, and consider putting your student ID there too, especially if the name you write and the name Moodle is set up to use aren’t the same.
  • Speaking of handwriting: try to write clearly on things we grade. Especially come quiz and exam time, you’re gonna have a bad time if we can’t read your writing. If your writing is bad and ambiguous, we’re going to mark things wrong and not accept “but it really says 1, not L!” if you dispute the grade.

Grade replacement: If you previously took 187, decided to leave the CS major (but maybe pursue informatics) and are in this course, you may be eligible to have your 187 grade replaced with this one, assuming you do better here than in 187. If you think you might be interested, send email to upd@cs.umass.edu and let our undergrad program director know; we’re building a list of students who might want this to happen.

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.

Or, we could execute a sequence of statements zero, one, or more times, as in the while, do/while or for statements. Within these statements, we might break out of the lop

Or, we could throw an exception, unwinding the stack of methods 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 = y + z;
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 = y + z;
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).

Your other option is a switch statement, which evaluates an expression then jumps to a branch that’s labeled with the matching value:

int areaCode = getAreaCode();

switch (areaCode) {
  case 413:
    System.out.println("WMass!");
  case 617:
    System.out.println("Boston!");
}

But this might not do what you think. Control falls through from each matching statement to the next! You need to insert a break to tell Java you want control to exit the switch statement.

int areaCode = getAreaCode();

switch (areaCode) {
  case 413:
    System.out.println("WMass!");
    break;
  case 617:
    System.out.println("Boston!");
    break;
}

If there is no matching value, nothing happens. Unless you use the default: label, which will be matched if no other is:

int areaCode = getAreaCode();

switch (areaCode) {
  case 413:
    System.out.println("WMass!");
    break;
  case 617:
    System.out.println("Boston!");
    break;
  default:
    System.out.println("What? There are other area codes?");
}

Note that a switch statement can always be rewritten as a sequence of if/else statements, but sometimes switch is clearer.

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 == true)
  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()).

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 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):

while (true) {
  i = i + 1;
  if (!isEligible(words[i])) {
    continue;
  }
  doSomethingWith(words[i]);
}

If you know you want to run the body of the loop at least once, you can use the (much less common) do/while statement:

do {
  something()
} while (!isDone());

This is exactly equivalent to:

something();
while (!isDone()) {
  something();
}

Though the latter is slightly longer, it’s almost always a better idea, unless you’re 100% sure you really want to execute the loop body at least once. Otherwise you’ll run into errors that only show up in odd circumstances, for example, only when an input file is empty, or an array is of length 0, or the like.

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. So you can write something like:

void printInts(int[] theInts) {
  for (int anInt: theInts) {
    System.out.println(anInt);
  }
}

There is another way to write a for loop, where 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)?

But you when do need to manually increment a value, for loops like the one above (though with a correct termination condition!) are the way to do it.

In Java8, there is also new syntax for “lambda expressions” (which sounds scary, but means, in essence, “one line methods”) that you can use with a forEach method. We may see this later, but for now we’ll skip it.

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.

In-class exercises

Reminder: Clearly write your name(s) and optionally student IDs on your paper!

  1. Write a method which given an array of ints, returns as a double the average of the values in the array.
  2. Write a method which given an array of ints, returns as an int the sum of the values stored at odd indices in the array.

Hopefully the only tricky bit about the first method was remembering that if you divide an int by another int, you get truncated division – Java throws away the fractional part (if any) of the result. You need to make sure one of the operands of the division is a double, either by declaring it as one (double total = 0.0;), or by casting it as one (return (double)total / numbers.length;).

As a student pointed out in class, the second exercise is not well-specified: What should it return if the array only has one element (and thus, no elements at odd-numbered indices)? Most people’s implementations returned 0, which is fine.

Here are two possible solutions, though as we showed in class, even this toy problems can be approached in many different (and correct) ways:

double average(int[] numbers) {
  double total = 0.0;
  for (int number: numbers) {
    total = total + number;
  }
  return total / numbers.length;
}
int sumOddIndices(int[] numbers) {
  int total = 0;
  for (int i = 1; i < numbers.length; i = i + 2) {
    total = total + numbers[i];
  }
  return total;
}