java全端课--迭代

73 阅读8分钟

一、迭代器

迭代:循环遍历的意思。遍历:挨个查看元素的行为。

迭代器:遍历集合元素的工具。

迭代器是为Collection系列的集合服务的。Map系列的集合通常要通过 keySet、values 、entrySet方法转换为Collection系列的集合再遍历。

1.1 Iterator接口

Collection系列的集合的底层实现各不相同,有数组、链表等,具体的遍历细节肯定不相同,但是Java为了统一标准,为所有Collection系列的集合设计了统一的迭代器的接口java.util.Iterator。

这个接口有2个抽象方法:

  • boolean hasNext():判断是否还有元素可迭代
  • E next():取出迭代器当前位置的元素,然后让迭代器走向下一个元素。
package com.mytest.iter;

import org.junit.Test;
import java.util.*;

public class TestIterator {
    @Test
    public void test1(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //这里方法是Collection接口提供的
        Iterator<String> iterator = list.iterator();//这句代码可以得到一个迭代器对象
        while(iterator.hasNext()){
            String s = iterator.next();
            System.out.println(s);
        }
    }

    @Test
    public void test2(){
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1,"hello");
        map.put(2,"world");
        map.put(3,"java");

        /*
        Map接口及其实现类,都没有实现Iterable接口,因此不能对Map及其实现类,直接使用foreach循环和Iterator迭代器进行遍历。
        只能将Map先转为Collection系列的集合。通过(1)keySet(2)values(3)entrySet方法转换
        Map与Collection类型无父子类关系,怎么能转换呢?
        因为这里的转换,不是强制类型转换。而是将元素重新组装到新的集合类型中。
         */
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();
        while(iterator.hasNext()){
            //现在Set中元素的类型是 Map.Entry类型,它有2个属性,属性的类型分别是Integer和String
            Map.Entry<Integer, String> s = iterator.next();
            System.out.println(s);
        }
    }
}

1.2 foreach循环与Iterator有什么关系?

foreach循环是一种语法糖。

语法糖(Syntactic Sugar)是指在编程语言中添加的某种语法,这种语法本身不会增加语言的功能,但是可以使程序更加简洁易读,减少编写代码时的重复工作。语法糖通常是为了提高开发效率和代码可读性而设计的。

foreach在遍历数组时,其实本质上仍然普通for循环。

foreach在遍历集合时,其实本质上仍然迭代器。

1.3 Iterable接口

Comparable VS Comparator
Iterable   VS Iterator
形容词      VS 名词
    
Comparable:是要比较大小的元素类自己实现的。 Comparator:是需要单独一个类(有名字,或匿名)来实现它。
Iterable:需要被遍历的集合自己实现的。这里就是所有Collection系列的集合实现。 Iterator:是需要单独一个类(有名字,或匿名)来实现它。 

凡是实现Iterable接口的集合,都可以使用foreach循环进行遍历,本质上都是使用Iterator进行遍历。在这些集合内部都会有一个单独的类,来实现Iterator接口。

例如:ArrayList 实现了 Iterable接口,所以它支持foreach循环。 ArrayList的内部类有一个内部类 Itr ,它实现Iterator接口。

例如:HashSet 实现了 Iterable接口,所以它支持foreach循环。HashSet 的内部本质上是HashMap,HashMap类有一个内部类KeyIterator ,它实现Iterator接口。

思考:若自写的MyArrayList,和MyLinkedList 集合,它们是模仿ArrayList和LinkedList,能不能直接使用foreach循环遍历呢?

  • 如果没有实现Iterable接口,就不支持。实现了Iterable接口,就支持。
  • 实现Iterable接口,就要重写 Iterator iterator()方法,以便可以获取到Iterator的对象。

1.4 列表迭代器ListIterator

列表是指所有的List系列的集合,与Set、Map等无关。

ListIterator是Iterator的子接口。但是,它比Iterator功能更强大。它在遍历List集合的时候:

  • 可以实现从头到尾遍历,也可以实现从尾到头遍历
  • 在遍历时,可以获取元素的下标
  • 在遍历时,可以实现增、删、改、查
package com.mytest.iter;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.ListIterator;

public class TestListIterator {
    @Test
    public void test1(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //演示从头到尾遍历
        ListIterator<String> listIterator = list.listIterator();
        while(listIterator.hasNext()){
            String s = listIterator.next();
            System.out.println(s);
        }
    }
    @Test
    public void test2(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //演示从尾到头遍历
        ListIterator<String> listIterator = list.listIterator(list.size());
        //迭代器一开始 [size]位置,第一次previous()取[size-1]位置的元素
        while(listIterator.hasPrevious()){//判断前面还有没有元素可迭代器
            String s = listIterator.previous();//previous取迭代器当前位置的前一个位置的元素
            System.out.println(s);
        }
    }

    @Test
    public void test3(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //在所有包含"a"字母的单词后面添加一个“test”
        //演示从头到尾遍历
        ListIterator<String> listIterator = list.listIterator();
        while(listIterator.hasNext()){
            String s = listIterator.next();
            if(s.contains("a")){//这里contains是String类的方法
                listIterator.add("test");//迭代器的add方法,不是集合的add方法
            }
        }

        //再次查看结果
        System.out.println(list);//[hello, java, test, world]
    }

    @Test
    public void test4(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //把所有包含"a"字母的单词改为大写
        //演示从头到尾遍历
        ListIterator<String> listIterator = list.listIterator();
        while(listIterator.hasNext()){
            String s = listIterator.next();
            if(s.contains("a")){//这里contains是String类的方法
                listIterator.set(s.toUpperCase());//迭代器的set方法,不是集合的set方法
            }
        }

        //再次查看结果
        System.out.println(list);//[hello, JAVA, world]
    }

    @Test
    public void test5(){
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "hello","java","world");

        //删除所有包含"a"字母的单词
        //演示从头到尾遍历
        ListIterator<String> listIterator = list.listIterator();
        while(listIterator.hasNext()){
            String s = listIterator.next();
            if(s.contains("a")){//这里contains是String类的方法
                listIterator.remove();//迭代器的remove方法,不是集合的remove方法
            }
        }

        //再次查看结果
        System.out.println(list);//[hello, world]
    }
}

1.6 foreach或迭代器遍历过程中调用集合的add和remove问题

遍历Collection系列集合有更简便的foreach循环,为什么还要将Iterator?

  • (1)foreach本质上还是Iterator
  • (2)少量集合方法的源码中,使用的是Iterator
  • (3)面试题中,出现了一种新的面试题,如本节标题

当我们在foreach或迭代器遍历集合的过程中,如果调用了集合的add、remove、sort等会影响集合元素个数,元素顺序的方法时,都会有风险,要么出现漏删,要么出现ConcurrentModificationException并发修改异常。Java中的集合设计这个“快速失败”机制就是为了预防不确定的问题。

设计思路是给集合增加了一个modCount的变量,它时刻记录集合元素个数,元素顺序发生的次数,每变一次,modCount就+1。迭代器遍历集合的过程中,会时刻检查modCount是否发生变化,如果是通过迭代器的add、remove等方法操作的集合,它会及时同步 modCount 和expectedModCount的值。如果通过集合的add、remove等方法,那么迭代器就无法实现同步,就会尽快抛出ConcurrentModificationException异常实现快速失败。

结论:

千万不要在foreach或迭代器遍历过程中调用集合的add和remove等方法。

建议:

如果你要根据条件删除,JDK8之后请用 removeIf方法。

如果你要在遍历过程中添加,当然这个仅限于列表集合,那么只能使用迭代器的add方法。

迭代器快速失败机制分析:

ArrayList集合的相关变量:
Object[]  elementData :数组,用于存储元素
int size:用于记录元素个数
int modCount:主要用于记录元素个数变化次数,添加或删除元素等都会使得modCount值++
sort和replaceAll方法也会使得modCount++,因为这两个方法会使得元素顺序或元素值发生大变化,导致未遍历过的元素跑到迭代器已遍历过的位置上去,或已遍历过的元素值发生修改
    
ArrayList内部类Itr的相关变量:
int cursor:迭代器当前游标值,即迭代器当前指向elementData元素的下标值
int lastRet:迭代器刚刚访问过的元素下标值,如果迭代器还未访问过元素,或者刚刚访问过的元素已被删除,那么它的值为-1,表示该元素不存在了。
int exepectedModCount:迭代器“预计的”modCount值,它应该与ArrayList的modCount值相等,否则就说明集合在迭代器之外修改了集合,对集合做了添加或删除元素操作。

1.7 集合与数组的区别

数组集合
元素是否可以是基本数据类型可以不可以,集合只能装对象
是否支持自动扩容不支持支持
是否支持多种数据结构很丰富

列表迭代器与普通迭代器有什么区别?

普通迭代器Iterator列表迭代器ListIterator
关系
遍历哪些集合所有Collection系列仅限于遍历List系列
支持从左往右/从前往后遍历支持支持
支持从右往左/从后往前遍历不支持支持
遍历过程中支持删除吗(迭代器的删除)支持支持
遍历过程中支持添加和修改(迭代器的添加和修改)不支持支持
遍历List的过程中是否支持获取下标不支持支持

问:Iterable 与 Iterator有什么关系 或 区别?

答:Iterable 依赖于 Iterator。Iterable接口有一个抽象方法: Iterator iterator();

Iterable 是集合类本身实现, Iterator是要单独的类,通常是集合中的内部类来实现。例如:ArrayList集合就实现了Iterable 接口,ArrayList的内部有一个内部类Itr实现类Iterator接口。

问:foreach循环与迭代器有什么关系?

答:foreach循环的底层就是使用迭代器来完成遍历集合的。凡是实现Iterable接口的集合,都可以使用foreach循环遍历。

问:在foreach或Iterator遍历集合的过程中,能不能调用集合的add,remove,sort等方法?

答:千万不能。如果调用了,可能结果不正常,或者发生ConcurrentModificationException异常。

1.8 其他

快捷键:

  • 快速查看一个类的成员有哪些,打开这个类,按快捷键Ctrl + F2
  • 查看接口的实现类重写接口方法的源码,按快捷键Ctrl + Alt + B,然后选择接口的实现类