Lec 04 - Exception and Wrapper Classes

Slides:

Wrapper Class

In the previous lec, we have introduced how to write general code on reference type by using polymorphism. Then, how about the primitive type? How do we write general code for the primitive type (a.k.a make primitive types less primitive)? Here it comes — The Wrapper class.

Another reason to use Wrapper Class is that int[] cannot be converted to Obj[] automatically.


A wrapper class is a class that encapsulates a type. For example, the wrapper class for int is called Integer, for double is called Double, etc. The table for the wrapper class for all primitive types in Java is summarised as follows:

Primitive
Wrapper

byte

Byte

short

Short

int

Integer

long

Long

float

Float

double

Double

char

Character

boolean

Boolean

In particular, the wrapper class is considered as reference type and their instances can be created with new and stored on the heap.

All primitive wrapper class objects are immutable — once you create an object, it cannot be changed. This will result that if we want to change the value of a Wrapper class object, we need to create a new object.

Conversion between primitive types and its wrapper class

This is known as auto-boxing and unboxing.

1

Auto-boxing

This is used when you convert a primitive type to its wrapper class.

Here, the primitive value of int 4 is converted into an instance of Integer.

Auto-boxing doesn't work for complex types, like Array. So, Java cannot convert int[] to Integer[].

2

Unboxing

This is used when you convert an instance of a wrapper class to its primitive type.

Line 2 converts an instance of Integer back to int.

Both auto-boxing and unboxing are implemented automatically.

Performance

For the sake of general code and polymorphism, why don't we use wrapper class all the time? The reason is performance. Using primitive types is faster than using its wrapper class since the latter needs to instantiate an object every time it is being used.

Run-Time Class Mismatch

The main point of this part is about when to use the explicit casting?


Explicit casting is usually done during the narrowing type conversion process, this happens in compile-time! As the name suggests, the type must be "narrowed down". So,

for an explicit casting to be successful, the two types must have a subtype relationship.

Run-Time error or Compile-Time error?

When doing assignment, if the compile-time type of the two objects have no subtype relationship, then a compile-time error will be generated.

Explicit Type casting using () will happen during the compile-time.

On the other hand, a run-time error will be generated when there is no subtype relationship between the run-time type of the two objects you are operating on.

During run-time, we should ignore the type casting and only look at the RTT! See more 03. Type casting and rum-time error

Variance

Till now, we have seen the subtype relationship on classes and interefaces, this is trivial. However, how about the complex types such as arrays? Seems that the subtype relationship is not that trivial. So, here it comes — the principle of variance of types:

The variance of types refers to how the subtype relationship between complex types relates to the subtype relationship between components.

Variance of types

Let C(S)C(S) correspond to some complex type based on type SS, which means C(S)C(S) is the type of the "array" and SS is the type of each element/component in the "array". We say a complex type is:

  • covariant: which means if S<:TS<:T, then C(S)<:C(T)C(S)<:C(T)

  • contravariant: which means if S<:TS<:T, then C(T)<:C(S)C(T)<:C(S)

  • invariant: t is neither covariant nor contravariant

Java Array is covariant.

Exceptions

try-catch-finally block

In Java, we use try-catch-finally block in our process to handle exceptions. Their usage is summarised as follows:

Compile-error in catch block

In the catch block, the first catch block that has an exception type compatible with the type of the thrown exception (i.e. a subtype) is selected to handle the exception. So, if there are blocks that are unreachable, a compile error will be generated! For example, the following code will generate a compile error (the second exception is the subclass of the first exception)

However, if we write the following code, it works fine,

Exceptions are always triggered at run-time.

Throwing Exceptions

We can control our program to throw an exception when our program doesn't behave as we expect. To throw an exception, we need to:

  1. use the keyword throw (not throws)

  2. create a new exception object and throw it to the caller. (e.g. IllegalArgumentException("radius cannot be negative."))

For example, the complete Java code should be as follows:

Checked vs. Unchecked Exceptions

An unchecked exception is an exception caused by a programmer's errors. They should not happen if perfect code is written. IllegalArgumentException, NullPointerException, ClassCastException are examples of unchecked exceptions. Generally, unchecked exceptions are not explicitly caught or thrown. They indicate that something is wrong with the program and cause run-time errors.

A checked exception is an exception that a programmer has no control over. Even if the code written is perfect, such an exception might still happen. The programmer should thus actively anticipate the exception and handle them. For instance, when we open a file, we should anticipate that in some cases, the file cannot be opened. FileNotFoundException and InputMismatchException are two examples of is an example of a checked exception. A checked exception must be either handled, or else the program will not compile.

In Java, unchecked exceptions are subclasses of the class RuntimeException.

Handle the exception

An unchecked exception, if not caught, will propagate automatically down the stack until either it is caught or if it is not caught at all, resulting in an error message displayed to the user.

A checked exception, however, must be handled. And this is done by either handling it in the calling method, or handling it in the caller. Below is an example regarding handling the FileNotFoundException)

1

Handle in the called method

This means we will handle the exception in the method itself, thus it won't pass the exception to other methods, so we don't need to state that this method may throw an exception.

2

Handle in the calling method

Line 2 is a method declaration which indicates that the method openFile() may throw a FileNotFoundException.

A good program always handles checked exception gracefully and hides the details from the users.

Control Flow of Exceptions

The use of Exceptions can affect the control flow of our program. For example, with the code following

Our normal control flow is as follows,

Then, what if we have thrown an exception E2 inside the m4()? (We decomment the Line 31 in the code above). Then, our control flow will become:

Note that the finally block is always executed even when return or throw is called in a catch block.

Good Practices for Exception Handling

Catch Exceptions to Clean Up

In the example, we may notice that if we have allocated some resources in m2() or m3(), they might not be deallocated because of the control flow of our exception handling.

So, it is recommended to handle the exception in the called method itself by using another try-catch-finally block. And if you still feel a need to pass the exception to the calling method, you can also do it by throwing this exception again in the catch block. For example,

Last updated