摘自:https://www.cnblogs.com/54chensongxia/p/11722828.html

目录


在Java中,集合框架的使用频率非常高。在平时开发中,集合常常被用来装盛其他数据,或者用来实现常见的数据结构比如数组、队列和栈等。Java中集合主要可以分为Collection和Map两个大类。Collection又分为List、Queue和Set(见下图)。本篇博客主要来介绍下List集合。

图片. Java集合体系

关于List集合,主要掌握ArrayList和LinkedList。同时需要注意是这两个类都不是线程安全的。


1. ArrayList#

ArrayList是开发过程中使用的最多的List接口的实现类。这个类底层用数组实现,可以动态调整数组大小。ArrayList集合中可以盛放任意类型的元素,包括null。这个类还提供了操作数组大小的方法用于store列表。ArrayList和Vector类似,最大的区别就是ArrayList是非线程安全的而Vector是线程安全的。

1.1 ArrayList的构造#

创建ArrayList的常见方式有下面两种

Copy
    //创建一个空的数组,当我们向这个ArrayList中添加第一个元素时,会创建一个默认容量为10的数组
List<String> strList = new ArrayList<String>();
//创建指定长度为16的数组
List<String> strList2 = new ArrayList<String>(16);

对于ArrayList的创建,需要提下capacity和size这两个概念。capacity是指ArrayList底层实现数组的长度,而size是指数组中已经存放的元素的个数。

另外ArrayList还有一个通过Collection创建的构造函数,通过这个构造函数产生的ArrayList中的元素的顺序是Collection通过迭代器返回的元素的顺序。

Copy
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

在使用创建ArrayList的时候,最好能对ArrayList的大小做一个判断,这样有一下几个好处:

  • 节省内存空间(eg.我们只需要放置两个元素到数组,new ArrayList(2));
  • 避免数组扩容,引起的效率下降;

1.2 add方法#

向ArrayList中添加元素常用的两个方法是:

  • add(E e);
  • addAll(Collection<? extends E> c);
  • set(int index, E e);
  1. add(E e)方法
Copy
public boolean add(E e) {
ensureCapacity(size + 1);//确保对象数组elementData有足够的容量,可以将新加入的元素e加进去
elementData[size++] = e;//加入新元素e,size加1
return true;
}
//扩容的逻辑
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList的add(E e)的添加逻辑比较简单,就不把源码全部贴出来了,大家可以自己去看下。大致的添加过程是:首先判断当前数组是不是空数组,如果还是空数组,就创建一个长度是10的默认长度的数组,再将元素添加进去;如果当前的ArrayList不是空数组,判断当前的数组是否已经满了,如果满了就进行扩容(扩容的逻辑是oldCapa+oldCapacity/2,如果这个长度还比所需要的最小长度小,就使用所需的最小长度,如果这个最小值大于了数组的最大长度,就是用Integer.MAX_VALUE作为数组长度),再将元素添加进去。在扩容过程中,ArrayList其实是重新创建了一个长度是newCapacity的数组,创建的代码如下:

Copy
//这段代码效率较高,我们开发过程中可以借鉴使用
elementData = Arrays.copyOf(elementData, newCapacity);
  1. addAll(Collection<? extends E> c)方法
Copy
//将集合c中的元素全部添加到ArrayList的尾部
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
} //在ArrayList的指定位置添加元素,同时将ArrayList中其他元素右移
//这个方法在使用时需要特别注意index的范围
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index); Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved); System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
  1. set(int index,E e)
Copy
//将下标位置的元素替换成新的元素,并且返回原来位置上的元素
public E set(int index, E element) {
rangeCheck(index); E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

1.3 remove方法#

常用的删除方法有如下,这些方法的功能根据方法名很容易就看懂,这边就不贴源代码了。

  • public E remove(int index);
  • public boolean remove(Object o);
  • public boolean removeAll(Collection<?> c);
  • protected void removeRange(int fromIndex, int toIndex);
  • public boolean removeIf(Predicate<? super E> filter);
  • public void clear()。

1.4 查询方法#

查询方法用来查询ArrayList中是否包含某个元素,常用的查询方法有如下几个:

  • public boolean contains(Object o);
  • public int indexOf(Object o);
  • public int lastIndexOf;
  • public E get(int index)。

通过下面的代码可以看出,判断相等的标准是两个元素通过equals方法比较相等。

Copy
public boolean contains(Object o) {
return indexOf(o) >= 0;
} public int indexOf(Object o) {
if (o == null) {//返回第一个null的索引
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {//返回第一个o的索引
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;//若不包含,返回-1
} public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size - 1; i >= 0; i--)
if (elementData[i] == null)
return i;
} else {
for (int i = size - 1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

1.5 一些其他常用方法#

  • public List subList(int fromIndex, int toIndex); //构造子数组,调用这个方法前,做好调用下subListRangeCheck这个方法判断下参数的正确性
  • public Spliterator spliterator(); //获得ArrayListSpliterator可分割迭代器,Spliterator是JDK8中添加的可以并行迭代器,可用于多线程迭代,增加效率
  • public void sort(Comparator<? super E> c); //将集合元素根据某种规则排序
  • public Iterator iterator(); //获取普通的迭代器,也可以通过for循环迭代
  • public Object[] toArray(); //将ArrayList集合中的元素转换成数组;
  • public T[] toArray(T[] a)

1.6 ArrayList小结#

  • ArrayList基于数组方式实现,无容量的限制(最大值是Integer.MAX_VALUE);
  • 添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量,trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间;
  • 线程不安全;
  • add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位;
  • get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1));
  • remove(Object o)需要遍历数组;
  • remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高;
  • contains(E)需要遍历数组。

2. Vector#

Vector和ArrayList非常相似,底层都是通过数组来实现集合的。Vector和ArrayList最大的区别是Vector的很多方法都是用synchronize修饰的,所以是线程安全的。下面列举下两者的主要区别:

  • ArrayList是非线程安全的,Vector是线程安全的;
  • 在创建容器时如果不指定容量,ArrayList会先创建一个空的数组,当第一次添加元素时再将容量扩容到10;Vertor在创建时如果没指定容量会默认创建一个容量为10的数组;
  • ArrayL在扩容时是1.5倍的扩容,Vector是2倍的扩容;
  • ArrayList支持序列化,Vector不支持。

3. LinkedList#

LinkedList的内部维护的是一个双向链表,定义了如下节点(JDK1.8中的代码,其他版本的会有不同),同时还定义了头尾指针:

Copy
...
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
...
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev; Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

3.1 LinkedList的构造#

Copy
 //构造一个空的List
public LinkedList() {
}
//通过Collection集合构造LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

3.2 链表的常见操作#

由于LinkedList底层是有链表实现的,所以LinkedList提供了很多链表的常见操作:

  • private void linkFirst(E e); //头插法;
  • void linkLast(E e); //尾插法;
  • void linkBefore(E e, Node succ); //在某个节点前插入元素e
  • private E unlinkFirst(Node f); //删除头部元素
  • private E unlinkLast(Node l); //删除尾部元素
  • E unlink(Node x); // 删除非空元素x

3.3 add操作#

  • public boolean add(E e); //在链表尾部添加元素;
  • public void add(int index, E element); //在指定位置插入元素element,下标也是从0开始计算的
  • public boolean addAll(Collection<? extends E> c); 添加整个集合
  • public boolean addAll(int index, Collection<? extends E> c); //在指定位置添加集合,下标从0开始计算
  • public E set(int index, E element); //将指定位置的元素设置为element,注意和add的区别,set操作List的size不会增加
Copy
public void add(int index, E element) {
//检查下标合法性
checkPositionIndex(index); if (index == size)
//在尾部插入
linkLast(element);
else
//在指定位置插入
linkBefore(element, node(index));
}

由于底层使用链表,所以LinkedList不会有扩容机制。每次add过后,LinkedList的结构如下:

3.4 remove操作#

Copy
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
} //判断相等的标准也是两个对象通过equals方法比较相等
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
} //清空链表
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}

3.5 查询操作#

查询方法用来查询LinkedList中是否包含某个元素,常用的查询方法有如下几个(和ArrayList中的一致):

  • public boolean contains(Object o);
  • public int indexOf(Object o);
  • public int lastIndexOf;
  • public E get(int index)。
Copy
public boolean contains(Object o) {
return indexOf(o) != -1;
}
//判断的标准也是equals方法
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}

3.6 其他方法#

由于LinkedList还实现了Deque这个接口,所以这个类还包含了许多其他方法。这些方法在介绍Queue时在整理。

3.7 LinkedList小结#

  • LinkedList基于环形双向链表方式实现,无容量的限制;
  • 添加元素时不用扩容(直接创建新节点,调整插入节点的前后节点的指针属性的指向即可);
  • 线程不安全;
  • get(int index):需要遍历链表;
  • remove(Object o)需要遍历链表;
  • remove(int index)需要遍历链表;
  • contains(E)需要遍历链表。

4. 线程安全的List#

上面提到的ArrayList和LinkedList都是非线程安全的,如果想要得到线程安全的类,可以通过线面的操作进行包装。

Copy
    List list = Collections.synchronizedList(new ArrayList(...));

以上代码返回的是一个SynchronizedList。这个类和Vector一样,也是线程安全的。Vector是java.util包中的一个类,而SynchronizedList是java.util.Collections中的一个静态内部类。这两个类都是List的子类,而且都是线程安全的。这两个类有如下的区别:

  • 他们实现同步的方式不一样,Vector使用同步方法来实现同步,SynchronizedList使用同步代码块来实现同步,只是用同步代码块将ArrayList的方法封装了下;
  • 两个扩容的机制不一样,Vector扩容成原来的2倍,但是SynchronizedList扩容成原来的1.5倍;
  • SynchronizedList有很好的扩展和兼容功能,他可以将所有的List的子类转成线程安全的类;
  • SynchronizedList中的方法不全都是同步的,获取迭代器方法listIterator()就不是同步的,所以在使用迭代器进行遍历时要手动进行同步处理(或者使用for循环,再调用get方法)。

这边需要注意的是虽然上述两个类是线程安全的,但是如果我们在迭代时进行增减元素操作,仍然会有fast-fail异常。也就是说线程安全和快速失败机制是无关的,快速失败机制的目的是为了防止在迭代元素的过程中有其他线程改变了当前集合的元素。线程安全是为了解决数据脏读等问题。

5. 一个注意点#

为了将数组转换为ArrayList,开发者经常会这样做:

Copy
List<String> list = Arrays.asList(arr);

使用Arrays.asList()方法可以得到一个ArrayList,但是得到这个ArrayList其实是定义在Arrays类中的一个私有的静态内部类。这个类虽然和java.util.ArrayList同名,但是并不是同一个类。java.util.Arrays.ArrayList类中实现了set(), get(), contains()等方法,但是并没有定义向其中增加元素的方法。也就是说通过Arrays.asList()得到的ArrayList的大小是固定的。

如果在开发过程中,想得到一个真正的ArrayList对象(java.util.ArrayList的实例),可以通过以下方式:

Copy
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));

java.util.ArrayList中包含一个可以接受集合类型参数的构造函数。因为java.util.Arrays.ArrayList这个内部类继承了AbstractList类,所以,该类也是Collection的子类。

6. 总结#

学东西的最终目的是为了能够理解、使用它。下面先概括的说明一下各个List的使用场景。如果涉及到可变长度的“数组”,应该首先考虑用List,具体的选择哪个List,根据下面的标准来取舍。

(01) 对于需要快速插入,删除元素,应该使用LinkedList。

(02) 对于需要快速随机访问元素,应该使用ArrayList。
(03) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector),或者通过Collections工具类将ArrayList和LinkedList包装成线程安全的类再使用。

7. 参考#

作者:写代码的木公

出处:https://www.cnblogs.com/54chensongxia/p/11722828.html

版权:本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注明作者及出处。

 
分类: Java基础

【Java基础】谈谈集合.List的更多相关文章

  1. java基础-Map集合

    java基础-Map集合 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Map集合概述 我们通过查看Map接口描述,发现Map接口下的集合与Collection接口下的集合,它 ...

  2. 第6节:Java基础 - 三大集合(上)

    第6节:Java基础 - 三大集合(上) 本小节是Java基础篇章的第四小节,主要介绍Java中的常用集合知识点,涉及到的内容包括Java中的三大集合的引出,以及HashMap,Hashtable和C ...

  3. Java基础之 集合体系结构(Collection、List、ArrayList、LinkedList、Vector)

    Java基础之 集合体系结构详细笔记(Collection.List.ArrayList.LinkedList.Vector) 集合是JavaSE的重要组成部分,其与数据结构的知识密切相联,集合体系就 ...

  4. 备战金三银四!一线互联网公司java岗面试题整理:Java基础+多线程+集合+JVM合集!

    前言 回首来看2020年,真的是印象中过的最快的一年了,真的是时间过的飞快,还没反应过来年就夸完了,相信大家也已经开始上班了!俗话说新年新气象,马上就要到了一年之中最重要的金三银四,之前一直有粉丝要求 ...

  5. java基础技术集合面试【笔记】

    java基础技术集合面试[笔记] Hashmap: 基于哈希表的 Map 接口的实现,此实现提供所有可选的映射操作,并允许使用 null 值和 null 键(除了不同步和允许使用 null 之外,Ha ...

  6. Java基础--说集合框架

    版权所有,转载注明出处. 1,Java中,集合是什么?为什么会出现? 根据数学的定义,集合是一个元素或多个元素的构成,即集合一个装有元素的容器. Java中已经有数组这一装有元素的容器,为什么还要新建 ...

  7. JAVA基础学习-集合三-Map、HashMap,TreeMap与常用API

    森林森 一份耕耘,一份收获 博客园 首页 新随笔 联系 管理 订阅 随笔- 397  文章- 0  评论- 78  JAVA基础学习day16--集合三-Map.HashMap,TreeMap与常用A ...

  8. 《回炉重造 Java 基础》——集合(容器)

    整体框架 绿色代表接口/抽象类:蓝色代表类. 主要由两大接口组成,一个是「Collection」接口,另一个是「Map」接口. 前言 以前刚开始学习「集合」的时候,由于没有好好预习,也没有学好基础知识 ...

  9. java基础之集合长度可变的实现原理

    首先我们要明白java中的集合Collection,List,ArrayList之间的关系: ArrayList是具体的实现类,实现了List接口 List是接口,继承了Collection接口 Li ...

  10. 【Java基础】集合

    集合 集合概述 一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象 的操作,就要对对象进行存储.另一方面,使用 Array 存储对象方面具有一些弊端,而 Java 集合就像一种容器 ...

随机推荐

  1. java多线程之并发编程

    1.并发不一定比串行更快 因为并发有线程创建和上下文切换的开销 2.java的并发采用内存共享模型 3.单线程中重排序不会影响到结果 但多线程中重排序可能会影响到结果 4.votaile变量 当线程A ...

  2. luogu P5366 [SNOI2017]遗失的答案

    luogu 首先gcd为\(G\),lcm为\(L\),有可能出现的数(指同时是\(G\)的因数以及是\(L\)的倍数)可以发现只有几百个.如果选出的数要能取到gcd,那么对于每种质因子,都要有一个数 ...

  3. LintCode 29---交叉字符串

    public class Solution { /** * @param s1: A string * @param s2: A string * @param s3: A string * @ret ...

  4. 使用GDB和GEF进行调试

    使用GDB进行调试 这是编译ARM二进制文件和使用GDB进行基本调试的简单介绍.在您按照教程进行操作时,您可能需要按照自己的习惯使用ARM程序集.在这种情况下,你要么需要一个备用的ARM设备,或者你只 ...

  5. Android九宫格解锁有多少种姿势

    参考知乎:知乎.

  6. touch cyusbConfig.cmake

    touch cyusbConfig.cmake cmake文件丢失,与其解决问题,不如临时建立一个临时文件

  7. 多线程-生产者消费者(lock同步)

    二.采用Lock锁以及await和signal方法是实现 import java.io.IOException; import java.util.concurrent.locks.Condition ...

  8. 前端_DOM&BOM

    前端BOM BOM:浏览器对象模型 window alert:弹出信息框 alert('String') // 或者 Window.alert("String") confirm: ...

  9. idea无法创建javaclass文件

    一直用pycharm和jupyter. 今天发现打开IDEA 创建一个新的java项目(maven)后无法在里面的module中创建相应的java class文件 解决方案: (1)选择 File—— ...

  10. MYSQL之数据库初识、安装详解、sql语句基本操作

    目录 MYSQL之数据库初识及安装详解 1.什么是数据库? 1.什么是数据?(data) 2.什么是数据库?(databases,简称DB) 2.为什要用数据库? 3.什么是数据库管理系统?(Data ...