JAVA基础之异常
异常
一、异常概念
1.1、异常
异常,通俗点来说就是不正常的意思。
在生活中:医生说,你的身体某个部位有些异常,该部位和正常相比有点不同,该部位的功能将受影响。
在程序中:程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
1.2、异常引发后果
如果说程序因为错误而到了JVM停止运行,那么就会导致整个应用程序停止,从而无法对外停止服务。那么这种是属于生产事故。
1.3、异常解决
程序导致了错误,导致了JVM停止,那么有没有办法做到:程序即使出现了错误,也不应该让程序停止呢?
这个时候应该提出异常捕获的概念:为了保证程序的健壮性,就算了是程序出现了异常,那么也不应该让JVM停止运行。
出现了异常之后,需要注意到异常是否是在我们的可控范围之内?
那么什么是可控范围之内?首先可以肯定的是:程序在运行时我们不敢保证一定不会出现问题,而编译异常我们可以来解决。
而运行时异常不是可控的吗?说起来这个,也有一定的可控范围。比如说常见的NPE,这个是因为程序代码的逻辑性不够严谨。
除了让其暴露出来之外,我们还有另外的方式来处理,比如说检查出来了是NPE,那么重新赋值,而不是说直接让程序终止运行。
再比如说,我们可以在捕捉到异常之后,在异常逻辑中写对应的代码。不一定是一些回滚、删除之类的逻辑。也就是说,第一种情况不合适,那么用第二种方式来试试;第二种不合适,那么使用第三种来试试。
但是,如果出现了运行时异常,而没有来进行处理,导致main方法提交异常给JVM来处理,而JVM无法处理,将会停止JVM运行,所以这种情况不要让其发生,尤其是在生产上。
1.4、异常体系
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。
异常机制其实是帮助我们定位、排查程序中的问题,既然异常是个类,那么来看下对应的体系结构
异常的根类是Java.lang.Throwable
,其下有两个子类:Java.lang.Error
与Java.lang.Exception
,平常所说的异常指Java.lang.Exception
。
Throwable类
- Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
- Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎等症状。
Throwable中的属性
detailMessage:这个是极其重要的属性,这个值就是e.getMessage()返回的值;
那么这个属性会在什么时候赋值呢,追溯源码发现,该属性只会在Throwable构造函数中赋值。
public Throwable() {
// 在默认构造函数中不会给detailMessage属性赋值
fillInStackTrace();
}
public Throwable(String message) {
fillInStackTrace();
// 直接将参数赋值给detailMessage
detailMessage = message;
}
public Throwable(String message, Throwable cause) {
fillInStackTrace();
// 直接将参数赋值给detailMessage
detailMessage = message;
this.cause = cause;
}
public Throwable(Throwable cause) {
fillInStackTrace();
// 当传入的Throwable对象不为空时,为detailMessage赋值
detailMessage = (cause==null ? null : cause.toString());
this.cause = cause;
}
protected Throwable(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
if (writableStackTrace) {
fillInStackTrace();
} else {
stackTrace = null;
}
// 直接将参数赋值给detailMessage
detailMessage = message;
this.cause = cause;
if (!enableSuppression)
suppressedExceptions = null;
}
显然,从源码中可以看到在Throwable的默认构造函数中是不会给detailMessage属性赋值的。
也就是说,当异常对象是通过默认构造函数实例化的,或者实例化时传入的message为空字符串,那么调用getMessage()方法时返回值就为空。所以,在程序日志中不要单纯使用getMessage()方法获取异常信息(返回值为空时,不利于问题排查)。
Throwable常用方法
-
public void printStackTrace()
:打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
-
public String getMessage()
:获取发生异常的原因。提示给用户的时候,就提示错误原因。
-
public String toString()
:获取异常的类型和异常描述信息(不用)。
写个程序来测试一个三个方法:
try {
int i = 1 /0;
}catch (Exception e){
e.printStackTrace();
}
对应的打印信息:可以看到哪个方法中的哪行出现了什么异常,对应的异常信息是什么
Java.lang.ArithmeticException: / by zero
at com.guang.exception.TestException.main(TestException.Java:9)
try {
int i = 1 /0;
}catch (Exception e){
System.out.println(e.getMessage());
}
对应的打印信息:
/ by zero
try {
int i = 1 /0;
}catch (Exception e){
System.out.println(e);
}
输出信息:
Java.lang.ArithmeticException: / by zero
小结
-
1、根据异常信息来定位问题的时候,需要根据顺序来查找具体的问题:因为方法是在栈中进行操作的,所以是先进后出结构,打印的顺序也是这个结构,报错第一行先打印出来肯定是最先报错的地方,最下面一方是调用方法出现的问题,这里是根本方法。如果是方法中调用方法,这种排查方式是最佳选择方式。
-
2、异常提供的方法中,肯定是e.printStackTrace()这个方法最能够满足我们的需求,所以使用这个是最佳推荐
1.5、异常分类
我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。
异常(Exception)的分类:根据在编译时期还是运行时期去检查异常
- 编译时期异常 :checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
- 运行时期异常 :runtime异常。在运行时期,检查异常.在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
二、异常处理
Java为了保证程序中出现了异常,因为需要对异常进行进行处理,所以Java也提供的了对应的处理机制。
Java异常处理的五个关键字:try、catch、finally、throw、throws
2.1、抛出异常throw
在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。
在Java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?
-
创建一个异常对象。封装一些提示信息(信息可以自己编写)。
-
需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象。
throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
例如:
第一种:
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
第二种:
try{
int i = 1 / 0;
processService();
}catch(Exception e){
logger.error("当前程序处理过程中出现异常");
// 将异常丢给调用者来进行处理
throw e;
}
注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。
那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。
2.2、声明异常throws
声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理,那么必须通过throws进行声明,让调用者去处理。如果调用者也不去进行处理,那么异常最终将会抛给main方法,main线程中一旦出现了异常,将会抛给JVM,最终导致当前的JVM工作系统奔溃。
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
声明异常的代码演示:
public class ThrowsDemo {
// main方法也不来进行处理,如果出现问题将会导致JVM停止运行
public static void main(String[] args) throws FileNotFoundException {
read("a.txt");
}
// 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
public class ThrowsDemo2 {
public static void main(String[] args) throws IOException {
read("a.txt");
}
public static void read(String path)throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
throw new IOException();
}
}
}
2.4、捕获异常try…catch
如果异常出现的话,会立刻终止程序。所以我们得处理异常:
- 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
- 在方法中使用try-catch的语句块来处理异常。
try-catch**的方式就是捕获异常,Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。
捕获异常语法如下:
try{
编写可能会出现异常的代码
}catch(异常类型 e){
// 常用用法
// 记录日志/打印异常信息
// 继续抛出异常
}
try:该代码块中编写可能产生异常的代码。
catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。
注意:try和catch都不能单独使用,必须连用。
演示如下:
public class TryCatchDemo {
public static void main(String[] args) {
try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
read("b.txt");
} catch (FileNotFoundException e) {// 括号中需要定义什么呢?
//try中抛出的是什么异常,在括号中就定义什么异常类型
System.out.println(e);
}
System.out.println("over");
}
/*
*
* 我们 当前的这个方法中 有异常 有编译期异常
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
如何获取异常信息:
Throwable类中定义了一些查看方法:
-
public String getMessage()
:获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。 -
public String toString()
:获取异常的类型和异常描述信息(不用)。 -
public void printStackTrace()
:打印异常的跟踪栈信息并输出到控制台。*包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。*
在开发中呢也可以在catch将编译期异常转换成运行期异常处理。
2.5、finally代码块
finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。
finally的语法:
try...catch....finally:自身需要处理异常,最终还得关闭资源。
注意:finally不能单独使用。
比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。
finally代码参考如下:
public class TryCatchDemo4 {
public static void main(String[] args) {
try {
read("a.txt");
} catch (FileNotFoundException e) {
//抓取到的是编译期异常 抛出去的是运行期
throw new RuntimeException(e);
} finally {
System.out.println("不管程序怎样,这里都将会被执行。");
}
System.out.println("over");
}
/*
*
* 我们 当前的这个方法中 有异常 有编译期异常
*/
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {//如果不是 a.txt这个文件
// 我假设 如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常 throw
throw new FileNotFoundException("文件不存在");
}
}
}
当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。
2.6、异常注意事项
-
1、运行时异常被抛出可以不处理。即不捕获也不声明抛出。
-
2、如果父类的方法抛出了多个异常,子类覆盖(重写)父类方法时,只能抛出相同的异常或者是他的子集。
-
3、父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
-
4、当多异常分别处理时,捕获处理,前边的类不能是后边类的父类
-
5、在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。
-
6、多个异常使用捕获又该如何处理呢?
- 多个异常分别处理。
- 多个异常一次捕获,多次处理。
- 多个异常一次捕获一次处理。
一般我们是使用一次捕获多次处理方式,格式如下:
try{ 编写可能会出现异常的代码 }catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获. 处理异常的代码 //记录日志/打印异常信息/继续抛出异常 }
注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。
三、自定义异常
3.1、自定义异常概述
为什么需要自定义异常类:
我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN公司没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。
在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?
异常类如何定义:
- 自定义一个编译期异常: 自定义类 并继承于
Java.lang.Exception
。 - 自定义一个运行时期的异常类:自定义类 并继承于
Java.lang.RuntimeException
。
3.2、自定义异常练习
要求:我们模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。
首先定义一个注册异常类RegisterException:
// 业务逻辑异常
public class RegisterException extends Exception {
// 程序中必须有的两个异常构造函数
/**
* 空参构造 【但是通常都会设置有参,而不是无参】
*/
public RegisterException() {
}
/**
*
* @param message 表示异常提示
*/
public RegisterException(String message) {
super(message);
}
}
模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。
public class Demo {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("nill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(LoginException e){
//处理异常
e.printStackTrace();
}
}
//判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new LoginException("亲"+name+"已经被注册了!");
}
}
return true;
}
}
四、异常规范
将工作中使用异常的总结:
1、【强制要求】【重点】不要捕获Java类库中定义的继承自RuntimeException的运行时异常类,如:IndexOutOfBoundsException 、 NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。
在我们程序编码中最常见的就是:
if(obj != null){
// 操作
}
而不是:
try {
// 可能存在的问题
obj.method();
} catch(NullPointerException e){
// 对异常处理
}
2、【强制】异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
3、【强制】对大段代码进行try-catch,这是不负责任的表现。catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
4、【强制】【重点】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
通常我们会在service业务层这样子来写:
try{
}catch(Exception e){
log.error();
throw new RunTimeException();
}
然后在controller层中这样子来写:
try{
xxxService.yyyy(zz);
}catch(Exception e){
// 在这里对抛出来的异常进行处理
// 这里有不同的几种处理情况,下面分别介绍。
}
5、【强制】【重点】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch
6、【强制】【重点】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
7、【强制】【重点】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。调用方需要进行null判断防止NPE(NullPointException)问题。 说明:本规约明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回null的情况。
8、【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景:
(1) 返回类型为包装数据类型,有可能是null,返回int值时注意判空。 反例:public int f(){ return Integer对象}; 如果为null,自动解箱抛NPE。
反例:public int f(){ return Integer对象}; 如果为null,自动拆箱将会抛出NPE
(2)数据库的查询结果可能为null
(3) 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。 如map,但是更多的是针对对象属性操作的。
(4) 远程调用返回对象,一律要求进行NPE判断。
( 5) 对于Session中获取的数据,建议NPE检查,避免空指针。
( 6) 级联调用obj.getA().getB().getC();一连串调用,易产生NPE。
(7)、【推荐】【重点】在代码中使用“抛异常”还是“返回错误码”,对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess、“错误码”、“错误简短信息”。
说明:关于RPC方法返回方式使用Result方式的理由:
(1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
(2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
所以:注意写的接口是对外开放的,还是系统内开放的。对外开放,利用错误码显示;系统内部中调用的返回自定义的Result来进行返回;
(8)、【推荐】定义时区分unchecked / checked 异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException等。
(9)、【参考】避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。 说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。 正例:一个类中有多个public方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto){...}
五、简单的面试题
public class TestFinally {
public static void main(String[] args) {
Boolean b = get();
System.out.println("最终成功的是:"+b); // false
System.out.println("=====================");
int i = testOne();
System.out.println("对应的i的值是:"+i);
System.out.println("====================="); // 20
int i1 = testTwo();
System.out.println("对应的i1的值是:"+i1); // 10
}
public static int testOne(){
int i = 10;
try {
return i;
}finally {
return i = 200;
}
}
public static int testTwo(){
int i = 10;
try {
return i;
}finally {
i = 200;
}
}
public static boolean get(){
try {
return Boolean.TRUE;
}finally {
return Boolean.FALSE;
}
}
}
可以执行一下,看一下对应的答案。
首先必须要来确定的是:finally中的代码块是一定为执行的。
1、finnally中如果有return,那么返回的时候肯定返回的是return的,而不是try中的;
2、如果没有return,那么返回的将会是try中的值;
3、如果说程序中出现了对应的异常信息,try和catch代码块中出现了异常,那么首先也是会先执行finally中的代码块,然后再抛出异常;
六、阿里巴巴异常规范
1、【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:if (obj != null) {...}
反例:try { obj.method(); } catch (NullPointerException e) {…}
2、【强制】异常不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
如下面所示:
try{
...
}catch(NullPointerException e){
fun();
}
也就是说,不要在catch中做业务逻辑运算,因为异常是用来解决程序中不可控的意外情况,而不是给你做条件分支的。
同时,异常的处理效率比条件判断方式要慢很多。实际这么使用的的情况还是比较少。
3、【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户
4、【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。【可以理解web中必须返回的是用户能够看得懂的】
5、【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务
6、【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
7、【强制】不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
反例:
private int x = 0;
public int checkReturn() {
try {
// x 等于 1,此处不返回
return ++x;
} finally {
// 返回的结果是 2
return ++x;
}
}
8、【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
9、【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
10、【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
11、【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
12、.【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),
更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
13、【参考】对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;
跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”;而应用内部推荐异常抛出。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
14、【参考】避免出现重复的代码(Don't Repeat Yourself),即 DRY 原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。
必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {...}
七、总结
1、打印日志的时候不要打印大对象,尤其是使用JSON频繁打印占据日志文件,而日志文件就是为了方便我们来定位和查看的,所以不要把日志信息搞的一团糟;
2、打印关键参数,不需要打印完整的数据。也就是说重要的数据打印出来即可;
3、使用logger.info({},{},e1,e2); 而不是使用+在后面来进行拼接;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?