`
cleverGump
  • 浏览: 10765 次
社区版块
存档分类
最新评论

Java中List的subList()方法的使用陷阱

    博客分类:
  • Java
阅读更多

如果没有看过List或者两个常用的实现类ArrayList、LinkedList的subList()方法的源码,而只是通过API文档,那么很多朋友很容易调入一个陷阱。或者有些朋友根据String的subString()方法来推测,List的subList()方法应该和String的subString()方法类似吧。的确,subList()得到的结果确实是该List的一个子list,这没有错,但是在得到该子list的同时,系统还做了一件隐蔽的事情,那就是,将该子List(我们称作LIst B)内部的一个重要的List(我们称作LIst C)引用字段指向了该父List (我们称作LIst A)所指向的对象(也就是说,经过subList()方法运算之后,原先只有父List (LIst A)一个引用指向的对象,现在增加为两个引用指向该对象了,这两个引用分别是List A 和 List C),而之所以说这个内部的List 引用(List C)重要,是因为凡是该子List (List B)后续的增删操作,其实在实现他自己的容量和数据变化之外,还对他内部的这个List引用字段(List C)也进行了相应的增删操作,而List A (也就是原先的父List)和 该List C又同时指向原先List A (原先的父List)所指向的对象,所以在子List(List B)进行增删操作的时候,原先的父List(List A)内存放的内容也必定会一起进行相同的增删变化。

 
先举个例子说明一下,下面分别用常见的ArrayList和LinkedList进行举例 :
public class SubListDemo
{
    public static void main(String[] args)
    {
        System.out.println("---------------ArrayList------------");
        subListTest(new ArrayList<Integer>());
        System.out.println("---------------LinkedList------------");
        subListTest(new LinkedList<Integer>());
    }
 
    private static void subListTest(List<Integer> list)
    {
        if(list == null)
        {
            throw new IllegalArgumentException("Argument " + list + " is null.");
        }
        for(int i = 0; i<5; i++)
        {
            list.add(i);
        }
         
        List<Integer> subList = list.subList(2, list.size());
        // 期望输出和实际输出一致,都是[0, 1, 2, 3, 4]
        System.out.println("Original list: " + list); 
        // 期望输出和实际输出一致,都是[2, 3, 4]
        System.out.println("Sublist:       " + subList);
         
        subList.add(10);
        // 但这里,实际输出结果却可能会出乎我们的意料,我们可能会认为输出结果不变,
 // 但却发现实际输出结果竟然变化了,比原先多了个元素10,变为 [0, 1, 2, 3, 4, 10]
        System.out.println("Original list: " + list);
        // 期望输出和实际输出一致,都是[2, 3, 4, 10]
        System.out.println("Sublist:       " + subList);
    }
}

 

实际输出结果如下:

---------------ArrayList------------
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4, 10]
Sublist:       [2, 3, 4, 10]
---------------LinkedList------------
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4, 10]   // 多了一个元素10
Sublist:       [2, 3, 4, 10]
从上述输出结果的标黄部分可知,在sublist进行add()操作时,原先的list也被add了相同的元素。同样地,sublist进行删除操作也将导致原先的list也会删除相同的元素。
为什么呢? 下面从源码角度来分析原因:
 
先分析ArrayList,下面是ArrayList的subList()方法的源码:
public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
}
该方法其实调用的是 new SubList(this, 0, fromIndex, toIndex); 这个构造方法,注意该构造方法的第一个参数this,我用黄色标注了,需要引起大家的注意。也就是说,我们在调用ArrayList的subList()方法时,他实际上是new了一个ArrayList.SubList对象(我们称作List B)作为返回值,同时在new该对象的时候将当前的ArrayList对象(我们称作List A)作为参数传递给了该ArrayList.SubList的构造方法。 
下面我们来看看ArrayList的这个内部类SubList的部分源码:
private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;
 
        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;
        }
 
       public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }
 
        public E remove(int index) {
            rangeCheck(index);
            checkForComodification();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }
 
        protected void removeRange(int fromIndex, int toIndex) {
            checkForComodification();
            parent.removeRange(parentOffset + fromIndex,
                               parentOffset + toIndex);
            this.modCount = parent.modCount;
            this.size -= toIndex - fromIndex;
        }
 
        public boolean addAll(Collection<? extends E> c) {
            return addAll(this.size, c);
        }
 
        public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
            int cSize = c.size();
            if (cSize==0)
                return false;
 
            checkForComodification();
            parent.addAll(parentOffset + index, c);
            this.modCount = parent.modCount;
            this.size += cSize;
            return true;
        }
// ..............此处省略其他代码...................
}
 刚才提到,在new这个内部类的时候,也将原ArrayList的引用作为参数传了进去,经过查看上面的源码可知,传进去的原ArrayList引用(List A),被赋值给了SubList 类内部一个名为parent的AbstractList引用(我们称作 List C, 见源码第9行),这句代码的含义是:该parent引用(List C)将指向原ArrayList引用(即:List A)所指向的对象,这样该对象将会由一个引用指向变为两个引用指向了,这两个引用有任何一对该对象进行了增删改,都会影响到另一个引用对该对象查询的结果。而我们在得到sublist后,再对该sublist进行增删改操作(见源码中的add,remove等方法)时,都会执行parent的add,remove等方法,而parent和原先的ArrayList引用指向同一个对象,因此parent执行他的add,remove等方法,其实增删的就是原ArrayList所指向的对象,所以我们就不难理解,在调用sublist的add(10)方法,让子list增加一个元素10的时候,为何原先的ArrayList中也会增加一个元素10了。 
 
下面看LinkedList的subList()的源码:
LinkedList中没有定义subList()方法,所以我们就找其父类AbstractSequentialList的源码,发现还是没有定义该方法,于是我们再找其父类的父类AbstractList的源码,终于在该类中找到了subList()方法的定义。
public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
}
 发现需要判断当前类是否是 RandomAccess 的子类,我们双击 RandomAccess ,在Eclipse中按Ctrl+T, 查看该接口的继承关系树,如下:
发现LinkedList并未实现该接口,所以LinkedList的subList()方法调用的是 new SubList<>(this, fromIndex, toIndex)); 该SubList类是AbstractList类的一个内部类,其实这里和前面分析ArrayList的subList()方法类似,也是将LinkedList的引用this作为参数传给了另一个List的内部类(AbstractList.SubList)的构造方法,并且该内部类中同样包含了一个List引用类型的字段(名为l),在new该内部类时,传递的this同样被赋值给了该内部的引用字段l,并且该内部类的增删改查方法同样调用的是该内部引用字段l的增删改查方法。思路和ArrayList的一样。所以就不分析了,直接贴出相关源码,并把重要的代码标黄,如下:
class SubList<E> extends AbstractList<E> {
    private final AbstractList<E> l;
    private final int offset;
    private int size;
 
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > list.size())
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
        l = list;
        offset = fromIndex;
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
 
    public E set(int index, E element) {
        rangeCheck(index);
        checkForComodification();
        return l.set(index+offset, element);
    }
 
    public E get(int index) {
        rangeCheck(index);
        checkForComodification();
        return l.get(index+offset);
    }
 
    public void add(int index, E element) {
        rangeCheckForAdd(index);
        checkForComodification();
        l.add(index+offset, element);
        this.modCount = l.modCount;
        size++;
    }
 
    public E remove(int index) {
        rangeCheck(index);
        checkForComodification();
        E result = l.remove(index+offset);
        this.modCount = l.modCount;
        size--;
        return result;
    }
 
    protected void removeRange(int fromIndex, int toIndex) {
        checkForComodification();
        l.removeRange(fromIndex+offset, toIndex+offset);
        this.modCount = l.modCount;
        size -= (toIndex-fromIndex);
    }
 
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
 
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        int cSize = c.size();
        if (cSize==0)
            return false;
 
        checkForComodification();
        l.addAll(offset+index, c);
        this.modCount = l.modCount;
        size += cSize;
        return true;
    }
}
 
分析到此为止,我们再来回想一下文章开始处提到的那个例子中我们的疑惑,想必现在应该明白原因了吧。 那么,我们有没有什么办法,可以让获得的sublist在进行增删改查时,不会干扰到原list呢?其实是有办法的,根据我们先前的分析,subList()方法的返回值的内部包含一个引用指向了先前的List的对象,导致对该返回值进行增删改查的操作都会干扰到原先的List的内容。所以我们需要对这个方法的返回值进行一番处理,我们有两种处理方式:
(1)将subList()方法得到的结果,再进行一次包装,将他作为一个新的List对象构造方法的参数即可: 对于ArrayList: 
List<Integer> subList = new ArrayList<>(list.subList(2, list.size()));
 对于LinkedList:
List<Integer> subList = new LinkedList<>(list.subList(2, list.size()));
 
(2)将subList()方法得到的结果,也是进行一次包装,只不过是作为另一个List对象addAll()方法的参数传递过去:
对于ArrayList:
List<Integer> subList = new ArrayList<>();
subList.addAll(list.subList(2, list.size()));
 对于LinkedList:
List<Integer> subList = new LinkedList<>();
subList.addAll(list.subList(2, list.size()));
这样,输出的结果就是:
Original list: [0, 1, 2, 3, 4]
Sublist:       [2, 3, 4]
Original list: [0, 1, 2, 3, 4]   // 这里没有增加元素10了
Sublist:       [2, 3, 4, 10]
  • 大小: 24 KB
分享到:
评论
1 楼 cleverGump 2015-05-17  
请教下大家,我想把重点代码标黄,但文中只有this被成功标黄了,其他都显示成了如下的html代码 <span style="background-color: #ffff00;">,请问我该怎么处理才能标黄呢?在此先提前谢谢了!

相关推荐

    java中利用List的subList方法实现对List分页(简单易学)

    在Java中,`List`接口提供了`subList(int fromIndex, int toIndex)`方法,用于返回列表的一个子列表,这个子列表是从`fromIndex`(包含)到`toIndex`(不包含)的元素。这意味着,你可以根据当前页数和每页的记录数...

    关于Java中List对象的分页思想-按10个或者n个数对list进行分组

    在Java中, List对象的分页思想可以通过使用subList()方法来实现。subList()方法可以将一个大的List对象分割成多个小的List对象,每个小的List对象包含一定数量的元素。例如,以下代码将一个大的List对象分割成多个...

    java的list取之方法

    List是Java中最常用的集合类型之一,理解并熟练掌握它的使用方法对于日常开发来说至关重要。通过上面的学习,我们可以看到List提供了丰富的API来帮助我们处理数据。无论是简单的元素增删改查还是更复杂的排序和过滤...

    javaList的定义和方法

    - `List`接口中的方法`add`, `remove`, `set`等操作可能会影响迭代器,因此在遍历列表时应避免修改列表,除非使用`Iterator`的`remove()`方法。 - `List`接口还提供了一些其他的方法,如`contains()`, `indexOf()`, ...

    为什么阿里要慎重使用ArrayList中的subList方法

    2. **并发修改异常**:如果原ArrayList在多线程环境下被修改,而subList同时也在另一个线程中使用,可能会引发`ConcurrentModificationException`,因为修改原ArrayList会影响subList的视图。 3. **不可预测的行为*...

    JavaList.rar_javalist

    在本教程中,我们将深入探讨如何创建和使用`Java List`。 首先,我们需要导入`java.util.List`和`java.util.ArrayList`(或`java.util.LinkedList`)这两个包。`ArrayList`和`LinkedList`都是`List`接口的实现类,...

    Java List集合的应用.rar

    在这个“Java List集合的应用.rar”压缩包中,我们可以预见到一系列关于如何在实际场景中使用List集合的实例,例如学生管理系统中的注册、登录和日志管理等常见功能。 首先,让我们深入了解一下Java List集合的基础...

    重写subList

    在Java编程中,`subList`方法是`ArrayList`、`LinkedList`等集合类的一个重要功能,它允许我们从原始列表中创建一个子列表,这个子列表是原列表的一个视图,而不是复制。当我们调用`subList`时,对子列表的操作会...

    javalist.zip_list集合 java

    例如,在数据库操作中,可以将查询结果存储到List中;在处理用户输入时,可以将多个输入项保存为List;在设计模式中,适配器模式或装饰器模式也可能用到List来组合多个对象。 另外,List接口还定义了一些特有的方法...

    英语Sublist list1答案

    英语Sublist list1答案

    java中将一个List等分成n个list的工具方法(推荐)

    3. 在循环中,使用subList方法将原始List分成小List,并将其添加到结果List中。 4. 如果余数remainder大于0,则将余数部分分配给每个小List。 5. 最后,返回结果List。 测试例子 以下是一个简单的测试例子: ```...

    list常用方法

    在Java编程语言中,`List`接口是集合框架的重要组成部分,它提供了有序元素集合的实现,并允许重复元素的存在。本文将详细解释`List`接口中的常用方法,帮助开发者更好地理解和运用这些功能强大的工具。 #### 1. ...

    java list常用方法总结

    1. 截取指定部分:使用 subList() 方法可以截取 List 中的指定部分,例如: ```java List&lt;String&gt; sublist = new ArrayList(alist.subList(2, 4)); ``` 这里截取 alist 中的第 2 个到第 4 个元素,并将其存储在 ...

    java中对List分段操作的实例

    2. 使用subList方法实现List分段:Java中提供了一个名为subList的方法,可以用于将List拆分成多个小的List。subList方法的语法是list.subList(from, to),其中from是起始索引,to是结束索引。例如,list.subList(0, ...

    Java List简介_动力节点Java学院整理

    Java List 是一种集合框架中的接口,它代表了一个有序的、可重复的元素序列。与数组类似,List 可以通过角标访问元素,...在实际应用中,应根据具体场景选择合适的 List 实现类,并合理使用 List 的方法和迭代器功能。

    java中List集合及其实现类的方法详解

    Java 中 List 集合及其实现类的方法详解 List 集合是 Java 中最基本的集合类型之一,它提供了许多方法来操作和管理集合中的元素。在 Java 中,List 集合有多种实现类,例如 ArrayList、LinkedList、Vector 等,每种...

    实现对List的分页

    5. `content`:当前页的数据,即从原始List中截取出来的子List。 `ListPageModel`类的构造函数可以接收原始List、当前页数和每页显示的条目数作为参数,然后通过以下步骤计算和设置相关属性: 1. 计算总页数:`...

    Java 实例 - List 截取源代码+详细指导教程.zip

    在Java编程语言中,List接口是集合框架的重要组成部分,提供了有序的元素存储和访问功能。本教程将深入探讨如何在Java中截取List,并通过源代码实例进行详细讲解。我们将覆盖以下几个关键知识点: 1. **List接口**...

    Java List 用法实例详解

    Java 中的 List 集合有多种实现,每种实现都有其特点和应用场景,了解这些特点和应用场景可以帮助我们更好地使用 List 集合。 在实际开发中,List 集合经常用来存储数据,我们可以使用 List 集合来存储用户信息、...

Global site tag (gtag.js) - Google Analytics