[Java学习之路篇] Java集合类库之Collection接口

所在包:java.util

在编程的过程中,但凡遇到与数据结构相关的问题时,都离不开Collection接口与Map接口,两者是整个集合类库中最基本的根接口。而Collection接口主要负责实现一些线性结构,如线性表(顺序表)、链表、栈、队列等。

集合类库的关系图:

集合简略关系图
这里主要表现了部分与集合类库有关的接口与实现类们,其中粗体黑框是我们最为常用、尤其重要的实现类。
集合完整关系图
这张是完整的集合接口、子接口和实现类们的关系图,其中拓展了Vector、Stack、LinkedHashMap等实现或继承的子类们,关系网还是十分庞大的。

Collection接口声明的常用方法有:

1、 添加元素

  • boolean add(E e)
  • boolean addAll(Collection<? extends E> c)

2、 删除元素

  • boolean remove(Object o)
  • boolean removeAll(Collection<?> c)

3、 查找元素

  • boolean contains(Object o)
  • boolean containsAll(Collection<?> c)

4、 判断集合是否为空

  • boolean isEmpty()

5、 清空集合所有元素

  • void clear()

6、 获取集合元素的个数

  • int size()

7、 获取迭代对象Iterator (用于foreach)

  • Iterator iterator()

8、 将集合中所有元素转化为数组返回

  • Object[] toArray()
  • T[] toArray(T[] a)

由此,Collection接口又引申出了两个子接口:List和Set,两者主要区别在于 元素是否可以重复

List接口

List接口是 允许元素可以重复的 ,故又扩充了一些方法。

List接口扩充方法有:

1、 指定位置添加元素

  • void add(int index,E element)
  • boolean addAll(int index,Collection<? extends E> c)

2、 通过索引获取元素

  • E get(int index)
  • List subList(int fromIndex,int toIndex) (返回子集)

3、 通过对象查找索引

  • int indexOf(Object o) (前往后找)
  • int lastIndexOf(Object o) (后往前找)

4、 获取ListIterator迭代对象 (此迭代对象可往前往后迭代)

  • ListIterator listIterator()
  • ListIterator listIterator(int index) 指定位置

5、 删除指定位置元素

  • E remove(int index)

6、 修改指定位置元素

  • E set(int index,E element)

实现List接口的类有三,分别为ArrayList、Vector、LinkedList。

ArrayList 与 Vector

ArrayList 与 Vector 均是实现了顺序表(线性表)的数据结构,查找、修改元素的时间复杂度为 O(1),添加、删除元素的时间复杂度为 O(n)(若操作的是最后一个元素的话为 O(1)),其中 n 为指定位置之后元素的个数。两者主要的区别如下:
ArrayList与Vector的比较
此处的 Enumeration 迭代器是老款的迭代器,与Iterator功能上无异。值得注意的是 ArrayList 使用频次 远远高于 Vector,主要原因在于 ArrayList 效率高

如何动态实现扩容?

这个问题需要阅读源代码解决,此处也是面试常常出题的地方。

// 创建两个 ArrayList 集合
ArrayList<String> array1 = new ArrayList<>();
ArrayList<String> array2 = new ArrayList<>(5);

// 创建两个 Vector 集合
Vector<String> vector1 = new Vector<>();
Vector<String> vector2 = new Vector<>(5, 5);

ArrayList的实现和Vector的实现差别挺大的。
ArrayList的关键属性有五个,构造函数有三个:

// ArrayList 关键属性
// 默认容量
private static final int DEFAULT_CAPACITY = 10;
// 空元素数组,用于给 elementData 数组初始化
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量的空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 实际装元素的数组
transient Object[] elementData; // non-private to simplify nested class access
// 装有元素的个数
private int size;

// ArrayList 构造函数
// 无参构造
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 有参构造 指定容量
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
}
// 根据提供的集合构造
public ArrayList(Collection<? extends E> c);

从上面可以看出,ArrayList 的无参构造方法是将一个空Object数组赋值给elementData,此时数组的容量为0 ,即使存在默认容量为10,这一点值得探究;而 ArrayList 的指定容量 initialCapacity 的有参构造方法是new一个指定容量的数组赋值给 elementData,此时容量为指定容量 initialCapacity。

接下来我们来看看 Vector的关键属性和构造方法:

// Vector 关键属性
// 实际装元素的数组
protected Object[] elementData;
// 装有元素的个数(与ArrayList的size一样)
protected int elementCount;
// 单次扩容数量
protected int capacityIncrement;

// Vector 构造函数
// 无参构造
public Vector() { this(10); }
// 有参构造 指定容量
public Vector(int initialCapacity) { this(initialCapacity, 0); }
// 有参构造 指定容量 单次扩容数量
public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

Vector 的关键属性较少,只有装元素的容器(数组)elementData、元素个数 elementCount 和 单次扩容数量 capacityIncrement ,最后一个主要用于指定当集合需要扩容时,每次扩容的数量,可以控制扩容速度,需要考虑内存使用的效益。再看 Vector 的构造方法,与 ArrayList 差别不大。

两者在初始化空集合时,均只是指定了容量以及分配了内存空间,并未涉及扩容的部分,接下来我们再看看添加元素的部分,观察当集合添加溢出时,集合内部是如何处理的。

程序执行如下操作:

// 为各个集合添加一个元素 "a"
array1.add("a");
array2.add("a");
vector1.add("a");
vector2.add("a");

从添加第一个元素可以看出,只有 array1 触发了扩容机制,其中经过了 add → grow → newCapacity 三个阶段。

// ArrayList 扩容流程
// 添加元素源码
public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
}
private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
}

// 扩容源码
private Object[] grow() {
        return grow(size + 1);
}
private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}

// 生成新容量数组源码
// 输入的最小容量为最小需要扩大的容量,一般为 size + 1
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        // 旧数组容量
        int oldCapacity = elementData.length;
        // 新数组容量 = 旧数组容量 * 2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 判断计算出来的新数组容量是否满足最小要求
        // 不满足
        if (newCapacity - minCapacity <= 0) {
        	// 判断是否为空数组
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            	// ※ 若默认容量大,返回默认容量 DEFAULT_CAPACITY 
            	// 否则返回最小容量 minCapacity
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            // 判断最小容量是否小于0
            // 此情况存在于多个线程同时对集合进行操作
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            // 各种情况都不满足,返回最小容量
            return minCapacity;
        }
        // 满足
        // 再判断新数组容量是否超过了最大数组容量
        // 没超过就返回新数组容量
        // 超过了就返回最大数组容量(不细看)
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
}

经过程序的跟踪,我们知道 array1 走的是程序的 ※ 出口,即此时集合的容量为10,之前均为0。另外,ArrayList 实现扩容的方法通常为 原来的数组容量*2 ,而初始化空集合时,初始容量为0

接下来再看看Vector扩容过程:

// Vector 扩容流程
// 添加元素源码
public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
}
private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        elementCount = s + 1;
}

// 扩容源码
private Object[] grow() {
        return grow(elementCount + 1);
}
private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
// 生成新容量数组源码
// 输入的最小容量为最小需要扩大的容量,一般为 elementCount + 1
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        // 旧容量
        int oldCapacity = elementData.length;
        // 新容量 = 旧容量 + 单次扩容数量
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        // 判断计算出来的新数组容量是否满足最小要求
        // 不满足
        if (newCapacity - minCapacity <= 0) {
            // 判断最小容量是否小于0
            // 此情况存在于多个线程同时对集合进行操作
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        // 满足
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
}

经过程序跟踪,Vector 实现扩容的方式与 ArrayList 的差别在于 Vector 可以指定单次扩容数量 capacityIncrement ,而 ArrayList 一律扩容为原来的两倍

LinkedList

LinkedList 实现的是 链表 的数据结构,查找、修改元素的时间复杂度为 O(n),添加、删除元素的时间复杂度为 O(1)。另外,由于 LinkedList 还实现了 Deque 接口,它还能实现 栈、队列 这样的数据结构,可谓是多功能集合。

LinkedList扩展的方法有:

1、实现栈的方法:

  • void push(E e) 入栈
  • E pop() 出栈

2、实现(双向)队列的方法

  • boolean offerFirst(E e) 在队头添加元素
  • boolean offerLast(E e) 在队尾添加元素
  • E peekFirst() 检索队头元素
  • E peekLast() 检索队尾元素
  • E pollFirst() 队头元素出队
  • E pollLast() 队尾元素出队

LinkedList 的关键属性有:

// LinkedList 关键属性
// 元素的个数
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;
        }
    }

内部实现主要就是通过 Node 类进行增删查改结点,具体细节也就不一一列举了。

Set接口

Set接口则 不允许元素重复,其实现方式是用到之后要提到的 Map接口,以确保其中的元素不重复。

// 添加元素时,只存 key,不存 value
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
// PRESENT 是其内部自己定义的一个空对象,用于存入value
private static final Object PRESENT = new Object();

要做到集合内的元素不重复,得按顺序确保以下几点:

  1. 元素的编码值不同,即元素的 HashCode() 方法获得的值不同
  2. 元素内对应的属性均不相同,可以通过 equals() 方法进行比较

所以,运用 Set 接口存储的元素类 必须实现 HashCode() 和 equals() 方法

Set接口的实现类有:
  • HashSet (由HashMap实现)
  • 继承 HashSet 的 LinkedHashSet (与LinkedHashMap对应)
  • TreeSet (由TreeMap实现)
// Set 的实现类们的无参构造函数
public HashSet() { map = new HashMap<>(); }
public TreeSet() { this(new TreeMap<>()); }

每个 Set 的实现类都有一个 Map 的实现类与之对应,具体实现原理与方法我们在后面的 Map 集合再展开。

HashSet 与 TreeSet

两者具体区别如下:

Set集合HashSetTreeSet
数据结构哈希表二叉树
元素是否需要实现Comparable
元素是否有序
是否可以放入null值可以,但只能放入一个不可以

两者内部都是由一个一个包含 keyEntry 键值对的 Node 结点元素组成,只是存储与连接的方式不同。

LinkedHashSet 则额外比 HashSet 多提供了可预测的 迭代顺序 ,因为插入元素时是 有序 插入的,即元素之间有前后关系。

参考文献:JDK 11 API中文帮助文档

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值