Java教程

Java学废之路09——异常、断言与日志

本文主要是介绍Java学废之路09——异常、断言与日志,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

异常、断言和日志

image-20210507150030989

一、背景

在程序运行期间,由于程序的错误或者环境的影响造成用户的数据丢失等,为了避免这些错误的发生,系统应该做到以下几点:

  • 向用户通告错误
  • 保存所有的工作结果
  • 允许用户以妥善的形式退出程序

异常——因为错误的输入或者网络的连接出现问题等;

断言——在测试期间,需要各种检测以验证程序操作的正确性。然而在测试程序时,这些检测将会浪费特别多的时间,为了方便程序的测试,略过各种铺垫性的检测程序,引入断言;

日志——当程序出现错误的时候,将错误的信息记录下来,方便用户的检查。

二、异常Throwable

1、处理错误

异常处理的任务是将控制权从错误产生的地方转移到能够处理这种情况的错误处理器

常见的异常情况:

  • 用户输入错误
  • 设备错误
  • 物理限制
  • 代码错误

传统的方法是返回一个特殊的错误码,由调用方法分析。但是并不所有的情况都适合返回一个错误码,在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。并且这个方法将会立即退出,调用这个方法的其他方法也将无法继续执行。取而代之的是,异常处理机制开始搜索能够处理这种异常情况的异常处理器(exception handler)。

2、异常分类

异常具有自己的语法和特定的继承结构。

在Java中,异常对象都是派生于Throwable类的一个实例。如果Java内置的异常类不能满足需求,用户还可以创建自己的异常类,该异常类要继承于Exception类。

image-20200713211730732

Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身处理,错误则无法处理。

(1)ERROR类

error类层次结构描述了Java运行时系统内部的错误和资源的耗尽错误。Error是程序无法处理的错误,大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。

(2)EXCEPTION类

exception类分两大类:运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

  • 运行时异常:RuntimeException及其子类,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过,但是运行时会报错。

    image-20200805102904623

  • 编译时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。是指编译器要求必须处置的异常。即程序在运行时由于外界因素造成的一般性异常。 编译器要求Java程序必须捕获或声明所有编译时异常。对于这类异常,如果程序不处理,可能会带来意想不到的结果。

JAVA语言规范将派生于Error类和RuntimeException类的所有异常类都称为非受查(unchecked)异常。其他的异常成为受查异常(checked)。

  • 受查异常:必须要捕获的异常,即强制要求开发人员在代码中进行显式的声明(throw)和捕获(try/catch),否则无法编译通过
  • 非受查异常:如果不捕获异常,不会出现编译错误,而该异常会在运行时打印日志

(3)自定义异常

常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。

在使用时,使用throw抛出。

BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生,并提供多个构造方法:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}

//其他业务类型的异常从BaseException再继续派生

注:RuntimeException这个可以抛出异常,并准确定位,缺点是不能处理这个异常,自定义异常的话可以捕获并且处理,如可以使用 throw new GuliException(10000,”不存在”),创建一个自定义异常对象,交由自定义异常类来进行处理。

在接口开发的过程中,为了程序的健壮性,我们经常考虑到代码执行的异常,并给前端一个友好的展示,这里就用到自定义异常,继承RuntimeException类。这个RuntimeException和普通的Exception有什么区别呢?

(1)Exception:非运行时异常,在项目运行之前必须处理掉。一般由程序员 try…catch掉。

(2)RuntimeException:运行时异常,在项目运行之后出错则直接终止运行,异常由JVM虚拟机处理。

在接口的逻辑判断出现异常时,可能会影响后面的代码,或者说绝不允许该代码块出错,那么我们就用RuntimeException,但是我们又需要在系统挂掉后,返回给前端出现异常的原因。因此接口的自定义异常作用就体现了出来。

3、异常声明——throws

根据异常规范,在方法的首部必须声明该方法可能抛出的所有异常。每个异常类之间用逗号分开。但是不需要声明Java的内部错误,即从error继承的错误,也不能声明从RuntimeException继承的那些非受查异常。

一个方法必须声明所有可能抛出的受查异常(IOException),而非受查异常要么不可控制(error),要么就应该避免发生(RuntimeException)。

如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用,即子类中的可以抛出更特定的异常,或者不用抛出任何异常。但是若超类方法没有抛出任何受查异常,子类也不能抛出任何异常。JAVA中的throws说明符与C++中的throw说明符基本类似。

区别:

  • 在C++中,throw说明符在运行时执行,而不是在编译时执行,即C++编译器将不处理任何异常规范;

    在Java中,如果方法没有声明所有可能发生发生的受查异常,编译器将会发出一个错误消息;

  • 在C++中,如果没有给出throw声明,函数可能会抛出任何异常;

    在Java中,没有throw说明符的方法将不能抛出任何受查异常;

  • 在C++中,抛出的可以是任何类型的值;

    在Java中,只能抛出Throwable子类的对象;

(1)异常的传播

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch被捕获为止:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        process2();
    }

    static void process2() {
        Integer.parseInt(null); // parseInt()会抛出NumberFormatException
    }
}

(2)如何抛出异常

1)找到一个合适的异常类,也可自定义派生于Exception的异常类

2)创建这个类的一个对象

3)用throw语句将对象抛出(被调用的方法一般已经实现了throw异常)如文件读取时的异常:

public static void main(String[] args) { 
    String s = "abc"; 
    if(s.equals("abc")) { 
      	throw new NumberFormatException(); 	// 使用throw抛出异常
    } else { 
      	System.out.println(s); 
    } 
    //function(); 
}

4)使用throws在方法头声明异常,也可实现对异常的抛出,将异常交由上层处理函数处理。

image-20201117205914007

(3)转换异常

如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
            throw new IllegalArgumentException();
        }
    }

    static void process2() {
        throw new NullPointerException();
    }
}

当process2()抛出NullPointerException后,被process1()捕获,然后抛出IllegalArgumentException(),然后在main()中捕获的是IllegalArgumentException异常。这说明新的异常丢掉了原始的异常,即实现了”转换“异常。

若要跟踪到原始的异常,则需要在抛出新的异常的时候,把原始异常的对象存入到新异常中:

static void process1() {
    try {
        process2();
    } catch (NullPointerException e) {
        throw new IllegalArgumentException(e);  //保存原始异常对象e
    }
}

(4)throws与throw

  • throws出现在方法函数头,用于声明异常;而throw出现在函数体,用于抛出异常
  • throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,当throw被执行时则代表一定是发生了某种异常,并抛出了该异常对象
  • 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层错误处理器处理

4、捕获异常——try/catch块

如果某个异常发生的时候没有任何地方进行捕获,那程序就会终止执行,并在控制台打印异常信息;

要想捕获一个异常,必须设置try/catch语句块;

若在try语句块中任何代码抛出了一个在catch子句说明的异常类,那么

1)程序将跳过try语句块的其他代码

2)程序将执行catch子句中的处理器代码

3)如果抛出了一个在catch子句中没有声明的异常类型,那么该方法将立即退出,并提示用户补充代码

若没有抛出任何异常,将跳过catch子句。

被检测的部分必须放在try块中;try与catch作为一个整体出现,catch必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句;一个try-catch结构中只能有一个try块,但是可以有多个catch块,以便于不同的异常信息匹配。

(1)多catch语句

可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。

存在多个catch的时候,由于其中的异常类是继承关系,所以catch的顺序非常重要:子类必须写在前面

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕获不到,因为该类是IO的子类
        System.out.println("Bad encoding");
    }
}

(2)finally子句

当代码抛出一个异常后,就会终止方法中剩余代码的处理,并退出这个方法的执行。finally是用来保证一些代码必须执行的。

finally子句:不管是否有异常被捕获,finally子句中的代码都将被执行,为程序提供了一个统一的出口。

  • finally语句不是必须的,可写可不写
  • finally总是最后执行
// 输出-1
try {
    int a = 5/0;
} catch (Exception e) {
    return 1;
} finally {
    return -1;
}

如果没有发生异常,就正常执行try { … }语句块,然后执行finally。如果发生了异常,就中断执行try { … }语句块,然后跳转执行匹配的catch语句块,最后执行finally。

在C++中,在catch中使用删节号(···)来捕获任何类型的异常信息,这段处理程序必须是try-catch块的最后一段处理程序。

(3)捕获多种异常

如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条catch子句。

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("Bad input");
    } catch (NumberFormatException e) {
        System.out.println("Bad input");
    } catch (Exception e) {
        System.out.println("Unknown error");
    }
}
//===============================================================================
//IOException是受查异常,NumberFormatException是RuntimeException非受查异常
//但是处理IOException和NumberFormatException的代码是相同的,所以我们可以把它两用|合并到一起:
public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException | NumberFormatException e) { 
        // IOException或NumberFormatException
        System.out.println("Bad input");
    } catch (Exception e) {
        System.out.println("Unknown error");
    }
}

(4)堆栈轨迹分析

堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

5、小结

异常处理的两种方法:

  • 使用try-catch在程序内部处理异常
  • 抛出异常,交由上层处理函数处理
    • 使用throws在函数头声明异常
    • 在程序内部使用throw关键字向上抛出异常
/**
 * 使用try-catch处理异常
 * @author Administrator
 */
public class Test02 {
	public static void main(String[] args) {
		readMyFile();
	}
	//使用try-catch块包裹起来由方法自己处理异常
	public  static  void  readMyFile(){
		FileReader  reader = null;
		try{
			reader = new FileReader("d:/b.txt");
			System.out.println("step1");
			char  c1 = (char)reader.read();
			System.out.println(c1);			
		}catch(FileNotFoundException e){		//子类异常在父类异常前面
			System.out.println("step2");
			e.printStackTrace();
		} catch (IOException e) {
            throw new MyException(e); 	//使用throw向上抛出自定义异常
			e.printStackTrace();
		}finally{
			System.out.println("step3");
			try {
				if(reader!=null){
					reader.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}	
}
/**
 * 使用throws声明异常
 * @author Administrator
 */
public class Test03 {
    //主方法可以选择处理异常,或者继续抛出异常交给JRE处理
	public static void main(String[] args) throws IOException { 
		readMyFile();
	}
	//子方法自己不处理异常,而是抛出异常
	public static void readMyFile() throws IOException { 
		FileReader reader = null;
		reader = new FileReader("d:/b.txt");
		System.out.println("step1");
		char c1 = (char) reader.read();
		System.out.println(c1);

		if (reader != null) {
			reader.close();
		}
	}
}

image-20200714113852621

三、断言Assert

假设确信某个属性符合要求,并且代码的执行依赖于这个属性。比如一个函数的参数要求是非负数,而该参数是另一个函数返回的结果,并且该结果可以保证是非负的,但是程序还是希望去检查该参数的合法性,所以可以写一个if判断语句,并抛出一个异常;但是若在程序存在大量的此类检查,程序运行起来将变得相当慢。

断言机制允许在测试期间向代码中插入一些检查语句,当代码发布时,这些插入的检测语句将会被自动移走。

断言是一种软件调试的方法,提供了一种在代码中进行正确性检查的机制,目前很多开发语言都支持这种机制。它的主要作用是对一个 boolean 表达式进行检查,一个正确运行的程序必须保证这个boolean表达式的值为 true,若 boolean 表达式的值为 false ,则说明程序已经 处于一种不正确的状态下,系统需要提供告警信息并且退出程序。在实际开发中,断言主要用来保证程序的正确性,通常在程序开发与测试中使用。

断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。

image-20200714140146647

注:断言很少使用,更好的方法是编写单元测试

四、日志Logging

1、背景

在编写程序的过程中,发现程序运行结果与预期不符,怎么办?当然是用System.out.println()打印出执行过程中的某些变量,观察每一步的结果与代码逻辑是否符合,然后有针对性地修改代码。

代码改好了怎么办?当然是删除没有用的System.out.println()语句了。

如果改代码又改出问题怎么办?再加上System.out.println()

反复这么搞几次,很快大家就发现使用System.out.println()非常麻烦。

怎么办?解决方法是使用日志

2、日志Logging

  • 日志就是Logging,它的目的是为了取代System.out.println()
  • 可以设置输出样式
  • 可以设置输出级别,禁止某些级别输出
  • 可以被重定向到文件
  • 可以按包名控制日志

3、JDK Logging

image-20200714141027188

默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。

image-20200714141150547

因此,Java标准库内置的Logging使用并不是非常广泛。

4、更方便的日志系统

(1)Commons Logging

和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。

Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Logging自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。

image-20200714141731172

(2)Log4j

Log4j是一个组件化设计的日志系统,它的架构大致如下:

image-20200714143049697

当我们使用Log4j输出一条日志时,Log4j自动通过不同的Appender把同一条日志输出到不同的目的地。例如:

  • console:输出到屏幕;
  • file:输出到文件;
  • socket:通过网络输出到远程计算机;
  • jdbc:输出到数据库

在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件来配置它(添加依赖+填写配置信息)。

image-20200714143315619

这篇关于Java学废之路09——异常、断言与日志的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!