异常处理和文本I/O(Java)

本文深入探讨Java中的异常处理机制,包括异常的抛出、捕获和处理,以及如何使用try-catch-finally结构。同时,介绍了如何利用File类和Scanner、PrintWriter类进行文件的读写操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异常处理和文本 I/O

异常处理使得程序可以处理非预期的情景,并且继续正常的处理。

在程序运行过程中,如果 JVM 检测出一个不可能执行的操作,就会出现运行时错误 (runtime error) 。例如,如果使用一个越界的下标访问数组,程序就会产生一个 ArraylndexOutOfBoundsException 的运行时错误。如果程序需要输入一个整数的时候用户输入了一个 double 值,会得到一个 InputMismatchException 的运行时错误。

在 Java 中,运行时错误会作为异常抛出。异常就是一种对象,表示阻止正常进行程序执行的错误或者情况。如果异常没有被处理,那么程序将会非正常终止。

异常处理概述

异常是从方法抛出的。方法的调用者可以捕获以及处理该异常。不应该让方法来终止程序,应该由调用者决定是否终止程序。Java 可以让一个方法可以抛出一个异常,该异常可以被调用者捕获和处理,从而通知方法的调用者产生了异常。

throw 语句的执行称为批出一个异常 (throwing an exception)。异常就是一个从异常类创建的对象。当异常被抛出时,正常的执行流程就被中断。“抛出异常” 就是将异常从一个地方传递到另一个地方, 调用方法的语句包含在一个 try 块和一个 catch 块中。try 块包含了正常情况下执行的代码,异常被 catch 块所捕获。catch 块中的代码执行以处理异常。

throw 语句类似于方法的调用,但不同于调用方法,它调用的是 catch 块。从某种意义上讲,catch 块就像带参数的方法定义,这些参数匹配抛出的值的类型。但是, 它不像方法,在执行完 catch 块之后,程序控制不返回到 throw 语句,而是执行 catch 块后的下一条语句。

catch 块的头部

catch(ExceptionClass ex)

标识符 ex 的作用很像是方法中的参数。所以,这个参数称为 catch 块的参数。ex 之前的类型指定了 catch 块可以捕获的异常类型。一旦捕获该异常 ,就能从 catch 块体中的参数访问这个抛出的值。总之,一个 try-throw-catch 块的模板可能会如下所示:

try
{
	Code to run;
	A statement or a method that may throw an exception;
    More code to run;
}
catch(type ex)
{
	Code to process the exception;
}

一个异常可能是通过 try 块中的 throw 语句直接抛出,或者调用一个可能会抛出异常的方法而抛出。

使用异常处理的优点: 使方法抛出一个异常给它的调用者,并由调用者处理该异常。如果没有这个能力,那么被调用的方法就必须自己处理异常或者终止该程序。被调用的方法通常不知道在出错的情况下该做些什么,这是库方法的一般情况。库方法可以检测出错误,但是只有调用者才知道出现错误时需要做些什么。异常处理最根本的优势就是将检测错误 (由被调用的方法完成) 从处理错误 (由调用方法完成) 中分离出来。

异常类型

异常是对象,对象都采用类来定义。

异常的根类是 java.lang.Throwable,Throwable 类是所有异常类的根类。所有的 Java 异常类都直接或者间接地继承自 Throwable 类。可以通过继承 Exception 或者 Exception 的子类来创建自己的异常类。

类名 Error 、Exception 和 RuntimeException 这三种类都是异常,错误都发生在运行时。这些异常类可以分为三种主要类型:系统错误、异常和运行时异常。

  • 系统错误 (system error) 是由 Java 虚拟机抛出的,用 Error 类表示。Error 类描述的是内部系统错误。这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也不能做。
  • 异常 (exception) 是用 Exception 类表示的,它描述的是由程序和外部环境所引起的错误,这些错误能被程序捕获和处理。
  • 运行时异常 (runtime exception) 是用 RuntimeException 类表示的,它是 Exception 类的一个子类,描述的是程序设计错误。例如,错误的类型转换、访问一个越界数组或数值错误等等。运行时异常通常是由 Java 虚拟机抛出的。

RuntimeException、Error 以及它们的子类都称为免检异常 (unchecked exception) 。除了运行时异常 (RuntimeException) 之外所有其他异常 (Exception) 都称为必检异常 (checked exception) ,意思是指编译器会强制程序员检査并通过 try-catch 块处理它们,或者在方法头进行声明,否则就不能通过编译。

在大多数情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。例如,如果通过一个引用变量访问一个对象之前并未将一个对象陚值给它,就会抛出 NullPointerException 异常;如果访问一个数组的越界元素,就会抛出 IndexOutOfBoundsException 异常。这些都是程序中必须纠正的逻辑错误。免检异常可能在程序的任何一个地方出现。为避免过多地使用 try-catch 块,Java 语言不强制要求编写代码捕获或声明免检异常。

关于异常处理

异常的处理器是通过从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到的 。

Java 的异常处理模型基于三种操作:声明一个异常 (declaring an exception) 、 抛出一个异常 (throwing an exception) 和捕获一个异常 (catching an exception) 。

声明异常
在 Java 中,当前执行的语句必属于某个方法。Java 解释器调用 main 方法开始执行一个程序。每个方法都必须声明它可能抛出的必检异常的类型。这称为声明异常 (declaring exception) 。因为任何代码都可能发生系统错误和运行时错误,因此 Java 不要求在方法中显式声明 Error 、RuntimeException (免检异常) 。但是,方法要拋出的其他异常都必须在方法头中显式声明,这样方法的调用者会被告知有异常。

在方法中声明一个异常,就要在方法头中使用关键字 throws ,如下所示:

public void myMethod() throws IOException

关键字 throws 表明 myMethod 方法可能会抛出异常 IOException 。

如果方法可能会抛出多个异常,可以在关键字 throws 后添加一个用逗号分隔的异常列表:

public void myMethod() throws Exceptionl , Exception2 , ··· , ExceptionN

注意: 如果方法没有在父类中声明异常,那么就不能在子类中对其进行继承来声明异常。

抛出异常
检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这就称为抛出一个异常 (throwing an exception) 。

假如程序发现传递给方法的参数与方法的合约不符,这个程序就可以创建 IllegalArgumentException 的一个实例并抛出它,如下所示 :

IllegalArgumentException ex = new IllegalArgumentException("Wrong Argument");
throw ex ;

或者,也可以使用下面的语句 :

throw new IllegalArgumentException("Wrong Argument");

注意: IllegalArgumentException 是 Java API 中的一个异常类。通常,Java API 中的每个异常类至少有两个构造方法:一个无参构造方法和一个带可描述这个异常的 String 参数的构造方法。该参数称为异常消息(exceptionmessage) ,它可以用 getMessage() 获取。

提示: 声明异常的关楗字是 throws ,抛出异常的关键字是 throw 。

捕获异常
当抛出一个异常时,可以在 try-catch 块中捕获和处理它,如下所示:

try 
{
	statements ; / / Statements that may throw exceptions
}
catch (Exceptionl exVarl) 
{
	handler for exceptionl;
}
catch (Exception2 exVar2) 
{
	handler for exception2;
}

	···

catch (ExceptionN exVarN) 
{
	handler for exceptionN;
}

如果在执行 try 块的过程中没有出现异常,则跳过 catch 子句。如果 try 块中的某条语句抛出一个异常,Java 就会跳过 try 块中剩余的语句,然后开始査找处理这个异常的代码的过程。处理这个异常的代码称为异常处理器 (exception handler) ;可以从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到这个处理器。从第一个到最后一个逐个检査 catch 块,判断在 catch 块中的异常类实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行 catch 块中的代码。如果没有发现异常处理器,Java 会退出这个方法,把异常传递给调用这个方法的方法,继续同样的过程来査找处理器。如果在调用的方法链中找不到处理器,程序就会终止并且在控制台上打印出错信息。寻找处理器的过程称为捕获一个异常 (catching an exception) 。

如果异常没有在当前的方法中被捕获,就被传给该方法的调用者。这个过程一直重复,直到异常被捕获或被传给 main 方法。如果异常类型最终在 main 方法中也没有被捕获,程序就会终止。

注意: 从一个通用的父类可以派生出各种异常类。如果一个 catch 块可以捕获一个父类的异常对象,它就能捕获那个父类的所有子类的异常对象。

注意: 在 catch 块中异常被指定的顺序是非常重要的。如果父类的 catch 块出现在子类的 catch 块之前,就会导致编译错误。RuntimeException 类是 Exception 类的子类,注意二者顺序。

注意: Java 强迫程序员处理必检异常。如果方法声明了一个必检异常 (即 Error 或 Runtime Exception 之外的异常) ,就必须在 try-catch 块中调用它,或者在调用方法中声明要抛出异常。

注意: 对于使用同样的处理代码处理多个异常的情况,可以使用新的 JDK 7 的多捕获特征
(multi-catch feature) 简化异常的代码编写。语法是:

catch (Exceptionl | Exception2 丨 ··· 丨 ExceptionN ex) 
{
	/ / Same code for handling these exceptions
}

每个异常类型使用竖线 (|) 与下一个分隔。如果其中一个异常被捕获,则执行处理的代码。

从异常中获取倍息
异常对象包含关于异常的有价值的信息。可以利用 java.lang.Throwable 类中的实例方法获取有关异常的信息。

注意: 在异常事件中,执行仍然会继续。如果处理器没有捕获到这个异常,程序就会突然中断。

finally 子句

无论异常是否产生,finally 子句总是会被执行的。

有时候,不论异常是否出现或者是否被捕获,都希望执行某些代码。Java 有一个 finally 子句 , 可以用来达到这个目的。finally 子句的语法如下所示:

try 
{
	statements;
}
catch (TheException ex) 
{
	handling ex;
}
finally 
{
	finalStatements;
}

在任何情况下,finally 块中的代码都会执行,不论 try 块中是否出现异常或者是否被捕获。

三种可能出现的情况 :

  • 如果 try 块中没有出现异常,执行 try 块 statements 语句,然后执行 finally 块 finalStatements 语句,最后执行 try-catch 块以及 finally 块之后的语句。
  • 如果 try 块中有一条语句引起异常,并被 catch 块捕获,然后跳过 try 块的其他语句,执行 catch 块和 finally 子句。最后执行 try-catch 块以及 finally 块之后的语句。
  • 如果 try 块中有一条语句引起异常,但是没有被任何 catch 块捕获,就会跳过 try 块中的其他语句,执行 finally 子句,并且将异常传递给这个方法的调用者。注意,即使在到达 finally 块之前有一个 return 语句,finally 块还是会执行。

提示: try-catch 块和 finally 块中最好不用 return ;

注意: 使用 finally 子句时可以省略掉 catch 块。

何时使用异常

当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。

try 块包含正常情况下执行的代码。catch 块包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计任务中分离出来,这样,可以使程序更易读,更易修改。但是,由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器。所以,异常处理通常需要更多的时间和资源。

异常出现在方法中。如果想让该方法的调用者处理异常,应该创建一个异常对象并将其抛出。如果能在发生异常的方法中处理异常,那么就不需要抛出或使用异常。

一般来说,一个项目中多个类都会发生的共同异常应该考虑作为一种异常类。对于发生在个别方法中的简单错误最好进行局部处理,无须抛出异常。

当必须处理不可预料的错误状况时应该使用 try-catch 块。不要用 try-catch 块处理简单的、可预料的情况。有一点要把握住,不要把异常处理用作简单的逻辑测试。

重新抛出异常

如果异常处理器不能处理一个异常,或者只是简单地希望它的调用者注意到该异常,Java 允许该异常处理器重新抛出异常。

重新拋出异常的语法如下所示:

try 
{
	statements ;
}
catch (TheException ex) 
{
	perform operations before exits;
	throw ex;
}

语句 throw ex 重新抛出异常给调用者,以便调用者的其他处理器获得处理异常 ex 的机会。

链式异常

和其他异常一起抛出一个异常,构成了链式异常。

创建自定义异常类

可以通过派生 java.lang.Exception 类来定义一个自定义异常类。

catch 块重新抛出原始的异常。有时候,可能需要同原始异常一起抛出一个新异常 (带有附加信息) ,这称为链式异常 (chained exception) 。

File类

File 类包含了获得一个文件/目录的属性,以及对文件/目录进行改名和删除的方法。

存储在程序中的数据是暂时的,当程序终止时它们就会丢失。为了能够永久地保存程序中创建的数据,需要将它们存储到磁盘或其他永久存储设备的文件中。这样,这些文件其后可以被其他程序传送和读取。

使用 File 类获取文件/目录的属性以及删除和重命名文件/目录、创建目录,以及从/向一个文本文件读/写数据 。

在文件系统中,每个文件都存放在一个目录下。绝对文件名 (absolute file name) 是由文件名和它的完整路径以及驱动器字母组成。绝对文件名是依赖机器的。相对文件名是相对于当前工作目录的。对于相对文件名而言,完整目录被忽略。

File 类意图提供了一种抽象,这种抽象是指以不依赖机器的方式来处理很多依赖于机器的文件和路径名的复杂性。File 类包含许多获取文件属性的方法,以及重命名和删除文件和目录的方法。但是,File 类不包含读写文件内容的方法。

File 类可以用来获取文件和目录的属性,删除和重命名文件和目录,以及创建目录。

文件名是一个字符串。File 类是文件名及其目录路径的一个包装类。

警告: 在 Windows 中目录的分隔符是反斜杠 (\) 。 但是在 Java 中,反斜杠是一个特殊的字符,应该写成 (\\) 的形式。

注意: 构建一个 File 实例并不会在机器上创建一个文件。不管文件是否存在,都可以创建任意文件名的 File 实例。

在程序中,不要直接使用绝对文件名。因为这样可能能在 Windows 上工作,但是不能在其他平台上工作。应该使用与当前目录相关的文件名。

斜杠 (/) 是 Java 的目录分隔符,这点和 UNIX 是一样的。使用斜杠在 Windows、UNIX 或任何其他系统上都能工作。

文件输入和输出

使用 Scanner 类从文件中读取文本数据,使用 PrintWriter 类向文本文件写入数据。

File 对象封装了文件或路径的属性,但是它既不包括创建文件的方法,也不包括从/向文件读/写数据 (称为数据输入输出,简称 I/O) 的方法。为了完成 I/O 操作,需要使用恰当的 Java I/O 类创建对象。这些对象包含从/向文件读/写数据的方法。文本文件本质上是存储在磁盘上的字符。使用 Scanner 和 PMntWriter 类从/向文本文件读/写字符串和数值信息 。

使用 PrintWriter 写数据
java.io.PrintWriter 类可用来创建一个文件并向文本文件写人数据。首先,必须为一个文本文件创建一个 PrintWriter 对象,如下所示:

PrintWriter output = new PrintWriter(filename);

调用 PrintWriter 的构造方法可能会抛出某种 I/O 异常。Java 强制要求编写代码来处理这类异常。

System.out 是控制台的标准 Java 对象。使用 System.out.print、System. out. println 和 System.out.printf 方法向控制台输出文本。可以创建 PrintWriter 的对象,然后使用 print、println 和 printf 向文件中写人文本。必须使用 closeO 方法关闭文件。如果没有调用该方法,数据就不能正确地保存在文件中。

使用 try-with-resources 自动关闭资源
JDK 7 提供了下面的新的 try-with-resources 语法来自动关闭文件。

try(声明和创建资源)
{
	使用资源来处理文件;
}

关键字 try 后声明和创建了一个资源。注意,资源放在括号中。资源必须是 AutoCloseable 的子类型,比如 PrinterWite ,具有一个 close() 方法。资源的声明和创建必须在同一行语句中,可以在括号中进行多个资源的声明和创建。紧接着资源声明的块中的语句使用资源。块结束后,资源的 close() 方法自动调用以关闭资源。使用 try-with-resourse 不仅可以避免错误,而且可以简化代码。

使用 Scanner 读数据
java.util.Scanner 类用来从控制台读取字符串和基本类型数值。Scanner 可以将输人分为由空白字符分隔的标记。为了能从键盘读取,需要为 System.in 创建一个 Scanner ,如下所示:

Scanner input = new Scanner(System.in);

为了从文件中读取,为文件创建一个 Scanner ,如下所示:

Scanner input = new Scanner(new File(filename));

注意,new Scanner(String) 为给定的字符串创建一个 Scanner 。为创建 Scanner 从文件中读取数据,必须使用构造方法 new File(filename) 利用 java.io.File 类创建 File 的一个实例,然后使用 new Scanner(File) 为文件创建一个 Scanner 。调用构造方法 new Scanner(File) 可能会抛出一个 I/O 异常。因此,方法要声明 throws Exception 。使用 try-with-resources 语法就没有必要关闭输人文件,这样做是一种释放被文件占用的资源的好方法 。

方法 nextByte() 、nextShort() 、nextlnt() 、nextLong() 、nextFloat() 、nextDouble() 和 next() 等都称为标记读取方法 (token-reading method) ,因为它们会读取用分隔符分隔开的标记 。 默认情况下,分隔符是空格。可以使用 useDelimiter(String regex) 方法设置新的分隔符模式 。

一个标记读取方法首先跳过任意分隔符 (默认情况下是空格) ,然后读取一个以分隔符结束的标记 。然后,对应于 nextByte() 、nextShort() 、nextInt() 、nextLong() 、nextFloat() 和 nextDouble() ,这个标记就分别被自动地转换为 1 byte 、short 、int 、long 、float 或 double 型的值 。 对于 next() 方法而言是无须做转换的 。如果标记和期望的类型不匹配 , 就会抛出一个运行异常 java.util.InputMismatchException。

方法 next() 和 nextLine() 都会读取一个字符串 。next() 方法读取一个由分隔符分隔的字符串,但是 nextLine() 读取一个以换行符结束的行。

注意: 行分隔符字符串是由系统定义的,在 Windows 平台上 \r\n ,而在 UNIX 平台上
是 \n 。为了得到特定平台上的行分隔符,使用

String lineSeparator = System.getProperty("line.separator");

如果从键盘输入,每行就以囘车键 (Enter key) 结束,它对应于 \n 字符。

标记读取方法不能读取标记后面的分隔符。如果在标记读取方法之后调用 nextLine() ,该方法读取从这个分隔符开始,到这行的行分隔符结束的字符。这个行分隔符也被读取,但是它不是 nextLine() 返回的字符串部分。

从 Web 上读取数据

如同从电脑中的文件中读取教据一样,也可以从 Web 上的文件中读取数据。

除开从电脑中的本地文件或者文件服务器中读取数据,如果知道 Web 上文件的 URL (Uniform Resource Locator) ,统一资源定位器,即为 Web 上的文件提供唯一的地址) ,也可以 Web
从 Web 上访问数据。当在一个 Web 浏览器中输入 URL 之后,Web 服务器将数据传送给浏览
器,浏览器则将数据渲染成图形。

为了读取一个文件,首先要使用:java.net.URL 类的这个构造方法,为该文件创建一个 URL 对象。

public URL(String spec) throws MalformedURLException

如果 URL 字符串出现语法错误的话,将会有一个 MalformedURLException 被抛出。

注意,要让 URL 类来识别一个有效的 URL ,前缀 http:// 是必需的。

创建一个 URL 对象后,可以使用 URL 类中定义的 openStream() 方法来打开输入流和用输人流创建如下 Scanner 对象。

Scanner input = new Scanner(url.openStream());

可以从输人流中读取数据了,如同从本地文件中读取一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值