八、异常处理和I/O
8.1 异常处理
8.1.1 异常概述
异常处理使得程序可以处理非预期的情景,并且继续正常的处理。 异常是从方法抛出的。方法的调用者可以捕获以及处理该异常 使用异常处理的优点。它能使方法抛出一个异常给它的调用者,并由调用者处理该异常。如果没有这个能力,那么被调用的方法就必须自己处理异常或者终止该程序。 异常处理最根本的优势就是将检测错误(由被调用的方法完成)从处理错误(由调用方法完成)中分离出来。
8.1.2 异常类型
异常是对象,而对象都采用类来定义。异常的根类是 java.lang.Throwable 类名 Error、Exception 和 RuntimeException 有时候容易引起混淆。这三种类都是异常,这里讨论的错误都发生在运行时 Throwable 类是所有异常类的根。所有的 Java 异常类都直接或者间接地继承自Throwable。可以通过继承 Exception 或者 Exception 的子类来创建自己的异常类。 异常类可以分为三种主要类型: 1、系统错误:是由 Java 虚拟机抛出的,用 Error 类表示。Error 类描述的是内部系统错误。这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也不能做。 2、异常:是用 Exception 类表示的,它描述的是由程序和外部环境所引起的错误,这些错误能被程序捕获和处理。 3、运行时异常:是用 RuntimeException 类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界数组或数值错误。运行时异常通常是由 Java 虚拟机抛出的。 RuntimeException、Error 以及它们的子类都称为免检异常(unchecked exception )。所有其他异常都称为必检异常(checked exception), 意思是指编译器会强制程序员检査并通过 try- catch 块处理它们,或者在方法头进行声明
8.1.3 异常的处理
Java 的异常处理模型基于三种操作: 1、 声明一个异常(declaring an exception) 2、 抛出异常(throwing an exception) 3、 捕获一个异常(catching an exception) 如果方法没有在父类中声明异常,那么就不能在子类中对其进行继承来声明异常 通常,JavaAPI 中的每个异常类至少有两个构造方法:一个无参构造方法和一个带可描述这个异常的 String 参数的构造方法。该参数称为异常消息, 它可以用 getMessage()获取 声明异常的关楗字是 throws, 抛出异常的关键字是 throw。 从一个通用的父类可以派生出各种异常类。如果一个 catch 块可以捕获一个父类的异常对象,它就能捕获那个父类的所有子类的异常对象。 在 catch 块中异常被指定的顺序是非常重要的。如果父类的 catch 块出现在子类的catch 块之前,就会导致编译错误。 Java 强迫程序员处理必检异常。如果方法声明了一个必检异常(即 Error 或Runtime Exception 之外的异常),就必须在 try-catch 块中调用它,或者在调用方法中声明要抛出异常 在异常事件中,执行仍然会继续。如果处理器没有捕获到这个异常,程序就会突然中断
8.1.4 finally子句
无论异常是否产生,finally 子句总是会被执行的。 在任何情况下,finally 块中的代码都会执行,不论 try 块中是否出现异常或者是否被捕获。考虑下面三种可能出现的情况: 1、如果 try 块中没有出现异常,执行 finally, 然后执行 try 语句的下一条语句 2、如果 try 块中有一条语句引起异常,并被 catch 块捕获,然后跳过 try 块的其他语句,执行 catch 块和 finally 子句。执行 try 语句之后的下一条语句。 3、如果 try 块中有一条语句引起异常,但是没有被任何 catch 块捕获,就会跳过 try 块中的其他语句,执行 finally 子句,并且将异常传递给这个方法的调用者 即使在到达 finally 块之前有一个 return 语句,finally 块还是会执行。 使用 finally 子句时可以省略掉 catch 块
8.1.4 何时使用异常
当姆误需要被方法的调用者处理的时候,方法应该抛出一个异常 异常处理将错误处理代码从正常的程序设计任务中分离出来,这样,可以使程序更易读、更易修改。但是,应该注意,由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以,异常处理通常需要更多的时间和资源。 异常出现在方法中。如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。 — 般来说,一个项目中多个类都会发生的共同异常应该考虑作为一种异常类。对于发生在个别方法中的简单错误最好进行局部处理,无须抛出异常。 有一点要把握住,不要把异常处理用作简单的逻辑测试。
8.1.5 重新抛出异常
如果异常处理器不能处理一个异常,或者只是简单地希望它的调用者注意到该异常,Java 允许该异常处理器重新抛出异常
try{
...
}catch(Exception e){
throw e;
}
8.1.6 链式异常
catch(Exception e){
throw new Exception("....",e);//先执行...信息,在执行e
}
8.1.7 创建自定义异常
可以通过派生 java.lang.Exception 类来定义一个自定义异常类 可以扩展 RuntimeException 声明一个自定义异常类吗?可以,但这不是一个好方法,因为这会使自定义异常成为免检异常。最好使自定义异常必检,这样,编译器就可以在程序中强制捕获这些异常
8.2 I/O流
8.2.1 File类
File 类包含了获得一个文件 / 目录的属性,以及对文件 / 目录进行改名和删除的方法 在 Windows 中目录的分隔符是反斜杠()。但是在 Java 中,反斜杠是一个特殊的字符,应该写成 \的形式 构建一个 File 实例并不会在机器上创建一个文件。不管文件是否存在,都可以创建任意文件名的 File 实例。可以调用 File 实例上的 exists() 方法来判断这个文件是否存在。 斜杠(/)是 Java 的目录分隔符,这点和 UNIX 是一 样的。
8.2.1 文件的输入和输出
使用 Scanner 类从文件中读取文本数据,使用 PrintWriter 类向文本文件写入数据
使用 try-with-resources 自动关闭资源
try( 声明和创建资源){
使用资源来处理文件 ;
}
行分隔符字符串是由系统定义的,在 Windows 平台上是\r\n, 而在 UNIX 平台上是 \n。为了得到特定平台上的行分隔符,使用:
8.2.2 从web上读取数据
首先要使用:java.net.URL 类的这个构造方法,为该文件创建一个URL 对象 创建一个 URL 对象后,可以使用 URL 类中定义的 openStream()方法来打开输入流 现在可以从输人流中读取数据了,如同从本地文件中读取一样。
8.2.3 在 Java 中如何处理文本I/O
使用 Scanner 类读取文本数据,使用 PrintWriter 类写文本数据
8.2.4 文本I/O和二进制I/O
二进制 I/O 不涉及编码和解码,因此比文本 I/O 更加高效
二进制I/O
抽象类 InputStream 是读取二进制数据的根类,抽象类 OutputStream 是写入二进制数据的根类。 二进制 I/O 类 中 的 所 有 方 法 都 声 明 为 抛 出 java.io.IOException 或 java.io.IOException 的子类。 当流不再需要使用时,记得使用 closeO 方法将其关闭,或者使用 try-with-resource语句自动关闭。不关闭流可能会在输出文件中造成數据受损,或导致其他的程序设计错误。 FilelnputStream 类 的 实 例 可 以 作 为 参 数 去 构 造 一 个 Scanner 对 象, 而FileOutputStream 类的实例可以作为参数构造一个 PrinterWriter 对象。可以创建一个 PrinterWriter 对象来向文件中追加文本。如果 temp.txt 不存在,就会创建这个文件。如果 temp.txt 文件已经存在,就将新数据追加到该文件中
FilterlnputStream/ FilterOutputStream
过滤器数据流( filter stream) 是为某种目的过滤字节的数据流。基本字节输人流提供的读取方法 read 只能用来读取字节。如果要读取整数值、双精度值或字符串,那就需要一个过滤器类来包装字节输入流。使用过滤器类就可以读取整数值、双精度值和字符串,而不是字节或字符。FilterlnputStream 类和 FilterOutputStream 类是过滤数据的基类。需要处理基本数值类型时,就使用 DatalnputStream 类和 DataOutputStream 类来过滤字节,应该按存储的顺序和格式读取文件中的数据。
二进制 I/O 中的字符与字符串
— 个统一码由两个字节构成。writerCharCchar O 方法将字符 c 的统一码写入输出流。writerChars(String s) 方法将字符串 s 中所有宇符的统一码写到输出流中。 writeBytes(String s) 方法将字符串 s 中每个字符统一码的低字节写到输出流。统一码的高字节被丢弃。 writeBytes 方法适用于由 ASCII 码字符构成的字符串,因为 ASCII 码仅存储统一码的低字节。如果一个字符串包含非 ASCII 码的字符,必须使用 writeChars 方法实现写入这个字符串。 UTF-8 的修改版方案分别使用 1 字节、2字节或 3 字节来存储字符。如果字符的编码值小于或等于 0x7F 就将该字符编码为一个宇节,如果字符的编码值大于 0X7F 而小于或等于 0X7FF 就将该字符编码为两个字节,如果该字符的编码值大于0X7FF 就将该字符编码为三个字节。 UTF-8 字符起始的几位表明这个字符是存储在一个字节、两个字节还是三个字节中。如果首位是 0, 那它就是一个字节的字符。如果前三位是 110, 那它就是两字节序列的第一个宇节。如果前四位是 1110, 那它就是三字节序列的第一个字节。UTF-8 字符之前的两个字节用来存储表明字符串中的字符个数的信息。 UTF-8 格式具有存储每个 ASCII 码就节省一个字节的优势,因为一个统一码字符的存储需要两个字节,而在 UTF-8 格式中 ASCII 字符仅占一个字节。如果一个长字符串的大多数字符都是普通的 ASCII 字符,采用 UTF-8 格式存储更加高效
BufferedlnputStream/BufferedOutputStream
BufferedlnputStream 类和 BufferedOutputStream 类可以通过减少磁盘读写次数来提高输入和输出的速度。 使用 BufferdlnputStream 时,磁盘上的整块数据一次性地读入到内存中的缓冲区中。然后从缓冲区中将个别的数据传递到程序中。 使用BufferedOutputStream, 个别的数据首先写人到内存中的缓冲区中。当缓冲区已满时,缓冲区中的所有数据一次性写入到磁盘中。 应该总是使用缓冲区 1/0 来加速输入和输出。对于小文件,我们可能注意不到性能的提升。但是,对于超过 100MB 的大文件,我们将会看到使用缓冲的 I/O 带来的实质性的性能提升。
8.2.5 对象I/O
ObjectlnputStream 类和 ObjectOutputStreara 类可以用于读写可序列化的对象 完全可以用 ObjectlnputStream 和ObjectOutputStream 类代替 DatalnputStream 类和DataOutputStream 类。 必须以数据写人文件时的顺序和格式从文件中读取这些数据
Serializable 接口
并不是每一个对象都可以写到输出流。可以写入输出流中的对象称为可序列化的( serializable) ,因为可序列化的对象是java.io.Serial’izable 接口的实例,所以,可序列化对象的类必须实现 Serializable 接口。 当存储一个可序列化对象时,会对该对象的类进行编码。编码包括类名、类的签名、对象实例变量的值以及该对象引用的任何其他对象的闭包,但是不存储对象静态变量的值 。 非序列化的数据域 如果一个对象是 Serializable 的实例,但它包含了非序列化的实例数据域,那么可以序列化这个对象吗?答案是否定的。为了使该对象是可序列化的,需要给这些数据域加上关键字 transient, 告诉 Java 虚拟机将对象写入对象流时忽略这些数据域。思考下面的类:
public class C implements java.1o.Serializable {
private int vl;
private static double v2;
private transient A v3 = new A();
}
class A { }// A is not serializable
当 c 类的一个对象进行序列化时,只需序列化变量v1。因为 v2 是一个静态变量,所以没有序列化。因为 v3 标记为 transient, 所以也没有序列化。如果 v3 没有标记为 transient, 将会发生java.io.NotSerializableException 重复的对象如果一个对象不止一次写入对象流,会存储对象的多份副本吗?答案是不会。第一次写入一个对象时,就会为它创建一个序列号。Java 虚拟机将对象的所有内容和序列号一起写入对象流。以后每次存储时,如果再写入相同的对象,就只存储序列号。读出这些对象时,它们的引用相同,因为在内存中实际上存储的只是一个对象 如果数组中的所有元素都是可序列化的,这个数组就是可序列
8.2.6 随机访问数据
Java 提供了 RandomAccessFile 类,允许从文件的任何位置进行数据的读写 当创建一个 RandomAccessFile 时,可以指定两种模式( “r"或"rw”) 之一。模式"r"表明这个数据流是只读的,模式"rw"表明这个数据流既允许读也允许写。
RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
以调用 raf .seek(position) 方法将文件指针移到指定的位置