Like methods, constructors can be overloaded. Since the constructors in a class all have the same name as the class, their signatures are differentiated by their parameter lists. In the following example, the class Light now provides explicit implementation of the no-argument constructor at (1) and that of a non-zero argument constructor at (2). The constructors are overloaded, as is evident by their signatures. The non-zero argument constructor at (2) is called when an object of the class Light is created at (4), and the no-argument constructor is likewise called at (3). Overloading of constructors allows appropriate initialization of objects on creation, depending on the constructor invoked (see chaining of constructors in §5.3, p. 209). It is recommended to use the @param tag in a Javadoc comment to document the formal parameters of a constructor.
Example 4.4 illustrates using strings in a switch statement. The thing to note is what constitutes a constant string expression that can be used as a case constant. The case constants at (3), (4), (5), and (6) are all valid constant string expressions, as the compiler can figure out their value at compile time. String literals, used at (3) and (6), and constant field values, declared at (1) and (2a) and used at (4) and (5), are all valid case constants. In contrast, the HOT reference from declarations (2b) and (2c) cannot be used as a case constant. From the declaration at (2a), the compiler cannot guarantee that the value of the reference will not change at runtime. From the declaration at (2c), it cannot deduce the value at compile time, as the constructor must be run to construct the value.
Switching on strings is essentially based on equality comparison of integer values that are hash values of strings, followed by an object equality test to rule out the possibility of collision between two different strings having the same hash value. Switching on strings should be used judiciously, as it is less efficient than switching on integers. Switching on strings is not advisable if the values being switched on are not already strings.
public class SwitchingOnAString { public static final String MEDIUM = “Medium”; // (1) public static final String HOT = “Hot”; // (2a) //public static String HOT = “Hot”; // (2b) Not OK as case label //public static final String HOT = new String(“Hot”); // (2c) Not OK as case label public static void main(String[] args) { String spiceLevel = “Medium_Hot”; switch (spiceLevel) { case “Mild”, // (3) MEDIUM + “_” + HOT -> System.out.println(“Enjoy your meal!”); // (4) case HOT -> System.out.println(“Have fun!”); // (5) case “Suicide” -> System.out.println(“Good luck!”); // (6) default -> System.out.println(“You being funny?”); } } }
Output from the program:
Enjoy your meal!
Using Enum Constants as case Constants
Example 4.5 illustrates the use of enum types (§5.13, p. 287) in a switch statement with the arrow notation. The enum type SpiceGrade is defined at (1). The type of the selector expression at (2) is the enum type SpiceGrade. Note that the enum constants are not specified with their fully qualified name (see (3a)). Using the fully qualified name results in a compile-time error, as shown at (3b). Only enum constants that have the same enum type as the selector expression can be specified as case label values.
The semantics of the switch statement are the same as described earlier. Switching on enum values is essentially based on equality comparison of unique integer values that are ordinal values assigned by the compiler to the constants of an enum type.
When the switch rules cover all values of the selector expression type, the switch statement is said to be exhaustive. Non-exhaustive switch statements are a common cause of programming errors. It is up to the programmer to ensure that the switch statement is exhaustive, as the compiler does not provide any help in this regard for the switch statement. Judicious use of the default label should be considered, as illustrated in the examples provided in this section that use the switch statement.
A switch expression evaluates to a value, as opposed to a switch statement that does not. Conceptually we can think of a switch expression as an augmented switch statement that returns a value. We look at both forms of the switch expression, defined using the colon notation and the arrow notation, how it yields a value, and compare it to the switch statement. The switch expression is analogous to the switch statement, except for the provision to return a value.
The yield Statement
The yield statement in a switch expression is analogous to the break statement in a switch statement. It can only be used in a switch expression, where the identifier yield is a contextual keyword only having a special meaning in the context of a switch expression.
yield expression ;
Execution of the yield statement results in the expression being evaluated, and its value being returned as the value of the switch expression.
The switch Expression with the Colon (:) Notation
The switch expression with the colon notation has the same form as the switch statement with the colon notation (Figure 4.2), except that the execution of the switch body results in a value (or it throws an exception).
Example 4.6 is a reworking of Example 4.2 with seasons, where the group of statements associated with a case label print information about the season and return a constant of the enum type Season that is defined at (1). Note that the yield statement is the last statement in the group of statements associated with each case label. Execution of the yield statement results in its expression being evaluated, and its value being returned as the value of the switch expression, thereby also terminating the execution of the switch expression. Not surprisingly, a break or a return statement is not allowed in a switch expression. Note that the switch expression is on the right-hand side of the assignment statement defined at (2) and is terminated by a semicolon (;).
The fall-through of execution in the switch expression with the colon notation is analogous to that of the switch statement with the colon notation (Figure 4.3). If a group of statements associated with a case label does not end in a yield statement, execution continues with the next group of statements, if any.
The switch expression with the colon notation must be exhaustive, meaning the case labels, and if necessary the default label, must cover all values of the selector expression type. Non-exhaustive switch expressions will result in a compile-time error. The default label is typically used to make the switch expression exhaustive. In Example 4.6, the type of the selector expression is int, but the case labels only cover the int values from 1 to 12. A default label is necessary to cover the other int values or to throw an exception, as in this case, and make the switch expression exhaustive.
Example 4.6 A yield Statement in a switch Expression with the Colon Notation
public class SeasonsIII { enum Season { WINTER, SPRING, SUMMER, FALL } // (1) public static void main(String[] args) { int monthNumber = 11; Season season = switch(monthNumber) { // (2) case 12: case 1: case 2: // (3) System.out.println(“Snow in the winter.”); yield Season.WINTER; // (4) case 3, 4: case 5: // (5) System.out.println(“Green grass in the spring.”); yield Season.SPRING; // (6) case 6, 7, 8: // (7) System.out.println(“Sunshine in the summer.”); yield Season.SUMMER; // (8) case 9, 10, 11: // (9) System.out.println(“Yellow leaves in the fall.”); yield Season.FALL; // (10) default: // (11) throw new IllegalArgumentException(monthNumber + ” not a valid month.”); }; // (12) System.out.println(season); } }
The switch Expression with the Arrow (->) Notation
The switch expression with the arrow notation also has the same form as the switch statement with the arrow notation (Figure 4.4), except that the execution of the switch body must result in a value (or it must throw an exception).
The execution of the switch rules in a switch expression is mutually exclusive, analogous to the switch rules in a switch statement (Figure 4.5). Once the action in the switch rule has completed execution, the value computed by the action is returned and the execution of the switch expression terminates. There is no fall-through and no break statement is allowed.
Whereas the actions in the switch rules of a switch statement only allowed an expression statement (Figure 4.5), the actions in the switch rules of a switch expression allow any expression, in addition to allowing a block or throwing an exception, as in the switch rules of a switch statement.
By far, the canonical action of a case label in a switch rule of a switch expression is an arbitrary expression. Such an expression is always terminated by a semicolon (;). The expression value is returned as the value of the switch expression whose execution is then terminated. Note that no yield statement is necessary or allowed.
A block of statements can be used if program logic should be refined, but the last statement in the block should be a yield statement to return its value and terminate the execution of the switch expression (alternatively, the last statement can be a throw statement). In a switch expression with the arrow notation, the yield statement is only allowed as the last statement in a block that constitutes the action in a switch rule.
… case ALARM -> { soundTheAlarm(); callTheFireDepartment(); yield Status.EVACUATE; } // OK case ALL_CLEAR -> { yield Status.NORMAL; // Compile-time error: not last statement standDown(); } // in the block. …
… default -> throw new IllegalArgumentException(“Not a valid value”); …
Example 4.6 has been refactored to use the switch expression with the arrow notation in Example 4.7. Each action associated with a case label of a switch rule is a block of statements, where a yield statement is the last statement in a block. If this is not the case, the code will not compile.
The switch expression with the arrow notation must also be exhaustive. Again a non-exhaustive switch expression with the arrow notation will result in a compile-time error. In Example 4.7, the type of the selector expression is int, but the switch rules only cover the int values from 1 to 12. A default label is necessary to make the switch expression exhaustive, as shown at (11).
Example 4.7 Statement Blocks in a switch Expression with the Arrow Notation
Example 4.8 is a reworking of Example 4.7 that defines expressions as the actions in the switch rules. No yield statement is necessary or allowed in this case. The switch expression is also exhaustive.
Example 4.8 Expression Actions in a switch Expression with the Arrow Notation
public class SeasonsV { enum Season { WINTER, SPRING, SUMMER, FALL } // (1) public static void main(String[] args) { int monthNumber = 11; Season season = switch(monthNumber) { // (2) case 12, 1, 2 -> Season.WINTER; // (3) case 3, 4, 5 -> Season.SPRING; // (4) case 6, 7, 8 -> Season.SUMMER; // (5) case 9, 10, 11 -> Season.FALL; // (6) default -> throw new IllegalArgumentException(monthNumber + ” not a valid month.”); }; System.out.println(season); } }
Output from the program:
FALL
The switch expression can only evaluate to a single value. Multiple values can be returned by constructing an object with the required values and returning the object as a result of evaluating the switch expression. Record classes are particularly suited for this purpose (§5.14, p. 299). The switch expression at (3) in Example 4.9 returns an object of the record class SeasonInfo, defined at (2), to return the month number and the season in which it occurs.
Example 4.9 Returning Multiple Values as a Record from a switch Expression
public class SeasonsVI { enum Season { WINTER, SPRING, SUMMER, FALL } // (1) record SeasonInfo(int month, Season season) {} // (2) public static void main(String[] args) { int monthNumber = 11; SeasonInfo seasonInfo = switch(monthNumber) { // (3) case 12, 1, 2 -> new SeasonInfo(monthNumber, Season.WINTER); // (4) case 3, 4, 5 -> new SeasonInfo(monthNumber, Season.SPRING); // (5) case 6, 7, 8 -> new SeasonInfo(monthNumber, Season.SUMMER); // (6) case 9, 10, 11 -> new SeasonInfo(monthNumber, Season.FALL); // (7) default -> throw new IllegalArgumentException(monthNumber + ” not a valid month.”); }; System.out.println(seasonInfo); } }
The scope of a local variable declared in a switch statement or a switch expression is the entire switch block. Any local block in the switch body introduces a new local scope. Any local variable declared in it has block scope, and therefore, is only accessible in that block. A local variable declared in an enclosing local scope cannot be redeclared in a nested local scope (§6.6, p. 354).
Summary of the switch Statement and the switch Expression
Table 4.1 summarizes the features of the switch statement and the switch expression, and provides a comparison of the two constructs.
Table 4.1 Comparing the switch Statement and the switch Expression
Notation
The switch statement
The switch expression
The colon (:) notation: case label: statements
Executes statements associated with the matching case label.Fall-through can occur.No compile-time check for exhaustiveness.Only break and return statements allowed to control fall-through.
Executes statements associated with the matching case label, but must have a yield statement to return a value.Fall-through can occur.Compile-time check for exhaustiveness.No break or return statement allowed.
The arrow (->) notation: case label -> action
Action associated with a switch rule can be an expression statement, can be a block, or can throw an exception.Mutually exclusive switch rules: no fall-through can occur.No compile-time check for exhaustiveness.break and return statements allowed.
Action associated with a switch rule can be any expression, can be a block, or can throw an exception.Mutually exclusive switch rules: no fall-through can occur.Compile-time check for exhaustiveness.No break or return statement allowed.Must return a value that is either the value of a stand-alone expression or the value of the expression in a yield statement that can occur as the last statement in a block.
Loops allow a single statement or a statement block to be executed repeatedly (i.e., iterated). A boolean condition (called the loop condition) is commonly used to determine when to terminate the loop. The statements executed in the loop constitute the loop body.
Java provides four language constructs for loop construction:
The while statement
The do-while statement
The basic for statement
The enhanced for statement
These loops differ in the order in which they execute the loop body and test the loop condition. The while loop and the basic for loop test the loop condition before executing the loop body, whereas the do-while loop tests the loop condition after execution of the loop body.
The enhanced for loop (also called the for-each loop) simplifies iterating over arrays and collections. We will use the notations for(;;) and for(:) to designate the basic for loop and the enhanced for loop, respectively.
4.5 The while Statement
The syntax of the while loop is
while ( loop_condition ) loop_body
The loop condition is evaluated before executing the loop body. The while statement executes the loop body as long as the loop condition is true. When the loop condition becomes false, the loop is terminated and execution continues with any statement immediately following the loop. If the loop condition is false to begin with, the loop body is not executed at all. In other words, a while loop can execute zero or more times. The loop condition must evaluate to a boolean or a Boolean value. In the latter case, the reference value is unboxed to a boolean value. The flow of control in a while statement is shown in Figure 4.6.
Figure 4.6 Activity Diagram for the while Statement
The while statement is normally used when the number of iterations is not known.
while (noSignOfLife()) keepLooking();
Since the loop body can be any valid statement, inadvertently terminating each line with the empty statement (;) can give unintended results. Always using a block statement as the loop body helps to avoid such problems.
In a do-while statement, the loop condition is evaluated after executing the loop body. The loop condition must evaluate to a boolean or Boolean value. The value of the loop condition is subjected to unboxing if it is of the type Boolean. The do-while statement executes the loop body until the loop condition becomes false. When the loop condition becomes false, the loop is terminated and execution continues with any statement immediately following the loop. Note that the loop body is executed at least once. Figure 4.7 illustrates the flow of control in a do-while statement.
Figure 4.7 Activity Diagram for the do-while Statement
The loop body in a do-while loop is invariably a statement block. It is instructive to compare the while and do-while loops. In the examples that follow, the mice might never get to play if the cat is not away, as in the loop at (1). The mice do get to play at least once (at the peril of losing their life) in the loop at (2).
while (cat.isAway()) { // (1) mice.play(); } do { // (2) mice.play(); } while (cat.isAway());
4.7 The for(;;) Statement
The for(;;) loop is the most general of all the loops. It is mostly used for counter-controlled loops, in which the number of iterations is known beforehand.
for ( initialization ; loop_condition ; update_expression ) loop_body
The initialization usually declares and initializes a loop variable that controls the execution of the loop body. The loop body can be a single statement or a statement block. The loop condition must evaluate to a boolean or Boolean value. In the latter case, the reference value is converted to a boolean value by unboxing. The loop condition usually involves the loop variable, and if the loop condition is true, the loop body is executed; otherwise, execution continues with any statement following the for(;;) loop. After each iteration (i.e., execution of the loop body), the update expression is executed. This usually modifies the value of the loop variable to ensure eventual loop termination. The loop condition is then tested to determine whether the loop body should be executed again. Note that the initialization is executed only once, on entry into the loop. The semantics of the for(;;) loop are illustrated in Figure 4.8, and are summarized by the following equivalent while loop code template:
int sum = 0; int[] array = {12, 23, 5, 7, 19}; for (int index = 0; index < array.length; index++) // (1) sum += array[index];
The loop variable index is declared and initialized in the initialization section of the loop. It is incremented in the update expression section. This loop is an example of a forward for(;;) loop, where the loop variable is incremented.
The next code snippet is an example of a backward for(;;) loop, where the loop variable is decremented to sum the values in the array:
int sum = 0; int[] array = {12, 23, 5, 7, 19}; for (int index = array.length – 1; index >= 0; index–) sum += array[index];
It is instructive to compare the specification of the loop header in the forward and backward for(;;) loops in these examples.
The loop at (1) earlier showed how a declaration statement can be specified in the initialization section. Such a declaration statement can also specify a comma-separated list of variables:
The variables i, j, and k in the declaration statement all have type int. All variables declared in the initialization section are local variables in the for(;;) statement and obey the scope rules for local blocks, as do any variables declared in the loop body. The following code will not compile, however, as variable declarations of different types (in this case, int and String) require declaration statements that are terminated by semicolons:
for (int i = 0, String str = “@”; … ; …) …; // (3) Compile-time error
The initialization section can also be a comma-separated list of expression statements (§3.3, p. 101). Any value returned by an expression statement is discarded. For example, the loop at (2) can be rewritten by factoring out the variable declarations:
int i, j, k; // Variable declaration for (i = 0, j = 1, k = 2; … ; …) …; // (4) Only initialization
The initialization section is now a comma-separated list of three expressions. The expressions in such a list are always evaluated from left to right, and their values are discarded. Note that the variables i, j, and k at (4) are not local to the loop.
Declaration statements cannot be mixed with expression statements in the initialization section, as is the case at (5) in the following example. Factoring out the variable declaration, as at (6), leaves a legal comma-separated list of expression statements.
// (5) Not legal and ugly: for (int i = 0, System.out.println(“This won’t do!”); flag; i++) { // Error! // loop body } // (6) Legal, but still ugly: int i; // Declaration factored out. for (i = 0, System.out.println(“This is legal!”); flag; i++) { // OK. // loop body }
The update expression can also be a comma-separated list of expression statements. The following code specifies a for(;;) loop that has a comma-separated list of three variables in the initialization section, and a comma-separated list of two expressions in the update expression section:
// Legal usage but not recommended, as it can affect code comprehension. int[][] sqMatrix = { {3, 4, 6}, {5, 7, 4}, {5, 8, 9} }; for (int i = 0, j = sqMatrix[0].length – 1, asymDiagonal = 0; // initialization i < sqMatrix.length; // loop condition i++, j–) // update expression asymDiagonal += sqMatrix[i][j]; // loop body
All sections in the for(;;) header are optional. Any or all of them can be left empty, but the two semicolons are mandatory. In particular, leaving out the loop condition signifies that the loop condition is true. The “crab”, (;;), can be used to construct an infinite loop, where termination is presumably achieved through code in the loop body (see the next section on transfer statements):
Objects communicate by calling methods on each other. A method call is used to invoke a method on an object. Parameters in the method call provide one way of exchanging information between the caller object and the callee object (which need not be different).
The syntax of a method call can be any one of the following:
The object_reference must be an expression that evaluates to a reference value denoting the object on which the method is called. If the caller and the callee are the same, object reference can be omitted (see the discussion of the this reference on p. 106). The class_name can be the fully qualified name (§6.3, p. 326) of the class. The actual_parameter_list is comma-separated if there is more than one parameter. The parentheses are mandatory even if the actual parameter list is empty. This distinguishes the method call from field access. One can specify fully qualified names for classes and packages using the dot operator (.).
objRef.doIt(time, place); // Explicit object reference int i = java.lang.Math.abs(-1); // Fully qualified class name int j = Math.abs(-1); // Simple class name someMethod(ofValue); // Object or class is implied someObjRef.make().make().make(); // make() returns a reference value
The dot operator (.) has left associativity. In the last line of the preceding code, the first call of the make() method returns a reference value that denotes the object on which to execute the next call, and so on. This is an example of call chaining.
Each actual parameter (also called an argument) is an expression that is evaluated, and whose value is passed to the method when the method is invoked. Its value can vary from invocation to invocation. Formal parameters are parameters defined in the method declaration and are local to the method.
It should also be stressed that each invocation of a method has its own copies of the formal parameters, as is the case for any local variables in the method. The JVM uses a stack to keep track of method execution and a heap to manage the objects that are created by the program (§7.1, p. 365). Values of local variables and those passed to the method as parameters, together with any temporary values computed during the execution of the method, are always stored on the stack. Thus only primitive values and reference values are stored on the stack, and only these can be passed as parameters in a method call, but never any object from the heap.
In Java, all parameters are passed by value—that is, an actual parameter is evaluated and its value from the stack is assigned to the corresponding formal parameter. Table 3.2 summarizes the value that is passed depending on the type of the parameters. In the case of primitive data types, the data value of the actual parameter is passed. If the actual parameter is a reference to an object, the reference value of the denoted object is passed and not the object itself. Analogously, if the actual parameter is an array element of a primitive data type, its data value is passed, and if the array element is a reference to an object, then its reference value is passed.
Table 3.2 Parameter Passing by Value
Data type of the formal parameter
Value passed
Primitive data type
Primitive data value of the actual parameter
Reference type (i.e., class, interface, array, or enum type)
Reference value of the actual parameter
The order of evaluation in the actual parameter list is always from left to right. The evaluation of an actual parameter can be influenced by an earlier evaluation of an actual parameter. Given the following declaration:
int i = 4;
the method call
leftRight(i++, i);
is effectively the same as
leftRight(4, 5);
and not the same as
leftRight(4, 4);
An overview of the conversions that can take place in a method invocation context is provided in §2.4, p. 48. Method invocation conversions for primitive values are discussed in the next subsection (p. 129), and those for reference types are discussed in §5.10, p. 265. Calling variable arity methods is discussed in the next section (p. 136).
For the sake of simplicity, the examples in subsequent sections primarily show method invocation on the same object or the same class. The parameter passing mechanism is no different when different objects or classes are involved.
The size of the array is specified in the array creation expression, which creates the array and initializes the array elements to their default values. By comparison, the following declaration statement both creates the array and initializes the array elements to specific values given in the array initializer:
However, the array initializer is not an expression. Java has another array creation expression, called an anonymous array, which allows the concept of the array creation expression from (1) to be combined with the array initializer from (2), so as to create and initialize an array:
This construct has enough information to create a nameless array of a specific type and specific length. Neither the name of the array nor the size of the array is specified. The construct returns the reference value of the newly created array, which can be assigned to references and passed as arguments in method calls. In particular, the following declaration statements are equivalent:
At (1), an array initializer is used to create and initialize the elements. At (2), an anonymous array expression is used. It is tempting to use the array initializer as an expression—for example, in an assignment statement, as a shortcut for assigning values to array elements in one go. However, this is not allowed; instead, an anonymous array expression should be used. The concept of the anonymous array combines the definition and the creation of the array into one operation.
In Example 3.8, an anonymous array is constructed at (1), and passed as an actual parameter to the static method findMinimum() defined at (2). Note that no array name or array size is specified for the anonymous array.
public class AnonArray { public static void main(String[] args) { System.out.println(“Minimum value: ” + findMinimum(new int[] {3, 5, 2, 8, 6})); // (1) } public static int findMinimum(int[] dataSeq) { // (2) // Assume the array has at least one element. int min = dataSeq[0]; for (int index = 1; index < dataSeq.length; ++index) if (dataSeq[index] < min) min = dataSeq[index]; return min; } }
This form of initialization applies to fields as well as to local arrays. The array_initialize_list is a comma-separated list of zero or more expressions. Such an array initializer results in the construction and initialization of the array.
In the declaration statement above, the variable anIntArray is declared as a reference to an array of ints. The array initializer results in the construction of an array to hold five elements (equal to the length of the list of expressions in the block), where the first element is initialized to the value of the first expression (13), the second element to the value of the second expression (49), and so on.
Pizza[] pizzaOrder = { new Pizza(), new Pizza(), null };
In this declaration statement, the variable pizzaOrder is declared as a reference to an array of Pizza objects. The array initializer constructs an array to hold three elements. The initialization code sets the first two elements of the array to refer to two Pizza objects, while the last element is initialized to the null reference. The reference value of the array of Pizza objects is assigned to the reference pizzaOrder. Note also that this declaration statement actually creates three objects: the array object with three references and the two Pizza objects.
The expressions in the array_initialize_list are evaluated from left to right, and the array name obviously cannot occur in any of the expressions in the list. In the preceding examples, the array_initialize_list is terminated by the right curly bracket, }, of the block. The list can also be legally terminated by a comma. The following array has length 2, and not 3:
Topping[] pizzaToppings = { new Topping(“cheese”), new Topping(“tomato”), };
The declaration statement at (1) in the following code defines an array of four String objects, while the declaration statement at (2) shows that a String object is not the same as an array of char.
Click here to view code image // Array with 4 String objects: String[] pets = {“crocodiles”, “elephants”, “crocophants”, “elediles”}; // (1)
// Array of 3 characters: char[] charArray = {‘a’, ‘h’, ‘a’}; // (2) Not the same as “aha”