受检查异常的一个问题是,有时根本不允许您抛出它们。 特别是,如果您要重写超类中声明的方法或实现接口中声明的方法,并且该方法没有声明任何检查的异常,则您的实现也不能声明一个。 这迫使您过早地处理异常。 您可以将异常转换为运行时异常,也可以不处理而将其抑制。 但这是您应该做的事情,还是这里存在更严重的错误?
问题
看一个例子可以使这个问题更清楚。 假设您有一个File
对象List
,并且想按其规范路径,即按别名,符号链接和/../
和/./
之后的完整绝对路径,按字典顺序对它们进行排序。 天真的方法使用比较器,如清单1所示:
清单1.通过规范路径比较两个文件
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class FileComparator implements Comparator<File> {
public int compare(File f1, File f2) {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
public static void main(String[] args) {
ArrayList<File> files = new ArrayList<File>();
for (String arg : args) {
files.add(new File(arg));
}
Collections.sort(files, new FileComparator());
for (File f : files) {
System.out.println(f);
}
}
}
不幸的是,该代码无法编译。 问题在于getCanonicalPath()
方法抛出IOException
因为它需要访问文件系统。 通常,在使用检查的异常时,您可以通过以下两种方式之一来考虑这一点:
- 将
try
块包装在有问题的代码周围,并捕获所有引发的异常。 - 声明此示例中的封闭方法
compare()
也会抛出IOException
。
通常,选择取决于您是否可以在引发异常时合理地处理异常。 如果可以,请使用try
- catch
块。 如果不能,则声明封闭方法本身引发异常。 不幸的是,这些技术都不适用于本示例。
您不能合理地在compare()
方法内部处理IOException
。 从技术上讲,您可以-只需返回0
或1
或-1
,如清单2所示:
清单2.返回异常的默认值
public int compare(File f1, File f2) {
try {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
catch (IOException ex) {
return -1;
}
}
但是,这违反了compare()
方法的约定,因为它不是稳定的结果。 使用相同的对象两次调用它,您可能会得到不同的答案。 如果使用比较器进行排序,则可能意味着列表最后未正确排序。 因此,现在尝试选项2 —声明compare() throws IOException
:
public int compare(File f1, File f2) throws IOException {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
这甚至不编译。 因为检查异常是方法签名的一部分,所以您不能将一个添加到覆盖的变量中,而只能更改其返回类型。 剩下的选项1.5:在compare()
捕获异常并将其转换为运行时异常,您可以抛出该异常,如清单3所示:
清单3.将检查到的异常转换为运行时异常
public int compare(File f1, File f2) {
try {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
不幸的是,尽管它可以编译,但由于更细微的原因,这种方法也不起作用。 Comparator
接口定义了一个合同(请参阅参考资料 )。 该合同不允许该方法引发运行时异常(除非违反了在调用代码中有错误的通用类型安全性)。 使用此比较器的方法合法地依赖于它比较两个文件,而不会引发任何异常。 他们将不准备处理意外异常,而异常会从compare()
冒出来。
确实,这种微妙之处恰恰是为什么运行时异常对于应该由代码处理的外部条件而言不是一个好主意。 它们使您可以彻底解决问题,而无需真正解决。 不处理异常的所有不良后果仍然存在,包括数据损坏和错误结果。
您陷入了两难的困境。 您不能现实地处理compare()
内的异常,也不能处理compare()
外的异常。 剩下的是— System.exit()
? 唯一正确的解决方案是完全避免困境。 幸运的是,您至少有两种方法可以做到这一点。
拆分问题
第一个解决方案是将问题分为两部分。 比较不会引起异常。 那只是字符串。 异常是由于通过规范路径将文件转换为字符串引起的。 如果将可以引发异常的操作与不能引发异常的操作分开,则问题将变得更容易解决。 也就是说,首先将所有文件对象转换为字符串,然后通过字符串比较器(甚至java.lang.String
的自然顺序)对字符串进行排序,最后使用字符串的排序列表对文件的原始列表进行排序。 这种方法不太直接,但是它的优点是可以在IOException
列表之前IOException
。 如果发生异常,则该异常会在任何损坏发生之前的明确点发生,并且调用代码可以弄清楚如何对其进行处理。 清单4演示:
清单4.首先阅读,然后排序
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
public class FileComparator {
private static ArrayList<String> getCanonicalPaths(ArrayList<File> files)
throws IOException {
ArrayList<String> paths = new ArrayList<String>();
for (File file : files) paths.add(file.getCanonicalPath());
return paths;
}
public static void main(String[] args) throws IOException {
ArrayList<File> files = new ArrayList<File>();
for (String arg : args) {
files.add(new File(arg));
}
ArrayList<String> paths = getCanonicalPaths(files);
// to maintain the original mapping
HashMap<String, File> map = new HashMap<String, File>();
int i = 0;
for (String path : paths) {
map.put(path, files.get(i));
i++;
}
Collections.sort(paths);
files.clear();
for (String path : paths) {
files.add(map.get(path));
}
}
}
清单4并未消除发生I / O错误的可能性。 您不能这样做,因为它是代码外的强制功能。 但是您已经将该问题移到了更可行的地方。
避免问题
上面提到的方法有点复杂,所以我建议第二种解决方案:根本不要使用内置的compare()
函数或Collections.sort()
。 考虑一下,尽管这可能很方便,但可能不适用于此用例。 Comparable
和Comparator
设计用于比较操作是确定性和可预测的情况。 一旦I / O进入画面,情况便不再如此。 通常的算法和接口可能不适用。 即使它们确实起作用,它们也可能效率极低。
例如,假设您不是通过文件的规范路径来比较文件,而是通过文件的内容来比较它们。 每个比较操作都需要读取要比较的两个文件的内容,甚至可能是完整的内容。 如果是这样,一种有效的算法将希望最大程度地减少读取次数,并且很可能希望缓存每次读取的结果(如果每个文件很大,则可能缓存每个文件的哈希码),而不是每次都重新读取每个文件比较。 同样,您会想到先填充比较键列表然后进行排序,而不是内联排序。
您可以想象定义一个单独的并行IOComparator
接口,该接口的确会引发必要的异常,如清单5所示:
清单5.一个独立的IOComparator
接口
import java.io.IOException;
public interface IOComparator<T> {
int compare(T o1, T o2) throws IOException;
}
然后,基于此类定义一个单独的并行实用程序树,该类需要采取必要的措施处理集合的临时副本,以便它可以引发异常而不会使数据结构处于潜在的损坏中间状态。 例如,清单6提供了基本的冒泡排序:
清单6.气泡排序文件
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class IOSorter {
public static <T> void sort(List<T> list, IOComparator<? super T> comparator)
throws IOException {
List<T> temp = new ArrayList<T>(list.size());
temp.addAll(list);
bubblesort(temp, comparator);
// copy back to original list now that no exceptions have been thrown
list.clear();
list.addAll(temp);
}
// of course you can replace this with a better algorithm such as quicksort
private static <T> void bubblesort(List<T> list, IOComparator<? super T> comparator)
throws IOException {
for (int i = 1; i < list.size(); i++) {
for (int j = 0; j < list.size() - i; j++) {
if (comparator.compare(list.get(j), list.get(j + 1)) > 0) {
swap(list, j);
}
}
}
}
private static <T> void swap(List<T> list, int j) {
T temp = list.get(j);
list.set(j, list.get(j+1));
list.set(j + 1, temp);
}
}
这几乎不是唯一的方法。 为了清楚起见,清单6故意与现有的Collections.sort()
方法并行。 但是返回一个新列表而不是对旧列表进行变异可能更有意义,这是为了避免发生在变异中间引发异常时发生的问题。
最后,既然您实际上已经确认并处理了I / O错误的真正可能性,而不是一味地解决这个问题,则可以进行更复杂的错误纠正。 例如,一个IOComparator
可能不会出现I / O错误,但是由于许多I / O问题是暂时的,因此您可以重试几次,如清单7所示:
清单7.如果一开始您没有成功,请尝试,然后再试一次(但次数不要太多)
import java.io.File;
import java.io.IOException;
public class CanonicalPathComparator implements IOComparator<File> {
@Override
public int compare(File f1, File f2) throws IOException {
for (int i = 0; i < 3; i++) {
try {
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
catch (IOException ex) {
continue;
}
}
// last chance
return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}
}
对于常规的Comparator
此技术无法解决问题,因为您必须无限期地重试以避免抛出异常,并且许多I / O问题不是暂时的。
检查异常是个坏主意吗?
如果java.io.IOException
是运行时异常而不是检查异常,那么所有这些都会有所不同吗? 答案是肯定的。 如果IOException
扩展了RuntimeException
而不是java.lang.Exception
,那么编写容易出错的错误代码(忽略I / O错误的真正可能性并在运行时意外失败)将更加容易。
但是,编写准备和处理I / O错误的正确代码并不容易。 是的,这种方法比永远不会发生意外的I / O错误并且不需要为它们进行计划的方法更为复杂。 但是,从Java语言中消除受检查的异常不会使我们进入这种幸福的状态。 I / O错误和其他环境问题已成事实,因此为它们做准备比忽略它们要好得多。
最重要的是,出于充分的原因,检查的异常是方法签名的一部分。 当您发现自己试图从不允许抛出的方法中抛出一个已检查的异常,从而抑制了一个您不应抑制的异常时,请进行备份,重新组合,并考虑为什么要覆盖该方法。第一名。 很有可能您应该完全做其他事情。
翻译自: https://www.ibm.com/developerworks/java/library/j-ce/index.html