• Java基础学习总结--容器的遍历


    Java基础学习总结--容器的遍历

    通常,我们可以使用for、for each以及迭代器iterator进行遍历。但是这三种方法其实还是有很大的区别的。

    一、for、for each以及iterator的用法和区别:

    (一)相同点: 三个都可以用来遍历数组和集合

    (二)不同点:

    1. 形式差别
    //for的形式是
    for(int i=0;i<arr.size();i++){...}
    
    //foreach的形式是
    for(int i:arr){...}
    
    //iterator的形式是
    Iterator it = arr.iterator();
    while(it.hasNext()){ 
        object o =it.next(); 
        ...
    }
    
    1. 条件差别:
      for需要知道集合或数组的大小,而且需要是有序的(有下标),不然无法遍历;
      foreach和iterator都不需要知道集合或数组的大小,他们都是得到集合内的每个元素然后进行处理

    2. 多态差别:

      for和foreach都需要先知道集合的类型,甚至是集合内元素的类型,即需要访问内部的成员,不能实现态;
      iterator是一个接口类型,他不关心集合或者数组的类型,而且他还能随时修改和删除集合的元素。

      该例子很好的体现了多态差别:

    public void display(Iterator<object> it){
        while(it.hasNext()){
            system.out.print(it.next()+"");
        }
    }
    

    当我们需要遍历不同的集合时,我们只需要传递集合的iterator(如arr.iterator())。这就是iterator的好处,他不包含任何有关他所遍历的序列的类型信息,能够将遍历序列的操作与序列底层的结构分离。迭代器统一了对容器的访问方式,这也是接口的解耦的最好体现。

    1. 用法差别
      (1)for循环一般用来处理比较简单的有序的,可预知大小的集合或数组;

      (2)foreach可用于遍历任何集合或数组,而且操作简单易懂,但是需要了解集合内部类型,不能修改或者删除集合内部的元素;
      (3)iterator是最强大的,可以随时修改或者删除集合内部的元素,并且是在不需要知道元素和集合的类型的情况下进行的(原因可参考第三点:多态差别),当你需要对不同的容器实现同样的遍历方式时,迭代器是最好的选择!

      补充:

      for each循环里不能remove或者add元素的原因

      Java中迭代器实现的原理

    二、for,for each和iterator的选择使用:

       (一) 如果是ArrayList,用三种方式遍历的速度是for>Iterator>foreach,但基本上属于同一个速度级别;
      (二)如果是LinkedList,则三种方式遍历的差距很大了,用for遍历的效率远远落后于foreach和Iterator,Iterator>foreach>>>for

      下面对ArrayList和LinkedList的普通for循环效率进行测试以及分析原因。

    ArrayList和LinkedList使用普通for循环遍历速度对比

    public class ListIteratorTest{
        private final static int LIST_SIZE = 1000;
        
        public static void main(String[] args){
            List<Integer> arrayList = new ArrayList<Integer>();
            List<Integer> linkedList = new LinkedList<Integer>();
            
            for (int i = 0; i < LIST_SIZE; i++){
                arrayList.add(i);
                linkedList.add(i);
            }
            
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < arrayList.size(); i++){
                arrayList.get(i);
            }
            System.out.println("ArrayList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
            
            startTime = System.currentTimeMillis();
            for (int i = 0; i < linkedList.size(); i++){
                linkedList.get(i);
            }
            System.out.println("LinkedList遍历速度:" + (System.currentTimeMillis() - startTime) + "ms");
        }
    }
    

    不断增大LIST_SIZE,运行结果如下:

    从运行结果我们看到,按倍数增大List容量,ArrayList的遍历显得比较稳定,而LinkedList的遍历几乎是爆发式的增长。

    ArrayList使用普通for循环遍历快的原因

    先看一下ArrayList的get方法源代码:

    public E get(int index) {
    RangeCheck(index);
    
    return (E) elementData[index];
    }
    

    看到ArrayList的get方法只是从数组里面拿一个位置上的元素罢了。我们有结论,ArrayList的get方法的时间复杂度是O(1)

    我们都知道在计算机底层,数据都是有地址的,就像人有住址一样。假设有代码:

    int[3] ints = {1, 3, 5};
    

    在Java中一个int型数据是4个字节,此时计算机内部做的事情是,在内存空间中找到一块连续的、足以存放3个4字节也就是12字节的数组的内存空间,并返回该内存空间的首地址。比方说该内存空间的首地址是0x00吧,那么1就放在0x00-0x03中、3就放在0x04-0x07中、5就放在0x08-0x0B中。

    这时就很简单了,取ints[1]的时候,计算机就会算出ints[1]的数据是存放在以0x04开头,占据4个字节空间的内存中,因此,计算机会从0x04~0x07这块地址空间中读取数据出来。

    整个过程,和数组有多大,并没有关系,计算机做的只是算出起始地址-->去该地址中取数据而已,因此我们看到使用普通for循环遍历ArrayList的速度很快,也很稳定。

    LinkedList使用普通for循环遍历慢的原因

    再看一下LinkedList的get方法做了什么:

    public E get(int index) {
        return entry(index).element;
    }
    
    private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }
    

    由于LinkedList是双向链表,因此第6行的意思是算出i在一半前还是一半后,一半前正序遍历、一半后倒序遍历,这样会快很多,当然,先不管这个,分析一下为什么使用普通for循环遍历LinkedList会这么慢。

    原因就在第7-8行,第10-11行的两个for循里面,以前者为例:

    1、get(0),直接拿到0位的Node0的地址,拿到Node0里面的数据

    2、get(1),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,拿到Node1里面的数据

    3、get(2),直接拿到0位的Node0的地址,从0位的Node0中找到下一个1位的Node1的地址,找到Node1,从1位的Node1中找到下一个2位的Node2的地址,找到Node2,拿到Node2里面的数据。

    后面的以此类推。

    也就是说,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。假如我有10个数据,那么将要查询1+2+3+4+5+5+4+3+2+1=30次数据,相比ArrayList,却只需要查询10次数据就行了,随着LinkedList的容量越大,差距会越拉越大。其实使用LinkedList到底要查询多少次数据,大家应该已经很明白了,来算一下:按照前一半算应该是(1 + 0.5N) * 0.5N / 2,后一半算上即乘以2,应该是(1 + 0.5N) * 0.5N = 0.25N2 + 0.5N,忽略低阶项和首项系数,得出结论,LinikedList遍历的时间复杂度为O(N2)N为LinkedList的容量

    时间复杂度有以下经验规则:

    O(1) < O(log2N) < O(n) < O(N * log2N) < O(N2) < O(N3) < 2N < 3N < N!

    根据以上的分析,各位Java程序员朋友们,切记一定不要使用普通for循环去遍历LinkedList。使用迭代器或者foreach循环(foreach循环的原理就是迭代器)去遍历LinkedList即可,这种方式是直接按照地址去找数据的,将会大大提升遍历LinkedList的效率。

    Java新手,若有错误,欢迎指正!

    本文参考自:

    Java遍历总结:for、for each和迭代器iterator

    To Java程序员:切勿用普通for循环遍历LinkedList

  • 相关阅读:
    iOS 手势操作:拖动、捏合、旋转、点按、长按、轻扫、自定义
    一个基于MVVM的TableView组件化实现方案
    代码审查和不良编程习惯
    十二步创建你的第一个JavaScript库
    可简单避免的三个 JavaScript 发布错误
    巧用Javascript将相对路径地址转换为绝对路径
    jquery 事件对象属性小结
    26个Jquery使用小技巧
    应用于网站导航中的 12 个 jQuery 插件
    使用 jQuery 避免鼠标双击
  • 原文地址:https://www.cnblogs.com/Java-biao/p/12534535.html
Copyright © 2020-2023  润新知