Java基础知识(异常,反射,注解,SPI,序列化,I/O,语法糖)

目录

1. 异常

1.1 Exception和Error的区别

1.2 try-catch-finally的使用

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

1.4 异常的使用有哪些需要注意的?

2. 反射

2.1 反射的优缺点

2.2 反射的应用场景

3. 注解

3.1 注解的定义

3.2 注解的解析方法

4. SPI

4.1 什么是SPI?

4.2 SPI和API的区别

4.3 SPI的优缺点

5. 序列化和反序列化

5.1 什么是序列化和反序列化

5.2 序列化协议对应于TCP/IP4的哪一层呢?

5.3 常用的序列化协议

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

JDK自带的序列化方式

6. I/O

6.1 什么是I/O流?

6.2 字节流

6.2.1 字节输入流

6.2.2 字节输出流

6.3 字符流

6.3.1 为什么要分为字节流和字符流?

6.3.2 Reader

6.3.3 Writer

6.4 字节缓冲流

6.5 字符缓冲流

6.6 打印流

6.7 随机访问流

6.7.1 RandomAccessFile的构造函数

6.7.2 read & write

6.7.3 断点续传

6.8 I/O的设计模式

6.8.1 装饰器模式

为啥我们直接不弄一个BufferedFileInputStream(字符缓冲文件输入流)呢?

6.8.2 适配器模式

适配器模式和装饰器模式有什么区别呢?

6.8.3 工厂模式

6.8.4 观察者模式

6.9 IO模型

6.9.1 IO的工作流程

6.9.2 IO的常见模型

6.9.3 阻塞IO(BIO)

6.9.4 非阻塞IO(NIO)

6.9.5 IO多路复用

6.9.6 信号驱动IO

6.9.7 异步IO(AIO)

6.9.8 同步和异步的区别

7. 语法糖

7.1 常见语法糖

7.2 几个常见的坑

7.2.1 泛型

7.2.1.1 泛型遇到重载

7.2.1.2 当泛型遇到catch

7.2.1.3 当泛型内包含静态变量

7.2.2 自动装箱和拆箱

7.2.2.1 对象相等比较

7.2.3 增强For循环

参考:


1. 异常

1.1 Exception和Error的区别

        在 Java 中,所有的异常都继承自一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:Exception和error。

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • ErrorError 属于程序无法处理的错误 ,不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

注:Checked Exception 就是在无法通过编译过程的异常,如IO 相关的异常、ClassNotFoundException、SQLException;Unchecked Exception就是在Java 代码在编译过程中 ,我们即使不处理也可以正常通过编译,但是会在运行时出现的异常,如:NullPointerException, IllegalArgumentException等。

1.2 try-catch-finally的使用

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

注:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值(即finally语句的return执行后,直接就返回了,不会执行try中的return)。

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

不一定,下面三种情况就不会:

  • finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
  • 程序所在的线程死亡。
  • 关闭 CPU。

1.4 异常的使用有哪些需要注意的?

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义。
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
  • 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。

2. 反射

2.1 反射的优缺点

  • 更加灵活,反射赋予了我们在运行时分析类以及执行类中方法的能力,为各种框架提供开箱即用的功能提供了便利。
  • 安全问题,比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。
  • 性能稍差,反射的性能要稍差点,不过,对于框架来说实际是影响不大的。

2.2 反射的应用场景

  • 框架中也大量使用了动态代理,而动态代理的实现也依赖反射
  • Java的注解

2.3 反射的原理

  1. Java源代码(.java文件)经过编译后生成字节码文件(.class文件)。当Java程序运行时,JVM会加载这些字节码文件到内存中,并转换成java.lang.Class对象。
  2. 通过Class.forName(类的全限定名)获得Class对象。 或者利用类的加载器来加载类并获取其Class对象,classLoader.loadClass("com.example.MyClass")。
  3. 通过调用Class.newInstance()Constructor.newInstance()方法,可以在运行时动态地创建类的对象,而不需要提前在代码中显式地声明该对象。加载一个类时,JVM会委托给ClassLoader去完成。
  4. 通过Method.invoke()Field.get()/Field.set()方法,反射机制允许程序在运行时调用对象的任意方法,包括私有方法。
// 1. 获取Class对象
Class<?> personClass = Class.forName("com.example.Person");
// 2. 获取构造函数,也可以直接Class.newInstance
Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
// 3. 创建对象
Object personInstance = constructor.newInstance("Alice", 30);
// 4. 调用方法
Method toStringMethod = personClass.getMethod("toString");  
String result = (String) toStringMethod.invoke(personInstance);  
System.out.println(result); // 输出: Person{name='Alice', age=30}

3. 注解

3.1 注解的定义

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

3.2 注解的解析方法

注解只有在被解析后才会生效。

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

4. SPI

4.1 什么是SPI?

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

4.2 SPI和API的区别

        API是已经实现完了接口,调用方调用就可以使用了;SPI是调用方定义接口,实现方进行实现。

4.3 SPI的优缺点

  • 通过 SPI 机制能够大大地提高接口设计的灵活性
  • 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
  • 当多个 ServiceLoader 同时 load 时,会有并发问题

5. 序列化和反序列化

        如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。(网络传输,文件存储,数据库存储,内存存储)序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

5.1 什么是序列化和反序列化

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

5.2 序列化协议对应于TCP/IP4的哪一层呢?

OSI七层模型:表示层

TCP/IP四层模型:应用层

5.3 常用的序列化协议

        JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

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

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

JDK自带的序列化方式

JDK 自带的序列化,只需实现 java.io.Serializable接口即可。

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    private String interfaceName;
    private String methodName;
    private Object[] parameters;
    private Class<?>[] paramTypes;
    private RpcMessageTypeEnum rpcMessageTypeEnum;
}

注:

  1. 序列化号 serialVersionUID 属于版本控制的作用。反序列化时,会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID
  2. 在序列化时,static注释的变量都不会被序列化为二进制流,但serialVersionUID比较特殊,其会被用作一个标记,与序列化后的数据一起存储,反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 InvalidClassException,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。
  3. 不想要序列化的字段可以用 transient 关键字修饰。只能修饰变量,不能修饰类和方法。修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0

6. I/O

6.1 什么是I/O流?

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

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

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

6.2 字节流

6.2.1 字节输入流

  • InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类
  • FileInputStream 是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中,一般我们是不会直接单独使用 FileInputStream ,通常会配合 BufferedInputStream(字节缓冲输入流)来使用。
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
  • DataInputStream 用于读取指定类型数据,不能单独使用,必须结合其它流,比如 FileInputStream。
FileInputStream fileInputStream = new FileInputStream("input.txt");
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();
  • ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流(序列化)。
ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data"));
MyClass object = (MyClass) input.readObject();

6.2.2 字节输出流

  • OutputStream用于将数据(字节信息)写入到目的地(通常是文件)java.io.OutputStream抽象类是所有字节输出流的父类
  • FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。类似于 FileInputStreamFileOutputStream 通常也会配合 BufferedOutputStream(字节缓冲输出流)来使用。
  • DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合其它流,比如 FileOutputStream 。
  • ObjectOutputStream 用于将对象写入到输出流(序列化)。

6.3 字符流

        不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。

6.3.1 为什么要分为字节流和字符流?

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

注:

  • I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
  • 字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码。(utf8 :英文占 1 字节,中文占 3 字节,unicode:任何字符都占 2 个字节,gbk:英文占 1 字节,中文占 2 字节)

6.3.2 Reader

  • Reader用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader抽象类是所有字符输入流的父类
  • InputStreamReader 字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件

6.3.3 Writer

  • Writer用于将数据(字符信息)写入到目的地(通常是文件)java.io.Writer抽象类是所有字符输出流的父类
  • OutputStreamWriter 字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上的封装,可以直接将字符写入到文件

6.4 字节缓冲流

        IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区一次性读取/写入多个字节,从而避免频繁的 IO 操作,减少了系统调用的次数提高流的传输效率。字节缓冲流这里采用了装饰器模式来增强 InputStream 和OutputStream子类对象的功能。

注:

  • 字节流和字节缓冲流性能差别主要体现在我们使用两者的时候都是调用 write(int b)read() 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区(每次都会读取一批数据),大幅减少 IO 次数,提高读取效率。
  • 如果是调用 read(byte b[])write(byte b[], int off, int len) 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
  • BufferedInputStreamBufferedOutputStream的缓冲区(字节数组)的大小默认为 8192 字节(8KB)。

6.5 字符缓冲流

  BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。

6.6 打印流

  • System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。
  • PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。PrintStreamOutputStream 的子类,PrintWriterWriter 的子类。

6.7 随机访问流

        这里指的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile。RandomAccessFile 的实现依赖于 FileDescriptor (文件描述符) 和 FileChannel (内存映射文件)。

6.7.1 RandomAccessFile的构造函数

public RandomAccessFile(File file, String mode)
    throws FileNotFoundException {
    this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete)  throws FileNotFoundException{
  // 省略大部分代码
}

我们可以指定 mode读写模式),读写模式主要有下面四种:

  • r : 只读模式。
  • rw: 读写模式
  • rws: 相对于 rwrws 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。
  • rwd : 相对于 rwrwd 同步更新对“文件的内容”的修改到外部存储设备。

文件内容指的是文件中实际保存的数据元数据则是用来描述文件属性比如文件的大小信息、创建修改时间

注:RandomAccessFile 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 RandomAccessFileseek(long pos) 方法来设置文件指针的偏移量(距文件开头 pos 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer() 方法。

6.7.2 read & write

  • read会读取文件指针所指的位置,read后文件指针会指向下一个位置
  • write方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖

6.7.3 断点续传

  RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。RandomAccessFile 可以帮助我们合并文件分片

6.8 I/O的设计模式

6.8.1 装饰器模式

        装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。

注:组合通常指的是一个对象包含另一个对象作为其属性(也称为成员变量或字段)。

        对于字节流来说, FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是装饰器模式的核心,分别用于增强 InputStreamOutputStream子类对象的功能。我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。

  BufferedInputStream 构造函数如下,BufferedInputStream 的构造函数其中的一个参数就是 InputStream

public BufferedInputStream(InputStream in) {
    this(in, DEFAULT_BUFFER_SIZE);
}

public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}

可以通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream(InputStream的子类) 的功能:

try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
    int content;
    long skip = bis.skip(2);
    while ((content = bis.read()) != -1) {
        System.out.print((char) content);
    }
} catch (IOException e) {
    e.printStackTrace();
}
为啥我们直接不弄一个BufferedFileInputStream(字符缓冲文件输入流)呢?

:如果 InputStream的子类比较少的话,这样做是没问题的。不过, InputStream子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。

注:

  • 装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器
  • 装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口

6.8.2 适配器模式

        适配器(Adapter Pattern)模式 主要用于接口互不兼容的类协调工作。适配器模式中存在被适配的对象或者类称为 适配者(Adapter) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

  • 类适配器模式基于继承来实现适配器功能。当我们要访问的接口A中没有我们想要的方法,却在另一个接口B或类B中发现了合适的方法时,我们可以定义一个适配器P来进行中转。这个适配器P需要实现我们访问的接口A,并继承接口B的实现类BB(或类B,如果B不是接口)。这样,适配器P就能访问接口B(或类B)的方法,并在接口A的方法中直接引用这些方法。
  • 对象适配器模式基于组合来实现适配器功能。与类适配器类似,当我们要访问的接口A中没有我们想要的方法,却在另一个接口B或类B中发现了合适的方法时,我们可以定义一个适配器P来进行中转。这个适配器P需要实现我们访问的接口A,并在其内部定义一个私有变量来持有接口B(或类B)的实例。适配器P在接口A的方法中调用这个实例的相应方法。

        IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。

  InputStreamReaderOutputStreamWriter 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。InputStreamReader 使用 StreamDecoder (流解码器)对字节进行解码,实现字节流到字符流的转换, OutputStreamWriter 使用StreamEncoder(流编码器)对字符进行编码,实现字符流到字节流的转换。InputStream 和 OutputStream 的子类是被适配者, InputStreamReader 和 OutputStreamWriter是适配器。

// InputStreamReader 是适配器,FileInputStream 是被适配的类
InputStreamReader isr = new InputStreamReader(new FileInputStream(fileName), "UTF-8");
// BufferedReader 增强 InputStreamReader 的功能(装饰器模式)
BufferedReader bufferedReader = new BufferedReader(isr);

InputStreamReader的部分源码:

public class InputStreamReader extends Reader {
 //用于解码的对象
 private final StreamDecoder sd;
    public InputStreamReader(InputStream in) {
        super(in);
        try {
            // 获取 StreamDecoder 对象
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }
    // 使用 StreamDecoder 对象做具体的读取工作
 public int read() throws IOException {
        return sd.read();
    }
}
适配器模式和装饰器模式有什么区别呢?
  • 装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。
  • 适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 StreamDecoder (流解码器)和StreamEncoder(流编码器)就是分别基于 InputStreamOutputStream 来获取 FileChannel对象调用对应(sd)的 read 方法和 write 方法进行字节数据的读取和写入。适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口

6.8.3 工厂模式

        工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)、ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。

6.8.4 观察者模式

        NIO 中的文件目录监听服务使用到了观察者模式。NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。WatchService 属于观察者,Watchable 属于被观察者。

  Watchable 接口定义了一个用于将对象注册到 WatchService(监控服务) 并绑定监听事件的方法 register

public interface Path
    extends Comparable<Path>, Iterable<Path>, Watchable{
}

public interface Watchable {
    WatchKey register(WatchService watcher,
                      WatchEvent.Kind<?>[] events,
                      WatchEvent.Modifier... modifiers)
        throws IOException;
}

  WatchService 用于监听文件目录的变化,同一个 WatchService 对象能够监听多个文件目录。register 方法的第二个参数 events (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。

常用的监听事件有 3 种:

  • StandardWatchEventKinds.ENTRY_CREATE:文件创建。
  • StandardWatchEventKinds.ENTRY_DELETE : 文件删除。
  • StandardWatchEventKinds.ENTRY_MODIFY : 文件修改。

  register 方法返回 WatchKey 对象,通过WatchKey 对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。

注:WatchService 内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化。

6.9 IO模型

6.9.1 IO的工作流程

        首先要明确的一点是,用户空间的程序不能直接访问内核空间,要执行IO操作时,只能发起系统调用请求操作系统帮忙间接访问内核空间。总的来说,应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。具体可分为两个步骤:

  • 内核等待 I/O 设备准备好数据
  • 内核将数据从内核空间拷贝到用户空间。

6.9.2 IO的常见模型

        IO 模型一共有 5 种:同步阻塞 I/O同步非阻塞 I/OI/O 多路复用信号驱动 I/O 和异步 I/O

6.9.3 阻塞IO(BIO)

        阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

        在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

6.9.4 非阻塞IO(NIO)

        非阻塞 IO 模型中,应用程序会一直发起 read 调用(轮询),等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

        相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

6.9.5 IO多路复用

        IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

        IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

注:select,poll,epoll的区别:

  • select需要将fd-set(需要监听事件的fd集合)从内核空间和用户空间中反复拷贝(无论是就绪的还是未就绪的),并且无法得知哪个fd就绪了,fd-set还有数量上限(1024)。
  • poll仅仅对select的fd-set存储数量上限进行了改进(链表,大小自定义),理论上无上限,实际上,监听的fd越多,每次遍历看数组里就绪的fd的时间就越长,性能反而会下降。
  • epoll使用红黑树存储要监听的fd(fd只需要添加一次进红黑树即可,无需重复拷贝至内核空间),理论无上限,而且增删改查的效率特别高,性能不会随着监听的fd数量的增多而下降。当某个文件描述符的状态发生变化时,内核会将其添加到就绪队列中,并通知用户空间。已就绪的fd会存入一个就绪列表中并拷贝回用户空间,这样就可以得知哪个fd就绪了(无需像select和poll那样遍历fd-set)。

基于epoll模式的web服务的基本流程:

注:当应用程序调用 epoll_wait 时,它会阻塞等待,直到至少有一个文件描述符上的事件发生了或者超过了指定的超时时间或者调用被信号中断,epoll_wait 系统调用会被唤醒,并返回这些事件给用户空间的应用程序。

6.9.6 信号驱动IO

       信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其他业务,无需阻塞。

        当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出。而且内核空间与用户空间的频繁信号交互会导致性能下降。

6.9.7 异步IO(AIO)

        异步IO的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其他事情,内核等待数据就绪并拷贝到用户空间后才会递交信号(信号驱动IO在数据就绪后就会返回信号),通知用户进程。 AIO 的应用还不是很广泛,性能提升不大。

6.9.8 同步和异步的区别

        IO操作是同步还是异步,关键在看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是第二阶段是同步还是异步。

7. 语法糖

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

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

7.1 常见语法糖

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

7.2 几个常见的坑

7.2.1 泛型

7.2.1.1 泛型遇到重载
public class GenericTypes {

    public static void method(List<String> list) {
        System.out.println("invoke method(List<String> list)");
    }

    public static void method(List<Integer> list) {
        System.out.println("invoke method(List<Integer> list)");
    }
}

      上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>另一个是List<Integer> ,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>List<String>编译之后都被类型擦除了变成了一样的原生类型 List,擦除动作导致这两个方法的特征签名变得一模一样

7.2.1.2 当泛型遇到catch

        泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException<String>MyException<Integer>的。      

7.2.1.3 当泛型内包含静态变量
public class StaticTest{
    public static void main(String[] args){
        GT<Integer> gti = new GT<Integer>();
        gti.var=1;
        GT<String> gts = new GT<String>();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class GT<T>{
    public static int var=0;
    public void nothing(T x){}
}

// 输出 2

        可能会误认为泛型类是不同的类,对应不同的字节码,其实由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的静态变量是共享的。上面例子里的GT<Integer>.varGT<String>.var其实是一个变量。

7.2.2 自动装箱和拆箱

7.2.2.1 对象相等比较
public static void main(String[] args) {
    Integer a = 1000;
    Integer b = 1000;
    Integer c = 100;
    Integer d = 100;
    System.out.println("a == b is " + (a == b));
    System.out.println(("c == d is " + (c == d)));
}
// 输出
a == b is false
c == d is true

        在 Java 5 中,在 Integer 的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用(只适用于-128至127;只适用于自动装箱,使用构造函数创建对象new Integer()不适用

注:具体是通过整型常量池实现的,整型常量在编译后存储在.class文件的静态常量池中,当类被加载到JVM时,静态常量池中的常量会被加载到方法区的运行时常量池中。

7.2.3 增强For循环

for-each 的实现原理其实就是使用了普通的 for 循环迭代器

for (Student stu : students) {
    if (stu.getId() == 2)
        students.remove(stu);
}
// 会抛出ConcurrentModificationException异常

        Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变(list.remove),所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。

        所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法remove()来删除对象(students.remove是list.remove方法)Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

参考:

Java IO 模型详解 | JavaGuide

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值