本文部分内容节选自Java Guide, 地址: https://javaguide.cn/java/basis/java-basic-questions-03.html
异常
Java 异常类层次图概览
Exception 和 Error 有什么区别?
在 Java 中, 所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类. Throwable
类有两个重要的子类:
Exception
: 程序本身可以处理的异常, 可以通过catch
来捕获.Exception
又分为 受检查异常和不受检查异常. 受检查异常是必须处理的异常, 不受检查异常可以不处理Error
: 程序本身无法处理的异常, 不建议用catch
来捕获. 常见的Error
包括 Java虚拟机运行错误Virtual Machine Error
, 内存不足的错误OutOfMemoryError
……当这些异常发生的时候, 线程会自动终止
Checked Exception(受检查异常) 和 Unchecked Exception(不受检查异常) 有什么区别?
前面已经简单提及, 这里再补充一下: Java 代码在编译阶段, 如果没有 catch
或 throws
关键字来处理 Checked Exception 的话, 就无法通过编译
除了 RuntimeException
及其子类外, 其他的 Exception
都是 Checked Exception
Unchecked Exception 也就是不受检查异常, Java代码在编译阶段, 即使没有 catch
或 throws
关键字来处理 Unchecked Exception, 也能通过编译
try-catch-finally 如何使用?
try
块:用于捕获异常. 其后可接零个或多个catch
块, 如果没有catch
块, 则必须跟一个finally
块catch
块:用于处理 try 捕获到的异常finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行. 当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行
代码示例
try {
System.out.println("I will throw an Exception");
throw new RuntimeException("Exception");
} catch (RunException e) {
System.out.println("I catch the Exception");
} finally {
System.out.println("Finally");
}
输出:
I will throw an Exception
I catch the Exception
Finally
⚠️特别注意!!! 千万不能在finally块中使用return! 当try 和 finally中都有 return
时, try 中的 return
会被忽略. 这是因为 try语句中的 return
返回值会保存在一个本地变量中, 如果执行了 finally中的 return
, 这个本地变量就会变成 finally 中 return
的返回值了
代码示例
public static void main(String[] args) {
System.out.println(f(2));
}
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
输出:
0
finally 中的代码一定会被执行吗?
答案是不一定
以下三种情况会导致 finally 中的代码不会被执行
- 关闭 Java 虚拟机
- 程序所在的线程死亡
- 关闭CPU
代码示例
try {
System.out.println("I will throw an Exception");
throw new RuntimeException("Exception");
} catch (RuntimeException e) {
System.out.println("I catch the Exception");
System.exit(1); // 注意, 这里终止掉了Java虚拟机
} finally {
System.out.println("Finally");
}
输出:
I will throw an Exception
I catch the Exception
如何用 try-with-resource
代替 try-catch-finally
?
- 适用范围 : 任何实现
java.lang.AutoCloseable
或java.io.Closeable
的对象 - 关闭资源和finally块的执行顺序 : 在
try-with-resources
语句中,任何 catch 或 finally 块在声明的资源关闭后运行
类似于 InputStream
, OutputStream
, Scanner
, PrintWriter
等资源都需要我们调用 close()
方法来手动关闭
可以在 try-with-resource
的 try语句块中用分号分隔定义多个资源
异常使用有哪些要注意的地方?
- 不要把异常定义为静态变量, 因为这样会导致异常栈信息错乱. 正确的做法是每次要抛出一个异常的时候就 new 一个出来
- 抛出的异常信息一定要有意义
- 建议抛出更加具体的异常. 同理, 在异常处理的时候, 具体的异常放前面, 通用的异常放后面
代码示例
try {
System.out.println("I will throw an Exception");
throw new RuntimeException("Exception");
} catch (RuntimeException re) { // 具体异常放前面
System.out.println("Runtime Exception Catched");
} catch (Exception e) { // 通用异常放后面
System.out.println("Exception catched");
}
- 如果使用了日志 (例如log4j) 打印异常, 那么就不要再抛出异常, 或者说两者不应该存在于同一段代码中
错误代码示例
try {
System.out.println("I will throw an Exception");
throw new Runtime Exception("Exception");
} catch (RuntimeException re) {
log.error("Exception. System Error");
}
泛型
什么是泛型? 有什么用?
Java 泛型 是 JDK5 引入的新特性, 使用泛型参数, 可以提高代码的可读性以及稳定性
编译器可以对泛型参数进行检测, 并且通过泛型参数指定传入的参数类型
泛型的使用方式有哪几种?
- 泛型类
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
如何实例化泛型类
Generic<Integer> genericInteger = new Generic<Integer>(114514);
- 泛型接口
public interface Generator<T> {
public T method();
}
实现泛型接口, 不指定类型
class GeneratorImpl<T> implements Generator<T> {
@Override
public T method() {
return null;
}
}
实现泛型接口, 指定类型
class GeneratorImpl<T> implements Generator<String> {
@Override
public String method() {
return "Yonagi";
}
}
- 泛型方法
public static<E> void printArray(E[] array) {
for (E element : array) {
System.out.println("%s", element);
}
}
使用泛型方法:
Integer[] intArray = {1, 1, 4, 5, 1, 4};
String[] stringArray = {"heng", "heng", "aaaaa"};
pringArray(intArray);
pringArray(stringArray);
反射
反射的应用场景?
动态代理的实现(基于反射)
public class DebugInvocationHandler implements InvocationHandler {
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
注解的实现(基于反射)
反射的优缺点?
优点 : 提高代码的灵活性, 为框架的开箱即用提供了便利(Spring框架不少地方用了动态代理, 而动态代理本身是基于反射实现的)
缺点 : 性能偏差(但是影响不大), 反射带来的运行时分析类的能力, 也带来了安全问题, 例如它可以无视泛型参数的安全检查
注解
注解是什么
Annotation
(注解) 是Java5 开始引入的新特性, 主要用于修饰类, 方法, 变量, 提供某些信息供程序在编译或运行时使用
注解本质上是继承了 Annotation
的特殊接口:
@Target(ElementType.Method)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation {
}
注解的解析方式有哪几种?
注解只有在解析之后才会生效
- 编译期直接扫描 : 编译器在编译Java代码的时候扫描出对应的注解并处理, 例如某个方法使用@Override注解, 编译器在编译的时候就会检测当前方法是否重写了父类对应的方法
- 运行期通过反射处理 : 框架自带的注解一般都是通过反射来处理的
序列化和反序列化
什么是序列化和反序列化?
- 序列化 : 把数据结构或对象转换成二进制字节流的过程
- 反序列化 : 通过序列化形成的二进制字节流转换成数据结构或对象的过程
常见场景:
- 对象进行网络传输, 发送方要进行序列化, 接收到对象后接收方要进行反序列化
- 将对象存储到文件要进行序列化, 将对象从文件读出要进行反序列化
- 将对象存储到数据库之前要进行序列化, 从数据库读取出来要用到反序列化
- 对象存储到内存之前要进行序列化, 从内存读取要进行反序列化
如果有些字段不想序列化怎么办
对于不想序列化的变量, 可以使用 transient
关键字修饰
transient
关键字的作用是: 阻止实例中那些用此关键字修饰的变量序列化, 当对象被反序列化时, 被 transient
修饰的变量值不会被持久化和恢复
transient
只能修饰变量, 不能修饰类和方法transient
修饰的变量, 在反序列化之后变量值会被置成默认值static
变量因为不属于任何对象, 所以有没有transient
关键字修饰都不会被序列化
I/O
I/O流了解吗?
I/O, 即 Input/Output. 数据输入到内存为输入, 反之输出到外部存储的过程称为输出. I/O流在 Java 中分为 输入流和输出流, 根据数据处理方式又分为字符流和字节流
InputStream
/Reader
: 所有输入流的基类, 前者是字节流, 后者是字符流OutputStream
/Reader
: 所有输出流的基类, 前者是字节流, 后者是字符流
I/O流为什么要分为字节流和字符流
- 字节流是由 Java 虚拟机将字节转换得到的, 这个过程比较耗时
- 如果不知道编码类型的话, 使用字节流容易出现乱码
有哪些常用的I/O模型?
同步阻塞I/O, 异步I/O, 同步非阻塞I/O, I/O多路复用, 信号驱动I/O
Java 中常见的3种I/O模型
BIO(Blocking I/O)
BIO属于同步阻塞IO模型
同步阻塞 IO 模型中, 应用程序发起 read 调用后, 会一直阻塞, 直到内核把数据拷贝到用户空间
NIO (Non-blocking I/O)
NIO属于I/O多路复用模型
对于高负载, 高并发的网络应用程序, 应当使用 NIO
AIO(Asynchronous I/O)
AIO属于异步IO模型
最后简单给一张图, 总结一下 Java 的三种I/O模型