이 기사에서는 컬렉션 프레임워크와 관련된 문제를 주로 소개하는 java에 대한 관련 지식을 제공합니다. Java 컬렉션 프레임워크는 뛰어난 성능과 사용하기 쉬운 인터페이스 및 클래스 세트를 제공합니다. 그것이 모두에게 도움이 되기를 바랍니다.
추천 학습: "java 학습 튜토리얼"
Java 컬렉션 프레임워크는 뛰어난 성능과 사용하기 쉬운 인터페이스 및 클래스 집합을 제공합니다. java.util
패키지에 있습니다. 컨테이너에는 주로 Collection과 Map이 포함됩니다. Collection은 개체 컬렉션을 저장하고 Map은 키-값 쌍(두 개체)의 매핑 테이블을 저장합니다java.util
包中。容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表
ConcurrentHashMap
来支持线程安全,并且 ConcurrentHashMap
的效率会更高,因为 ConcurrentHashMap
는 이중 연결 목록을 기반으로 구현되어 순차적으로만 접근할 수 있지만 연결 목록 중간에 요소를 빠르게 삽입하고 삭제할 수 있습니다. 뿐만 아니라 LinkedList는 스택, 큐 및 데큐로도 사용할 수 있습니다. | LinkedList |
---|---|
은 힙 구조를 기반으로 하며 우선순위 대기열을 구현하는 데 사용할 수 있습니다. | |
TreeMap | 은 레드-블랙 트리를 기반으로 구현되었습니다. | HashMap
HashTable | |
LinkedHashMap | 은 이중 연결 목록을 사용하여 삽입 순서 또는 가장 최근에 사용된(LRU) 순서로 요소의 순서를 유지합니다. 개체 | 목록 인터페이스는 고유하지 않고 순서가 지정된 개체 집합을 저장합니다.
Map 인터페이스는 키-값 개체 집합을 저장하여 키-값 매핑을 제공합니다. | ArrayList는 가변 길이 배열을 구현하고 메모리에 연속 공간을 할당합니다. 요소를 순회하고 요소에 무작위로 액세스하는 효율성은 상대적으로 높습니다. |
HashSet은 해시 알고리즘을 사용하여 Set을 구현합니다 | HashSet의 하위 레이어는 HashMap을 사용하여 구현되므로 hashCode 알고리즘을 사용하여 메모리 주소를 직접 결정하므로 쿼리 효율성이 높습니다. 요소의 추가 및 삭제 효율성이 높습니다 |
2. ArrayList 분석 | 1. ArrayList는 |
3. 소스 코드 분석
public class ArrayList<e> extends AbstractList<e> implements List<e>, RandomAccess, Cloneable, java.io.Serializable</e></e></e>
다음은 몇 가지 인터페이스를 간략하게 설명합니다
3.2 Properties
//版本号 private static final long serialVersionUID = 8683452581122892189L; //缺省容量 private static final int DEFAULT_CAPACITY = 10; //空对象数组 private static final Object[] EMPTY_ELEMENTDATA = {}; //缺省空对象数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //存储的数组元素 transient Object[] elementData; // non-private to simplify nested class access //实际元素大小,默认为0 private int size; //最大数组容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** * 构造具有指定初始容量的空列表 * 如果指定的初始容量为负,则为IllegalArgumentException */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); }}/** * 默认空数组的大小为10 * ArrayList中储存数据的其实就是一个数组,这个数组就是elementData */public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}/** * 按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表 */public ArrayList(Collection extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // 转换为数组 //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么久需要使用ArrayList中的方法去改造一下。 if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 否则就用空数组代替 this.elementData = EMPTY_ELEMENTDATA; }}
ensureCapacity(int minCapacity)
를 통해 이루어집니다. ensureCapacity
를 사용하여 ArrayList 인스턴스의 용량을 수동으로 늘려 증분 재할당량을 줄일 수도 있습니다. ensureCapacity(int minCapacity)
来实现。在实际添加大量元素前,我也可以使用ensureCapacity
来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
数组进行扩容时,会将**老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。**这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) { //判断初始化的elementData是不是空的数组,也就是没有长度 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了 //所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小 return Math.max(DEFAULT_CAPACITY, minCapacity); } //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用 return minCapacity;}private void ensureExplicitCapacity(int minCapacity) { modCount++; //minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用 /*第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候, minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal) 就会判断出是空的数组,就会给将minCapacity=10,到这一步为止,还没有改变elementData的大小。 第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是 minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length 是否够用,如果length不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。*/ if (minCapacity - elementData.length > 0) grow(minCapacity);}//ArrayList核心的方法,能扩展数组大小的真正秘密。private void grow(int minCapacity) { //将扩充前的elementData大小给oldCapacity int oldCapacity = elementData.length; //newCapacity就是1.5倍的oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1); /*这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0, 所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。 */ if (newCapacity - minCapacity 0) newCapacity = hugeCapacity(minCapacity); //新的容量大小已经确定好就copy数组,改变容量大小。 elementData = Arrays.copyOf(elementData, newCapacity);}//用来赋最大值private static int hugeCapacity(int minCapacity) { if (minCapacity MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
/** * 添加一个特定的元素到list的末尾。 * 先size+1判断数组容量是否够用,最后加入元素 */public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}/** * Inserts the specified element at the specified position in this * list. Shifts the element currently at that position (if any) and * any subsequent elements to the right (adds one to their indices). * * @param index index at which the specified element is to be inserted * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */public void add(int index, E element) { //检查index也就是插入的位置是否合理。 rangeCheckForAdd(index); //检查容量是否够用,不够就自动扩容 ensureCapacityInternal(size + 1); // Increments modCount!! //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;}
当调用add()方法时,实际函数调用:
add→ensureCapacityInternal→ensureExplicitCapacity(→grow→hugeCapacity)
例如刚开始初始化一个空数组后add一个值,会首先进行自动扩容
将底层数组的容量调整为当前列表保存的实际元素的大小的功能
public void trimToSize() { modCount++; if (size <h3>3.7 remove()方法</h3><p><code>remove()</code>方法也有两个版本,一个是<code>remove(int index)</code>删除指定位置的元素,另一个是<code>remove(Object o)</code>删除第一个满足<code>o.equals(elementData[index])</code>的元素。删除操作是<code>add()</code>操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋<code>null</code>值。</p><pre class="brush:php;toolbar:false">public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; //清除该位置的引用,让GC起作用 return oldValue; }
这里简单介绍了核心方法,其他方法查看源码可以很快了解
ArrayList采用了快速失败的机制,通过记录modCount
参数来实现。在面对并发的修改时,迭代器很快就会完全失败,并抛出ConcurrentModificationException
public class LinkedList<e> extends AbstractSequentialList<e> implements List<e>, Deque<e>, Cloneable, java.io.Serializable</e></e></e></e>🎜3.5 add() 메소드🎜
//实际元素个数transient int size = 0; //头结点transient Node<e> first; //尾结点transient Node<e> last;</e></e>🎜add() 메소드가 호출되면 실제 함수가 호출됩니다. 🎜🎜🎜add→ensureCapacityInternal→ensureExplicitCapacity(→grow→hugeCapacity)🎜🎜🎜예를 들어, 빈 공간을 초기화한 후 배열에 값을 추가하면 먼저 자동으로 확장됩니다🎜
public LinkedList() {}public LinkedList(Collection extends E> c) { this(); //将集合c中的各个元素构建成LinkedList链表 addAll(c);}🎜3.7 Remove() 메서드🎜🎜
remove( )
메소드에도 두 가지 버전이 있는데, 하나는 지정된 위치의 요소를 삭제하는 remove(int index)
이고, 다른 하나는 remove(Object o)
입니다. o.equals(elementData[ index])
요소를 만족하는 첫 번째 요소를 삭제합니다. 삭제 작업은 add()
작업의 반대 프로세스로, 삭제 지점 이후의 요소를 한 위치 앞으로 이동해야 합니다. GC가 작동하려면 마지막 위치에 null
값이 명시적으로 할당되어야 한다는 점에 유의해야 합니다. 🎜//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里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; }}</e></e></e></e></e>🎜3.8 기타 방법🎜🎜여기에서는 핵심 방법을 간략하게 소개합니다. 소스 코드를 보면 다른 방법을 빠르게 이해할 수 있습니다.🎜🎜3.9 Fail-Fast 메커니즘🎜🎜ArrayList는
modCount를 기록하여 빠른 실패 메커니즘을 채택합니다. code> 달성할 매개변수입니다. 동시 수정에 직면하면 반복자는 미래의 불특정 시간에 불특정 임의 동작의 위험을 감수하는 대신 신속하게 완전히 실패하고 <code>ConcurrentModificationException
예외를 발생시킵니다🎜메소드 이름 | |
---|---|
Void AddL ast(객체 O) | |
LinkedList
同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList
,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue_的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque
,它有着比LinkedList
(当作栈或队列使用时)有着更好的性能。
LinkedList的实现方式决定了所有跟下标相关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()
方法对其进行包装
public class LinkedList<e> extends AbstractSequentialList<e> implements List<e>, Deque<e>, Cloneable, java.io.Serializable</e></e></e></e>
这里可以发现LinkedList多了一层AbstractSequentialList
的抽象类,这是为了减少实现顺序存取(例如LinkedList)这种类的工作。如果自己想实现顺序存取这种特性的类(就是链表形式),那么就继承 这个AbstractSequentialList抽象类,如果想像数组那样的随机存取的类,那么就去实现AbstracList抽象类。
transient关键字修饰,这也意味着在序列化时该域是不会序列化的
//实际元素个数transient int size = 0; //头结点transient Node<e> first; //尾结点transient Node<e> last;</e></e>
public LinkedList() {}public LinkedList(Collection extends E> c) { this(); //将集合c中的各个元素构建成LinkedList链表 addAll(c);}
//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里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; }}</e></e></e></e></e>
public boolean add(E e) { linkLast(e); return true;}void linkLast(E e) { //临时节点l(L的小写)保存last,也就是l指向了最后一个节点 final Node<e> l = last; //将e封装为节点,并且e.prev指向了最后一个节点 final Node<e> newNode = new Node(l, e, null); //newNode成为了最后一个节点,所以last指向了它 last = newNode; if (l == null) //判断是不是一开始链表中就什么都没有,如果没有,则new Node就成为了第一个结点,first和last都指向它 first = newNode; else //正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的 最后一个节点,原先的最后一个节点就变成了倒数第二个节点 l.next = newNode; //添加一个节点,size自增 size++; modCount++;}</e></e>
addAll()
有两个重载函数,addAll(Collection extends E>)
型和addAll(int,Collection extends E>)
型,我们平时习惯调用的addAll(Collection<?extends E>)
型会转化为addAll(int,Collection extends<e>)</e>
型
public boolean addAll(Collection extends E> c) { return addAll(size, c);}public boolean addAll(int index, Collection extends E> c) { //检查index这个是否为合理 checkPositionIndex(index); //将集合c转换为Object数组 Object[] a = c.toArray(); //数组a的长度numNew,也就是由多少个元素 int numNew = a.length; if (numNew == 0) //如果空的就什么也不做 return false; Node<e> pred, succ; //构造方法中传过来的就是index==size //情况一:构造方法创建的一个空的链表,那么size=0,last、和first都为null。linkedList中是空的。 //什么节点都没有。succ=null、pred=last=null //情况二:链表中有节点,size就不是为0,first和last都分别指向第一个节点,和最后一个节点, //在最后一个节点之后追加元素,就得记录一下最后一个节点是什么,所以把last保存到pred临时节点中。 //情况三index!=size,说明不是前面两种情况,而是在链表中间插入元素,那么就得知道index上的节点是谁, //保存到succ临时节点中,然后将succ的前一个节点保存到pred中,这样保存了这两个节点,就能够准确的插入节点了 if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; } for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<e> newNode = new Node(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; } if (succ == null) { /*如果succ==null,说明是情况一或者情况二, 情况一、构造方法,也就是刚创建的一个空链表,pred已经是newNode了, last=newNode,所以linkedList的first、last都指向第一个节点。 情况二、在最后节后之后追加节点,那么原先的last就应该指向现在的最后一个节点了, 就是newNode。*/ last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true;}//根据引下标找到该结点并返回Node<e> node(int index) { //判断插入的位置在链表前半段或者是后半段 if (index > 1)) { Node<e> x = first; //从头结点开始正向遍历 for (int i = 0; i x = last; //从尾结点开始反向遍历 for (int i = size - 1; i > index; i--) x = x.prev; return x; }}</e></e></e></e>
/*如果我们要移除的值在链表中存在多个一样的值,那么我们 会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做 */public boolean remove(Object o) { if (o == null) { for (Node<e> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<e> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;}不能传一个null值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的前后指向都为null了,也把item为null,让gc回收它 x.item = null; size--; modCount++; return element;}</e></e></e></e></e>
**get(index)、indexOf(Object o)**等查看源码即可
在LinkedList中除了有一个Node的内部类外,应该还能看到另外两个内部类,那就是ListItr
,还有一个是DescendingIterator
内部类
/*这个类,还是调用的ListItr,作用是封装一下Itr中几个方法,让使用者以正常的思维去写代码, 例如,在从后往前遍历的时候,也是跟从前往后遍历一样,使用next等操作,而不用使用特殊的previous。 */private class DescendingIterator implements Iterator<e> { private final ListItr itr = new ListItr(size()); public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); }}</e>
两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效。
java.util의 모든 컬렉션은 실패하지 않는 반면, java.util.concurrent의 모든 컬렉션은 실패하지 않습니다
java.util.concurrent
아래의 빠른 실패java.util.ConcurrentModificationException
异常(并发修改异常),这就是快速失败java.util.concurrent
下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行结构的改变,不会报异常,而是正常遍历,这就是安全失败Arrays.copyOf()
来拷贝副本,在副本上增加元素,如果有其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出现一个副本。所以能防止fail-fast,这种机制并不会出错,所以我们叫这种现象为fail-safe总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent
包下去拿相应的类。
通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash 值和数组的长度length来计算出entry放在数组中的哪个位置上面,每次存放都是将entry放在第一个位置。
HashMap实现了Map接口,即允许放入key
为null
的元素,也允许插入value
为null
的元素;除该类未实现同步外,其余跟Hashtable
fail-safe
안전 실패인 java.util.ConcurrentModificationException
예외(동시 수정 예외) 클래스는 모두 스레드로부터 안전합니다. 스레드가 구조를 변경하는 경우 예외는 보고되지 않지만 순회는 정상입니다. 왜 이것이 java.util.concurrent 패키지에 있습니까? 컬렉션에 구조적 변경이 있는 경우 예외가 보고되지 않습니까?
Arrays.copyOf()
를 사용하여 복사본을 복사하고 요소를 복사본에 추가하면 여기에서 컬렉션의 구조도 변경됩니다. 순회 후에는 원래 참조가 복사본을 가리키도록 변경됩니다. 따라서 간단히 말해서 이 패키지 아래의 클래스가 추가되거나 삭제되면 복사본이 됩니다. 나타납니다. 따라서 이 메커니즘은 잘못되지 않으므로 이 현상을 오류 방지라고 부릅니다.🎜🎜🎜벡터도 스레드 안전합니다. 🎜🎜 안전 장치가 나타나는 이유는 추가 및 삭제를 구현하기 위해 서로 다른 기본 메커니즘을 사용하기 때문입니다. 위에서 언급한 것처럼 복사본이 있는 반면 arrayList, linekdList, verctor 등의 하위 레이어는 실제 참조에서 작동합니다. 그래서 예외가 발생합니다🎜🎜🎜4. 지금 벡터를 사용하지 않는 것이 좋습니다🎜🎜🎜벡터가 스레드 안전성을 달성하는 방법은 각 작업 메서드에 잠금을 추가하는 것입니다. 이러한 잠금은 실제로 필요하지 않습니다. 일반적으로 스레드 안전성은 일련의 작업을 잠그는 방식, 즉 스레드 안전성을 보장하기 위해 함께 동기화해야 하는 리소스를 잠그는 방식으로 달성됩니다.🎜🎜여러 스레드가 동시에 잠긴 메서드를 실행하는 경우, 이 메서드에는 다음과 같은 존재가 있습니다. Vector의 Vector🎜는 자체 구현에서 잠겨 있으므로 잠그고 다시 잠그는 것과 동일하며 이로 인해 추가 오버헤드가 발생합니다.🎜🎜Vector에도 빠른 실패 문제가 있어 순회 보안을 보장할 수 없습니다. 탐색할 때 추가 잠금을 추가하려면 arrayList를 직접 사용한 다음 잠그는 것이 좋습니다🎜🎜🎜요약: Vector는 스레드로부터 안전할 필요가 없을 때도 잠기므로 이로 인해 추가 오버헤드가 발생하므로 jdk1.5 이후에는 더 이상 사용되지 않습니다. 이제 스레드로부터 안전한 컬렉션을 사용하려는 경우 java.util.concurrent
패키지에서 해당 클래스를 가져올 수 있습니다. 🎜🎜5. HashMap 분석🎜🎜1. HashMap 소개🎜key
가 null
인 요소를 넣을 수 있고 값
을 삽입할 수도 있습니다. null
요소여야 하며, 이 클래스가 동기화를 구현하지 않는다는 점을 제외하면 나머지는 TreeMap과 달리 Hashtable
과 거의 동일하며 이 컨테이너는 요소의 순서를 보장하지 않습니다. 컨테이너는 필요에 따라 요소를 다시 해시할 수 있습니다. 요소의 순서도 다시 섞이므로 서로 다른 시간에 동일한 HashMap을 반복하는 순서가 다를 수 있습니다. 충돌을 처리하는 방법에 따라 해시 테이블을 구현하는 방법에는 두 가지가 있는데, 하나는 개방형 주소 지정 방식(Open addressing)이고, 다른 하나는 충돌 연결 목록 방식(연결 목록과 분리 연결)입니다. 🎜Java7 HashMap은 충돌 연결 목록 방법🎜을 사용합니다. 🎜🎜🎜🎜Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode
public class HashMap<k> extends AbstractMap<k> implements Map<k>, Cloneable, Serializable</k></k></k>
//序列号private static final long serialVersionUID = 362498820763181265L; //默认的初始容量static final int DEFAULT_INITIAL_CAPACITY = 1 [] table; //存放具体元素的集transient Set<map.entry>> entrySet; //存放元素的个数,注意这个不等于数组的长度transient int size; //每次扩容和更改map结构的计数器transient int modCount; //临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold; //填充因子,计算HashMap的实时装载因子的方法为:size/capacityfinal float loadFactor;</map.entry>
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小于0,否则报错 if (initialCapacity MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //填充因子不能小于或等于0,不能为非数字 if (loadFactor >> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}/** * 自定义初始容量,加载因子为默认 */public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}/** * 使用默认的加载因子等字段 */public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}public HashMap(Map extends K, ? extends V> m) { //初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; //将m中的所有元素添加至HashMap中 putMapEntries(m, false);}//将m的所有元素存入该实例final void putMapEntries(Map extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { //判断table是否已经初始化 if (table == null) { // pre-size //未初始化,s为m的实际元素个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); //将m中的所有元素添加至HashMap中 for (Map.Entry extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } }}
put()方法
先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则查找是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链表尾部,注意JDK1.7中采用的是头插法,即每次都将冲突的键值对放置在链表头,这样最初的那个键值对最终就会成为链尾,而JDK1.8中使用的是尾插法。此外,若table在该处没有元素,则直接保存。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<k>[] tab; Node<k> p; int n, i; //第一次put元素时,table数组为空,先调用resize生成一个指定容量的数组 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //hash值和n-1的与运算结果为桶的位置,如果该位置空就直接放置一个Node if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果计算出的bucket不空,即发生哈希冲突,就要进一步判断 else { Node<k> e; K k; //判断当前Node的key与要put的key是否相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判断当前Node是否是红黑树的节点 else if (p instanceof TreeNode) e = ((TreeNode<k>)p).putTreeVal(this, tab, hash, key, value); //以上都不是,说明要new一个Node,加入到链表中 else { for (int binCount = 0; ; ++binCount) { //在链表尾部插入新节点,注意jdk1.8是在链表尾部插入新节点 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果当前链表中的元素大于树化的阈值,进行链表转树的操作 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //在链表中继续判断是否已经存在完全相同的key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //走到这里,说明本次put是更新一个已存在的键值对的value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //在hashMap中,afterNodeAccess方法体为空,交给子类去实现 afterNodeAccess(e); return oldValue; } } ++modCount; //如果当前size超过临界值,就扩容。注意是先插入节点再扩容 if (++size > threshold) resize(); //在hashMap中,afterNodeInsertion方法体为空,交给子类去实现 afterNodeInsertion(evict); return null;}</k></k></k></k>
resize() 数组扩容
用于初始化数组或数组扩容,每次扩容后,容量为原来的 2 倍,并进行数据迁移
final Node<k>[] resize() { Node<k>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 对应数组扩容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 将数组大小扩大一倍 else if ((newCap = oldCap = DEFAULT_INITIAL_CAPACITY) // 将阈值扩大一倍 newThr = oldThr 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候 newCap = oldThr; else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap [] newTab = (Node<k>[])new Node[newCap]; table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可 if (oldTab != null) { // 开始遍历原数组,进行数据迁移。 for (int j = 0; j e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 如果是红黑树,具体我们就不展开了 else if (e instanceof TreeNode) ((TreeNode<k>)e).split(this, newTab, j, oldCap); else { // 这块是处理链表的情况, // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序 // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的 Node<k> loHead = null, loTail = null; Node<k> hiHead = null, hiTail = null; Node<k> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; // 第一条链表 newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; // 第二条链表的新的位置是 j + oldCap,这个很好理解 newTab[j + oldCap] = hiHead; } } } } } return newTab;}</k></k></k></k></k></k></k>
get()过程
public V get(Object key) { Node<k> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<k> getNode(int hash, Object key) { Node<k>[] tab; Node<k> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 判断第一个节点是不是就是需要的 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 判断是否是红黑树 if (first instanceof TreeNode) return ((TreeNode<k>)first).getTreeNode(hash, key); // 链表遍历 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;}</k></k></k></k></k>
HashSet是对HashMap的简单包装,其他还有迭代器等
关于数组扩容,从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还有31个桶是空的,也会进行扩容处理,其实此时,还有31个桶是空的,好像似乎不需要进行扩容处理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各个桶中,会提升访问效率。所以说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于元素在桶中均匀分布所带来的好处。
另外LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表(doubly-linked list)的形式将所有**entry**
连接起来,这样是为保证元素的迭代顺序跟插入顺序相同
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。如果为此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出NullPointerException
//反转列表中元素的顺序 static void reverse(List> list) //对List集合元素进行随机排序 static void shuffle(List> list) //根据元素的自然顺序 对指定列表按升序进行排序 static void sort(List<t> list) //根据指定比较器产生的顺序对指定列表进行排序 static <t> void sort(List<t> list, Comparator super T> c) //在指定List的指定位置i,j处交换元素 static void swap(List> list, int i, int j) //当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度 static void rotate(List> list, int distance)</t></t></t>
//使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引 //注意:此前必须保证List集合中的元素已经处于有序状态 static <t> int binarySearch(List extends Comparable super T>>list, T key) //根据元素的自然顺序,返回给定collection 的最大元素 static Object max(Collection coll) //根据指定比较器产生的顺序,返回给定 collection 的最大元素 static Object max(Collection coll,Comparator comp): //根据元素的自然顺序,返回给定collection 的最小元素 static Object min(Collection coll): //根据指定比较器产生的顺序,返回给定 collection 的最小元素 static Object min(Collection coll,Comparator comp): //使用指定元素替换指定列表中的所有元素 static <t> void fill(List super T> list,T obj) //返回指定co1lection中等于指定对象的出现次数 static int frequency(collection>c,object o) //返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1 static int indexofsubList(List>source, List>target) //返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1 static int lastIndexofsubList(List>source,List>target) //使用一个新值替换List对象的所有旧值o1dval static <t> boolean replaceA1l(list<t> list,T oldval,T newval)</t></t></t></t>
Collectons提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。
//返回指定 Collection 支持的同步(线程安全的)collection static <T> Collection<T> synchronizedCollection(Collection<T> c) //返回指定列表支持的同步(线程安全的)列表 static <T> List<T> synchronizedList(List<T> list) //返回由指定映射支持的同步(线程安全的)映射 static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) //返回指定 set 支持的同步(线程安全的)set static <T> Set<T> synchronizedSet(Set<T> s)
//返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。 emptyXxx() //返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。 singletonXxx(): //返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map unmodifiableXxx()
推荐学习:《java教程》
위 내용은 Java 수집 프레임워크의 상세 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!