版权声明:本文章原创于 RamboPan ,未经允许,请勿转载。
对于 Java 参数传入 null 判断思考
最近重温了《Effective Java》之后,在日常工作中写代码时感觉也会慢一点,多一点思考这样是不是比较合理。
在一个常见的情况下迟疑了下:在定义方法时,经常需要传入非基本类型的参数。
//定义一个简单的类
class Hello{
public void say(){
System.out.print("Hello");
}
}
//类似这种情况,需要传入一个对象进行它的操作
public void test01(Hello hello) {
hello.say();
}
按理说我们会在这里进行一个判断,判断是否是空,以防我们在后续使用它的方法时出现空指针。就像这样进行一次判断。
//类似这样的情况,传一个简单的参数
//对字符进行判断一下,如果无效就不进行下一步操作
public void test01(Hello hello) {
if(hello == null){
……
return;
}
hello.say();
}
一:按照一些编程书中的意见,我们最好不要做这些容错机制,如果是第一次调用该方法时,参数传入错误就直接报错,而不是后面发现没有输出 “Hello” 这个字符的时候再来寻找哪里的问题,这样就减少了调试的成本。
二:但是这样一想,要是这段代码自己测着没什么问题,其他懂代码的同事用了之后肯定也会懂。但如果这段代码在你自己的程序里,其他应用传入错误参数时,让你的程序崩溃了,虽然是别人的问题,但是你说是你慌一点还是别人慌一点 …… 🙃
所以感觉这还是要区分场景的,比如在前一种情况下,就是需要直接把问题暴露出来,而在后一种情况,类似于需要保证程序的健壮性,那么还是需要做容错的处理。
在写一个工具类的时候想到有些 Java 框架,在内部使用时,就进行了非空判断的操作。
比如 RxJava 中有个判断工具。
//如果不为空则直接返回该对象
public static <T> T checkNull(T t){
if(t == null)
throw new NullPointerException("空指针啦,亲");
return t;
}
//这样写也是可以的,只是在没法嵌套在其他函数参数中。
public static <T> void checkNull(T t){
if(t == null)
throw new NullPointerException("空指针啦,亲");
}
//======================= 示范 ===================
//定义一个简单的类
class Hello{
public void say(){
System.out.print("Hello");
}
}
//类似这种情况,需要传入一个对象进行它的操作
public void test01(Hello hello) {
hello.say();
}
//这种是使用带返回的 checkNull ,可以进行嵌套使用
public void checkNull01(){
Hello hello = new Hello();
test01(checkNull(hello));
}
//这种是不带返回的 checkNull ,不能进行嵌套使用,就是多了一行 ……
public void checkNull02(){
Hello hello = new Hello();
checkNull(hello);
test01(hello);
}
其实这种的好处就是在调试期间,如果出错的话,肯定会第一时间找到出错位置,就是类似上面说的第一种情况。
既然要做这种判断非空的工具,那就做一个通用的方法,再做几个简单封装的方法,是常规思路嘛。比如这里我们可以加入需要的 Exception 类型,出错的 String 提示,大概就要这样调用效果。
//定义一个简单的类
class Hello{
public void say(){
System.out.print("Hello");
}
}
//测试
public void test01() {
Hello hello = new Hello();
hello = checkNull(hello,NullPointerException.class,"空指针啦,亲");
}
第一个是需要检测的对象,第二个是异常的类型,第三个就是提示语。
虽然我们这里是只检测是否为空,按理说就应该抛空指针,你还要抛什么其他的花样 ?好奇的你肯定会问。
虽然直观上是空指针,但是如果你在一个方法中对成员变量进行判断,可能这个对象已经完成使命,执行了一些释放的操作,成员变量已经被置空,那么此时如果抛出 IllegalStateException 加上说明,比如 “ xx 已经被释放了,请检查状态 ” 是不是更合适一点 ?所以还是灵活一点好。(毕竟泛型帅嘛,啊哈哈哈)
//通用的接口
public static <T> T checkNull(T t,Class<? extends Exception> eClazz,String msg){
if(t == null){
try {
Constructor<? extends Exception> eCons = eClazz.getDeclaredConstructor(String.class);
eCons.setAccessible(true);
throw eCons.newInstance(msg);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return t;
}
第一个参数是对于对象的判断,第二个参数是采用泛型限定了 Exception 类及其子类,第三个参数针对错误的提示。
因为前面的一些限定,所以后续的四个 Exception 中没有再次抛出(比如大部分 Exception 子类,都有单个 String 的构造函数;虽然都是 public,此处也加上了访问权限设置 ,所以忽略了这些情况)
编译器没有提示错误,仿佛没有什么不对,于是编译跑起来试试,毕竟看着对和跑得起来是两回事 …… 果然,编译的途中出错了(男人的直觉 😂)。
错误: 未报告的异常错误CAP#1; 必须对其进行捕获或声明以便抛出
其中, CAP#1是新类型变量:
CAP#1从? extends Exception的捕获扩展Exception
说实话这个真的有点摸不着的头脑,感觉逻辑上也没错啊,这究竟是怎么肥事 …… 🤔
直接搜这个错误,还没什么参考,于是把几个关键字都试了一下,看了别人的意见有点启发。
Exception 分为 编译期的错误 与 运行时的错误 ,像 NullPointerException 这种就属于运行时的错误,如果是 Exception 、 IOException 这种一定是需要自己处理了之后才能通过编译的。
处理的方式就是两种,正如报错的提示:必须对其进行捕捉或者声明以便抛出。
//声明:当其他方法调用此方法时需要它处理异常
public void say() throws Exception {
……
}
//捕捉:这个方法内部已经处理了,其他方法调用时不需要处理
public void say(){
try{
……
}catch(Exception e){
……
}
}
现在我们来做个试验,测试下是不是正如我们猜想的那种:这里需要抛出一个 RuntimeException ,而不是 Exception。
public void test01(){
throw new Expcetion("试一试,好像不行");
}
提示: Unhandled Exception: java.lang.Exception
如果我们按照两种处理异常的方式处理一下,那么就没有问题了,也能顺利通过编译。
当然,这不是我们要的结果,此处我们想要的就是在运行时,遇到问题直接抛出(就是这么刚)。
那我们把泛型限定条件改一下: Class<? extends Exception> 改为 Class<? extends RuntimeException>
然后再进行编译就没问题了,当然 Constructor 对应的泛型限定符也需要对应的修改。
分享下平时思考的笔记,如有不对之处,欢迎指出讨论。