很久没更新博客了,想到几个小坑,虽然没啥技术含量,但或许有人不知道呢。

1.删除sublist的元素导致原对象元素被删除

看下面这段代码

List<Integer> students=new ArrayList<Integer>();
        for (int i = 0; i <5 ; i++) {
            students.add(i);
        }
        List<Integer> subList=new ArrayList<Integer>();
        subList=students.subList(0,5);
        subList.remove(0);
        subList.remove(1);
        for (int i = 0; i <5 ; i++) {
            System.out.println(i+"="+students.get(i));
        }

students是个list,然后我们新建立了一个subList对象,这个对象截取了students的一部分,我们删除了subList对象里的一些元素,看下运行结果。

0=1
1=3
2=4
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.get(ArrayList.java:433)
	at bai.ListDo.main(ListDo.java:17)

难道说,删除subList对象里的元素也会导致students里的元素被删除?我明明是新建了一个对象啊。然而,事实确实是这样的。 我们要理解一个事情,使用new新建一个对象,只是开辟了一块空间,用来存放这个对象的地址指针,但是这个新建的对象地址,指向的却是原有对象,也就是说,使用subList这个方法的时候,并没有从students里把内容拷贝了一份,仅仅是纪录了一个指针的移动,这样从某种角度来说,是提高了性能节省内存的做法。 看一下subList这个方法的JavaDoc我们就更清楚了。

Returns a view of the portion of this list between the specified
     * <tt>fromIndex</tt>, inclusive, and <tt>toIndex</tt>, exclusive.  (If
     * <tt>fromIndex</tt> and <tt>toIndex</tt> are equal, the returned list is
     * empty.)  The returned list is backed by this list, so non-structural
     * changes in the returned list are reflected in this list, and vice-versa.
     * The returned list supports all of the optional list operations supported
     * by this list.<p>

什么时候会用到subList方法呢,通常是接收到了一个大的list,需要切割成一个个小的子list再加工处理,以减少内存占用和提高性能,如果不注意的话,就很容易触发这种隐形的bug。所以,使用subList时不要轻易做增删操作,要么不使用subList方法,而是手动add.

2.SimpleDateFormat的线程安全问题

很多博客和文章都会告诉我们,一定要注意SimpleDateFormat的线程安全问题,那究竟是怎么回事呢? 看下面的代码

public class DateFormatTest extends Thread {
    @Override
    public void run() {
        while(true) {
            try {
                this.join(2000);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
            try {
                System.out.println(this.getName()+":"+DateUtil.parse("2018-05-05 12:12:12"));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        for(int i = 0; i < 3; i++){
            new DateFormatTest().start();
        }

    }
}

class DateUtil {

    private static final  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static  String formatDate(Date date)throws ParseException{
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException{
        return sdf.parse(strDate);
    }
}

运行这段代码后,会发现Thread-1会报出Exception in thread “Thread-0” Exception in thread “Thread-1” java.lang.NumberFormatException: multiple points 的异常,并且导致Thread-2有一些错误的日期输出。为什么呢,原因在于SimpleDataFormat不是线程安全的,因为SimpleDataFormat里面用了Calendar 这个成员变量来实现SimpleDataFormat,并且在Parse 和Format的时候对Calendar 进行了修改,calendar.clear(),calendar.setTime(date); 为了线程安全和效率的双重兼顾,建议使用ThreadLocal,代码如下:

public class DateUtil1 {

    private static final ThreadLocal<DateFormat> messageFormat = new ThreadLocal<DateFormat>();
    private static final String MESSAGE_FORMAT = "MM-dd HH:mm:ss.ms";

    private static final DateFormat getDateFormat() {
        DateFormat format = messageFormat.get();
        if (format == null) {
            format = new SimpleDateFormat(MESSAGE_FORMAT, Locale.getDefault());
            messageFormat.set(format);
        }

        return format;
    }
}

如果自己没有把握的话,还是建议每次new一个SimpleDataFormat对象。 Java里面还有许多线程不安全的类,使用这些类的时候,务必注意使用同步原语,或者使用new新建一个对象省事,或者使用对应的线程安全的类。比如hashMap对应的ConcurrentHashMap.

3.split的坑

看下面的代码,

String[] re="2|33|4".split("|");
        for (int i = 0; i <re.length ; i++) {
            System.out.println(re[i]);
        }

你以为输出的结果会是2,33,4,实际上却是 2,|,3,3,|,4。为什么呢,稍微看一下split的方法注释就知道了,原来split的分隔符参数实际上是一个正则表达式,而不是普通的字符串。 所以,正确的写法应该是String.split("\|") 当然,这种坑纯粹是由于对Java基本方法的使用不熟悉造成的,是完全可以避免的。