回看了一下之前自己大学时候总结的C渣渣博客,没想到自己毕业不知不觉已经在java的开发之路上走了好几个月了,在java开发上还是一只小菜鸡,希望自己快点成长吧,借鉴多个大佬优秀博客,整理出来的java.Collection集合,欢迎大家纠错,菜鸡慢慢进步吧~~
ヽ(◕ฺˇд ˇ◕)ノ (ง •̀_•́)ง (ฅ´ω`ฅ) ✿◡‿◡
1.1 集合概述
集合是什么?
集合是java中提供的一种容器,可以用来存储多个数据。
1.2 集合框架
JAVASE提供了满足各种需求的API,在使用这些API前,先了解其继承与接口操作架构,才能了解何时采用哪个类,以及类之间如何彼此合作,从而达到灵活应用。
集合按照其存储结构可以分为两大类,分别是
单列集合java.util.Collection
双列集合java.util.Map。
接下来通过一张图来描述整个集合类的继承体系。
说明:
黄色:代表接口
绿色:代表抽象接口
蓝色:代表实现类
本篇文章我们主要来看看Collection 的相关内容。
Collection
2.1 Collection 简介
集合和数组既然都是容器,它们有什么区别呢?
1、数组长度固定,集合长度可变。
2、数组中只能是同一类型的元素且可以存储基本数据类型值。集合只能存对象,类型可以不一致。
Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有三个重要的子接口,分别是java.util.List,java.util.Set和java.util.queue。
List的特点是元素有序、元素可重复。
Set的特点是元素无序,而且不可重复。
Queue的特点是先进先出。
2.2 Collection 框架示意图
2.3 Collection 常用功能
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法适用于操作所有的单列集合。方法如下:
- public boolean add(E e):把给定的对象添加到当前集合中 。
- public void clear():清空集合中所有的元素。
- public boolean remove(E e):把给定的对象在当前集合中删除。
- public boolean contains(E e):判断当前集合中是否包含给定的对象。
- public boolean isEmpty():判断当前集合是否为空。
- public int size():返回集合中元素的个数。
- public Object[] toArray():把集合中的元素,存储到数组中。
代码示例:
package com.tanrui.collection;
import java.util.ArrayList;
import java.util.Collection;
public class DemoCellection {
public static void main(String[] args) {
//创建集合对象
Collection<String> coll = new ArrayList<String>();
//add
coll.add("李明");
coll.add("小花");
coll.add("张三");
System.out.println(coll);
//contains
System.out.println("判断 张三 是否在集合中 :" + coll.contains("张三"));
//remove
System.out.println("删除张三 :" + coll.remove("张三"));
System.out.println("删除张三后集合为 :" + coll);
//size
System.out.println("集合中有 :" + coll.size() + "个元素");
//toArray()
Object[] objects = coll.toArray();
for (int i = 0; i < objects.length; i++) {
System.out.println("集合为 :" + objects[i]);
}
//clear()
coll.clear();
System.out.println("clear后集合为:" + coll);
//isEmpty()
System.out.println("集合是否为空:" +coll.isEmpty());
}
}
运行结果为:
[李明, 小花, 张三]
判断 张三 是否在集合中 :true
删除张三 :true
删除张三后集合为 :[李明, 小花]
集合中有 :2个元素
集合为 :李明
集合为 :小花
clear后集合为:[]
集合是否为空:true
List接口
3.1 List接口介绍
java.util.List接口继承自Collection接口,在List集合元素可重复、元素有序。所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素,而且元素的存入顺序和取出顺序一致。
List接口特点:
- 元素存取有序
- 带有索引的集合:与数组的索引是一个道理
- 元素重复:通过元素的equals方法,来比较是否为重复的元素。
3.2 List接口中常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,还有一些根据元素索引来操作集合的特有方法,如下:
- public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
- public E get(int index):返回集合中指定位置的元素。
- public E remove(int index):移除列表中指定位置的元素, 返回的是被移除的元素。
- public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
代码示例:
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
// add
list.add("张三");
list.add("李四");
list.add("赵让");
System.out.println(list);
// add(int index,String s) 往指定位置添加
list.add(1,"李亮");
System.out.println(list);
// String remove(int index) 删除指定位置元素 返回被删除元素
// 删除索引位置为2的元素
System.out.println("删除索引位置为2的元素");
System.out.println(list.remove(2));
System.out.println(list);
// String set(int index,String s)
// 在指定位置 进行 元素替代(改)
// 修改指定位置元素
list.set(0, "太二");
System.out.println(list);
// String get(int index) 获取指定位置元素
// 跟size() 方法一起用 来 遍历的
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//还可以使用增强for
for (String string : list) {
System.out.println(string);
}
}
}
List的子类
4.1 ArrayList
java.util.ArrayList集合数据存储的结构是数组结构—元素增删慢,查找快。
由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。
ArrayList是基于数组实现的,ArrayList内部维护一个数组elementData,用于保存列表元素,基于数组的数组这数据结构,我们知道,其索引元素是非常快:
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index]; // 索引无需遍历,效率非常高!
}
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element; // 索引无需遍历,效率非常高!
return oldValue;
}
从插入操作的源码看,插入前,要先判断是否需要扩容,然后把Index后面的元素都偏移一位,这里的偏移是需要把元素复制后,再赋值当前元素的后一索引的位置。显然,这样一来,插入一个元素,牵连到多个元素,效率自然就低了:
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, // 把index后面的元素都向后偏移一位
size - index);
elementData[index] = element;
size++;
}
同样,删除一个元素,需要把index后面的元素向前偏移一位,填补删除的元素,也是牵连了多个元素:
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0) {
// 把index后面的元素向前偏移一位,填补删除的元素
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
}
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
4.2 LinkedList
java.util.LinkedList集合数据存储的结构是双向链表结构-----元素增删快,查找慢的集合。
插入元素只需新建一个node,再把前后指针指向对应的前后元素即可:
// 链尾追加
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++;
}
// 指定节点前插入
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 插入节点,succ为Index的节点,可以看到,是插入到index节点的前一个节点
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
删除元素只要把删除节点的链剪掉,再把前后节点连起来就搞定:
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> 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;
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
但由于链表我们只知道头和尾,中间的元素要遍历获取的,所以导致了访问元素时,效率就不好:
Node<E> node(int index) {
// 使用了二分法
if (index < (size >> 1)) { // 如果索引小于二分之一,从first开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { // 如果索引大于二分之一,从last开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
4.3 Vector 和 Stack
Vector和Stack我们几乎是不使用的了,了解一下缺点:
- Vector线程同步影响效率
Vector也是基于数组实现,同样支持快速访问,并且线程安全因为跟ArrayList一样,都是基于数组实现,所以ArrayList具有的优势和劣势Vector同样也有,只是Vector在每个方法都加了同步锁,所以它是线程安全的。但我们知道,同步会大大影响效率的,所以在不需要同步的情况下,Vector的效率就不如ArrayList了。所以我们在不需要同步的情况下,优先选择ArrayList;而在需要同步的情况下,也不是使用Vector,而是使用SynchronizedList。 - Vector的扩容机制不完善
Vector默认容量也是10,跟ArrayList不同的是,Vector每次扩容的大小是可以指定的,如果不指定,每次扩容原来容量大小的2倍。不像ArrayList,如果是用Vector的默认构造函数创建实例,那么第一次添加元素就需要扩容,但不会扩容到默认容量10,只会根据用户指定或两倍的大小扩容。如果容量大小和扩容大小都不指定,开始可能会频繁地进行扩容
如果指定了容量大小不指定扩容大小,以2倍的大小扩容会浪费很多资源,如果指定了扩容大小,扩容大小就固定了,不管数组多大,都按这大小来扩容,那么这个扩容大小的取值总有不理想的时候。
protected Object[] elementData; // 元素数组
protected int elementCount; // 元素数量
protected int 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;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0); // 默认扩容大小为0,那么扩容时会增大两倍
}
public Vector() {
this(10); // 默认容量为10
}
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
modCount++;
ensureCapacityHelper(minCapacity);
}
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0) // 大于当前容量就扩容
grow(minCapacity);
}
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);
}
- Stack继承于Vector,在其基础上扩展了栈的方法
Stack我们也不使用了,它只是添加多几个栈常用的方法(这个LinkedList也有),简单来看下它们的实现:
// 进栈
public E push(E item) {
addElement(item);
return item;
}
// 出栈
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
Set接口
java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序且不重复,刚好全与list相反,set会以某种规则保证存入的元素不出现重复。
Set接口子类
5.1 HashSet
java.util.HashSet底层的实现其实是一个java.util.HashMap支持。
HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode与equals方法。
代码示例:
public class HashSetDemo {
public static void main(String[] args) {
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("张三"));
set.add("李四");
set.add("王二");
set.add("张三");
//遍历
for (String name : set) {
System.out.println(name);
}
}
}
运行结果:
张三
李四
王二
即,重复的元素,set不存储
HashSet集合存储数据的结构—哈希表
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
因此保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的,如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。HashSet没有提供get()方法,是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得。
5.2 LinkedHashSet
LinkedHashSet是 HashSet的子类,底层数据结构由哈希表和链表组成。
哈希表:保证元素的唯一性
链表:保证元素有序(存储和取出顺序一致)
代码示例:
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("秃头哥");
set.add("地中海哥");
set.add("平头哥");
set.add("假发哥");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
5.3 TreeSet
TreeSet底层是红黑树,TreeSet 真正的比较是依赖于元素的 compareTo()方法,而这个方法是定义在 Comparable 接口里面的。
所以,你要想重写该方法,就必须先实现 Comparable 接口。这个接口表示的就是自然排序。
代码示例:
public class TreeSetTest {
public static void main(String[] args) {
testTreeSetAPIs();
}
// 测试TreeSet的api
public static void testTreeSetAPIs() {
String val;
// 新建TreeSet
TreeSet tSet = new TreeSet();
// 将元素添加到TreeSet中
tSet.add("aaa");
// Set中不允许重复元素,所以只会保存一个“aaa”
tSet.add("aaa");
tSet.add("bbb");
tSet.add("eee");
tSet.add("ddd");
tSet.add("ccc");
System.out.println("TreeSet:"+tSet);
// 打印TreeSet的实际大小
System.out.printf("size : %d\n", tSet.size());
// 导航方法
// floor(小于、等于)
System.out.printf("floor bbb: %s\n", tSet.floor("bbb"));
// lower(小于)
System.out.printf("lower bbb: %s\n", tSet.lower("bbb"));
// ceiling(大于、等于)
System.out.printf("ceiling bbb: %s\n", tSet.ceiling("bbb"));
}
}
Queue接口
6.1 Queue接口介绍
队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
在队列这种数据结构中,最先插入的元素将是最先被删除的元素;反之最后插入的元素将是最后被删除的元素,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常
Queue是非线程安全的。
时间复杂度:
● 索引: O(n)
● 搜索: O(n)
● 插入: O(1)
● 移除: O(1)
6.2 Queue接口中常用的方法
- booleab add(E e):插入指定的元素要队列中,并返回true或者false,如果队列数量超过了容量,则抛出IllegalStateException的异常。
- boolean offer(E e):插入指定的元素到队列,并返回true或者false,如果队列数量超过了容量,不会抛出异常,只会返回false。
- E remove():搜索并删除最顶层的队列元素,如果队列为空,则抛出一个Exception
- E poll():搜索并删除最顶层的队列元素,如果队列为空,则返回null
- E element():检索但不删除并返回队列中最顶层的元素,如果该队列为空,则抛出一个Exception
- E peek(): 检索但不删除并返回最顶层的元素,如果该队列为空,则返回null
值得注意的是LinkedList实现了Deque接口,Deque接口继承了Queue接口,所以LinkedList也可以当做Queue来操作:
public class QueueTest {
public static void main(String[] args){
LinkedList linkedList = new LinkedList();
System.out.println("poll搜索并删除最顶层的队列元素,如果队列为空,则返回null:"+linkedList.poll());
// System.out.println("搜索并删除最顶层的队列元素,如果队列为空,则抛出一个Exception:"+linkedList.remove());
linkedList.add("100");
linkedList.add("200");
System.out.println("LinkedList数量"+linkedList.size());
linkedList.poll();
System.out.println("LinkedList数量"+linkedList.size());
System.out.println("检索但不删除并返回最顶层的元素,如果该队列为空,则返回nul:"+linkedList.peek());
}
Iterator迭代器
7.1 Iterator接口
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator。
Iterator接口也是Java集合中的一员,但它与Collection、Map接口有所不同,Collection接口与Map接口主要用于存储元素,Iterator主要用于迭代访问(即遍历)Collection中的元素,因此Iterator对象也被称为迭代器。
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,获取迭代器的方法:
public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素。
7.2 迭代的概念
即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
Iterator接口的常用方法有:
- public E next() : 返回迭代的下一个元素。
- public boolean hasNext() : 如果仍有元素可以迭代,则返回
true。
代码示例:
package com.tanrui.collection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<String>();
coll.add("张三");
coll.add("李四");
coll.add("王二");
//使用迭代器遍历--每个集合对象都有自己的迭代器
Iterator<String> it = coll.iterator();
while (it.hasNext()) { //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
}
}
运行结果:
张三
李四
王二
7.3 迭代器的实现原理
当遍历集合时,首先通过调用集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。
Iterator对象迭代元素的过程:
在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。
7.4 增强for(foreach)
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
格式:
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
代码示例:
package com.tanrui.collection;
import java.util.ArrayList;
import java.util.Collection;
public class For {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<String>();
coll.add("张三");
coll.add("李四");
coll.add("王五");
for(String s :coll){
System.out.println(s);
}
}
}
运行结果:
张三
李四
王五
来源:CSDN
作者:芮小谭
链接:https://blog.csdn.net/tanrui519521/article/details/103830633