做Java开发的,大多数可能都有看过阿里的Java后台开发手册,里面有关于Java后台开发规范的一些内容,基本覆盖了一些通用、普适的规范,但大多数都讲的比较简洁,本文主要会用更多的案例来对一些规范进行解释,以及结合自己的经验做补充!
【Java后台开发规范】— 不简单的命名
【Java后台开发规范】— 日志的输出
【Java后台开发规范】— 线程与并发
【Java后台开发规范】— 长函数、长参数
【Java后台开发规范】— 设计原则
【Java后台开发规范】— 圈复杂度
【Java后台开发规范】— Null值处理
异常处理应该算是一种我们非常熟悉的话题了,Java中对于异常的处理也非常便捷、灵活,但往往越是简单的东西,越容易忽视它,恰巧异常也存在很多容易忽视的陷阱,一起来看看吧!
Throwable
是所有异常的错误的父类,printStackTrace()
方法就是由Throwable
提供的。
Error
表示程序遇到了无法处理的问题,出现了严重的错误,常见的比如:OutOfMemoryError,StackOverflowError
程序本身可以处理的异常,Exception
类本身又分为两类:运行时异常和编译时异常。
RuntimeException
类及其子类产生的异常,编译时不会进行检查,只有在程序运行时才会产生,也可以通过try-catch
来进行处理,但通常不需要我们这样做,因为运行时异常一般都是我们代码本身编写存在问题,应该在处理逻辑上进行修正。
常见的有:NullPointerException,ArrayIndexOutBoundException,ClassCastException
Exception
下除了RuntimeException
类型的其他异常都是编译时异常,这类异常在编译时就会进行检查,并强制要求对其进行处理,否则无法通过编译。
常见的有:ClassNotFoundException、IOException
绝大多数情况下都不应该像如下这样忽视异常的存在,因为这样会让你无法发现问题。
try{ doSomething(); }catch(Exception e){ // 什么也不做 }
当然也有例外
如果选择了忽略异常,那么最好在catch
中通过注释的方式给出原因,并且变量名使用ignored
下面两个案例,都将变量名改为了ignored
,但都没有在catch
中给出具体的原因。
原因写在了方法注释上。
这个解释绝绝子,不可能发生
统一语言、统一认知一直是我们强调的,让异常标准化也算其实现手段之一,得益于标准化好处,当你看到如下这些异常时,会感到非常的熟悉:NullPointerException、IllegalArgumentException、IllegalStateException、ClassCastException、IllegalFormatConversionException、IndexOutOfBoundsException
如果没有这些标准化的异常分类,实际上所有的异常都可以归为IllegalStateException
(非法状态)或者IllegalArgumentException
(非法参数)。
TreeMap
中的Key
不允许为null
HashTable
中的value
不允许为null
以上两个案例,实际上都可以按照IllegalArgumentException
(非法参数)来处理,但是作者并没有这样做,IndexOutOfBoundsException
异常也一样,并没有用IllegalArgumentException
来替代。
常见的一些标准异常:
IllegalArgumentException IndexOutOfBoundsException NullPointerException ClassCastException IllegalFormatConversionException UnsupportedOperationException
一种基于异常的循环控制,这种做法的原因是因为有人认为JVM底层就是这样终止的。
List<User> userList = new ArrayList<>(); userList.add(new User("a")); userList.add(new User("b")); userList.add(new User("c")); userList.add(new User("d")); try { int i = 0; while (true) { User user = userList.get(i++); System.out.println(user.getName()); } } catch (IndexOutOfBoundsException e) { // 什么也不做 }
比如,正常你应该会写成像下面这样,那JVM又是怎么判断数据边界的呢?
for (User user : userList) { System.out.println(user.getName()); }
为了省去每次的边界检查,所以采用异常捕获的方式,这明显是错误的,实际上测试对比后,后者比前者快很多,原因主要在于以下两点:
try-catch
中的代码,JVM一般不会对其进行优化。基于上述这个案例,也告诫我们在做设计时,不要企图让你的调用者通过异常控制的方式来完成正常的流程。
再来看一个案例
Iterator<User> iterator = userList.iterator(); while(iterator.hasNext()){ User user = iterator.next(); }
假如Iterator
没有提供hasNext
方法,那可能你只能通过try-catch
的方式来解决了。
Iterator<User> iterator = userList.iterator(); try{ while(true){ User user = iterator.next(); } } catch(NoSuchElementException e){ }
这条原则的含义是指,当调用某行代码产生异常时,应该使当前对象仍能保持异常前的数据状态。
通常有下面几种方式:
举一个list集合移除元素的例子,其中rangeCheck
方法中对当前集合的size
做了检查,如果index>=size
则抛出异常
public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
其实通过这个简单的rangeCheck
方法就能让异常保持原子性,因为它使得在modCount
在修改之前就已经抛出了异常,假设你没有提前做rangeCheck
检查,那么你在调用E oldValue = elementData(index)
这一行时,仍然会遇到IndexOutOfBoundsException
异常,但modCount
状态却已经被修改了,你不得不再去维护它的状态。
很多场景中不可变对象总是安全的,异常也不例外。
如果你每次操作的都是新拷贝出来的对象,那么即使失败了,也并没有对原数据产生影响。
通过手动补偿的方式来保证失败后状态的正确性,就有点像如何解决分布式事务的问题,在遇到失败后,主动调用一段事先准备好的回滚逻辑,使数据回到失败前的状态。