龙空技术网

从浅入深,讲透java的list

空亦语 98

前言:

眼前各位老铁们对“java中的list怎么用”可能比较珍视,各位老铁们都需要分析一些“java中的list怎么用”的相关知识。那么小编同时在网摘上收集了一些关于“java中的list怎么用””的相关内容,希望看官们能喜欢,各位老铁们快快来学习一下吧!

列表只有两种形式,一种是连续内存的数组,一种是通过指针链起来的链表。

数据结构数组

存放在一个连续的空间中。元素的地址与当前的下标为线性关系。也就是说如果起始地址为start,要获取下标为 i 的元素,每个元素的大小为x,那么元素 x 的地址就为add(x)=start+x*i.在java中使用ArrayList来实现。

因此我们可以知道数组具有的特性:

随机访问非常简单,只需要通过起始地址和下标计算即可扩展困难,由于数组需要连续的空间,当我们开了存放n个元素的空间时,如果要存放n+1个则需要根据扩容策略另外开辟一个大于存放n个元素的空间,将原数组拷贝过去,然后将第n+1放到对应位置插入删除困难。将元素插入到i位置,都需要将i位置及以后的元素搬移到后一个位置,将i位置腾出来。同理将第i个元素删除,则需要将第i+1到最后一个元素向前搬移一个位置。

链表

存放在一个非连续的空间中,元素通过一个next指针来存储下一个元素的地址,通过next指针,就能链头(第一个元素)遍历到链尾(最后一个元素)。还可以增加一个before指针来存储当前元素上一个元素的地址,来支持反向遍历,即从链尾遍历到链头。java中使用LinkedList来实现

我们也总结链表的特性:

随机访问困难,想要访问第i个元素,必须从链头通过i个next指针,获取到该元素。扩展简单。当需要新增一个元素时,只需要找个空间放新的元素,并更改对应的next指针,before指针插入删除简单。插入只需将对应的next指针和before指针做更改。删除也一样,将前一个元素的next更改为删除元素的next,将后一个元素的before更改为删除元素的before即可。

遍历

对于数组遍历有以下多种形式:

使用下标遍历iterator遍历for-each循环遍历foreach方法遍历stream的foreach遍历

我们将arrayList和linkedList分开来分析。

ArrayList的遍历

 public static void arraylist(ArrayList<Integer> arrayList){        //1. 下标遍历,get(i)的复杂度为O(1),因此遍历复杂度为O(n)        for(int i=0;i<arrayList.size();i++){            System.out.println( arrayList.get(i));        }        //2. iterator遍历,本质上也是下标获取,it.next为O(1),因此遍历复杂度为O(n)        Iterator<Integer> it=arrayList.iterator();        while(it.hasNext()){            int i=it.next();            System.out.println(i);        }        //3. for-each循环遍历,底层使用iterator实现.因此遍历复杂度为O(n)        for(int i:arrayList){            System.out.println(i);        }        //4. foreach方法遍历。内部使用index遍历实现,与 for(int i=0;i<arrayList.size();i++)类似.因此遍历复杂度为O(n)        arrayList.forEach(item->{            System.out.println(item);        });        //5. stream 的foreach遍历.本质上调用的是ArrayList中的ArrayListSpliterator.forEachRemaining方法。本质上也是个for循环的下标遍历.遍历复杂度O(n)        arrayList.stream().forEach(item->{System.out.println(item);});        //6. stream的foreach并行遍历。与stream类似,也是ArrayListSpliterator.forEachRemaining方法。遍历复杂度O(n)        arrayList.parallelStream().forEach(item->{System.out.println(item);});    }public static void linkedList(LinkedList<Integer> linkedList){         //1. 下标遍历。get(i)的复杂度为O(n),遍历复杂度O(n^2)        for(int i=0;i<linkedList.size();i++){            System.out.println(linkedList.get(i));        }        //2. iterator遍历,使用链表元素遍历,next()复杂度为O(1),遍历复杂度O(n)        Iterator<Integer> it=linkedList.iterator();        while(it.hasNext()){            int i=it.next();            System.out.println(i);        }        //3. for-each循环遍历。底层使用iterator,因此复杂度为O(1),遍历复杂度O(n)        for(int i:linkedList){            System.out.println(i);        }        //4. foreach方法遍历。底层使用itreble的for-each循环遍历,因此复杂度为O(1),遍历复杂度O(n)        linkedList.forEach(item->{System.out.println(item);});        //5. stream的foreach遍历。调用的是linkedList的LLSpliterator的forEachRemaining,也是通过节点的next指针遍历,遍历复杂度O(n)        linkedList.stream().forEach(item->{            System.out.println(item);        });        //6. stream的foreach并行遍历。调用的是linkedList的LLSpliterator的forEachRemaining,也是通过节点的next指针遍历,遍历复杂度O(n)        linkedList.parallelStream().forEach(item->{            System.out.println(item);        });    }
linkedList的遍历
         //1. 下标遍历。get(i)的复杂度为O(n),遍历复杂度O(n^2)        for(int i=0;i<linkedList.size();i++){            System.out.println(linkedList.get(i));        }        //2. iterator遍历,使用链表元素遍历,next()复杂度为O(1),遍历复杂度O(n)        Iterator<Integer> it=linkedList.iterator();        while(it.hasNext()){            int i=it.next();            System.out.println(i);        }        //3. for-each循环遍历。底层使用iterator,因此复杂度为O(1),遍历复杂度O(n)        for(int i:linkedList){            System.out.println(i);        }        //4. foreach方法遍历。底层使用itreble的for-each循环遍历,因此复杂度为O(1),遍历复杂度O(n)        linkedList.forEach(item->{System.out.println(item);});        //5. stream的foreach遍历。调用的是linkedList的LLSpliterator的forEachRemaining,也是通过节点的next指针遍历,遍历复杂度O(n)        linkedList.stream().forEach(item->{            System.out.println(item);        });        //6. stream的foreach并行遍历。调用的是linkedList的LLSpliterator的forEachRemaining,也是通过节点的next指针遍历,遍历复杂度O(n)        linkedList.parallelStream().forEach(item->{            System.out.println(item);        });
Iterator(迭代器)原理

我们从arraylist讲这个,linkedlist的实现方式基本上是一样的。

ArrayList的iterator()方法返回一个Iterator对象,他是ArrayList的内部类Itr类型的。ArrayList中有一个modCount值,这个值主要是用来标识当前list的更改的次数,相当于一个版本号。这个值初始为0,每当调用list的add或remove等增加删除元素的操作时,就会对modCount进行自增。(batch操作的话会增加对应的数)Itr类新建时,会初始化expectedModCount 为modCount,用来记录当前期望的arrayList的版本Itr还会记录两个值,一个cursor表示下一个要访问元素的下标默认为0,一个lastRet表示上一次操作的元素的下标,默认-1。这两个值有什么用呢:在Itr的hasNext方法中,判断cursor是否为当前list的size来返回是否有下一个元素在Itr的next方法中,检查cursor的值是否未越界,检查expectedModCount是否与modCount一致,如果符合则将lastRet赋值为cursor,将cursor自增1,然后返回lastRet下标的元素在Itr的remove方法中,检查expectedModCount是否与modCount一致,检查lastRet不小于0,则调用ArrayList的remove方法移除元素,将cursor回退未lastRet,将lastRet设为-1,同步expectedModCount为modCount的值。

做个总结:

通过迭代器的三个方法,我们可以知道,当迭代器在迭代过程中,如果直接在ArrayList中直接进行修改,导致modCount值增加,而迭代器不知道,expectedModCount就会与modCount不一致,就会使得next和remove都无法执行。这种机制就被称为fail-fast机制在Itr执行remove之后将lastRet设为了-1,也就是说remove不能连续执行,因为remove会先检查lastRet不能小于0

如何删除元素

讲清楚了迭代器原理,我们就可以来看看如何才能正常的删除元素了,以及为什么有些方式删除会出现问题:

同样分开arrayList和linkedList的删除

arrayList的删除

public static void delArrayList(ArrayList<Integer> arrayList) {        //1.1 下标遍历值顺序删除。        //arrayList的remove方法会将第i+1到最后一个元素往前挪动,调用System.arrayCopy()方法。如果从第一个元素开始删除,那是非常低效的        for (int i = 0; i < arrayList.size(); i++) {            if (arrayList.get(i) % 2 == 0) {                arrayList.remove(i);                i--;            }        }        //1.2 下表遍历之逆序删除。        //如果从最后一个元素开始删除,如果连续删除后面n个元素,复杂度为O(n)没有额外的挪动工作,但如果间隔删除复杂度就变成O(n^2)每次删除都会带来元素挪动,间隔删除不建议使用        for (int i = arrayList.size(); i >= 0; i--) {            if (arrayList.get(i) % 2 == 0) {                arrayList.remove(i);                i--;            }        }        //2.迭代器遍历删除。        //本质上调用的是ArrayList的删除方法,而且是正向删除,也就是说每次删除会导致后面的元素挪动,复杂度为O(n^2),不建议使用        Iterator iterator = arrayList.iterator();        while (iterator.hasNext()) {            iterator.remove();        }        // 3. for-each循环不能删除。        //for-each 是基于迭代器的,但没有返回迭代器,无法通过迭代器来修改list,同时如果在遍历过程中,有其他线程对list进行修改,也会造成异常        /* for(Integer i:arrayList){            arrayList.remove(i);        }*/        // 4. foreach方法遍历不能删除        //本质上是for循环,因为没有正确控制index下标,因此也不能正常删除。删除第i个元素后,第i+1元素到最后的元素会往前移,而此时下标会变为i+1也就导致了原先数组的第i+1个元素被跳过的问题        //arrayList.foreach(item->item);        //5. 使用removeIf方法删除        // 使用一个bitSet来记录需要的元素下标,最后再将元素进行统一移动,复杂度O(n),空间复杂度O(n)。推荐使用这种方式        arrayList.removeIf(item -> item % 2 == 0);        //6. stream的foreach方法中也是不能正常删除的。但有filter函数.        //7. filter本质上是将符合条件的元素复制过去,不会对原数组操作,最后需要collect()获取到新的对象。        arrayList.stream().filter(item -> item % 2 == 0);    }public static void delLinkedList(LinkedList<Integer> linkedList){        // 1. 下标删除        // remove方法通过更改链表的节点来删除,删除本身是个O(1)的操作。但定位第i个元素,是个O(n)的操作        //因此连续删除n个元素是个O(n)复杂度,间隔删除n个元素则O(n^2)复杂度。        // 因为linkedList是双向链表,因此与反向删除也一样。        for(int i=0;i<linkedList.size();i++){            linkedList.remove(i);            i--;        }        //2. 迭代器删除        // 删除单个元素为O(1),删除n个元素为O(n)        Iterator iterator=linkedList.iterator();        while(iterator.hasNext()){            iterator.next();            iterator.remove();;        }        //3. removeIf        //使用迭代器的删除。删除n个元素为O(n)        linkedList.removeIf(item->item>2);        //4. stream filter删除。不会对原数组操作。复杂度也是O(n)        linkedList.stream().filter(item->item>2).forEach(item-> System.out.println(item));    }
linkedList的删除
        // 1. 下标删除        // remove方法通过更改链表的节点来删除,删除本身是个O(1)的操作。但定位第i个元素,是个O(n)的操作        //因此连续删除n个元素是个O(n)复杂度,间隔删除n个元素则O(n^2)复杂度。        // 因为linkedList是双向链表,因此与反向删除也一样。        for(int i=0;i<linkedList.size();i++){            linkedList.remove(i);            i--;        }        //2. 迭代器删除        // 删除单个元素为O(1),删除n个元素为O(n)        Iterator iterator=linkedList.iterator();        while(iterator.hasNext()){            iterator.next();            iterator.remove();;        }        //3. removeIf        //使用迭代器的删除。删除n个元素为O(n)        linkedList.removeIf(item->item>2);        //4. stream filter删除。不会对原数组操作。复杂度也是O(n)        linkedList.stream().filter(item->item>2).forEach(item-> System.out.println(item));

标签: #java中的list怎么用 #java里面list用法