【7】反射机制
7.1 什么是反射机制
在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取信息以及动态调用对象的方法的功能称为反射机制。
7.2 如何实现反射
Java代码在计算机中会经历三个阶段:
- 阶段一:Source源代码阶段(.java文件通过javac被编译成.class字节码文件)
- 阶段二:Class类对象阶段(.class字节码文件被类加载器ClassLoader加载进内存,并将其封装成Class对象)
- 阶段三:RuntTime阶段(通过new创建对象)
每个类都有一个Class对象,包含了与类相关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内保存着 Class 对象。类加载相当于 Class 对象的加载。
7.3 反射相关的类
- Field 类:提供有关类的属性信息,以及对它的动态访问权限。它是一个封装反射类的属性的类。
- Constructor 类:提供有关类的构造方法的信息,以及对它的动态访问权限。它是一个封装反射类的构造方法的类。
- Method 类:提供关于类的方法的信息,包括抽象方法。它是用来封装反射类方法的一个类。
- Class 类:表示正在运行的 Java 应用程序中的类的实例。
- Object 类:Object 是所有 Java 类的父类。所有对象都默认实现了 Object 类的方法。
7.4 反射机制的作用
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。具体来说:
- 在运行时判断任意一个对象所属的类
- 在运行时构造一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法,生成动态代理
7.5 反射机制的优缺点
优点:提高代码的灵活性;
缺点:
- 性能开销增大:反射涉及动态类型的解析,所以JVM无法对这些代码进行优化,因此,反射操作效率比非反射操作低得多;
- 安全问题:例如可以无视泛型参数的安全检查;
- 内部暴露:由于反射可以允许代码执行一些在正常情况下不被允许的操作,例如,访问私有属性,所以使用反射可能会导致代码功能失调并破坏可移植性;
- 破坏了封装性以及泛型约束。
7.6 反射机制的应用场景
- 框架中的动态代理的实现依赖于反射机制
- 注解的实现也用到了反射
7.7 反射实战
1、获取Class对象
- 通过类的全路径字符串获取Class对象(Class.forName("路径"))
Class studentClass1 = Class.forName("com.test.reflection.Student");
- 通过类的class属性(类名.class)
Class studentClass2 = Student.class;
- 通过对象的getClass()函数(对象名.getClass())
Student studentObject = new Student();
Class studentClass3 = studentObject.getClass();
- 通过类加载器ClassLoader.loadClass()传入类路径
Class studentClass4 = ClassLoader.LoadClass("com.test.reflection.studentObject");
2、获取成员变量
- 通过getDeclaredFields()函数(获取所有成员变量)
Field[] declaredFieldList = studentClass.getDeclaredFields();
- 通过getFields()函数(只能获取公有成员变量)
Field[] fieldList = studentClass.getFields();
3、获取构造方法
- 通过getDeclaredConstructors()函数(获取所有声明的构造方法)
Constructor[] declaredConstructorList = studentClass.getDeclaredConstructors();
- 通过getConstructors()函数(只能获取公有构造方法)
Constructor[] constructorList = studentClass.getConstructors();
4、获取非构造方法
- 通过getDeclaredMethods()函数(获取所有声明的函数)
Method[] declaredMethodList = studentClass.getDeclaredMethods();
- 通过getMethods()函数(只能获取公有函数)
Method[] methodList = studentClass.getMethods();
反射使用的步骤:
1. 获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
2. 调用Class类中的方法,也就是反射的使用阶段。
3. 使用反射API来操作这些信息。
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常的调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
Class clz = Class.forName("com.chenshuyi.api.Apple");//获取类的Class实例对象
Constructor appleConstructor = clz.getConstructor();//根据Class对象实例获取Constructor对象
Object appleObj = appleConstructor.newInstance();//使用Constructor对象的newInstance方法获取反射类对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);//获取方法的Method对象
setPriceMethod.invoke(appleObj, 14);//利用invoke()方法调用方法
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
}
}
【8】异常
8.1 异常分类
java.lang包中的Throwable类是所有异常的父类。Throwable类有两个重要的子类,Error类(错误)和Exception类(异常):
- Error类:一般指与虚拟机相关的问题,是程序无法处理的错误。如:虚拟机内存不够错误(OutOfMemoryError)、Java虚拟机运行错误(VirtualMachineError)等。这类错误发生时,Java虚拟机(JVM)一般会做中断处理。
- Exception类:程序本身可以处理的异常。可分为受检查异常(必须处理)和不受检查异常(可以不处理)。
受检查异常(Checked Exception):在编译器,受检查异常必须被处理,否则就没法通过编译。要么通过try...catch...捕获,要么通过throws声明抛出,交给父类处理。如:ClassNotFoundException,SQLException等。
不受检查异常(Un-Checked Exception):即运行期异常(RuntimeException),可以编译通过,但程序一运行就会停止,程序不会自己处理。如:NullPointerException、ArrayIndexOutOfBoundsException等。
8.2 异常的处理方式
1、throws声明异常
当前方法不处理异常,而是将异常交给方法的调用者来进行异常处理
public static void main(String[] args) throws FileNotFoundException {
read("a.txt");
}
public static void read(String path) throws FileNotFoundException{
throw new FileNotFoundException("File not exist");
}
2、try-catch捕获异常
在该方法中使用try...catch...语句进行异常处理
try:捕获异常,其后可以接零个或多个catch,如果没有catch,则必须跟一个finally
catch:处理捕获到的异常
try{
可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
8.3 throw和throws的区别
throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例;
throw new XXXException(参数);
throws:在方法声明后面,表示如果抛出异常,则交由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。
8.4 try-catch-finally详解
try{
可能出现异常的代码
代码语句1
}catch (异常类名A e){
A类型异常处理方式
}catch (异常类名B e){
B类型异常处理方式
}...(catch可以有多个)
finally{
最终必须要执行的代码(例如释放资源的代码)
}
代码语句2
代码执行顺序如下,分两种情况讨论:
1)若try没有捕获到异常:
- try内语句将逐一被执行;
- 程序将跳过catch语句块,执行finally内语句和其后的语句“代码语句2”。
2)若try捕获到异常:
- try内中断执行,异常之后的语句“代码语句1”将不会被执行;
- 若catch内没有处理此类异常的方法,此异常将会抛出给JVM处理,finally内语句还是会被执行,但是finally之后的语句“代码语句2”不会被执行;
- 若catch内有处理此类异常的方法,将按顺序与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行。然后执行finally内语句和其后的语句“代码语句2”。
注意:
- 不论是否捕获或处理异常,finally里的语句都会被执行;
- 如果catch内的异常类存在父子类的关系,则子类在前,父类在后;
- 如果finally中有return语句,永远返回finally中的结果。
但在以下3种情况中,finally语句将不会被执行:
- 在try或finally中使用System.exit(int)退出程序,但若System.exit(int)在异常语句之后,finally还是会被执行;
- 程序所在的线程死亡;
- 关闭CPU。
【9】文件与I/O流
9.1 什么是IO流
流(Stream)是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
9.2 IO流的分类
按数据流的方向:
输入流:把数据从其他设备上读取到内存中的流
输出流:把数据从内存中写出到其他设备上的流
按数据处理单位:
字节流:以字节为单位,读写数据的流(一般用于图像或其他文件)
字符流:以字符为单位,读写数据的流(一般用于文本文件)
PS:1字符=2字节 1字节=8比特
按实现供能(按照流的作用对象):
节点流:可以从或向一个特定的地方(节点)读写数据(直接作用于数据)
处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写(作用于数据的传输,即作用于节点流)
Java IO流的4个抽象基类:
输入流 | 输出流 | |
字节流 | 字节输入流(InputStream) | 字节输出流(OutputStream) |
字符流 | 字符输入流(Reader) | 字符输出流(Writer) |
9.3 既然有了字节流,为什么还要有字符流
字符流是由Java虚拟机将字节转换得到的,但是这个转换过程非常耗时,且如果不知道编码类型,很容易出现乱码问题。因此,I/O提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
9.4 IO模型
补充:进程的地址空间分为用户空间和内核空间,我们平时所运行的应用程序都运行在用户空间,只有内核空间才能进行系统级别的资源有关的操作,如文件管理、进程通信、内存管理等。因此,想要进行IO操作,就必须依赖内核空间的能力。因为用户空间的程序不能直接访问内核空间,因此,当想要执行IO操作时,只能发起系统调用请求操作系统帮忙完成。
即,应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起IO调用后,会经历两个步骤:
- 内核等待I/O设备准备好数据;
- 内核将数据从内核空间拷贝到用户空间。
三种常见的IO模型:
- BIO(Blocking I/O):同步阻塞I/O模型。数据的读写必须阻塞在一个线程内等待其完成。
如图所示,当应用程序发起read调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO(Non-blocking/New I/O):同步非阻塞I/O模型。NIO支持面向缓冲的、基于通道的I/O操作方法。
如图所示, 应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。相比于同步阻塞 I/O 模型,同步非阻塞 I/O 模型有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。因此,提出了I/O多路复用模型。
如图所示,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。I/O 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java中的NIO,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
- AIO(Asynchromous I/O):异步非阻塞I/O模型。
异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
最后,用一张图,简单总结一下Java中的BIO、NIO和AIO。
参考