Exceptions and Generics

Exceptions

Throwable

Alt text Throwable has two subclasses: Exception and Error

The Error class represents serious errors that are generally not recommended to be caught with catch in our programs. We can only optimize Error by optimizing code. When Error occurs, JVM generally chooses to terminate the thread.

The Exception class has two subclasses: Checked Exception and Unchecked Exception.
Checked Exception indicates that the exception will be checked at compile time. If the exception is not caught with catch or declared with throws, it won’t pass compilation. This includes IOException, ClassNotFoundException.
Unchecked Exception indicates that it won’t be checked at compile time. It includes RuntimeException and its subclasses, such as NullPointerException, ArrayIndexOutOfBoundsException, and ClassCastException that may occur during forced type conversion.

try-catch-finally

finally is used to close input/output streams and database connections.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class FinallyDemo {
    public static void main(String[] args) {
        System.out.println(testReturn());
    }

    public static int testReturn() {
        int num = 10;
        try {
            num = 20;
            return num;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            num = 30;
            System.out.println("finally block executed, num value is: " + num);
        }
        return num;
    }
}

If return is executed in the try block, finally will still be executed. The num value is modified to 30, but the returned value is still 20. This is because when return is executed in the try block, the return value has already been determined.
If System.exit() is executed in try or catch, then the finally block will not execute.

How to Handle Multiple Exceptions?

  1. Multiple catch blocks separately catch different exceptions.
1
2
3
4
5
6
7
try {
    // Code that may throw exceptions
} catch (IOException e) {
    // Logic to handle IO exceptions
} catch (SQLException e) {
    // Logic to handle database exceptions
}
1
catch (IOException | SQLException e) {... }
  1. One catch block catches the parent class of these exceptions.

It’s best to write catch blocks for subclass exceptions first, so they won’t be uniformly treated as parent class exception handling. You can add some logging in catch blocks to facilitate problem troubleshooting.

throw and throws

throw is for actively throwing exceptions.

1
throw new RuntimeException("Custom error message")

throws is placed in the function signature to declare which exceptions this method might throw, letting the caller handle them. If the caller doesn’t use catch to handle them, they will be thrown layer by layer up the call stack until reaching the main function.

1
public void readFile() throws IOException {... }

Generics

Generic Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//The <T> before void is used to declare this is a generic method
public static <T> void swap(T[] array, int i, int j) {
    T temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

Integer[] intArray = {1, 2, 3};
swap(intArray, 0, 1); 

String[] strings = {"apple", "banana", "cherry"};
swap(strings, 0, 2);

Generic Classes

A generic class can accept properties of multiple types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class GenericClassExample {
    // Define static inner class
    static class Pair<K, V> {
        private K key;
        private V value;

        public Pair(K key, V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }
    }

    public static void main(String[] args) {
        // Usage
        Pair<String, Integer> pair = new Pair<>("age", 25);
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());

        Pair<String, String> pair2 = new Pair<>("name", "bob");
        System.out.println("Key: " + pair2.getKey() + ", Value: " + pair2.getValue());
    }
}

Generic Interfaces

When implementing the interface, specify a concrete class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
interface DataAccessObject<T> {
    T getById(int id);
    void save(T entity);
}

// Implementation class
class UserDataAccessObject implements DataAccessObject<User> {
    @Override
    public User getById(int id) {
        // Logic to get user data from database
        return null;
    }
}

You can also not specify a concrete class when implementing the interface, allowing the specific type to be specified when creating instances of the GeneratorImpl class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Generator<T> {
    T method();
}

class GeneratorImpl<T> implements Generator<T> {
    @Override
    public T method() {
        return null;
    }
}

GeneratorImpl<String> stringGenerator = new GeneratorImpl<>();
String result = stringGenerator.method(); 

GeneratorImpl<Integer> integerGenerator = new GeneratorImpl<>();
Integer num = integerGenerator.method(); 

Bounded Types

1
class GenericClass<T extends Number> { }

Bytecode Files

  1. Type Erasure: During compilation, generic type parameters are erased and usually replaced with Object
  2. Bridge Methods: Subclasses may override generic methods from parent classes. To ensure subclass methods can correctly override parent class methods, the compiler generates bridge methods.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Parent<T> {
    public void set(T value) {
        System.out.println("Parent set: " + value);
    }
}

class Child extends Parent<String> {
    @Override
    public void set(String value) {
        System.out.println("Child set: " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        Parent<String> parent = new Child();
        parent.set("Hello");
    }
}

Obviously, the final output will be “Child Set”. But this actually calls the bridge method in the subclass that we cannot see.

1
2
3
public void set(Object value) {
    set((String) value); // Call the subclass's set(String value) method
}
  1. Generic Signatures: Generic information for generic classes, methods, and fields is stored in bytecode in a special format. This information can be obtained through reflection APIs (such as getGenericType()).