6.1 Java 语法相关的异常
这类Crash,通常是和Java语法有关系。同样的错误,在Java项目中也屡见不鲜。所幸的是,这些纯语言相关的异常都有相应的解决方案。
6.1.1 空指针
异常中的关键字:
NullPointException
发生频率:★★★★★
笼统地说,80%的Crash在异常信息中都带有NullPointException这样的关键字,散布在各个Activity和Adapter中,但其实有很多是其他原因导致的。比如说窗体泄露很多时候也表现为NullPointException。我们这里只讨论几种最简单的几种情况(其他复杂的情况散布在后续的异常分析中):
1)方法需要对传入的参数判空后再使用。调用MobileAPI的接口时,过于相信返回的数据,一旦使用了空的JSON值,App就有可能崩溃。这类原因导致的Crash,修复是比较容易的,只要在MobileAPI接口返回的数据上增加非空判断或try-catch语句即可。
很多App开发都使用了AsyncTask来调用MobileAPI接口并返回数据,在AsyncTask的doInBackground中,会因为有空指针而崩溃。
2)对于外部接口调用,需要确保返回值中不为空,甚至需要确保执行该接口不会抛出其他异常导致程序退出。比如,页面跳转前后,跳转前没准备好数据,跳转到目标页执行onCreate方法时解析传过来的Intent时,发现bundle这个字典中的某些数据为空,那么使用的时候就崩溃了。
3)在App中过多使用全局变量,一旦发生内存回收,这些全局变量会被设置为空,而我们的程序又没有考虑如何处理这种情况。针对于这类原因导致的Crash,我们要避免使用全局变量,如果万不得已必须要使用全局变量,也要使全局变量支持序列化到本地的机制,一旦我们要使用全局变量而又发现其为空的时候,就从本地反序列化回来。
全局变量这类问题多发生在把App切换到后台,过一段时间后再切换到前台,因为要执行所在页面的onResume和onCreate方法,如果这些方法中有全局变量并且被回收,那么会立刻就崩溃。
此外,即使切换回前台不会崩溃,由于这个页面所使用的全局变量被回收了,那么在页面跳转时,还是会发生崩溃。
关于全局变量的详细介绍,参见第3章的3.5节“消灭全局变量”。
6.1.2 角标越界
异常中的关键字:
·关键字1:IndexOutOfBoundsException
·关键字2:StringIndexOutOfBoundsException
·关键字3:ArrayIndexOutOfBoundsException
发生频率:★★★
如图6-1所示,IndexOutOfBoundsException是基类。对于字符串截取时发生的越界,会抛出StringIndexOutOfBoundsException的异常信息;而对于数组越界,则会抛出ArrayIndexOutOfBoundsException。
这类Crash也是由于程序的不严谨导致的。相应的解决方案是:
·在遍历一个数组/集合时,要预判数组/集合是否为空,长度是否大于0。
·在使用数组/集合中的元素时,要预判数组/集合长度是否有这么长。
图6-1 IndexOutOfBoundsException与其子类的继承关系
字符串也是一种数组,我们经常会使用subString(start,end)这样的函数,如果start或end超过了字符串的长度,就会崩溃。解决方案是,每次使用该函数时,都要判断字符串的长度。
ListView操作不当也会导致IndexOutOfBoundsException的异常,请参阅6.4.3节的相关介绍。
6.1.3 试图调用一个空对象的方法
异常中的关键字:
Attempt to invoke virtual method on a null object reference
发生频率:★★★
这种Crash的产生,是因为在使用一个对象的某个方法时,这个对象为空,就是说没有实例化。比如,我们经常犯的一个错误是,将实例化的语句写在if-else的一个分支中,日常开发和测试工作只保证了带有实例化的情况,所以不会崩溃。发版后,没有实例化的分支才会被大量的用户群所点到,于是就崩溃了。
我还经常看见这样的程序,在一个Activity中,调用另一个Activity B的方法,为此在B中建立一个static变量。当这个static变量被回收时,就会有上述异常。
还有一种可能,那就是推送,点击推送消息,根据事先定好的协议,跳过首页直接进入二级甚至三级页面。这时,二级页面要使用首页某个对象时,这个对象势必为空,那也会引发同样的异常。
6.1.4 类型转换异常
异常中的关键字:
ClassCastException:classA cannot be cast to classB
发生频率:★★★★
这类Crash都是由于强制类型转换导致的,如下所示:
Object x = new Integer(0); String str = (String)x;
这就会抛出ClassCastException的异常了。
解决方案是,使用安全类型转换函数,参见本书第1章中1.6节介绍的类型安全转换函数。在把字符串转换为整数、小数或布尔类型时,我们要为其指定转换失败时的默认值。否则,就会得到一个空值,放到哪里使用都会崩溃。
6.1.5 数字转换错误
异常中的关键字:
NumberFormatException
发生频率:★★★★
在数据类型转换过程中,如果转换不成功,一般抛出ClassCastException的异常。只有一个例外情况,当字符型转换为数字失败时,Android系统会抛出NumberFormatException异常,如下所示:
String abc = "123xxx45"; int result = Integer.parseInt(abc);
这种情况多发生在服务器返回数据,没有按照约定返回整数而是字符串,客户端必须要事先考虑到这种情况,如果转换失败,必须有默认值而不是直接就崩溃了。
6.1.6 声明数组时长度为-1
异常中的关键字:
NegativeArraySizeException
发生频率:★★
数组大小为负值异常。当使用负数大小值创建数组时抛出该异常。
我认为程序员不可能犯int arr=new int[-1];这样的低级错误,所以我继续试图寻找其他导致这个异常的场景。我在网上找了很久,直到有一天,我发现了下述语句:
String[] arg 1 = new String[args.length – 1];
当args数组中没有元素时,就会出现int[-1]的场景。
此外,我还尝试声明int arr=new int[0];的语句,发现程序并不会报错,但是这样的语句声明得到的变量arr毫无意义,因为arr的长度为0,arr只能是一个空数组,不能设置其中的任何一个元素。所以我们在声明数组时,不能出现类似int[0]这样的语句。
综上所述,在声明一个数组时,如果数组长度是由另一个变量动态得到的,要保证中括号[]中的值必须大于0。
6.1.7 遍历集合同时删除其中元素
异常中的关键字:
ConcurrentModificationException
发生频率:★★
能犯这种错误的人,还是拖出去打八十大板吧,而且要翻过来打的那种。
但凡有点编程常识的程序员都知道在遍历一个集合时不能删除该集合中的元素,如下所示,必然产生这样的崩溃:
HashMap<Integer, String> map = new HashMap<Integer, String>(); for (int i = 0; i < 10; i++) { map.put(i, "value" + i); } for (Map.Entry<Integer, String> entry : map.entrySet()) { Integer key = entry.getKey(); if (key % 2 == 0) { map.remove(key); } }
该问题的解决方案是,需要再定义一个列表结合delList,用来保存需要删除的对象,如下所示:
HashMap<Integer, String> map = new HashMap<Integer, String>(); for (int i = 0; i < 10; i++) { map.put(i, "value" + i); } List delList = new ArrayList(); for (Map.Entry<Integer, String> entry : map.entrySet()) { Integer key = entry.getKey(); if (key % 2 == 0) { delList.add(key); } } for (int i = 0; i < delList.size(); i++) { map.remove(delList.get(i)); }
还有另一种产生这种崩溃的情况,那就是在多个线程中删除同一个集合中的元素。
如下列代码所示,vector是一个集合,我们建立了两个线程,线程1对其进行遍历,线程2对其进行插入操作。由于这两个线程同时在执行,所以就会产生ConcurrentModification Exception的异常了:
static ArrayList<Integer> list = new ArrayList<Integer>(); void testScenario2() { for (int i = 0; i < 100; i++) { list.add(i); } Thread1 thread1 = new Thread1(); thread1.start(); Thread2 thread2 = new Thread2(); thread2.start(); } class Thread1 extends Thread { public void run() { while (true) { Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } } } class Thread2 extends Thread { public void run() { while (true) { for (int j = 101; j < 200; j++) { list.add(j); } } } }
ArrayList继承自AbstractList,这是一个迭代器,所有继承自AbstractList的集合类,都是线程不安全的。
相应的解决方案是,将Vector换为CopyOnWriteArrayList,这是一个线程安全的集合类。
6.1.8 比较器使用不当
异常中的关键字:
Comparison method violates its general contract!
发生频率:★★
这个错误是因为Comparator的compare方法使用姿势不正确导致的。
说起Comparator,是基于插入排序算法与归并排序算法相结合的产物 [1] ,要比我们日常所使用的冒泡排序算法快很多,但缺点就是不易掌握,于是就产生了这里所讨论的异常。
我们先写一个正确的用法:
List<Double> list = new ArrayList<Double>(); list.add(22.1); list.add(22.1); list.add(19.7); list.add(26.3); Comparator<Double> comparator = new Comparator<Double>() { public int compare(Double d1, Double d2) { if (d1 < d2) { return -1; } else if (d1 > d2) { return 1; } else { return 0; } } }; Collections.sort(list, comparator);
但是,我们经常会偷懒,把这个compare方法写成这样:
Comparator<Double> comparator = new Comparator<Double>() { public int compare(Double d1, Double d2) { return p1 > p2 ? 1 : -1; } };
这就忽略了p1和p2的age相等的情况,这时应该返回0。当数组或集合中的元素以某种方式排列的时候,就会报Comparison method violates its general contract!的异常了,如下所示 [2] :
public static void compare() { int[] sample = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 1, 0, -2, 0, 0, 0, 0 }; ArrayList<Integer> list = new ArrayList<Integer>(); for (int i : sample) { list.add(i); } Comparator<Integer> comparator = new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { if (o1 < 02) return -1; else if (o1 > o2) return 1; else return 0; } }; Collections.sort(list, comparator); }
为了预防这类Crash的发生,我的解决方案是对每个自定义的比较器进行单元测试,用充足的测试数据来保障逻辑没有问题。参见本书第8章中8.9节介绍的单元测试。
6.1.9 当除数为0
异常中的关键字:
java.lang.ArithmeticException:divide by zero
发生频率:★★
当在程序中执行一个除法时,如果除数为0,就会发生上述崩溃。
我们一般不会直接写出除数为0的异常来。这样的Crash多发生在第三方控件中,比如说GifView,这个框架很有名,用于显示gif动画。
GifView这个开源项目有很多变体,但是无论如何,都应该注意其中movie的duration方法,这个值表示动画持续的时间,在接下来的代码中将会作为除数,如果为0,就会抛出上述的异常信息了,这时候要将其设置为默认值1秒。 [3]
6.1.10 不能随便使用的asList
异常中的关键字:
java.lang.UnsupportedOperationException at
java.util.AbstractList.remove(AbstractList.java:144)at
java.util.AbstractList$Itr.remove(AbstractList.java:360)at
java.util.AbstractCollection.remove(AbstractCollection.java:252)at
发生频率:★★
这个异常是因为对asList方法的理解有误导致。Arrays.asList()的返回值类型为java.util.Arrays$ArrayList,而不是ArrayList。画一个类的继承关系图,如图6-2所示。
图6-2 AbstractList类的继承关系图
从图中看到,AbstractList这个基类有两个方法add和remove。但是它的两个子类,只有ArrayList实现了add和remove这两个方法,而Arrays$ArrayList却没有实现这两个方案,而直接抛出UnsupportedOperation Exception异常。
写一段导致这个异常的代码,如下所示:
String str = "1,2,3,4,5"; List<String> test = Arrays.asList(str.split(",")); test.remove("1");
相应的解决方案是,将java.util.Arrays$ArrayList转换为ArrayList,如下所示:
String str = "1,2,3,4,5"; List<String> list = Arrays.asList(str.split(",")); List arrayList = new ArrayList(list); arrayList.remove("1");
6.1.11 又有类找不到了(一):ClassNotFoundException
异常中的关键字:
ClassNotFoundException
发生频率:★★★★
当我们动态加载一个类的时候,如果这个类在运行时找不到,就会抛出这个异常。比如说,Class会有一个forName方法:
Class.forName("com.company.package.class");
由于类的全名称是字符串形式,这个值极有可能可能是不正确的,那自然就会加载不成功了。类似的方法还有:
·ClassLoader中的findSystemClass(“classname”)方法。
·ClassLoader中的loadClass(“classname”)方法。
我们在6.2节中会介绍导致ClassNotFoundException的几种情况,比如说使用Proguard会把一些类混淆了,但是Class.forName中的参数值并不会改变,那么自然就会找不到类了。
6.1.12 又有类找不到了(二):NoClassDefFoundError
异常中的关键字:
NoClassDefFoundError
发生频率:★★★★
当我们在B类中声明一个A类的实例,如下所示:
ClassA obj = new ClassA();
但是打包时B和A分别位于不同的dex中,这时如果在A所在的dex中把A类删除了,那么在运行时执行到这句话时就会抛出NoClassDefFoundError的异常信息。
通常插件化编程的时候会牵扯出这个异常,因为要使用到DexClassLoader。也许你的项目中没有用到插件化编程但是也有类似的问题,那么就看一下你所使用的第三方SDK吧。
[1] 关于Comparator的算法实现机制,详细信息请参见http://blog.2baxb.me/993/?utm_source=tuicool
[2] 关于这个bug的详细介绍,网上已经找不到原创,请参见其中一篇转载文章:http://blog.csdn.net/sells2012/article/details/18947849,隐约能查到的是,此文为HuangWei所写,相关代码请到GitHub下载:https://github.com/Huang-Wei/understanding-timsort-java7。
[3] 详细内容请参见http://blog.csdn.net/loongggdroid/article/details/21166563。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论