Thinking in java Chapter12 Java 通过异常处理错误信息

java的基本理念是"结构不佳的代码不能够运行"

发现错误的理想时机是编译阶段,然而,编译期间并不能找出所有的错误,余下的问题必须在运行时期解决。

红色部分为受检查的异常。它们必须被捕获,或者在函数中声明为抛出异常

1 概念

2 基本异常

异常情形(exceptional conditin)是指阻止当前方法或作用域继续执行的问题.

普通问题是指在当前环境下能得到足够的信息,总能处理这个错误

当抛出异常后,有几件事会随之发生。首先,同Java中其它对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

if(t == null)
throw new NullPointerException();//这样就把错误信息传播到更大的环境中,

标准异常都有两个构造器:一个是默认构造器,一个是接受字符串作为参数,以便能把有关信息放入异常对象的构造器;

能够抛出任意类型的Throwable对象,它是异常的根类

3 捕获异常

监控区域(guarded region):一段可能产生异常的代码,并且后面跟着处理这些异常的代码

  1. try块

如果在方法内部抛出异常后,这个方法将在抛出异常的过程中结束,如果不想就此结束,可以设置一个try块来捕获异常

try{
//Code that might generate exception
}
2.异常处理程序

异常处理程序紧跟在try块之后,以关键字catch表示,异常处理程序必须紧跟在try块之后,当异常被抛出时,异常处理机制将负责搜寻参数与异常类型想匹配的第一个处理程序,然后进入catch子句执行,此时认为异常得到了处理,一旦catch子句结束才处理程序的查找过程结束,并不会在调用其它catch子句.

异常处理模型

终止模型:错误非常关键,一旦异常被抛出,即终止程序

4 创建自定义异常

编译器创建了默认构造器,它将自动调用基类的默认构造器,一般不会用Excetption(String)这样的构造器,这种构造器不实用,对异常来说最重要的是类名

package exceptions;

class SimpleException extends Exception{} 
// 编译器创建了默认构造器,自动调用基类的默认构造器  
/*
public class Exception extends Throwable 
    public Exception() {
        super();
    }
 
 public class Throwable implements Serializable    
        public Throwable() {
        fillInStackTrace();
    }

 */

public class InheritingExceptions{
    public void f() throws SimpleException{
        System.out.println("Throw SimpleException from f()");
        throw new SimpleException();
    }

    public static void main(String[] args) {
        InheritingExceptions sed = new InheritingExceptions();
        try{
            sed.f();
        }catch (SimpleException e){
            System.out.println("Caught it");
        }
    }
}
/*
Throw SimpleException from f()
Caught it
 */

也可以通过写入System.err而将错误发送给标准错误流,更容易被用户注意。

通常这比System.out要好,因为System.out也许被重定向

package exceptions;

class MyException extends Exception{
    public MyException(){}
    public MyException(String msg){ super(msg);} //接受字符串参数的构造器
}

public class FullConstructors {
    public static void f() throws MyException {
        System.out.println("Throwing MyException from f()");
        throw new MyException();
    }

    public static void g() throws MyException {
        System.out.println("Throwing MyException from g()");
        throw new MyException("Originate in g()");
    }

    public static void main(String[] args) {
        try {
            f();
        } catch (MyException e) {
//            e.printStackTrace(); // 默认 标准错误流,不会被重定向,容易引起用户注意
            e.printStackTrace(System.err); // 标准错误流,不会被重定向,容易引起用户注意
            e.printStackTrace(System.out); // 标准输出流,也许会被重定向
        }

        try {
            g();
        } catch (MyException e) {
            e.printStackTrace(System.out);
        }
    }
}
/*
Throwing MyException from f()
exceptions.MyException
	at exceptions.FullConstructors.f(FullConstructors.java:11) // printStackTrace 打印 从方法调用处 直到异常抛出处的方法调用序列。
	at exceptions.FullConstructors.main(FullConstructors.java:21)
Throwing MyException from g()
exceptions.MyException: Originate in g()
	at exceptions.FullConstructors.g(FullConstructors.java:16)
	at exceptions.FullConstructors.main(FullConstructors.java:28)
 */
作业1
package exceptions.e1;

public class E1 {
    public static void main(String[] args) {
        try {
            throw new Exception("An exception in E1 main");

        } catch (Exception e) {
            System.out.println(e.getMessage());
//            e.printStackTrace();
        }finally {
            System.out.println("In finally clause");
        }
    }
}
/*
An exception in E1 main
In finally clause
 */
作业2

package exceptions.e2;

public class E2 {
public static void main(String[] args) {
String s = null;
try{
s.toString();
}catch (Exception e){
e.printStackTrace();
}
}
}
/*
java.lang.NullPointerException
at exceptions.e2.E2.main(E2.java:8)
*/

作业3
package exceptions.e3;

public class E3 {

    public static void main(String[] args) {
        String[] strings = new String[5];
        for (int i = 0; i < strings.length; i++) {
            strings[i] = Integer.toString(i);
        }
        try {
            System.out.println(strings[6]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("e: " + e);
        }
    }
}
/*
e: java.lang.ArrayIndexOutOfBoundsException: 6
 */

作业4
package exceptions.e4;


class MyException extends Exception {
    String msg;

    public MyException(String msg) {
        this.msg = msg;
    }

    public void printMsg() {
        System.out.println("msg = " + msg);
    }
}

// Or take a more clever approach,
// noting that string storage and printing are
// built into Exception:
class MyException2 extends Exception {
    public MyException2(String s) {
        super(s);
    }
}

public class E4 {
    public static void main(String[] args) {
        try {
            throw new MyException("MyException message");
        } catch (MyException e) {
            e.printMsg();
        }
        try {
            throw new MyException2("MyException2 message");
        } catch (MyException2 e) {
            System.out.println(
                    "e.getMessage() = " + e.getMessage());
        }
    }
}

作业5
package exceptions.e5;

/*终止模型 Java支持
错误非常关键,一旦异常被抛出,即终止程序
 */

/*恢复模型
恢复模型:意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次可以成功
如果要用java实现类似的恢复行为,那么在遇到错误时,就不能抛出异常,
而时调用正确的方法来修正该错误,或者把try块放进一个while循环里,这样就不断进入while块,直到得到满意的结果。
 */


class ResumerException extends Exception {
}

class Resumer {
    static int count = 3;

    static void f() throws ResumerException {
        if (--count > 0)
            throw new ResumerException();
    }
}

public class E5 {
    public static void main(String[] args) {
        while (true) {
            try {
                Resumer.f();
            } catch (ResumerException e) {
                e.printStackTrace();
                continue;// 停止当前while迭代,退回循环起始处,开始下一次迭代
            }
            System.out.println("Got through...");
            break;//强行退出while循环,不执行剩余语句
        }
        System.out.println("Successful execution");
    }
}
/*
exceptions.e5.ResumerException
	at exceptions.e5.Resumer.f(E5.java:22)
	at exceptions.e5.E5.main(E5.java:30)
exceptions.e5.ResumerException
	at exceptions.e5.Resumer.f(E5.java:22)
	at exceptions.e5.E5.main(E5.java:30)
Got through...
Successful execution

 */

4.1异常与记录日志

1.利用java.util.logging工具将输出记录到日志中

package exceptions;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;

class LoggingException extends Exception{
    private static Logger logger = Logger.getLogger("LoggingException");
    //Logger.getLogger()方法创建了一个String参数相关联的Logger对象(通常是与错误相关的包名或类名)
    //这个Logger对象会将其输出发送到System.err
    public LoggingException(){
        StringWriter trace = new StringWriter();
        printStackTrace(new PrintWriter(trace));
        //printStackTrace不会产生默认字符串,为了获取字符串,我们使用重载的
        //printStackTrace()方法,它接受一个java.io.PrintWriter()对象作为参数,如果我们将一个SrtingWriter对象传递给
        //这个Printwrite()构造器,那么就可以调用toString()方法,就可以抽取为一个String
        logger.severe(trace.toString());//severe写入日志
    }

}
public class LoggingExceptions {
    public static void main(String[] args) {
        try{
            throw new LoggingException();
        }catch (LoggingException e){
            System.out.println("Caught " + e);
        }

        try{
            throw new LoggingException();
        }catch (LoggingException e){
            System.out.println("Caught " + e);
        }
    }
}
/*
十一月 19, 2019 5:45:16 下午 exceptions.LoggingException <init>
严重: exceptions.LoggingException
	at exceptions.LoggingExceptions.main(LoggingExceptions.java:24)

Caught exceptions.LoggingException
十一月 19, 2019 5:45:16 下午 exceptions.LoggingException <init>
严重: exceptions.LoggingException
	at exceptions.LoggingExceptions.main(LoggingExceptions.java:30)

Caught exceptions.LoggingException
 */

2.捕获和记录其它人编写的异常

尽管LoggingException将所有记录日志的基础设施都构建在了异常自身中,使用它非常方便.

但更常见的是我们需要捕获和记录其它人编写的异常,必须在异常处理程序中生成日志消息

package exceptions;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;

public class LoggingExceptions2 {
    private static Logger logger =
            Logger.getLogger("LoggingExceptions2");
    static void logException(Exception e) {
        StringWriter trace = new StringWriter();
        e.printStackTrace(new PrintWriter(trace));
        logger.severe(trace.toString());
    }
    public static void main(String[] args) {
        try {
            throw new NullPointerException();
        } catch(NullPointerException e) {
            logException(e);
        }
    }
}
/*
十一月 19, 2019 5:56:10 下午 exceptions.LoggingExceptions2 logException
严重: java.lang.NullPointerException
	at exceptions.LoggingExceptions2.main(LoggingExceptions2.java:17)
 */

3.自定义异常

自定义异常,比如加入额外的构造器和成员,因为我们必须在异常处理信息中生成日志信息

对异常类来说,覆盖了 Throwable的 getMessage
public String getMessage() {
return detailMessage;
}

getMessage() { //相当于toString()方法
package exceptions;

import static net.mindview.util.Print.print;

class MyException2 extends Exception {
    private int x;
    public MyException2() {}
    public MyException2(String msg) { super(msg); }
    public MyException2(String msg, int x) {
        super(msg);
        this.x = x;
    }
    public int val() { return x; }
    public String getMessage() { //相当于toString()方法
        return "Detail Message: "+ x + " "+ super.getMessage();
    }
}

public class ExtraFeatures {
    public static void f() throws MyException2 {
        print("Throwing MyException2 from f()");
        throw new MyException2();
    }
    public static void g() throws MyException2 {
        print("Throwing MyException2 from g()");
        throw new MyException2("Originated in g()");
    }
    public static void h() throws MyException2 {
        print("Throwing MyException2 from h()");
        throw new MyException2("Originated in h()", 47);
    }
    public static void main(String[] args) {
        try {
            f();
        } catch(MyException2 e) {
            e.printStackTrace(System.out);
        }
        try {
            g();
        } catch(MyException2 e) {
            e.printStackTrace(System.out);
        }
        try {
            h();
        } catch(MyException2 e) {
            e.printStackTrace(System.out);
            System.out.println("e.val() = " + e.val());
        }
    }
}
/*
Throwing MyException2 from f()
exceptions.MyException2: Detail Message: 0 null
	at exceptions.ExtraFeatures.f(ExtraFeatures.java:22)
	at exceptions.ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
exceptions.MyException2: Detail Message: 0 Originated in g()
	at exceptions.ExtraFeatures.g(ExtraFeatures.java:26)
	at exceptions.ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
exceptions.MyException2: Detail Message: 47 Originated in h()
	at exceptions.ExtraFeatures.h(ExtraFeatures.java:30)
	at exceptions.ExtraFeatures.main(ExtraFeatures.java:44)
e.val() = 47
 */

作业6
package exceptions.e6;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;

class LoggingException1 extends Exception {
    private static Logger logger = Logger.getLogger("LoggingException1");

    public LoggingException1() {
        StringWriter trace = new StringWriter();
        printStackTrace(new PrintWriter(trace));
        logger.severe(trace.toString());
    }

}

class LoggingException2 extends Exception {
    private static Logger logger = Logger.getLogger("LoggingException2");

    public LoggingException2() {
        StringWriter trace = new StringWriter();
        printStackTrace(new PrintWriter(trace));
        logger.severe(trace.toString());
    }


}

public class E6 {
    public static void main(String[] args) {
        try {
            throw new LoggingException1();
        } catch (LoggingException1 e) {
            System.out.println("Caught" + e);
        }

        try {
            throw new LoggingException2();
        } catch (LoggingException2 e) {
            System.out.println("Caught  " + e);
        }
    }
}
/*
每个异常都使用自己的logger实例,分别是LoggingException1和LoggingException2。
十一月 19, 2019 6:15:02 下午 exceptions.e6.LoggingException1 <init>
严重: exceptions.e6.LoggingException1
	at exceptions.e6.E6.main(E6.java:33)

Caughtexceptions.e6.LoggingException1
十一月 19, 2019 6:15:02 下午 exceptions.e6.LoggingException2 <init>
严重: exceptions.e6.LoggingException2
	at exceptions.e6.E6.main(E6.java:39)

Caught  exceptions.e6.LoggingException2
 */

作业7
package exceptions.e7;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;

public class E3 {
    private static Logger logger = Logger.getLogger("E3");

    static void logExcption(Exception e) {
        StringWriter trace = new StringWriter();
        e.printStackTrace();
        new PrintWriter(trace);
        logger.severe(trace.toString());
    }

    public static void main(String[] args) {
        String[] strings = new String[5];
        for (int i = 0; i < strings.length; i++) {
            strings[i] = Integer.toString(i);
        }
        try {
            System.out.println(strings[6]);
        } catch (ArrayIndexOutOfBoundsException e) {
            logExcption(e);
        }
    }
}
/*
e: java.lang.ArrayIndexOutOfBoundsException: 6
 */

5 异常说明

异常说明使用了附加的关键字 throws ,后面接一个所有潜在异常类型的列表,方便客户端程序员查看.

public static void main(String[] args) 
throws NullPointerException,NoSuchFieldError //这里就是异常说明
  {
    try {
      throw new NullPointerException();
    } catch(NullPointerException e) {
      logException(e);
    }
  }

自顶向下强制执行对异常说明机制,java在编译时候就保证一定水平对异常准确性。

要么处理这个异常,要么就在异常说明中表面此方法将产生异常。

“作弊”方法:声明方法将抛出异常,实际不抛出:为异常占个位置,以后就可以抛出异常而不用修改已有代码。在抽象基类和接口定义时这种能力很重要,派生类或接口实现就能抛出预先声明的异常。

作业7
package exceptions.e8;


class MyException extends Exception {
    public MyException(String s) {
        super(s);
    }
}

class Thrower {
//    public void f(){
//        throw new MyException("MyException in f()");
//    }
    public void g() throws MyException {
        throw new MyException("MyException in g()");
    }
}

public class E8 {
    public static void main(String[] args) {
        Thrower t = new Thrower();
        try{
            t.g();
        } catch (MyException myException2) {
            myException2.printStackTrace();
        }
    }
}
/*
exceptions.e8.MyException: MyException in g()
	at exceptions.e8.Thrower.g(E8.java:15)
	at exceptions.e8.E8.main(E8.java:23)
 */

6 捕获所有异常

1.)通过捕获异常类型的基类Exception就可以处理所有类型的异常.(事实上还有其它的基类,但Exception是同编程活动相关的基类)

2.)因为Exception是与编程有关的所有异常类的基类,所以捕获包含太多的具体的信息,不过可以调用它从其基类Throwable继承的方法

String getMessage() //详细信息
String getLocalizedMessage() //本地语言描述详细信息
String toString() //返回对Throwbale的简单描述,如有详细信息也会包含在内
void printStakTrace()//调用栈显示了"把你带到异常抛出地点"的方法调用序列,输出到标准错误
void printStackTrace(PrintStream)//调用栈显示了"把你带到异常抛出地点"的方法调用序列,输出到要你选择的要输出的流.
void printStackTrace(java.io.PrintWriter)//调用栈显示了"把你带到异常抛出地点"的方法调用序列,输出到要你选择的要输出的流.
Throwable fillInStackTrace()//用于在Throwable对象的内部记录栈帧的当前状态
/* 此外还可以用Object的方法
  getClass()   getName()  getSimpleName() 等方法
*/
package exceptions;

public class ExceptionMethods {
    public static void main(String[] args) {
        try{
            throw new Exception("My Exception");
        }catch (Exception e){
            System.out.println("Caught Exception");
            System.out.println("e.getMessage(): " + e.getMessage());
            System.out.println("e.getLocalizedMessage(): " + e.getLocalizedMessage());
            System.out.println("toString: " + e);
            System.out.println("printStackTrace: " );
            e.printStackTrace(System.out);
        }
    }
}
/*
Caught Exception
e.getMessage(): My Exception
e.getLocalizedMessage(): My Exception
toString: java.lang.Exception: My Exception
printStackTrace: 
java.lang.Exception: My Exception
	at exceptions.ExceptionMethods.main(ExceptionMethods.java:6)
 */

作业9
package exceptions.e9;

class Exception1 extends Exception {
}

class Exception2 extends Exception {
}

class Exception3 extends Exception {
}

class Thrower {
    public  void f() throws Exception1, Exception2, Exception3 {
        throw new Exception1();

        // You aren't forced to throw all the 不必强制抛出规范中所有异常。
        // exceptions in the specification.
//        throw new Exception2();
//        throw new Exception3();
    }
}

public class E9 {
    public static void main(String[] args) {
        Thrower  thrower = new Thrower();
        try {
            thrower.f();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6.1栈轨迹

printStackTrace()方法所提供的信息可以通过getStackTrace()方法直接访问.

getStackTrace()方法返回一个由根轨迹中的元素所构成的数组,

每一个元素都表示栈中的一帧,

元素0是栈顶元素,并且是调用序列中的最后一个方法调用,

数组中最后一个元素和栈底元素是调用序列中的第一个方法调用

异常对象是用 new在堆上创建的,所以垃圾回收器会自动回收

package exceptions;

public class WhoCalled {
    static void f() {
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement ste : e.getStackTrace())
//                System.out.println(ste); //直接System.out.println(ste)可以显示更详细信息
                System.out.println(ste.getMethodName());
        }
    }

    static void g() {
        f();
    }

    static void h() {
        g();
    }

    public static void main(String[] args) {
        f();
        System.out.println("---------");
        g();
        System.out.println("----------");
        h();
    }
}
/*
f
main
---------
f
g
main
----------
f
g
h
main
 */

/* //直接System.out.println(ste)可以显示更详细信息
exceptions.WhoCalled.f(WhoCalled.java:6)
exceptions.WhoCalled.main(WhoCalled.java:23)
---------
exceptions.WhoCalled.f(WhoCalled.java:6)
exceptions.WhoCalled.g(WhoCalled.java:15)
exceptions.WhoCalled.main(WhoCalled.java:25)
----------
exceptions.WhoCalled.f(WhoCalled.java:6)
exceptions.WhoCalled.g(WhoCalled.java:15)
exceptions.WhoCalled.h(WhoCalled.java:19)
exceptions.WhoCalled.main(WhoCalled.java:27)
 */

6.2重新抛出异常

一.有时希望把刚捕获的异常重新抛出,尤其时在使用Exception捕获所以异常的时候,既然已经得到了对当前异常对象的引用,可以重新把它抛出:

catch(Exception e){
System.out.println("An exception was thrown");
throw e;
}

二.
1.重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch字句将忽略.

2.异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有消息.

3.如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而非重新抛出点的信息.

想要更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的.

package exceptions;


public class Rethrowing {
    public static void f() throws Exception {
        System.out.println("originating the exception in f()");
        throw new Exception("thrown from f()");
    }

    public static void g() throws Exception {
        try {
            f();
        } catch (Exception e) {
            System.out.println("Inside g().e.printStackTrace()");
            e.printStackTrace(System.out);
            throw e;
        }
    }

    public static void h() throws Exception {
        try {
            f();
        } catch (Exception e) {
            System.out.println("Inside h().e.printStackTrace()");
            e.printStackTrace(System.out);
             throw (Exception) e.fillInStackTrace(); // 重新抛出异常
        }
    }

    public static void main(String[] args) {
        try {
            g();
        } catch (Exception e) {
            System.out.println("main: printStackTrace()");
            e.printStackTrace(System.out);
        }
        System.out.println("========");

        try {
            h();
        } catch (Exception e) {
            System.out.println("main: printStackTrace()");
            e.printStackTrace(System.out);
        }
    }
}
/*
originating the exception in f()
Inside g().e.printStackTrace()
java.lang.Exception: thrown from f()
	at exceptions.Rethrowing.f(Rethrowing.java:7)
	at exceptions.Rethrowing.g(Rethrowing.java:12)
	at exceptions.Rethrowing.main(Rethrowing.java:32)
main: printStackTrace()
java.lang.Exception: thrown from f()
	at exceptions.Rethrowing.f(Rethrowing.java:7)
	at exceptions.Rethrowing.g(Rethrowing.java:12)
	at exceptions.Rethrowing.main(Rethrowing.java:32)
========
originating the exception in f()
Inside h().e.printStackTrace()
java.lang.Exception: thrown from f()
	at exceptions.Rethrowing.f(Rethrowing.java:7)
	at exceptions.Rethrowing.h(Rethrowing.java:22)
	at exceptions.Rethrowing.main(Rethrowing.java:40)
main: printStackTrace()
java.lang.Exception: thrown from f()
	at exceptions.Rethrowing.h(Rethrowing.java:26) // 异常新发生地
	at exceptions.Rethrowing.main(Rethrowing.java:40)
 */


三. 有可能在捕获异常后抛出了另一种异常,这么做的话,得到的效果类似于使用fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息:

package exceptions;

class OneException extends Exception{
    public OneException(String s){super(s);}
}

class TwoException extends Exception{
    public TwoException(String s){super(s);}
}
public class RethrowNew {
    public static void f() throws OneException {
        System.out.println("Originating the exception in f()");
        throw new OneException("thrown from f()");
    }
    public static void main(String[] args) {
        try{
            try{
                f();
            } catch (OneException e) {
                System.out.println("Caught in inner try, e.printStackTrace()");
                e.printStackTrace(System.out);
                throw new TwoException("from inner try"); //重新抛出异常
            }
        }catch (TwoException e){
            System.out.println("Caught in outer try,e.printStackTrace()");
            e.printStackTrace(System.out);
        }
    }
}
/*
Originating the exception in f()
Caught in inner try, e.printStackTrace()
exceptions.OneException: thrown from f()
	at exceptions.RethrowNew.f(RethrowNew.java:13)
	at exceptions.RethrowNew.main(RethrowNew.java:18)
Caught in outer try,e.printStackTrace()
exceptions.TwoException: from inner try // 原来异常发生点的信息丢失,剩下是与新的抛出点有关的信息。//最后的那个异常仅知道自己来自main(),而对f()一无所知
	at exceptions.RethrowNew.main(RethrowNew.java:22)
 */

6.3异常链

  1. 常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,被称为异常链.

2.Throwable子类在构造器中可以接受一个cause(因由)对象作为参数.这个cause就是用来表示原始异常,这样通过原是异常传递给新的异常,使得即使在当前位置抛出了新的异常,也能通过异常链追踪到最初发生的位置

3.三种基本异常链:

Error(用于Java虚拟机报告系统错误)
Exception
RuntimeException

package exceptions;

import java.util.NoSuchElementException;

class DynamicFieldException extends Exception {
}

public class DynamicFields {
    private Object[][] fields;

    public DynamicFields(int initialSize) {
        fields = new Object[initialSize][2];
        for (int i = 0; i < initialSize; i++)
            fields[i] = new Object[]{null, null};
    }

    public String toString() {
        StringBuilder result = new StringBuilder();
        for (Object[] obj : fields) {
            result.append(obj[0]);
            result.append(": ");
            result.append(obj[1]);
            result.append("
");
        }
        return result.toString();
    }

    private int hasField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (id.equals(fields[i][0]))
                return i;
        return -1;
    }

    private int getFieldNumber(String id)throws NoSuchElementException{
        int fieldNum = hasField(id);
        if (fieldNum == -1)
            throw new NoSuchElementException();
        return fieldNum;
    }

    private int makeField(String id) {
        for (int i = 0; i < fields.length; i++)
            if (fields[i][0] == null) {
                fields[i][0] = id;
                return i;
            }
        // No empty fields.Add one:
        Object[][] tmp = new Object[fields.length + 1][2];
        for (int i = 0; i < fields.length; i++)
            tmp[i] = fields[i];
        for (int i = fields.length; i < tmp.length; i++)
            tmp[i] = new Object[]{null, null};
        fields = tmp;
        //递归调用扩容字段
        return makeField(id);
    }

    public Object getField(String id) throws NoSuchElementException{
        return fields[getFieldNumber(id)][1];
    }

    public Object setField(String id, Object value) throws DynamicFieldException {
        if (value == null) {
            DynamicFieldException dfe = new DynamicFieldException();
            dfe.initCause(new NullPointerException());//除了三种基本异常类,其它类型的异常应该使用initCause()方法
            throw dfe;
        }
        int fieldNumber = hasField(id);
        if (fieldNumber == -1)
            fieldNumber = makeField(id);
        Object result = null;
        try{
            result = getField(id);
        }catch (NoSuchElementException e){//原始异常传递给了新的异常
            throw new RuntimeException(e);
        }
        fields[fieldNumber][1] = value;
        return result;
    }

    public static void main(String[] args) {
        DynamicFields df = new DynamicFields(3);
        System.out.println(df);
        try{
            df.setField("d","A value for d");
            df.setField("number",47);
            df.setField("number2",48);
            System.out.println(df);
            df.setField("d","A new value for d");
            df.setField("number3",11);
            System.out.println("df: " + df);
            System.out.println("df.getField("d": " +df.getField("d"));
            System.out.println("df.getField("d": " +df.getField("5"));
//            Object field = df.setField("d",null); //Exception
        }catch (NoSuchElementException e){
            e.printStackTrace(System.out);
        }catch (DynamicFieldException e){
            e.printStackTrace(System.out);
        }
    }
}
/*
null: null
null: null
null: null

d: A value for d
number: 47
number2: 48

df: d: A new value for d
number: 47
number2: 48
number3: 11

df.getField("d": A new value for d
exceptions.DynamicFieldException
	at exceptions.DynamicFields.setField(DynamicFields.java:65)
	at exceptions.DynamicFields.main(DynamicFields.java:94)
Caused by: java.lang.NullPointerException //这里通过异常链显示了前一个异常
	at exceptions.DynamicFields.setField(DynamicFields.java:66)
	... 1 more
 */

作业10
package exceptions.e10;

class AnException extends Exception{};
class AnotherException extends Exception{};


public class E10 {
    void f() throws AnotherException {
        try {
            g();
        } catch (AnException e) {
//            e.printStackTrace();
            throw new AnotherException();
        }
    };
    void g() throws AnException {
        throw new AnException();
    };

    public static void main(String[] args) {
        E10 e10 = new E10();
        try {
            e10.f();
        } catch (AnotherException e) {
            e.printStackTrace();
        }
    }
}
/*
catch子句一旦捕捉到异常,就会进行处理,您可以对它做任何事情,包括抛出另一个异常。

exceptions.e10.AnotherException
	at exceptions.e10.E10.f(E10.java:13)
	at exceptions.e10.E10.main(E10.java:23)
 */

package exceptions.e11;


class AnException extends Exception {
};

class AnotherException extends Exception {
};


public class E11 {
    void f(){
        try {
            g();
        } catch (AnException e) {
            throw new RuntimeException(e); //RuntimeException now envelopes Exception, so we do not need a try block.
        }
    }

    void g() throws AnException {
        throw new AnException();
    }

    ;

    public static void main(String[] args) {
        E11 e10 = new E11();
//        try {
        e10.f();  //RuntimeException 不需要try块
//        } catch (AnotherException e) {
//            e.printStackTrace();
//        }
    }
}
/*
Exception in thread "main" java.lang.RuntimeException: exceptions.e11.AnException
	at exceptions.e11.E11.f(E11.java:16)
	at exceptions.e11.E11.main(E11.java:29)
Caused by: exceptions.e11.AnException
	at exceptions.e11.E11.g(E11.java:21)
	at exceptions.e11.E11.f(E11.java:14)
	... 1 more
 */

7 java标准异常

一.Throwable这个Java类被用来表示任何可以作为异常被抛出的类.Throwable对象可分为两种类型(从Throwable继承而得到的类型):

1.) Error用来表示编译时和系统错误

2.) Exception是可以被抛出的基本类型(程序员关心的通常是Exception

二.特例RuntimeException

1.) 属于运行时异常的类型有很多,它们会自动被Java虚拟机抛出,这些异常都是从RuntimeException中继承而来的.

2.) RuntimeException是 "不受检查异常" ,这种异常属于错误,将被自动捕获.

3.) RuntimeException异常也许会穿越所有的执行路径知道main()方法,而捕获被捕获,如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用printStackTrace()方法

4.) 只能在代码中忽略RuntimeException(及其子类)类型的异常,其他类型的异常处理都是由编译器强制实施的

5.)RuntimeException代表的是编程错误:

无法预料的错误.比如你从控制范围之外传递进来的null引用.
作为程序员,应该在代码中进行检查的错误.在一个地方放生异常,常常在另一个地方导致错误.

package exceptions;

public class NeverCaught {
    static void f(){
        throw new RuntimeException("From f()");
    }
    static void g(){
        f();
    }

    public static void main(String[] args) {
        g(); // 编译器不需要异常说明,其输出报告给了System.err. 直达main() 程序退出前调用异常的printStackTrace()方法。
//        代表了编程错误
    }
}
/*
Exception in thread "main" java.lang.RuntimeException: From f()
	at exceptions.NeverCaught.f(NeverCaught.java:5)
	at exceptions.NeverCaught.g(NeverCaught.java:8)
	at exceptions.NeverCaught.main(NeverCaught.java:12)
 */

8 使用finally进行清理

无论try块中的异常是否被抛出,它们都能得到执行,这通常适用于内存回收之外的情况(内存回收由垃圾回收器完成)

package exceptions;


class ThreeException extends Exception {}

public class FinallyWorks {
    static int count = 0;
    public static void main(String[] args) {
        while(true) {
            try {
                // Post-increment is zero first time:
                if(count++ == 0)
                    throw new ThreeException();
                System.out.println("No exception");
            } catch(ThreeException e) {
                System.out.println("ThreeException");
            } finally {
                System.out.println("In finally clause");//在异常被抛出是执行了一次,在System.out.println(Noexception")后又执行了一次
                if(count == 2) break; // out of "while"
            }
        }
    }
}
/*
ThreeException
In finally clause
No exception
In finally clause
 */

finally可以用来做什么

Java中使用finally一般把除内存之外的资源恢复到它们的初始状态时.这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关

异常处理机制会在跳到更高一层的异常处理程序之前,执行finall当涉及到break和continue语句时finally也会被执行

作业14
package exceptions.e14;

import exceptions.OnOffException1;
import exceptions.OnOffException2;
import exceptions.Switch;

public class E14OnOffSwitch {
    static Switch sw = new Switch();

    static void f() throws OnOffException1, OnOffException2 {
        throw new RuntimeException("Insie try");
    }

    public static void main(String[] args) {
        try {
            try {
                sw.on();
                f();

            } catch (OnOffException1 | OnOffException2 e) {
                sw.off();

            }

        } catch (RuntimeException e) {
            System.out.println(sw);
            System.out.println("Oops! the exception '"
                    + e + "' slipped through without "
                    + "turning the switch off!");
        }

    }
}
/*
on
on
Oops! the exception 'java.lang.RuntimeException: Insie try' slipped through without turning the switch off!
 */

在return中使用finally

因为finally子句总是会执行,所以在一个方法中,可以从多个点返回,并且可以保证重要清理工作仍旧会执行.从输出可以看出在fianlly类内部,从何处返回无关紧要,return语句返回之前会执行finally子句的代码块.

package exceptions;
import static net.mindview.util.Print.*;

public class MultipleReturns {
    public static void f(int i){
        print("Initialization that requires cleanup");
        try{
            print("point 1");
            if (i == 1)return;
            print("point 2");
            if (i == 2)return;
            print("point 3");
            if (i == 3)return;
            print("End");
            return;
        }finally {
            print("Performing cleanup");
        }
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 4;i++)
            f(i);
    }
}
/*
Initialization that requires cleanup
point 1
Performing cleanup
Initialization that requires cleanup
point 1
point 2
Performing cleanup
Initialization that requires cleanup
point 1
point 2
point 3
Performing cleanup
Initialization that requires cleanup
point 1
point 2
point 3
End
Performing cleanup
 */

缺憾:异常的丢失

va的异常实现也是又缺陷的,异常作为程序出错的标志决不能被忽略,但它还是可能被轻易地忽略.下了可以看到前一个异常还没处理就抛出下一个异常,没有catch捕获异常,它被finally抛出下一个异常所取代

package exceptions;

class VeryImportantException extends Exception {
   public String toString() {
       return "A very important exception!";
   }
}

class HoHumException extends Exception {
   public String toString() {
       return "A trivial exception";
   }
}

public class LostMessage {
   void f() throws VeryImportantException {
       throw new VeryImportantException();
   }
   void dispose() throws HoHumException {
       throw new HoHumException();
   }
   public static void main(String[] args) {
       try {
           LostMessage lm = new LostMessage();
           try {
               lm.f();
           } finally {
               lm.dispose();
           }
       } catch(Exception e) {
           System.out.println(e);
       }
   }
}

一种更加简单的丢失异常的方式是从finally子句中返回

package exceptions;

public class ExceptionSilencer {
   public static void main(String[] args) {
       try{
           throw new RuntimeException();
       }finally {
           // Using 'return' inside the finally block
           // will silence any thrown exception.
           return;
       }
   }
}

异常的限制

1.) 方法的重写:
在覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常

在基类和接口方法声明的异常,子类覆盖的方法可以不抛出基类和接口方法声明的异常以外的异常,但可以少或不抛出。如果冲突,就不要抛。

2.) 构造器:
在基类构造器声明的异常,在子类必须抛出,

子类的构造器可以抛出任何异常,但是必须抛出基类构造器的异常

3.) 向上转型:
子类没有向上转型为基类和接口时,可以不捕获基类和接口的异常,

反之.如有向上转型,必须捕获基类和接口的异常。编译器一定会提示你抛父类的异常。

package exceptions;

class BaseballException extends Exception {
}

class Foul extends BaseballException {
}

class Strike extends BaseballException {
}

abstract class Inning {
    public Inning() throws BaseballException {
    }

    public void event() throws BaseballException {
        // Doesn't actually have to throw anything
    }

    public abstract void atBat() throws Strike, Foul;

    public void walk() {
    } // Throws no checked exceptions
}

class StormException extends Exception {
}

class RainedOut extends StormException {
}

class PopFoul extends Foul {
}

interface Storm {
    public void event() throws RainedOut;

    public void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
    // OK to add new exceptions for constructors, but you
    // must deal with the base constructor exceptions:
    // 构造器需要继承的异常是BaseballException,也可以新增异常
    public StormyInning()
            throws RainedOut, BaseballException {
        super();////此句明确或者隐含调用;
    }

    public StormyInning(String s)
            throws Foul, BaseballException {
    }

    // Regular methods must conform to base class:
    //! void walk() throws PopFoul {} //Compile error 父类方法并没有抛出任何异常  所以编译此方法出错
    // Interface CANNOT add exceptions to existing
    // methods from the base class:  //接口不能从基类中向现有的//方法添加异常:
//     public void event() throws RainedOut {}
    // If the method doesn't already exist in the
    // base class, the exception is OK:
    public void rainHard() throws RainedOut {
    }
    // You can choose to not throw any exceptions,
    // even if the base version does:
    // storm和Inning的方法都跑出了异常,不兼容,这个方法不能抛这两个异常

    /**
     * 由于此方法类Inning与接口Strom中同时存在
     * 那么Strom中的event方法就不能改变Inning中的event异常接口
     * 同样Inning中的也不能改变Strom中的,所以最后只能是不抛出异常
     */
    public void event() {
    }

    // Overridden methods can throw inherited exceptions:
    public void atBat() throws PopFoul {
    }  //子类方法抛出比父类此方法更少的异常,或者异常的子类

    //public void atBat() throws PopFoul,R  ainedOut {}不能throws出基类方法没有的异常
    public static void main(String[] args) {
        try {
            StormyInning si = new StormyInning();
            si.atBat();
        } catch (PopFoul popFoul) {
            popFoul.printStackTrace();
        } catch (BaseballException e) {
            e.printStackTrace();
        } catch (RainedOut rainedOut) {
            rainedOut.printStackTrace();
        }

        // Strike not thrown in derived version.
        try {
            // What happens if you upcast?
            Inning i = new StormyInning();
            i.atBat();
            // You must catch the exceptions from the
            // base-class version of the method:
        } catch (Strike strike) {
            strike.printStackTrace();
        } catch (Foul foul) {
            foul.printStackTrace();
        } catch (BaseballException e) {
            e.printStackTrace();
        } catch (RainedOut rainedOut) {
            rainedOut.printStackTrace();
        }
    }
}  


package exceptions;
// Guaranteeing proper cleanup of a resource.

public class Cleanup {
    public static void main(String[] args) {
        try {
//            InputFile in = new InputFile("/Users/erin/JavaProject/thinking_in_java_example/src/main/java/exceptions/Cleanup.java");
            InputFile in = new InputFile("Cleanup.java");
            try {
                String s;
                int i = 1;
                while((s = in.getLine()) != null)
                    ; // Perform line-by-line processing here...
            } catch(Exception e) {//这里捕捉的是getLine()方法重新抛出的异常 RuntimeException
                System.out.println("Caught Exception in main");
                e.printStackTrace(System.out);
            } finally { //如果构造成功,则一定会执行in.dispose()清理
                in.dispose();
            }
        } catch(Exception e) { //InputFile对象在自己的try语句块优先,因此构造失败会进入这里,而不会执行内部的try块的in.colse()
            System.out.println("InputFile construction failed");
        }
    }
} /* Output:
dispose() successful



Could not open Cleanup.java
InputFile construction failed
*///:~

这种通用的清理惯用法在构造器不抛出任何异常时也应该应用,其基本规则时:在需要清理的对象之后,立即进入一个try-finally语句块.

//仔细考虑所有的可能细节,例如本例的dispose()如果可以抛出异常,那么就需要额外的try语句块
package exceptions;
// Each disposable object must be followed by a try-finally

class NeedsCleanup { // Construction can't fail
   private static long counter = 1;
   private final long id = counter++;
   public void dispose() {
       System.out.println("NeedsCleanup " + id + " disposed");
   }
}

class ConstructionException extends Exception {}

class NeedsCleanup2 extends NeedsCleanup {
   // Construction can fail:
   public NeedsCleanup2() throws ConstructionException {}
}

public class CleanupIdiom {
   public static void main(String[] args) {
       // Section 1:
       NeedsCleanup nc1 = new NeedsCleanup();
       try {
           // ...
       } finally {
           nc1.dispose();
       }

       // Section 2:
       // If construction cannot fail you can group objects:
       // nc5 constructor 如果对象构造不能失败就不需要任何catch
       //不能失败的对象构造器对象可以群众在一起
       NeedsCleanup nc2 = new NeedsCleanup();
       NeedsCleanup nc3 = new NeedsCleanup();
       try {
           // ...
       } finally {
           nc3.dispose(); // Reverse order of construction
           nc2.dispose();
       }

       // Section 3:
       // If construction can fail you must guard each one:
       try {
           NeedsCleanup2 nc4 = new NeedsCleanup2();
           try {
               NeedsCleanup2 nc5 = new NeedsCleanup2();
               try { //如果nc5对象构造失败则会调用try块清理,否则永不调用
                   // ...
               } finally {
                   nc5.dispose();
               }
           } catch(ConstructionException e) {
               System.out.println(e);
           } finally {
               nc4.dispose();
           }
       } catch(ConstructionException e) { // nc4 constructor
           System.out.println(e);
       }
   }
} /* Output:
NeedsCleanup 1 disposed
NeedsCleanup 3 disposed
NeedsCleanup 2 disposed
NeedsCleanup 5 disposed
NeedsCleanup 4 disposed
*///:~

10构造器

构造器抛出异常要格外注意清理方法是否有必要调用,如果方法恰当,直接向上层抛出的确能简化编程

package exceptions;

import java.io.*;

public class InputFile {
   private BufferedReader in;
   public InputFile(String fname) throws Exception {
       try {
           in = new BufferedReader(new FileReader(fname));
           // Other code that might throw exceptions
       } catch(FileNotFoundException e) {
           System.out.println("Could not open " + fname);
           // Wasn't open, so don't close it //如果没有打开文件就不需要关闭
           throw e;
       } catch(Exception e) {
           // All other exceptions must close it  如果时其它异常则必须关闭文件
           try { //in.close()也可能抛出异常,所有要放到try块里面
               in.close();
           } catch(IOException e2) {
               System.out.println("in.close() unsuccessful");
           }
           throw e; // Rethrow
       } finally {  //由于finally总会被执行,如果在这里关闭文件则文件刚打开还没开始使用就关闭了
           // Don't close it here!!!
       }
   }
   public String getLine() {
       String s;
       try {
           s = in.readLine();
       } catch(IOException e) { //这这异常已被捕获,因此getLine不会抛出任何异常
           throw new RuntimeException("readLine() failed");//重新抛出新的异常到上层环境,有时会简化编程
       }
       return s;
   }
   public void dispose() {
       try {
           in.close();
           System.out.println("dispose() successful");
       } catch(IOException e2) {
           throw new RuntimeException("in.close() failed");
       }
   }
}

11异常匹配

抛出异常的时候,异常处理系统会安照代码书写顺序找出"最近"的处理程序. 找到匹配的程序后,它就认为异常将得到清理,然后就不再继续查找.

查找的时候并不要求抛出的异常同处理程序的异常完全匹配.派生类的对象也可以配备其基类的处理程序

package chapter10Innerclasses;


class Annoyance extends Exception {}
class Sneeze extends Annoyance {}

public class Human {
  public static void main(String[] args) {
      // Catch the exact type:
      try {
          throw new Sneeze();
      } catch(Sneeze s) {
          System.out.println("Caught Sneeze");
      } catch(Annoyance a) {
          System.out.println("Caught Annoyance");
      }
      // Catch the base type:
      try {
          throw new Sneeze();
      } catch(Annoyance a) {
          System.out.println("Caught Annoyance");
      }


//        try {
//            throw new Sneeze();
//        } catch(Annoyance a) {
//        } catch(Sneeze s) { //这句编译器会报错,异常已由前面catch子句处理
//        }
  }
}
/*
Caught Sneeze
Caught Annoyance
*/

12其他可选方式

异常处理的一个原则时,只有当你在知道如何处理的情况下才捕获异常,异常处理的一个重要目标时将错误处理代码同错误发生的地点相分离.

"被检查异常"强制你在还没准备好处理错误的时候被迫加上catch子句,这就导致了吞食则有害的问题.异常被吞食了.

try{
//... to do something useful
}catch(ObligatoryException e ){}//Gulp! 异常在这里被捕捉但没有处理

12.1 异常历史

异常起源于PL/1和Mesa之类的系统中.

12.2 观点

1.) 不在于编译器是否会强制程序员去处理错误,而是要由一致的,使用异常来报告错误

2.) 不在于什么时候进行检查,而是一定要有检查.

12.3 把异常传递给控制台

Exception 是所有“被检查异常的基类,
通过把它传递到控制它这里就不必在main()写try-catch子句了.

package exceptions;

//: exceptions/MainException.java
import java.io.*;

public class MainException {
    // Pass all exceptions to the console:
    public static void main(String[] args) throws Exception {
        // Open the file:
        FileInputStream file =
                new FileInputStream("MainException.java");
        // Use the file ...
        // Close the file:
        file.close();
    }
} 
/*
Exception in thread "main" java.io.FileNotFoundException: MainException.java (No such file or directory)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at exceptions.MainException.main(MainException.java:10)
 */

12.4 把被检查的异常转换为不检查的异常

一.当我们不知道该怎么处理这个异常,但是也不想把它"吞"了,或者打印一些无用的信息,可以使用异常链的思路解决.可以直接报"被检查的异常"包装进RuntimeException里面,就像这样:

try{
//... to do something useful
} catch(IDontKnowWhatToDoWithThisCheckedException e){
throw new RuntimeException(e);
}

这种技巧给了你一种选择,你可以不写try-catch子句或异常说明,直接忽略异常,让它自己沿着调用栈往上冒泡.同时还可以用getCause()捕获并处理特定异常,就像这样

package exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

import static net.mindview.util.Print.print;

class WrapCheckedException {
    void throwRuntimeException(int type) {
        try {
            switch(type) {
                case 0: throw new FileNotFoundException();
                case 1: throw new IOException();
                case 2: throw new RuntimeException("Where am I?");
                default: return;
            }
        } catch(Exception e) { // Adapt to unchecked:
            throw new RuntimeException(e);  // 生成不同类型的异常,这些异常被捕获并包装进RuntimeException对象,它们成为运行时异常的cause
        }
    }
}

class SomeOtherException extends Exception {}

public class TurnOffChecking {
    public static void main(String[] args) {
        WrapCheckedException wce = new WrapCheckedException();
        // You can call throwRuntimeException() without a try
        // block, and let RuntimeExceptions leave the method:
        wce.throwRuntimeException(3); //没有抛出"被检查的异常",可以不用try块调用
        // Or you can choose to catch exceptions:
        for(int i = 0; i < 4; i++)
            try {
                if(i < 3)
                    wce.throwRuntimeException(i);
                else
                    throw new SomeOtherException();
            } catch(SomeOtherException e) {
                print("SomeOtherException: " + e);
            } catch(RuntimeException re) {
                try {
                    throw re.getCause(); // 被包装的原始异常抛出来,catch语句去处理
                } catch(FileNotFoundException e) {
                    print("FileNotFoundException: " + e);
                } catch(IOException e) {
                    print("IOException: " + e);
                } catch(Throwable e) {
                    print("Throwable: " + e);
                }
            }
    }
}
/*
FileNotFoundException: java.io.FileNotFoundException
IOException: java.io.IOException
Throwable: java.lang.RuntimeException: Where am I?
SomeOtherException: exceptions.SomeOtherException
 */

13异常使用指南

1.)在恰当的级别处理问题.(在知道如何处理的情况下才捕获异常)

2.)解决问题并且重新调用异常的方法

3.)进行少许的修补,然后绕过异常发生的地方继续执行

4.)用别的数据进行计算,以代替预计返回的值

5.)把当前运行环境下能做的事情尽量做完,然后把相同的异常重新抛到高层.

6.)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到高层.

7.)终止程序

8.)进行简化.(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也烦人.

9.)让类库和程序更安全,(这既是在为调试做短期投资,也使为程序的健壮性做长期投资)

10).异常不能跨线程传播,必须在当前线程处理异常

原文地址:https://www.cnblogs.com/erinchen/p/11888357.html