To understand this, let's look at an example where we have a Mammal class that defines a readAndGet method that reads a file, performs some operation on it, and returns an instance of the Mammal class.
class Mammal { public Mammal readAndGet() throws IOException {
The Human class extends the Mammal class and overrides the readAndGet method readAndGet returning an instance of Human instead of an instance of Mammal .
class Human extends Mammal { @Override public Human readAndGet() throws FileNotFoundException {
To call readAndGet we need to handle an IOException because it is a checked exception, and the readAndMethod mammal readAndMethod it.
Mammal mammal = new Human(); try { Mammal obj = mammal.readAndGet(); } catch (IOException ex) {..}
And we know that for the compiler, mammal.readAndGet() is called from an object of the Mammal class, but at run time the JVM will allow mammal.readAndGet() method to be called from the Human class because the mammal holding new Human() .
The readAndMethod method from Mammal readAndMethod IOException and, since it is a proven exception compiler, it will force us to catch it whenever we call readAndGet for a mammal
Now suppose that readAndGet in Human readAndGet any other checked exception, such as Exception, and we know that readAndGet will be called from the Human instance because the mammal contains new Human() .
Since for the compiler the method is called from Mammal , therefore, the compiler will force us to handle only an IOException but at runtime we know that the method will Exception which is not being processed, and our code will abort if the method throws an exception.
This is why this is prevented at the compiler level, and we are not allowed to throw any new or wider checked exceptions, because in the end they will not be processed by the JVM.
There are other rules that we must follow when redefining methods, and you can read more about why we should follow redefinition rules to find out the reasons.