日常编程中的小技巧以及注意点(二)

    之前写过一篇《日常编程中的小技巧以及注意点(一)》,感兴趣可以查看。


<1> ArrayList&LinkedList

  • 顺序插入随机访问比较多的场景使用ArrayList。
  • 元素删除中间插入比较多的场景使用LinkedList。

<2> 字符串变量和字符串常量equals的时候将字符串常量写在前面

    这是一个比较常见的小技巧了,如果有以下代码:

        String str = "abc";
        if (str.equals("abc")) {
            //do something... 
        }

    建议修改为:

        String str = "abc";
        if ("abc".equals(str)) {
            //do something... 
        }

    这么做主要是可以避免空指针异常。


<3> 把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、""+数据最慢

    看如下测试代码:

        final int COUNT = 100000000;
        long start = System.currentTimeMillis();
        Integer test = 0;
        for (int i = 0; i < COUNT; i++) {
            String str = String.valueOf(test);
        }
        long end = System.currentTimeMillis();
        System.out.println("String.valueOf():" + (end - start) + "ms");
        start = System.currentTimeMillis();
        test = 0;
        for (int i = 0; i < COUNT; i++) {
            String str = test.toString();
        }
        end = System.currentTimeMillis();
        System.out.println("String.toString():" + (end - start) + "ms");
        start = System.currentTimeMillis();
        test = 0;
        for (int i = 0; i < COUNT; i++) {
            String str = "" + test;
        }
        end = System.currentTimeMillis();
        System.out.println("\"\"+test:" + (end - start) + "ms");

    运行结果:

String.valueOf():503ms
String.toString():446ms
""+test:1642ms

    运行多次后发现:toString()方法最快,String.valueOf()次之,""+方式最慢。那这是为什么呢?String.valueOf的源码如下:

    /**
     * Returns the string representation of the {@code Object} argument.
     *
     * @param   obj   an {@code Object}.
     * @return  if the argument is {@code null}, then a string equal to
     *          {@code "null"}; otherwise, the value of
     *          {@code obj.toString()} is returned.
     * @see     java.lang.Object#toString()
     */
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

    由上可以看到String.valueOf方式会在调用toString()前做空判断,所以要比toString()方式慢一些。

    而""+方式的反编译代码如下:

日常编程中的小技巧以及注意点(二)

    由上可以看到其底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串,所以最慢。


<4> 如何重构“箭头型”代码

    顾名思义,“箭头型”代码如下所示:

    public static String test(Role role) {
        String message = "";
        if (role != null) {
            if (role.getId() != null) {
                if (!"".equals(role.getId())) {
                    if (role.getRoleName() != null) {
                        if (!"".equals(role.getRoleName())) {
                            if (role.getNote() != null) {
                                if (!"".equals(role.getNote())) {
                                    message = "all attribute is not empty";
                                } else {
                                    message = "\"\".equals(role.getNote())";
                                }
                            } else {
                                message = "role.getNote() == null";
                            }
                        } else {
                            message = "\"\".equals(role.getRoleName())";
                        }
                    } else {
                        message = "role.getRoleName() == null";
                    }
                } else {
                    message = "\"\".equals(role.getId())";
                }
            } else {
                message = "role.getId() == null";
            }
        } else {
            message = "role == null";
        }
        return message;
    }

    “箭头型”代码如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的。

    上面这段代码,可以把条件反过来写,就可以把箭头型的代码解掉了,重构的代码如下所示:

    public static String test(Role role) {
        if (role == null) {
            return "role == null";
        }
        if (role.getId() == null) {
            return "role.getId() == null";
        }
        if ("".equals(role.getId())) {
            return "\"\".equals(role.getId())";
        }
        if (role.getRoleName() == null) {
            return "role.getRoleName() == null";
        }
        if ("".equals(role.getRoleName())) {
            return "\"\".equals(role.getRoleName())";
        }
        if (role.getNote() == null) {
            return "role.getNote() == null";
        }
        if ("".equals(role.getNote())) {
            return "\"\".equals(role.getNote())";
        }
        return "all attribute is not empty";
    }

    当然也可以抽取成函数,这样会更容易阅读和更容易维护。对于多个状态的判断和组合,如果复杂了,可以使用“组合状态表”,或是状态机加Observer的状态订阅的设计模式。这样的代码既解了耦,也干净简单,同样有很强的扩展性。


<5> 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长

正例:MAX_STOCK_COUNT/PRIZE_NUMBER_EVERYDAY

反例:MAX_COUNT/PRIZE_NUMBER


<6> 抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类名开始,以Test结尾


<7> POJO类中布尔类型的变量都不要加is前缀,否则部分框架解析会引起序列化错误

反例:定义为基本数据类型Boolean isDeleted;的属性,它的方法名称也是isDeleted(),RPC框架在反向解析的时候,“误以为”对应的属性名称是deleted,导致属性获取不到抛出异常。


<8> 接口类中的方法和属性不要加任何修饰符号

    接口类中的方法和属性不要加任何修饰符号(public也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,必须是与接口方法相关的,并且是整个应用的基础常量。

正例:接口方法签名:void commit();

           接口基础常量:String COMPANY = "alibaba";

反例:接口方法定义:public abstract void commit();

说明:如果JDK8中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。


<9> 枚举类名建议带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开

说明:枚举其实就是特殊的常量类,且构造方法被默认强制为私有。

正例:枚举名字为ProcessStatusEnum的成员名称:SUCCESS/UNKNOWN_REASON


<10> 各层命名规约

1.Service/DAO层方法命名规约如下:

  • 获取单个对象的方法用get作为前缀。
  • 获取多个对象的方法用list作为前缀。
  • 获取统计值的方法用count作为前缀。
  • 插入的方法用save/insert作为前缀。
  • 删除的方法用remove/delete作为前缀。
  • 修改的方法用update作为前缀。

2.领域模型命名规约如下:

  • 数据对象:xxxDO,xxx为数据表名。
  • 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
  • 展示对象:xxxVO,xxx一般为网页名称。
  • POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

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

说明:Long a = 2l;写的是数字的21,还是Long型的2?


<12> 对外部正在调用或者二方库依赖的接口,不允许修改方法签名,以避免对接口调用方产生影响。若接口过时,必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么


<13> 所有相同类型的包装类对象之间值的比较,全部使用equals方法

说明:对于Integer var = ?在-128~127范围内的赋值,Integer对象是在IntegerCache.cache中产生的,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象。这是一个大坑,推荐使用equals方法进行判断。


<14> 关于基本数据类型与包装数据类型的使用标准

  1. 所有的POJO类属性必须使用包装数据类型。
  2. RPC方法的返回值和参数必须使用包装数据类型。
  3. 所有的局部变量使用基本数据类型。

说明: POJO类属性没有初值,是要提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。

正例:数据库的查询结果可能是null,因为自动拆箱,所以用基本数据类型接收有NPE风险。

反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务在调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。


<15> 当序列化类新增属性时,请不要修改serialVersionUID字段,以避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值

说明:注意serialVersionUID不一致会抛出序列化运行时异常。


<16> 泛型通配符<? extends T>用来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,因为其作为接口调用赋值时易出错

说明:扩展说一下PECS(Producer Extends Consumer Super)原则:第一,频繁往外读取内容的,适合用<? extends T>;第二,经常往里插入的,适合用<? super T>。


<17> 不要在foreach循环里进行元素地remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁

正例:

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            if (删除元素的条件) {
                iterator.remove();
            }
        }

反例:

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        for (String item : list) {
            if ("1".equals(item)) {
                list.remove(item);
            }
        }

说明:以上代码的执行结果肯定会出乎大家的意料。试一下把"1"换成"2",会是同样的结果吗?

运行了结果的读者们可能已经看到结果了:"1"不会报错但是换成"2"就会报java.util.ConcurrentModificationException异常。这是为什么呢?ArrayList的remove方法源码如下:

    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    可以看到核心方法是fastRemove,再看一下它的源码:

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        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
    }

    其中需要关注的是modCount这个变量,它代表着改变的次数。也就是说每删除一次元素modCount就会+1。同时add方法也会使modCount++。看下面源码:

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        //do something...

    以上是ArrayList的一个内部类Itr,实现了Iterator接口。foreach每次遍历都是通过它的hasNext和next方法来确定的。checkForComodification方法如下,用来判断modCount和expectedModCount是否相等,问题就出在这里:

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

    所以我们来看一下这两者运行时的区别:第一次"1".equals(item)时remove后,下一次hasNext时,此时cursor(当前位置)=1,size(元素个数)=1,两者相等,所以hasNext返回false,不会进入next方法,所以循环不报错。但是也只是循环了一次而已

    而"2".equals(item)时remove后,下一次hasNext时,此时cursor=2,size=1,两者不等,所以hasNext返回true,进入到next方法中。但是因为上次remove后modCount+1,也就是modCount != expectedModCount,所以checkForComodification方法抛出了ConcurrentModificationException异常。同时这也提示我们不要在foreach增强型for循环中调用add/remove方法。

    那为什么iterator的remove方法就会正确执行呢?让我们看一下它的源码:

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

    关键在于,在执行完remove方法后,会有一步同步expectedModCount和modCount值的操作:expectedModCount = modCount;这样下次循环时就不会抛出异常了。


<18> 在集合初始化时,指定集合初始值大小

说明:HashMap使用HashMap(int initialCapacity)初始化。

正例:initialCapacity = (int)(需要存储的元素个数 / 负载因子 + 1),并且尽量为2的幂。注意负载因子(即loader factor)默认为0.75,如果暂时无法确定初始值大小,请设置为16(即默认值)。

反例 :HashMap需要放置1024个元素,由于没有设置容量初始大小,随着元素不断增加,容量*扩大7次,resize需要重建hash表,这严重影响性能。


<19> 高度注意Map类集合K/V能不能存储null值的情况

集合类 Key Value Super 说明
Hashtable 不允许为null 不允许为null Dictionary 线程安全
ConcurrentHashMap 不允许为null 不允许为null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap 线程不安全

反例:由于HashMap的干扰,很多人认为ConcurrentHashMap是可以置入null值的,而事实上,存储null值时会抛出NPE异常。


<20> 线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式创建,这样的处理方式能让编写代码的工程师更加明确线程池的运行规则,规避资源耗尽的风险

说明:Executors返回的线程池对象的弊端如下:

  • FixedThreadPool和SingleThreadPool:

       允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  • CachedThreadPool和ScheduledThreadPool:

       允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。