文章目录
java集合类是java数据结构的实现,它允许以各种方式将元素分组,并定义了各种使用这些元素更容易操作的方法。集合类有一个共同特点,就是它们只容纳对象(实际上是对象名,即指向地址的指针)。这一点和数组不同,数组可以容纳对象和简单数据。java中的集合类可以分为两大类: 一类是实现Collection接口;另一类是实现Map接口。
Java基本集合类如下图所示:
Collection接口
Collection<E>
接口是java集合类的基本接口,扩展了Iterable<E>
接口,for each
循环可与任何实现了Iterable接口的对象一起工作。Collection接口有两个基本方法:
public interface Collection<E>
{
boolean add (E element);
Iterator<E> iterator;
......
}
iterator
方法用于返回一个实现了Iterator
的对象,可以使用这个迭代器对象依次访问集合中的元素。Iterator
接口包含3个方法:
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
}
注:
next()
方法和remove()
方法的调用具有依赖性,如果调用remove
之前没有调用next
将会抛出IllegalStateException
异常。
List集合
List<E>
接口继承自Collection<E>
接口,描述的是一个有序的集合。元素可以添加到容器中某个特定的位置。将对象放置在某个位置上可以采用两种方式:使用整数索引或使用列表迭代器ListIterator
。可以通过调用listIterator()
方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)
方法创建一个一开始就指向列表索引为n的元素处的ListIterator。List接口定义了几个用于随机访问的方法:
void add(int index, E element)
E get(int index)
E remove(int index)
E set(int index, E element)
ListIterator<E> listIterator()
ListIterator<E> listIterator(int index)
ListIterator
是一个功能更加强大的, 它继承于Iterator
接口,只能用于各种List类型的访问。ListIterator能向List集合中添加对象,还可以逆向遍历,通过nextIndex()
和previousIndex()
定位当前的索引位置,还可以用set()
实现对象的修改。
public interface ListIterator<E> extends Iterator<E>
{
void add(E element)
void set(E element)
boolean hasPrevious()
E previous()
int nextIndex()
int previousIndex()
}
ArrayList
ArrayList继承自AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable这些接口。
- ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
- ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
- ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList。
**ArrayList常用方法:**有实现List
接口的带有索引的增删改查方法,也有size()
、isEmpty()
等方法。
扩容机制
ArrayList的底层是数组队列,相当于动态数组。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 空数组(用于空实例)。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于默认大小空实例的共享空数组实例。
//我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存ArrayList数据的数组
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList 所包含的元素个数
*/
private int size;
在无参构造中,ArrayList创建对象的时候其实就是创建了一个空数组,长度为0。
/**
*默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在有参构造中,传入的参数是正整数就按照传入的参数来确定创建数组的大小,否则抛出IllegalArgumentException
异常。
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
通过add(E e)
方法的调用可以达到扩容的目的,如果在添加的时候原数组是空的,就直接初始化数组长度为10,否则就加1。当需要的长度大于原来数组长度的时候就需要扩容,扩容后,新数组长度为原数组的1.5倍。
//下面是ArrayList的扩容机制
//ArrayList的扩容机制提高了性能,如果每次只扩充一个,
//那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//再检查新容量是否超出了ArrayList所定义的最大容量,
//若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
//如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
//比较minCapacity和 MAX_ARRAY_SIZE
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
注:
int newCapacity = oldCapacity + (oldCapacity >> 1);
移位运算符就是在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。作用:对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源。
ArrayList经典Demo
package list;
import java.util.ArrayList;
import java.util.Iterator;
public class ArrayListDemo {
public static void main(String[] srgs){
ArrayList<Integer> arrayList = new ArrayList<Integer>();
System.out.printf("Before add:arrayList.size() = %d\n",arrayList.size());
arrayList.add(1);
arrayList.add(3);
arrayList.add(5);
arrayList.add(7);
arrayList.add(9);
System.out.printf("After add:arrayList.size() = %d\n",arrayList.size());
System.out.println("Printing elements of arrayList");
// 三种遍历方式打印元素
// 第一种:通过迭代器遍历
System.out.print("通过迭代器遍历:");
Iterator<Integer> it = arrayList.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
// 第二种:通过索引值遍历
System.out.print("通过索引值遍历:");
for(int i = 0; i < arrayList.size(); i++){
System.out.print(arrayList.get(i) + " ");
}
System.out.println();
// 第三种:for循环遍历
System.out.print("for循环遍历:");
for(Integer number : arrayList){
System.out.print(number + " ");
}
// toArray用法
// 第一种方式(最常用)
Integer[] integer = arrayList.toArray(new Integer[0]);
// 第二种方式(容易理解)
Integer[] integer1 = new Integer[arrayList.size()];
arrayList.toArray(integer1);
// 抛出异常,java不支持向下转型
//Integer[] integer2 = new Integer[arrayList.size()];
//integer2 = arrayList.toArray();
System.out.println();
// 在指定位置添加元素
arrayList.add(2,2);
// 删除指定位置上的元素
arrayList.remove(2);
// 删除指定元素
arrayList.remove((Object)3);
// 判断arrayList是否包含5
System.out.println("ArrayList contains 5 is: " + arrayList.contains(5));
// 清空ArrayList
arrayList.clear();
// 判断ArrayList是否为空
System.out.println("ArrayList is empty: " + arrayList.isEmpty());
}
}
LinkedList
LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
List list=Collections.synchronizedList(new LinkedList(...));
LinkedList常用方法:
package list;
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedListDemo {
public static void main(String[] srgs) {
//创建存放int类型的linkedList
LinkedList<Integer> linkedList = new LinkedList<>();
/************************** linkedList的基本操作 ************************/
linkedList.addFirst(0); // 添加元素到列表开头
linkedList.add(1); // 在列表结尾添加元素
linkedList.add(2, 2); // 在指定位置添加元素
linkedList.addLast(3); // 添加元素到列表结尾
System.out.println("LinkedList(直接输出的): " + linkedList);
System.out.println("getFirst()获得第一个元素: " + linkedList.getFirst()); // 返回此列表的第一个元素
System.out.println("getLast()获得第最后一个元素: " + linkedList.getLast()); // 返回此列表的最后一个元素
System.out.println("removeFirst()删除第一个元素并返回: " + linkedList.removeFirst()); // 移除并返回此列表的第一个元素
System.out.println("removeLast()删除最后一个元素并返回: " + linkedList.removeLast()); // 移除并返回此列表的最后一个元素
System.out.println("After remove:" + linkedList);
System.out.println("contains()方法判断列表是否包含1这个元素:" + linkedList.contains(1)); // 判断此列表包含指定元素,如果是,则返回true
System.out.println("该linkedList的大小 : " + linkedList.size()); // 返回此列表的元素个数
/************************** 位置访问操作 ************************/
System.out.println("-----------------------------------------");
linkedList.set(1, 3); // 将此列表中指定位置的元素替换为指定的元素
System.out.println("After set(1, 3):" + linkedList);
System.out.println("get(1)获得指定位置(这里为1)的元素: " + linkedList.get(1)); // 返回此列表中指定位置处的元素
/************************** Search操作 ************************/
System.out.println("-----------------------------------------");
linkedList.add(3);
System.out.println("indexOf(3): " + linkedList.indexOf(3)); // 返回此列表中首次出现的指定元素的索引
System.out.println("lastIndexOf(3): " + linkedList.lastIndexOf(3));// 返回此列表中最后出现的指定元素的索引
/************************** Queue操作 ************************/
System.out.println("-----------------------------------------");
System.out.println("peek(): " + linkedList.peek()); // 获取但不移除此列表的头
System.out.println("element(): " + linkedList.element()); // 获取但不移除此列表的头
linkedList.poll(); // 获取并移除此列表的头
System.out.println("After poll():" + linkedList);
linkedList.remove();
System.out.println("After remove():" + linkedList); // 获取并移除此列表的头
linkedList.offer(4);
System.out.println("After offer(4):" + linkedList); // 将指定元素添加到此列表的末尾
/************************** Deque操作 ************************/
System.out.println("-----------------------------------------");
linkedList.offerFirst(2); // 在此列表的开头插入指定的元素
System.out.println("After offerFirst(2):" + linkedList);
linkedList.offerLast(5); // 在此列表末尾插入指定的元素
System.out.println("After offerLast(5):" + linkedList);
System.out.println("peekFirst(): " + linkedList.peekFirst()); // 获取但不移除此列表的第一个元素
System.out.println("peekLast(): " + linkedList.peekLast()); // 获取但不移除此列表的第一个元素
linkedList.pollFirst(); // 获取并移除此列表的第一个元素
System.out.println("After pollFirst():" + linkedList);
linkedList.pollLast(); // 获取并移除此列表的最后一个元素
System.out.println("After pollLast():" + linkedList);
linkedList.push(2); // 将元素推入此列表所表示的堆栈(插入到列表的头)
System.out.println("After push(2):" + linkedList);
linkedList.pop(); // 从此列表所表示的堆栈处弹出一个元素(获取并移除列表第一个元素)
System.out.println("After pop():" + linkedList);
linkedList.add(3);
linkedList.removeFirstOccurrence(3); // 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表)
System.out.println("After removeFirstOccurrence(3):" + linkedList);
linkedList.removeLastOccurrence(3); // 从此列表中移除最后一次出现的指定元素(从尾部到头部遍历列表)
System.out.println("After removeFirstOccurrence(3):" + linkedList);
/************************** 遍历操作 ************************/
System.out.println("-----------------------------------------");
linkedList.clear();
for (int i = 0; i < 100000; i++) {
linkedList.add(i);
}
// 迭代器遍历
long start = System.currentTimeMillis();
Iterator<Integer> iterator = linkedList.iterator();
while (iterator.hasNext()) {
iterator.next();
}
long end = System.currentTimeMillis();
System.out.println("Iterator:" + (end - start) + " ms");
// 顺序遍历(随机遍历)
start = System.currentTimeMillis();
for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i);
}
end = System.currentTimeMillis();
System.out.println("for:" + (end - start) + " ms");
// 另一种for循环遍历
start = System.currentTimeMillis();
for (Integer i : linkedList)
;
end = System.currentTimeMillis();
System.out.println("for2:" + (end - start) + " ms");
// 通过pollFirst()或pollLast()来遍历LinkedList
LinkedList<Integer> temp1 = new LinkedList<>();
temp1.addAll(linkedList);
start = System.currentTimeMillis();
while (temp1.size() != 0) {
temp1.pollFirst();
}
end = System.currentTimeMillis();
System.out.println("pollFirst()或pollLast():" + (end - start) + " ms");
// 通过removeFirst()或removeLast()来遍历LinkedList
LinkedList<Integer> temp2 = new LinkedList<>();
temp2.addAll(linkedList);
start = System.currentTimeMillis();
while (temp2.size() != 0) {
temp2.removeFirst();
}
end = System.currentTimeMillis();
System.out.println("removeFirst()或removeLast():" + (end - start) + " ms");
}
}
LinkedList使用带索引的
get()
方法获取某个特定元素时效率并不高,每次查找元素都要从链表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。get 方法做了微小的优化:如果索引大于size( )/ 2
就从列表尾端开始搜索元素 。
Vector
Vector集合其实现90%都和ArrayList一样,其内部都是通过一个容量能够动态增长的数组来实现的。不同点是Vector是线程安全的,因为其内部有很多同步代码块来保证线程安全。但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间,更推荐使用ArrayList。值得一提的是,Vector在进行扩容时新数组的长度为原来数组的2倍。
Set集合
Set<E>
接口继承了Collection<E>
接口,特点是不允许存储重复的元素,也无法使用普通for
循环来遍历集合中的元素。Set集合使用hashCode()
和equals()
方法来存储元素,而当存储自定义数据类型时这两个方法都要重写。
HashSet
HashSet是基于HashMap来实现的,操作很简单,更像是对HashMap做了一次“封装”,而且只使用了HashMap的key来实现各种特性。HashSet实际上是一个HashMap实例,都是一个存放链表的数组。它不保证存储元素的迭代顺序;此类允许使用null元素。HashSet不是线程安全的。
HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个固定对象:
private static final Object PRESENT = new Object();
HashSet常用方法:
boolean add(E e)
boolean remove(Object o)
boolean contains(Object o)
int size()
当向Set中添加对象时,首先调用此对象所在类的hashCode()方法,计算次对象的哈希值,此哈希值决定了此对象在Set中存放的位置;若此位置没有被存储对象则直接存储,若已有对象则通过对象所在类的equals()比较两个对象是否相同,相同则不能被添加。
LinkedHashSet
LinkedHashSet继承于HashSet,又是基于LinkedHashMap来实现的,具有可预测的迭代顺序,此类不是线程安全的。LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。
TreeSet
TreeSet是一个有序的集合,它的作用是提供有序的Set集合。它继承了AbstractSet抽象类,实现了NavigableSet,Cloneable,Serializable接口。TreeSet是基于TreeMap实现的,TreeSet的元素支持2种排序方式:自然排序或者根据提供的Comparator进行排序。
TreeSet通过实现Comparable接口并重写的compareTo()方法来判断元素是否重复、以及确定元素的顺序。
Map接口
Map<K,V>接口采用映射来存放键值对,将键映射到值的对象,一个映射不能包含重复的键,每个键最多只能映射一个值。Map集合是一个双列集合,数据结构针对键有效,跟值无关。
Map集合常用的方法:
V put(K key,V value)
V get(Object key)
void clear()
V remove(Object key)
boolean containsKey(Object key)
boolean containsValue(Object value)
boolean isEmpty()
int size()
处理映射时的一个难点就是更新映射项,当存放的键值对值为null时用get()
方法取出并操作时会返回null,因此会出现一个NullPointerException异常。下面是Map集合为了解决此问题的一些更新映射项的方法:
返回指定键对应的值,否则以给定的值映射给指定的键
default V getOrDefault(Object key,V defaultValue)
如果指定的键尚未与某个值相关联(或映射到null)将其与给定值相关联并返回null,否则返回当前值
default V putIfAbsent(K key,V value)
如果键与一个非null值关联,将函数应用到值并将键与结果关联返回get(key),若结果为空则删除这个键值对。
default V merge ( K key , V value , BiFunction < ? super V , ? super V , ?
extends V > remappingFunctlon )
Map集合有3种映射视图:键集合、值集合以及键值对集。键和键值对可以构成一个集,因为映射中一个键只能有一个副本。
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
注:keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collection接口。因此,可以像使用集合一样使用keySet。
HashMap
HashMap是基于哈希表实现了Map<K,V>接口的一个集合容器,并允许null值和null键。HashMap大致相当于Hashtable,除了它不是线程同步的,并允许null。jdk1.8之前HashMap由数组+链表组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在。jdk1.8之后在解决哈希冲突时,当链表长度大于阈值threshold(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断当前数组长度,小于64时选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。
HashMap的构造方法有4个:
// 默认构造函数。
public HashMap()
// 包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m)
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity)
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor)
loadFactor加载因子是控制数组存放数据的疏密程度,默认值为0.75。加载因子初始值大可以减少散列表再散列(扩容的次数),但同时会导致散列冲突的可能性变大(耗性能,遍历要操作红黑树),初始值小了,可以减小散列冲突的可能性,但同时扩容的次数可能就会变多。
常用方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
当用put()方法往集合中添加元素时,HashMap通过调用hash(key)
将key的哈希值与高16位做异或运算,增加了随机性,减少了冲突碰撞的可能性。当集合存储容量超过阈值threshold=capacity*loadFactor
且数组长度大于64时,HashMap调用resize()
方法对集合进行扩容,扩容后为容量原来的两倍。此时会进行rehash,并且会遍历hash表中所有的元素复制到新的HashMap,是非常耗时的。集合初始化时也会调用resize()
方法。
LinkedHashMap
LinkedHashMap继承于HashMap,实现了Map<K,V>接口,非线程同步。底层基于HashMap多了一个双向链表的维护,具有可预测的迭代次序。允许为null。可以设置两种遍历顺序:访问顺序(access-ordered)和插入顺序(insertion-ordered)。访问顺序是LRU(最近最少使用)算法的实现,可扩展成LRUMap使用,否则用处不大。
LinkedHashMap的构造方法有5个:
// 默认构造函数。
LinkedHashMap()
// 指定“容量大小”的构造函数
LinkedHashMap(int initialCapacity)
// 指定“容量大小”和“加载因子”的构造函数
LinkedHashMap(int initialCapacity, float loadFactor)
// 指定“容量大小”、“加载因子”和访问模式的构造函数
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
// 包含另一个“Map”的构造函数
LinkedHashMap(Map<? extends K, ? extends V> m)
LinkedHashMap和HashMap的put()、get()方法大致是一样的,put方法在创建结点的时候,构建的是LinkedHashMap.Entry
不再是Node
。get方法中需判断是否为访问顺序,如果是访问顺序的话,把该结点放到链表最后面。在遍历时,LinkedHashMap重写了Set<Map.Entry<K,V>> entrySet()
方法,从内部维护的双链表的表头开始循环输出,所以初始容量对遍历没有影响。
TreeMap
TreeMap基于Map<K,V>实现了NavigableMap接口,是一个有序集合,底层为红黑树。此类不是线程安全的,方法的时间复杂度为log(n),通过使用Comparator或者Comparable来比较key是否相等与排序的问题。
TreeMap的构造方法有4个:
//使用键的自然顺序构造TreeMap
TreeMap()
//构造一个新的TreeMap,按照给定的比较器排序
TreeMap(Comparator<? super K>)
//包含一个“Map”的构造函数
TreeMap(Map<? extends K,? extends V>)
//构造一个包含使用指定排序的“Map”的TreeMap
TreeMap(SortedMap<K,? extends V>)
在实例化一个TreeMap时,如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使用Comparable的compareTo(T o)方法来比较。无论哪种比较方式,key值都不能为null。