`

枚举的构造函数中抛出异常会怎样

阅读更多
首先从使用enum实现单例说起。

为什么要用enum来实现单例?
这篇文章(http://javarevisited.blogspot.sg/2012/07/why-enum-singleton-are-better-in-java.html)阐述了三个理由:
1.enum单例简单、容易,只需几行代码:
public enum Singleton {
	INSTANCE;
}

2.enum单例自动处理序列化问题
传统的单例实现方式(例如懒汉式饿汉式),如果它implements Serializable,那它就不再是单例了,因为readObject方法总是会返回新的对象。
enum虽然implements Serializable,但它仍然是单例,这是由jvm保证的。

3.enum单例是线程安全的

此外,《Effective Java》也建议用enum实现单例,当然还有stackoverflow的讨论:http://stackoverflow.com/questions/70689/what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java

但是,用enum实现单例的话,它的构造函数不能抛出异常,否则会抛出Error(而不是Exception)。
试想这样一种情况,在远程调用中,服务端抛出了Error,而客户端try-catch的是Exception,那就捕获不到出错信息,客户端就直接崩溃了。
说到远程调用,说点题外话,dubbo当中是不建议传递枚举的(http://dubbo.io/User+Guide-zh.htm#UserGuide-zh-%E5%85%BC%E5%AE%B9%E6%80%A7):

枚举值

    如果是完备集,可以用Enum,比如:ENABLE, DISABLE。
    如果是业务种类,以后明显会有类型增加,不建议用Enum,可以用String代替。
    如果是在返回值中用了Enum,并新增了Enum值,建议先升级服务消费方,这样服务提供方不会返回新值。
    如果是在传入参数中用了Enum,并新增了Enum值,建议先升级服务提供方,这样服务消费方不会传入新值。



测试代码:
public enum EnumSingleton {

    INSTANCE;
    
    private EnumSingleton () {
        
        //模拟在构造函数中抛出异常的情况。实际情况中可能抛异常的情况包括:读取配置文件时文件不存在,连接数据库失败等等。
        int i = 1 / 0;
    }
    
    public String hello() {
        return "hello";
    }

}

public class LazyClassSingleton {
    
    private static LazyClassSingleton instance;
    
    private LazyClassSingleton () {
        int i = 1 / 0;
    }
    
    public static synchronized LazyClassSingleton getInstance () {
        if (instance == null) {
            instance = new LazyClassSingleton();
        }
        return instance;
    }

    public String hello() {
        return "hello";
    }
}


public class EagerClassSingleton {
    
    private static EagerClassSingleton instance = new EagerClassSingleton();
    
    private EagerClassSingleton () {
        
        int i = 1 / 0;
    }
    
    public static EagerClassSingleton getInstance() {
        return instance;
    }
    
    public String hello() {
        return "hello";
    }
}

public class TestSingleton {
    
    public static void main(String[] args){
//        testEnumSingleton();
//        testEagerClassSingleton();
        testLazyClassSingleton();
    }
    
    
    public static void testEnumSingleton() {
        try {
            System.out.println(EnumSingleton.INSTANCE.hello());
        } catch (Throwable e) {
            
            //抛出的是Error:java.lang.ExceptionInInitializerError。如果“catch Exception”而不是“catch Throwable”的话,那就捕获不到出错信息了
            System.out.println(e);
        }
    }
    

    public static void testEagerClassSingleton() {
        try {
            System.out.println(EagerClassSingleton.getInstance().hello());
        } catch (Throwable e) {
            
            //抛出的是Error:java.lang.ExceptionInInitializerError。如果“catch Exception”而不是“catch Throwable”的话,那就捕获不到出错信息了
            System.out.println(e);
        }
    }
    

    public static void testLazyClassSingleton() {
        try {
            System.out.println(LazyClassSingleton.getInstance().hello());
        } catch (Exception e) {
            
            //抛出的是Exception:java.lang.ArithmeticException
            System.out.println(e);
        }
    }


}

对三种单例实现的方式(枚举、懒汉模式、饿汉模式)进行测试,发现只有懒汉模式是抛出Exception,其它两种都是抛出ExceptionInInitializerError。
这很好解释,因为懒汉模式是在方法(getInstance)调用中出错,而枚举方式和饿汉方式都是在类加载(Class initialization)时出错(The constructors are invoked when the enum class is initialized)。
类实例化出错显然更严重一些。


所以,在枚举方式和饿汉方式实现单例时,注意不要让构造函数抛出异常。

这就引申出第二个问题,在构造函数中要不要抛出异常呢?
《编写高质量代码-改善Java程序的151个建议》一书当中,作者在第114条建议中认为:不要在构造函数中抛出异常,尽管你可以这么做:
1.抛出unchecked Exception
例如:
public class Person {

    public Person (int age) {
        if (age < 0) {
            throw new IllegalArgumentException("age cannot be less than 0");
        }
    }
}

这也是比较常见的一种做法。

这个做法的问题是,调用者不知道是捕获这个异常还是不捕获。
捕获吧,要看文档或者源码才知道会抛什么异常,而且捕获的代码显得非常难看:

		try {
            Person p = new Person(20);
        } catch (Exception e) {
            e.printStackTrace();
        }

不捕获吧,出现IllegalArgumentException时,后续代码就无法执行了。

2.抛出checked Exception
这种做法引起的主要问题是,子类的构造函数中也要抛出checked Exception

看看stackoverflow的讨论:
http://stackoverflow.com/questions/6086334/is-it-good-practice-to-make-the-constructor-throw-an-exception
得票最高的看法是:
当参数不合法时,抛出异常是唯一的、合理的做法。但是要选择合适的Exception,而不是直接抛出java.lang.Exception。
也有人认为,在构造函数中抛出异常是“坏的实践”:你应该在传递参数给构造函数之前,检查参数的合法性。

说法不一。

我认为还是按简单的来处理,也就是不抛异常,把参数合法性的检查交给调用方。例如平时代码中我们写得最多的当然是类似这样的:

public class Person {
    
    private int age;

    public Person (int age) {
        this.age = age;
    }
}


没有进行参数检查。

如果某个类不是普通的java bean,而且参数合法性非常重要,那可以考虑在构造函数中检查参数并抛出合适的异常。

2
3
分享到:
评论

相关推荐

    Absolute C++中文版(原书第2版)-完美的C++教程,文档中还包含英文版

    18.1.4 在函数中抛出异常 546 18.1.5 异常说明 547 18.2 异常处理的编程技术 549 18.2.1 抛出异常的时机 549 18.2.2 异常类的层次结构 552 18.2.3 测试可用内存 552 18.2.4 再次抛出异常 552 第19章 标准...

    C++ Primer中文版(第5版)李普曼 等著 pdf 1/3

    C++ Primer中文版(第5版... 18.1.1 抛出异常 684  18.1.2 捕获异常 687  18.1.3 函数try语句块与构造函数 689  18.1.4 noexcept异常说明 690  18.1.5 异常类层次 693  18.2 命名空间 695  18.2.1 命名空间定义...

    C++Primer(第5版 )中文版(美)李普曼等著.part2.rar

    C++ Primer中文版(第5版... 18.1.1 抛出异常 684  18.1.2 捕获异常 687  18.1.3 函数try语句块与构造函数 689  18.1.4 noexcept异常说明 690  18.1.5 异常类层次 693  18.2 命名空间 695  18.2.1 命名空间定义...

    C++ Primer第四版【中文高清扫描版】.pdf

    15.4.5 构造函数和析构函数中的虚函数 497 15.5 继承情况下的类作用域 497 15.5.1 名字查找在编译时发生 498 15.5.2 名字冲突与继承 498 15.5.3 作用域与成员函数 499 15.5.4 虚函数与作用域 500 15.6 纯虚函数 502 ...

    c# program

    17. 在捕获(catch)语句的抛出异常子句中(throw),总是抛出原始异常维护原始错误的堆栈分配。 catch(Exception exception) { MessageBox.Show(exception.Message); throw ; //和throw exception一样。 } ...

    visualC++2010入门经典源代码

    6.3.1 抛出异常 253 6.3.2 捕获异常 254 6.3.3 mfc中的异常处理 255 6.4 处理内存分配错误 256 6.5 函数重载 257 6.5.1 函数重载的概念 258 6.5.2 引用类型和重载选择 260 6.5.3 何时重载函数 260 6.6 函数...

    C++和面向对象数值计算

    5.2 拷贝构造函数和拷贝赋值 5.3 友元 5.4 静态成员 5.5 常量和可变成员 5.6 类的对象作为成员 5.7 类的数组 5.8 成员指针 5.9 常微分方程的数值解法 5.10 练习 第6章 运算符重载 6.1 复数 ...

    [Visual.C++.2010入门经典(第5版)].Ivor.Horton.part1

    6.3.1 抛出异常 253 6.3.2 捕获异常 254 6.3.3 mfc中的异常处理 255 6.4 处理内存分配错误 256 6.5 函数重载 257 6.5.1 函数重载的概念 258 6.5.2 引用类型和重载选择 260 6.5.3 何时重载函数 260 6.6 函数模板 261 ...

    Visual C++ 2005入门经典.part08.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    Visual C++ 2005入门经典.part04.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    Visual C++ 2005入门经典.part07.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    Visual C++ 2005入门经典.part09.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    Visual C++ 2005入门经典.part06.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    Visual C++ 2005入门经典.part05.rar (整理并添加所有书签)

    6.3.1 抛出异常 6.3.2 捕获异常 6.3.3 MFC中的异常处理 6.4 处理内存分配错误 6.5 函数重载 6.5.1 函数重载的概念 6.5.2 何时重载函数 6.6 函数模板 6.7 使用函数的示例 6.7.1 实现计算器 6.7.2 从字符串中删除空格 ...

    TextRPG:我尝试基于文本的RPG

    文字RPG 我尝试使用基于文本的控制台RPG练习C#的基础知识。... 从技术上讲,它现在可以正常工作,除非抛出异常。 我处理了例外情况,但未提示用户重新输入,然后移至下一步。 我需要提示用户输入,直到输入有效为止。

    NET设计规范-.NET约定、惯用法与模式.part2

    5.3 构造函数的设计 117 5.4 事件的设计 123 5.5 字段的设计 130 5.6 操作符重载 132 5.6.1 重载operator== 136 5.6.2 类型转换操作符 136 5.7 参数的设计 138 5.7.1 枚举和布尔参数之间的选择 140 ...

    超级有影响力霸气的Java面试题大全文档

    java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。 9、说出Servlet的生命周期,并说出Servlet和CGI的区别。  Servlet被服务器实例化后,容器运行其init方法...

    Visual C++ 2005入门经典--源代码及课后练习答案

    6.3.1 抛出异常 255 6.3.2 捕获异常 256 6.3.3 MFC中的异常处理 257 6.4 处理内存分配错误 258 6.5 函数重载 259 6.5.1 函数重载的概念 260 6.5.2 何时重载函数 262 6.6 函数模板 262 6.7 使用...

Global site tag (gtag.js) - Google Analytics