JavaSE - 集合类-单列集合框架

JavaSE - 集合类-单列集合框架

本节学习目标:

  • 了解Java单列集合框架结构;
  • 了解并掌握Collection接口及其方法;
  • 了解并掌握List集合(接口)及其方法;
  • 了解并掌握Set集合(接口)及其方法;
  • 了解并掌握Queue集合(接口)及其方法;
  • 了解并掌握单列集合实现类的特性和优缺点。

1. 单列集合框架结构

单列集合指只有一列数据的集合,单列集合中的每个数据被称为元素(Element),单列集合使用泛型E限制元素数据类型

Java单列集合框架由Collection接口,继承Collection接口ListSet接口,以及它们对应的多种实现类构成:

JavaSE - 集合类-单列集合框架_第1张图片

2. Collection 接口

Collection接口为Java单列集合框架中的根接口,它预先定义了单列集合中的基本方法,单列集合中的常用方法如下:

方法 返回值类型 功能
size() int 返回单列集合的长度
isEmpty() boolean 判断单列集合是否为空集合
contains(Object o) boolean 判断单列集合内是否包含o对象
iterator() Iterator 继承于Iterable接口的方法,返回当前单列集合的迭代器
toArray() Object[] Object数组形式返回当前单列集合
toArray(T[] a) T[] 将当前单列集合转换为T类型数组后返回
add(E e) boolean 向单列集合中添加元素E,返回是否添加成功
remove(Object o) boolean 移除单列集合中的对象o,返回是否移除成功
containsAll(Collection c) boolean 判断单列集合c是否为当前单列集合的子集
addAll(Collection c) boolean 将单列集合c中的所有元素添加至当前单列集合,返回是否添加成功
removeAll(Collection c) boolean 移除当前单列集合中与单列集合c交集,返回是否移除成功
removeIf(Predicate filter) boolean 根据选择器中的条件移除当前单列集合中的元素,返回是否移除成功
retainAll(Collection c) boolean 移除当前单列集合中与单列集合c的补集,即保留交集内的元素,返回是否移除成功
clear() void 移除当前单列集合中的所有元素

编写代码进行测试:

public class TestCollection {
     
    public static void main(String[] args) {
     
        Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
        System.out.println("集合的长度:" + collection.size());
        System.out.println("集合是否为空:" + collection.isEmpty());
        System.out.println("集合的Object数组形式:" + Arrays.toString(collection.toArray()));
        Integer[] i = collection.toArray(new Integer[0]);
        System.out.println("将Integer集合转换为Integer数组:" + Arrays.toString(i));
        collection.add(1);
        System.out.println("向集合中添加元素1:" + Arrays.toString(collection.toArray()));
        collection.remove(2);
        System.out.println("移除集合中的2:" + Arrays.toString(collection.toArray()));
        System.out.println("判断集合{1, 3}是否为当前集合的子集:" + collection.containsAll(new ArrayList<>(Arrays.asList(1, 3))));
        collection.addAll(new ArrayList<>(Arrays.asList(6, 7)));
        System.out.println("将集合{6, 7}加入到当前集合:" + Arrays.toString(collection.toArray()));
        collection.removeAll(new ArrayList<>(Arrays.asList(1, 3)));
        System.out.println("将集合{1, 3}从当前集合移除:" + Arrays.toString(collection.toArray()));
        collection.removeIf(new Predicate<Integer>() {
     
            @Override
            public boolean test(Integer integer) {
     
                return integer == 4;
            }
        });
        System.out.println("使用选择器移除集合中的4:" + Arrays.toString(collection.toArray()));
        collection.retainAll(new ArrayList<>(Arrays.asList(6, 7, 8)));
        System.out.println("移除当前集合与集合{6, 7, 8}的补集:" + Arrays.toString(collection.toArray()));
        collection.clear();
        System.out.println("移除当前集合的所有元素,集合的长度为:" + collection.size());
    }
}

运行结果:

集合的长度:5
集合是否为空:false
集合的Object数组形式:[1, 2, 3, 4, 5]
将Integer集合转换为Integer数组:[1, 2, 3, 4, 5]
向集合中添加元素1:[1, 2, 3, 4, 5, 1]
移除集合中的2:[1, 3, 4, 5, 1]
判断集合{1, 3}是否为当前集合的子集:true
将集合{6, 7}加入到当前集合:[1, 3, 4, 5, 1, 6, 7]
将集合{1, 3}从当前集合移除:[4, 5, 6, 7]
使用选择器移除集合中的4:[5, 6, 7]
移除当前集合与集合{6, 7, 8}的补集:[6, 7]
移除当前集合的所有元素,集合的长度为:0

Arrays工具类的asList方法返回的ArrayList对象与集合中的ArrayList对象是两个类,前者的全限定类名为java.util.Arrays.ArrayList,后者为java.util.ArrayList
前者是Arrays工具类的内部类,继承于AbstractList抽象类,间接实现了List接口,但未重写add()方法。如果直接调用由asList()方法返回的ArrayList对象的add()方法,
会抛出UnsupportedOperationException异常。所以要使用后者的构造方法重新包装一下,才能使用add()方法。

3. List 集合(接口及其实现类)

List集合包括List接口及其实现类。List集合中的元素允许重复有序,各元素的顺序就是元素插入时的顺序
List集合中的每一个元素都有对应的顺序索引,所以List集合也被称为动态数组

List接口继承了Collection接口,因此List集合具有Collection集合的所有特性,包含Collection接口的所有方法

List接口除了有继承于Collection接口的所有方法自身又定义了很多方法,常用方法如下:

方法 返回值类型 功能
get(int index) E 返回集合中指定索引位置的元素
set(int index, E e) E 将指定索引位置的元素替换为新的元素E,返回被替换的元素
add(int index, E e) void 在指定索引位置插入新的元素E,位于指定索引后的所有元素索引后退
remove(int index) E 将指定索引位置的元素移除,返回被移除的元素
addAll(int index, Collection c) boolean 在指定索引位置插入集合c中的所有元素,位于指定索引后的所有元素索引后退,返回是否插入成功
sort(Comparetor c) void 使用比较器对集合内的元素进行排序,排序后原集合的顺序将被更改
indexOf(Object o) int 返回对象o在集合中第一次出现的索引
lastIndexOf(Object o) int 返回对象o在集合中最后一次出现的索引
subList(int fromIndex, int toIndex) List 将集合中从索引fromIndex到索引toIndex的元素(不包括位于toIndex索引的元素)以子集合的形式返回

编写代码进行测试:

import java.util.*;
public class TestList {
     
    public static void main(String[] args) {
     
        List<Integer> list = new ArrayList<>(Arrays.asList(57, 62, 6, -47, 6));
        System.out.println("集合中索引为2的元素:" + list.get(2));
        list.set(1, 5);
        System.out.println("将集合中索引为1的元素替换为5:" + Arrays.toString(list.toArray()));
        list.add(2, 50);
        System.out.println("向集合中2索引处添加元素50:" + Arrays.toString(list.toArray()));
        list.remove(5);
        System.out.println("移除集合中索引为5的元素:" + Arrays.toString(list.toArray()));
        list.addAll(2, new ArrayList<>(Arrays.asList(-68, -68)));
        System.out.println("向集合中2索引处添加集合{-68, -68}:" + Arrays.toString(list.toArray()));
        list.sort(new Comparator<Integer>() {
     
            @Override
            public int compare(Integer o1, Integer o2) {
     
                if (o1.equals(o2)) {
     
                    return 0;
                } else if (o1 < o2) {
     
                    return -1;
                } else {
     
                    return 1;
                }
            }
        });
        System.out.println("使用选择器(从小到大)将集合排序:" + Arrays.toString(list.toArray()));
        System.out.println("返回-68在集合中第一次出现的索引:" + list.indexOf(-68));
        System.out.println("返回-68在集合中最后一次出现的索引:" + list.lastIndexOf(-68));
        System.out.println("返回集合从索引1到索引3之间的元素:" + Arrays.toString(list.subList(1, 3).toArray()));
    }
}

运行结果:

集合中索引为2的元素:6
将集合中索引为1的元素替换为5:[57, 5, 6, -47, 6]
向集合中2索引处添加元素50:[57, 5, 50, 6, -47, 6]
移除集合中索引为5的元素:[57, 5, 50, 6, -47]
向集合中2索引处添加集合{-68, -68}:[57, 5, -68, -68, 50, 6, -47]
使用选择器(从小到大)将集合排序:[-68, -68, -47, 5, 6, 50, 57]
返回-68在集合中第一次出现的索引:0
返回-68在集合中最后一次出现的索引:1
返回集合从索引1到索引3之间的元素:[-68, -47]

3.1 ArrayList 集合类

ArrayList类为List接口的实现类之一,它是List集合的主要实现,一般情况下都使用ArrayList集合作为List集合的实现。

1. 成员变量

ArrayList类的成员变量

成员变量名 类型 说明
elementData Object[] 用于存储集合中的元素
size int 集合的长度(并非容量),未添加任何元素长度0
  • elementData.length指当前集合的容量(集合可以容纳元素的个数);
  • size指当前集合的长度(集合现有元素的个数)。

2. 构造方法

ArrayList类的常用构造方法

构造方法 说明
ArrayList(int initialCapacity) 创建一个指定容量大小的ArrayList集合对象
ArrayList() 创建一个ArrayList集合对象,且elementData为空数组({}
ArrayList(Collection c) 以集合c创建一个ArrayList集合对象

3. 添加操作与扩容原理(源码)

进入调试,调用add()方法:

// java.util.ArrayList.add(E)
// ArrayList.java jdk1.8.0_202 Line:439~443

public boolean add(E e) {
     
    // 检测当前容量,容量需要能装下当前集合长度+1的长度(最小容量)
    ensureCapacityInternal(size + 1);
    // 在Object数组末尾添加数据,同时集合的长度+1(size++)
    elementData[size++] = e;
    return true;
}

继续深入调试,进入ensureCapacityInternal()方法:

// java.util.ArrayList.ensureCapacityInternal
// ArrayList.java jdk1.8.0_202 Line:230~232

private void ensureCapacityInternal(int minCapacity) {
     
    // 先调用calculateCapacity()方法确定所需容量:
    //  private static int calculateCapacity(Object[] elementData, int minCapacity) {
     
    //      // 如果此时集合中没有任何元素,即等于常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA({})
    //      if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
     
    //          // 在默认容量和最小容量取最大值返回
    //          return Math.max(DEFAULT_CAPACITY, minCapacity);
    //      }
    //      // 如果集合中存在元素,直接返回最小容量
    //      return minCapacity;
    //  }
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

继续深入调试,进入ensureExplicitCapacity()方法:

// java.util.ArrayList.ensureExplicitCapacity
// ArrayList.java jdk1.8.0_202 Line:234~240

private void ensureExplicitCapacity(int minCapacity) {
     
    // 自增修改次数
    modCount++;

    // overflow-conscious code
    // 如果最小容量比当前容量大,则需要扩容,调用扩容方法grow()
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

继续深入调试,进入grow()方法:

// java.util.ArrayList.grow
// ArrayList.java jdk1.8.0_202 Line:256~266

private void grow(int minCapacity) {
     
    // overflow-conscious code
    // 获取旧容量(当前容量)
    int oldCapacity = elementData.length;
    // 计算扩容后的容量(旧容量的1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果扩容后的容量比最小容量小
    if (newCapacity - minCapacity < 0)
        // 直接扩容至最小容量
        newCapacity = minCapacity;
    // 如果扩容后的容量比ArrayList最大容量(Integer.MAX_VALUE - 8)大
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        // 调用hugeCapacity()确定扩容后的容量:
        // private static int hugeCapacity(int minCapacity) {
     
        //     // 如果扩容后的容量溢出(指计算后的)
        //     if (minCapacity < 0) // overflow
        //         // 抛出OutOfMemoryError异常
        //         throw new OutOfMemoryError();
        //     // 返回扩容后的容量与int类型最大值的较大值
        //     return (minCapacity > MAX_ARRAY_SIZE) ?
        //         Integer.MAX_VALUE :
        //         MAX_ARRAY_SIZE;
        // }
        newCapacity = hugeCapacity(minCapacity);
    // 将原Object数组中的内容复制到一个长度为扩容后的容量的一个新数组中
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

总结JDK1.8中ArrayList集合的添加和扩容机制:

  • 调用添加方法(如add())时,会进行容量检测,检测所需的最小容量size + 1)是否满足当前容量elementData.length):
    • 先判断elementData是否为空数组,如果是,则在最小容量默认容量(10)之间取最大值,并赋给最小容量,如果不是则无变化;
    • 后判断最小容量是否比当前容量大,如果不是则无变化,如果是则需要扩容
      • 当前容量扩容至1.5倍,比较扩容后的容量最小容量
        • 如果扩容后的容量比最小容量小,直接将当前容量扩容至最小容量
      • 如果扩容后的容量ArrayList最大容量大:
        • 先判断扩容后的容量是否小于0(即溢出),则抛出OutOfMemoryError异常。
        • 后判断扩容后的容量是否比ArrayList最大容量大,如果是,将容量扩容至int类型最大值。如果不是,将容量扩容至ArrayList最大容量
      • 创建一个长度为扩容后的容量Object数组,将原Object数组内容复制进新的Object数组里,并将elementData引用指向新数组
  • 将需要添加的元素添加至集合的末尾,同时集合长度自增(size++),操作结束。

在JDK1.7中机制相似,唯一不同点在于构造方法:

  • JDK1.7中使用无参构造方法创建ArrayList集合时,创建的Object数组为一个长度为10默认数组
  • JDK1.8中使用无参构造方法创建ArrayList集合时,创建的Object数组为空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA);

4. 常量

ArrayList类提供的常用常量

常量名 类型 说明
DEFAULT_CAPACITY int 10 默认容量
DEFAULTCAPACITY_EMPTY_ELEMENTDATA Object[] {} 默认Object数组
MAX_ARRAY_SIZE int Integer.MAX_VALUE - 8 ArrayList最大容量

5. 特性

  • ArrayList集合底层使用Object数组顺序表)存储数据;
  • 由于数据结构采用了顺序表,所以进行频繁查询修改效率较快,增加删除插入效率较慢
  • ArrayList集合允许元素为null
  • ArrayList集合的操作方法没有同步,因此ArrayList集合是非线程安全的,不适用于多线程环境;

3.2 LinkedList 集合类

LinkedList类也为List接口的实现类之一,实际开发中也经常使用。

LinkedList集合的核心结构为它的内部类——节点(Node):

// java.util.LinkedList.Node
// LinkedList.java jdk1.8.0_202 Line:970~980

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;
    }
}

LinkedList集合底层采用双向链表存储数据,每个元素就是一个节点,每个节点相互连接构成了双向链表结构。

一个元素为{57, 61}的LinkedList集合对象list的内存结构示意图如图所示:

JavaSE - 集合类-单列集合框架_第2张图片

1. 成员变量

LinkedList类的成员变量

成员变量名 类型 说明
first Node 指向双向链表首节点引用
last Node 指向双向链表尾节点引用
size int 集合的长度(并非容量),未添加任何元素长度0

Node(节点)类的成员变量

成员变量名 类型 说明
item E 存储在节点中的元素
next Node 指向下一个节点的引用
prev Node 指向上一个节点的引用

2. 构造方法

构造方法 说明
LinkedList() 默认构造方法,创建一个空的集合
LinkedList(Collection c) 以集合c创建一个LinkedList集合对象

3. 添加与插入操作原理(源码)

调用add(E e)方法进行添加操作,开始调试,进入add()方法:

// java.util.LinkedList.add(E)
// LinkedList.java jdk1.8.0_202 Line:337~340

public boolean add(E e) {
     
    // 使用linkLast()方法进行添加操作
    linkLast(e);
    return true;
}

继续深入调试,进入linkLast()方法:

// java.util.LinkedList.linkLast
// LinkedList.java jdk1.8.0_202 Line:140~150

void linkLast(E e) {
     
    // 获取链表的最后一个节点(last引用)
    final Node<E> l = last;
    // 将要添加的数据包装为一个节点对象,同时将新节点对象的prev引用指向最后一个节点,next引用设为null
    final Node<E> newNode = new Node<>(l, e, null);
    // 将last引用指向新节点对象
    last = newNode;
    // 如果最后一个节点为null,即当前链表为空链表
    if (l == null)
        // 则将first引用指向新节点对象,创建一个新链表
        first = newNode;
    else
        // 不为null说明当前链表不为空,将最后一个节点的next引用指向新节点,将新节点连接至最后
        l.next = newNode;
    // 集合长度自增
    size++;
    // 修改计数自增
    modCount++;
}

总结LinkedList集合的添加操作机制:

  • 将要添加的元素包装为新节点对象,同时将新节点对象的prev引用指向链表最后一个节点(last引用);
  • 如果最后一个节点为null,则将first引用指向新节点对象;
  • 如果不为null,将最后一个节点next引用指向新节点对象;

调用add(int index, E element)方法进行插入操作,开始调试,进入add()方法:

// java.util.LinkedList.add(int, E)
// LinkedList.java jdk1.8.0_202 Line:506~513

public void add(int index, E element) {
     
    // 使用checkPositionIndex()方法检查索引是否合法
    // 索引必须大于等于0且小于等于集合的长度
    // 否则抛出IndexOutOfBoundsException异常
    checkPositionIndex(index);

    // 如果索引等于集合长度
    if (index == size)
        // 即在集合末尾添加元素,调用linkLast()方法
        linkLast(element);
    else 
        // 否则调用linkBefore()方法
        // node()方法遍历获取索引位置的元素
        // 遍历方法为中值遍历
        // 如果索引比集合长度的一半大,则从后往前遍历,否则从前往后遍历
        linkBefore(element, node(index));
}

继续深入调试,进入linkBefore()方法:

// java.util.LinkedList.linkBefore
// LinkedList.java jdk1.8.0_202 Line:155~166

void linkBefore(E e, Node<E> succ) {
     
    // assert succ != null;
    // 获取索引前一个节点
    final Node<E> pred = succ.prev;
    // 将要添加的元素包装为一个节点对象,同时将新节点的prev引用指向索引前一个节点,next引用指向索引节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 将索引节点的prev引用指向新节点
    succ.prev = newNode;
    // 如果索引前一个节点为null
    if (pred == null)
        // 即向链表头部插入节点
        // 将first引用指向新节点
        first = newNode;
    else
        // 不为null说明向链表中的某个位置插入节点
        // 将索引前一个节点的next引用指向新节点
        pred.next = newNode;
    // 集合长度自增
    size++;
    // 修改计数自增
    modCount++;
}

总结LinkedList集合的插入操作机制:

  • 首先检查插入的索引位置是否合法,非法则抛出IndexOutOfBoundsException异常;
  • 判断插入位置,如果插入的位置在链表尾部,则使用添加机制进行插入;
  • 如果不是,使用插入机制插入:
    • 将要插入的元素包装为新节点对象,将新节点prev引用指向索引前一个节点next引用指向索引节点
    • 索引节点prev引用指向新节点(完成新节点和原索引节点的连接);
    • 如果插入位置在链表头部,将first引用指向新节点
    • 如果不是,将索引前一个节点next引用指向新节点(完成索引前一个节点和新节点的连接)。

4. 特性

  • LinkedList集合底层使用双向链表存储数据;
  • 由于数据结构采用了链表,所以进行频繁查找修改操作效率较慢添加删除插入操作较快
  • LinkedList集合允许元素为null
  • LinkedList集合的操作方法没有同步,因此LinkedList集合是非线程安全的,不适用于多线程环境。

3.3 Vector 集合类

Vector类是List接口的实现类,是List接口还未出现前(JDK1.2之前)Collection接口的早期实现类。

1. 成员变量

Vector类的成员变量

成员变量名 类型 说明
elementData Object[] 用于存储集合中的元素
elementCount int 元素的个数,即集合的长度ArrayList类中的size
capacityIncrement int 每次进行扩容时的增量

2. 构造方法

Vector类的常用构造方法

构造方法 说明
Vector() 默认构造方法,以默认容量10创建一个Vector集合
Vector(int initialCapacity) 指定容量创建一个Vector集合
Vector(Collection c) 以集合c创建一个Vector集合

3. 扩容原理概述

Vector集合的扩容原理与ArrayList集合相似。唯一不同点在于扩容时如果没有指定capacityIncrement小于或等于0)时,
扩容为当前容量2倍(ArrayList为1.5倍);指定capacityIncrement大于0)时,扩容为当前容量加上capacityIncrement

4. 常量

Vector类提供的常用常量

常量名 类型 说明
MAX_ARRAY_SIZE int Integer.MAX_VALUE - 8 Vector最大容量

5. 特性

  • Vector集合底层使用Object数组顺序表)存储数据;
  • 由于数据结构采用了顺序表,所以进行频繁查询修改效率较快,增加删除插入效率较慢
  • Vector集合允许元素为null
  • Vector集合的操作方法进行了同步,因此Vector集合是线程安全的,适用于多线程环境;
  • Vector集合由于年代久远,效率低下,同时还存在很多缺点,因此现已弃用
  • 如果需要线程安全环境,使用Collections工具类提供的synchronizedList()方法获取同步集合

3.4 List 接口实现类之间的异同和优缺点

List接口的三大实现类:ArrayListLinkedListVector

List集合 元素可为null 查找/修改效率 添加/插入/删除效率 是否线程安全 存储结构 说明
ArrayList 较高 较低 顺序表(Object数组) 经常使用的List集合,适合用于固定元素数量的集合,需要进行频繁的查改操作的
LinkedList 较低 较高 双向链表 适合用于未知元素数量的集合,需要进行频繁的增删插操作的
Vector 较高 较低 顺序表(Object数组) 实际开发中很少使用,以Collections.synchronizedList()替代

4. Set 集合(接口及其实现类)

Set集合包括Set接口及其实现类。Set集合中的元素不可重复唯一)且无序,对应数学意义上的“集合”。

  • 对于Set集合中元素的无序性的理解:

无序性并不等于随机性。Set集合的无序性是相对于List集合有序性而言的,List集合中元素的顺序是按照它们添加集合中顺序紧挨排列。
而Set集合的元素的顺序是按照元素的哈希值进行排列,第一个添加的元素不一定就在集合的最前面,有可能随着元素的添加而排在最后面。
所以Set集合相对于List集合是无序的。

  • 对于Set集合中元素的不可重复性的理解:

学习JavaBean与其方法章节的equals()hashCode()方法后,我们知道两个对象相等,则它们的哈希值一定相同;反之,两个对象的哈希值相同,但这两个对象不一定相等。
Set集合的不可重复性可以基于这句话理解。Set集合添加或插入数据时,会先调用hashCode()方法查找是否有相同哈希值的元素,再调用equals()方法查找是否有相同的元素。
通过对比哈希值equals()方法的返回值来保证元素的唯一性

Set接口也继承了Collection接口,Set集合同样具有Collection集合的所有特性。包含Collection接口的所有方法。

Set接口本身没有新增方法,它的方法全都继承于Collection接口的方法。

4.1 HashSet 集合类

HashSet类为Set接口的实现类之一,它是Set集合的主要实现,一般情况下都使用HashSet集合作为Set集合的实现。

HashSet类底层采用哈希表存储数据(实际使用一个HashMap集合对象存储)。

1. 成员变量

HashSet类只有一个成员变量

成员变量名 类型 说明
map HashMap 存储数据的HashMap对象

2. 构造方法

HashSet类的常用构造方法:

构造方法 说明
HashSet() 默认构造方法,创建一个空HashSet集合,以默认参数初始化map
HashSet(Collection c) 以集合c创建一个HashSet集合,将数据存入map
HashSet(int initialCapacity) 指定容量创建一个HashSet集合,以指定容量初始化map

3. 添加操作原理概述

HashSet集合中所有的操作方法都是调用的HashMap集合的操作方法实现的。所以源码分析和实现细节放在下章节。

  • 根据元素的哈希值hashCode()方法返回值),使用哈希函数计算元素在哈希表数组)中的存放位置
  • 存放时有四种情况(以元素E1和E2为例):
    • 元素E1的存放位置处没有元素,则直接存放
    • 元素E1的存放位置处为元素E2,通过比较E1和E2的哈希值不同,则通过链表结构将两个元素存入同一个存放位置,JDK1.7中是新元素指向旧元素E2.next = E1),JDK1.8中是旧元素指向新元素E1.next = E2);
    • 元素E1的存放位置处为元素E2,通过比较E1和E2的哈希值相同,但通过E1.equals(E2)E1和E2不同,则也通过链表结构将两个元素存入同一个位置,连接方法同上;
    • 元素E1的存放位置处为元素E2,通过比较E1和E2的哈希值相同,通过E1.equals(E2)E1和E2也相同,则认为E1和E2是同一个元素,不做任何处理。

元素E1与E2在哈希表中存储的内存示意图:

JavaSE - 集合类-单列集合框架_第3张图片

4. 常量

HashSet只有一个常量:

常量名 类型 说明
PRESENT Object new Object() map中与每个元素关联的虚拟值

5. 特性

  • HashSet集合底层使用哈希表数组单向链表)存储数据;
  • 由于数据结构采用了哈希表,所以增删查改插效率都较快,但创建时效率较慢;
  • 哈希表保证元素不可重复
  • HashSet集合允许元素为null,但只能有一个元素为null
  • HashSet集合的操作方法没有同步,因此HashSet集合是非线程安全的,不适用于多线程环境。

4.2 LinkedHashSet 集合类

LinkedHashSet类为Set接口的实现类之一,同时它也是HashMap类的子类。它可以使元素按照插入顺序排序

LinkedHashSet类底层采用哈希表双向链表存储数据(实际使用一个LinkedHashMap集合对象存储)。

1. 构造方法

LinkedHashSet没有自己的成员变量,只有构造方法。常用的构造方法:

构造方法 说明
LinkedHashSet() 默认构造方法,创建一个空LinkedHashSet集合,以默认参数初始化map
LinkedHashSet(Collection c) 以集合c创建一个LinkedHashSet集合,将数据存入map
LinkedHashSet(int initialCapacity) 指定容量创建一个LinkedHashSet集合,以指定容量初始化map

LinkedHashSet类的构造方法都调用了它的父类HashSet构造方法

// java.util.HashSet.HashSet(int, float, boolean)
// HashSet.java jdk1.8.0_202 Line:161~163

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
     
    // 调用此构造方法时将map初始化为一个LinkedHashMap对象并使用它存储数据
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

2. 添加操作原理概述

LinkedHashSet类本身没有定义任何成员变量成员方法,它的功能全部继承HashSet类。

LinkedHashSet集合中的所有操作方法都是调用的LinkedHashMap集合的操作方法实现的。由于LinkedHashMap类继承于HashMap,
所以LinkedHashSet的实现方式和HashSet相似。

在哈希表的基础上使用双向链表对元素的插入顺序进行排序(内存示意图):

JavaSE - 集合类-单列集合框架_第4张图片

红色为一般引用,绿色为哈希表中相同索引位置的元素链表引用,蓝色为排序的双向链表引用。

  • 元素E1最先插入,计算得出E1的索引位置没有元素,插入成功。双向链表的头节点引用设为E1(即head = E1);
  • 元素E2随后插入,计算得出E2的索引位置为E1,通过比较哈希值发现E1与E2哈希值相同,但E1.equals(E2)E1与E2不同,所以E2也插入成功并连接到E1之后(E1.next = E2),双向链表将E1与E2按照插入顺序排序(E1.after = E2E2.before = E1);
  • 元素E3最后插入,计算得出E3的索引位置没有元素,插入成功。E3也加入双向链表排到E2之后(E2.after = E3E3.before = E2),双向链表的尾节点引用设为E3(即tail = E3)。

3. 特性

  • LinkedHastSet集合底层使用哈希表双向链表存储数据;
  • 哈希表保证集合内元素唯一双向链表保证集合内元素有序插入顺序);
  • 由于数据结构采用了哈希表,所以增删查改插效率都较快,但创建时效率较慢;
  • LinkedHashSet集合允许元素为null,但只能有一个元素为null
  • LinkedHashSet集合的操作方法没有同步,因此LinkedHashSet集合是非线程安全的,不适用于多线程环境。

4.3 TreeSet 集合类

TreeSet类为SortedSet接口的实现类之一,间接实现了Set接口。TreeSet是一种有序Set集合,按照元素的某一属性进行排序。

TreeSet底层使用红黑树(自平衡二叉树)存储数据(实际使用一个TreeMap集合对象存储)。

1. 成员变量

TreeSet类只有一个成员变量

成员变量名 类型 说明
m NavigableMap 存储数据的NavigableMap对象(实际使用TreeMap对象)

2. 构造方法

TreeSet类的常用构造方法:

构造方法 说明
TreeSet() 默认构造方法,创建一个空的TreeSet集合,排序方法为自然排序
TreeSet(Comparator comparator) 使用指定比较器创建一个空的TreeSet集合,排序方法由比较器指定
TreeSet(Collection c) 使用集合c创建一个TreeSet集合,排序方法为自然排序

3. 自然排序与定制排序

TreeSet中元素排序和判断元素是否重复不再使用equals()hashCode()方法,取而代之的是比较器返回值

  • 返回值为0则两元素相同
  • 返回值为1新元素
  • 返回值为-1旧元素

比较器两种实现方式:

  • 内部比较器:元素类实现Comparable接口,重写compareTo()方法(自然排序);
  • 外部比较器:编写比较器类并实现Comparator接口,重写compare()方法(定制排序);

编写代码进行测试:

import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class Test {
     
    public static void main(String[] args) {
     
        // 未传入比较器,将使用元素的compareTo()方法进行自然排序
        Set<Integer> set1 = new TreeSet<>();
        set1.add(24);
        set1.add(6);
        set1.add(-5);
        // 传入了自定义比较器,将使用比较器进行定制排序
        Set<Integer> set2 = new TreeSet<>(new Comparator<Integer>() {
     
            @Override
            public int compare(Integer o1, Integer o2) {
     
                if (o1.equals(o2)) {
     
                    return 0;
                }
                if (o1 < o2) {
     
                    return 1;
                }
                return -1;
            }
        });
        set2.add(247);
        set2.add(21);
        set2.add(-8);
        System.out.println("自然排序set1:" + Arrays.toString(set1.toArray()));
        System.out.println("定制排序set2:" + Arrays.toString(set2.toArray()));
    }
}

// 运行结果
// 自然排序set1:[-5, 6, 24]
// 定制排序set2:[247, 21, -8]

5. 特性

  • TreeSet集合底层是用红黑树(自平衡二叉树)存储数据(实际使用一个TreeMap集合对象存储);
  • 由于使用红黑树数据结构存储数据,元素不允许重复,操作效率较高
  • TreeSet集合是一种有序Set集合,存储的元素需要实现Comparable接口或者传入比较器,否则将无法进行排序,抛出异常。
  • TreeSet集合不允许元素为null
  • TreeSet集合的操作方法没有同步,所以TreeSet集合是非线程安全的,不适用于多线程环境。

JavaSE - 集合类-单列集合框架_第5张图片

4.4 Set 接口实现类之间的异同和优缺点

Set接口的三大实现类:HashSetLinkedHashSetTreeSet

Set集合 元素可为null 查找/修改效率 添加/插入/删除效率 是否线程安全 存储结构 说明
HashSet 是(只有一个) 较高 较高 哈希表+红黑树 Set集合的主要实现,元素为无序
LinkedHashSet 是(只有一个) 较高 较高 哈希表+红黑树+双向链表 元素为有序(按照插入顺序)
TreeSet 较高 较高 红黑树 元素为有序(按照元素的某个属性排序)

如果在多线程环境中需要使用Set集合,使用Collections工具类提供的synchronizedSet()方法获取线程安全的Set集合。

你可能感兴趣的:(我的Java基础学习之路,java,javase,面试,集合,单列集合)