基础版
- 异常
- 异常关键字
- 异常的申明 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异常类层次结构图:
- 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。注解可以应用于类、字段、方法和其他程序元素的声明。
怎么自定义注解?
- 使用@interface声明。自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。
- 定义注解器。使程序读取注解的方法和工作,否则注解与注释用处无异。
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 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。
常见的序列化协议有哪些?
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)