Analogous to sealed classes, sealed interfaces can also be defined. However, a sealed interface can have both subinterfaces and subclasses as its permitted direct subtypes. The permitted subtypes can be subclasses that implement the sealed interface and subinterfaces that extend the sealed interface. This is in contrast to a sealed class that can have direct subclasses, but not direct subinterfaces—as classes cannot be extended or implemented by interfaces.
Figure 5.10 shows the book domain from Figure 5.9 that has been augmented with a sealed interface that specifies its permitted direct subtypes in a permits clause:
public sealed interface Subscribable permits Ebook, Audiobook, VIPSubcribable {}
The sealed superinterface Subscribable has two final permitted direct subclasses (that implement the superinterface) and one non-sealed direct subinterface (that extends the superinterface). The declarations of the permitted direct subclasses Ebook and Audiobook have been updated accordingly so that they implement the direct superinterface Subscribable.
public final class Ebook extends Book implements Subscribable {} public final class Audiobook extends Book implements Subscribable {} public non-sealed interface VIPSubcribable extends Subscribable {}
public abstract sealed class Book permits PrintedBook, Ebook, Audiobook {} public non-sealed class PrintedBook extends Book {}
Note that it is perfectly possible for a class or an interface to be a permitted direct subtype of more than one direct supertype—as is the case for the Ebook and the Audiobook subclasses in Figure 5.10.
Figure 5.10 Sealed Classes and Interfaces
We see from the discussion above that the permitted direct subtypes of a sealed superinterface abide by the same contract rules as for sealed superclasses:
A permitted direct subclass or subinterface must extend or implement its direct superinterface, respectively.
Any permitted subclass of a sealed interface must be declared either sealed, non-sealed or final, but any permitted subinterface can only be declared either sealed or non-sealed. The modifier final is not allowed for interfaces.
The same rules for locality also apply for sealed interfaces and their permitted direct subtypes: All are declared either in the same named module or in the same package (named or unnamed) in the unnamed module.
Enum and Record Types as Permitted Direct Subtypes
By definition, an enum type (p. 287) is either implicitly final (has no enum constants that have a class body, as shown at (2)) or implicitly sealed (has at least one enum constant with a class body that constitutes an implicitly declared direct subclass, as shown at (3) for the constant DVD_R that has an empty class body). Thus an enum type can be specified as a permitted direct subtype of a sealed superinterface, as shown at (1). The modifiers final and sealed cannot be explicitly specified in the enum type declaration.
sealed interface MediaStorage permits CD, DVD {} // (1) Sealed interface enum CD implements MediaStorage {CD_ROM, CD_R, CD_W} // (2) Implicitly final enum DVD implements MediaStorage {DVD_R {}, DVD_RW} // (3) Implicitly sealed
Analogously, a record class (p. 299) is implicitly final, and can be specified as a permitted direct subtype of a sealed superinterface. The sealed interface MediaStorage at (1a) now permits the record class HardDisk as a direct subtype. Again note that the modifier final cannot be specified in the header of the HardDisk record class declared at (4).
A throws clause can be specified in a method or a constructor header to declare any checked exceptions that can be thrown by a statement in the body of a method or a constructor. It is declared immediately preceding the body of the method or the constructor.
… throws ExceptionType 1 , ExceptionType 2 ,…, ExceptionType n { /* Body */ }
Each ExceptionTypei is an exception type (i.e., a Throwable or one of its subclasses), although usually only checked exceptions are specified. The compiler enforces that if a checked exception can be thrown from the body of the method or the constructor, then either the type of this exception or a supertype of its exception type is specified in the throws clause of the method or the constructor. The throws clause can specify unchecked exceptions, but this is seldom done and the compiler does not enforce any restrictions on their usage.
The throws clause is part of the contract that a method or a constructor offers to its clients. The throws clause can specify any number of exception types in any order, even those that are not thrown by the method or the constructor. The compiler simply ensures that any checked exception that can actually be thrown in the method or constructor body is covered by the throws clause. Of course, any caller of the method or constructor cannot ignore the checked exceptions specified in the throws clause.
In a method or a constructor, a checked exception can be thrown directly by a throw statement, or indirectly by calling other methods or constructors that can throw a checked exception. If a checked exception is thrown, the code must obey the following rule (known by various names: catch-or-declare rule, handle-or-declare rule, catch-or-specify requirement):
Either use a try block and catch the checked exception in a catch block and deal with it
Or explicitly allow propagation of the checked exception to its caller by declaring it in the throws clause
Note that catching and dealing with a checked exception does not necessarily imply resumption of normal execution. A catch clause can catch the checked exception and choose to throw some other exception or even the same exception that is either unchecked or declared in the throws clause (p. 401). This rule ensures that a checked exception will be dealt with, regardless of the path of execution. This aids development of robust programs, as allowance can be made for many contingencies.
In Example 7.8, a new checked exception is defined, where the checked exception class IntegerDivisionByZero extends the Exception class. The method call at (2) in the try block at (1) results in the printAverage() method at (6) to be executed. The method call at (7) results in the computeAverage() method at (8) to be executed.
In the if statement at (9), the method computeAverage() throws the checked exception IntegerDivisionByZero. Neither the computeAverage() method nor the printAverage() method catches the exception, but instead throws it to the caller, as declared in the throws clauses in their method headers at (6) and (8). The exception propagates to the main() method. Since the printAverage() method was called from the context of the try block at (1) in the main() method, the exception is successfully caught by its catch clause at (3). The exception is handled and the finally clause at (4) is executed, with normal execution resuming from (5). If the method main() did not catch the exception, it would have to declare this exception in a throws clause. In that case, the exception would end up being handled by the default exception handler.
Computing average. IntegerDivisionByZero: Integer Division By Zero at Average8.computeAverage(Average8.java:27) at Average8.printAverage(Average8.java:17) at Average8.main(Average8.java:5) Exception handled in main(). Finally done in main(). Exit main().
As mentioned earlier, the exception type specified in the throws clause can be a superclass of the actual exceptions thrown—that is, the exceptions thrown must be assignable to the type of the exceptions specified in the throws clause. If a method or a constructor can throw a checked exception, then the throws clause must declare its exception type or a supertype of its exception type; otherwise, a compile-time error will occur. In the printAverage() method, the superclass Exception of the subclass IntegerDivisionByZero could be specified in the throws clause of the method. This would also entail that the main() method either catch an Exception or declare it in a throws clause.
public static void main(String[] args) throws Exception { /* … */ } public static void printAverage(int totalSum, int totalCount) throws Exception { /* … */ }
It is generally considered bad programming style to specify exception superclasses in the throws clause when the actual exceptions thrown are instances of their subclasses. It is also recommended to use the @throws tag in a Javadoc comment to document the checked exceptions that a method or a constructor can throw, together with any unchecked exceptions that might also be relevant to catch.
Java provides explicit access modifiers to control the accessibility of members in a class by external clients, but in two areas access is governed by specific scope rules:
Class scope for members: how member declarations are accessed within the class
Block scope for local variables: how local variable declarations are accessed within a block
Class Scope for Members
Class scope concerns accessing members (including inherited ones) from code within a class. Table 6.4 gives an overview of how static and non-static code in a class can access members of the class, including those that are inherited. Table 6.4 assumes the following declarations:
The golden rule is that static code can only access other static members by their simple names. Static code is not executed in the context of an object, so the references this and super are not available. An object has knowledge of its class, so static members are always accessible in a non-static context.
Note that using the class name to access static members within the class is no different from how external clients access these static members.
The following factors can all influence the scope of a member declaration:
Shadowing of a field declaration, either by local variables (p. 354) or by declarations in the subclass (§5.1, p. 203)
Overriding an instance method from a superclass (§5.1, p. 196)
Hiding a static method declared in a superclass (§5.1, p. 203)
Within a class, references of the class can be declared and used to access all members in the class, regardless of their access modifiers. In Example 6.9, the method duplicateLight at (1) in the class Light has the parameter oldLight and the local variable newLight that are references of the class Light. Even though the fields of the class are private, they are accessible through the two references (oldLight and newLight) in the method duplicateLight(), as shown at (2), (3), and (4).
Exceptions in Java are objects. All exceptions are derived from the java.lang.Throwable class. Figure 7.3 shows a partial hierarchy of classes derived from the Throwable class. The two main subclasses Exception and Error constitute the main categories of throwables, the term used to refer to both exceptions and errors. Figure 7.3 also shows that not all exception classes are found in the java.lang package.
All throwable classes in the Java SE Platform API at least define a zero-argument constructor and a one-argument constructor that takes a String parameter. This parameter can be set to provide a detail message when an exception is constructed. The purpose of the detail message is to provide more information about the actual exception.
Throwable() Throwable(String msg)
The first constructor constructs a throwable that has null as its detail message. The second constructor constructs a throwable that sets the specified string as its detail message.
Most exception types provide analogous constructors.
The class Throwable provides the following common methods to query an exception:
String getMessage()
Returns the detail message.
void printStackTrace()
Prints the stack trace on the standard error stream. The stack trace comprises the method invocation sequence on the JVM stack when the exception was thrown. The stack trace can also be written to a PrintStream or a PrintWriter by supplying such a destination as an argument to one of the two overloaded printStackTrace() methods. Any suppressed exceptions associated with an exception on the stack trace are also printed (p. 415). It will also print the cause of an exception (which is also an exception) if one is available (p. 405).
String toString()
Returns a short description of the exception, which typically comprises the class name of the exception together with the string returned by the getMessage() method.
In dealing with throwables, it is important to recognize situations in which a particular throwable can occur, and the source that is responsible for throwing it. By source we mean:
The JVM that is responsible for throwing the throwable, or
The throwable that is explicitly thrown programmatically by the code in the application or by any API used by the application.
In further discussion of exception types, we provide an overview of situations in which selected throwables can occur and the source responsible for throwing them.
A package hierarchy represents an organization of the Java classes and interfaces. It does not represent the source code organization of the classes and interfaces. The source code is of no consequence in this regard. Each Java source file (also called compilation unit) can contain zero or more type declarations, but the compiler produces a separate class file containing the Java bytecode for each of them. A type declaration can indicate that its Java bytecode should be placed in a particular package, using a package declaration.
At most, one package declaration can appear in a source file, and it must be the first statement in the source file. The package name is saved in the Java bytecode of the types contained in the package. Java naming conventions recommend writing package names in lowercase letters.
Note that this scheme has two consequences. First, all the classes and interfaces in a source file will be placed in the same package. Second, several source files can be used to specify the contents of a package.
If a package declaration is omitted in a compilation unit, the Java bytecode for the declarations in the compilation unit will belong to an unnamed package (also called the default package), which is typically synonymous with the current working directory on the host system.
Example 6.1 illustrates how the packages in Figure 6.2 can be defined using the package declaration. There are four compilation units. Each compilation unit has a package declaration, ensuring that the type declarations are compiled into the correct package. The complete code can be found in Example 6.7, p. 345.
Example 6.1 Defining Packages and Using Type Import
// File name: Baldness.java // This file has 2 type declarations package wizard.spells; // Package declaration import wizard.pandorasbox.*; // (1) Type-import-on-demand import wizard.pandorasbox.artifacts.*; // (2) Import from subpackage public class Baldness extends Ailment { // Simple name for Ailment wizard.pandorasbox.LovePotion tlcOne; // (3) Fully qualified class name LovePotion tlcTwo; // Class in same package // … } class LovePotion { /* … */ }
Shared resources are typically implemented using synchronized code in order to guarantee thread safety of the shared resource (§22.4, p. 1387). However, if the shared resource is an immutable object, thread safety comes for free.
An object is immutable if its state cannot be changed once it has been constructed. Since its state can only be read, there can be no thread interference and the state is always consistent.
Some examples of immutable classes from the Java SE Platform API are listed in Table 6.5. Any method that seemingly modifies the state of an immutable object is in fact returning a new immutable object with the state modified appropriately based on the original object. Primitive values are of course always immutable.
Appointments(week 45):[5, 3, 8, 10, 7, 8, 9] Exception in thread “main” java.lang.IllegalArgumentException: Stats not for whole week: [10, 5, 20, 7] at WeeklyStats.<init>(WeeklyStats.java:14) at StatsClient.main(StatsClient.java:7)
There are certain guidelines that can help to avoid common pitfalls when implementing immutable classes. We will illustrate implementing an immutable class called WeeklyStats in Example 6.10, whose instances, once created, cannot be modified. The class WeeklyStats creates an object with weekly statistics of a specified entity.
It should not be possible to extend the class.
Caution should be exercised in extending an immutable class to prevent any subclass from subverting the immutable nature of the superclass.
A straightforward approach is to declare the class as final, as was done in Example 6.10 at (1) for the class WeeklyStats. Another approach is to declare the constructor as private and provide static factory methods to construct instances (discussed below). A static factory method is a static method whose sole purpose is to construct and return a new instance of the class—an alternative to calling the constructor directly.
All fields should be declared final and private.
Declaring the fields as private makes them accessible only inside the class, and other clients cannot access and modify them. This is the case for the fields in the WeeklyStats class at (2), (3), and (4).
Declaring a field as final means the value stored in the field cannot be changed once initialized. However, if the final field is a reference to an object, the state of this object can be changed by other clients who might be sharing this object, unless the object is also immutable. See the last guideline on how to safeguard the state of an mutable object referenced by a field.
Check the consistency of the object state at the time the object is created.
Since it is not possible to change the state of an immutable object, the state should be checked for consistency when the object is created. If all relevant information to initialize the object is available when it is created, the state can be checked for consistency and any necessary measures taken. For example, a suitable exception can be thrown to signal illegal arguments.
In the class WeeklyStats, the constructor at (5) is passed all the necessary values to initialize the object, and it checks whether they will result in a legal and consistent state for the object.
No set methods (a.k.a. setter or mutator methods) should be provided.
Set methods that change values in fields or objects referenced by fields should not be permitted. The class WeeklyStats does not have any set methods, and only provides get methods (a.k.a. getter or assessor methods).
If a setter method is necessary, then the method should create a new instance of the class based on the modified state, and return that to the client, leaving the original instance unmodified. This approach has to be weighed against the cost of creating new instances, but is usually offset by other advantages associated with using immutable classes, like thread safety without synchronized code. Caching frequently used objects can alleviate some overhead of creating new objects, as exemplified by the immutable wrapper classes for primitive types. For example, the Boolean class has a static factory method valueOf() that always returns one of two objects, Boolean.TRUE or Boolean.FALSE, depending on whether its boolean argument was true or false, respectively. The Integer class interns values between –128 and 127 for efficiency so that there is only one Integer object to represent each int value in this range.
A client should not be able to access mutable objects referred to by any fields in the class.
The class should not provide any methods that can modify its mutable objects. The class WeeklyStats complies with this requirement.
A class should also not share references to its mutable objects. The field at (4) has the type array of int that is mutable. An int array is passed as a parameter to the constructor at (5). The constructor in this case makes its own copy of this int array, so as not to share the array passed as an argument by the client. The getWeeklyStats() method at (8) does not return the reference value of the int array stored in the field stats. It creates and returns a new int array with values copied from its private int array. This technique is known as defensive copying. This way, the class avoids sharing references of its mutable objects with clients.
The class declaration below illustrates another approach to prevent a class from being extended. The class WeeklyStats is no longer declared final at (1), but now has a private constructor. This constructor at (5a) cannot be called by any client of the class to create an object. Instead, the class provides a static factory method at (5b) that creates an object by calling the private constructor. No subclass can be instantiated, as the superclass private constructor cannot be called, neither directly nor implicitly, in a subclass constructor.
public class WeeklyStatsV2 { // (1) Class is not final. … private WeeklyStatsV2(String description, int weekNumber, int[] stats) { // (5a) Private constructor this.description = description; this.weekNumber = weekNumber; this.stats = Arrays.copyOf(stats, stats.length); // Create a private copy. } // (5b) Static factory method to construct objects. public static WeeklyStatsV2 getNewWeeklyStats(String description, int weekNumber, int[] stats) { if (weekNumber <= 0 || weekNumber > 52) { throw new IllegalArgumentException(“Invalid week number: ” + weekNumber); } if (stats.length != 7) { throw new IllegalArgumentException(“Stats not for whole week: ” + Arrays.toString(stats)); } return new WeeklyStatsV2(description, weekNumber, stats); } … }
A class having just static methods is referred to as a utility class. Such a class cannot be instantiated and has no state, and is thus immutable. Examples of such classes in the Java API include the following: the java.lang.Math class, the java.util.Collections class, the java.util.Arrays class, and the java.util.concurrent.Executors class.
Apart from being thread safe, immutable objects have many other advantages. Once created, their state is guaranteed to be consistent throughout their lifetime. That makes them easy to reason about. Immutable classes are relatively simple to construct, amenable to testing, and easy to use compared to mutable classes. There is hardly any need to make or provide provisions for making copies of such objects. Their hash code value, once computed, can be cached for later use, as it will never change. Because of their immutable state, they are ideal candidates for keys in maps, and as elements in sets. They also are ideal building blocks for new and more complex objects.
A downside of using an immutable object is that if a value must be changed in its state, then a new object must be created, which can be costly if object construction is expensive.
7.1 Stack-Based Execution and Exception Propagation
The exception mechanism is built around the throw-and-catch paradigm. To throw an exception is to signal that an unexpected event has occurred. To catch an exception is to take appropriate action to deal with the exception. An exception is caught by an exception handler, and the exception need not be caught in the same context in which it was thrown. The runtime behavior of the program determines which exceptions are thrown and how they are caught. The throw-and-catch principle is embedded in the try-catch-finally construct (p. 375).
Several threads can be executing at the same time in the JVM (§22.2, p. 1369). Each thread has its own JVM stack (also called a runtime stack, call stack, or invocation stack in the literature) that is used to handle execution of methods. Each element on the stack is called an activation frame or a stack frame and corresponds to a method call. Each new method call results in a new activation frame being pushed on the stack, which stores all the pertinent information such as the local variables. The method with the activation frame on the top of the stack is the one currently executing. When this method finishes executing, its activation frame is popped from the top of the stack. Execution then continues in the method corresponding to the activation frame that is now uncovered on the top of the stack. The methods on the stack are said to be active, as their execution has not completed. At any given time, the active methods on a JVM stack make up what is called the stack trace of a thread’s execution.
Example 7.1 is a simple program to illustrate method execution. It calculates the average for a list of integers, given the sum of all the integers and the number of integers. It uses three methods:
The method main() calls the method printAverage() with parameters supplying the total sum of the integers and the total number of integers, (1).
The method printAverage() in turn calls the method computeAverage(), (3).
The method computeAverage() uses integer division to calculate the average and returns the result, (7).
Execution of Example 7.1 is illustrated in Figure 7.1. Each method execution is shown as a box with the local variables declared in the method. The height of the box indicates how long a method is active. Before the call to the method System.out.println() at (6) in Figure 7.1, the stack trace comprises the three active methods: main(), printAverage(), and computeAverage(). The result 5 from the method computeAverage() is returned at (7) in Figure 7.1. The output from the program corresponds with the sequence of method calls in Figure 7.1. As the program terminates normally, this program behavior is called normal execution.
Computing average. Exception in thread “main” java.lang.ArithmeticException: / by zero at Average1.computeAverage(Average1.java:18) at Average1.printAverage(Average1.java:10) at Average1.main(Average1.java:5)
Figure 7.2 illustrates the program execution when the method printAverage() is called with the arguments 100 and 0 at (1). All goes well until the return statement at (7) in the method computeAverage() is executed. An error event occurs in calculating the expression sum/number because integer division by 0 is an illegal operation. This event is signaled by the JVM by throwing an ArithmeticException (p. 372). This exception is propagated by the JVM through the JVM stack as explained next.
Figure 7.1 Normal Method Execution
Figure 7.2 illustrates the case where an exception is thrown and the program does not take any explicit action to deal with the exception. In Figure 7.2, execution of the computeAverage() method is suspended at the point where the exception is thrown. The execution of the return statement at (7) never gets completed. Since this method does not have any code to deal with the exception, its execution is likewise terminated abruptly and its activation frame popped. We say that the method completes abruptly. The exception is then offered to the method whose activation is now on the top of the stack (printAverage()). This method does not have any code to deal with the exception either, so its execution completes abruptly. The statements at (4) and (5) in the method printAverage() never get executed. The exception now propagates to the last active method (main()). This does not deal with the exception either. The main() method also completes abruptly. The statement at (2) in the main() method never gets executed. Since the exception is not caught by any of the active methods, it is dealt with by the main thread’s default exception handler. The default exception handler usually prints the name of the exception, with an explanatory message, followed by a printout of the stack trace at the time the exception was thrown. An uncaught exception, as in this case, results in the death of the thread in which the exception occurred.
Figure 7.2 Exception Propagation
If an exception is thrown during the evaluation of the left-hand operand of a binary expression, then the right-hand operand is not evaluated. Similarly, if an exception is thrown during the evaluation of a list of expressions (e.g., a list of actual parameters in a method call), evaluation of the rest of the list is skipped.
If the line numbers in the stack trace are not printed in the output as shown previously, use the following command to run the program:
If the try block executes, then the finally clause is guaranteed to be executed, regardless of whether any catch clause was executed, barring the two special cases (JVM crashes or the System.exit() method is called). Since the finally clause is always executed before control transfers to its final destination, the finally clause can be used to specify any clean-up code (e.g., to free resources such as files and network connections). However, the try-with-resources statement provides a better solution for handling resources, and eliminates the use of the finally clause in many cases (p. 407).
A try-finally construct can be used to control the interplay between two actions that must be executed in the correct order, possibly with other intervening actions. In the code below, the operation in the calculateAverage() method (called at (2)) is dependent on the success of the sumNumbers() method (called at (1)). The if statement at (2) checks the value of the sum variable before calling the calculateAverage() method:
int sum = 0; try { sum = sumNumbers(); // (1) // other actions } finally { if (sum > 0) calculateAverage(); // (2) }
This code guarantees that if the try block is entered, the sumNumbers() method will be executed first, and later the calculateAverage() method will be executed in the finally clause, regardless of how execution proceeds in the try block. We can, if desired, include any catch clauses to handle any exceptions.
If the finally clause neither throws an exception nor executes a control transfer statement like a return or a labeled break, the execution of the try block or any catch clause determines how execution proceeds after the finally clause (Figure 7.4, p. 376).
If no exception is thrown during execution of the try block or the exception has been handled in a catch clause, normal execution continues after the finally clause.
If there is any uncaught exception (either because no matching catch clause was found or because the catch clause threw an exception), the method completes abruptly and the exception is propagated after the execution of the finally clause.
The output of Example 7.4 shows that the finally clause at (4) is executed, regardless of whether an exception is thrown in the try block at (2). If an Arithmetic-Exception is thrown, it is caught and handled by the catch clause at (3). After the execution of the finally clause at (4), normal execution continues at (5).
Computing average. java.lang.ArithmeticException: / by zero at Average4.computeAverage(Average4.java:24) at Average4.printAverage(Average4.java:10) at Average4.main(Average4.java:4) Exception handled in printAverage(). Finally done. Exit printAverage(). Exit main().
On exiting from the finally clause, if there is any uncaught exception, the method completes abruptly and the exception is propagated as explained earlier. This is illustrated in Example 7.5. The method printAverage() is aborted after the finally clause at (3) has been executed, as the ArithmeticException thrown at (4) is not caught by any method. In this case, the exception is handled by the default exception handler. Notice the difference in the output from Example 7.4 and Example 7.5.
Computing average. Finally done. Exception in thread “main” java.lang.ArithmeticException: / by zero at Average5.computeAverage(Average5.java:21) at Average5.printAverage(Average5.java:10) at Average5.main(Average5.java:4)
If the finally clause executes a control transfer statement, such as a return or a labeled break, this control transfer statement determines how the execution will proceed—regardless of how the try block or any catch clause was executed. In particular, a value returned by a return statement in the finally clause will supercede any value returned by a return statement in the try block or a catch clause.
Example 7.6 shows how the execution of a control transfer statement such as a return in the finally clause affects the program execution. The first output from the program shows that the average is computed but the value returned is from the return statement at (3) in the finally clause, not from the return statement at (2) in the try block. The second output shows that the ArithmeticException thrown in the computeAverage() method and propagated to the printAverage() method is suppressed by the return statement in the finally clause. Normal execution continues after the return statement at (3), with the value 0 being returned from the printAverage() method.
If the finally clause throws an exception, this exception is propagated with all its ramifications—regardless of how the try block or any catch clause was executed. In particular, the new exception overrules any previously uncaught exception (p. 415).
Example 7.6 The finally Clause and the return Statement
Deriving Permitted Direct Subtypes of a Sealed Supertype
The permits clause of a sealed class or interface can be omitted, if all its permitted direct subtypes are declared in the same compilation unit—that is, are in the same source file. Since only one reference type can be declared public in a compilation unit, the accessibility of the other type declarations cannot be public.
The compiler can infer the permitted direct subtypes of any sealed class or interface in a compilation unit by checking whether a type declaration fulfills the contract for declaring a permitted direct subtype; that is, it extends/implements a sealed supertype and is declared either (implicitly) sealed, non-sealed, or (implicitly) final.
// File: Book.java (compilation unit) public abstract sealed class Book {} // Permitted subclasses are derived. non-sealed class PrintedBook extends Book {} sealed interface Subscribable {} // Permitted subtypes are derived. final class Ebook extends Book implements Subscribable {} final class Audiobook extends Book implements Subscribable {} non-sealed interface VIPSubcribable extends Subscribable {}
Given the type declarations in the compilation unit Book.java, the compiler is able to infer that PrintedBook, Ebook, and Audiobook are permitted direct subclasses of the sealed superclass Book, whereas Ebook, Audiobook, and VIPSubcribable are permitted direct subtypes of the sealed superinterface Subscribable.
Using Sealed Classes and Interfaces
As we have seen, sealed classes and interfaces restrict which other classes or interfaces can extend or implement them. Apart from providing the programmer with a language feature to develop improved domain models for libraries, the compiler can also leverage the sealed types to provide better analysis of potential problems in the code at compile time.
Example 5.33 is a client that uses the sealed classes and their permitted direct subclasses defined in Example 5.32. Typically, a reference of the sealed superclass is used to process a collection of objects of its permitted subtypes. The array books is such a collection created at (2). The objects of the permitted subtypes are processed by the enhanced for(:) loop at (3). Typically, a cascading if-else statement is used in the loop body to distinguish each object in the collection and process it accordingly. Any object of a permitted subclass can be processed by the code as long as the cascading if-else statement guarantees that the checks are exhaustive—that is, all permitted subtypes are covered. The compiler cannot help to ensure that this is the case, as it cannot extract the necessary information from the cascading if-else statement for this analysis. Note that the conditionals in the cascading if-else statement use the instanceof pattern match operator to make the code at least less verbose.
The structure of a skeletal Java source file is depicted in Figure 6.1. A Java source file can have the following elements that, if present, must be specified in the following order:
An optional package declaration to specify a package name. Packages are discussed in §6.3, p. 326.
Zero or more import declarations. Since import declarations introduce type or static member names in the source code, they must be placed before any type declarations. Both type and static import declarations are discussed in §6.3, p. 329.
Any number of top-level type declarations. Class, enum, and interface declarations are collectively known as type declarations. Since these declarations belong to the same package, they are said to be defined at the top level, which is the package level.
The type declarations can be defined in any order. Technically, a source file need not have any such declarations, but that is hardly useful.
The JDK imposes the restriction that at most one public class declaration per source file can be defined. If a public class is defined, the file name must match this public class. For example, if the public class name is NewApp, the file name must be NewApp.java.
Classes are discussed in §3.1, p. 99; interfaces are discussed in §5.6, p. 237; and enums are discussed in §5.13, p. 287.
Modules introduce another Java source file that contains a single module declaration (§19.3, p. 1168).
Note that except for the package and the import statements, all code is encapsulated in classes, interfaces, enums, and records. No such restriction applies to comments and whitespace.
A package in Java is an encapsulation mechanism that can be used to group related classes, interfaces, enums, and records.
Figure 6.2 shows an example of a package hierarchy comprising a package called wizard that contains two other packages: pandorasbox and spells. The package pandorasbox has a class called Clown that implements an interface called Magic, also found in the same package. In addition, the package pandorasbox has a class called LovePotion and a subpackage called artifacts containing a class called Ailment. The package spells has two classes: Baldness and LovePotion. The class Baldness is a subclass of class Ailment found in the subpackage artifacts in the package pandorasbox.
The dot (.) notation is used to uniquely identify package members in the package hierarchy. The class wizard.pandorasbox.LovePotion, for example, is different from the class wizard.spells.LovePotion. The Ailment class can be easily identified by the name wizard.pandorasbox.artifacts.Ailment, which is known as the fully qualified name of the type. Note that the fully qualified name of the type in a named package comprises the fully qualified name of the package and the simple name of the type. The simple type name Ailment and the fully qualified package name wizard.pandorasbox.artifacts together define the fully qualified type name wizard.pandorasbox.artifacts.Ailment.
Java programming environments usually map the fully qualified name of packages to the underlying (hierarchical) file system. For example, on a Unix system, the class file LovePotion.class corresponding to the fully qualified name wizard.pandorasbox.LovePotion would be found under the directory wizard/pandorasbox.
Figure 6.2 Package Structure
Conventionally, the reverse DNS (Domain Name System) notation based on the Internet domain names is used to uniquely identify packages. If the package wizard was implemented by a company called Sorcerers Limited that owns the domain sorcerersltd.com, its fully qualified name would be
com.sorcerersltd.wizard
Because domain names are unique, packages with this naming scheme are globally identifiable. It is not advisable to use the top-level package names java and sun, as these are reserved for the Java designers.
Note that each component of a package name must be a legal Java identifier. The following package would be illegal:
org.covid-19.2022.vaccine
The package name below is legal:
org.covid_19._2022.vaccine
A subpackage would be located in a subdirectory of the directory corresponding to its parent package. Apart from this locational relationship, a subpackage is an independent package with no other relation to its parent package. The subpackage wizard.pandorasbox.artifacts could easily have been placed elsewhere, as long as it was uniquely identified. Subpackages in a package do not affect the accessibility of the other package members. For all intents and purposes, subpackages are more an organizational feature than a language feature. Accessibility of members defined in type declarations is discussed in §6.5, p. 345.