2.Java中List.subList使用的陷阱

无论是在学习中还是在工作中,对于String类型我们并不陌生,也会经常使用到subString()方法。

使用subString()方法得到子字符串,当子字符串添加一个元素改变时,并不影响原来的字符串。

subString(1,3)包含1索引位置的字符,不包含3索引位置的字符。

看下面的例子,也许你会更加明白!

public class SubStringTest {

    private static String str;
    private static String subStr;

    public static void main(String[] args) {
        str = "01234";
        subStr = str.substring(1,3);
        print();
        subStr += "5";
        System.out.println("----------此时将subStr中添加了一个5----------");
        print();

    }
    
    /**
     * 输出
     */
    public static void print(){
        System.out.println("str    = "    + str);
        System.out.println("subStr = " + subStr);
    }
}

结果:

 然而,在List集合中有这么一个方法subList()。第一眼看上去和String中的subString()很是相像,这使我们会想当然的认为它们是类似的。如果我们这样想就陷入了误区,空口白牙谁会相信,好了,废话不多说,上菜吧!

import java.util.ArrayList;
import java.util.List;

public class SubListTest {
    private static List list;
    private static List subList;

    public static void main(String[] args) {
        list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        subList = list.subList(1,3);

        System.out.println("==========对子List修改(增、删、改)之前==========");
        print();

        System.out.println("==========对子List修改(增、删、改)之后==========");
        subList.add("a");
        print();
    }


    /**
     * 输出
     */
    public static void print(){
        System.out.println("原List: " + list);
        System.out.println("子List: " + subList);
    }
}

结果:

 从结果来看,由subList()方法得到子List,当子List中添加一个元素a后,原List也跟着增加了。看到这里,我们会有疑问,这是为什么呢?要弄明白这个问题,我们需要来翻一翻源码了。

以ArrayList中的subList()来分析,下面是ArrayList的subList()的源码:

public List<E> subList(int fromIndex, int toIndex) {  
    subListRangeCheck(fromIndex, toIndex, size);  
    return new SubList(this, 0, fromIndex, toIndex);  
} 

该方法返回的是ArrayList的内部类SubList的一个实例,同时也将当前的ArrayList对象作为第一个参数传入该构造方法。我们看一下这个构造方法的源码:

private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;

    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }
}

通过这个构造方法的源码可知,在创建这个内部类ArrayList.SubList的实例时,会将外部类ArrayList的引用作为该内部类对象中的parent。也就是说,这个ArrayList.SubList内部类实例中的parent字段会拥有ArrayList对象的一个引用, 只是添加了一定的偏移量而已. 由于 List 中存放的元素都是引用类型, 而非基本类型, 所以, 这个子 List 中的每一个元素所代表的引用, 其实就和原 List 中在相同索引处偏移 fromIndex 位置后的那个位置上的元素所代表的引用, 二者指向的是相同的对象. 我们换用更直白的方式来说, 就是: 

假设有1,2,3,4,5这5个字符串,被依次添加到原List中,假设我们将原List称作ListA,这时,这5个对象中的每一个都分别被一个引用指向着,这些引用刚好就是ListA中存放的所有元素。(PS:ListA中存放的元素其实是引用,而不是对象本身)这时对ListA执行了subList(1,3)方法,创建了一个子List,我们将这个子List称作ListB。那么这时,"1","4","5"各自还只是被一个引用指向着,但是"2","3"却分别被两个引用指向着,一个引用来自ListA,一个来自ListB,请看下面表格:

在创建子List(即:ListB)之前,每个String被引用的情况如下:

对象  被指向的引用 被指向的引用的总数
         "1"  ListA.get(0)                      1
         "2" ListA.get(1)                      1
         "3" ListA.get(2)                      1
         "4" ListA.get(3)                      1
         "5" ListA.get(4)                      1

在创建子 List ( 即: listB) 之后, 各个 Integer 对象被引用指向的情况如下: 

对象 被指向的引用 被指向的引用的总数
"1"  ListA.get(0) 1
"2"  ListA.get(1)  ListB.get(0) 2
"3"  ListA.get(2)  ListB.get(1) 2
"4"  ListA.get(3) 1
"5"  ListA.get(4) 1

留意红色的字. 在创建了 listB, 也就是子 List 以后, "2"和"3"对象, 都分别被 listA 和 listB中各有一个引用所指向着. 而且还有个规律: listB 中每一个元素(其实这里的元素是引用, 不是对象本身) 所指向的对象, 都会同时被两个引用所指向着. 所以, 对于这些同时被两个引用所指向的对象来说, 不论是用哪个引用来修改这些对象的值, 或者对他们进行增删, 都将影响到另外一个引用的指向结果.

 ListA.get(0)

猜你喜欢

转载自www.cnblogs.com/xmc-ccz/p/12718386.html