问题和练习:泛型
原文:docs.oracle.com/javase/tutorial/java/generics/QandE/generics-questions.html
- 编写一个通用方法来计算集合中具有特定属性的元素数量(例如,奇数、质数、回文数)。
- 以下类会编译吗?如果不会,为什么?
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}
- 编写一个通用方法来交换数组中两个不同元素的位置。
- 如果编译器在编译时擦除所有类型参数,为什么应该使用泛型?
- 在类型擦除后,以下类被转换为什么?
public class Pair<K, V> {
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
private K key;
private V value;
}
- 以下方法在类型擦除后会转换为什么?
public static <T extends Comparable<T>>
int findFirstGreaterThan(T[] at, T elem) {
// ...
}
- 以下方法会编译吗?如果不会,为什么?
public static void print(List<? extends Number> list) {
for (Number n : list)
System.out.print(n + " ");
System.out.println();
}
- 编写一个通用方法来查找列表范围
begin, end)
中的最大元素。 - 以下类会编译吗?如果不会,为什么?
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
- 给定以下类:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
class Node<T> { /* ... */ }
以下代码会编译吗?如果不会,为什么?
Node<Circle> nc = new Node<>();
Node<Shape> ns = nc;
- 考虑这个类:
class Node<T> implements Comparable<T> {
public int compareTo(T obj) { /* ... */ }
// ...
}
以下代码会编译吗?如果不会,为什么?
Node<String> node = new Node<>();
Comparable<String> comp = node;
- 如何调用以下方法来找到列表中与指定整数列表互质的第一个整数?
public static <T>
int findFirst(List<T> list, int begin, int end, UnaryPredicate<T> p)
注意,两个整数a和b互质,如果 gcd(a, b) = 1,其中 gcd 是最大公约数的缩写。
[检查你的答案。
课程:包(Packages)
这节课讲解了如何将类和接口打包成包,如何使用在包中的类,以及如何安排文件系统以便编译器能够找到你的源文件。
创建和使用包
原文:docs.oracle.com/javase/tutorial/java/package/packages.html
为了使类型更容易找到和使用,避免命名冲突,并控制访问权限,程序员将相关类型的组合打包成包。
定义: 包 是提供访问保护和命名空间管理的相关类型的分组。请注意,类型 指的是类、接口、枚举和注解类型。枚举和注解类型是类和接口的特殊种类,因此在本课程中,类型 经常简称为类和接口。
Java 平台中的类型是通过功能将类打包在一起的各种包的成员:基本类在java.lang
中,用于读写(输入和输出)的类在java.io
中,依此类推。你也可以将你的类型放在包中。
假设你编写了一组表示图形对象的类,比如圆、矩形、线条和点。你还编写了一个接口,Draggable
,如果类可以被鼠标拖动,则实现该接口。
//*in the Draggable.java file*
public interface Draggable {
...
}
//*in the Graphic.java file*
public abstract class Graphic {
...
}
//*in the Circle.java file*
public class Circle extends Graphic
implements Draggable {
. . .
}
//*in the Rectangle.java file*
public class Rectangle extends Graphic
implements Draggable {
. . .
}
//*in the Point.java file*
public class Point extends Graphic
implements Draggable {
. . .
}
//*in the Line.java file*
public class Line extends Graphic
implements Draggable {
. . .
}
你应该将这些类和接口打包到一个包中,原因包括以下几点:
- 你和其他程序员可以轻松确定这些类型是相关的。
- 你和其他程序员知道在哪里找到可以提供与图形相关功能的类型。
- 你的类型名称不会与其他包中的类型名称冲突,因为包会创建一个新的命名空间。
- 你可以允许包内的类型彼此之间具有无限制的访问,但仍然限制包外类型的访问。
创建一个包
原文:docs.oracle.com/javase/tutorial/java/package/createpkgs.html
要创建一个包,你需要为包选择一个名称(命名约定将在下一节讨论),并在包含你想要放入包中的类型(类、接口、枚举和注解类型)的每个源文件的顶部放置一个带有该名称的package
语句。
包语句(例如,package graphics;
)必须是源文件中的第一行。每个源文件中只能有一个包语句,并且它适用于文件中的所有类型。
**注意:**如果你在单个源文件中放入多个类型,只能有一个是public
的,并且它必须与源文件同名。例如,你可以在文件Circle.java
中定义public class Circle
,在文件Draggable.java
中定义public interface Draggable
,在文件Day.java
中定义public enum Day
,等等。
你可以在同一个文件中包含非公共类型和一个公共类型(这是强烈不推荐的,除非非公共类型很小并且与公共类型密切相关),但只有公共类型可以从包外访问。所有顶级的非公共类型将是包私有的。
如果你将前面部分列出的图形接口和类放在一个名为graphics
的包中,你将需要六个源文件,如下所示:
//*in the Draggable.java file*
package graphics;
public interface Draggable {
. . .
}
//*in the Graphic.java file*
package graphics;
public abstract class Graphic {
. . .
}
//*in the Circle.java file*
package graphics;
public class Circle extends Graphic
implements Draggable {
. . .
}
//*in the Rectangle.java file*
package graphics;
public class Rectangle extends Graphic
implements Draggable {
. . .
}
//*in the Point.java file*
package graphics;
public class Point extends Graphic
implements Draggable {
. . .
}
//*in the Line.java file*
package graphics;
public class Line extends Graphic
implements Draggable {
. . .
}
如果你不使用package
语句,你的类型将会进入一个无名包。一般来说,无名包只适用于小型或临时应用程序,或者当你刚开始开发过程时。否则,类和接口应该放在命名包中。
包命名
原文:docs.oracle.com/javase/tutorial/java/package/namingpkgs.html
全球范围内的程序员使用 Java 编程语言编写类和接口,很可能许多程序员会为不同类型使用相同的名称。事实上,前面的例子就是这样做的:它定义了一个 Rectangle
类,而 java.awt
包中已经有一个 Rectangle
类。尽管如此,如果它们位于不同的包中,编译器仍允许两个类具有相同的名称。每个 Rectangle
类的完全限定名称包括包名。也就是说,graphics
包中的 Rectangle
类的完全限定名称是 graphics.Rectangle
,而 java.awt
包中的 Rectangle
类的完全限定名称是 java.awt.Rectangle
。
这种方法很有效,除非两个独立的程序员使用相同的包名。如何避免这个问题?约定。
命名约定
包名全部小写以避免与类或接口的名称冲突。
公司使用其反转的互联网域名作为其包名的起始部分—例如,com.example.mypackage
表示由 example.com
的程序员创建的名为 mypackage
的包。
公司内部发生的名称冲突需要在公司内部通过约定处理,也许可以在公司名称后面加上地区或项目名称(例如,com.example.region.mypackage
)。
Java 语言中的包以 java.
或 javax.
开头。
在某些情况下,互联网域名可能不是有效的包名。如果域名包含连字符或其他特殊字符,如果包名以数字或其他 Java 名称不允许用作 Java 名称开头的字符开头,或者包名包含保留的 Java 关键字,例如 “int”。在这种情况下,建议的约定是添加下划线。例如:
合法化包名
域名 | 包名前缀 |
|
|
|
|
|
|
使用包成员
原文:docs.oracle.com/javase/tutorial/java/package/usepkgs.html
组成包的类型被称为包成员。
要从其包外部使用public
包成员,你必须执行以下操作之一:
- 通过其完全限定名称引用成员
- 导入包成员
- 导入成员的整个包
每种情况都适用于不同的情况,如下面的部分所解释的。
通过其限定名称引用包成员
到目前为止,在本教程中的大多数示例都通过其简单名称引用类型,如Rectangle
和StackOfInts
。如果你编写的代码与该成员在同一个包中,或者已经导入了该成员,你可以使用包成员的简单名称。
但是,如果你尝试使用来自不同包的成员,并且该包尚未被导入,你必须使用成员的完全限定名称,其中包括包名称。以下是在前面示例中声明的graphics
包中的Rectangle
类的完全限定名称。
graphics.Rectangle
你可以使用这个限定名称来创建graphics.Rectangle
的实例:
graphics.Rectangle myRect = new graphics.Rectangle();
对于不经常使用的限定名称是可以的。然而,当一个名称被重复使用时,反复输入名称变得乏味,代码变得难以阅读。作为替代方案,你可以导入成员或其包,然后使用其简单名称。
导入包成员
要将特定成员导入当前文件,请在文件开头放置一个import
语句,在任何类型定义之前,但在package
语句之后(如果有的话)。以下是如何从前一节中创建的graphics
包中导入Rectangle
类。
import graphics.Rectangle;
现在你可以通过其简单名称引用Rectangle
类。
Rectangle myRectangle = new Rectangle();
如果你只从graphics
包中使用少量成员,这种方法很有效。但如果你从一个包中使用许多类型,你应该导入整个包。
导入整个包
要导入特定包中包含的所有类型,请使用带有星号(*)
通配符的import
语句。
import graphics.*;
现在你可以通过其简单名称引用graphics
包中的任何类或接口。
Circle myCircle = new Circle();
Rectangle myRectangle = new Rectangle();
import
语句中的星号只能用于指定包中的所有类,如下所示。它不能用于匹配包中的一部分类。例如,以下内容不匹配以A
开头的graphics
包中的所有类。
// *does not work*
import graphics.A*;
相反,它会生成编译器错误。通常情况下,使用import
语句只导入单个包成员或整个包。
注意: 另一种不太常见的 import
形式允许你导入封闭类的公共嵌套类。例如,如果 graphics.Rectangle
类包含有用的嵌套类,比如 Rectangle.DoubleWide
和 Rectangle.Square
,你可以通过以下两个语句导入 Rectangle
及其嵌套类。
import graphics.Rectangle;
import graphics.Rectangle.*;
请注意,第二个导入语句不会导入 Rectangle
。
另一种不太常见的 import
形式,即静态导入语句,将在本节末尾讨论。
为了方便起见,Java 编译器会自动为每个源文件导入两个完整的包:(1)java.lang
包和(2)当前包(当前文件的包)。
包的表面层次结构
起初,包看起来是分层的,但实际上并非如此。例如,Java API 包括一个 java.awt
包,一个 java.awt.color
包,一个 java.awt.font
包,以及许多以 java.awt
开头的其他包。然而,java.awt.color
包、java.awt.font
包和其他 java.awt.xxxx
包不包含在 java.awt
包中。前缀 java.awt
(Java 抽象窗口工具包)用于一些相关的包,以明确显示它们之间的关系,而不是表示包含关系。
导入 java.awt.*
导入了 java.awt
包中的所有类型,但不会导入 java.awt.color
、java.awt.font
或任何其他 java.awt.xxxx
包。如果你计划使用 java.awt.color
中的类以及 java.awt
中的类,你必须导入这两个包及其所有文件:
import java.awt.*;
import java.awt.color.*;
名称歧义
如果一个包中的成员与另一个包中的成员同名,并且两个包都被导入,你必须通过其限定名称引用每个成员。例如,graphics
包定义了一个名为 Rectangle
的类。java.awt
包也包含一个 Rectangle
类。如果 graphics
和 java.awt
都被导入,以下内容是模棱两可的。
Rectangle rect;
在这种情况下,你必须使用成员的完全限定名称来指示你想要的确切 Rectangle
类。例如,
graphics.Rectangle rect;
静态导入语句
有些情况下,你需要频繁访问一个或两个类的静态 final 字段(常量)和静态方法。反复添加这些类的名称可能会导致代码混乱。静态导入语句为你提供了一种导入你想要使用的常量和静态方法的方式,这样你就不需要为它们的类名添加前缀。
java.lang.Math
类定义了 PI
常量和许多静态方法,包括用于计算正弦、余弦、正切、平方根、最大值、最小值、指数等的方法。例如,
public static final double PI
= 3.141592653589793;
public static double cos(double a)
{
...
}
通常,要从另一个类中使用这些对象,你需要添加类名前缀,如下所示。
double r = Math.cos(Math.PI * theta);
使用静态导入语句可以导入 java.lang.Math 的静态成员,这样就不需要在类名Math
前加前缀了。Math
的静态成员可以单独导入:
import static java.lang.Math.PI;
或者作为一个组:
import static java.lang.Math.*;
一旦它们被导入,静态成员可以无需限定地使用。例如,前面的代码片段将变为:
double r = cos(PI * theta);
显然,你可以编写自己的类,其中包含你经常使用的常量和静态方法,然后使用静态导入语句。例如,
import static mypackage.MyConstants.*;
注意: 静态导入要非常谨慎使用。过度使用静态导入会导致代码难以阅读和维护,因为代码读者无法知道哪个类定义了特定的静态对象。正确使用静态导入可以通过消除类名重复使代码更易读。
管理源文件和类文件
原文:docs.oracle.com/javase/tutorial/java/package/managingfiles.html
Java 平台的许多实现依赖于分层文件系统来管理源文件和类文件,尽管Java 语言规范并不要求这样做。策略如下。
将一个类、接口、枚举或注解类型的源代码放在一个文本文件中,文件名为类型的简单名称,扩展名为.java
。例如:
//in the Rectangle.java file
package graphics;
public class Rectangle {
...
}
然后,将源文件放在一个反映类型所属包名的目录中:
.....\graphics\Rectangle.java
包成员的限定名称和文件的路径名称是平行的,假设使用 Microsoft Windows 文件名分隔符反斜杠(对于 UNIX,请使用正斜杠)。
- 类名 –
graphics.Rectangle
- 文件路径 –
graphics\Rectangle.java
正如你应该记得的那样,按照惯例,公司使用其反转的互联网域名作为其包名。例如,其互联网域名为example.com
的 Example 公司将在其所有包名之前加上com.example
。包名的每个组件对应一个子目录。因此,如果 Example 公司有一个包含Rectangle.java
源文件的com.example.graphics
包,它将包含在一系列子目录中,如下所示:
....\com\example\graphics\Rectangle.java
当你编译一个源文件时,编译器为其中定义的每个类型创建一个不同的输出文件。输出文件的基本名称是类型的名称,其扩展名是.class
。例如,如果源文件如下所示
//in the Rectangle.java file
package com.example.graphics;
public class Rectangle {
. . .
}
class Helper{
. . .
}
然后编译后的文件将位于:
<path to the parent directory of the output files>\com\example\graphics\Rectangle.class
<path to the parent directory of the output files>\com\example\graphics\Helper.class
与.java
源文件一样,编译后的.class
文件应该在反映包名的一系列目录中。然而,.class
文件的路径不一定要与.java
源文件的路径相同。你可以将源文件和类文件目录分开管理,如:
<path_one>\sources\com\example\graphics\Rectangle.java
<path_two>\classes\com\example\graphics\Rectangle.class
通过这样做,你可以将classes
目录提供给其他程序员,而不会泄露你的源代码。你还需要以这种方式管理源文件和类文件,以便编译器和 Java 虚拟机(JVM)可以找到程序中使用的所有类型。
classes
目录的完整路径,<path_two>\classes
,被称为类路径,并通过CLASSPATH
系统变量设置。编译器和 JVM 都会通过将包名添加到类路径来构建到你的.class
文件的路径。例如,如果
<path_two>\classes
是你的类路径,包名是
com.example.graphics,
然后编译器和 JVM 会在以下位置查找.class
文件
<path_two>\classes\com\example\graphics.
类路径可以包括多个路径,用分号(Windows)或冒号(UNIX)分隔。默认情况下,编译器和 JVM 会搜索当前目录和包含 Java 平台类的 JAR 文件,因此这些目录会自动包含在你的类路径中。
设置 CLASSPATH 系统变量
要在 Windows 和 UNIX(Bourne shell)中显示当前的CLASSPATH
变量,请使用以下命令:
In Windows: C:\> set CLASSPATH
In UNIX: % echo $CLASSPATH
要删除当前CLASSPATH
变量的内容,请使用以下命令:
In Windows: C:\> set CLASSPATH=
In UNIX: % unset CLASSPATH; export CLASSPATH
要设置CLASSPATH
变量,请使用以下命令(例如):
In Windows: C:\> set CLASSPATH=C:\users\george\java\classes
In UNIX: % CLASSPATH=/home/george/java/classes; export CLASSPATH
创建和使用包的总结
原文:docs.oracle.com/javase/tutorial/java/package/summary-package.html
要为一个类型创建一个包,将一个package
语句放在包含该类型(类、接口、枚举或注解类型)的源文件中的第一个语句位置。
要使用不同包中的公共类型,你有三种选择:(1)使用类型的完全限定名称,(2)导入类型,或者(3)导入包含该类型的整个包。
包的源文件和类文件的路径名称与包的名称相对应。
你可能需要设置你的CLASSPATH
,以便编译器和 JVM 可以找到你的类型的.class
文件。
问题和练习:创建和使用包
原文:docs.oracle.com/javase/tutorial/java/package/QandE/packages-questions.html
问题
假设你已经编写了一些类。突然间,你决定将它们分成三个包,如下表所列。此外,假设这些类当前位于默认包中(没有package
语句)。
目标包
包名称 | 类名称 |
|
|
|
|
|
|
- 你需要在每个源文件中添加哪行代码才能将每个类放在正确的包中?
- 为了遵循目录结构,你需要在开发目录中创建一些子目录,并将源文件放在正确的子目录中。你需要创建哪些子目录?每个源文件应放在哪个子目录中?
- 你认为你需要对源文件进行其他任何更改才能使它们正确编译吗?如果需要,是什么?
练习
下载列在这里的源文件。
Client
Server
Utilities
- 使用刚刚下载的源文件实现你在问题 1 到 3 中提出的更改。
- 编译修改后的源文件。(提示:如果你是从命令行调用编译器(而不是使用构建工具),请从包含你刚刚创建的
mygame
目录的目录中调用编译器。)
检查你的答案。
Trail: Java 基础类
本教程讨论了对大多数程序员至关重要的 Java 平台类。
解释了异常机制以及如何用它来处理错误和其他异常情况。本课程描述了异常是什么,如何抛出和捕获异常,一旦捕获异常后该如何处理,以及如何使用异常类层次结构。
包括用于基本输入和输出的 Java 平台类。它主要关注I/O 流,这是一个极大简化 I/O 操作的强大概念。本课程还介绍了序列化,它允许程序将整个对象写入流并再次读取它们。然后,课程介绍了一些文件系统操作,包括随机访问文件。最后,简要介绍了新 I/O API 的高级功能。
解释了如何编写能够同时执行多个任务的应用程序。Java 平台从头开始设计,以支持并发编程,在 Java 编程语言和 Java 类库中提供基本的并发支持。自 5.0 版本以来,Java 平台还包括高级并发 API。本课程介绍了平台的基本并发支持,并总结了java.util.concurrent
包中一些高级 API。
是由底层操作系统、Java 虚拟机、类库和应用程序启动时提供的各种配置数据定义的。本课程描述了应用程序用于检查和配置其平台环境的一些 API。
是一种根据集合中每个字符串共享的共同特征描述字符串集合的方法。它们可用于搜索、编辑或操作文本和数据。正则表达式的复杂程度各不相同,但一旦理解了它们的构造基础,您就能解读(或创建)任何正则表达式。本课程教授了java.util.regex
API 支持的正则表达式语法,并提供了几个工作示例以说明各种对象之间的交互方式。
课程:异常
原文:docs.oracle.com/javase/tutorial/essential/exceptions/index.html
Java 编程语言使用异常来处理错误和其他异常事件。本课程描述了何时以及如何使用异常。
什么是异常?
异常是程序执行过程中发生的事件,打断了指令的正常流程。
捕获或声明要求
这一部分涵盖了如何捕获和处理异常。讨论包括try
、catch
和finally
块,以及链式异常和日志记录。
如何抛出异常
这一部分涵盖了throw
语句和Throwable
类及其子类。
try-with-resources 语句
这一部分描述了try-with-resources
语句,它是一个声明一个或多个资源的try
语句。资源是程序完成后必须关闭的对象。try-with-resources
语句确保在语句结束时关闭每个资源。
未经检查的异常 - 争议
这一部分解释了由RuntimeException
子类指示的未经检查异常的正确和不正确使用。
异常的优势
使用异常来管理错误相对于传统的错误管理技术有一些优势。您将在本节中了解更多信息。
总结
问题和练习
什么是异常?
原文:docs.oracle.com/javase/tutorial/essential/exceptions/definition.html
术语异常是“异常事件”的简称。
定义: 异常是程序执行过程中发生的事件,打断了程序正常指令流程。
当方法内部发生错误时,方法会创建一个对象并将其交给运行时系统。这个对象称为异常对象,包含有关错误的信息,包括错误发生时的类型和程序状态。创建异常对象并将其交给运行时系统称为抛出异常。
方法抛出异常后,运行时系统会尝试寻找处理异常的方法。可以处理异常的一系列可能的“方法”是调用到发生错误的方法的有序方法列表。这些方法的列表称为调用堆栈(见下图)。
调用堆栈。
运行时系统在调用堆栈中搜索包含可以处理异常的代码块的方法。这个代码块称为异常处理程序。搜索从发生错误的方法开始,并按照调用方法的相反顺序在调用堆栈中进行。当找到适当的处理程序时,运行时系统将异常传递给处理程序。如果抛出的异常对象的类型与处理程序可以处理的类型匹配,则认为异常处理程序是适当的。
选择的异常处理程序被称为捕获异常。如果运行时系统在调用堆栈上详尽搜索而找不到适当的异常处理程序,如下图所示,运行时系统(以及因此程序)将终止。
在调用堆栈中搜索异常处理程序。
使用异常来管理错误相比传统的错误管理技术有一些优势。您可以在异常的优势部分了解更多信息。
捕获或指定要求
原文:docs.oracle.com/javase/tutorial/essential/exceptions/catchOrDeclare.html
有效的 Java 编程语言代码必须遵守捕获或指定要求。这意味着可能引发某些异常的代码必须被以下之一包围:
- 一个捕获异常的
try
语句。try
必须提供异常处理程序,如 Catching and Handling Exceptions 中所述。 - 指定可能引发异常的方法。该方法必须提供列出异常的
throws
子句,如 Specifying the Exceptions Thrown by a Method 中所述。
未遵守捕获或指定要求的代码将无法编译。
并非所有异常都受捕获或指定要求的约束。要理解原因,我们需要看一下三种基本类别的异常,其中只有一种受到该要求的约束。
三种异常类型
第一种异常是受检异常。这些是一个良好编写的应用程序应该预料并从中恢复的异常情况。例如,假设一个应用程序提示用户输入文件名,然后通过将名称传递给java.io.FileReader
的构造函数来打开文件。通常,用户提供现有的可读文件的名称,因此FileReader
对象的构造成功,应用程序的执行正常进行。但有时用户提供不存在文件的名称,构造函数会抛出java.io.FileNotFoundException
。一个良好编写的程序将捕获此异常并通知用户错误,可能提示更正的文件名。
受检异常受到捕获或指定要求的约束。所有异常都是受检异常,除了由Error
、RuntimeException
及其子类指示的异常。
第二种异常是错误。这些是应用程序外部的异常情况,应用程序通常无法预料或从中恢复。例如,假设一个应用程序成功打开一个输入文件,但由于硬件或系统故障无法读取文件。读取失败将抛出java.io.IOError
。应用程序可能选择捕获此异常,以通知用户问题,但程序打印堆栈跟踪并退出也是有道理的。
错误不受捕获或指定要求的约束。错误是由Error
及其子类指示的异常。
第三种异常是运行时异常。这些是应用程序内部的异常情况,应用程序通常无法预料或恢复。这通常表示编程错误,如逻辑错误或不正确使用 API。例如,考虑先前描述的应用程序将文件名传递给FileReader
构造函数。如果逻辑错误导致将null
传递给构造函数,则构造函数将抛出NullPointerException
。应用程序可以捕获此异常,但更有意义的是消除导致异常发生的错误。
运行时异常不受捕获或指定要求的约束。运行时异常是由RuntimeException
及其子类指示的异常。
错误和运行时异常统称为未经检查的异常。
绕过捕获或指定
一些程序员认为捕获或指定要求是异常机制中的一个严重缺陷,并通过在需要检查的异常位置使用未经检查的异常来绕过它。一般来说,这是不推荐的。章节未经检查的异常 - 争议讨论了何时适合使用未经检查的异常。
捕获和处理异常
原文:docs.oracle.com/javase/tutorial/essential/exceptions/handling.html
本节描述了如何使用三个异常处理组件——try
、catch
和finally
块——编写异常处理程序。然后,解释了在 Java SE 7 中引入的try-
with-resources 语句。try-
with-resources 语句特别适用于使用Closeable
资源的情况,比如流。
本节的最后部分通过一个示例演示并分析了各种情况下的发生情况。
以下示例定义并实现了一个名为ListOfNumbers
的类。在构造时,ListOfNumbers
创建一个包含 10 个顺序值为 0 到 9 的Integer
元素的ArrayList
。ListOfNumbers
类还定义了一个名为writeList
的方法,该方法将数字列表写入名为OutFile.txt
的文本文件。此示例使用了java.io
中定义的输出类,这些类在基本 I/O 中有介绍。
// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;
public class ListOfNumbers {
private List<Integer> list;
private static final int SIZE = 10;
public ListOfNumbers () {
list = new ArrayList<Integer>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(new Integer(i));
}
}
public void writeList() {
// The FileWriter constructor throws IOException, which must be caught.
PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
// The get(int) method throws IndexOutOfBoundsException, which must be caught.
out.println("Value at: " + i + " = " + list.get(i));
}
out.close();
}
}
粗体的第一行是对构造函数的调用。构造函数在文件上初始化一个输出流。如果文件无法打开,构造函数会抛出一个IOException
异常。第二行粗体是对ArrayList
类的get
方法的调用,如果其参数的值太小(小于 0)或太大(大于ArrayList
当前包含的元素数量),则会抛出IndexOutOfBoundsException
异常。
如果尝试编译ListOfNumbers
类,编译器会打印有关FileWriter
构造函数抛出的异常的错误消息。但是,它不会显示有关get
方法抛出的异常的错误消息。原因是构造函数抛出的异常IOException
是一个已检查异常,而get
方法抛出的异常IndexOutOfBoundsException
是一个未检查异常。
现在您熟悉了ListOfNumbers
类以及其中可能抛出异常的位置,您可以编写异常处理程序来捕获和处理这些异常了。
try 块
原文:docs.oracle.com/javase/tutorial/essential/exceptions/try.html
构建异常处理程序的第一步是将可能会抛出异常的代码放在一个try
块中。一般来说,一个try
块看起来像下面这样:
try {
*code*
}
*catch and finally blocks . . .*
在示例中标记为*code*
的部分包含一个或多个可能会抛出异常的合法代码行。(catch
和finally
块将在接下来的两个小节中解释。)
要为ListOfNumbers
类中的writeList
方法构建一个异常处理程序,将writeList
方法中可能会抛出异常的语句放在一个try
块中。有多种方法可以做到这一点。你可以将每一行可能会抛出异常的代码放在自己的try
块中,并为每个提供单独的异常处理程序。或者,你可以将所有的writeList
代码放在一个单独的try
块中,并为其关联多个处理程序。以下清单使用一个try
块来处理整个方法,因为相关代码非常简短。
private List<Integer> list;
private static final int SIZE = 10;
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entered try statement");
FileWriter f = new FileWriter("OutFile.txt");
out = new PrintWriter(f);
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
catch and finally blocks . . .
}
如果在try
块中发生异常,该异常将由与之关联的异常处理程序处理。要为try
块关联一个异常处理程序,必须在其后放置一个catch
块;下一节,catch 块,将向你展示如何做到这一点。
异常处理块
原文:docs.oracle.com/javase/tutorial/essential/exceptions/catch.html
通过在try
块后直接提供一个或多个catch
块,将异常处理程序与try
块关联起来。在try
块结束和第一个catch
块开始之间不能有任何代码。
try {
} catch (*ExceptionType name*) {
} catch (*ExceptionType name*) {
}
每个catch
块都是一个异常处理程序,处理其参数指示的异常类型。参数类型*ExceptionType*
声明了处理程序可以处理的异常类型,必须是从Throwable
类继承的类名。处理程序可以使用*name*
引用异常。
catch
块包含在异常处理程序被调用时执行的代码。当处理程序是调用堆栈中第一个*ExceptionType*
与抛出异常类型匹配的处理程序时,运行时系统会调用异常处理程序。如果抛出的对象可以合法地分配给异常处理程序的参数,则系统认为它匹配。
以下是writeList
方法的两个异常处理程序:
try {
} catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
}
异常处理程序不仅可以打印错误消息或停止程序。它们可以进行错误恢复,提示用户做出决定,或者使用链式异常将错误传播到更高级别的处理程序,如链式异常部分所述。
使用一个异常处理程序捕获多种类型的异常
在 Java SE 7 及更高版本中,单个catch
块可以处理多种类型的异常。这个特性可以减少代码重复,并减少捕获过于宽泛异常的诱惑。
在catch
子句中,指定阻止处理的异常类型,并用竖线(|
)分隔每种异常类型:
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
注意:如果一个catch
块处理多种异常类型,则catch
参数隐式为final
。在这个例子中,catch
参数ex
是final
,因此您不能在catch
块内为其分配任何值。
finally 块
原文:docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
finally
块总是在try
块退出时执行。这确保了即使发生意外异常,finally
块也会被执行。但finally
不仅仅用于异常处理,它允许程序员避免清理代码被return
、continue
或break
意外绕过。将清理代码放在finally
块中始终是一个良好的实践,即使不预期发生异常。
**注意:**如果 JVM 在执行try
或catch
代码时退出,则finally
块可能不会执行。
你一直在这里工作的writeList
方法的try
块打开了一个PrintWriter
。程序应该在退出writeList
方法之前关闭该流。这带来了一个有点复杂的问题,因为writeList
的try
块可以以三种方式之一退出。
new FileWriter
语句失败并抛出IOException
。list.get(i)
语句失败并抛出IndexOutOfBoundsException
。- 一切顺利,
try
块正常退出。
无论try
块内发生了什么,运行时系统始终执行finally
块中的语句。因此,这是执行清理操作的完美位置。
以下writeList
方法的finally
块清理并关闭PrintWriter
和FileWriter
。
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
} else {
System.out.println("PrintWriter not open");
}
if (f != null) {
System.out.println("Closing FileWriter");
f.close();
}
}
**重要:**在关闭文件或恢复资源时,请使用try-
with-resources 语句而不是finally
块。以下示例使用try
-with-resources 语句清理和关闭writeList
方法的PrintWriter
和FileWriter
:
public void writeList() throws IOException {
try (FileWriter f = new FileWriter("OutFile.txt");
PrintWriter out = new PrintWriter(f)) {
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
}
try
-with-resources 语句在不再需要时自动释放系统资源。请参见 try-with-resources 语句。
try
-with-resources 语句
原文:docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
try
-with-resources 语句是一个声明一个或多个资源的try
语句。资源是程序在使用完后必须关闭的对象。try
-with-resources 语句确保每个资源在语句结束时关闭。任何实现java.lang.AutoCloseable
接口的对象,包括所有实现java.io.Closeable
接口的对象,都可以用作资源。
以下示例从文件中读取第一行。它使用FileReader
和BufferedReader
的实例从文件中读取数据。FileReader
和BufferedReader
是在程序使用完后必须关闭的资源:
static String readFirstLineFromFile(String path) throws IOException {
try (FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr)) {
return br.readLine();
}
}
在这个示例中,try
-with-resources 语句中声明的资源是FileReader
和BufferedReader
。这些资源的声明语句出现在try
关键字之后的括号内。在 Java SE 7 及更高版本中,FileReader
和BufferedReader
类实现了java.lang.AutoCloseable
接口。因为FileReader
和BufferedReader
实例是在try
-with-resources 语句中声明的,无论try
语句是否正常完成或突然中断(因为BufferedReader.readLine
方法抛出IOException
),它们都将被关闭。
在 Java SE 7 之前,可以使用finally
块来确保资源在try
语句正常完成或突然中断时关闭。以下示例使用finally
块而不是try
-with-resources 语句:
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr);
try {
return br.readLine();
} finally {
br.close();
fr.close();
}
}
然而,这个示例可能会有资源泄漏。程序不能仅仅依赖垃圾回收器(GC)在完成后回收资源的内存。程序还必须将资源释放回操作系统,通常通过调用资源的close
方法。但是,如果程序在 GC 回收资源之前未能执行此操作,则释放资源所需的信息将丢失。这个资源,仍然被操作系统视为正在使用,已经泄漏。
在这个示例中,如果readLine
方法抛出异常,并且finally
块中的br.close()
语句也抛出异常,那么FileReader
就会泄漏。因此,使用try
-with-resources 语句而不是finally
块来关闭程序的资源。
如果readLine
和close
方法都抛出异常,则readFirstLineFromFileWithFinallyBlock
方法会抛出从finally
块中抛出的异常;从try
块中抛出的异常会被抑制。相比之下,在示例readFirstLineFromFile
中,如果try
块和try
-with-resources 语句都抛出异常,则readFirstLineFromFile
方法会抛出从try
块中抛出的异常;从try
-with-resources 块中抛出的异常会被抑制。在 Java SE 7 及更高版本中,您可以检索被抑制的异常;有关更多信息,请参阅被抑制的异常部分。
以下示例检索打包在 zip 文件zipFileName
中的文件的名称,并创建一个包含这些文件名称的文本文件:
public static void writeToFileZipFileContents(String zipFileName,
String outputFileName)
throws java.io.IOException {
java.nio.charset.Charset charset =
java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath =
java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with
// try-with-resources statement
try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements();) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName =
((java.util.zip.ZipEntry)entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
在此示例中,try
-with-resources 语句包含两个声明,它们之间用分号分隔:ZipFile
和BufferedWriter
。当直接跟在其后的代码块正常终止或因异常终止时,BufferedWriter
和ZipFile
对象的close
方法会按照这个顺序自动调用。请注意,资源的close
方法按照它们创建的相反顺序调用。
以下示例使用try
-with-resources 语句自动关闭java.sql.Statement
对象:
public static void viewTable(Connection con) throws SQLException {
String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
String coffeeName = rs.getString("COF_NAME");
int supplierID = rs.getInt("SUP_ID");
float price = rs.getFloat("PRICE");
int sales = rs.getInt("SALES");
int total = rs.getInt("TOTAL");
System.out.println(coffeeName + ", " + supplierID + ", " +
price + ", " + sales + ", " + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
此示例中使用的资源java.sql.Statement
是 JDBC 4.1 及更高版本 API 的一部分。
注意:try
-with-resources 语句可以像普通的try
语句一样具有catch
和finally
块。在try
-with-resources 语句中,任何catch
或finally
块都会在声明的资源关闭后运行。
被抑制的异常
与try
-with-resources 语句相关联的代码块中可能会抛出异常。在示例writeToFileZipFileContents
中,异常可能会从try
块中抛出,当尝试关闭ZipFile
和BufferedWriter
对象时,try
-with-resources 语句最多可能会抛出两个异常。如果从try
块中抛出异常,并且从try
-with-resources 语句中抛出一个或多个异常,则这些从try
-with-resources 语句中抛出的异常会被抑制,而由该块抛出的异常就是writeToFileZipFileContents
方法抛出的异常。您可以通过调用由try
块抛出的异常的Throwable.getSuppressed
方法来检索这些被抑制的异常。
实现 AutoCloseable 或 Closeable 接口的类
查看AutoCloseable和Closeable接口的 Javadoc,以获取实现这两个接口之一的类列表。Closeable
接口扩展了AutoCloseable
接口。Closeable
接口的close
方法会抛出IOException
类型的异常,而AutoCloseable
接口的close
方法会抛出Exception
类型的异常。因此,AutoCloseable
接口的子类可以重写close
方法的行为,以抛出特定的异常,比如IOException
,或者根本不抛出异常。
将所有内容整合在一起
原文:docs.oracle.com/javase/tutorial/essential/exceptions/putItTogether.html
前面的部分描述了如何为ListOfNumbers
类中的writeList
方法构造try
、catch
和finally
代码块。现在,让我们走一遍代码,看看可能发生什么。
当所有组件放在一起时,writeList
方法看起来像下面这样。
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering" + " try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
如前所述,此方法的try
块有三种不同的退出可能性;以下是其中两种。
try
语句中的代码失败并抛出异常。这可能是由new FileWriter
语句引起的IOException
,也可能是由for
循环中错误的索引值引起的IndexOutOfBoundsException
。- 一切顺利,
try
语句正常退出。
让我们看看在这两种退出可能性中writeList
方法中会发生什么。
情况 1:发生异常
创建FileWriter
的语句可能因多种原因而失败。例如,如果程序无法创建或写入指定文件,则FileWriter
的构造函数会抛出IOException
。
当FileWriter
抛出IOException
时,运行时系统立即停止执行try
块;正在执行的方法调用不会完成。然后,运行时系统从方法调用堆栈的顶部开始搜索适当的异常处理程序。在本例中,当发生IOException
时,FileWriter
构造函数位于调用堆栈的顶部。然而,FileWriter
构造函数没有适当的异常处理程序,因此运行时系统检查下一个方法——writeList
方法——在方法调用堆栈中。writeList
方法有两个异常处理程序:一个用于IOException
,一个用于IndexOutOfBoundsException
。
运行时系统按照try
语句后出现的顺序检查writeList
的处理程序。第一个异常处理程序的参数是IndexOutOfBoundsException
。这与抛出的异常类型不匹配,因此运行时系统检查下一个异常处理程序——IOException
。这与抛出的异常类型匹配,因此运行时系统结束了对适当异常处理程序的搜索。现在,运行时已找到适当的处理程序,将执行该catch
块中的代码。
异常处理程序执行后,运行时系统将控制权传递给finally
块。finally
块中的代码无论上面捕获的异常如何,都会执行。在这种情况下,FileWriter
从未被打开,也不需要关闭。finally
块执行完毕后,程序将继续执行finally
块后的第一条语句。
下面是当抛出IOException
时出现的ListOfNumbers
程序的完整输出。
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open
以下清单中的粗体代码显示了在这种情况下执行的语句:
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++)
out.println("Value at: " + i + " = " + list.get(i));
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
情景 2:try 块正常退出
在这种情况下,try
块范围内的所有语句都成功执行且没有抛出异常。执行流程跳出try
块,运行时系统将控制权传递给finally
块。因为一切顺利,当控制权到达finally
块时,PrintWriter
是打开状态,finally
块关闭了PrintWriter
。同样,在finally
块执行完毕后,程序将继续执行finally
块后的第一条语句。
这是ListOfNumbers
程序在没有抛出异常时的输出。
Entering try statement
Closing PrintWriter
以下示例中的粗体代码显示了在这种情况下执行的语句。
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++)
out.println("Value at: " + i + " = " + list.get(i));
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
指定方法抛出的异常
原文:docs.oracle.com/javase/tutorial/essential/exceptions/declaring.html
前一节展示了如何为ListOfNumbers
类中的writeList
方法编写异常处理程序。有时,代码捕获可能发生的异常是合适的。然而,在其他情况下,最好让调用堆栈中更高层的方法处理异常。例如,如果你将ListOfNumbers
类作为一个类包的一部分提供,你可能无法预料到包的所有用户的需求。在这种情况下,最好不要捕获异常,而是让调用堆栈中更高层的方法来处理它。
如果writeList
方法不捕获其中可能发生的已检查异常,那么writeList
方法必须指定它可以抛出这些异常。让我们修改原始的writeList
方法,以指定它可以抛出的异常,而不是捕获它们。为了提醒你,这里是原始版本的writeList
方法,它不会编译通过。
public void writeList() {
PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
out.close();
}
要指定writeList
可能会抛出两个异常,需要在writeList
方法的方法声明中添加一个throws
子句。throws
子句包括throws
关键字,后面跟着一个逗号分隔的由该方法抛出的所有异常的列表。该子句放在方法名和参数列表之后,方法范围定义的大括号之前;以下是一个示例。
public void writeList() throws IOException, IndexOutOfBoundsException {
请记住IndexOutOfBoundsException
是一个未经检查的异常;在throws
子句中包含它并不是强制性的。你可以只写如下内容。
public void writeList() throws IOException {
如何抛出异常
原文:docs.oracle.com/javase/tutorial/essential/exceptions/throwing.html
在捕获异常之前,某个地方的代码必须抛出异常。任何代码都可以抛出异常:您的代码、其他人编写的包中的代码(例如 Java 平台提供的包)或 Java 运行时环境。无论是什么引发了异常,它总是使用 throw
语句抛出。
正如您可能已经注意到的,Java 平台提供了许多异常类。所有这些类都是 Throwable 类的后代,所有这些类都允许程序在程序执行期间区分各种可能发生的异常类型。
您还可以创建自己的异常类来表示您编写的类中可能发生的问题。实际上,如果您是一个包开发者,您可能需要创建自己的一组异常类,以允许用户区分在您的包中可能发生的错误与在 Java 平台或其他包中发生的错误。
您还可以创建链接异常。有关更多信息,请参阅链接异常部分。
抛出语句
所有方法都使用 throw
语句来抛出异常。throw
语句需要一个参数:一个可抛出对象。可抛出对象是 Throwable
类的任何子类的实例。这里是一个 throw
语句的示例。
throw *someThrowableObject*;
让我们看看 throw
语句的上下文。以下 pop
方法取自实现常见堆栈对象的类。该方法从堆栈中移除顶部元素并返回该对象。
public Object pop() {
Object obj;
if (size == 0) {
throw new EmptyStackException();
}
obj = objectAt(size - 1);
setObjectAt(size - 1, null);
size--;
return obj;
}
pop
方法检查堆栈上是否有任何元素。如果堆栈为空(其大小等于 0
),pop
实例化一个新的 EmptyStackException
对象(java.util
的成员)并将其抛出。本章的创建异常类部分解释了如何创建自己的异常类。现在,您只需要记住您只能抛出继承自 java.lang.Throwable
类的对象。
注意,pop
方法的声明中不包含 throws
子句。EmptyStackException
不是一个受检异常,因此 pop
不需要声明它可能发生。
Throwable 类及其子类
继承自Throwable
类的对象包括直接后代(直接从Throwable
类继承的对象)和间接后代(从Throwable
类的子类或孙子类继承的对象)。下图说明了Throwable
类及其最重要的子类的类层次结构。正如你所看到的,Throwable
有两个直接后代:Error和Exception。
Throwable 类。
错误类
当 Java 虚拟机发生动态链接失败或其他严重故障时,虚拟机会抛出一个Error
。简单的程序通常不会捕获或抛出Error
。
异常类
大多数程序会抛出和捕获从Exception
类派生的对象。Exception
表示发生了问题,但不是严重的系统问题。你编写的大多数程序会抛出和捕获Exception
,而不是Error
。
Java 平台定义了Exception
类的许多后代。这些后代表示可能发生的各种异常类型。例如,IllegalAccessException
表示找不到特定方法,而NegativeArraySizeException
表示程序尝试创建一个负大小的数组。
一个Exception
子类,RuntimeException
,用于指示 API 的不正确使用的异常。一个运行时异常的例子是NullPointerException
,当一个方法尝试通过null
引用访问对象的成员时会发生。本节未经检查的异常 — 争议讨论了为什么大多数应用程序不应该抛出运行时异常或子类化RuntimeException
。
链式异常
原文:docs.oracle.com/javase/tutorial/essential/exceptions/chained.html
应用程序通常通过抛出另一个异常来响应异常。实际上,第一个异常导致第二个异常。知道一个异常导致另一个异常时会非常有帮助。链式异常帮助程序员做到这一点。
以下是支持链式异常的Throwable
中的方法和构造函数。
Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)
initCause
和Throwable
构造函数的Throwable
参数是导致当前异常的异常。getCause
返回导致当前异常的异常,initCause
设置当前异常的原因。
以下示例展示了如何使用链式异常。
try {
} catch (IOException e) {
throw new SampleException("Other IOException", e);
}
在这个例子中,当捕获到IOException
时,会创建一个带有原始原因附加的新的SampleException
异常,并将异常链抛到下一个更高级别的异常处理程序。
访问堆栈跟踪信息
现在假设更高级别的异常处理程序想要以自己的格式转储堆栈跟踪。
定义: 堆栈跟踪提供了关于当前线程执行历史的信息,并列出了在异常发生时调用的类和方法的名称。堆栈跟踪是一个有用的调试工具,当抛出异常时,通常会利用它。
以下代码展示了如何在异常对象上调用getStackTrace
方法。
catch (Exception cause) {
StackTraceElement elements[] = cause.getStackTrace();
for (int i = 0, n = elements.length; i < n; i++) {
System.err.println(elements[i].getFileName()
+ ":" + elements[i].getLineNumber()
+ ">> "
+ elements[i].getMethodName() + "()");
}
}
日志记录 API
下一个代码片段记录了异常发生的位置,位于catch
块内部。然而,它不是手动解析堆栈跟踪并将输出发送到System.err()
,而是使用java.util.logging包中的日志记录功能将输出发送到文件。
try {
Handler handler = new FileHandler("OutFile.log");
Logger.getLogger("").addHandler(handler);
} catch (IOException e) {
Logger logger = Logger.getLogger("*package.name*");
StackTraceElement elements[] = e.getStackTrace();
for (int i = 0, n = elements.length; i < n; i++) {
logger.log(Level.WARNING, elements[i].getMethodName());
}
}
创建异常类
原文:docs.oracle.com/javase/tutorial/essential/exceptions/creating.html
在选择要抛出的异常类型时,你可以使用其他人编写的异常 — Java 平台提供了许多可以使用的异常类 — 或者你可以编写自己的异常。如果对以下任何问题回答是肯定的,那么你应该编写自己的异常类;否则,你可能可以使用别人的。
- 是否需要一个 Java 平台中没有的异常类型?
- 如果用户能够区分你的异常和其他供应商编写的异常,这是否有助于用户?
- 你的代码是否抛出了多个相关的异常?
- 如果使用别人的异常,用户是否能够访问这些异常?一个类似的问题是,你的包是否独立且自包含?
一个示例
假设你正在编写一个链表类。该类支持以下方法,以及其他方法:
objectAt(int n)
— 返回列表中第n
个位置的对象。如果参数小于 0 或大于当前列表中对象的数量,则抛出异常。firstObject()
indexOf(Object o)
— 搜索列表中指定的Object
并返回其在列表中的位置。如果传入方法的对象不在列表中,则抛出异常。
链表类可以抛出多个异常,能够使用一个异常处理程序捕获链表抛出的所有异常将会很方便。此外,如果计划在一个包中分发你的链表,所有相关代码应该打包在一起。因此,链表应该提供自己的一组异常类。
下图展示了链表抛出的异常可能的类层次结构。
示例异常类层次结构。
选择一个超类
任何Exception
子类都可以用作LinkedListException
的父类。然而,快速浏览这些子类显示它们不合适,因为它们要么过于专业化,要么与LinkedListException
完全无关。因此,LinkedListException
的父类应该是Exception
。
你编写的大多数小程序和应用程序将抛出Exception
对象。Error
通常用于系统中的严重、严重错误,例如阻止 JVM 运行的错误。
注意: 为了编写可读性强的代码,将Exception
字符串附加到所有直接或间接继承自Exception
类的类名后是一个好习惯。
未检查异常 — 争议
原文:docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html
因为 Java 编程语言不要求方法捕获或指定未检查异常(RuntimeException
,Error
及其子类),程序员可能会倾向于编写仅抛出未检查异常的代码,或者使所有异常子类继承自RuntimeException
。这两种捷径使程序员能够编写代码,而不必理会编译器错误,也不必指定或捕获任何异常。尽管这对程序员来说可能很方便,但它绕过了catch
或specify
要求的意图,可能会给使用您的类的其他人造成问题。
设计者为什么决定强制一种方法来指定在其范围内可能抛出的所有未捕获的已检查异常?方法可能抛出的任何Exception
都是方法的公共编程接口的一部分。调用方法的人必须了解方法可能抛出的异常,以便他们可以决定如何处理这些异常。这些异常与方法的编程接口一样重要,就像它的参数和return
值一样。
下一个问题可能是:“如果记录方法的 API,包括它可能抛出的异常是如此重要,为什么不也指定运行时异常呢?”运行时异常代表的是由编程问题导致的问题,因此,API 客户端代码不能合理地预期从中恢复或以任何方式处理它们。这些问题包括算术异常,例如除以零;指针异常,例如尝试通过空引用访问对象;以及索引异常,例如尝试通过太大或太小的索引访问数组元素。
运行时异常可能在程序的任何地方发生,在典型情况下可能非常多。在每个方法声明中添加运行时异常会降低程序的清晰度。因此,编译器不要求您捕获或指定运行时异常(尽管您可以)。
一个常见的情况是抛出RuntimeException
的情况是当用户错误调用方法时。例如,一个方法可以检查其参数是否不正确为null
。如果参数为null
,方法可能会抛出NullPointerException
,这是一个未检查异常。
一般来说,不要仅仅因为不想麻烦指定方法可能抛出的异常而抛出RuntimeException
或创建RuntimeException
的子类。
这里是底线指导原则:如果客户端可以合理地预期从异常中恢复,那么将其作为已检查异常。如果客户端无法从异常中恢复,那么将其作为未检查异常。
异常的优点
原文:docs.oracle.com/javase/tutorial/essential/exceptions/advantages.html
现在您知道什么是异常以及如何使用它们,是时候学习在程序中使用异常的优点了。
优点 1:将错误处理代码与“常规”代码分开
异常提供了一种将发生异常情况时的处理细节与程序的主要逻辑分开的手段。在传统编程中,错误检测、报告和处理经常导致令人困惑的意大利面代码。例如,考虑这里的伪代码方法,它将整个文件读入内存。
readFile {
*open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;*
}
乍一看,这个函数似乎足够简单,但它忽略了所有以下潜在的错误。
- 如果文件无法打开会发生什么?
- 如果无法确定文件的长度会发生什么?
- 如果无法分配足够的内存会发生什么?
- 如果读取失败会发生什么?
- 如果文件无法关闭会发生什么?
要处理这种情况,readFile
函数必须有更多的代码来进行错误检测、报告和处理。以下是该函数可能的示例。
errorCodeType readFile {
initialize errorCode = 0;
*open the file;*
if (*theFileIsOpen*) {
*determine the length of the file;*
if (*gotTheFileLength*) {
*allocate that much memory;*
if (*gotEnoughMemory*) {
*read the file into memory;*
if (*readFailed*) {
errorCode = -1;
}
} else {
errorCode = -2;
}
} else {
errorCode = -3;
}
*close the file;*
if (*theFileDidntClose* && *errorCode* == 0) {
errorCode = -4;
} else {
errorCode = errorCode and -4;
}
} else {
errorCode = -5;
}
return errorCode;
}
这里有太多的错误检测、报告和返回,原始的七行代码在混乱中丢失了。更糟糕的是,代码的逻辑流也丢失了,因此很难判断代码是否在做正确的事情:如果函数无法分配足够的内存,文件是否真的被关闭了?当您在编写三个月后修改方法时,确保代码继续执行正确的事情更加困难。许多程序员通过简单地忽略它来解决这个问题——当他们的程序崩溃时会报告错误。
异常使您能够编写代码的主要流程,并在其他地方处理异常情况。如果readFile
函数使用异常而不是传统的错误管理技术,它会看起来更像以下内容。
*readFile* {
try {
*open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;*
} catch (*fileOpenFailed*) {
*doSomething;*
} catch (*sizeDeterminationFailed*) {
*doSomething;*
} catch (*memoryAllocationFailed*) {
*doSomething;*
} catch (*readFailed*) {
*doSomething;*
} catch (*fileCloseFailed*) {
*doSomething;*
}
}
请注意,异常并不免除您进行错误检测、报告和处理的工作,但它们确实帮助您更有效地组织工作。
优点 2:将错误传播到调用堆栈上
异常的第二个优点是能够将错误报告传播到方法的调用堆栈上。假设readFile
方法是主程序进行的一系列嵌套方法调用中的第四个方法:method1
调用method2
,method2
调用method3
,最终调用readFile
。
method1 {
*call method2;*
}
method2 {
*call method3;*
}
method3 {
*call readFile;*
}
假设method1
是唯一对readFile
中可能发生的错误感兴趣的方法。传统的错误通知技术会强制method2
和method3
将readFile
返回的错误代码传播到调用堆栈上,直到错误代码最终到达method1
——唯一对它们感兴趣的方法。
method1 {
errorCodeType error;
error = *call method2;*
if (error)
*doErrorProcessing;*
else
*proceed;*
}
errorCodeType method2 {
errorCodeType error;
error = call method3;
if (error)
return error;
else
*proceed;*
}
errorCodeType method3 {
errorCodeType error;
error = call readFile;
if (error)
return error;
else
*proceed;*
}
请记住,Java 运行时环境会逆向搜索调用堆栈,以找到任何对处理特定异常感兴趣的方法。一个方法可以规避其内部抛出的任何异常,从而允许调用堆栈中更高层的方法捕获它。因此,只有关心错误的方法才需要担心检测错误。
method1 {
try {
*call method2;*
} catch (*exception* e) {
*doErrorProcessing;*
}
}
method2 throws *exception* {
*call method3;*
}
method3 throws exception {
*call readFile;*
}
然而,正如伪代码所示,规避异常需要中间方法付出一些努力。在方法内部可能抛出的任何已检查异常必须在其throws
子句中指定。
优势 3:分组和区分错误类型
因为程序中抛出的所有异常都是对象,异常的分组或分类是类层次结构的自然结果。Java 平台中一组相关的异常类的示例是java.io
中定义的那些 — IOException
及其后代。IOException
是最一般的,表示在执行 I/O 时可能发生的任何类型的错误。其后代表示更具体的错误。例如,FileNotFoundException
表示无法在磁盘上找到文件。
一个方法可以编写特定的处理程序,可以处理非常具体的异常。FileNotFoundException
类没有后代,因此以下处理程序只能处理一种类型的异常。
catch (FileNotFoundException e) {
...
}
一个方法可以通过在catch
语句中指定任何异常的超类来基于其组或一般类型捕获异常。例如,为了捕获所有 I/O 异常,无论其具体类型如何,异常处理程序指定一个IOException
参数。
catch (IOException e) {
...
}
这个处理程序将能够捕获所有 I/O 异常,包括FileNotFoundException
、EOFException
等。您可以通过查询传递给异常处理程序的参数来找到发生的详细信息。例如,使用以下内容打印堆栈跟踪。
catch (IOException e) {
// Output goes to System.err.
e.printStackTrace();
// Send trace to stdout.
e.printStackTrace(System.out);
}
你甚至可以设置一个异常处理程序,用于处理这里的任何Exception
。
// *A (too) general exception handler*
catch (Exception e) {
...
}
Exception
类接近Throwable
类层次结构的顶部。因此,此处理程序将捕获许多其他异常,除了处理程序打算捕获的异常之外。如果您只希望程序执行的操作是打印出用户的错误消息,然后退出,您可能希望以这种方式处理异常。
然而,在大多数情况下,您希望异常处理程序尽可能具体。原因是处理程序必须首先确定发生了什么类型的异常,然后才能决定最佳的恢复策略。实际上,通过不捕获特定错误,处理程序必须适应任何可能性。过于一般化的异常处理程序可能会使代码更容易出错,因为它会捕获和处理程序员未预料到的异常,而处理程序并不打算处理这些异常。
正如所指出的,您可以创建异常组并以一般方式处理异常,或者您可以使用特定的异常类型来区分异常并以精确方式处理异常。
摘要
原文:docs.oracle.com/javase/tutorial/essential/exceptions/summary.html
程序可以使用异常来指示发生了错误。要抛出异常,请使用throw
语句,并提供一个异常对象 — 一个Throwable
的后代 — 以提供有关发生的具体错误的信息。抛出未捕获的已检查异常的方法必须在其声明中包含一个throws
子句。
程序可以通过使用try
、catch
和finally
块的组合来捕获异常。
try
块标识出可能发生异常的代码块。catch
块标识出一个代码块,称为异常处理程序,可以处理特定类型的异常。finally
块标识出保证执行的代码块,并且是关闭文件、恢复资源以及在try
块中封闭的代码之后进行清理的正确位置。
try
语句应至少包含一个catch
块或一个finally
块,并且可以有多个catch
块。
异常对象的类表示抛出的异常类型。异常对象可以包含有关错误的进一步信息,包括错误消息。通过异常链接,一个异常可以指向导致它的异常,后者又可以指向导致它的异常,依此类推。
问题和练习
原文:docs.oracle.com/javase/tutorial/essential/exceptions/QandE/questions.html
问题
- 以下代码是否合法?
try {
} finally {
}
- 以下处理程序可以捕获哪些异常类型?
catch (Exception e) {
}
使用这种类型的异常处理程序有什么问题?
- 以下异常处理程序的写法有什么问题?这段代码能编译吗?
try {
} catch (Exception e) {
} catch (ArithmeticException a) {
}
- 将第一个列表中的每种情况与第二个列表中的一项进行匹配。
- `int[] A;
A[0] = 0;` - JVM 开始运行您的程序,但 JVM 找不到 Java 平台类。(Java 平台类位于
classes.zip
或rt.jar
中。) - 一个程序正在读取流并达到
流结束
标记。 - 在关闭流之前和达到
流结束
标记之后,一个程序尝试再次读取流。 - __ 错误
- __ 已检查异常
- __ 编译错误
- __ 无例外
练习
- 在
ListOfNumbers.java
中添加一个readList
方法。该方法应从文件中读取int
值,打印每个值,并将它们附加到向量的末尾。您应该捕获所有适当的错误。您还需要一个包含要读取的数字的文本文件。 - 修改以下
cat
方法以便能够编译。
public static void cat(File file) {
RandomAccessFile input = null;
String line = null;
try {
input = new RandomAccessFile(file, "r");
while ((line = input.readLine()) != null) {
System.out.println(line);
}
return;
} finally {
if (input != null) {
input.close();
}
}
}
检查您的答案。
课程:基本 I/O
本课程涵盖了用于基本 I/O 的 Java 平台类。它首先关注* I/O 流*,这是一个极大简化 I/O 操作的强大概念。该课程还涉及序列化,它允许程序将整个对象写入流并再次读取它们。然后课程将查看文件 I/O 和文件系统操作,包括随机访问文件。
大多数在“ I/O 流”部分涵盖的类位于java.io
包中。大多数在“文件 I/O”部分涵盖的类位于java.nio.file
包中。
I/O 流
- 字节流处理原始二进制数据的 I/O。
- 字符流处理字符数据的 I/O,自动处理与本地字符集之间的转换。
- 缓冲流通过减少对本机 API 的调用次数来优化输入和输出。
- 扫描和格式化允许程序读取和写入格式化文本。
- 从命令行进行 I/O 描述了标准流和控制台对象。
- 数据流处理基本数据类型和
String
值的二进制 I/O。 - 对象流处理对象的二进制 I/O。
文件 I/O(使用 NIO.2)
- 什么是路径?探讨了文件系统上路径的概念。
- Path 类介绍了
java.nio.file
包的基石类。 - 路径操作查看了
Path
类中处理语法操作的方法。 - 文件操作介绍了许多文件 I/O 方法共有的概念。
- 检查文件或目录展示了如何检查文件的存在性和可访问性级别。
- 删除文件或目录。
- 复制文件或目录。
- 移动文件或目录。
- 管理元数据解释了如何读取和设置文件属性。
- 读取、写入和创建文件展示了读取和写入文件的流和通道方法。
- 随机访问文件展示了如何以非顺序方式读取或写入文件。
- 创建和读取目录涵盖了特定于目录的 API,例如如何列出目录的内容。
- 链接,符号或其他涵盖了与符号链接和硬链接相关的特定问题。
- 遍历文件树演示了如何递归访问文件树中的每个文件和目录。
- 查找文件展示了如何使用模式匹配搜索文件。
- 监视目录变化展示了如何使用监视服务检测一个或多个目录中添加、删除或更新的文件。
- 其他有用的方法涵盖了在本课程中其他地方无法涵盖的重要 API。
- 旧版文件 I/O 代码展示了如何利用
Path
功能,如果你的旧代码使用了java.io.File
类。提供了一个将java.io.File
API 映射到java.nio.file
API 的表格。
总结
本教程涵盖的关键要点总结。
问题和练习
通过尝试这些问题和练习来测试你在本教程中学到的知识。
I/O 类的实际运用
下一个教程中的许多示例,自定义网络,使用了本课程中描述的 I/O 流来从网络连接读取和写入。
安全注意事项: 一些 I/O 操作需要当前安全管理器的批准。这些教程中包含的示例程序是独立应用程序,默认情况下没有安全管理器。要在小程序中运行,大多数这些示例都需要进行修改。查看小程序的能力和限制以获取有关小程序所受的安全限制的信息。
I/O 流
原文:docs.oracle.com/javase/tutorial/essential/io/streams.html
I/O 流表示输入源或输出目的地。流可以表示许多不同类型的源和目的地,包括磁盘文件、设备、其他程序和内存数组。
流支持许多不同类型的数据,包括简单的字节、基本数据类型、本地化字符和对象。一些流只是传递数据;另一些以有用的方式操作和转换数据。
无论它们内部如何工作,所有流对使用它们的程序呈现相同简单的模型:流是一系列数据。程序使用输入流从源读取数据,一次读取一个项目:
将信息读入程序。
程序使用输出流向目的地写入数据,一次写入一个项目:
将信息从程序写入。
在本课程中,我们将看到可以处理从基本值到高级对象的各种数据的流。
上图中的数据源和数据目的地可以是任何保存、生成或消耗数据的东西。显然,这包括磁盘文件,但源或目的地也可以是另一个程序、外围设备、网络套接字或数组。
在下一节中,我们将使用最基本的流类型,字节流,来演示流 I/O 的常见操作。作为示例输入,我们将使用示例文件xanadu.txt
,其中包含以下诗句:
In Xanadu did Kubla Khan
A stately pleasure-dome decree:
Where Alph, the sacred river, ran
Through caverns measureless to man
Down to a sunless sea.
字节流
原文:docs.oracle.com/javase/tutorial/essential/io/bytestreams.html
程序使用字节流来执行 8 位字节的输入和输出。所有字节流类都是从InputStream和OutputStream继承而来。
有许多字节流类。为了演示字节流的工作原理,我们将重点放在文件 I/O 字节流FileInputStream和FileOutputStream上。其他类型的字节流使用方式基本相同;它们主要在构造方式上有所不同。
使用字节流
我们将通过检查一个名为CopyBytes
的示例程序来探讨FileInputStream
和FileOutputStream
,该程序使用字节流逐字节复制xanadu.txt
。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class CopyBytes {
public static void main(String[] args) throws IOException {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("xanadu.txt");
out = new FileOutputStream("outagain.txt");
int c;
while ((c = in.read()) != -1) {
out.write(c);
}
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}
CopyBytes
在一个简单的循环中花费大部分时间,逐字节读取输入流并写入输出流,如下图所示。
简单的字节流输入和输出。
总是关闭流
当不再需要流时关闭流非常重要—非常重要,以至于CopyBytes
使用finally
块来确保即使发生错误,两个流也将被关闭。这种做法有助于避免严重的资源泄漏。
一个可能的错误是CopyBytes
无法打开一个或两个文件。当发生这种情况时,对应于文件的流变量从未从其初始的null
值更改。这就是为什么CopyBytes
确保每个流变量在调用close
之前包含一个对象引用。
何时不使用字节流
CopyBytes
看起来像一个普通程序,但实际上代表了一种应该避免的低级 I/O。由于xanadu.txt
包含字符数据,最好的方法是使用字符流,如下一节所讨论的。还有用于更复杂数据类型的流。字节流应该仅用于最基本的 I/O。
那么为什么要谈论字节流呢?因为所有其他流类型都是建立在字节流之上的。
字符流
原文:docs.oracle.com/javase/tutorial/essential/io/charstreams.html
Java 平台使用 Unicode 约定存储字符值。字符流 I/O 会自动将内部格式与本地字符集进行转换。在西方区域,本地字符集通常是 ASCII 的 8 位超集。
对于大多数应用程序,使用字符流进行 I/O 与使用字节流进行 I/O 并无太大区别。使用流类进行的输入和输出会自动转换为本地字符集。使用字符流而不是字节流的程序会自动适应本地字符集,并且为国际化做好准备,而无需程序员额外努力。
如果国际化不是首要任务,您可以简单地使用字符流类,而不必过多关注字符集问题。稍后,如果国际化成为首要任务,您的程序可以在不进行大量重编码的情况下进行调整。查看国际化教程以获取更多信息。
使用字符流
所有字符流类都是从Reader和Writer继承而来。与字节流一样,有专门用于文件 I/O 的字符流类:FileReader和FileWriter。CopyCharacters
示例演示了这些类。
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyCharacters {
public static void main(String[] args) throws IOException {
FileReader inputStream = null;
FileWriter outputStream = null;
try {
inputStream = new FileReader("xanadu.txt");
outputStream = new FileWriter("characteroutput.txt");
int c;
while ((c = inputStream.read()) != -1) {
outputStream.write(c);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
CopyCharacters
与CopyBytes
非常相似。最重要的区别在于,CopyCharacters
使用FileReader
和FileWriter
进行输入和输出,而不是FileInputStream
和FileOutputStream
。请注意,CopyBytes
和CopyCharacters
都使用一个int
变量进行读取和写入。但是,在CopyCharacters
中,int
变量在其最后 16 位中保存字符值;而在CopyBytes
中,int
变量在其最后 8 位中保存byte
值。
使用字节流的字符流
字符流通常是字节流的“包装器”。字符流使用字节流执行物理 I/O,而字符流处理字符和字节之间的转换。例如,FileReader
使用FileInputStream
,而FileWriter
使用FileOutputStream
。
有两个通用的字节到字符的“桥梁”流:InputStreamReader和OutputStreamWriter。当没有符合您需求的预打包字符流类时,请使用它们来创建字符流。网络教程中的套接字课程展示了如何从套接字类提供的字节流创建字符流。
面向行的 I/O
字符 I/O 通常以比单个字符更大的单位进行。一个常见的单位是行:一串带有行终止符的字符。行终止符可以是回车/换行序列("\r\n"
),单个回车("\r"
)或单个换行("\n"
)。支持所有可能的行终止符允许程序读取在任何广泛使用的操作系统上创建的文本文件。
让我们修改CopyCharacters
示例以使用面向行的 I/O。为此,我们必须使用两个以前未见过的类,BufferedReader和PrintWriter。我们将在缓冲 I/O 和格式化中更深入地探讨这些类。现在,我们只关注它们对面向行的 I/O 的支持。
CopyLines
示例调用BufferedReader.readLine
和PrintWriter.println
来逐行进行输入和输出。
import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;
public class CopyLines {
public static void main(String[] args) throws IOException {
BufferedReader inputStream = null;
PrintWriter outputStream = null;
try {
inputStream = new BufferedReader(new FileReader("xanadu.txt"));
outputStream = new PrintWriter(new FileWriter("characteroutput.txt"));
String l;
while ((l = inputStream.readLine()) != null) {
outputStream.println(l);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
调用readLine
返回带有行的文本。CopyLines
使用println
输出每一行,该方法会附加当前操作系统的行终止符。这可能与输入文件中使用的行终止符不同。
除了字符和行之外,还有许多结构化文本输入和输出的方式。更多信息,请参见扫描和格式化。