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.
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); } }
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):
An array is a data structure that defines an indexed collection with a fixed number of data elements that all have the same type. A position in the array is indicated by a non-negative integer value called the index. An element at a given position in the array is accessed using the index. The size of an array is fixed and cannot be changed after the array has been created.
In Java, arrays are objects. Arrays can be of primitive data types or reference types. In the former case, all elements in the array are of a specific primitive data type. In the latter case, all elements are references of a specific reference type. References in the array can then denote objects of this reference type or its subtypes. Each array object has a public final field called length, which specifies the array size (i.e., the number of elements the array can accommodate). The first element is always at index 0 and the last element at index n – 1, where n is the value of the length field in the array.
Simple arrays are one-dimensional arrays—that is, a simple list of values. Since arrays can store reference values, the objects referenced can also be array objects. Thus a multidimensional arrays is implemented as an array of arrays (p. 124).
Passing array references as parameters is discussed in §3.10, p. 127. Type conversions for array references on assignment and on method invocation are discussed in §5.9, p. 261, and §5.10, p. 265, respectively.
Declaring Array Variables
A one-dimensional array variable declaration has either of the following syntaxes:
element_type [] array_name ;
or
element_type array_name [];
where element_type can be a primitive data type or a reference type. The array variable array_name has the type element_type[]. Note that the array size is not specified. As a consequence, the array variable array_name can be assigned the reference value of an array of any length, as long as its elements have element_type.
It is important to understand that the declaration does not actually create an array. Instead, it simply declares a reference that can refer to an array object. The [] notation can also be specified after a variable name to declare it as an array variable, but then it applies to just that variable.
int anIntArray[], oneInteger; Pizza[] mediumPizzas, largePizzas;
These two declarations declare anIntArray and mediumPizzas to be reference variables that can refer to arrays of int values and arrays of Pizza objects, respectively. The variable largePizzas can denote an array of Pizza objects, but the variable oneInteger cannot denote an array of int values—it is a simple variable of the type int.
An array variable that is declared as a field in a class, but is not explicitly initialized to any array, will be initialized to the default reference value null. This default initialization does not apply to local reference variables, and therefore, does not apply to local array variables either. This behavior should not be confused with initialization of the elements of an array during array construction.
An array can be constructed for a fixed number of elements of a specific type, using the new operator. The reference value of the resulting array can be assigned to an array variable of the corresponding type. The syntax of the array creation expression is shown on the right-hand side of the following assignment statement:
The minimum value of array_size is 0; in other words, zero-length arrays can be constructed in Java. If the array size is negative, a NegativeArraySizeException is thrown at runtime.
In the preceding syntax, the array type element_type2[] must be assignable to the array type element_type1[] (§5.8, p. 261). When the array is constructed, all of its elements are initialized to the default value for element_type2. This is true for both member and local arrays when they are constructed.
In the following examples, the code constructs the array, and the array elements are implicitly initialized to their default values. For example, all elements of the array anIntArray get the value 0, and all elements of the array mediumPizzas get the value null when the arrays are constructed.
int[] anIntArray = new int[10]; // Default element value: 0 Pizza[] mediumPizzas = new Pizza[5]; // Default element value: null
The value of the field length in each array is set to the number of elements specified during the construction of the array; for example, mediumPizzas.length has the value 5.
Once an array has been constructed, its elements can also be explicitly initialized individually—for example, in a loop. The examples in the rest of this section make use of a loop to iterate over the elements of an array for various purposes.
Static members belong to the class in which they are declared and are not part of any instance of the class. The declaration of static members is prefixed by the keyword static to distinguish them from instance members.
Static code inside a class can access a static member in the following three ways:
By the static member’s simple name
By using the class name with the static member’s name
By using an object reference of the static member’s class with the static member’s name
Depending on the access modifier of the static members declared in a class, clients can only access these members by using the class name or using an object reference of their class.
The class need not be instantiated to access its static members. This is in contrast to instance members of the class which can only be accessed by references that actually refer to an instance of the class.
Static Fields in Classes
Static fields (also called static variables and class variables) exist only in the class in which they are defined. When the class is loaded, static fields are initialized to their default values if no explicit initialization is specified. They are not created when an instance of the class is created. In other words, the values of these fields are not a part of the state of any object. Static fields are akin to global variables that can be shared with all objects of the class and with other clients, if necessary.
Light.counter == dimLight.counter: true Calling static method using class name: 2, 2, 2 Local counter: 10 Static counter: 2 Calling static method using object reference: 2, 2, 2 Local counter: 10 Static counter: 2
In Example 3.6, the static field counter at (1) will be initialized to the default value 0 when the class is loaded at runtime, since no initializer expression is specified. The print statement at (2) in the static method printCount() shows how this static field can be accessed in three different ways, respectively: simple name counter, the class name Light, and object reference myLight of class Light, although no object has been created.
Shadowing of fields by local variables is different from hiding of fields by field declarations in subclasses. In Example 3.6, a local variable is declared at (3) that has the same name as the static field. Since this local variable shadows the static field, the simple name at (4) now refers to the local variable, as shown by the output from the program. The shadowed static field can of course be accessed using the class name, as shown at (5). It is the local variable that is accessed by its simple name as long as it is in scope.
Trying to access the static field with the this reference at (6) results in a compile-time error, since the this reference cannot be used in static code. Invoking the non-static method at (7) also results in a compile-time error, since static code cannot refer to non-static members by its simple name in the class.
The print statement at (8) in the method printNonStatic() illustrates referring to static members in non-static code: It refers to the static field counter by its simple name, with the this reference, and using the class name.
In Example 3.6, the class StaticTest is a client of the class Light. The client must use the class name or an object reference of class Light at (9) and (10), respectively, to access the static field counter in the class Light. The result from the print statement at (11) shows that these two ways of accessing a static field are equivalent.
An actual parameter is an expression that is evaluated first, with the resulting value then being assigned to the corresponding formal parameter at method invocation. The use of this value in the method has no influence on the actual parameter. In particular, when the actual parameter is a variable of a primitive data type, the value of the variable from the stack is copied to the formal parameter at method invocation. Since formal parameters are local to the method, any changes made to the formal parameter will not be reflected in the actual parameter after the call completes.
Legal type conversions between actual parameters and formal parameters of primitive data types are summarized here from Table 2.17, p. 47:
Primitive widening conversion
Unboxing conversion, followed by an optional widening primitive conversion
These conversions are illustrated by invoking the following method
Integer intRef = 34; Long longRef = 34L; doIt(34); // (1) Primitive widening conversion: long <– int doIt(longRef); // (2) Unboxing: long <– Long doIt(intRef); // (3) Unboxing, followed by primitive widening conversion: // long <– int <– Integer
However, for parameter passing, there are no implicit narrowing conversions for integer constant expressions (§2.4, p. 48).
Value of pricePrPizza before call: 15 Changed pizza price in the method: 7.5 Value of pricePrPizza after call: 15
In Example 3.10, the method calcPrice() is defined in the class PizzaFactory at (2). It is called from the CustomerOne.main() method at (1). The value of the first actual parameter, 4, is copied to the int formal parameter numberOfPizzas. Note that the second actual parameter pricePrPizza is of the type int, while the corresponding formal parameter pizzaPrice is of the type double. Before the value of the actual parameter pricePrPizza is copied to the formal parameter pizzaPrice, it is implicitly widened to a double. The passing of primitive values is illustrated in Figure 3.2.
Figure 3.2 Parameter Passing: Primitive Data Values
The value of the formal parameter pizzaPrice is changed in the calcPrice() method, but this does not affect the value of the actual parameter pricePrPizza on return. It still has the value 15. The bottom line is that the formal parameter is a local variable, and changing its value does not affect the value of the actual parameter.
The discussion of passing reference values in the previous section is equally valid for arrays, as arrays are objects in Java. Method invocation conversions for array types are discussed along with those for other reference types in §5.10, p. 265.
In Example 3.12, the idea is to repeatedly swap neighboring elements in an integer array until the largest element in the array percolates to the last position in the array.
public class Percolate { public static void main (String[] args) { int[] dataSeq = {8,4,6,2,1}; // Create and initialize an array. // Write array before percolation: printIntArray(dataSeq); // Percolate: for (int index = 1; index < dataSeq.length; ++index) if (dataSeq[index-1] > dataSeq[index]) swap(dataSeq, index-1, index); // (1) // Write array after percolation: printIntArray(dataSeq); } public static void swap(int[] intArray, int i, int j) { // (2) int tmp = intArray[i]; intArray[i] = intArray[j]; intArray[j] = tmp; } public static void swap(int v1, int v2) { // (3) Logical error! int tmp = v1; v1 = v2; v2 = tmp; } public static void printIntArray(int[] array) { // (4) for (int value : array) System.out.print(” ” + value); System.out.println(); } }
Output from the program:
8 4 6 2 1 4 6 2 1 8
Note that in the declaration of the method swap() at (2), the formal parameter intArray is of the array type int[]. The other two parameters are of type int. They denote the values in the array that should be swapped. The signature of the method is
swap(int[], int, int)
This swap() method is called in the main() method at (1), where one of the actual parameters is the array variable dataSeq. The reference value of the array variable dataSeq is assigned to the array variable intArray at method invocation. After return from the call to the swap() method, the array variable dataSeq will reflect the changes made to the array via the corresponding formal parameter. This situation is depicted in Figure 3.4 at the first call and return from the swap() method, indicating how the values of the elements at indices 0 and 1 in the array have been swapped.
Figure 3.4 Parameter Passing: Arrays
However, the declaration of the swap() method at (3) will not swap two values. The method call
will result in the swap() method at (3) to be invoked. Its execution will have no effect on the array elements, as the swapping is done on the values of the formal parameters.
The method printIntArray() at (4) also has a formal parameter of array type int[]. Note that the formal parameter is specified as an array reference using the [] notation, but this notation is not used when an array is passed as an actual parameter.
A variable declaration requires the type of the variable to be specified in the declaration. However, in the case of local variables, the type can be specified by the reserved type name var, if the local declaration also specifies an initialization expression in the declaration. The compiler uses the type of the initialization expression to infer the type of the local variable. The restricted type name var denotes this inferred type in the local declaration. This is an example of type inference, where the type of a variable or an expression is derived from the context in which it is used. If the compiler cannot infer the type, it reports a compile-time error. A local variable declaration that uses var is also called a var declaration. A local variable declared this way is no different from any other local variable.
It is important to note that the type of the local variable is solely inferred from the initialization expression specified in the declaration. The following variable declaration in a local context (e.g., body of a method) is declared using the reserved type name var:
var year = 2022;
The compiler is able to infer that the type of the initialization expression 2022 is int in the above declaration, and therefore the variable year has the type int. The declaration above is equivalent to the declaration below, where the type is explicitly specified:
int year = 2022;
A cautionary note going forward: This subsection refers to many concepts and constructs that might not be familiar at this stage. It might be a good idea to get an overview now and to come back later for a more thorough review of this topic. The exhaustive index at the end of the book can of course be used at any time to look up a topic.
The class ValidLVTI in Example 3.17 illustrates valid uses of the restricted type name var. The comments in the code should be self-explanatory.
The var restricted type name is allowed in local variable declarations in blocks (including initializer blocks), constructors, and methods, as can be seen in the class ValidLVTI at (1a), (1b), and (2) and the method main(), respectively.
Note that at (3b) and (3c), the compiler is able to infer the type of the local variable from the return type of the method on the right-hand side.
It is worth noting that the cast operator, (), can be necessary to indicate the desired type, as shown at (5) and (7).
For array variables, the initialization expression must be an array creation expression that allows the array size and the array element type to be inferred, as shown at (11a), (11b), (11c), and (11d). A local declaration with var requires an initialization expression, which in the case of local arrays must be either an array creation expression or an anonymous array expression. In other words, it should be possible to infer both the array element type and the size of the array. It cannot be an array initializer.
The bodies (and the headers) of the for(;;) and for(:) loops can define their own local variables in their block scope. The type of the local variable vowel at (13) is inferred to be char from the array vowels (of type char[]) in the header of the for(:) loop. The type of the local variable i in the header of the for(;;) loop at (16) is determined to be int from the initial value. The switch statement also defines its own block scope in which local variables can be declared, as shown at (18).
Example 3.17 Illustrating Local Variable Type Reference
// Class ValidLVTI illustrates valid use of the restricted type name var. public class ValidLVTI { // Static initializer block: static { var slogan = “Keep calm and code Java.”; // (1a) Allowed in static } // initializer block // Instance initializer block: { var banner = “Keep calm and catch exceptions.”; // (1b) Allowed in instance } // initializer block // Constructor: public ValidLVTI() { var luckyNumber = 13; // (2) Allowed in a constructor. } // Method: public static void main(String[] args) {
var virus = “COVID-19”; // (3a) Type of virus is String. var acronym = virus.substring(0, 5); // (3b) Type of acronym is String. var num = Integer.parseInt(virus.substring(6)); // (3c) Type of num is int. var obj = new Object(); // (4) Type of obj is Object. var title = (String) null; // (5) Initialization expression type is String. // Type of title is String. var sqrtOfNumber = Math.sqrt(100); // (6) Type of sqrtOfNumber is double, // since the method returns // a double value. var tvSize = (short) 55; // (7) Type of tvSize is short. var tvSize2 = 65; // (8) Type of tvSize2 is int. var diameter = 10.0; // (9) Type of diameter is double. var radius = 2.5F; // (10) Type of radius is float. // Arrays: var vowels = new char[] {‘a’, ‘e’, ‘i’, ‘o’, ‘u’ }; // (11a) Type of vowels // is char[]. Size is 5. var zodiacSigns = new String[12]; // (11b) Type of zodiacSigns is String[]. // Size is 12. var a_2x3 = new int[2][3]; // (11c) Type of a_2x3 is int[][]. Size is 2×3. var a_2xn = new int[2][]; // (11d) Type of a_2xn is int[][]. Size is 2x?, // where second dimension can be undefined. // The for(:) loop: var word1 = “”; // (12) Type of word2 is String. for (var vowel : vowels) { // (13) Type of vowel is char in the for(:)loop. var letter = vowel; // (14) Type of letter is char. word1 += letter; } // The for(;;) loop: var word2 = “”; // (15) Type of word2 is String. for (var i = 0; i < vowels.length; i++) { // (16) Type of i is int in // the for loop. var letter = vowels[i]; // (17) Type of letter is char. word2 += letter; } // switch-statement: switch(virus) { case “Covid-19”: var flag = “Needs to be tested.”; // (18) Type is String. // Do testing. break; default: // Do nothing. } } }
The if-else statement is used to decide between two actions, based on a condition. It has the following syntax:
if ( condition ) statement 1 else statement 2
The condition is evaluated first. If its value is true (or unboxed to true), statement1 (the if block) is executed and then execution continues with the rest of the program. If the value is false (or unboxed to false), statement2 (the else block) is executed and then execution continues with the rest of the program. In other words, one of two mutually exclusive actions is performed. The else clause is optional; if omitted, the construct is equivalent to the simple if statement. The semantics are illustrated by the activity diagram in Figure 4.1b.
In the following examples of the if-else statement, it is assumed that all variables and methods have been appropriately defined:
if (temperature >= upperLimit) { // (1) if (danger) // (2) Simple if. soundAlarm(); if (critical) // (3) evacuate(); else // Goes with if at (3). turnHeaterOff(); } else // Goes with if at (1). turnHeaterOn();
The use of block notation, {}, can be critical to the execution of if statements. The if statements (A) and (B) in the following examples do not have the same meaning. The if statements (B) and (C) are the same, with extra indentation used in (C) to make the meaning evident. Leaving out the block notation in this case could have catastrophic consequences: The heater could be turned on when the temperature is above the upper limit.
// (A): if (temperature > upperLimit) { // (1) Block notation. if (danger) soundAlarm(); // (2) } else // Goes with if at (1). turnHeaterOn(); // (B): if (temperature > upperLimit) // (1) Without block notation. if (danger) soundAlarm(); // (2) else turnHeaterOn(); // Goes with if at (2). // (C): if (temperature > upperLimit) // (1) if (danger) // (2) soundAlarm(); else // Goes with if at (2). turnHeaterOn();
The rule for matching an else clause is that an else clause always refers to the nearest if that is not already associated with another else clause. Block notation and proper indentation can be used to make the meaning obvious.
Cascading of if-else statements comprises a sequence of nested if-else statements where the if block of the next if-else statement is joined to the else clause of the previous if-else statement. The decision to execute a block is then based on all the conditions evaluated so far.
The block corresponding to the first if condition that evaluates to true is executed, and the remaining if statements are skipped. In the preceding example, the block at (3) will execute only if the conditions at (1) and (2) are false and the condition at (3) is true. If none of the conditions is true, the block associated with the last else clause is executed. If there is no last else clause, no actions are performed.