Day4 | Java基础 | 4 异常、泛型、反射、注解、SPI、I/O

基础版

  • 异常
  • 异常关键字
  • 异常的申明 throws
  • 泛型
  • 反射
  • 动态代理
  • 注解
  • 元注解。
  • 自定义注解
  • 问题回答版
  • 异常
  • 介绍Java的异常体系?
  • Exception和Error有什么区别?
  • 受检和未受检异常是什么?
  • 如何自定义异常?
  • 泛型
  • 什么是泛型?有什么作用?
  • 泛型原理是什么?
  • 反射
  • 什么是反射?反射作用是什么?
  • 动态代理有几种实现方式?有什么特点?
  • JDK动态代理和CGLIB动态代理有什么区别?
  • 注解
  • 什么是注解?作用是什么?
  • 怎么自定义注解?
  • SPI
  • SPI是什么?有什么好处?
  • I/O-序列化
  • 什么是序列化?什么是反序列化?
  • Java是怎么实现序列化的?
  • 常见的序列化协议有哪些?
  • IO-网络IO
  • BIO/NIO/AIO有什么区别?
  • 什么是NIO?
  • I/O多路复用是什么?
  • select和epoll有什么区别?

基础版

异常

Java 基础 - 异常机制详解

Java 异常(文件找不到、网络连接失败、非法参数等)时Java提供的一种识别及相应错误的一致性机制,Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。

Java编程思想一书中,对异常的总结。

  • 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
  • 解决问题并且重新调用产生异常的方法。
  • 进行少许修补,然后绕过异常发生的地方继续执行。
  • 用别的数据进行计算,以代替方法预计会返回的值。
  • 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
  • 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。
  • 终止程序。
  • 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
  • 让类库和程序更安全。

异常关键字

  • try:用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch:用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finally:finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw:用于抛出异常。至于该异常被捕获还是继续抛出都与它无关。
  • throws:用在方法签名中,用于声明该方法可能抛出的异常。告知方法调用者。

异常的申明 throws

在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:

public static void method() throws IOException, FileNotFoundException{
    //something statements
}

注意:若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。通常,应该捕获那些知道如何处理的异常,将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。

throws抛出异常的规则:

  • 对于未受检异常,即 Error 和 RuntimeException及其子类,可以不用throws关键字来声明要抛出的异常,编译仍然可以顺利通过,但在运行时会被系统抛出。
  • 对于受检异常,一定要用try-catch语句捕获,或者用throws子句声明将它抛出,否则会导致编译错误。
  • 只有抛出了异常时,才必须取处理或者重新抛出该异常。当没有办法处理该异常时,应该继续抛出。
  • 调用方法必须遵循任何受检异常的处理和声明规则。如果覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法锁声明异常的同类或子类。

泛型

Java 中的泛型 Generics深入理解 Java 泛型

反射

大白话说Java反射:入门、使用、原理 Java中的反射 Reflection in JavaJava 反射机制详解

动态代理

谈谈Java反射机制,动态代理是基于什么原理?-极客时间

通过代理,可以让调用者与实现者之间解耦。

注解

深入理解Java注解全面解析Java注解你真的会创建和使用自定义的Java注解吗

元注解。

自定义注解时,需要使用元注解。

Java中提供了以下元注解类型:

  • @Retention:指明了注解的保留级别。RetentionPolicy.SOURCE - 标记的注解仅在源文件中有效,编译器会忽略。RetentionPolicy.CLASS - 标记的注解在 class 文件中有效,JVM 会忽略。RetentionPolicy.RUNTIME - 标记的注解在运行时有效。
  • @Target:指定注解可以修饰的元素类型。ElementType.ANNOTATION_TYPE - 标记的注解可以应用于注解类型。ElementType.CONSTRUCTOR - 标记的注解可以应用于构造函数。ElementType.FIELD - 标记的注解可以应用于字段或属性。ElementType.LOCAL_VARIABLE - 标记的注解可以应用于局部变量。ElementType.METHOD - 标记的注解可以应用于方法。ElementType.PACKAGE - 标记的注解可以应用于包声明。ElementType.PARAMETER - 标记的注解可以应用于方法的参数。ElementType.TYPE - 标记的注解可以应用于类的任何元素。
  • @Document:表示无论何时使用指定的注解,都应使用Javadoc。(默认情况下,注释不包含在Javadoc中)
  • @Inherited (JDK8 引入):表示注解类型可以被继承。
  • @Repeatable (JDK8 引入):表示注解可以重复使用。

自定义注解

语法格式:

//定义注解
public @interface 注解名 {定义体}

//注解属性
[访问级别修饰符] [数据类型] 名称() default 默认值;

问题回答版


异常

介绍Java的异常体系?

Java通过API中Throwable类的众多子类描述各种不同的异常。因此,Java异常都是对象,是Throwable子类的示例,描述了一段出现在编码中的错误条件。当条件生成时,错误将引发异常。

Java异常类层次结构图:

java 泛型 反射获取class对象_java 泛型 反射获取class对象

  • Throwable类:所有错误与异常的超类。包含了其线程创建时线程执行堆栈的快照,提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

Exception和Error有什么区别?

  • Exceprion:程序本身可以捕获并可以处理的异常。分为两类:
  • 运行时异常:RuntimeException类及其子类异常。
  • 不受检异常。Java编译器不会检查它。程序中可以选择捕获处理,也可以不处理。也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获、也没有用throws子句声明抛出它,也能编译通过。
  • 一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常发生。如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等。
  • 非运行时异常(编译异常):RuntimeException 以外的异常。
  • 从程序语法角度讲,是必须进行处理的异常。如果不处理,编译不通过。如 IOException、SQLException等、及 用户自定义的 Exception 异常,一般情况下不自定义受检异常。
  • Error:程序中无法处理的错误,表示运行程序中出现了严重的错误。
  • 一般表示代码运行时 JVM 出现问题。此类错误发生时,JVM 将终止线程。
  • 是不受检异常,非代码性错误。因此,此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们不应该实现任何新的Error子类。如 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、 OutOfMemoryError(内存不足错误)、StackOverflowError(栈溢出错误)等。

受检和未受检异常是什么?

  • 受检/可查异常(checked exceptions):编译器要求必须处置的异常。
  • 受检异常虽然是异常状况,但在一定程度上它的发生是可以预计的、很容易出现的、情理可容的。这种异常状况发生时,必须采取某种方式进行处理。
  • Java 编译器会检查它,也就是说,当程序中可能出现这类异常,要用 try-catch 语句捕获它,或用 throws 子句声明抛出它,否则编译不会通过。
  • 包括 非运行时异常。除 RuntimeException 及其子类外,其他的 Exception 类及其子类都属于受检异常。
  • 未受检/不可查异常(unchecked exceptions):编译器不要求强制处置的异常。
  • 包括 运行时异常 和 错误。即,RuntimeException 及其子类,和 Error。

如何自定义异常?

习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(throwable 的 toString 方法会打印这些详细信息,调试时很有用)

public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}

泛型

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

JDK 5 中引入的特性。允许在定义类和接口的时候使用类型参数(type parameter),而在使用时使用具体的类型来替换。(即,泛型类型是被参数化的类或接口)目的是为了在编译时提供更严格的类型检查,并支持泛型编程。

泛型具有以下优点:

  • 编译时的强类型检查:泛型要求在声明时指定实际数据类型,Java编译器会在编译时堆泛型代码做强类型检查,并在代码违反安全类型时发出告警。
  • 避免了类型转换
  • 可以实现通用算法:通用算法可以处理不同类型的集合,可以自定义,并且类型安全、易于阅读。

语法格式:

// 泛型类
class name<T1, T2, ..., Tn> { /* ... */ }

// 泛型方法
public <T> T func(T obj) {}

泛型原理是什么?

类型擦除。使用泛型时,任何具体的类型信息都被擦除了。具体做了以下工作:

  • 把泛型中的所有类型参数替换为Object,如果指定类型边界,则使用类型边界(上、下、无界通配符)来替换。因此,生成的字节码仅包含普通的类、接口和方法。
  • 擦除出现的类型声明,即去掉<>的内容。比如 T get() 方法声明就变成了 Object get() ;List<String> 就变成了 List。如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型中的多态性。类型擦除确保不为参数化类型创建新类,因此,反省不会产生运行时开销。

反射

什么是反射?反射作用是什么?

反射:一种基础功能。在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

作用:使程序在运行时能反观和修改其内部结构。具体来说,使程序能在运行时动态地与类和对象交互。或者说,反射赋予我们在运行时分析类以及执行类中方法的能力。真正价值在于,处理编译时未知的类型,从而编写更具有通用性的代码。

通过反射,可以获取任意一个类的所有属性和方法,还可以调用这些属性。这可以让我们的代码更灵活、为各种框架提供开箱即用的功能提供便利。

通过反射,即使是私有字段值,也是可以随意访问和修改的,包括final字段。这其实其实增加了安全问题,破坏了Java的封装性和设计原则,可能导致不可预见的副作用,尽量避免使用。此外,反射机制可以无视泛型参数的安全检查(此检查发生在编译时)。反射的性能要差一点,但对于框架的实际影响不大。

反射的应用场景:像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

动态代理有几种实现方式?有什么特点?

动态代理是一种方便运行时动态构建代理、动态处理代码方法调用的机制。实现方式有:

  • JDK 自身提供的动态代理:主要利用反射机制。
  • 字节码操作机制(性能更高):类似ASM、cglib(基于ASM)、Javassit等。

JDK动态代理和CGLIB动态代理有什么区别?

  • JDK Proxy的优势
  • 最小化以来关系,减少以来意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠。
  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用。
  • 代码实现简单。
  • 基于类似chlib框架的优势
  • 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理就没有这种限制。
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能。

注解

什么是注解?作用是什么?

是一种标签,实质上可以视为一种特殊的注释,如果没有解析它的代码,它并不比普通注释强。

作用:

  • 编译器信息:编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时的处理:程序可以处理注解信息以生成代码、XML文件等。
  • 运行时处理:可以在运行时检查某些注解并处理。

过多的配置文件会使得项目难以维护。使用注解以减少配置文件或代码,是注解最大的用处。

在Java中,注解是以@字符开始的修饰符。如果注解没有属性,成为标记注解,如@override。注解可以应用于类、字段、方法和其他程序元素的声明。

怎么自定义注解?

  1. 使用@interface声明。自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。
  2. 定义注解器。使程序读取注解的方法和工作,否则注解与注释用处无异。

SPI

Java SPI机制详解、Java中的SPI、源码级深度理解 Java SPI10分钟让你彻底明白Java SPI

SPI是什么?有什么好处?

Service Provider Interface,提供服务的接口。是Java提供的一套用来被第三方实现或扩展的接口,实现了接口的动态扩展,让第三方的实现类能像插件一样嵌入到系统。

好处:SPI将服务接口和具体的服务实现分离开,将服务调用方和服务实现方解耦,能够提升程序的扩展性、可维护性。修改或替换服务实现并不需要修改调用方。

I/O-序列化

深入理解 Java 序列化、避免使用Java 序列化

网络传输的数据必须为二进制数据,但调用方请求的出入参数都是对象。对象不能直接再网络中传输,所以需要提前把它转成可传输的二进制,并且要求转换算法是可逆的。

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

  • 序列化(serialize):将对象转换为二进制数据(字节数组)。用于写入磁盘或输出到网络。
  • 反序列化(deserialize):将二进制数据转换为对象。

Java是怎么实现序列化的?

JDK 提供的两个输入、输出流对象 ObjectInputStream 和 ObjectOutputStream ,只能对实现了 Seializeable 接口的类的对象进行反序列化和序列化。

Java 默认的序列化是通过 Serializable 接口实现的,只要类实现了该接口,同时生成一个默认的版本号,我们无需手动设置,该类就会自动实现序列化与反序列化。

Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷,所以强烈建议避免使用 Java 序列化。

注意:使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。

java 泛型 反射获取class对象_网络_02

常见的序列化协议有哪些?

FastJson、Protobuf、Hessian等。

IO-网络IO

Java IO模型、Java提供了哪些IO方式? NIO如何实现多路复用?IO 模型

IO:计算机内存与外部设备之间拷贝数据的过程。

传统IO的数据读写是在用户空间和内核空间来回复制,而内核空间的数据是通过操作系统层面的IO接口从磁盘数据读取或写入。

BIO/NIO/AIO有什么区别?

-

BIO

NIO

AIO

名称

blocking IO,阻塞IO

non-blocking IO,非阻塞IO

Asynchronous IO,异步非阻塞IO

Java包

java.io

java.nio

Java 7

功能

File抽象、输入输出流等

Channel、Selector、Buffer

基于事件和回调机制

交互方式

同步阻塞

读取输入流或写入输出流时,读、写动作完成之前,线程会一直阻塞在那里,他们之间的调用是可靠的线性顺序

多路复用、同步非阻塞

优点

代码简单、直观

使用缓冲区优化读写流

使用DirectBuffer减少内存复制

应用操作直接返回,且不会阻塞。当后台处理完成,操作系统会通知相应线程进行后续工作。

缺点

IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。会阻塞进程,不适合高并发场景。

什么是NIO?

NIO(non-blocking IO) 即非阻塞 IO。指的是 Java 1.4 中引入的 java.nio 包。主要目的是为了解决BIO的性能问题。

java.nio 包提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。

NIO 主要由三个优点:使用缓冲区Buffer优化读写流;使用DirectBuffer减少内存复制;使用Channel 中的处理器,可以完成内核空间和磁盘之间的IO操作,避免了阻塞。

I/O多路复用是什么?

对于一个网络IO通信过程,比如数据读取,会涉及两个对象,一个是调用这个IO操作的线程,另一个是操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。

当用户线程发起IO操作后,网络数据读取操作会经历两个步骤:1.用户线程等待内核数据从网卡拷贝到内核空间;2.内核将数据从内核空间拷贝到用户空间。

用户线程的读取分成两步,1.线程先发起 select 调用,目的是问内核数据准备好了吗?2.等内核把数据准备好了,用户线程再发起 read调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。

因为一次 select 调用可以向内核查多个数据通道 channel 的状态,所以叫多路复用。

select和epoll有什么区别?

深入了解NIO的优化实现原理

select:

  • 用途:在超时时间内,监听用户感兴趣的文件描述符上的可读可写和异常事件的发生。Linux操作系统的内核将所有外部设备都看作一个文件来描述,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符(fd)。
  • select()函数监视的文件描述符分为3类:writefds(写文件描述符)、readfds(读文件描述符)、exceptfds(异常事件文件描述符)。
  • 调用select()函数会阻塞,直到由描述符就绪或超时,函数返回。当select函数返回后,可以通过函数FD_ISSET遍历fd_set,来找到就绪的描述符。fd_set可以理解为一个集合,集合中存放的是文件描述符。epoll()函数:
  • select是顺序扫描fd是否就绪,而且支持fd数量不宜过大,因此它的使用受到了一些制约。
  • epoll使用事件驱动的方式代替轮询扫描fd。epoll事先通过epoll_ctl()来注册一个文件描述符,将文件描述符存放到内核的一个事件表中,这个时间表是基于红黑树实现的,所以在大量IO请求的场景下,插入和删除的性能比select的数组fd_set要好。因此,epoll的性能更胜一筹,且不会受到fd数量的限制。
  • epoll_ctl()函数中的epfd是由epoll_create()函数生成的一个epoll专用文件描述符。op代表操作事件类型,fd表示关联文件描述符,event表示指定监听的事件类型。一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知,之后进程将完成相关IO操作。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)

int epoll_wait(int epfd, struct epoll_event events,int maxevents,int timeout)

java 泛型 反射获取class对象_java 泛型 反射获取class对象_03

java 泛型 反射获取class对象_java_04