要分清一个东西: Complie-time error:编译时错误 checked exception:受检异常/检查型异常,不能翻译成 compile-time exception,因为异常并不是在编译时发生的;编译器只会在编译阶段检查它有没有被捕获或者声明
例如:
IOException is a checked exception.
意思是:
IOException 是一种受检异常
而不是: IOException is a complie-time exception.
异常在运行时发生
↓
checked exception
↓
编译器在编译时检查是否 catch 或 throws
“编译期异常”,这个中文说法在一些教材中常见,但使用 checked exception(受检异常)最准确
checked exception 和 complie-time error 之间的关系:当 checked exception 没有被正确捕获或声明时会导致一个 complite-time error:
checked exception 没有 catch 或 throws
↓
编译器检查不通过
↓
产生 compile-time error
例如:
public static void main(String[] args) {
Files.readString(Path.of("test.txt"));
}
Files.readString 可能抛出 IOException,它属于 checked exception。如果既不使用 try-catch,也不在方法上写 throws IOException,编译器就会报告 complie-time error
但这里不是说:
IOException 在编译时被抛出了
而是:
编译器在编译时发现你没有处理或声明将来运行时可能发生的 IOException
JLS §11.1.1 把异常类区分为 checked exception classes 和 unchecked exception;
非受检异常 = 运行时异常 + Error
受检异常 = 全部异常 - 非受检异常 = 全部异常 - 运行时异常 - Error
§11.2 的标题则是 Compile-Time Checking of Exceptions,即“对异常进行编译期检查”,这说明“编译期”描述的是检查发生的阶段,不是异常实际发生的阶段。
Checked exception:受检异常
编译器要求可能抛出受检异常的代码:
1. 使用 try-catch 捕获;或者
2. 使用 throws 声明。
如果两者都没有,则产生 compile-time error。
异常本身仍然只会在程序运行时真正被抛出。
Java语法硬性规定:所有继承 Throwable 的类(自定义异常、Error、Exception 等)不能带泛型参数:
public class MyException<T> extends Excpetion{} // 非法代码,编译失败
底层原因:JVM 异常处理机制不支持泛型异常。
public class test<T> extends Exception {} // 编译时错误
// Generic class may not extend java.lang.Throwable
泛型异常类继承 Throwable
↓
违反 Java 类声明规则
↓
compile-time error
TestException extends Exception
↓
TestException 是 checked exception
↓
抛出时必须 catch 或 throws
IDEA红色感叹号不等于“发生异常”,红色标记只表示检测到了一个会导致代码无法正常编译的问题
int number = "hello"; // 类型不兼容
unknownMethod(); // 找不到方法
privateValue = 10; // 访问权限不允许
class Test<T> extends Exception { } // 违反类声明规则
这些都是编译时错误,但都不是运行时抛出的异常对象。
“报错”和“异常”不是完全相同的概念
日常说话时,人们经常把所有红色提示都叫“异常”或“报错”。但在 Java 的严格术语中需要区分:
| 情况 | 是否属于 Java 异常 |
|---|---|
| IDEA 红色波浪线 | 不一定 |
| 语法错误 | 不是异常 |
| 类型不兼容 | 不是异常 |
| 找不到变量或方法 | 不是异常 |
| 违反泛型类声明规则 | 不是异常 |
运行时抛出 NullPointerException |
是异常 |
主动执行 throw new IOException() |
是异常 |
代码还没有成功编译:
编写源代码
↓
IDEA 静态分析
↓
发现泛型类不能继承 Exception
↓
产生 compile-time error
↓
无法正常生成对应的 .class 文件
程序没有开始运行,所以也没有真正创建、抛出某个异常对象。
真正的“抛出异常”
String str = null;
str.length();
这段代码可以通过编译,但是运行到 str.length() 时会抛出:
NullPointerException
这是异常:
成功编译
↓
程序开始运行
↓
执行非法操作
↓
创建并抛出异常对象
再例如:
throw new IOException("读取失败");
执行到这一行时,才是在运行期间真正抛出异常对象。
改掉 <T> 后才是受检异常类
public class TestException extends Exception {
public TestException(String message) {
super(message);
}
}
此时:
TestException 是合法的类
↓
它继承 Exception
↓
但不是 RuntimeException 的子类
↓
它是 checked exception
之后这样使用:
throw new TestException("出现问题");
编译器才会检查你是否进行了:
try {
// ...
} catch (TestException e) {
// ...
}
或者声明:
throws TestException
结论:
红色标记表示代码存在错误,但不等于发生了 Java 异常。
图中是违反 Java 类声明规则而产生的编译时错误,不是 checked exception,也没有在运行时抛出异常。
在代码编译阶段违反 Java 语言规则,编译器必须拒绝生成正常的 .class 文件
程序编译成功后,如果 JVM 找不到二进制文件,或者无法正常加载 .class 文件就会产生 loading error
e.g. ClassNotFoundException, NoClassDefFoundException
类已经被编译,但运行时不同的 .class 文件不兼容
e.g.NoSuchMethodError, NoSuchFileError, AbstractMethodError, UnsaisfieldLinkError
类加载和链接完成之后,JVM 还要执行:
static 变量初始化器static 代码块e.g.
class Test {
	static int number = 1 / 0;
}
第一次主动使用 Test 类时,其静态初始化失败:ExceptionInInitialzerError
程序通过了编译但是在执行过程中出现了错误:
e.g.
int reslut = 1 / 0; // ArithmeticException
String text = null;
System.out.println(text.lengt); // NullPointerException
程序可以编译、运行,但是不符合预期
int width = 5;
int width = 3;
int area = width + height; // 程序不会抛异常,但是矩形面积应是:width * height