Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 280|回复: 0

[JavaIO学习]当不能抛出异常时...

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-11-5 23:58:49 | 显示全部楼层 |阅读模式
    checked 异常的一个问题是,有时候不允许抛出这样的异常。特别是,如果要覆盖超类中声明的方法,或者实现接口中声明的方法,而那个方法没有声明任何 checked 异常,那么新的实现也不能声明 checked 异常。因此必须预先处理异常。另外,可以将异常转换为运行时异常,或者绕过它而不处理它。但是,应该这样做吗,这其中是否隐藏着错误?  问题     只要看一个例子,问题就清楚了。假设有一个
    1. File
    复制代码
    对象的
    1. List
    复制代码
    ,需要按它们的标准路径以字典顺序排序。所谓标准路径,是指在解析别名、符号链接和
    1. /../
    复制代码
    1. /./
    复制代码
    之后得到的完整绝对路径。本地方法使用一个比较器,如清单 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);
          }
        }
       
    }[/code]
       
      

    不幸的是,该代码不能通过编译。问题在于,
    1. getCanonicalPath()
    复制代码
    方法抛出一个
    1. IOException
    复制代码
    ,因为它需要访问文件系统。通常,当使用 checked 异常时,可以使用以下两种方法之一:  

    将出错的代码包装在一个
    1. try
    复制代码
    块中,并捕捉抛出的异常。
    声明包装方法(本例为
    1. compare()
    复制代码
    )也抛出
    1. IOException
    复制代码
    。  
    通常,至于选择何种方法,取决于是否能在抛出异常时合理地处理异常。如果能,那么使用
    1. try
    复制代码
    -
    1. catch
    复制代码
    块。如果不能,那么声明包装方法本身抛出异常。不幸的是,这两种技巧对于本例都不管用。  在
    1. compare()
    复制代码
    方法中无法合理地处理
    1. IOException
    复制代码
    。从技术上讲,似乎可以做到 ― 即返回
    1. 0
    复制代码
    1. 1
    复制代码
    1. -1
    复制代码
    ,如清单 2 所示:  
    清单 2. 抛出异常时返回一个默认值


      
       
                                      
    public int compare(File f1, File f2) {
        try {
            return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
        }
        catch (IOException ex) {
           return -1;
        }
    }[/code]
       
      

    然而,这违反了
    1. compare()
    复制代码
    方法的约定,因为它不是一个稳定的结果。对于相同的对象,前后两次调用可能产生不同的结果。如果使用这个比较器来排序,那么意味着最终列表没有被正确排序。所以现在试试第 2 个选项 ― 声明
    1. compare() 抛出 IOException
    复制代码
    :  

      
       
       public int compare(File f1, File f2) throws IOException {
        return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
    }[/code]
       
      

    这也不能通过编译。因为 checked 异常是方法签名的一部分,在覆盖方法时,不能增加 checked 异常,就像不能改变 return 类型一样。那么最后还剩下一个折中选项:在
    1. compare()
    复制代码
    中捕捉异常,将它转换成运行时异常,然后抛出运行时异常,如清单 3 所示:  
    清单 3. 将 checked 异常转换成运行时异常


      
       
                                      
    public int compare(File f1, File f2) {
        try {
            return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
        }
        catch (IOException ex) {
           throw new RuntimeException(ex);
        }
    }[/code]
       
      

    不幸的是,虽然这样可以通过编译,但是这种方法也不管用,其原因较为微妙。
    1. Comparator
    复制代码
    接口定义一个合约(请参阅 参考资料)。这个合约不允许该方法抛出运行时异常(防止因违反泛型类型安全而成为调用代码中的 bug)。使用这个比较器的方法合理地依靠它来比较两个文件,而不抛出任何异常。它们没有准备好处理
    1. compare()
    复制代码
    中意外出现的异常。  正是由于这个微妙的原因,让运行时异常成为代码要处理的外部状况是一个坏主意。这样只是逃避问题,并没有真正处理问题。不处理异常所带来的不良后果仍然存在,包括毁坏数据和得到不正确的结果。  这样便陷入了困境。既不能在
    1. compare()
    复制代码
    内真正有效地处理异常,又不能在
    1. compare()
    复制代码
    之外处理异常。还剩下什么地方可以处理异常 ―
    1. System.exit()
    复制代码
    ? 惟一正确的办法是完全避免这种困境。幸运的是,至少有两种方法可以做到这一点。  将问题一分为二 第一种办法是将问题一分为二。比较本身不会导致异常。比较的只是字符串而已。通过标准路径将文件转换成字符串才会导致异常。如果将可能抛出异常的操作与不会抛出异常的操作分开,那么问题就更容易处理了。也就是说,首先将所有文件对象转换为字符串,然后通过字符串比较器(甚至可以通过
    1. java.lang.String
    复制代码
    的自然排序)对字符串排序,最后使用排序后的字符串列表对原始的文件列表排序。这种方法不太直接,但是优点是在列表被改变之前就抛出
    1. 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));
          }
        }
       
    }[/code]
       
      

    清单 4 并没有消除出现 I/O 错误的可能性。这一点无法做到,因为这里的代码无力提供这样的功能。但是,可以将这个问题交给更合适的地方来处理。  避免问题 前面提到的方法有点复杂,所以我建议另一种方法:不使用内置的
    1. compare()
    复制代码
    函数或
    1. Collections.sort()
    复制代码
    。使用这样的函数也许比较方便,但是不适合当前情况。
    1. Comparable
    复制代码
    1. Comparator
    复制代码
    是为确定的、可预测的比较操作而设计的。一旦 I/O 不再符合这种情况,很可能常用的算法和接口变得不适用。即使勉强可以使用,其效率也极其低下。  例如,假设不是按标准路径来比较文件,而是按内容来比较文件。对于所比较的两个文件,每个比较操作都需要读文件的内容 ― 甚至可能是完整的内容。这样一来,高效的算法会想要尽量减少读的次数,并且可能会想缓存每次读的结果 ― 或者,如果文件较大,则可能缓存每个文件的 hashcode ― 而不是每次比较时重新读每个文件。同样,您会想到首先填充一个比较键列表,然后进行排序,而不是进行内联排序。  可以想象定义一个单独的、并行的
    1. IOComparator
    复制代码
    接口,该接口抛出必要的异常,如清单 5 所示:  
    清单 5. 独立的
    1. IOComparator
    复制代码
    接口



      
       
                                      
    import java.io.IOException;
    public interface IOComparator<T> {
        int compare(T o1, T o2) throws IOException;
       
    }[/code]
       
      

    然后基于这个类定义一个单独的、相近实用程序树,由它对集合的临时副本进行必要的操作,从而允许抛出异常,同时又不会使数据结构处于可能受损害的、中间的状态。例如,清单 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);
        }

    }[/code]
       
      

    这不是唯一的方法。为了清晰,清单 6 有意模仿已有的
    1. Collections.sort()
    复制代码
    方法;但是,也许更有效的方法是返回一个新的列表,而不是直接修改旧列表,以防在修改列表时抛出异常所带来的损害。  最终,您实际上承认并着手处理可能出现的 I/O 错误,而不是逃避它,您甚至可以做更高级的错误修正。例如,
    1. 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());  
        }
    }[/code]
       
      

    这种技巧不能解决常规的
    1. Comparator
    复制代码
    的问题,因为必须重试无数次才能避免抛出异常,而且很多 I/O 问题并不是暂时性的。  checked 异常是坏主意吗? 如果
    1. java.io.IOException
    复制代码
    是运行时异常,而不是 checked 异常,问题是不是有所改观?答案是否定的。如果
    1. IOException
    复制代码
    扩展
    1. RuntimeException
    复制代码
    而不是
    1. java.lang.Exception
    复制代码
    ,那么更容易编写出有 bug 的、不正确的代码,这种代码忽略了真正可能发生的 I/O 错误,而在运行时出人意料地失败。 然而,编写正确的、有准备并且能够处理 I/O 错误的代码并不会更容易。是的,相对于不会出现意外 I/O 错误,不需要为此做准备的情况,这种方法更加复杂。但是,从 Java 语言中消除 checked 异常无助于我们实现那样的理想情况。I/O 错误和其他环境问题是常态,积极准备比视而不见要好得多。  总之,checked 异常作为方法签名的一部分并非没有道理。当您发现自己想要从一个方法抛出一个 checked 异常,而这又是不允许的 ― 因而抑制本不该抑制的异常 ― 那么回过头来,重新组织一下,考虑为什么一开始要覆盖那个方法。很可能,您本应该采取完全不同的方式。  

      
      
       
       

         
       

         
       
      
    复制代码
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-25 11:19 , Processed in 0.361105 second(s), 48 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表