代码静态检测

android静态代码检查是一项保证代码开发质量,确保App稳定必不可少的流程。如何借助检测工具有效的检查出项目中代码层面存在的问题呢?
阿里检查代码插件工具可以帮助你检查可能影响应用质量和性能的代码问题。该工具会报告检测到的每个问题并提供问题的描述信息和严重级别,以便你可以快速地确定需要优先进行哪些改进。

阿里检查代码插件

阿里检查代码插件如何安装和如何使用可以参考网上的博客,非常的简单。

集合相关检查

集合是程序中常用的数据结构,如果使用不当会造成意想不到的问题,因此在使用集合的时候需要特别注意,阿里检查代码插件有几条是专门针对集合的检查。使用这些检查可以很好的避免不必要的错误。

subList接口,返回的数据类型是List,不能进行强制转换

  • 检查条目描述

ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常。
说明:禁止强转,如果需要用到集合特性方法,请新建一个集合,然后置入sublist,new 集合(sublist结果)

  • 示例:
private void test2(){
        List<String> first = new ArrayList<>();
        for (int i = 0;i<10;i++){
            first.add("aa"+i);
        }
        // 代码会抛出异常  java.lang.ClassCastException
        ArrayList<String> second = (ArrayList<String>) first.subList(4,8);
    }

** 注意**

  1. subList 是 接口List的方法,因此只要实现接口List都有这样的方法。
  2. LinkedList,CopyOnWriteArrayList 的subList结果不可强转成相应的LinkedList,CopyOnWriteArrayList 。
  3. 第2点,插件没有实现相关的检查,因此在编写代码时需要特别注意。
原List使用SubList接口返回的子List,对子List进行修改会影响原List
  • 检查条目描述

在subList场景中,高度注意对原列表的修改,会导致子列表的遍历、增加、删除均产生ConcurrentModificationException异常。

  • 示例:
private void test1(){
        List<String> first = new ArrayList<>();
        for (int i = 0;i<10;i++){
            first.add("aa"+i);
        }
        // 获取子list
        List<String> second = first.subList(4,8);
        // 修改子list
        for (int i = 0; i < second.size();i++){
            System.out.println(second.get(i));
            first.set(i,"bb"+i);
        }
	//  遍历原list 确定原list 也被修改了。
        for (String str : first){
            System.out.println(str);
        }
    }

注意

  1. 通过接口SubList 获取的list 一旦修改了,会影响原list。
  2. 如果原List 和 子List 不在同一个线程。这时候是线程不安全的状态。

List 转 数组 toArray的注意事项

  • 检查条目描述

使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是list.size()

这条检查条目本质是不带传入参数的toArray()和带参数toArray(T[] array)的区别。

  1. 不带参数的toArray()得到的是Object[]的数组。
  2. 带参数的toArray(T[] array)返回的是 T[] 的数组。
  3. 任何对象的父类是Object,父类强制转换为子类,这时候会抛出异常。
  • 示例
    不带参数的toArray()的使用示例
private void test7(){
        List<String> list  = new ArrayList<>();
        for (int i = 0; i< 10; i++){
            list.add("mylist "+i);
        }
        // 无入参的toArray 得到的是 Object 类型的数组
        Object[] strings = list.toArray();
        for (Object str : strings){
            System.out.println(str);
        }
    }

带参数的toArray使用示例

private void test6(){
        String[] str = new String[]{"AA","BB","CC"};
        List list = Arrays.asList(str);

        String[] str1 = (String[]) list.toArray(new String[list.size()]);
    }

数组转List的注意事项

  • 检查条目描述

使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。

  • 示例
private void test9(){
 String[] str1 = new String[]{"13","12","11"};
 List<String> list1 = Arrays.asList(str1);
 // 转换后的list 可以进行排序
 Collections.sort(list1);
 //转换后的list 可以修改元素
 list1.set(1,"22");
 //转换后的list 不能进行元素的添加和删除,清空
 list1.add("33);// 抛出异常
 list.remove("13");// 抛出异常
 list.clear();// 抛出异常
 System.out.println(list1);
}

集合遍历

  • 检查条目描述

不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式。

遍历集合的方式有三种
方式1,通过游标遍历元素

void test1(){
        List<String> list = new ArrayList<>();

        for (int i = 0;i< 10;i++){
            list.add("item"+i);
        }

        System.out.println("修改前");
        list.forEach(str->System.out.println(str));

        for (int i=0;i< list.size();i++){
            if ("item1".equals(list.get(i))){
                // 可以修改制定元素
                list.set(list.indexOf("item1"),"update"+1);
            }
            if ("item2".equals(list.get(i))){
                // 添加元素
                list.add("item100");
            }
            if ("item3".equals(list.get(i))){
                list.remove(list.get(i));
            }
        }
        System.out.println("对元素修改");
        list.forEach(str->System.out.println(str));

        // 删除指定位置元素
        for (int i = 0;i < list.size();i++){
            if ("item4".equals(list.get(i))){
                list.remove(i+1);
            }
        }
        System.out.println("对元素删除");
        list.forEach(str->System.out.println(str));
    }

使用游标来获取遍历元素,效率上有所降低,但是,可以获取到元素在集合中的位置,同时能够对list 进行增删改。对list 能够进行增删改是优势也是劣势,劣势是在遍历过程中如果不小心处理会对list进行改变。
使用游标发来遍历元素的场景是:要获取元素的位置,且需求对原list进行增删改等较为复杂的操作。

方式 2 通过foreach表达式遍历元素

void test2(){
        List<String> list= new ArrayList<>();
        for (int i = 0;i< 10;i++){
            list.add("item"+i);
        }
        for (String str : list){
            if ("item1".equals(str)){
                // 可以修改制定元素
                list.set(list.indexOf("item1"),"update"+1);
            }
        }
        System.out.println("修改元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));

        // 删除元素
        for (String str : list){
            if ("item2".equals(str)){
                // 可以修改制定元素
                list.remove(str);
            }
        }
        System.out.println("删除元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));

        // 添加元素
        for (String str : list){
            if ("item2".equals(str)){
                // 可以修改制定元素
                list.add(list.indexOf("item2"),"update"+2);
            }
        }
        System.out.println("添加元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));
    }

通过foreach来遍历集合,可以修改元素,但是不能对集合进行添加和删除。一般来说foreach的遍历在不需要更改原集合的操作中比较实用,效率比较高。

方式3,通过迭代器进行遍历

void test2(){
        List<String> list= new ArrayList<>();
        for (int i = 0;i< 10;i++){
            list.add("item"+i);
        }
        for (String str : list){
            if ("item1".equals(str)){
                // 可以修改制定元素
                list.set(list.indexOf("item1"),"update"+1);
            }
        }
        System.out.println("修改元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));

        // 删除元素
        for (String str : list){
            if ("item2".equals(str)){
                // 可以修改制定元素
                list.remove(str);
            }
        }
        System.out.println("删除元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));

        // 添加元素
        for (String str : list){
            if ("item2".equals(str)){
                // 可以修改制定元素
                list.add(list.indexOf("item2"),"update"+2);
            }
        }
        System.out.println("添加元素");
        // foreach的 lambda 表达式
        list.forEach(str->System.out.println(str));
    }

优点:效率较高,可以修改元素,不能添加和删除元素,和foreach的功能差不多。如果想要能够对集合进行添加和删除需要用ListIterator。示例代码如下

void test3(){
        ArrayList list = new ArrayList();
        list.add("java01");
        list.add("java02");
        list.add("java03");
        list.forEach(str->System.out.println(str));
		// 使用for语句会更好,迭代器 iterator 一般出了循环体无效
        ListIterator<String>  iterator = list.listIterator();
        while (iterator.hasNext()){
            if ("java02".equals(iterator.next())){
                iterator.add("java09");
            }
        }
        System.out.println("添加后");
        list.forEach(str->System.out.println(str));
    }

遍历MAP的方式有两种

方式1 使用foreach 来遍历元素

void test4(){
        Map<String,String> map = new HashMap<>();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println("key = "+entry.getKey());
            System.out.println("value = "+entry.getValue());
        }

        // 只遍历值
        for (String key : map.keySet()){
            System.out.println("key = "+key);
        }
        // 只遍历value
        for (String value : map.values()){
            System.out.println("value = "+value);
        }
        // foreach的 lambda 表达式
        map.forEach((key, value) -> {
            System.out.println("key = "+key);
            System.out.println("value = "+value);
        });
    }

用foreach 遍历 map ,不能对map 进行增删改。

方式2 使用foreach 来遍历元素

void test6(){
        Map<String,String> map = new HashMap<>();
        for (int i= 0;i<5;i++){
            map.put("key"+i,"value"+i);
        }
        
        for (Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();iterator.hasNext();){
           System.out.println("key = "+iterator.next().getKey());
           System.out.println("value = "+iterator.next().getValue());

           if ("key1".equals(iterator.next().getKey())){
               map.put("update key","update value");
           }
        }
    }

时间格式注意事项

try catch finally注意事项

finally 代码块中无 return 语句

  • 检查条目描述

不能在finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。

注意
finally 中尽量不能使用return ,如果需要使用 return 需要特别的注释,说明使用 return 语句的原因。

finally 代码块一定执行。如果try 代码块 或者 catch 代码块中有return 语句,且 finally 代码块中没有 return 语句。则执行顺序是先保留return 的结果。 然后执行 finally 代码块的内容,最后执行try 代码块 或者 catch 代码块的语句。

  • 示例1
private int test3(){
        int i = 0;
        try{
   	        i = 100;
            System.out.println("try  i ="+i);
            return i;
        }catch (Exception e){
            i = 200;
            System.out.println("catch Exception i ="+i);
            return i;
        }finally {
            i = 300;
            System.out.println("finally i= "+i);
        }
    }

finally 代码块一定执行。如果try 代码块 或者 catch 代码块中有return 语句,且 finally 代码块中没有 return 语句。finally 代码块中对返回值进行操作,是否会响应方法(函数)的返回值,需要根据返回值的数据类型进行确定,如果返回值数类是集合或者数据,finally 代码块中的语句操作返回值,会对方法(函数)的返回值造成影响。

  • 示例2
private List<Integer> testReturn2() {
        List<Integer> list = new ArrayList<>();
        try {
            list.add(1);
            System.out.println("try:" + list);
            return list;
        } catch (Exception e) {
            list.add(2);
            System.out.println("catch:" + list);
        } finally {
            list.add(3);
            System.out.println("finally:" + list);
        }
        return list;
    }
    // 得到的返回值是
    // list = {1,2,3}

finally 代码块中有return 语句

如果finally 代码块中有 return 语句,则执行到 finally 语句中的 return 语句,直接返回这时候的结果,try 或者 catch 代码块中的return 语句被覆盖了。

  • 示例3
// 方法(函数)此时的返回值是 300
  	private int test2(){
        int i = 0;
        try{
            i = 200;
            System.out.println("try = "+i);
            return i;
        }finally {
            i = 300;
            System.out.println("finally = "+i);
            return i;
        }
    }

try 代码块中抛出异常,finally 代码块中有return 语句,无论是在 方法中的 catch 代码块抛出异常,还是在 方法 后添加异常,让调用者处理,这时候的异常都不能抛出。

  • 示例代码
// 不能抛出异常,正常返回值,且返回值为900
    private int test9() throws Exception {
        int i = 0;
        try{
            for (i =0 ;i < 10;i++){
                if(i == 8){
                    System.out.println("throw Exception");
                    throw new Exception();
                }
            }
        }finally {
            i = 900;
            System.out.println(" ==finally==");
            return i;
        }
    }
    // 不能抛出异常,但是catch 内部处理异常正常执行,返回值为300
    private int test6(){
        int i = 0;
        try{
            for (i =0 ;i < 10;i++){
                if(i == 8){
                    System.out.println("throw Exception");
                    throw new Exception();
                }
            }
            i = 100;
            System.out.println("try  i ="+i);
            return i;
        }catch (Exception e){
            i = 200;
            System.out.println("catch Exception i ="+i);
            throw new RuntimeException("测试异常2");
        }finally {
            i = 300;
            System.out.println("finally i= "+i);
            return i;
        }
    }

finally 和 锁的结合使用

在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。 java.concurrent.LockShouldWithTryFinallyRule.rule.desc

因此finally 和 锁的正确使用代码为:

lock.lock();
 try {
 doSomething();
 doOthers();
 } finally {
 lock.unlock();
 }

时间格式化

  • 检查条目描述

SimpleDateFormat 是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。 说明:如果是JDK8的应用,可以使用instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

时间格式化字母问题
  • 检查条目描述

日期格式化字符串[%s]使用错误,应注意使用小写‘y’表示当天所在的年,大写‘Y’代表week in which year。 日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。

时间格式化不同的字母代表不同的含义,尤其是大写字母和小写字母的区别需要特别注意。

字母

含义

y


Y

Week Year 当天所在的周属于的年份

M

年中的月份

m

小时中的分钟数

D

年份中的天数

d

月份中的天数

H

24小时制

h

12小时制

a

am / pm 标记

F

月份中的第几个星期

E

星期几

s

分钟的秒数

S

毫秒

代码书写规范

  • 检查条目描述

long或者Long初始赋值时,必须使用大写的L,不能是小写的l,小写容易跟数字1混淆,造成误解。

其他

  • 检查条目描述

iBATIS自带的queryForList(String statementName,int start,int size)不推荐使用

在android项目中不会遇到这个情况,可以忽略。