In Java, methods that throw checked exceptions (Exception or its subtypes - IOException, InterruptedException, etc) must declare throws sta
The issue here is that checked/unchecked exception limitations affect what your code is allowed to throw, not what it's allowed to catch. While you can still catch any type of Exception, the only ones you're allowed to actually throw again are unchecked ones. (This is why casting your unchecked exception into a checked exception breaks your code.)
Catching an unchecked exception with Exception is valid, because unchecked exceptions (a.k.a. RuntimeExceptions) are a subclass of Exception, and it follows standard polymorphism rules; it doesn't turn the caught exception into an Exception, just as storing a String in an Object doesn't turn the String into an Object. Polymorphism means that a variable that can hold an Object can hold anything derived from Object (such as a String). Likewise, as Exception is the superclass of all exception types, a variable of type Exception can hold any class derived from Exception, without turning the object into an Exception. Consider this:
import java.lang.*;
// ...
public String iReturnAString() { return "Consider this!"; }
// ...
Object o = iReturnAString();
Despite the variable's type being Object, o still stores a String, does it not? Likewise, in your code:
try {
safeMethod();
} catch (Exception e) { // catching checked exception
throw e; // so I can throw... a checked Exception?
}
What this means is actually "catch anything compatible with class Exception (i.e. Exception and anything derived from it)." Similar logic is used in other languages, as well; for example, in C++, catching a std::exception will also catch std::runtime_error, std::logic_error, std::bad_alloc, any properly-defined user-created exceptions, and so on, because they all derive from std::exception.
tl;dr: You're not catching checked exceptions, you're catching any exceptions. The exception only becomes a checked exception if you cast it into a checked exception type.