Java集合之常见问题

目录

遍历集合的多种方法

(1)概述

(2)iterable接口

(3)Iterator迭代器

(4)foreach遍历集合

(5)Lambda表达式遍历集合

(6)选择合理的遍历方式

集合相关的工具类

1、Collections工具类

2、comparable和comparator的区别

集合常见问题

1、快速失败机制

1、快速失败机制的工作方式

2、安全失败机制

3、使用isEmpty()

4、集合转数组

5、数组转集合


遍历集合的多种方法

(1)概述

迭代器的作用

迭代器模式:提供一个方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节。

Java容器的种类有很多种,每种容器都有自己特有的数据结构。

因为容器的内部结构不同,很多时候可能不知道该怎样去遍历一个容器中的元素。所以为了使对容器内元素的操作更为简单,Java引入了迭代器模式,把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。

迭代器的局限

  • 迭代器是针对单列集合使用的,即Collection家族。

  • 只有实现了iterable接口,才能使用迭代器

  • Map集合不能使用迭代器,但用Set集合获取它的键后,可以通过迭代器遍历Map的键,再获取对应的值,达到迭代Map的效果。

  • Iterator迭代器只能从头到尾,单向遍历集合

(2)iterable接口

Java提供了一个Iterable接口。常用的实现了该接口的子接口有:Collection、List、Set等。

该接口的iterator()方法返回一个标准的Iterator实现。

(3)Iterator迭代器

遍历ArrayList

List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");

//使用迭代器遍历ArrayList集合
Iterator<String> listIt = list.iterator();
while(listIt.hasNext()){
    System.out.println(listIt.next());
}

遍历LinkedList

List<String> linkList = new LinkedList<String>();
linkList.add("link1");
linkList.add("link2");
linkList.add("link3");
linkList.add("link4");

//使用迭代器遍历LinkedList集合
Iterator<String> linkIt = linkList.iterator();
while(linkIt.hasNext()){
    System.out.println(listIt.next());
}

遍历Set

Set<String> set = new HashSet<String>();
set.add("set1");
set.add("set2");
set.add("set3");
set.add("set4");

//使用迭代器遍历Set集合
Iterator<String> setIt = set.iterator();
while(setIt.hasNext()){
    System.out.println(listIt.next());
}

使用迭代器遍历集合不需要考虑集合的种类及内部逻辑等,只需要控制迭代器本身,降低了耦合

迭代器遍历集合,就是利用集合对象获取迭代器,利用Iterator类型实例的hasNext()和next()方法。

  • hasNext():判断集合中是否有下一个元素
  • next():返回集合的下一个元素

迭代器注意事项

注意,在使用Iterator时,禁止对所遍历的容器进行改变其大小结构的操作(add、remove)

否则会抛出ConcurrentModificationException(并发修改异常)。

由于foreach的本质也使用了迭代器,所以增强for同样有这个要求

错误代码示例

List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");

//使用迭代器遍历ArrayList集合
Iterator<String> listIt = list.iterator();
while(listIt.hasNext()){
    Object obj = listIt.next();
    if(obj.equals("张三3")){
        //改变集合大小
        list.remove(obj);
    }
}

在list.iterator()之后,迭代器就已经创建出来了。在这之后,不能对集合进行增删。否则java会认为出现了线程不安全的问题

  • 普通的迭代器,在迭代过程中,不能通过集合的方法,对集合本身作出增加元素和删除元素的修改。
  • 但是可以通过迭代器自身的方法Iterator.remove()来删除元素

通过源码,分析迭代器增删集合为什么抛出异常,以及Iterator.remove()为什么不会抛出异常

Iterator的源码

private class Itr implements Iterator<E> {
    //用于遍历集合的索引
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        //检查版本号是否发生了改变
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        //检查版本号是否发生了改变
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            //更新版本号
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        //如果当前集合的版本号,与迭代器的版本号不符,就抛出并发修改异常
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

分析

  • ConcurrentModificationException异常,是由checkForComodification()方法抛出的。

    迭代器每次next,都会先执行checkForComodification()

  • checkForComodification()方法的作用是比较modCount与expectedModCount是否相等,如果不想等,则抛出并发异常

  • modCount是当前集合的版本号,每次修改(增、删)集合都会加1

  • expectedModCount是当前迭代器的版本号,在迭代器实例化时初始化为modCount。

  • 说明,迭代器创建后,迭代器的版本号与集合的版本号相等,之后集合做出修改,集合版本号会加1,与迭代器版本号不相等,迭代器就抛出了并发异常

  • 在Iterator的remove()中同步了expectedModCount的值,所以下次再调用next()的时候,检查不会抛出异常。

Java设计迭代器并发异常的原因

使用该机制的主要目的是为了实现ArrayList中的快速失败机制(fail-fast),在Java集合中较大一部分集合是存在快速失败机制的。

快速失败机制产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过Iterator遍历集合时,该集合的内容被其他线程所改变,则会抛出ConcurrentModificationException异常。

所以要保证在使用Iterator遍历集合的时候不出错误,就应该保证在遍历集合的过程中不会对集合产生结构上的修改。

迭代器提供了删除数据的方法,既保证了功能的满足,又避免了可能出现的误操作现象(别的线程通过集合的方法删除元素,大概率是误操作)

(4)foreach遍历集合

foreach指的就是增强for循环。foreach是一种语法糖,并非方法或关键字,是Java5之后的特性。

实现Iterable接口的对象,才可以成为Foreach语句的目标。

Iterable接口包含一个能产生Iterator对象的方法,Iterable被foreach用来在序列中移动。

foreach要依赖于Iterable接口返回的Iterator对象,所以从本质上来讲,Foreach其实就是在使用迭代器

注意:如果要想使自己自定义的类可以采用foreach语法糖就必须实现Iterable接口。

Java中,数组也可以使用增强for的形式,但本质就不是调用迭代器了,而是转换为普通的遍历

示例 使用foreach遍历集合

List<String> list = new ArrayList<String>();
list.add("张三1");
list.add("张三2");
list.add("张三3");
list.add("张三4");

for (String string : list) {
    System.out.println(string);
}

使用foreach的优势:代码简洁,不易出错,不用担心数组越界,会完完整整遍历一遍,省心

局限性:无法使用索引,需要用到索引时还是需要for i循环

注:foreach通常与泛型结合使用

(5)Lambda表达式遍历集合

本质上也是Foreach,只是简化了写法。

示例

public static void main(String[] args) {

    //创建list对象
    List<String> list = new ArrayList<String>();

    //添加数据
    list.add("张三");
    list.add("李四");
    list.add("王二");
    list.add("麻子");

    //使用Lambda表达式遍历集合元素
    list.forEach((String name)->System.out.println(name));

(6)选择合理的遍历方式

for循环与迭代器的对比

  • ArrayList的随机访问比较快,而for循环中使用的get()方法,采用的即是随机访问的方法,因此在ArrayList里for循环快。
  • LinkedList则是顺序访问比较快,Iterator中的next()方法采用的是顺序访问方法,因此在LinkedList里使用Iterator较快。

各有优势,主要根据集合的数据结构不同去选择

集合相关的工具类

1、Collections工具类

概述

针对集合操作的工具类

常用方法

方法名说明
public static void sort(List list)将指定列表按升序排序
public static int binarySearch(List<?> list,T key)二分查找
public static T max(Collection<?> coll)获取最大值
public static void reverse(List<?> list)反转指定列表中的元素顺序
public static void shuffle(List<?> list)随机排列指定的列表

2、comparable和comparator的区别

comparable出自java.lang包,只有一个方法:

comparator出自java.util包,有很多方法,其中一个和上面的很类似:

它们的用途是不一样的:

  • compareTo:用于在实现了Comparable接口的类中,重写compareTo()方法,定义这个对象的排序规则(比较的是传入的对象和this对象)
  • compare:在给集合排序时,传入Collections.sort()方法,重写作为参数,自定义比较方法。(比较的是传入的两个参数)

集合常见问题

1、快速失败机制

它的目的是,如果一个线程在遍历集合,而其他线程修改了集合,那么遍历集合的线程能够感知到。

1、快速失败机制的工作方式

迭代器保存了一个cursor,作为当前遍历的下标。还有一个lastRet,表示最后一个元素的索引值。

另外迭代器保存了一个expectedModCount,这个东西是在创建迭代器的时候,获取到的集合的版本号,在遍历过程中不会修改,除了remove()方法。

快速失败的原因就是,在每次调用next()方法的最开始,会调用这个checkForComodification()方法来检查集合是否发生过修改:

final void checkForComodification() {
    if (ArrayList.this.modCount != this.expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
public boolean hasNext() {
    return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

迭代器遍历的过程

  • hasNext()会判断是否存在下一个元素,具体方式就是判断当前游标是否等于集合容量。
  • next()是获取下一个元素的值,在迭代过程中,每次都会返回目前cursor的值,然后cursor自增1。直到不存在下一个元素,则迭代结束。

迭代器遍历时删除的一个坑

平时在迭代器中调用集合的remove()方法删除元素,会抛出异常,原因就是:

  • 本次操作之后,集合的modCount发生了变化
  • 遍历时会调用迭代器的hasNext(),发现仍然有下一个元素,就会去调用迭代器的next()
  • 迭代器的next()的最开头,会判断当前集合的modCount和迭代器维护的expectedModCount是否相同,此时不同,就会抛出异常。

而如果删除的是集合的倒数第二个元素,那就不会抛出异常,原因是:

  • 比如集合为1,2,3,4,5,要删除4
  • 遍历到4时,迭代器的游标cursor为4,执行了删除4的逻辑,集合的modCount同样会自增
  • 但是,此时集合的容量变为4,调用hasNext(),发现游标cursor等于了集合容量,此时就会退出循环,不会执行下一次next()

总结:抛出异常的原因并不是删除这个操作本身,而是删除后调用了next(),检查集合的版本号和迭代器的版本号不符,此时才会抛出异常。

2、安全失败机制

fail-safe,采用安全失败机制的集合容器,不是直接在集合上遍历的,而是先复制原先的集合内容,在拷贝的集合上进行遍历。

所以,在遍历过程中对原集合做的修改,不能被迭代器检测到,所以不会抛出ConcurrentModificationException异常。

java.util.concurrent包下的集合都是安全失败的,所以支持并发修改

3、使用isEmpty()

对一个集合判空,有两种方式:isEmpty()、size() == 0

  • isEmpty()的时间复杂度是O(1)
  • 大多数集合,size()的时间复杂度也是O(1),但是java.util.concurrent下的某些集合不是O(1)

4、集合转数组

使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

5、数组转集合

使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法

因为Arrays.asList() 方法返回的并不是ArrayList ,而是 java.util.Arrays 的一个内部类

这个内部类并没有实现集合的修改方法,这些方法默认是直接抛异常的。

如果想要获得一个ArrayList,就这样做:

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值