概述

在开发中我们经常使用asList去把一个数组转换为List。也存在通过subList、subMap、来对List、Map、Set来进行类似使用subString方法来对String对象进行分割处理一样来对它们进行分割处理。

但是,asList存在一些缺陷。而sub方法也存在一些瑕疵。

使用8个基本类型数组转换为列表时会存在一个比较有味的缺陷。先看如下程序:

public static void main(String[] args) {

        int[] ints = {1,2,3,4,5,6} ;
List list = Arrays.asList(ints) ;
System.out.println("list.size() = "+list.size());
}
------------- output ------------
list.size() = 1

  程序的运行结果并没有像我们预期的那样是5而是逆天的1,这是什么情况?先看源码:

public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

  asLsit接受的参数是一个泛型的变长参数。我们知道基本数据类型是无法泛型化的。也就是说8个基本类型是无法作为asList的参数。要想作为泛型参数就必须使用其所对应的包装类型。但是,这个实例中为什么没有出错。并且返回 1 了 ? 因为该实例是将int类型的数组当了其参数。而在Java中数组是一个对象。它是可以泛型化的。所以该例子是不会产生错误的。既然例子是将整个int 类型的数组当做泛型参数。那么,经过asList转化就只有一个int 的列表了。如下:

public static void main(String[] args) {

        int[] ints = {1,2,3,4,5,6} ;
List list = Arrays.asList(ints) ; System.out.println("list 的类型 :"+list.get(0).getClass());
System.out.println("list.get(0) == ints :"+list.get(0).equals(ints));
}
------------ output ------------
list 的类型:class [I
list.get(0) == ints:true

  从这个运行结果我们可以充分证明 list里面的元素就是 int 数组。弄清楚这点了,那么修改方法也就一目了然了:将int 改变为Integer。

public static void main(String[] args) {

        Integer[] ints = {1,2,3,4,5,6} ;
List list = Arrays.asList(ints);
System.out.println("list.size() = " + list.size());
System.out.println("list.get(0) 的类型:" + list.get(0).getClass());
System.out.println("list.get(0) == ints[0]:" + list.get(0).equals(ints[0]));
}
--------- output ------------
list.size() = 6
list.get(0) 的类型:class java.lang.Integer
list.get(0) == ints[0]:true

  细节 : 在使用asList时不要将基本数据类型当作参数

二、asList产生的列表不可操作

对于上面的实例我们再做一个小小的修改:

public class asList {

    public static void main(String[] args) {

        Integer[] ints = {1,2,3,4,5,6} ;
List list = Arrays.asList(ints); list.add(7);
System.out.println("list = "+list );
}

  该实例就是讲ints通过asList转换为list 类别,然后再通过add方法加一个元素,这个实例简单的不能再简单了,但是运行结果呢?打出我们所料:

运行结果尽然抛出UnsupportedOperationException异常,该异常表示list不支持add方法。这就让我们郁闷了,list怎么可能不支持add方法呢?难道jdk脑袋堵塞了?我们再看asList的源码:

public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

  asList接受参数后,直接new 一个ArrayList,到这里看应该是没有错误的啊?别急,再往下看:


private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a; ArrayList(E[] array) {
if (array==null)
throw new NullPointerException();
a = array;
}
//.................
}

  这是ArrayList的源码,从这里我们可以看出,此ArrayList不是java.util.ArrayList,他是Arrays的内部类。该内部类提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改变list结果的方法从AbstractList父类继承过来,同时这些方法也比较奇葩,它直接抛出UnsupportedOperationException异常:

通过这些代码可以看出asList返回的列表只不过是一个披着list的外衣,它并没有list的基本特性(变长)。该list是一个长度不可变的列表,传入参数的数组有多长,其返回的列表就只能是多长。所以: 细节 : 不要试图改变asList返回结果的列表,不然坐看枫叶飘落

三、subList返回仅仅只是一个视图

首先我们先看如下实例:

  public static void main(String[] args) {

           List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
list1.add(2); //通过构造函数新建一个包含list1的列表 list2
List<Integer> list2 = new ArrayList<Integer>(list1); //通过subList生成一个与list1一样的列表 list3
List<Integer> list3 = list1.subList(0, list1.size()); //修改list3
list3.add(3); System.out.println("list _1 = "+list1+" list _2 = "+list2+" list _3 = "+list3); System.out.println("list1 == list2:" + list1.equals(list2));
System.out.println("list1 == list3 :" + list1.equals(list3));
}

  这个例子非常简单,无非就是通过构造函数、subList重新生成一个与list1一样的list,然后修改list3,最后比较list1 == list2?、list1 == list3?。按照我们常规的思路应该是这样的:因为list3通过add新增了一个元素,那么它肯定与list1不等,而list2是通过list1构造出来的,所以应该相等,所以我们猜想的结果应该是: >list1 == list2:true >list1 == list3 : false 首先我们先不论结果的正确与否,我们先看subList的源码:

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

  subListRangeCheck方式是判断fromIndex、toIndex是否合法,如果合法就直接返回一个subList对象, 注意在产生该new该对象的时候传递了一个参数 this ,该参数非常重要,因为他代表着原始list。

/**
* 继承AbstractList类,实现RandomAccess接口
*/
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;
} //set方法
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
} //get方法
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
} //add方法
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
} //remove方法
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
}

  

该SubLsit是ArrayList的内部类,它与ArrayList一样,都是继承AbstractList和实现RandomAccess接口。
同时也提供了get、set、add、remove等list常用的方法。但是它的构造函数有点特殊,在该构造函数中有两个地方需要注意:
1、this.parent = parent;而parent就是在前面传递过来的list,也就是说this.parent就是原始list的引用。

2、this.offset = offset + fromIndex;this.parentOffset = fromIndex;。同时在构造函数中它甚至将modCount(fail-fast机制)传递过来了。

  我们再看get方法,在get方法中return ArrayList.this.elementData(offset + index);这段代码可以清晰表明get所返回就是原列表offset + index位置的元素。同样的道理还有add方法里面的:

parent.add(parentOffset + index, e);

this.modCount = parent.modCount;

  remove方法里面的

E result = parent.remove(parentOffset + index);

this.modCount = parent.modCount;

  诚然,到了这里我们可以判断subList返回的SubList同样也是AbstractList的子类,同时它的方法如get、set、add、remove等都是在原列表上面做操作,它并没有像subString一样生成一个新的对象。所以 subList返回的只是原列表的一个视图,它所有的操作最终都会作用在原列表上;。 那么从这里的分析我们可以得出上面的结果应该恰恰与我们上面的答案相反:

list _1 = [1, 2, 3] list _2 = [1, 2] list _3 = [1, 2, 3]

list1 == list2:false

list1 == list3:true

  细节 :subList返回的只是一个原列表的视图而已,它所有的操作最终都会作用在原列表上

四、subList生成子列表后,不要试图去操作原列表

从上面我们知道subList生成的子列表只是原列表的一个视图而已,如果我们操作子列表它产生的作用都会在原列表上面表现,但是如果我们操作原列表会产生什么情况呢?


public static void main(String[] args) {

         List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
list1.add(2); //通过subList生成一个与list1一样的列表 list3
List<Integer> list3 = list1.subList(0, list1.size());
//修改原列表 list1的数据
list1.add(3); System.out.println("list1.size:" + list1.size()+" list1的元素 :"+list1);
System.out.println("list3.size:" + list3.size()+" list3的元素 :"+list3); }

  该实例如果不产生意外,那么他们两个list的大小都应该都是3,但是偏偏事与愿违,事实上我们得到的结果是这样的:

list1正常输出,但是list3就抛出ConcurrentModificationException异常,看过我另一篇博客的同仁肯定对这个异常非常,fail-fast?不错就是fail-fast机制,在fail-fast机制中,(更多请点这里:fail-fast机制)。我们再看size方法:

public int size() {
checkForComodification();
return this.size;
}

  

size方法首先会通过checkForComodification验证,然后再返回this.size。


private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}

  

该方法表明当原列表的modCount与this.modCount不相等时就会抛出ConcurrentModificationException。同时我们知道modCount 在new的过程中 “继承”了原列表modCount,只有在修改该列表(子列表)时才会修改该值(先表现在原列表后作用于子列表)。而在该实例中我们是操作原列表,原列表的modCount当然不会反应在子列表的modCount上啦,所以才会抛出该异常。

对于子列表视图,它是动态生成的,生成之后就不要操作原列表了,否则必然都导致视图的不稳定而抛出异常。最好的办法就是将原列表设置为只读状态,要操作就操作子列表:

public static void main(String[] args) {

         List<Integer> list1 = new ArrayList<Integer>();
list1.add(1);
list1.add(2); //通过subList生成一个与list1一样的列表 list3
List<Integer> list3 = list1.subList(0, list1.size()); //对list1设置为只读状态
list1 = Collections.unmodifiableList(list1); //修改list3
list1.add(3); System.out.println("list1.size:" + list1.size()+" list1的元素 :"+list1);
System.out.println("list3.size:" + list3.size()+" list3的元素 :"+list3); }

  Exception in thread “main” java.lang.UnsupportedOperationException
        at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055)
        at hu.subList.main(subList.java:22)

细节 : :生成子列表后,不要试图去操作原列表,否则会造成子列表的不稳定而产生异常

五、推荐使用subList处理局部列表

在开发过程中我们一定会遇到这样一个问题:获取一堆数据后,需要删除某段数据。例如,有一个列表存在1000条记录,我们需要删除100-200位置处的数据,可能我们会这样处理:

for(int i = 0 ; i < list1.size() ; i++){
if(i >= 100 && i <= 200){
list1.remove(i);
/*
* 当然这段代码存在问题,list remove之后后面的元素会填充上来,
* 所以需要对i进行简单的处理,当然这个不是这里讨论的问题。
*/
}
}

  

这个应该是我们大部分人的处理方式吧,其实还有更好的方法,利用subList。在前面LZ已经讲过,子列表的操作都会反映在原列表上。所以下面一行代码全部搞定:

list1.subList(100, 200).clear();

  

asList和subList的缺陷的更多相关文章

  1. Java提高配(三七)-----Java集合细节(三):subList的缺陷

    我们经常使用subString方法来对String对象进行分割处理,同时我们也可以使用subList.subMap.subSet来对List.Map.Set进行分割处理,但是这个分割存在某些瑕疵. 一 ...

  2. 集合之subList的缺陷

    我们经常使用subString方法来对String对象进行分割处理,同时我们也可以使用subList.subMap.subSet来对List.Map.Set进行分割处理,但是这个分割存在某些瑕疵. 一 ...

  3. Java集合详解8:Java集合类细节精讲

    今天我们来探索一下Java集合类中的一些技术细节.主要是对一些比较容易被遗漏和误解的知识点做一些讲解和补充.可能不全面,还请谅解. 本文参考:http://cmsblogs.com/?cat=5 具体 ...

  4. 7.通用程序设计_EJ

    第45条: 将局部变量的作用域最小化 该条目与第13条(使类和成员的可访问性最小)本质上是类似的.要使局部变量的作用域最小化,最有利的方法就是在第一次使用它的地方声明.在每个局部变量的声明处都应该包含 ...

  5. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  6. Java集合详解8:Java的集合类细节精讲

    Java集合详解8:Java集合类细节精讲 今天我们来探索一下Java集合类中的一些技术细节.主要是对一些比较容易被遗漏和误解的知识点做一些讲解和补充.可能不全面,还请谅解. 本文参考:http:// ...

  7. chessy 提高篇系列 阅读笔记

    java提高篇(一)—–理解java的三大特性之封装 封装的好处, 汇聚属性和方法 减少修改对 其他处的影响 控制get和set方法. java提高篇(二)—–理解java的三大特性之继承 继承的好处 ...

  8. Java集合详解8:Java集合类细节精讲,细节决定成败

    <Java集合详解系列>是我在完成夯实Java基础篇的系列博客后准备开始写的新系列. 这些文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查 ...

  9. 读《Effect Java中文版》

    读<Effect Java中文版> 译者序 序 前言 第1章引言 1   第2章创建和销毁对象 4 第1条:考虑用静态工厂方法代替构造函数 4 第2条:使用私有构造函数强化singleto ...

随机推荐

  1. Protocol handler start failed

    问题描述: 启动项目的时候出现的中文大体意思是:协议处理程序启动失败看着这个启动失败,下意识就想是不是端口占用了,结果换个端口还是不行,于是百度了一个办法 问题解决: 打开任务管理器,找到Java的后 ...

  2. scala基础篇---- Try finally不加catch的使用情形

    普通的try-catch-finally Try{ } catch{//不加catch向上抛出异常 case  _=> } finally{//一般是资源关闭 } 普通的try-finally ...

  3. 时间轮机制在Redisson分布式锁中的实际应用以及时间轮源码分析

    本篇文章主要基于Redisson中实现的分布式锁机制继续进行展开,分析Redisson中的时间轮机制. 在前面分析的Redisson的分布式锁实现中,有一个Watch Dog机制来对锁键进行续约,代码 ...

  4. SpringCloud微服务实战——搭建企业级开发框架(四):集成SpringCloud+SpringBoot

    1.在GitEgg工程的根目录,最上级父pom.xml文件中引入需要依赖的库及Maven插件,设置编码方式: <!--?xml version="1.0" encoding= ...

  5. 【Java虚拟机11】线程上下文类加载器

    前言 目前学习到的类加载的知识,都是基于[双亲委托机制]的.那么JDK难道就没有提供一种打破双亲委托机制的类加载机制吗? 答案是否定的. JDK为我们提供了一种打破双亲委托模型的机制:线程上下文类加载 ...

  6. 如何访问位于内网的Ubuntu主机

    如何访问位于内网的Ubuntu主机 内网主机为Ubuntu桌面版 内网主机Ubuntu字符串界面版 SSH远程主机管理工具推荐 SSH远程文件访问工具推荐 如何访问位于内网的Ubuntu主机 内网主机 ...

  7. 写了10000条Airtest截图脚本总结出来的截图经验,赶紧收藏!

    前言 今天想先给大家分享1个小白用户的Airtest从入门到放弃的故事: 小A是一个自动化的小白,在逛测试论坛的时候,偶然间发现了Airtest这个基于图像识别的UI自动化框架. 出于好奇,小A试用了 ...

  8. 【二食堂】Alpha - Scrum Meeting 5

    Scrum Meeting 5 例会时间:4.15 12:30 - 13:00 进度情况 组员 昨日进度 今日任务 李健 1. 主页搭建结束issue2. 与后端协商确定接口的设计3. 查找文本区域功 ...

  9. POJ 3692 Kindergarten(二分图最大独立集)

    题意: 有G个女孩,B个男孩.女孩彼此互相认识,男孩也彼此互相认识.有M对男孩和女孩是认识的.分别是(g1,b1),.....(gm,bm). 现在老师要在这G+B个小孩中挑出一些人,条件是这些人都互 ...

  10. vue三级路由显示+面包屑

    问题一:如何让三级路由内容显示显示在一级路由页面 可以说是我点级二级路由导航的时候是不发生跳转的,但还要去动态的生成面包屑 const routes = [{ path: '/', name: 'Ho ...