JavaSE(三)

3.1 异常

Java 异常类层次结构图概览

JavaSE(三)_第1张图片

1.Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类,分别是 Exception 和 Error:

Exception:程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为

  • Checked Exception:受检查异常,必须处理
  • Unchecked Exception:不受检查异常,可以不处理

Error:Error 属于程序无法处理的错误 ,不建议通过catch捕获,Error 发生时,Java 虚拟机一般会选择线程终止。例如

  • Virtual MachineError:Java 虚拟机运行错误
  • OutOfMemoryError:虚拟机内存不够错误
  • NoClassDefFoundError:类定义错误

2.Checked Exception 和 Unchecked Exception 有什么区别?

Checked Exception :Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。

比如下面这段 IO 操作的代码:

JavaSE(三)_第2张图片

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、ClassNotFoundExceptionSQLException...。

Unchecked Exception:Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException 及其子类都统称为非受检查异常,常见的有:

  • NullPointerException:空指针错误
  • IllegalArgumentException:参数错误比如方法入参类型错误
  • NumberFormatException:字符串转换为数字格式错误,IllegalArgumentException的子类
  • ArrayIndexOutOfBoundsException:数组越界错误
  • ClassCastException:类型转换错误
  • ArithmeticException:算术错误
  • SecurityException:安全错误比如权限不够
  • UnsupportedOperationException:不支持的操作错误比如重复创建同一用户
  • ......

JavaSE(三)_第3张图片

3.Throwable 类的常用方法有哪些?

  • String getMessage(): 返回异常发生时的简要描述
  • String toString(): 返回异常发生时的详细信息
  • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

4.try-catch-finally 如何使用?

  • try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块:用于处理 try 捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

代码示例:

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
} finally {
    System.out.println("Finally");
}

输出:

Try to do something
Catch Exception -> RuntimeException
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

5.finally 中的代码一定会执行吗?

不一定,在某些情况下,finally 中的代码不会被执行,例如以下几种情况:

  • finally 执行之前虚拟机被终止运行,finally 中的代码就不会被执行
  • 程序所在的线程死亡
  • 关闭 CPU
try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
} finally {
    System.out.println("Finally");
}

输出:

Try to do something
Catch Exception -> RuntimeException

6.如何使用 try-with-resources 代替 try-catch-finally?

  1. 适用范围: 在定义资源时,实现java.lang.AutoCloseable或者java.io.Closeable的对象
  2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

Java 中类似于InputStreamOutputStreamScannerPrintWriter等资源都需要我们调用close()方法来手动关闭,一般情况下我们都是通过try-catch-finally语句来实现这个需求,如下:

//读取文本文件的内容
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

使用 Java 7 之后的 try-with-resources 语句改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

当然多个资源需要关闭的时候,使用 try-with-resources 实现起来也非常简单,如果你还是用try-catch-finally可能会带来很多问题。

通过使用分号分隔,可以在try-with-resources块中声明多个资源。

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();
}

7.异常使用有哪些需要注意的地方?

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
  • 使用日志打印异常之后就不要再抛出异常了,两者不要同时存在一段代码逻辑中。

3.2 泛型

1.什么是泛型?有什么作用?

Java 泛型是 JDK5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如:

 ArrayList persons = new ArrayList()

这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。

2.泛型的使用方式有哪几种?

泛型一般有三种使用方式:泛型类泛型接口泛型方法

泛型类

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

如何实例化泛型类:

Generic genericInteger = new Generic(123456);

泛型接口

public interface Generator {
    public T method();
}

实现泛型接口,不指定类型:

class GeneratorImpl implements Generator{
    @Override
    public T method() {
        return null;
    }
}

实现泛型接口,指定类型:

class GeneratorImpl implements Generator{
    @Override
    public String method() {
        return "hello";
    }
}

泛型方法

public static  void printArray(E[] inputArray){
      for (E element : inputArray){
         System.out.printf( "%s ", element );
      }
      System.out.println();
}

使用:

// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
printArray(intArray);
printArray(stringArray);

注意:

public static  void printArray(E[] inputArray)

上述方法一般被称为静态泛型方法,在 Java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的

3.项目中哪里用到了泛型?

  • 自定义接口通用返回结果 CommonResult 通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类 ExcelUtil 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类,参考 Collections 中的 sortbinarySearch 方法。

3.3 反射

1.何为反射?

反射是指在运行时获取类的字节码文件对象,然后通过字节码文件对象解析类中的全部成分。例如通过反射可以获取任意一个类的所有属性和方法,这种运行时动态获取类信息以及动态调用类中成分的能力称为反射。

2.反射的优缺点?

优点:反射让我们在运行时有了分析操作类的能力

缺点:

  • 增加了安全问题,比如可以无视泛型参数的安全检查,泛型参数的安全检查发生在编译时。
  • 反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

3.反射的应用场景?

我们平时大部分时候所写的业务代码,很少会接触到直接使用反射机制的场景。但是,这并不代表反射没有用。反射的主要应用场景如下:

  • 通用框架的底层原理:正是因为反射,我们才能这么轻松地使用各种框架。像 Spring、SpringBoot、MyBatis 等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也是依赖反射。
  • Java 中注解的实现也用到了反射:为什么你使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?这些都是因为你可以基于反射分析类,然后获取到类、属性、方法、方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。

比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法。

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;
    }
}

3.4 注解

1.什么是注解?

注解是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解本质是一个继承了 Annotation 的特殊接口:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

public interface Override extends Annotation{

}

JDK 提供了很多内置的注解(比如 @Override@Deprecated),同时,我们还可以自定义注解。

2.注解的解析方法有哪几种?

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理:框架中自带的注解都是通过反射来进行处理的。

3.5 SPI

1.什么是SPI

SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

2.SPI 和 API 有什么区别?

一般模块之间都是通过接口进行通讯,那我们在服务调用方和服务实现方之间引入一个“接口”。

  • API:当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
  • SPI:当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

JavaSE(三)_第4张图片

3.SPI 的优缺点?

优点

  • 松散耦合:SPI支持松散耦合的设计模式,,插件实现和调用者之间的关系非常灵活。插件可以简单地被添加、替换或删除,对调用者的影响也最小化。
  • 扩展性:SPI提供了非常高的扩展性。它允许应用程序以插件的形式提供自定义实现,并且框架会自动加载和调用这些插。

缺点SPI 机制也存在一些缺点,比如:

  • 性能问题:需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
  • 安全性:SPI允许第三方提供插件实现代码,这可能会导致一些安全性问题。如果第三方的插件实现代码存在漏洞或者恶意代码,就可能会对系统造成严重危害。

3.6 序列化和反序列化

1.什么是序列化?什么是反序列化?

简单来说:

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

应用场景:如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库之前需要用到序列化,将对象从数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

JavaSE(三)_第5张图片

序列化协议对应于 TCP/IP 4 层模型的哪一层?

如下图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。所以,表示层对应的就是序列化和反序列化。因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

JavaSE(三)_第6张图片

2.如果有些字段不想进行序列化怎么办?

对于不想进行序列化的变量,使用 transient 关键字修饰。

transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化,当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

关于 transient 还有几点注意:

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

3.常见序列化协议有哪些?

比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

4.为什么不推荐使用 JDK 自带的序列化?

我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

3.7 I/O

1.Java 中的 IO 流了解吗?

IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到数据库、文件、远程主机等外部存储的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStreamReader: 所有输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStreamWriter: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

2.I/O 流为什么要分为字节流和字符流呢?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

个人认为主要有两点原因:

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;
  • 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

3.Java IO 中的设计模式有哪些?

4.BIO、NIO 和 AIO 的区别?

3.8 语法糖

1.什么是语法糖?

语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。例如:

Java 中的 for-each 就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。

String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
  	System.out.println(s);
}

不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。

2.Java 中有哪些常见的语法糖?

Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。

你可能感兴趣的:(#,Java面试八股文,开发语言,java)