Introduction |
Statements are like complete imperative sentences in Java: each commands
Java to perform some action.
Just as we said that Java evaluates expressions, we say that Java executes
statements.
We already have studied the Java declaration statement, which declares
variables (and optionally initializes them).
We will classify many statements as control structures: such statements
control (conditionally/by repetition) the execution of other statements.
In this lecture we will first learn how to write the simplest kind of statement in Java, the expression statement, and the simplest control structure in Java, the block statement, We will also begin discussing two analysis tools: hand simulation via trace tables, and statement boxing (which is the statement equivalent of oval diagrams for expressions). Then we will learn about Java's most important control structures, starting with if statements for decisions and simple for/break statements for looping. We will extend our analysis tools, trace tables and boxing, to cover these statements. Eventually we will generalize the for loop and cover two variants of looping: while and do statements. Finally, we will begin learning about Java's try-catch statement: a control structure that programmers use to process exceptions. Sometimes when an operator or method fails by throwing an exception, it does not denote a failure of the program, but is just a signal to the program that it must take some special action. We will continue exploring the use of the try-catch statement in our next lecture, in the context of file I/O. Thus, we can summarize the language features that we learn in this lecture by
    statement <=
local-declaration-statement |
We will explore the semantics of the following control structures in detail.
|
Declaring Variables (review and extension) |
We have already discussed most aspects of the
declaration-statement
in depth.
Recall that the simplest declarations start with the type of the variable,
followed by the name of the variable, ended by a semicolon.
In more complex variable declarations, we can specify multiple variable
names (each declared to be the same type, the one that starts the
declaration), and each variable can be initialized to a value.
We originally specified the EBNF for each declarator as     variable-declarator <= identifier [=expression] and said that expression could be only a literal (the only kind of expression we knew at that time). Now we know about much more complicated expressions, including literals, variables, operators, and methods; we can use all these to declare the initial values for variables. Pragmatically, most intializations use either literals or calls to the Prompt methods, but other form arise less frequently. Here are some examples. int a = 0; int b = Prompt.forInt("Enter b"); int c = b; int d = Math.max(c,1) + 1; Here the declation of a is intialized using the literal 0; the declation of b is initialized to the result returned by calling the Prompt.forInt method; the declation of c is initialized using b, the value of a previous declared and intialized variable; finally, the declation of d is initialized to the result returned by an expression involving a method call, operator, literal, and the value of a previously declared variable (c). In addition, Java allows the keyword final to appear optionally in     local-variable-declaration-statement <= [final] type variable-declarators ;
Semantically, if such a declaration includes final, then all the
variables that it declares must include an initializer and the value they
store must never be changed.
That is, we cannot use any state-change operators on final variables.
If we declared
|
Expression Statements |
We can make an expression into expression statement by
appending a semicolon at its end.
Such a statement tells Java to evaluate the expression.
The EBNF rule for expression statements is simply
expression-statement <= [expression] ; In fact, by discarding the option, we can write just the semicolon as the simplest kind of expression statement: it is a statement that "does nothing". But Java imposes one important syntax constraint on expression statements: if the expression option is included
average = (score1 + score2 + score3) / 3; gameCount++; counter1 = counter2 = counter3 = 0; System.out.println("You have played "+gameCount+" games");Recall that most state-change operators have very low precedence, so they will naturally be evaluated last in an expression statement. Methods whose prototype lists void as the type of the result don't return a result anyway. Such methods (e.g., System.out.println) are often called last when evaluating expression statements. Note that x+1; is NOT a legal expression statement: the last operator that it applied is +, which computes a value but does not process this value further: e.g., doesn't store it anywhere; doesn't print it. Writing such an expression serves no purpose, and the Java compiler detects and reports an error in this case. Finally, notice that this syntax constraint still allows Prompt.forInt("Enter values"); as a legal expression statement, even though this method returns an int result which is not processed further. |
Block Statements (and scope) |
Some airlines restrict you to one carry-on bag.
If you show up with three small bags, they won't let you on the plane; but
if you buy a fourth, big bag, and put your original three into the
fourth, everything will be fine.
Java's syntax sometimes forces this same kind of behavior. There are places (mostly inside the control structures) where only one statement is allowed; if you want multiple statements there, you must put them inside one big statement. That single, big statement is called a block-statement, of sometimes just a block. The EBNF rule for blocks is
block-statement <= {{statement}} In this EBNF rule, the outer braces stand for themselves, the inner ones mean repetition. That is, blocks are some number of statements enclosed in braces. So, a block itself is a statement, inside which we can put other statements. Although we write this EBNF rule on one line, block statements in our code often span many lines, with each statement inside the block appearing on its own line, indented in the braces. Semantically, Java executes a block by sequentially executing the statements that it contains (in the exact same order that they appear in the block). When giving directions to humans or computers, often the order in which the directions are followed is critical. If the directions say, "To disarm the bomb, cut the blue wire and then cut the red wire" it would not be a good idea for us to change the order in which these wires are cut. If a statement inside a block declares a variable, that variable can be used in subsequent statements inside that block; after Java executes all the statements in a block, the block is finished executing, and ALL variables declared inside the block become undeclared. So, such variables are called local variables because they exist only locally, inside the block. Technically, we call all those places that a variable can be used the scope of that variable. So the scope of local declarations in blocks include all subsequence statements in that block. Blocks themselves can be nested. Any variables declared in an outer block can be used in an inner block. For example { //Outer block int x = 1; { //Inner block System.out.println(x); //Refers to x in outer block x = 3; //Refers to x in outer block } System.out.println(x); }This example illustrates that the scope of the variable x includes the whole outer-block, which includes its inner block too. The inner block is just one statement that is included in the outer block (which is itself one bigger statement). In this example, Java prints 1 followed by 3. If we had moved the declaration from the outer block to the start of the inner block, Java would detect and report an error at compile time. { //Outer block { //Inner block int x = 1; System.out.println(x); //Refers to x in inner block x = 3; //Refers to x in inner block } System.out.println(x); //Error! the x in the inner block // is undeclared for this statement. }With this placement, the variable x would not be accessible outside the inner block, so it would be unknown in the final print method. The scope of x is just the inner block in which it is declared. |
Putting Everything Together |
Finally, putting together everything that we have learned about Java, the
following block contains a variety of statements that perform a simple
computation: declaring variables, prompting the user for values to store
in these variables (by calling a method), performing a simple calculation
with these variables (storing the result in another variable with a state
change operator) and displaying the result in the console window.{ double gravity; //meter/sec/sec double height; //meters double time; //sec //Input gravity = Prompt.forDouble("Enter gravity (in m/s/s)"); height = Prompt.forDouble("Enter height of drop (in m)"); //Calculate time = Math.sqrt(2.*height/gravity); //Output System.out.println(); System.out.println("Drop time = " + time + " secs"); }Note that by intializing variables when they are declared, we could "simplify" this code, writing it as follows. Both blocks ultimate produce the same results. { //Input double gravity = Prompt.forDouble("Enter gravity (in m/s/s)"); double height = Prompt.forDouble("Enter height of drop (in m)"); //Calculate double time = Math.sqrt(2.*height/gravity); //Output System.out.println(); System.out.println("Drop time = " + time + " secs"); }In fact, there is no need in this program for variables at all! We can squeeze this entire program down to just one statement, using the \n escape sequence inside one huge (4 line) output statement. { System.out.println("\nDrop time = " + Math.sqrt(2.*Prompt.forDouble("Enter height of drop (in m)") /Prompt.forDouble("Enter gravity (in m/s/s)")) + " secs"); }Although this program is the smallest yet, it is a bit complicated to follow the calculation, which includes two prompts to the user as part of the formula being computed. Thus, while smaller is generally better, it isn't here; sometimes storing partial calculations in nicely named variables helps us to write clearer, easier to understand programs (a main goal of 15-200). Of course, if the program needed to use the values entered by the user more than once, we should store them in variables to avoid reprompting the user for the same information multiple times. |
Hand Simulating State Changes |
As programmers, we must be able to analyze our programs to verify that they
are correct, or detect where bugs occur (the hard part) and fix them (an
easier part).
The most important way to analyze code is to be able to hand simulate
it.
The "input" to a hand simulation is
Here is a simple example (no input/output) of such a trace table. Assume int x=5; int y=8; and the block {x=y; y=x;} If beginning students are asked to predict what the code does, the most common response is that it swaps the values in x and y. Let's see what really happens using a trace table (note that a table cell shows the value stored in a variable after the statement on its line is finished).
So, we see that the values in the variables are not swapped, but that y's initial value ends up stored in both x and y. In some sense, the simplest thing to do with two variables is to exchange their values; yet the intuitive way to write code for this task is incorrect. Don't gloss over this observation, because it is very important. The kind of reasoning a programmer does about state changes in code is very different from the kind of reasoning a mathematician does about equations. One correct way to swap the values stored in two variables is: {int temp=x; x=y; y=temp;}, and the hand simulation illustrating its correctness (using the same initial state).
Note how temp is shown as undeclared before the block is executed and also becomes undeclared after Java finishes executing the block. But temp plays a crucial part in the computation, while Java is executing the statements in the block that it is declared in.
As a final example, let's examine the trace table for a block that does
I/O too.
Here there are no variables in the initial state: the block to execute is:
Here the Console column shows what is on each line (on the first line, the prompt and the value that the user enters; on the second line the answer). This is certainly a lot of work for such a simple example; but if you can easily write such trace tables, you can use them to debug code that has much subtler errors. |
Boxing Statements |
Just as we used oval diagrams to understand the structure of expressions
(and their subexpressions), we will use box diagrams to understand
statements (and in the case of control structures, their substatements).
Right now we know three kinds of statements: declaration statements, expression statements, and block statements. The example below includes multiple occurences of each of these kinds of statements. Each of its statements is inside a box. |
  | Practice this skill. Learn to "see" statements inside of statements, the way a programmer does. Notice how consistent indenting in the code makes this task easier. |
if Statements |
There are three forms of if statements in Java.
if-statement <= if (expression) statement [else statement] As a syntax constraint, expression must result in a boolean value (if it doesn't, the Java compiler detects and reports this error). Note that both if and else are keywords in Java, and the test expression, no matter how simple or complicated, must always appear inside parentheses. Finally, although we write this EBNF rule on one line, we write if statements in code that span at least two (and often many more) lines, and contain indented statements. An if statement (discarding the option) decides whether or not to execute its statement. We write it as if (test) statementRecall that statement can also be a block. Two if statement examples are if (x < 0) x = -x; if (myNumber == rouletteNumber) { myWins++; myPurse += stakes; }Notice where the opening and closing brace appear for this block: this is the standard style that we will always use for blocks inside if statements. Semantically, Java executes an if statement as follows
An if/else statement (including the option) decides which one of its two statements to execute. We write it as if (test) statement1 else statement2Recall that statement1 and/or statement2 can also be a block Two example if/else statements are if (x%2 == 0) //is x is even? x = x/2; else x = 3*x+1; if (x > y) { min = y; max = x; }else{ min = x; max = y; }Semantically, Java executes an if/else statement as follows
A cascaded if (or cascaded if/else) decides which one (if any) of many statements to execute. The general form of the cascade if in Java is if (test1) statement1 else if (test2) statement2 else if (test3) statement3 else ... ... else if (testN) statementN or if (test1) statement1 else if (test2) statement2 else if (test3) statement3 else ... ... else if (testN) statementN else statementN+1A cascaded if is built from many if/else statements, where each of statements in the else part is another if statement. An example cascaded if statement is if (testScore >= 90) grade = 'A'; else if (testScore >= 80) grade = 'B'; else if (testScore >= 70) grade = 'C'; else if (testScore >= 60) grade = 'D'; else grade = 'R';Semantically, Java executes a cascaded if statement as follows
|
Hand Simulating if statements |
We can extend our use of trace tables to hand simulations of if
statements.
We include a special Explanation column to indicate the result of
evaluating test and which statement Java executes next.
Let's write two trace tables for hand simulating the first if
statement shown above.
Next, let's write two trace tables for hand simulating the second if/else statement shown above.
What is the trace table for this example if the values stored in x and y are equal? Does it produce the correct result? Can you change the test to >= and still always get the same result? Can there be two different ways of getting the same result? Finally, let's write a trace table for hand simulating the cascaded if statement shown above.
|
A Clock Example |
Let's take a quick look at an interesting task that combines all the
statements that we have studied.
Assume that we have declared the following variables for a "military" style
clock: e.g., 00:00 represents midnight, 9:03 represents 9:03am, 14:23
represents 2:23 pm, and 23:59 represents 11:59pm.
int minute; //in the range [0,59] inclusive int hour; //in the range [0..23] inclusiveAlso assume that the method emitBeeps takes a single int operand and emits that many beeps. Finally, assume that the following code is called once a minute by the operating system; when we study Java threads we will learn how to arrange for such an action. if (minute != 59) minute++; else { emitBeeps(hour+1); minute = 0; if (hour != 23) hour++; else hour = 0; }Each time the code is called, it advances minute (and hour, if necssary) ensuring they store only legal values; on the hour, the code beeps that many times (once at 1 am, twice at 2am, ... 12 times at noon, 13 times at 1pm, ..., and 24 times at midnight). Let's write two trace tables for hand simulating this code in two different initial situations: first at 10:15 (10:15am).
Here, the minute is incremented by 1, and nothing else happens. Now lets write a trace table for the initial situation 22:59 (10:59pm).
Here, much more happens: the clock beeps 23 times (for 11:00pm) and the minute is reset to 0 while the hour advances to 23. |
A Caution: = vs == |
Imagine that you want to write code that doubles the value stored in an
int variable x, but only if it stores 10.
The following if statement proposes to solve the problem
if (x = 10) x = 2*x;Carefully examine the test, it is written as x = 10 not x == 10. Did you see that the first time you read it? Most people don't. It is a common mistake for programmers to write = accidentally instead of == in if tests. But the good news is that the Java compiler will detect and report a syntax constraint error, because the result type of the test is not boolean. There are situations, though, where the Java compiler will not detect such a mistake will not be detected: when the expression itself is of type boolean. The code on the left uses = and the one on the right uses ==. boolean doIt = ...; boolean doIt = ...; if (doIt == true) if (doIt = true) System.out.println("Yes"); System.out.println("Yes");Assume in both cases the the ... code evalutes to false. The left test evaluates to false, so it does not print the message. But the right test stores true into doIt (wiping out the value computed before the if) AND evaluates to true, so it does print the message. The Java compiler does not report any error, because the type of the expression in both cases is boolean. This brings us to a style point: writing == true or == false in an if's test is unnecessary, and is prone to error. For any boolean expression e, we can write just e (instead of e == true) and we can write !e (instead of e == false). Avoiding the true and false literals here is the sign of a mature programmer. Finally, as we have already seen, if you accidentally write the expression statement x == 0; the Java compiler will detect and report a syntax constraint error, because the last operator applied in this expression statement is not a state-change operator. Older languages (C and C++) allow these expression statements, and cause programmers no end of debugging problems, so Java disallowed them, instead forcing them to be reported during compilation. |
Dangling else |
Examine the following two if statementsif (test1) if (test1) if (test2) if (test2) statement1 statement1 else else statement2 statement2The left code looks like it has an if/else inside an if. The right code looks like it has an if inside and if/else. But what we wee is based on whitespace. What Java sees for both is EXACTLY THE SAME TOKENS: if (test1) if (test2) statement1 else statement2 because whitespace is removed once Java tokenizes a program. So, Java interprets both code fragments in exactly the same way! Which interpretation does Java use for these tokens? We need an extra syntax rule that helps us gree on which interpretion is the correct one: an else belongs to the most recent if that it can belong to. So, Java uses the left interpretation. To force the other interpretation, matching the else with the first if, we must use a block, and write if (test1) { if (test2) statement1 }else statement2Now the else (which is outside the block) cannot possibly belong to the if that is inside the block, because all parts of that if statement must reside entirely in the block. So the final else now belongs with the first if. This is called th dangling else problem, and it is hard for programmers to see. We must carefully indent our if statements to accurately, to reflect which elses with which ifs, otherwise our program will contain a subtle error that is very hard to locate. In fact, some programmers advocate ALWAYS using block in if/else statements to avoid dangling elses. The disadvantage of this approach is that in simple cases, the extra blocks create code that is harder to read. We will discuss this style principle in more detail later. |
if Pragmatics |
When writing decisions, determine the correct form: if,
if/else, or cascaded if.
If you are unsure about which one is correct, try the simpler forms first.
Indent the parts of the if and the statements that it contains to illustrate the logical structure of the if; when blocks are used, place the braces in the positions shown in the examples above. Ensure that the indentation (making the code easier for humans to read) accurately reflects how Java reads the tokens (e.g., beware of a dangling else). The key to understanding an if statement is understanding its test(s). Ensure that for some values of its variables, every the test can evaluate to both true and false (otherwise the test is probably wrong). For example, what is wrong with the following code? Study it carefully and hand simulate it for a few different values of x. Is this test is really the right one? If so, how can we simply this code to always perform the same action, but more simply? I assert that there must be something wrong -not syntactically- with this test: can you prove it? if (x > 2 || x < 5) x++; |
for Statements |
To begin, we present a simple, useful, legal, but incomplete, form for the
for statement.
The EBNF rule for this simplified for statement is
for-statement <= for(;;) statement We call statement the body of the for. Although we write this EBNF rule on one line, we write for statements in code that span at least two (and often many more) lines. Finally, note that for is a keyword in Java. The most typical form of the for statement is for (;;) { statements (i.e. a sequence of statements inside a block) }Here the body of the for is a block. Semantically, Java executes the for statement by executing its body over and over again. Thus, when done executing the body, Java "loops back" and re-executes it. That is why we often refer to such a statement as a "for loop". Such a loop runs forever; or, more accurately, until Java executes a break statement inside the loop (discussed in the next section) forces Java to terminate the loop. Two example for loops (each infinite) are for (;;) System.out.println("You're Great!"); int count = 0; for (;;) { System.out.println(count); count++; }The first example fills the screen with Your're Great!. The second example starts by displaying the value 0, then 1, then 2, then 3, etc. with the next value displayed becoming larger by one for each iteration. Let's hand simulate this second example and write a trace table for it.
Of course, this process continues endlessly, so we cannot show a complete trace table for this code or any other infiniite loop. You can always terminate the console window, if you suspect your program is in an infinite loop and want to terminate the program. |
break Statements |
The EBNF rule for the break statement is very simple
break-statement <= break; Java imposes a syntax constraint that a break statement must appear inside the body of some loop. Finally, note that break is another keyword in Java.
In real programs, break statements appear inside if statements
(which themselves are inside the bodies of loops), so a typical example is
A tyical combination of for and break statements is int countdown = 3; for(;;) { System.out.print(countdown + "..."); if (countdown == 0) break; countdown--; } System.out.println("Blastoff");Let's hand simulate this example and write a trace table for it. We call such a for/break combination a count down loop. |
Statement | countdown | Console | Explanation |
---|---|---|---|
Initial State | Undeclared |   |   |
int countdown = 3; | 3 |   |   |
for(;;) { |   |   | execute body first time |
System.out.print(countdown + "..."); |   | 3... | 1st statement in block |
if (countdown == 0) |   |   | false skip next (break;) statement; if finished |
countdown--; | 2 |   | last statement in block |
for(;;) { |   |   | execute body again |
System.out.print(countdown + "..."); |   | 3...2... | 1st statement in block |
if (countdown == 0) |   |   | false skip next (break;) statement; if finished |
countdown--; | 1 |   | last statement in block |
for(;;) { |   |   | execute body again |
System.out.print(countdown + "..."); |   | 3...2...1... | 1st statement in block |
if (countdown == 0) |   |   | false skip next (break;) statement; if finished |
countdown--; | 0 |   | last statement in block |
for(;;) { |   |   | execute body again |
System.out.print(countdown + "..."); |   | 3...2...1...0... | 1st statement in block |
if (countdown == 0) |   |   | true execute next (break;) statement; |
break; |   |   | terminate for loop |
System.out.println("Blastoff"); |   | 3...2...1...0...Blastoff | 1st statement AFTER loop body |
  | We can graphically summarize the control flow in cooperating for and break statements as |
More for/break Examples |
Let's look at two more interesting kinds of loops that combine for
and break.
The first is called a count up loop: the variable x counts
up to the value stored in the variable max.
Notice that max is declared and initialized by the value entered by
the user.
int max = Prompt.forInt("Enter Number to Sum To"); int x = 0; //Stores value to add to sum int sum = 0; //Stores sum that x is added to for (;;) { if (x == max) break; x++; sum += x; } System.out.println("1+2+...+" + max + " = " + sum);Assuming the user enters 5 when prompted, let's hand simulate these statements and write a trace table for it. |
Statement | max | x | sum | Console | Explanation |
---|---|---|---|---|---|
Initial State | Undeclared | Undeclared | Undeclared |   |   |
int max = Prompt(...); | 5 |   |   | Enter ...: 5  |   |
int x = 0; |   | 0 |   |   |   |
int sum = 0; |   |   | 0 |   |   |
for (;;) { |   |   |   |   | execute body first time |
if (x == max) |   |   |   |   | false skip next (break;) statement; if finished |
x++; |   | 1 |   |   |   |
sum+= x; |   |   | 1 |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
if (x == max) |   |   |   |   | false skip next (break;) statement; if finished |
x++; |   | 2 |   |   |   |
sum+= x; |   |   | 3 |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
if (x == max) |   |   |   |   | false skip next (break;) statement; if finished |
x++; |   | 3 |   |   |   |
sum+= x; |   |   | 6 |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
if (x == max) |   |   |   |   | false skip next (break;) statement; if finished |
x++; |   | 4 |   |   |   |
sum+= x; |   |   | 10 |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
if (x == max) |   |   |   |   | false skip next (break;) statement; if finished |
x++; |   | 5 |   |   |   |
sum+= x; |   |   | 15 |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
if (x == max) |   |   |   |   | true execute next (break;) statement |
break; |   |   |   |   | terminate for loop |
System.out.println(...); |   |   |   | 1+2+...+5 = 15 | 1st statement AFTER loop body |
  |
The second example is called a sentinel terminated loop.
Here the user enters a special value (called a sentinel) to inform the program
that there are no more values to input.
The sentinel is not processed by the normal code in the loop bodyint count = 0; int sum = 0; for (;;) { int score = Prompt.forInt("Enter a Score (-1 to Terminate)"); if (score == -1) break; count++; sum += score; } System.out.println("Average = " + (double)sum/(double)count);Assuming the user enters the value 3, 6, 4, and the sentinel -1 respectively when prompted, let's hand simulate these statements and write a trace table for it. |
Statement | count | sum | score | Console | Explanation |
---|---|---|---|---|---|
Initial State | Undeclared | Undeclared | Undeclared |   |   |
int count = 0; | 0 |   |   |   |   |
int sum = 0; |   | 0 |   |   |   |
for (;;) { |   |   |   |   | execute body first time |
int score = Prompt.forInt(...); |   |   | 3 | Enter ...: 3 |   |
if (score == -1) |   |   |   |   | false skip next (break;) statement; if finished |
count++; | 1 |   |   |   |   |
sum+= score; |   | 3 | Undeclared |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
int score = Prompt.forInt(...); |   |   | 6 | Enter ...: 6 |   |
if (score == -1) |   |   |   |   | false skip next (break;) statement; if finished |
count++; | 2 |   |   |   |   |
sum+= score; |   | 9 | Undeclared |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
int score = Prompt.forInt(...); |   |   | 4 | Enter ...: 4 |   |
if (score == -1) |   |   |   |   | false skip next (break;) statement; if finished |
count++; | 3 |   |   |   |   |
sum+= score; |   | 13 | Undeclared |   | last statement in block |
for (;;) { |   |   |   |   | execute body again |
int score = Prompt.forInt(...); |   |   | -1 | Enter ...: -1 |   |
if (score == -1) |   |   |   |   | true execute next (break;) statement |
break; |   |   | Undeclared |   | terminate for loop |
System.out.println(...) |    |   | Average = 4.3333333 | 1st statement AFTER loop body |
  |
Notice that each time that the for loop executes its body, it declares, intializes, then undeclares its local variable score. This variable could be declared and left uninitialized before the loop, as int score; and then appear in the loop as just score = Prompt.forInt("Enter a Score (-1 to Terminate)");. But because the values stored in this variable are never (and should never be)used outside the loop, we have chosen to not even declare this variable outside the loop. The fact that this variable is declared/undeclared many times does not affect the correctness (nor the speed) of this code. |
Compact Trace Tables |
When we hand simulate programs with complicated control structures, most of
the trace table is occupied by information relating to control structures
deciding which statements to execute next, as opposed to statements that
actually change the state of variables or the console window.
Such trace tables are cumbersome to create and hard to read.
Compact trace tables remove all the information related to control structures, and instead focus on state changes to variables and the console window. To construct a compact trace table, we list all variables and Console in separate columns (and omit Explanation). Only when the code changes the state of a variable OR the console window do we update information in the appropriate column; and we always do so right beneath the last entry for this column. Note that what we lose in a compact trace table (we gain conciseness) is an indication of the order in which different variables have their state changed: because each column is shown as compactly as possible (no blank entries); there is no correlation among columns whose states changed. Here are compact trace tables for the three standard trace tables shown above. First, the count down loop.
Next, the count up loop.
Finally, the sentinel terminated loop.
Remember, in a compact trace table, all the blank entries are at the bottom of a column. A column entry is filled in only when all the column entries on top of it have been filled in. |
General for/break; while/do |
We can write ANY looping code using the for and break that we
know; but, there is a more general form of the for statement that
allows use to write many loops more compactly and clearly.
When we study arrays, iterators, and self referential objects, these forms
will become more and more useful.
The general for statement packages all the information needed for a count-down or count-up loop into one locality, making it easier to read, write, and understand. The EBNF rule for the general for statement is
expression-list <= expression{,expression} Note that if we discard all the options, we are back at the for(;;) statement that we have studied. As a syntax constraint, if the expression option (in the middle of the semi-colons) is included, its resulting type must boolean: this part is called the continuation test. Also, each expression in an expression-list must adhere to the same constraints as an expression-statement: it must apply a state-change operator or a method call at the end.
For example, we can use such a for statements to simplify the code
that sums all the integers up to max.
Semantically the for loop executes as follows.
Pictorially, the semantics look like
What makes the for loop so powerful is the way it groups together, in one locality, all the information that controls the loop. Here is a standard (not compact) trace table illustrating these semantics in the code above.
Note that the variable x becomes undeclared after the for
statement terminates.
Thus, we cannot refer to it after the for loop's body (where we print
the statistics); if we did write its name there, the Java compiler would
detect and report an error.
If we did want to refer to this value AFTER the for statement
finishes, we could write
Finally, a for statement can be named by an identifier (see the first option in the EBNF). Likewise, there is a more general break statement whose EBNF is break-statement <= break [identifier] ; In a simple break statement, Java terminates the inner-most loop that the break apears in. If a general break statement includes this option, it has a syntax constraint that it must appear inside a loop named by that same indentifier, and it terminanes that loop. This feature is only useful for loops inside loops, and even then it is very very rarely needed. |
for,while,do Semantics |
We will now show the EBNF rule for while and do statements and
explain their semantics, and the general for statement, by using
the simple for and break statements.
The EBNF for while and do statements is
while-statement <= while(expression) statement
Semantically, we can mechanically translate any general for loop,
while loop, or do loop into an equivalent simple
for(;;) loop.
The Java compiler performs just this kind of transformation when it
generates the machine instructions corresponding to these kinds of loops.
The while and do statements and just variants where the continuation condition is always tested first or last in the loop's body. In the case of the do loop, the body is always executed once. Pragmatically, you will see many more while loops than do loops. Finally, most students have two problems understanding general for loops.
|
for/break Pragmatics |
The following rules can help you synthesize and analyze (to fix bugs) loops.
When designing iterative code, think in terms of the simple for and
break statements and determine:
Sylistically, write loops as shown, with block braces as shown and the body of the loop slightly indented (typically 2 spaces). When hand simulating loops, pay special attention to the first few and last few iterations (certain kinds of errors occur only at the beginning or ending of a loop). Ensure that all the variables are properly initialized before they are examined in expressions (the Java compiler will help you here). Errors due to incorrect initialization are easy to spot if we carefully hand simulate the first iteration of a loop (and these are among the most frequent category of errors). The break statement is the most important statement inside a loop. Clearly mark break statements using white space and/or special comments (e.g., a comment sandwich). Ensure that for all possible initial states of its variables, a loop eventually terminates (the test in the if statement containing the break will always eventually evaluate to true). Most loops, even in industrial code, need one if/break combination. A loop to solve a very complicated problem may require multiple if/break combinations, but they can often be localized (grouped together). Only a loop that solves the most complicated kind of problems may require multiple if/break combinations distributed throughout the loop's body. It is the mark of a good programmer to write simple loops with simple terminations. The most frequent occuring location for the if/break is the first statement in the body of the for loop; try it there first, move it elsewhere if necessary. Finally, use the general form of the for statement to its maximum advantage, to clarify your loops. You may use while and do loops, but the extra thought that goes into considering them, and the fact that it is often harder to think of a "continuation" condition rather than a "termination" condition, means that I use them infrequently. |
Boxing if, for, and break Statements | Continuing our analysis of boxing statements, we illustrate below how to box if, for and break statements (as well as expression and block statements). Notice that EVERYTHING that can be syntactically considered to be a statement is in its own box. This includes declarations statements (there are none here), expression statements, blocks, entire if statements, break statements, and entire for statements. |
  | General for loops are boxed in a similar manner; none of the information within their parentheses are considered statements. |
try-catch |
The EBNF of the try-catch statement is the most complex of any
control structure that we have seen so far: that is a tipoff that
programming in Java with exception handling is interesting.
In fact, many Java courses don't cover exceptions and exception handling
until much later in the semester.
But, I think that the core concepts can be demonstrated early, can be used
to good advantage in stereotypical ways that are easy to understand, and
can be returned to repeatedly in more complicated contexts (a spiral
approach to learning).
The general form of a try-catch statement is
parameter               <= type identifier Although we write the try-catch-statement EBNF rule on one line, try-catch statements written in our code often span many lines (they contain mandatory blocks, which can contain many statements on different lines). The names of exceptions are actually special reference types. Although this is getting a bit ahead of ourselves, we will use the following reference types/exception names: ArithmeticException, IOException, NumberFormatException, IllegalArgumentException, and Exception (a generic name that includes all the others). As a syntax constraint, the right hand side of each try-catch-statement must have at least one catch-clause or one finally; there can be many of the former, and a combination of both, but we cannot take 0 repetitions of catch-clause and at the same time discard the finally block. We could actually encode this restriction in EBNF, but it would make a complicated description look horrible. The semantics of this statement, as you might expect, are complicated as well. Java starts a try-catch by executing the block-statement immediately following the keyword try (known as the try block), just as it would execute any block. One of two things happen.
|
A Simple Example |
In this section we will present a simplified example: it is not useful in
real programs, but is useful only to illustrate the semantics of
try-catch statements.
Recall that, the / operator throws an exception named
ArithmeticException if its second operand is zero.
Let us examine the effect of placing the following try-catch
statement in a program.
int percentage; ...some code try { int attended = Prompt.forInt("Enter attendance"); int capacity = Prompt.forInt("Enter capacity"); percentage = 100*attended/capacity; System.out.Println("percentage computed ok"); } catch (ArithmeticException a) { System.out.println("capacity was 0; I'll assume percentage was 100%"); percentage = 100; } ... more codeHere, if the division succeeds, percentage is set correctly, the "ok" message is printed, the try block finishes normally, and execution continues afterward, where it says ...more code (because there is no finally block). On the other hand, if the division fails (throwing ArithmeticException), percentage is not set (the = operator is never evaluted; it requires the result from the division which we have just seen has thrown an exception) the "ok" message is skipped as Java locates the appropriate catch-clause; both statements in the catch-clause block are executed, then the try block finishes, and execution again continues afterward, where it says ...more code (because there is no finally block). If we replaced ArithmeticException by Exception then the code would execute identically, because Exception matches all raised exceptions. If we replaced ArithmeticException by any other name, say IOException then Java would not find a matching exception; assuming that there is no outer try-catch statement to catch this exception, Java would terminate the program and print a trace on the console. One reason why this example is not realistic is that we can easily check whether the division will fail with an if statement and avoid the need for a try-catch statement all together. int percentage; ...some code int attended = Prompt.forInt("Enter attendance"); int capacity = Prompt.forInt("Enter capacity"); if (capacity != 0) { percentage = 100*attended/capacity; System.out.Println("percentage computed ok"); }else { System.out.println("capacity was 0; I'll assume percentage was 100%"); percentage = 100; } ... more codeIn the following two examples, which are much more realistic, we will need a try-catch statement to solve the problem: we cannot use an if statement to check whether an exception will be thrown. |
Prompting with try-catch |
In this section we will present a more realistic example.
In fact, similar code appears inside the Prompt.forInt method.
Understanding how this code works requires a mastery of the semantics of many
Java statements.
First, we must know that the Integer.parseInt method (from the Java
library) has the following prototype
int Integer.parseInt(String) throws NumberFormatExceptionThis methods takes a String as an argument. If that argument represents the value of a legal integer, it returns that value as an int; if it does not represent a legal integer, it cannot return any reasonable value, so it throws NumberFormatException. Thus Integer.parseInt("-10") returns the int -10 and Integer.parseInt("-1x0") throws NumberFormatException. There is no method that Java provides to check whether Integer.parseInt will throw an exception: we have to call that method to see what it does. Now, let us see how the following code, a combination of a for loop, break statement (not in an if!) and try-catch, prompts the usre until he/she enters a valid integer, whose value is stored into answer int answer; for(;;) try { answer = Integer.parseInt(Prompt.forString("Enter integer")); break; } catch (NumberFormatException e) { System.out.println("Error: please enter a valid integer"); } ...process answerHere, the for loop repeatedly executes the try-catch statement. First, let us see what happens if the user enters a valid integer. During the first iteration of the loop, Java executes the first statement; the user enters a valid integer (read as a String that is passed to the Integer.parseInt method); so, this method does not throw an exception, but instead returns a result that is stored in answer. Thus, the second statement in the block is reached; this break statement terminates the entire for loop, and execution continue after the for loop, where it says ...process answer. Now, let us see what happens if the user enters an INVALID integer. During the first iteration of the loop, Java executes the first statement; the user enters an invalid integer (read as a String that is passed to the Integer.parseInt method); so, this method throws a NumberFormatException. Java skips the break statement and instead finds the catch matching the exception; its following block prints an error message. Now the try-catch statement is finished; but this statement is the body of a for loop, so it is executed again! Therefore, this loop will continue executing so long as the user enters an invalid integer; the first time that the user enters a valid integer (see the description above) its value will be stored into answer and the break statement following it will be executed. So generally, we have designed code that (potentially) repeatedly performs some operation until an exception is NOT thrown. In the next section, we will design code that repeatedly performs some operation until an exception IS thrown: e.g., we are anticipating that an exception will eventually terminate the loop, which continues executing until it is does. Together, these two forms occur frequently in exception handing code. |
Reading Files with try-catch |
In this section we will present another realistic example.
In fact, code similar to this will be present in most programs that read
files.
First, we must learn that the readInt method (from a Java library
class that I have written) has the following prototype
int readInt() throws NumberFormatException, EndOfFileExceptionThis method skips any white space in a file and returns the next integer value that it contains (if it succeeds). There are two ways for it to fail, each denoted by a different exception name.
int sum = 0; for (;;) try { int aValue = inputFile.readInt(); sum += aValue; } catch (EndOfFileException eofe) { break; } System.out.println("Sum = " + sum);Let us see what happens if the file contains two integers.
This code that repeatedly performs some operation until an exception is thrown. Note that if a non-integer value appears in the file, then calling the readInt method causes it to throw NumberFormatException. This exception is not caught by the try-catch shown above, so Java terminates the program and prints a trace on the console. Finally, because the only place that aValue is used is to add to sum, we can simplify this code a bit and write. int sum = 0; for (;;) try { sum += inputFile.readInt(); } catch (EndOfFileException eofe) { break; } System.out.println("Sum = " + sum); |
Problem Set |
To ensure that you understand all the material in this lecture, please solve
the the announced problems after you read the lecture.
If you get stumped on any problem, go back and read the relevant part of the lecture. If you still have questions, please get help from the Instructor, a CA, or any other student.
|