在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的常见方式有下面两种

    //创建一个空的数组,当我们向这个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通过迭代器返回的元素的顺序。

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)方法
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的数组,创建的代码如下:

//这段代码效率较高,我们开发过程中可以借鉴使用
elementData = Arrays.copyOf(elementData, newCapacity);
  1. addAll(Collection<? extends E> c)方法
//将集合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)
//将下标位置的元素替换成新的元素,并且返回原来位置上的元素
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方法比较相等。

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中的代码,其他版本的会有不同),同时还定义了头尾指针:

...
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的构造

 //构造一个空的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不会增加
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操作

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)。
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都是非线程安全的,如果想要得到线程安全的类,可以通过线面的操作进行包装。

	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,开发者经常会这样做:

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的实例),可以通过以下方式:

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. 参考

公众号推荐

欢迎大家关注我的微信公众号「程序员自由之路」

【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. SpringBoot中获取微信用户信息从未如此简单!

    前言 不知道你是否参加过拼多多上邀请微信好友砍价功能,这个功能实现首先需要考虑的就是获取微信用户的信息.获取用户信息就是获取公众号下微信用户的信息,今天我就来讲讲如何从公众号下获取微信用户信息. 需要 ...

  2. Docker Compose基本使用-使用Compose启动Tomcat为例

    场景 Docker-Compose简介与Ubuntu Server 上安装Compose: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/deta ...

  3. struts2表单提单细节处理

    1. 上传文件 大部分项目避免不了要上传文件. struts2提供了封闭的上传文件的入口, 网络上也存在大量的插件用于网页表单中上传文件. 由于自己习惯用SSH框架, 所以介绍一下struts2中文件 ...

  4. vscode使用sftp同步服务器文件

    环境介绍 服务器:腾讯云 + centos + onestack搭建好的lnmp环境 本地:mac 安装openssh sudo yum install openssh-client openssh- ...

  5. Java第三次作业第三题

    3. 请补充下面的Socket通信程序内容: (1)Socket通信中的服务端程序:ChatServerSocket.java package naizi; import java.io.*; imp ...

  6. Angular4+Koa2+MongoDB开发个人博客

    **文章原创于公众号:程序猿周先森.本平台不定时更新,喜欢我的文章,欢迎关注我的微信公众号.** ![file](https://img2018.cnblogs.com/blog/830272/201 ...

  7. prometheus-operator告警模块alertmanager注意事项(QQ邮箱发送告警)--大大坑

    在/stable/prometheus-operator/values.yaml配置告警邮件 config: global: resolve_timeout: 5m smtp_smarthost: ' ...

  8. 【面试必备】小伙伴栽在了JVM的内存分配策略。。。

    周末有小伙伴留言说上周面试时被问到内存分配策略的问题,但回答的不够理想,小伙伴说之前公号里看过这一块的文章的,当时看时很清楚,也知道各个策略是干嘛的,但面试时脑子里清楚,心里很明白,但嘴里就是说不清楚 ...

  9. UnicodeDecodeError: 'gbk' codec can't decode byte 0xb0 in position 279: illegal multibyte sequence

    with open(r'E:\yy\mysql.txt') as wk: print(wk.readlines()) Traceback (most recent call last): File & ...

  10. python + selenium 环境搭建及问题

    搭建平台windows 准备工具如下: ------------------------------------------------------------- 下载python https://w ...