new collection java_Java集合-Collection

Java集合-Collection

一、Collection继承关系

7f5ebfbacd87

image-20200920144019585.png

由上图可知Collection有三个子类,分别是Set、List、Queue。

特点:

Set:无序且值唯一

List:有序、值可重复

Queue:先进先出的线性表

二、Collection提供的方法

7f5ebfbacd87

8F9BF2C9-EC1B-4461-951A-A9F5DA1E9B07.png

Collection提供了对集合的通用操作

三、Collection子类

1、Set

无序且值唯一。

Set子类有:

HashSet

底层数据结构是哈希表(实际是hashMap),从构造函数可以看出在创建实例时会创建一个HashMap,该HashMap就是用来实际存储元素的,除此之外在创建HashSet实例时我们可以指定其内部HashMap的容量和加载因子(默认大小为16,加载因子为0.75)

public HashSet() {

map = new HashMap<>();

}

再来看下增删查数据是如何实现的:

add操作

public boolean add(E e) {

//add是调用HashMap的put操作

return map.put(e, PRESENT)==null;

}

remove操作

public boolean remove(Object o) {

return map.remove(o)==PRESENT;

}

contains操作

public boolean contains(Object o) {

return map.containsKey(o);

}

HashSet如何来保证元素唯一性? 1.依赖两个方法:hashCode()和equals()。

TreeSet

TreeSet是一个非同步的非线程安全的二叉树,底层数据结构是红黑树。(唯一,排序),其add , remove和contains操作的时间复杂度为log(n)

来看下默认构造函数:

public TreeSet() {

this(new TreeMap());

}

private transient NavigableMap m;

private static final Object PRESENT = new Object();

TreeSet(NavigableMap m) {

this.m = m;

}

可以看出其内部默认是使用TreeMap存储元素的,因为其内部元素是有序的,对于元素的排序有两种方式自然排序和比较器排序,自然排序就是当comparator为空的时候,构建无参构造函数的时候默认的一种排序方式,比较器排序就是在构造函数中传入comparator从而指定排序方式。

treeSet = new TreeSet<>(new Comparator() {

@Override

public int compare(String o1, String o2) {

return o1.length()-o2.length();

}

});

TreeSet保证元素唯一性的是通过比较的返回值是否是0来决定

LinkedHashSet

Set接口的哈希表和链接列表实现即保证插入顺序,(FIFO插入有序,唯一)由链表保证元素有序由哈希表保证元素唯一。linkedHashSet是一个非线程安全的集合。如果有多个线程同时访问当前linkedhashset集合容器,并且有一个线程对当前容器中的元素做了修改,那么必须要在外部实现同步

来看下其构造函数

public LinkedHashSet() {

super(16, .75f, true);

}

HashSet(int initialCapacity, float loadFactor, boolean dummy) {

map = new LinkedHashMap<>(initialCapacity, loadFactor);

}

LinkedHashSet父类为HashSet,然后在HashSet的构造函数中创建了LinkedHashMap实例,也就是说LinkedHashSet最终是使用LinkedHashMap来存储元素。

Set小结

我们简绍了三种Set在实际使用时可以根据需求选择合适的,同时我们也看到这三种Set的实现最终都是通过Map来存储元素的。

2、List

List链表是一种线性结构,其内部元素有序(插入有序)、不唯一,可以根据索引来查找获取数据。

ArrayList

底层通过数组实现,查找快增删慢,线程不安全。来看下默认构造函数

transient Object[] elementData;

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

}

可以看到存储元素的是一个叫做elementData的数组。

add操作

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

我们看到在增加元素前会先调用 ensureCapacityInternal来确保数组elementData有足够的空间,如果空间不足会进行扩容操作。

private void ensureCapacityInternal(int minCapacity) {

if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

}

//判断是否需要扩容

ensureExplicitCapacity(minCapacity);

}

private void ensureExplicitCapacity(int minCapacity) {

modCount++;

// 需要扩容

if (minCapacity - elementData.length > 0)

grow(minCapacity);

}

private void grow(int minCapacity) {

// 当前数组大小

int oldCapacity = elementData.length;

//扩容为原来的1.5倍

int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容后还不满足所需最小容量则把容量设置为所需最小容量

if (newCapacity - minCapacity < 0)

newCapacity = minCapacity;

//MAX_ARRAY_SIZE的值为Integer.MAX_VALUE - 8表示最大可设置的值

if (newCapacity - MAX_ARRAY_SIZE > 0)

newCapacity = hugeCapacity(minCapacity);

// 真正扩容操作是通过Arrays.copyOf来完成的

elementData = Arrays.copyOf(elementData, newCapacity);

}

private static int hugeCapacity(int minCapacity) {

if (minCapacity < 0) // 溢出

throw new OutOfMemoryError();

//所需最小容量大于MAX_ARRAY_SIZE则扩容为Integer.MAX_VALUE

return (minCapacity > MAX_ARRAY_SIZE) ?

Integer.MAX_VALUE :

MAX_ARRAY_SIZE;

}

再来看下在指定位置插入元素的操作

public void add(int index, E element) {

if (index > size || index < 0)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

//检查是否需要扩容

ensureCapacityInternal(size + 1);

//把插入位置后面所有元素后移一位

System.arraycopy(elementData, index, elementData, index + 1,

size - index);

//插入元素

elementData[index] = element;

size++;

}

remove操作

public boolean remove(Object o) {

if (o == null) {

for (int index = 0; index < size; index++)

if (elementData[index] == null) {

fastRemove(index);

return true;

}

} else {

for (int index = 0; index < size; index++)

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

可以看到remove中是根据equals来判断元素是否是要删除的,具体移除操作是通过fastRemove来完成。

private void fastRemove(int index) {

modCount++;

int numMoved = size - index - 1;

if (numMoved > 0)

//把移除位置之后所有元素向前移动一位

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

}

总体来说ArrayList底层采用数组存储元素在元素增删时通过copy数组来实现元素移动,其增删操作的时间复杂度为O(n)。

Vector

底层数组实现,查找快增删慢,线程安全。构造函数

public Vector() {

this(10);

}

public Vector(int initialCapacity) {

this(initialCapacity, 0);

}

//这里的capacityIncrement是指扩容时增加的容量

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底层也是数组实现,所以在增删数据时会涉及到数组容量的变化,这跟ArrayList类似下面是Vector扩容的核心内容,可以看出其在容量不足时会增加capacityIncrement的容量,如果capacityIncrement<0则直接增加一倍的容量。

private void grow(int minCapacity) {

// overflow-conscious code

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

}

Vector实现跟ArrayList类似最大的不同在于Vector是线程安全的。

stack

先进后出的结构,stack中peek函数是查看栈顶元素但并不移除,pop是弹出栈顶元素。

其构造函数是空实现

public Stack() {

}

push操作

public E push(E item) {

addElement(item);

return item;

}

public synchronized void addElement(E obj) {

modCount++;

//检查是否需要扩容

ensureCapacityHelper(elementCount + 1);

//存入数据

elementData[elementCount++] = obj;

}

pop操作

public synchronized E pop() {

E obj;

int len = size();

obj = peek();

removeElementAt(len - 1);

return obj;

}

public synchronized void removeElementAt(int index) {

modCount++;

if (index >= elementCount) {

throw new ArrayIndexOutOfBoundsException(index + " >= " +

elementCount);

}

else if (index < 0) {

throw new ArrayIndexOutOfBoundsException(index);

}

int j = elementCount - index - 1;

if (j > 0) {

//移动数据

System.arraycopy(elementData, index + 1, elementData, index, j);

}

elementCount--;

//将删除位置置空

elementData[elementCount] = null; /* to let gc do its work */

}

peek操作

public synchronized E peek() {

int len = size();

if (len == 0)

throw new EmptyStackException();

return elementAt(len - 1);

}

public synchronized E elementAt(int index) {

if (index >= elementCount) {

throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);

}

return elementData(index);

}

E elementData(int index) {

return (E) elementData[index];

}

LinkedList

底层双链表实现,查找慢增删快,线程不安全,LinkedList同时实现了List, Deque两个接口也就是说它既可以作为list也可作为deque使用。

既然是双链表则会有节点的概念,我们来看下它的Node,这是LinkedList的一个内部类。

private static class Node {

E item;

Node next;

Node prev;

Node(Node prev, E element, Node next) {

this.item = element;

this.next = next;

this.prev = prev;

}

}

add操作

public boolean add(E e) {

linkLast(e);

return true;

}

//在表尾插入一个Node

void linkLast(E e) {

final Node l = last;

final Node newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

add(index,obj)

public void add(int index, E element) {

//检查插入位置是否合法

checkPositionIndex(index);

if (index == size)

linkLast(element);

else

linkBefore(element, node(index));

}

//插入链表

void linkBefore(E e, Node succ) {

// assert succ != null;

final Node pred = succ.prev;

final Node newNode = new Node<>(pred, e, succ);

succ.prev = newNode;

if (pred == null)

first = newNode;

else

pred.next = newNode;

size++;

modCount++;

}

remove操作

public boolean remove(Object o) {

if (o == null) {

for (Node x = first; x != null; x = x.next) {

if (x.item == null) {

unlink(x);

return true;

}

}

} else {

//先查找到要删除节点

for (Node x = first; x != null; x = x.next) {

if (o.equals(x.item)) {

//移除节点

unlink(x);

return true;

}

}

}

return false;

}

E unlink(Node x) {

// assert x != null;

final E element = x.item;

final Node next = x.next;

final Node prev = x.prev;

//修改前驱指针

if (prev == null) {

first = next;

} else {

prev.next = next;

x.prev = null;

}

//修改后继指针

if (next == null) {

last = prev;

} else {

next.prev = prev;

x.next = null;

}

x.item = null;

size--;

modCount++;

return element;

}

可以看出LinkedList的数据操作大多都是链表的操作所以其特点是增删快查找慢,在类内部LinkedList维护了first和last两个指针,这也是其能实现deque功能的基础。在作为deque时offer表示在队尾入队一个元素,poll是出队队首一个元素,peek是查看队首元素但并不出队。在作为deque时无法调用list相关接口方法。

3、Queue

队列是一种先进先出的线性结构,不支持随机访问数据。

PriorityQueue

优先队列是基于堆实现的,对内元素是有序的,offer,poll,remove和add等方法提供了O(log(n))的时间复杂度 ,而remove(obj)和contains方法的时间复杂度是O(n),peek时间复杂度为O(1)。排序是通过自然排序和比较器排序实现的,采用哪种排序是通过构造函数确定的,其中自然排序要求元素实现compare函数,比较排序则需要在构造函数中指明排序规则。

默认构造函数

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityQueue() {

this(DEFAULT_INITIAL_CAPACITY, null);

}

public PriorityQueue(int initialCapacity,

Comparator super E> comparator) {

//可以看出内部采用数组存储

if (initialCapacity < 1)

throw new IllegalArgumentException();

this.queue = new Object[initialCapacity];

this.comparator = comparator;

}

offer

public boolean offer(E e) {

if (e == null)

throw new NullPointerException();

modCount++;

int i = size;

if (i >= queue.length)

//扩容

grow(i + 1);

size = i + 1;

if (i == 0)

queue[0] = e;

else

//入队

siftUp(i, e);

return true;

}

private void siftUp(int k, E x) {

if (comparator != null)

siftUpUsingComparator(k, x);

else

//这里以分析siftUpComparable为例

siftUpComparable(k, x);

}

private void siftUpComparable(int k, E x) {

Comparable super E> key = (Comparable super E>) x;

while (k > 0) {

//找k位置的父节点的index

int parent = (k - 1) >>> 1;

//k位置的父节点

Object e = queue[parent];

//调整堆,大于父节点的就不动,小于父节点的就上浮

if (key.compareTo((E) e) >= 0)

break;

queue[k] = e;

k = parent;

}

queue[k] = key;

}

poll操作

public E poll() {

if (size == 0)

return null;

int s = --size;

modCount++;

E result = (E) queue[0];

E x = (E) queue[s];

queue[s] = null;

if (s != 0)

//调整堆

siftDown(0, x);

return result;

}

private void siftDown(int k, E x) {

if (comparator != null)

siftDownUsingComparator(k, x);

else

siftDownComparable(k, x);

}

private void siftDownComparable(int k, E x) {

Comparable super E> key = (Comparable super E>)x;

int half = size >>> 1;

while (k < half) {

int child = (k << 1) + 1; // assume left child is least

Object c = queue[child];

int right = child + 1;

if (right < size &&

((Comparable super E>) c).compareTo((E) queue[right]) > 0)

c = queue[child = right];

if (key.compareTo((E) c) <= 0)

break;

queue[k] = c;

k = child;

}

queue[k] = key;

}

ArrayDeque

双端队列,底层数组实现。

默认构造函数

public ArrayDeque() {

//数组大小默认16

elements = new Object[16];

}

因为可以双端操作数据所以其内部采用head和tail来存储头尾元素的index这样就可以快锁找到头尾元素。ArrayDeque还规定elements的size必须是2的整数次幂,当我们设置容量大小不是2的整数次幂时会进行调整

public ArrayDeque(int numElements) {

allocateElements(numElements);

}

private void allocateElements(int numElements) {

int initialCapacity = MIN_INITIAL_CAPACITY;

// Find the best power of two to hold elements.

// Tests "<=" because arrays aren't kept full.

if (numElements >= initialCapacity) {

initialCapacity = numElements;

initialCapacity |= (initialCapacity >>> 1);

initialCapacity |= (initialCapacity >>> 2);

initialCapacity |= (initialCapacity >>> 4);

initialCapacity |= (initialCapacity >>> 8);

initialCapacity |= (initialCapacity >>> 16);

initialCapacity++;

if (initialCapacity < 0) // Too many elements, must back off

initialCapacity >>>= 1; // Good luck allocating 2^30 elements

}

elements = new Object[initialCapacity];

}

allocateElements实现思路如下:

1.要明确2整数次幂使用二进制的表现形式如下:0...010...0,中间有一个1,其它的都是0。

2.根据1的形式,计算使输入任意的X,等式成立的Y。X的二进制形式为????????,是一个未知数,这样如何求得Y呢?方法很简单,找到X最高位为1的位置:那么X就是0..001???,这种形式了。那么所求的Y就是0..010...0,其值就是比X最高位为1再高一位为1,其它位为0的值。

3.X的最高为1的那一位是未知的,如何求更高一位为1的Y呢?直接求是没有办法的,但是可以通过将X最高位为1后面所有位都变成1,再加1进位的方式办到。就是0..001???变成0.001..1,使用这个+1就会变成所要的Y:0.010...0了。

4.如何保证X最高位为1后面都是1呢?这个就是上面位运算所实现的内容了。假设X是0..01???,左移一位就是0.001??,做或运算就变成了0..011??,是不是很巧妙,出现了两位为1的就移动2位,获得四位为1的值,这样移动到16的时候就涵盖了32位整数的所有范围了。这个时候+1可能发生整数溢出,所以再左移一位保证在整数范围内。

addFirst

public void addFirst(E e) {

if (e == null)

throw new NullPointerException();

//(head - 1) & (elements.length - 1)的作用是确定head的index

elements[head = (head - 1) & (elements.length - 1)] = e;

//首尾指向同一位置 扩容至原先两倍大小

if (head == tail)

doubleCapacity();

}

private void doubleCapacity() {

assert head == tail;

int p = head;

int n = elements.length;

int r = n - p; // number of elements to the right of p

int newCapacity = n << 1;

if (newCapacity < 0)

throw new IllegalStateException("Sorry, deque too big");

Object[] a = new Object[newCapacity];

System.arraycopy(elements, p, a, 0, r);

System.arraycopy(elements, 0, a, r, p);

elements = a;

head = 0;

tail = n;

}

pollFirst

public E pollFirst() {

final Object[] elements = this.elements;

final int h = head;

@SuppressWarnings("unchecked")

E result = (E) elements[h];

// Element is null if deque empty

if (result != null) {

elements[h] = null; // Must null out slot

head = (h + 1) & (elements.length - 1);

}

return result;

}

在addFirst中(head - 1) & (elements.length - 1)操作主要是确定入队的队首元素的位置,该操作相当于取模操作同时还很好的处理了head-1是-1的情况(head-1是-1时该操作的结果是elements.length - 1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值