Java 中的集合框架之 List接口、ArrayList类、LinkedList类、Vector类(1万字超全详解)

一、集合的框架体系

Java 集合框架提供了一套性能优良,使用方便的接口和类,其位于 java.util 包中, 所以当使用集合框架的时候需要进行导包。
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合;另一种是图(Map),存储键/值对映射。

  • 集合(Collection)体系如下:
    集合(Collection)体系图
  • 图(Map)体系如下:

说明:
(1)Collection 接口有两个重要的子接口 List、 Set , 他们的实现子类都是单列集合。
(2)Map 接口的实现子类是双列集合,存放的 K-V(键值对)

1. 常用集合接口概述

如下表:

接口 描述
Collection 接口 Collection 是最基本的集合接口,一个 Collection 代表一组 Object(即 Collection 的元素), Java不提供直接继承自 Collection的类,只提供继承自 Collection 接口的子接口(如 List和 set)。Collection 接口存储一组不唯一,无序的对象(不能通过索引来访问 Collection 集合中的对象)。
List 接口 List 接口继承自 Collection 接口 ,但 List 接口 是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(即元素在 List 中的位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0。而且 List 集合中允许有相同的元素。可以说,List 接口的集合存储一组不唯一,有序(插入顺序)的对象。
Set 接口 Set 接口继承自 Collection 接口,具有与 Collection 完全一样的接口,只是方法上有部分不同,和 Collection 接口 相同,Set 接口存储一组唯一,无序的对象。
Map 接口 Map 接口与 Collection 接口同级(彼此没有继承关系),Map 图存储一组 键-值 对象,提供key(键)到value(值)的映射。

Set 和 List 接口的区别:

(1)Set 接口集合存储的是无序的,不重复的数据。List 接口集合存储的是有序的,可以重复的元素。

(2)Set 集合 底层使用的是 链表数据结构,其检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 (实现子类有 HashSet , TreeSet 等)。

(3)List 结合 底层和数组类似,但是它可以动态增长,根据实际存储的数据的长度自动增长 List 的长度。其检索元素效率高,插入和删除效率低,插入和删除会引起其他元素位置改变 (实现子类有 ArrayList , LinkedList , Vector 等)。

2. 常用 Collection 集合的实现子类

Java 提供了一套实现了 Collection 接口的标准集合类。其中一些是具体类,这些类可以直接拿来使用,而另外一些是抽象类,提供了接口的部分实现。

如下表:

类名 描述
ArrayList 类 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素。该类实现了可变大小的数组,随机访问和遍历元素时,提供了更好的性能。该类是非同步的, 在多线程的情况下不要使用。ArrayList 类在扩容时会扩容当前容量的1.5倍。
Vector 类 该类和 ArrayList 类非常相似,但该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。
LinkedList 类 该类实现了 List 接口,允许存储 null(空值)元素,且可存储重复元素,主要用于创建链表数据结构,该类没有同步方法,如果多个线程同时访问一个 LinkedList,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候再构造一个同步的 LinkedList 。
HashSet 类 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。
TreeSet 类 该类实现了 Set 接口,不允许存储重复元素,并且不保证集合中元素的顺序,其允许存储 null (空值)元素,但最多只能存储一个。该类可以实现排序等功能。

3. 常用的 Map 图的实现子类

如下表:

类名 描述
HashMap 类 HashMap 类是一个散列表,它存储的内容是键-值对 (key-value) 映射。该类实现了 Map 接口,根据键的 HashCode 值存储元素,具有很快的访问速度,但最多允许一个元素的键为 null (空值),它不支持线程同步。
TreeMap 类 TreeMap 类继承了AbstractMap ,实现了大部分 Map 接口,并且使用一颗树。
HashTable 类 Hashtable 继承自 Dictionary(字典) 类,用来存储 键-值对。
Properties 类 Properties 继承自 HashTable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。
特此说明:由于集合框架的内容繁多,因此本文只介绍 Collection 集合下的 List 接口及其重要实现子类的内容,其余集合框架的知识将会在下篇博文分享。

二、Collection 接口

1. Collection 接口常用方法

  • 说明:所有实现了 Collection 接口的子类集合都可以使用 Collection 接口中的方法。下面使用的是实现了 List 接口的 ArrayList 子类来举例,但实现了 List 接口的子类都可以使用下列方法。
  • 代码实现:
import java.util.ArrayList;
import java.util.List;

public class CollectionMethod {
    
    
    public static void main(String[] args) {
    
    
		// 以实现了Collection 接口 的子类 ArrayList 来举例;
		
        ArrayList list = new ArrayList();

//      1.  add:添加单个元素
        list.add("jack");
        list.add(10);// 底层自动装箱:list.add(new Integer(10))
        list.add(true);// 同上
        System.out.println("list=" + list);// [jack, 10, true]

//      2.  addAll:添加多个元素
        ArrayList list2 = new ArrayList();// 创建一个新的集合
        list2.add("红楼梦");
        list2.add("三国演义");
        list.addAll(list2);
        System.out.println("list=" + list);// [jack, 10, true, 红楼梦, 三国演义]
      
//      3.  remove:删除指定元素,如果不指定则默认删除第一个元素
        list.remove(true);// 指定删除某个元素
        System.out.println("list=" + list);// [jack, 10, 红楼梦, 三国演义]

//      4.  removeAll:删除多个元素
        list.add("聊斋");
        list.removeAll(list2);
        System.out.println("list=" + list);// [jack, 10, 聊斋]
      
//      5.  contains:查找元素是否存在,返回 boolean 值
        System.out.println(list.contains("jack"));// T

//      6.  containsAll:查找多个元素是否都存在,返回 boolean 值
        System.out.println(list.containsAll(list2));// T
      
//      7.  size:获取元素个数
        System.out.println(list.size());// 3

//      8.  isEmpty:判断是否为空
        System.out.println(list.isEmpty());// F

//      9.  clear:清空
        list.clear();
        System.out.println("list=" + list);// []
    }
}

2. 迭代器(Iterator)

  • Iterator(迭代器)不是一个集合,它是一种用于访问 Collection 集合的接口,主要用于遍历 Collection 集合中的元素,所有实现了 Collection 接口的子类集合都可以使用迭代器。

  • 迭代器的执行原理:

  • 迭代器就相当于一个游标,初始时指向集合中的第1个元素的的前一个位置;
  • 首先使用 hasNext() 方法来判断迭代器的下一个位置是否还有元素,
  • 若下一个位置有元素则使用 next() 方法返回下一个元素,并将迭代器的位置向后移一位。
  • 若没有,则不调用 next() 方法,直接退出迭代器。

在这里插入图片描述

迭代器常用方法:
(1)调用 coll.next() 会返回迭代器的下一个元素,并且更新迭代器的状态(迭代器下移)。
(2)调用 coll.hasNext() 判断集合中是否还有下一个元素。
(3)调用 coll.remove() 将迭代器返回的元素删除。

  • 注意:在使用 next() 方法前必须使用 hasNext() 方法判断集合中是否还有下一个元素,否则可能会出现异常。

  • 代码演示:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class CollectionIterator {
    
    
    public static void main(String[] args) {
    
    

        List list = new ArrayList();// 创建一个新的集合,该集合实现了 Collection 接口

        list.add(1);
        list.add(2);
        list.add(3);

        // 1. 先得到 list集合 对应的 迭代器
        // 使用集合的 iterator() 方法来获得该集合的迭代器;
        // 所有实现了 Collection 接口的子类都拥有 iterator() 方法
        Iterator iterator = list.iterator();

        // 2. 使用 while 循环 + 迭代器 遍历集合

		// 首先判断集合中(下一个位置)是否还有元素
        while (iterator.hasNext()) {
    
     

            // 若有,则获取下一个位置的元素,迭代器位置向下移一位
            Object obj = iterator.next();
            // 输出该元素
            System.out.println("obj=" + obj);

        }

        // 3. 注意:当退出 while 循环后 , 这时 iterator 迭代器,指向的是集合中的最后一个元素;
        //  若再次 使用 iterator.next();  会产生 NoSuchElementException 异常,因为集合中下一个位置没有元素存在了; 
        
        // 4. 如果希望再次遍历集合,需要重置 迭代器的状态;
        
        iterator = list.iterator();// 重置迭代器
        System.out.println("===第二次遍历===");
        while (iterator.hasNext()) {
    
    
            Object obj = iterator.next();
            System.out.println("obj=" + obj);
        }
    }
}

3. Collection 集合的遍历

  • 说明:所有实现了 Collection 接口的子类集合都可以使用 下面的遍历方法。

(1)使用迭代器遍历(所有集合中都可以使用 迭代器 来遍历)。
(2)使用普通 for 循环遍历(普通 for 循环 是通过元素的索引来获取元素,在无序的集合中不能此方式来获取元素。比如 实现了 Set 接口的子类集合)。
(3)使用增强 for 循环遍历(增强 for 循环的底层其实是实现了 迭代器,因此在 所有集合中都可以使用该方式遍历)。

  • 代码实现:
import java.util.*;

public class ListFor {
    
    
    public static void main(String[] args) {
    
    

        ArrayList list = new ArrayList();// 创建一个有序的集合

        list.add("jack");
        list.add("tom");
        list.add("鱼香肉丝");
        list.add("北京烤鸭子");

        //遍历方式:
        //1. 迭代器
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
    
    
            Object obj =  iterator.next();
            System.out.println(obj);

        }

        System.out.println("=====增强for=====");
        //2. 增强 for 循环
        for (Object o : list) {
    
    
            System.out.println("o=" + o);
        }

        System.out.println("=====普通for====");
        //3. 普通 for 循环
        for (int i = 0; i < list.size(); i++) {
    
    
            System.out.println("对象=" + list.get(i));
        }
    }
}

三、 List 接口

(1)List 接口是继承了 Collection 接口的子接口,它是一个有序的集合,使用此接口能够精确的控制每个元素插入的位置。
(2)能够通过索引(元素在List中位置,类似于数组的下标)来访问 List 集合中的元素,第一个元素的索引为 0,而且允许有相同的元素。
(3)List 集合存储一组不唯一,有序(插入顺序)的对象/元素。

List 接口的常用方法

  • 实现 List 接口的子类可以使用以下的所有方法。

List 接口的常用方法:(以其子类 ArrayList 举例)


import java.util.ArrayList;
import java.util.List;

public class ListMethod {
    
    

    public static void main(String[] args) {
    
    

        List list = new ArrayList();// 向上转型

        list.add("张三丰");// 直接添加元素
        list.add("贾宝玉");

//      1. void add(int index, Object ele):在 index 位置插入 ele元素
        
        list.add(1, "韩顺平");// 在 index = 1的位置插入一个对象
        System.out.println("list=" + list);// [张三丰, 韩顺平, 贾宝玉]

//      2. boolean addAll(int index, Collection eles):从 index位置开始将 eles中的所有元素添加进来
        List list2 = new ArrayList();// 创建一个新的集合
        list2.add("jack");
        list2.add("tom");
        list.addAll(1, list2);// 从第 1个位置开始添加
        System.out.println("list=" + list);// [张三丰, jack, tom, 韩顺平, 贾宝玉]
      
//      3. Object get(int index):获取指定 index位置的元素
        Object obj = list.get(1);// 返回了第 2个元素, jack
        
//      4. int indexOf(Object obj):返回 obj在集合中首次出现的位置
        System.out.println(list.indexOf("tom"));// 2

//      5. int lastIndexOf(Object obj):返回 obj在当前集合中末次出现的位置
        list.add("韩顺平");
        System.out.println("list=" + list);
        System.out.println(list.lastIndexOf("韩顺平"));// 3

//      6. Object remove(int index):移除指定 index位置的元素,并返回此元素
        Object obj1 = list.remove(0);// 返回了第一个元素
        System.out.println("list=" + list);// [jack, tom, 韩顺平, 贾宝玉]

//      7. Object set(int index, Object ele):设置指定 index位置的元素为 ele , 相当于是替换
        list.set(1, "玛丽");
        System.out.println("list=" + list);// [jack, 玛丽, 韩顺平, 贾宝玉]

//      8.List subList(int fromIndex, int toIndex):返回从 fromIndex到 toIndex位置的子集合
        // 注意返回的子集合范围要满足: fromIndex <= subList < toIndex,否则会出现异常
        List returnlist = list.subList(0, 2);
        System.out.println("returnlist=" + returnlist);// [jack, 玛丽, 韩顺平]

    }
}

四、ArrayList 类(列表)

  • ArrayList 类实现了 List 接口;在ArrayList 集合中可以加入多个 null (空值)作为元素。
  • ArrayList 集合的底层是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以随意添加或删除元素。
  • ArrayList 类中的常用方法已经在上面的 Collection 接口和 List 接口常用方法中介绍过了,它可以直接使用Collection 接口和 List 接口中的常用方法。

ArrayList 类源码剖析

  • ArrayList 集合动态扩容的源码分析:

(1)ArrayList 类中维护了一个 Object 类型的数组 elementData。
transient Object elementData ;// transient: 表示瞬间, 短暂的, 代表该属性不会被序列化

(2)当创建 ArrayList 对象时, 如果使用的是无参构造器, 则初始 elementData 容量为 0;当第 1 次添加元素时, 会动态地将集合容量 elementData 扩充为 10 , 如需再次扩容, 则默认扩容为当前 elementData 的1.5倍。

(3)如果使用的是指定容量大小的构造器, 则初始 elementData 容量为指定值, 如果再次需要扩容, 则直接扩充为当前 elementData 的1.5倍。

  • 源码分析示意图:(很详细)

在这里插入图片描述
在这里插入图片描述

  • 注意:上面源码的分析需要小伙伴自己去 debug,这样才能深入理解。

五、Vector 类(向量)

  • Vector 类实现了 List 接口,底层也是一个动态的数组;
  • Vector 类和 ArrayList 类非常相似,但 Vector 类是同步的,可以用在多线程的情况。
  • Vector 类的带参构造器允许设置默认的集合长度,其默认扩容大小为原来容量的2 倍。

1. Vector 类源码剖析

  • 源码分析如下:

import java.util.Vector;

public class Vector_ {
    
    
    public static void main(String[] args) {
    
    

        // 无参构造器
        // Vector vector = new Vector();
        
        // 有参数的构造器
        Vector vector = new Vector(8);
        for (int i = 0; i < 10; i++) {
    
    
            vector.add(i);
        }

        vector.add(100);
        System.out.println("vector=" + vector);

源码分析:

1. Vector vector = new Vector() : 调用vector 的无参构造器

	// 无参构造器的底层还是调用了有参构造器,默认传入容量为 10
    public Vector() {
    
    
        this(10);
    }

 	//  Vector vector = new Vector(8) : 使用带参构造器 
    public Vector(int initialCapacity) {
    
    
        this(initialCapacity, 0);
    }

2. vector.add(i) : 调用 add() 方法添加元素;

 	2.1 // 下面这个方法添加数据到 vector集合

    public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

 	2.2 // 确定是否需要扩容条件 : minCapacity - elementData.length > 0

    private void ensureCapacityHelper(int minCapacity) {
    
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

 	2.3 // 如果需要的数组容量不够用,就扩容 , 扩容的算法:
      	newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
      	// 其实就是扩容两倍

    private void grow(int minCapacity) {
    
    
        
        int oldCapacity = elementData.length;

        int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    }
}

2. Vector 和 ArrayList 类的比较

  • 如下图:

在这里插入图片描述

六、LinkedList 类(链表)

  • LinkedList 类实现了 List 接口,允许有null(空)元素。主要用于创建链表数据结构。
  • LinkedList 类没有同步方法,如果多个线程同时访问一个 LinkedList 类,则必须自己实现访问同步,解决方法就是在创建 LinkedList 类 时候构造一个同步的 LinkedList 类。
  • LinkedList 类底层实现了双向链表和双向队列的特点(数据结构部分的知识)。

1. LinkedList 类的常用方法

  • 其实也是实现了 List 接口中的常用方法,只是底层的代码有区别,但最终呈现结果是一样的,了解即可。
import java.util.Iterator;
import java.util.LinkedList;

public class LinkedListCRUD {
    
    
    public static void main(String[] args) {
    
    

        LinkedList linkedList = new LinkedList();
        
        // 1.增加结点
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("linkedList=" + linkedList);

        // 2.删除结点
        linkedList.remove(); // 这里默认删除的是第一个结点
        linkedList.remove(2);// 删除指定位置的结点
        System.out.println("linkedList=" + linkedList);

        // 3.修改某个结点对象
        linkedList.set(1, 999);
        System.out.println("linkedList=" + linkedList);

        // 4.得到某个结点对象
        // get(1) 是得到双向链表的第二个对象
        Object o = linkedList.get(1);
        System.out.println(o);// 999

        // 因为 LinkedList 实现了 List接口, 可以使用迭代器遍历
        System.out.println("====LinkeList遍历迭代器====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
    
    
            Object next =  iterator.next();
            System.out.println("next=" + next);

        }
    }
}

2. LinkedList 的底层操作机制

  • 如下图:

在这里插入图片描述

  • 代码模拟双向链表:

public class LinkedList01 {
    
    
    public static void main(String[] args) {
    
    
        // 模拟一个简单的双向链表

        Node jack = new Node("jack");
        Node tom = new Node("tom");
        Node hsp = new Node("老韩");

        // 连接三个结点,形成双向链表
        // jack -> tom -> hsp
        jack.next = tom;
        tom.next = hsp;

        // hsp -> tom -> jack
        hsp.pre = tom;
        tom.pre = jack;

        Node first = jack;// 让 first 引用指向 jack,就是双向链表的头结点
        Node last = hsp; // 让 last 引用指向 hsp,就是双向链表的尾结点


        // 演示,从头到尾进行遍历

        System.out.println("===从头到尾进行遍历===");
        while (true) {
    
    
            if(first == null) {
    
    
                break;
            }
            // 输出 first 信息
            System.out.println(first);
            first = first.next;
        }

        // 演示,从尾到头的遍历
        System.out.println("====从尾到头的遍历====");
        while (true) {
    
    
            if(last == null) {
    
    
                break;
            }
            // 输出 last 信息
            System.out.println(last);
            last = last.pre;
        }

        // 演示链表的添加对象/数据,很方便
        // 要求,在 tom 直接插入一个对象 smith

        // 1. 先创建一个 Node 结点,name 就是 smith
        Node smith = new Node("smith");

        // 下面就把 smith 加入到双向链表了
        smith.next = hsp;
        smith.pre = tom;
        hsp.pre = smith;
        tom.next = smith;

        // 让first 再次指向 jack
        first = jack;// 让 first 引用指向 ack,就是双向链表的头结点

        System.out.println("===从头到尾进行遍历===");
        while (true) {
    
    
            if(first == null) {
    
    
                break;
            }
            // 输出 first 信息
            System.out.println(first);
            first = first.next;
        }

        last = hsp; // 让last 重新指向最后一个结点
        // 演示,从尾到头的遍历
        System.out.println("====从尾到头的遍历====");
        while (true) {
    
    
            if(last == null) {
    
    
                break;
            }
            // 输出 last 信息
            System.out.println(last);
            last = last.pre;
        }
    }
}

// 定义一个Node 类,一个 Node 对象表示双向链表的一个结点
class Node {
    
    

    public Object item; // 真正存放数据
    public Node next; // 指向后一个结点
    public Node pre; // 指向前一个结点

    public Node(Object name) {
    
    
        this.item = name;
    }
    
    public String toString() {
    
    
        return "Node name=" + item;
    }
}
  • 在简单了解了双向链表的结构后,我们进一步分析 LinkedList 集合添加和删除元素的底层机制源码。

  • 源码分析如下:


一、add 源码阅读:linkedList.add(1);

1. 使用无参构造器,创建一个集合:public LinkedList() {
    
    }

	LinkedList linkedList = new LinkedList();
      
2. 这时 linkeList 的属性 first = null,  last = null

3. 执行 add 方法

       public boolean add(E e) {
    
    
            linkLast(e);
            return true;
        }

4. 将新的结点,直接加入到双向链表的最后

     void linkLast(E e) {
    
    
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }


二、删除源码阅读: linkedList.remove(); // 不传入参数则默认删除的是第一个结点

1. 执行 removeFirst 方法

    public E remove() {
    
    
        return removeFirst();
    }
    
2. 执行 removeFirst()方法

    public E removeFirst() {
    
    
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

3. 执行 unlinkFirst 方法, 将 f 指向的双向链表的第一个结点拿掉

    private E unlinkFirst(Node<E> f) {
    
    
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    } 
    
  • 上面的源码小伙伴们要自己 debug 一遍,才能更深入地理解 LinkedList 集合的底层机制。

3. ArrayList 和 LinkedList 比较

  • 如下图:

在这里插入图片描述

总结

  • 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
  • 本文详细介绍了 集合框架 的基本概念,深入讲解了 Collection 集合 的常用接口 List, 常用子类 ArrayList 、LinkedList、Vector 的细节和常用方法;并介绍了迭代器的使用,分析了各个子类底层的源码实现,举了很多例子讲解,希望小伙伴们看后能有所收获!
  • 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!

猜你喜欢

转载自blog.csdn.net/weixin_45395059/article/details/125812297