Java基础汇总(十九)——Java集合细节

一、初始容量(ArrayList)

  • ArrayList每次新增一个元素,就会检测ArrayList的当前容量是否已经到达临界点,如果到达临界点则会扩容1.5倍
  • ArrayList的扩容是通过申请新的空间,拷贝原来数组生成新的数组,这个过程相当耗费资源
  • 若事先已知集合的使用场景和集合的大概范围,最好是指定初始化容量,这样对资源的利用会更加好。尤其是大数据量的前提下,效率的提升和资源的利用会显得更加具有优势
  • Collection的初始容量异常重要,所以:对于已知的情景,请为集合指定初始容量

例1(list1未指定初始容量,list2指定初始容量,list2的运行速度是list1的2倍)

public static void main(String[] args) {
    StudentVO student = null;
    long begin1 = System.currentTimeMillis();
    List<StudentVO> list1 = new ArrayList<>();
    for(int i = 0 ; i < 1000000; i++){
        student = new StudentVO(i,"chenssy_"+i,i);
        list1.add(student);
    }
    long end1 = System.currentTimeMillis();
    System.out.println("list1 time:" + (end1 - begin1));
    
    long begin2 = System.currentTimeMillis();
    List<StudentVO> list2 = new ArrayList<>(1000000);
    for(int i = 0 ; i < 1000000; i++){
        student = new StudentVO(i,"chenssy_"+i,i);
        list2.add(student);
    }
    long end2 = System.currentTimeMillis();
    System.out.println("list2 time:" + (end2 - begin2));
}


list1 time:1638
list2 time:921

Arraylist的add(): 

public boolean add(E e) {  
        ensureCapacity(size + 1);   
        elementData[size++] = e;  
        return true;  
    }  

public void ensureCapacity(int minCapacity) {  
    modCount++;         //修改计数器
    int oldCapacity = elementData.length;    
    //当前需要的长度超过了数组长度,进行扩容处理
    if (minCapacity > oldCapacity) {  
        Object oldData[] = elementData;  
        //新的容量 = 旧容量 * 1.5 + 1
        int newCapacity = (oldCapacity * 3)/2 + 1;  
            if (newCapacity < minCapacity)  
                newCapacity = minCapacity;  
      //数组拷贝,生成新的数组 
      elementData = Arrays.copyOf(elementData, newCapacity);  
    }  
}

二、asList

1.避免使用基本数据类型数组转换为列表

        例2(基本类型数组用asList转换为列表):

                程序的运行结果并没有像我们预期的那样是5而是1

public static void main(String[] args) {
        int[] ints = {1,2,3,4,5};
        List list = Arrays.asList(ints);
        System.out.println("list'size:" + list.size());
    }

//outPut:
//list'size:1

asList源码:

  • asList接受的参数是一个泛型的变长参数,基本数据类型是无法泛型化
  • 8个基本类型是无法作为asList的参数的, 要想作为泛型参数就必须使用其所对应的包装类型
public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

但是例2并没有出错的原因:

        因为该实例是将int类型的数组当做其参数,而在Java中数组是一个对象,它是可以泛型化的。所以该例子是不会产生错误的。既然例子是将整个int类型的数组当做泛型参数,那么经过asList转换就只有一个int 的列表了,如下:

public static void main(String[] args) {
    int[] ints = {1,2,3,4,5};
    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数组

修改例2(int变为Ineteger):

public static void main(String[] args) {
        Integer[] ints = {1,2,3,4,5};
        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:5
//list.get(0) 的类型:class java.lang.Integer
//list.get(0) == ints[0]:true

2.asList产生的列表不可操作

例3(ints通过asList转换为list 类别,然后再通过add方法加一个元素):

        运行结果抛出UnsupportedOperationException异常,该异常表示list不支持add方法(java.util.ArrayList中基本的add方法)

public static void main(String[] args) {
        Integer[] ints = {1,2,3,4,5};
        List list = Arrays.asList(ints);
        list.add(6);
    }



//output:
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(Unknown Source)
    at java.util.AbstractList.add(Unknown Source)
    at com.chenssy.test.arrayList.AsListTest.main(AsListTest.java:10)

asList源码:

  • asList接受参数后,直接new 一个ArrayList
  • 此ArrayList不是java.util.ArrayList,他是Arrays的内部类
//asList接受参数后,直接new 一个ArrayList

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


//此ArrayList不是java.util.ArrayList,他是Arrays的内部类
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;
        }
        //.................
    }
  •  Arrays的内部类提供了size、toArray、get、set、indexOf、contains方法,而像add、remove等改变list结果的方法从AbstractList父类继承过来,同时这些方法也比较奇葩,它直接抛出UnsupportedOperationException异常
public boolean add(E e) {
        add(size(), e);
        return true;
    }
    
    public E set(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
    
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }

        通过上述代码可以看出asList返回的列表只不过是一个披着list的外衣,它并没有list的基本特性(变长)。

        该list是一个长度不可变的列表,传入参数的数组有多长,其返回的列表就只能是多长。所以:不要试图改变asList返回的列表

三、SubList的缺陷

        除了SubString方法可以用来对String对象进行分割处理外,也可以使用SubList、SubMap、SubSet来对List、Map、Set进行分割处理

1.subList返回仅仅只是一个视图

例4(通过构造函数、subList重新生成一个与list1一样的list,然后list3通过add新增了一个元素,最后比较list1 == list2?、list1 == list3?):

public static void main(String[] args) { 
    List list1 = new ArrayList(); 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("list1 == list2:" + list1.equals(list2));
    System.out.println("list1 == list3:" + list1.equals(list3));
}

正常来说,output应该为:

list1 == list2:true
list1 == list3: false

实际output:

list1 == list2:false

list1 == list3:true

SubList源码:

  • SubList是ArrayList的内部类,它与ArrayList一样,都是继承AbstractList和实现RandomAccess接口。同时也提供了get、set、add、remove等list常用的方法
  • subListRangeCheck方式是判断fromIndex、toIndex是否合法,如果合法就直接返回一个subList对象
  • 在产生该new该对象的时候传递了一个参数 this ,该参数非常重要,因为他代表着原始list
public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);
        return new SubList(this, 0, fromIndex, toIndex);
}

    //构造函数
    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;
    }
}

 SubList构造函数中需要注意的地方:

  • this.parent = parent;而parent就是在前面传递过来的list,也就是说this.parent就是原始list的引用
  • this.offset = offset + fromIndex;this.parentOffset = fromIndex;      同时在构造函数中它甚至将modCount(fail-fast机制)传递过来了
    • SubList的get方法:
      • return ArrayList.this.elementData(offset + index):这段代码可以清晰表明get所返回就是原列表offset + index位置的元素
    • SubList的add方法:
      • parent.add(parentOffset + index, e); this.modCount = parent.modCount; 
    • SubList的remove方法:
      • result = parent.remove(parentOffset + index); this.modCount = parent.modCount;

从上述get、add和remove方法可知:

        SubList同样也是AbstractList的子类,同时它的方法如get、set、add、remove等都是在原列表上面做操作,它并没有像subString一样生成一个新的对象

故:

  • subList返回的只是原列表的一个视图,它所有的操作最终都会作用在原列表上!!!

例2输出结果与预想不一样的原因:

  • list1中首先包含1和2
  • 其次通过 List<Integer> list2 = new ArrayList<Integer>(list1);  创造的list2包含1和2
  • List<Integer> list3 = list1.subList(0, list1.size());包含1和2
  • list3通过add方法增加元素3时,其首先会对list1(即原列表进行操作)增加元素3,然后再将元素3添加到list3中
  • 最后输出:list1 == list2:false; list1 == list3:true

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

例5

public static void main(String[] args) { 
    List list1 = new ArrayList(); 
    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());
    System.out.println("list3'size:" + list3.size());
}


//预想输出:
//list1'size:3

//实际输出:
//list1'size:3
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(Unknown Source)
    at java.util.ArrayList$SubList.size(Unknown Source)
    at com.chenssy.test.arrayList.SubListTest.main(SubListTest.java:17)

list1正常输出,但是list3就抛出ConcurrentModificationException异常(即fail-fast机制)

list.size方法:

  • 首先通过checkForComodification验证,然后再返回this.size
  • 在checkForComodification验证会验证modCount与this.modCount是否相等,不相等则抛出ConcurrentModificationException
public int size() {
            checkForComodification();
            return this.size;
        }

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

        modCount 在new的过程中 "继承"了原列表modCount,只有在修改该列表(子列表)时才会修改该值(先表现在原列表后作用于子列表)

        而在例5中我们是操作原列表,原列表的modCount当然不会反应在子列表的modCount上啦,所以才会抛出该异常。

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

//通过subList生成一个与list1一样的列表 list3

List<Integer> list3 = list1.subList(0, list1.size());

3.推荐使用subList处理局部列表

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

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

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

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

四、保持compareTo和equals同步

        在Java中我们常使用Comparable接口来实现排序,其中compareTo是实现该接口方法。我们知道compareTo返回0表示两个对象相等,返回正数表示大于,返回负数表示小于。同时我们也知道equals也可以判断两个对象是否相等

public class Student implements Comparable<Student>{
    private String id;
    private String name;
    private int age;
    
    public Student(String id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public boolean equals(Object obj){
        if(obj == null){
            return false;
        }
        
        if(this == obj){
            return true;
        }
        
        if(obj.getClass() != this.getClass()){
            return false;
        }
        
        Student student = (Student)obj;
        if(!student.getName().equals(getName())){
            return false;
        }
        
        return true;
    }
    
    public int compareTo(Student student) {
        return this.age - student.age;
    }

    /** 省略getter、setter方法 */
}

        Student类实现Comparable接口和实现equals方法,其中compareTo是根据age来比对的,equals是根据name来比对的

public static void main(String[] args){
        List<Student> list = new ArrayList<>();
        list.add(new Student("1", "chenssy1", 24));
        list.add(new Student("2", "chenssy1", 26));
        
        Collections.sort(list);   //排序
        
        Student student = new Student("2", "chenssy1", 26);
        
        //检索student在list中的位置
        int index1 = list.indexOf(student);
        int index2 = Collections.binarySearch(list, student);
        
        System.out.println("index1 = " + index1);
        System.out.println("index2 = " + index2);
    }

        按照常规思路来说应该两者index是一致的,因为他们检索的是同一个对象,但是非常遗憾,其运行结果:

index1 = 0 index2 = 1

为什么会产生这样不同的结果呢?

因为indexOf和binarySearch的实现机制不同:

  • indexOf是基于equals来实现的只要equals返回TRUE就认为已经找到了相同的元素
  • binarySearch是基于compareTo方法的,当compareTo返回0 时就认为已经找到了该元素

        在我们实现的Student类中我们覆写了compareTo和equals方法,但是我们的compareTo、equals的比较依据不同,一个是基于age、一个是基于name

        比较依据不同那么得到的结果很有可能会不同。所以知道了原因,我们就好修改了:将两者之间的比较依据保持一致即可

对于compareTo和equals两个方法我们可以总结为:

  • 使其相等的方式就是两者应该依附于相同的条件。当compareto相等时equals也应该相等,而compareto不相等时equals不应该相等,并且compareto依据某些属性来决定排序

参考文章:

Java-Tutorial/Java集合详解8:Java集合类细节精讲.md at master · h2pl/Java-Tutorial · GitHub

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 一、 JavaSE 部分 1、 Java 基础Java 基础部分(基本语法, Java 特性等) ②关键字 ③面向对象 ④集合部分 2、 Java 高级知识 ①线程 ②锁 ③JDK ④反射 ⑤JVM ⑥GC ⑦ IO 和 NIO, AIO 二、 JavaEE 部分 1、 Spring ①IoC 与 Bean 配置、 管理 ②AOP 与事务、 权限控制 ③S2SH 整合开发 ④Spring, JPA 整合 2、 Hibernate ①ORM 与持久化映射 ②延迟加载、 性能优化 ③HQL 查询、 条件查询、 SQL 查询 ④二级缓存与查询缓存 3、 Struts ①MVC 模式与 Struts 体系 4、 mybatis 5、 MVC 框架 6、 各框架对比与项目优化 7、 JPA ①EJB 三、 Java web 开发核心内容 1、 web 编程基础 ①Tomcat 服务器NOWCODER.COM 牛客网——互联网学习求职必备神器 名企校招历年笔试面试真题, 尽在牛客网 牛客网, 互联网人都在用的学习求职神器 ②JSP 语法, EL, 内置对象 ③Listener 和 filter 2、 Web 编程进阶 ①Servlet、 标签的作用 ②redis ③MVC 和 DAO ④JSTL、 DisplayTag 等常见标签库的用法 3、 Web 编程原理 ① HTTP 协议 ②请求/相应架构原理 ③web 容器 四、 JDBC 编程 1、 SQL 基础 2、 JDBC 基础 ①数据库 ②数据库连接池 ③事物管理, 批处理 3、 JDBC 进阶 五、 XML 编程 1、 XML 基础 2、 XML 进阶 3、 Web service ①WSDL 与 SOAP 协议 六、 计算机网络 1、 网络概述 ①关于分层 2、 运输层 ①TCP 与 UDP ②协议 3、 网络层 ①网际协议 IP ②网际控制报文协议 ICMP ③因特网的路由器选择协议 4、 应用层 ①域名系统 DNS ②电子邮件NOWCODER.COM 牛客网——互联网学习求职必备神器 名企校招历年笔试面试真题, 尽在牛客网 牛客网, 互联网人都在用的学习求职神器 七、 操作系统 1、 操作系统概论 2、 进程的描述与控制 3、 输入输出系统 4、 存储器管理 5、 处理机调度与死锁 八、 算法与数据结构 1、 哈希 2、 树 3、 遍历 4、 链表 5、 数组 6、 排序 7、 堆与栈 8、 队列 9、 高级算法 九、 设计模式 1、 结构型模式 ①代理模式 ②装饰模式 ③适配器模式 2、 创建型模式 ①单例模式 3、 行为型模式 ①策略模式 ②观察者模式 4、 所有模式汇总 十、 场景题 十一、 UML
java面试笔试资料java笔试题大集合及答案题库java笔试题汇总资料188个合集 100家大公司java笔试题汇总.doc 125条常见的java 面试笔试题大汇总.pdf 2011最新整理java经典代码.doc 25个经典的Spring面试问答.docx JavaEE学习笔记.pdf java_Java_学习笔记.pdf Java_Performance.pdf java代码效率优化.docx Java内存模型的历史变迁.docx Java在游戏服务器开发中的应用.docx java基础总结大全.txt Java开发与技术挑战——关于技术的技术思考.docx Java框架研发思考.docx Java程序员们最常犯的10个错误.docx java程序员的就业指导(重点).docx Java程序员面试宝典 .pdf java笔试题大集合及答案 Java经典项目集锦.rar JAVA编程题全集(100题及答案).doc Java面试文档题库 Java面试笔试题库.CHM java面试笔试题库资料合集.zip Java面试问题集.pdf Java面试题以及答案(小生).pdf java面试题(题库全).doc JS 数据库答案.doc Land.the.Tech.Job.You.Love-人人都有好工作—IT行业求职面试必读.pdf Linux命令大全完整版.doc sql查询语句练习.doc Web服务器的工作原理.docx 依赖注入与JSR-330的参考实现——Guice.docx 关于Java框架Vert.x的几点思考.docx 关于堆和栈的那些事.docx 写好Java代码的30条经验总结.docx 华为java笔试面试题2014.doc 多态的理解.docx 大公司最喜欢问的Java集合类面试题.docx 大公司的Java面试题集.doc 就业相关java 广州传智播客JavaEE工程师测试题.doc 广州传智播客JavaEE工程师测试题(带答案的).doc 应聘时最漂亮的回答.docx 当面试官问「你有什么要问我的吗」时,应该问什么?.docx 提高 Java 代码性能的各种技巧.docx 搜狗商业平台Java技术实践.docx 最新JAVA编程题全集(50题及答案).doc 百度历年笔试面试150题.docx 笔试1.doc 答案1.doc 细品这杯香浓的咖啡——阿里中间件高级专家沈询的Java之旅.docx 给你一次机会面试架构师 你会问什么问题?.docx 超全面:程序员跳槽神级攻略.docx 跳还是不跳,是一个问题——跳槽时该如何权衡?.docx 进入IT企业必读的324个JAVA面试题.pdf 阿里2015实习生-客户端笔试题目解析.docx 面试帮-IT面试宝典.apk 面试题库 高吞吐低延迟Java应用的垃圾回收优化.docx 黑马程序员入学Java精华总结.pdf

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值