Java基础(三)——反射、异常和I/O流

【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调用后,会经历两个步骤:

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

三种常见的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。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值