【java学习】系统错误处理机制:异常(Exception)、断言(assert)和日志(log)

java语言中,给出了3种处理系统错误的机制:抛出一个异常、日志、使用断言。

1,异常

1)概念

当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。

处理异常的一个重要原则是"只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋向于减少。
——《Think in java》

2)好的编码习惯

  1. 尽量不要捕获Exception 这样的通用异常,而是应该捕获特定异常。
    首先,泛泛的 Exception 之类,隐藏了编码者的特定目的。其次,你可能更希望 RuntimeException 被扩散出来,而不是被捕获。最后进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正确程序处理 OutOfMemoryError。
  2. 不要生吞(swallow)异常。
    基于假设我们可能判断某段代码可能不会发生,或者感觉忽略异常是无所谓的,直接吞掉异常不让其抛出来也不让它打印。程序可能在后续代码以不可控的方式结束,或者产生很诡异的问题。

2)场景

违反语义规则包括2种情况:
①JAVA类库内置的语义检查
例如数组下标越界,会引发IndexOutOfBoundsException;访问null的对象时会引发NullPointerException。
②JAVA允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用throw关键字引发异常。
所有的异常都是java.lang.Thowable的子类。

3)Throwable

异常

4)Error与Exception区别

  1. 都是Throwable的子类。

①Error

表示系统级的错误和程序不能处理的异常,表示故障发生于JVM,只有退出才可以处理。

举例:

  1. ThreadDeath、
  2. Java.lang.OutOfMemoryError(内存空间不足)
  3. java.lang.StackOverFlowError(方法调用栈溢出)
    通常「堆栈溢出」是指「调用堆栈(call stack)的溢出」。程序中一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过默认大小而导致溢出。
    栈溢出的原因:
    递归调用层次太多
    大量循环或死循环
    全局变量过多
    数组、List、map数据过大
系统崩溃
虚拟机错误

②Exception

Exception类表示程序可以处理的异常,可以捕获且可能恢复。一般是程序设计的不完善导致,分为RuntimeException和其它异常。

5)检查型异常和非检查型异常

①检查异常(Checked)

i>概念

编译器要求必须处理的异常。
如果不处理这类异常,程序将来肯定会出错。当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

ii>包括:非运行时异常

即除了Error,RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。

注意:.try{}catch{}会增加额外的开销。

iii>异常链

是Java中非常流行的异常处理概念,是指在进行一个异常处理时抛出了另外一个异常,由此产生了一个异常链条。
该技术大多用于将“ 受检查异常” ( checked exception)封装成为“非受检查异常”(unchecked exception)或者RuntimeException。顺便说一下,如果因为因为异常你决定抛出一个新的异常,你一定要包含原有的异常,这样,处理 程序才可以通过getCause()和initCause()方法来访问异常最终的根源。

②非检查型异常(Unchecked Exception)

编译器不要求处理的异常。

包括:运行时异常(RuntimeException与其子类)和错误(Error)。

例如:你的程序逻辑本身有问题,比如数组越界、访问null对象,这种错误你自己是可以避免的。编译器不会强制你检查这种异常。

6)Excetion分类

①系统异常和普通异常

系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件死掉;
普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。

②运行时异常和非运行时异常,如下

i>RuntimeException(运行时异常)

运行时异常都是RuntimeException类及其子类。
这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。一般是由程序逻辑错误引起的, 如果语法没有错误他不会在编译时候报异常,只有运行的时候才会抛出异常!一但出错,后面程序将无法继续运行!

举例:

ArithmeticException

由于除数为0引起的异常;

ArrayStoreException

由于数组存储空间不够引起的异常;

ClassCastException

当把一个对象归为某个类,但实际上此对象并不是由这个类创建的,也不是其子类创建的,则会引起异常;

IllegalMonitorStateException

监控器状态出错引起的异常;

NegativeArraySizeException

数组长度是负数,则产生异常;

NullPointerException

程序试图访问一个空的数组中的元素或访问空的对象中的 方法或变量时产生异常;

导致空指针异常的原因:
对象未初始化而直接引用对象值或者方法;
对象引用已经不存在或者被关闭;
违反某些Java容器的限制,读写Null 值。
Java8使用Optional包装null值来避免NullPointerException。

OutofMemoryException

用new语句创建对象时,如系统无法为其分配内存空 间则产生异常;

SecurityException

由于访问了不应访问的指针,使安全性出问题而引起异常;

IndexOutOfBoundsExcention

由于数组下标越界或字符串访问越界引起异常;

IOException

由于文件未找到、未打开或者I/O操作不能进行而引起异常;

ClassNotFoundException

未找到指定名字的类或接口引起异常;

CloneNotSupportedException

程序中的一个对象引用Object类的clone方法,但 此对象并没有连接Cloneable接口,从而引起异常;

InterruptedException

当一个线程处于等待状态时,另一个线程中断此线程,从 而引起异常;

被打断不会导致InterruptedException的行为:
Thread.join、Thread.sleep、Object.wait、CyclicBarrier.await;

NoSuchMethodException

所调用的方法未找到,引起异常;

Illega1AccessExcePtion

试图访问一个非public方法;

StringIndexOutOfBoundsException

访问字符串序号越界,引起异常;

ArrayIdexOutOfBoundsException

访问数组元素下标越界,引起异常;

NumberFormatException

字符的UTF代码数据格式有错引起异常;

IllegalThreadException

线程调用某个方法而所处状态不适当,引起异常;

FileNotFoundException

未找到指定文件引起异常;

EOFException

未完成输入操作即遇文件结束引起异常

ii>非运行时异常

7)throw(句子抛出具体异常,需要处理:try-catch捕获、throws抛出)

throw不仅仅可以抛出异常,还可以抛出Error以及Throwable.

public class ThisDemo06{
    public static void main(String args[]){
        try{
            throw new Exception("test") ;    // 抛出异常的实例化对象
        }catch(Exception e){
            System.out.println(e) ;
        }
    }
};

8)throws(方法抛异常,并将异常声明并上传给方法调用者处理)

使用throws关键字声明的方法表示此方法不处理异常,而交给方法调用处进行处理。

  public int div(int i,int j) throws Exception{    // 定义除法操作,如果有异常,则交给被调用处处理
        int temp = i / j ;    // 计算,但是此处有可能出现异常
        return temp ;
    }

9)finally

①功能

定义一定执行的代码。通常用于关闭资源。如:数据库关闭。

②前面return,finally依然执行

try、catch块中有return语、throw语句,这两个语句都会导致该方法立即结束,系统并不会立即执行这两个语句,而是去寻找该异常处理流程中的finally块。
i>如果没有finally块,程序立即执行return语句或者throw语句,方法终止。
ii>如果有 finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的 return或throw语句
iii>如果finally块里也使用了return或throw等导致方法终止的语句,则finally块已经终止了方法,不用再跳回去执行try块、catch块里的任何代码了。

	try{ 
              throw new Exception(); 
       }catch(Exception e){ 
           return ; 
       }finally{ 
           output = "3"; //即使前面return了,这也一定执行
       }
   try
    {
        return true;
    }
    catch (Exception e)
    {
 
    }
    finally
    {
        return false;//最后结果返回false,不会返回true
    }

③finally唯一不执行的情况:System.exit(1);

在try块或者catch块中调用了退出虚拟机的方法(即System.exit(1);),finally不会被执行。
除此之外,都会执行。

10)异常-crash上传到服务器

通过设置CrashHandler来监视应用的crash信息。

当程序crash时会调用CrashHandler的uncaughtException方法。在这个方法中获取crash信息并上传到服务器。

2,断言

1)概念

JAVA是从JDK1.4才开始支持断言(添加了关键字assert)。

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。
在代码的测试过程中,如果加入异常,测试完毕也不会自动地删除代码,影响程序效率。而断言仅用于开发和测试阶段,代码发布时,这些插入的检测语句会被自动移走。
注意:断言失败是致命的、不可恢复的错误。

2)优缺点

使用断言可以创建更稳定,品质更好且易于除错的代码。

3)场景

可以将断言看作是异常处理的一种高级形式。
①类型检查
如当需要在一个值为FALSE时中断当前操作的话,可以使用断言。
②单元测试
单元测试必须使用断言(Junit/JunitX)。
③确定个种特性是否在程序中得到维护

可以用断言的地方:
①可以在预计正常情况下程序不会到达的地方放置断言 :assert false
②断言可以用于检查传递给私有方法的参数。(对于公有方法,因为是提供给外部的接口,所以必须在方法中有相应的参数检验才能保证代码的健壮性)
③使用断言测试方法执行的前置条件和后置条件
④使用断言检查类的不变状态,确保任何情况下,某个变量的状态必须满足。(如age属性应大于0小于某个合适值)

不可以用断言的地方:
1.不要使用断言作为公共方法的参数检查,公共方法的参数永远都要执行
2.断言语句不可以有任何边界效应,不要使用断言语句去修改变量和改变方法的返回值

4)使用

断言表示为一些 【布尔表达式】,程序员相信在程序中的某个特定点该表达式值为真
可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新起用断言。

①两种形式

  1.assert Expression1
  2.assert Expression1:Expression2

Expression1:总是一个布尔值
Expression2:断言失败时输出的失败消息的字符串。

如果Expression1为假,则抛出一个 AssertionError,AssertionError由于是错误,可以不捕获,但不推荐这样做,因为那样会使你的系统进入不稳定状态。

②启用断言

断言在默认情况下是关闭的,要在编译时启用断言。

eclipse启用断言

选择菜单:Run —> Run Configurations…—> 选择 Arguments 选项卡
在 VM arguments 文本框中输入: -ea
注意:中间没有空格,如果输入 -da 表示禁止断言。

IDEA启用断言

选择菜单:Run —> Edit Configurations…
在这里插入图片描述

命令行启用断言

使用source1.4标记,即javac source1.4 Test.java
在运行时启用断言需要使用 -ea参数 。要在系统类中启用和禁用断言可以使用 -esa 和 -dsa参数。

区别
      int x=10;
      System.out.println("Testing Assertion that x==100");
      assert x==100:"Out assertion failed!";
      System.out.println("Test passed!");

启用断言时,输出:

Testing Assertion that x==100
Exception in thread "main" java.lang.AssertionError: Out assertion failed!
	at test.test.Main.main(Main.java:9)

未启用断言时,输出:

    Testing Assertion that x==100
    Test passed

③断言的副作用

由于程序员的问题,断言的使用可能会带来副作用 ,例如:

  boolean isEnable=false;
  //...
  assert isEnable=true;

这个断言的副作用是因为它修改了程序中变量的值并且未抛出错误,这样的错误如果不细心的检查是很难发现的。但是同时我们可以根据以上的副作用得到一个有用的特性,根据它来测试断言是否打开。

 boolean isEnable=false;
     //...
     assert isEnable=true;
     if(isEnable==false){
       throw new RuntimeException("Assertion shoule be enable!");
     }

④文档注释和断言

如下,用注释说明假设条件。

if( x != 2 ) { 
……
}else{ //(x==2)
}

此处用断言可能更好一点:


if( x != 2 ) { 
……
}else{
     assert x == 2;
}

5)分类

前置条件断言:代码执行之前必须具备的特性
后置条件断言:代码执行之后必须具备的特性
前后不变断言:代码执行前后不能变化的特性

3,日志

1)概念

Java中关于日志系统的API,在 java.util.logging 包中,在这个包中,Logger类很重要。
相比System.out.println()方法输出,log更加灵活。

2)优点

①可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
②可以很简单地禁止日志记录的输出,因此将日志代码留在程序中的开销很小。
③日志记录可以被定向到不同的处理器,可以用于在控制台显示,也可以用于存储在文件中。
④日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
⑤日志记录可以采用不同的方式格式化,例如:纯文本或xml
⑥应用程序可以使用多个日志记录器,他们使用类似包名的这种具有层次结构的名字,如:com.mycompany.myapp
⑦在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。

3)使用

①Logger.global(全局日志记录器、默认日志记录器)

i>通过调用info方法记录日志信息:

Logger.getGlobal().info("File->Open menu");

输出内容为:(自动包含时间、调用的类和方法名)

十月 09, 2018 10:16:48 上午 test.test.Main main
信息: File->Open menu

ii>取消所有日志:(此句之后所有的日志不再有效)

Logger.getGlobal().setLevel(Level.OFF);

②自定义日志记录器

日志记录器名有层次结构(与包名类似),但日志记录器的父子之间共享某些属性:子继承父级别。

i>日志级别

Logger类是用来记录 某个级别的日志消息,级别越高,输出信息越少。
7个日志记录级别如下:(从上倒下,级别依次下降,可以查看java.util.logging.Level类))

编号级别说明
1SEVERE(严重)级别最高
2WARNING(警告)
3INFO
4CONFIG
5FINE
6FINER
7FINEST最低值
其他级别:
级别说明
--------
OFF关闭日志
ALL启用所有消息的日志记录

默认只记录前三个类别,要显示更低级别需要修改日志处理器配置。因此用后4个级别记录那些有助于诊断,但对于程序员又没有太大意义的调试信息。

ii>创建或检索记录器
    private static final Logger myLogger = Logger.getLogger("包名");

    public static void main(String[] args) {

        myLogger.severe("abc");
        myLogger.warning("abc");
        myLogger.info("abc");
        myLogger.config("abc");
        myLogger.fine("abc");
        myLogger.finer("abc");
        myLogger.finest("abc");
    }

输出:

十月 09, 2018 10:38:16 上午 test.test.Main main
严重: abc
十月 09, 2018 10:38:16 上午 test.test.Main main
警告: abc
十月 09, 2018 10:38:16 上午 test.test.Main main
信息: abc

③将日志输出到文件

        //日志记录器
        Logger logger = Logger.getLogger("chapter07");
        //日志处理器
        FileHandler fileHandler = new FileHandler("/Users/lwh/Desktop/test.txt");//mac路径。 windows: ("d:\\test.txt"); 

        //需要记录的日志消息
        LogRecord lr = new LogRecord(Level.INFO, "This is a text log.");

        //为处理器设置日志格式:Formatter
        SimpleFormatter sf = new SimpleFormatter();

        fileHandler.setFormatter(sf);
        //注册处理器
        logger.addHandler(fileHandler);
        //记录日志消息
        logger.log(lr);

如果把 SimpleFormatter 改成 XmlFormatter,记录的消息是xml形式。

4)原理

Logger记录的日志消息会被转发到已注册的Handler对象,handler对象可以将消息发送到:控制台,文件,网络等等。

①Handler类(抽象类):转发日志消息

Hanlder类下有2个子类:MemoryHandler、StreamHandler。
StreamHandler下有3个子类:ConsoleHandler(将日志消息打印到控制台)、FileHandler(将日志消息输出到文件)、SocketHandler(将日志发送到网络中的某个主机)。具体详情,查看API文档。

②Formatter类(抽象类):于格式化日志记录消息

有2个子类:SimpleFormatter(纯文本形式), XmlFormatter(XML形式)

5)常用日志框架

① Bugly

腾讯推出的一款免费崩溃日志收集统计sdk,可直接通过gradle依赖。
使用很简单,是目前国内免费软件中功能最强大的一款。

② Log4j

Apache Log4j是一个基于Java的日志记录工具。它是由Ceki Gülcü首创的,现在则是Apache软件基金会的一个项目。

③ Log4j 2

Apache Log4j 2是apache开发的一款Log4j的升级产品。

④ Commons Logging

Apache基金会所属的项目,是一套Java日志接口,之前叫Jakarta Commons Logging,后更名为Commons Logging。

⑤ Slf4j

类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。

⑥ Logback

一套日志组件的实现(slf4j阵营)。

⑦ Jul (Java Util Logging)

自Java1.4以来的官方日志实现。

6)用到的设计模式

外观模式、适配器模式。

4,常用注解

注解作用域说明
@ExceptionHandler方法捕获异常信息并交给开发者处理
@ControllerAdvice全局异常处理;全局数据绑定;全局数据预处理

1)文件上传大小限制Demo

@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public void uploadException(MaxUploadSizeExceededException e, HttpServletResponse resp) throws IOException {
        resp.setContentType("text/html;charset=utf-8");
        System.out.println(1111);
        PrintWriter out = resp.getWriter();
        out.write("上传文件大小超出限制!");
        out.flush();
        out.close();
    }
}

2)统一异常处理

统一异常处理类:

@ControllerAdvice
public class BaseController {
 
    @ExceptionHandler(value = Exception.class)
    public String handlerException(Exception e){
        if(e instanceof SQLException){
            return "sqlError";
        }else if(e instanceof MyException){
            return "myError";
        }else{
            return "noError";
        }
    }
 
}

5,其他问题

1)方法数越界

问题:
Android的限制:一个应用的方法数不能超过65536,否则会出现编译错误,程序无法成功安装到手机上。

解决方案一:multidex方案
将一个dex文件拆分为多个dex文件来避免单个dex文件方法数越界的问题。

解决方案二:动态加载。
将部分代码打包到一个单独的dex文件中,并在程序运行时根据需要动态加载dex。此方案实现了程序按需加载,同时提供了应用按模块更新的可能性。

2)慎用异常

异常对性能不利,抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为filllnStackTrace()的本地同步方法,filllnStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只要用于错误处理,不应该用来控制程序流程。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值