Introduction | In this short lecture, we will clarify some points about the meaning of final in variable declarations, introduce two operators used in conditional expressions and discuss short-circuit evaluation in logical operators All these features allow us to write more compact and understandable code, once we understand these language features. |
More on final |
In the real world, a constant is a named value that never changes.
Examples of constants are π (pi), e, the speed of light, the mass of a
proton (we think?) etc.
In programs, a constant is a variable whose value never changes within its
scope (i.e., during the time the variable is declared).
This is a slightly more liberal definition.
So, any real-world constant is a program constant, but a program constant
doesn't have to be a real-world constant.
For example, to compute a mortgage, a program uses the current interest rate. This value is not a real-world constant, because its value changes daily. But, once the program starts computing the mortgage payments, the current interest rate is a constant in that program. We have seen that we can declare both local variables and instance variables as constants by using the final access modifier. When a variable is declared final it must be intialized, its value can be examined in expressions, but its value can never be changed by a state-change operator. We will use the terms constant and final variable interchangably. If we write code to change a constant, the Java compiler detects and reports an error. In fact, we can use this rule to get some interesting information from the compiler: every statement where we change the state of a variable. We do so by changing it from a variable to a constant, and then let the compiler locate all the "errors" where we try to change its state. When we write programs, we should declare constants instead of using "magic" literals. The names of the constants will help us remember what the constant means (without having to see its values: what is 6.022141E23 or 2.99792458E8?). Using constants instead of variables makes our programs less prone to error: if we use a variable, we might accidentally change what value it stores -this is impossible with constants. Using constants also makes it easier to change our programs: in the Rocket program we could have written .01 in lots of places, but if we needed to change that value to .001 (for a more accurate simulation), we might have to search our code carefully to make the correct changes (there might be other .01s in our program not refering to the time increment). If instead we declared final double dT = .01; in our program, and then used the constant dT throughout our code, to change this value requires editing just this one line of code, and then recompiling the program. Although use of final in the example below may be a bit confusing, it is perfectly legal. int count = 0; int sum = 0; for (;;) { final int score = Prompt.forInt("Enter score (-1 to terminate)"); if (score == -1) break; count++; sum += score; } System.out.println("Average = " + sum/count);In this example, score is declared final and indeed, its value (once initialized) never changes in its scope: the block in which score is declared. When the block finishes, score becomes undeclared; then the for loop re-executes the block, redeclaring and reinitializing the score constant all over again. So, our use of score meets all the technical requirements for a constant. Some programmers would pronounce this code excellent; others would say that indicating final is not worth it. What do you think? Most constants specify an initializer in their declaration; but surprsingly, this is not necessary. If the initializer is omitted, it is called a blank final variable. The Java compiler is smart enought to ensure
final double d; //blank final ...code... //cannot refer to the constant d if (whatever) //value is assigned to constant d in one if branch d = ... else d = ... ...more code... //cannot change the constant dAny further attempt to store a value into d will be detected and reported as an error by the Java compiler. When we learn how to write instance variables in classes, we will see more reasonable uses of blank final. Finally, recall that when we declare a constant for a reference type, we must be a bit careful of its meaning. A reference variable stores a reference (as its state) which refers to an object (which stores its own state). So, using final with a reference variable DOES mean that once we store a reference into that variable, it always refers to the same object. It DOES NOT mean that the state of the object remains unchanged: we can still call mutator/command methods on a final variable, changing not its state (WHICH object it refers to) but the state IN the object it refers to. So, if we declare final DiceEnsemble d = new DiceEnsemble(2,6); we CAN write d.roll();, but we CANNOT write d = new DiceEnsemble(1,6); Again, the difference between what is stored in a variable (a reference) and what is stored in the object it refers to (its state) is crucial to understanding this distinction. |
Conditional Operators ? and : |
There are two operators that work together in Java, helping us to condense
our code by allowing us to write short expressions instead of longer
statements.
These two operators, ? and : constitute what is called a
conditional expression.
The EBNF rule for a conditional expression is
    conditional-expression <= expression ? expression : expression As a syntax constraint, the first expression must return a boolean result, and the second two expressions must return a result of the same type (it can be any type, but they must match).
We will write conditional expressions using the following form (almost
always putting them in parentheses, which makes reading them easier)
Semantically, Java first evaluates test, if it is true the result of the conditional expression is the result of evaluating expressionT; if it is false the result of the conditional expression is the result of evaluating expressionF. So, only two of the three expressions are ever evaluated. Because each conditional expression must have a unique result type, and because its value can be computed by either expressionT or expressionF, the Java compiler has a syntax constraint that requires these expressions to have the same type.
Let's look at three concrete examples of conditional expressions and the
if statements that they condense.
|
Short-Circuit Evaluation |
We have learned that binary infix operators evaluate both their operands
first, and then compute their resulting value.
Actually, this ordering is correct for all but the && and ||
logical operators.
Instead, these operators use short-circuit evaluation: they always
evaluate their left operand first; if they can compute their resulting
value from this operand alone, they do so without evaluating their right
operand; if they cannot determine the resulting value from the left operand
alone, then they evaluate their right operand and compute the resulting
value
Note that if the left operand of && evaluates to false, the result must be false: false && false as well as false && true evaluate to false, so the value of the right operand is irrelevant. Note that if the left operand of || evaluates to true, the result must be true: true || false as well as true || true evaluate to true, so again the value of the right operand is irrelevant.
To see how we can use this short-circuit property when programming, assume
that a program declares int totalParts = 0, badParts = 0; and
increments the appropriate variables when a part is tested.
Next, assume that if the ratio of bad parts to total parts is over 5% (or
.05) we want to recognize this problem and display a message.
Because we have short-circuit evaluation, we can simply write
In a programming language without short-circuit evaluation, we could safely
write the following, more complicated code
As a final example, suppose that we are writing a game-playing program, and
the user must terminate the bet-play loop if his/her purse is 0 or if
he/she elects to quit (if the former is true, the user shouldn't even be
prompted about electively quitting; he/she must quit because he/she has
no more money).
We can write one if statement that captures all these semantics
Again, in a programming language without short-circuit evaluation, we could
safely write the following, more complicated code
Finally, short-circuit evaluation actually works in conditional expressions too. For example, if we write the conditional expresson (true ? 1 : 1/0) Java's result is 1; because the expression evaluates to true Java evaluates only the expression 1 and not the expression 1/0. If Java fully evaluated all expressions first, it would throw an exception. Recall the semantics of the conditional expression: Java first evaluates the test, if it is true the result of the conditional expression is the result of evaluating expressionT; if it is false the result of the conditional expression is the result of evaluating expressionF. So, it uses test to determine which other expression to evaluate, and only evaluates that one other expression. It always evaluates two of the three expressions. |
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.
|