【Java】在设计Callback功能时,如何巧妙回避Java的强制异常处理机制

有这样一句话:衡量Java设计师水平和开发团队纪律性的一个好方法,就是读读他们应用程序里的异常处理代码

异常处理虽然不是什么高难度的技术点,但要是想要整个工程,所有的异常都考虑得周到,又处理得到,还要尽量少的使用try-catch,其实是很考验一个人的设计能力的

至少那些只求完成工作任务,平时不钻研技术细节的人,短时间内是很难做到这一点的。他们一般会做的,大概就是到处写try-catch,然后对异常的处理方式都是e.printStackTrace。偶尔被业务逼得无可奈何,才想起来加一个错误提示

什么是Java的强制异常处理

Java语言在设计时,为了保证安全性,考虑到了常见的可能发生的异常,会强制用户对这些异常进行处理
不过在后来的实践之中,逐渐证明这是一个比较失败的设计,包括Java之父自己也承认了这一点
因为很多时候我们是可以百分百确定某些异常不会发生的,或者说发生了也没关系,完全不需要进行处理
强制异常处理属于设计过度,强制开发者编写try-catch,最后让代码变得异常繁琐

强制异常处理案例


	new Thread(new Runnable() {
	    @Override
	    public void run() {
	        try {
	        	new File("C://x.mp3").createNewFile();
	            new FileOutputStream("C://x.mp3");
	        } catch (FileNotFoundException e) {
	            e.printStackTrace();
	        } catch (IOException e) {
                e.printStackTrace();
            }
	    }
	}).start();

比如我们这里创建线程的代码,第一行代码创建了文件,第二行代码获取文件流
如果代码执行到了第二行代码,说明文件肯定存在,是完全不用处理FileNotFoundException的
很多时候我们会被强制异常处理机制逼着编写这样冗余且繁琐的代码,让整个代码变得很难看

巧妙回避Java的强制异常处理机制

虽然Java SDK的已有代码,但我们可以将这些异常处理逻辑封装起来,对异常进行统一处理,这样我们在开发业务代码时,就可以大幅减少try-catch代码了

这里还是以上面的线程为例,来展示下如何通过设计技巧,回避Java的强制异常处理机制

我们先创建一个Action接口来取代Runnable接口,因为Runnable是Java内置代码,不可能再修改了


	//封装一组行为,和Runnable功能是一样的,但是回避了Java的强制处理机制
	//由于run方法增加了throws Exception选型,将异常抛给了调用者处理,开发者在实现run方法时就不用处理强制异常了
	//虽然run方法不用处理异常,但是runIgnoreException调用了run方法,等于接收了run方法的强制异常,需要对它们进行处理
	//我们进一步在runIgnoreException中将强制型异常转化为运行时异常抛出,运行时异常是不需要强制处理的
	//当我们使用Action对象时,调用runIgnoreException方法来替代run方法,这样就可以回避Java的强制处理机制了
	
	//上面的做法只是直接跳过了Java的强制异常处理机制,但并不是什么时候我们都可以这样做
	//毕竟我们的程序不是完美的,有些异常可能是我们没有考虑到的,一个完善的系统是不能仅仅忽略异常的
	//runAndPostException提供了一个额外的功能,它对任意异常进行捕捉,然后统一转交给Application处理
	@SuppressWarnings("all")
	public interface Action {
	
	    //封装一组行为
	    void run() throws Exception;
	
	    //忽略异常
	    default void runIgnoreException() {
	        try {
	            run();
	        } catch (Throwable e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    //将异常转交给Application统一处理
	    default void runAndPostException() {
	        try {
	            run();
	        } catch (Throwable e) {
	            CommonApplication.ctx.handleGlobalException(e);
	        }
	    }
	}

下面我们再封装一个方法,用Action代替Runnable来完成线程工作


	public class Threads {
	
	    public static void post(Action action) {
	        new Thread(action::runAndPostException).start();
	    }
	}

Threads + Action取代Thread + Runnable完成线程工作


	Threads.post(()->{
	    new File("C://x.mp3").createNewFile();
	    new FileOutputStream("C://x.mp3");
	});

可以看到,终于不用再进行强制异常处理了,而且代码还变得更加简洁了
Action不但帮我们回避了强制异常处理机制,还可以把所有异常交给Application处理
这样我们就可以在Application里面对所有异常进行统一处理,其它地方都不用编写try-catch代码了,以更少代码完成更强功能!

以上设计方法和UncaughtExceptionHandler的区别

有些人可能知道,通过Thread.setDefaultUncaughtExceptionHandler也可以对线程进行统一异常捕捉,但是它的功能很有限

首先,UncaughtExceptionHandler只是捕获未处理的异常,它并不能回避强制异常处理机制

其次,UncaughtExceptionHandler是对整个线程进行异常捕捉的,相当于在线程run方法的开头和结尾加了一个默认的try-catch
当线程发生异常时,代码就会跳到最后的UncaughtExceptionHandler处理中,虽然异常被捕捉了,但整个线程也停止工作了

而我们这套设计方法,不但可以回避强制异常处理机制,还能保证线程的继续运行
上面只是一个简单Demo,看不出二者差距,下面举两个例子来说明区别

案例一


	Action action;
	Threads.post(()->{
	    while (true)
	        action.runAndPostException();
	});

Action的异常处理范围是run方法,当run方法发生异常时,只是当前run方法结束,while还会继续跳到下一个run方法执行
而UncaughtExceptionHandler的异常处理范围是整个线程,一旦发生异常,整个线程都结束了

案例二


	Button button;
	Action listener;
	button.setOnClickListener(()->{
	    listener.runAndPostException();
	});

这里我们给按钮添加了一个点击事件,当run方法发生异常时,只是当前run方法结束,并不影响整个线程
如果使用UncaughtExceptionHandler的话,意味着整个线程都要结束了
有过界面开发经验的朋友应该知道,一般控件事件都是运行在主线程的,主线程结束了,也就意味着整个界面要崩溃了

关联

其实上面两个案例是有关联的在界面应用开发中,一般都是将用户的点击事件存储到事件队列中,监听器作为某个事件的回调,主线程会轮询这个事件队列,逐个取出事件的回调对象来执行

按照我们的设计方法,即便一个事件回调发生异常,执行失败了,队列还会继续轮询其它的事件,不影响整个事件队列

下面用伪代码来简单模拟下事件队列的工作机制


	Queue<Event> eventQueue;
	while (true){
	    if(eventQueue.isEmpty()) continue;
	    Event event = eventQueue.poll();
	    Action listener = event.listener;
	    listener.runAndPostException();
	}

总结

UncaughtExceptionHandler只是对没catch到的代码进行捕捉下,我们最多纪录下异常信息,并不能改变线程本身的运行流程

而我们的设计方法,则可以灵活变通,既能用于整个线程的异常处理,也能用於单个方法的异常处理
我们可以封装一个Action接口,同样可以将这套方法应用于其它的类或接口

Action提供了三个接口方法,它们的处理方法分别是:

  • run:将异常交给调用者处理
  • runIgnoreException:忽略异常处理
  • runAndPostException:将异常转交给Application统一处理

具体使用哪个,大家可以根据实际需要来决定

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章