1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-25 02:27:10 +08:00

Update java-basic-questions-03.md

This commit is contained in:
guide 2022-04-16 19:34:17 +08:00
parent 9dad8fbf65
commit 9c24659d4c

View File

@ -5,215 +5,22 @@ tag:
- Java基础
---
## 泛型
#### Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
**Java 泛型generics** 是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java 的泛型是伪泛型,这是因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。
```java
List<Integer> list = new ArrayList<>();
list.add(12);
//这里直接添加会报错
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加是可以的
//这就说明在运行期间所有的泛型信息都会被擦掉
add.invoke(list, "kl");
System.out.println(list);
```
泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。
**1.泛型类**
```java
//此处T可以随便写为任意标识常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时必须指定T的具体类型
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
```
如何实例化泛型类:
```java
Generic<Integer> genericInteger = new Generic<Integer>(123456);
```
**2.泛型接口**
```java
public interface Generator<T> {
public T method();
}
```
实现泛型接口,不指定类型:
```java
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
```
实现泛型接口,指定类型:
```java
class GeneratorImpl implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
```
**3.泛型方法**
```java
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
```
使用:
```java
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray);
printArray(stringArray);
```
#### 常用的通配符有哪些?
**常用的通配符为: TEKV**
- 表示不确定的 Java 类型
- T (type) 表示具体的一个 Java 类型
- K V (key value) 分别代表 Java 键值中的 Key Value
- E (element) 代表 Element
#### 你的项目中哪里用到了泛型?
- 可用于定义通用返回结果 `CommonResult<T>` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型
- 定义 `Excel` 处理类 `ExcelUtil<T>` 用于动态指定 `Excel` 导出的数据类型
- 用于构建集合工具类。参考 `Collections` 中的 `sort`, `binarySearch` 方法
- ......
## 反射
### 何为反射?
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
### 反射机制优缺点
- **优点** 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow)
### 反射的应用场景
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。**
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。
```java
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;
}
}
```
另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
## 注解
`Annotation` (注解) 是Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。
注解本质是一个继承了`Annotation` 的特殊接口:
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
```
注解只有被解析之后才会生效,常见的解析方法有两种:
- **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value``@Component`)都是通过反射来进行处理的。
JDK 提供了很多内置的注解(比如 `@Override``@Deprecated`),同时,我们还可以自定义注解。
## 异常
**Java 异常类层次结构图概览**
![types-of-exceptions-in-java](./images/types-of-exceptions-in-java.png)
### Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类:
- **`Exception`** :程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
- **`Error`** `Error` 属于程序无法处理的错误 ~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时Java 虚拟机JVM一般会选择线程终止。
- **`Error`** `Error` 属于程序无法处理的错误 ~~我们没办法通过 `catch` 来进行捕获~~不建议通过`catch`捕获 。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时Java 虚拟机JVM一般会选择线程终止。
### Checked Exception 和 Unchecked Exception 有什么区别?
**Checked Exception** 即受检查异常Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。
**Checked Exception** 即 受检查异常 Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` 处理的话,就没办法通过编译 。
比如下面这段 IO 操作的代码:
@ -223,7 +30,17 @@ JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`
**Unchecked Exception** 即 **不受检查异常** Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
`RuntimeException` 及其子类都统称为非受检查异常,例如:`NullPointerException``NumberFormatException`(字符串转换为数字)、`ArrayIndexOutOfBoundsException`(数组越界)、`ClassCastException`(类型转换错误)、`ArithmeticException`(算术错误)等。
`RuntimeException` 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
- `NullPointerException`(空指针错误)
- `IllegalArgumentException`(参数错误比如方法入参类型错误)
- `NumberFormatException`(字符串转换为数字格式错误,`IllegalArgumentException`的子类)
- `ArrayIndexOutOfBoundsException`(数组越界错误)
- `ClassCastException`(类型转换错误)
- `ArithmeticException`(算术错误)
- `SecurityException` (安全错误比如权限不够)
- `UnsupportedOperationException`(不支持的操作错误比如重复创建同一用户)
- ......
![](./images/unchecked-exception.png)
@ -236,11 +53,11 @@ JDK 提供了很多内置的注解(比如 `@Override` 、`@Deprecated`
### try-catch-finally 如何使用?
- **`try`块:** 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。
- **`catch`块:** 用于处理 try 捕获到的异常。
- **`finally` 块:** 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。
- `try` 用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。
- \*`catch` 用于处理 try 捕获到的异常。
- `finally` 无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。
示例:
代码示例:
```java
try {
@ -261,19 +78,17 @@ Catch Exception -> RuntimeException
Finally
```
**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
[jvm 官方文档](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.5)中有明确提到:
> If the `try` clause executes a *return*, the compiled code does the following:
> If the `try` clause executes a _return_, the compiled code does the following:
>
> 1. Saves the return value (if any) in a local variable.
> 2. Executes a *jsr* to the code for the `finally` clause.
> 2. Executes a _jsr_ to the code for the `finally` clause.
> 3. Upon return from the `finally` clause, returns the value saved in the local variable.
先执行一部分的,先把返回的结果存到一段内存中;
示例:
代码示例:
```java
public static void main(String[] args) {
@ -301,7 +116,7 @@ public static int f(int value) {
不一定的在某些情况下finally 中的代码不会被执行。
就比如说 `finally` 之前虚拟机被终止运行的话finally 中的代码就不会被执行。
就比如说 finally 之前虚拟机被终止运行的话finally 中的代码就不会被执行。
```java
try {
@ -330,7 +145,7 @@ Catch Exception -> RuntimeException
相关 issue <https://github.com/Snailclimb/JavaGuide/issues/190>
🧗🏻进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。
🧗🏻 进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。
### 如何使用 `try-with-resources` 代替`try-catch-finally`
@ -378,17 +193,203 @@ try (Scanner scanner = new Scanner(new File("test.txt"))) {
```java
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
```
### 异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`
- 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
- ......
## 泛型
### 什么是泛型?有什么作用?
**Java 泛型Generics** 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 `ArrayList<Persion> persons = new ArrayList<Persion>()` 这行代码就指明了该 `ArrayList` 对象只能传入 `Persion` 对象,如果传入其他类型的对象就会报错。
```java
ArrayList<E> extends AbstractList<E>
```
并且,原生 `List` 返回类型是 `Object` ,需要手动转换类型才能使用,使用泛型后编译器自动转换。
### 泛型的使用方式有哪几种?
泛型一般有三种使用方式:**泛型类**、**泛型接口**、**泛型方法**。
**1.泛型类**
```java
//此处T可以随便写为任意标识常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
```
如何实例化泛型类:
```java
Generic<Integer> genericInteger = new Generic<Integer>(123456);
```
**2.泛型接口**
```java
public interface Generator<T> {
public T method();
}
```
实现泛型接口,不指定类型:
```java
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
```
实现泛型接口,指定类型:
```java
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
```
**3.泛型方法**
```java
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
```
使用:
```java
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
```
### 项目中哪里用到了泛型?
- 自定义接口通用返回结果 `CommonResult<T>` 通过参数 `T` 可根据具体的返回类型动态指定结果的数据类型
- 定义 `Excel` 处理类 `ExcelUtil<T>` 用于动态指定 `Excel` 导出的数据类型
- 构建集合工具类(参考 `Collections` 中的 `sort`, `binarySearch` 方法)。
- ......
-
## 反射
### 何为反射?
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
### 反射机制优缺点
- **优点** 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- **缺点** :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow)
### 反射的应用场景
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。**
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。
```java
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;
}
}
```
另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。
为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。
## 注解
`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。
注解本质是一个继承了`Annotation` 的特殊接口:
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
```
注解只有被解析之后才会生效,常见的解析方法有两种:
- **编译期直接扫描** :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
- **运行期通过反射处理** :像框架中自带的注解(比如 Spring 框架的 `@Value``@Component`)都是通过反射来进行处理的。
JDK 提供了很多内置的注解(比如 `@Override``@Deprecated`),同时,我们还可以自定义注解。
## I/O
### 什么是序列化?什么是反序列化?