List(接口)
ArrayList(class、数组、随机访问、非线程安全)
-
Array的实现原理:动态数组。与Java中的数组相比,它的容量能动态增长。ArrayList 是 List接口的可变数组的实现。实现了所有可选列表操作,并允许包括== null== 在内的所有元素。除了实现List接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于Vector类,除了此类是不同步的。)
-
每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity操作来增加ArrayLis实例的容量,这可以减少递增式再分配的数量。
-
注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)
-
实现的接口
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}
- 底层使用数组实现
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;
- 构造方法
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this(10);
}
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 存储
- set(int index, E element):该方法首先调用==rangeCheck(index)==来校验 index 变量是否超出数组范围,超出则抛出异常。而后,取出原index位置的值,并且将新的 element 放入 Index 位置,返回 oldValue。
public E set(int index, E element) {
==rangeCheck(index);==
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
/**
* Checks if the given index is in range. If not, throws an appropriate
* runtime exception. This method does *not* check if the index is
* negative: It is always used immediately prior to an array access,
* which throws an ArrayIndexOutOfBoundsException if index is negative.
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- add(E e):该方法是将指定的元素添加到列表的尾部。当容量不足时,会调用 ==grow ==方法增长容量。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// 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 + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
关于上述Arrays.copyof():Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值,例如:
import java.util.Arrays;
public class ArrayDemo {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 5);
int[] arr3 = Arrays.copyOf(arr1, 10);
for(int i = 0; i < arr2.length; i++)
System.out.print(arr2[i] + " ");
System.out.println();
for(int i = 0; i < arr3.length; i++)
System.out.print(arr3[i] + " ");
}
}
执行结果为:
1 2 3 4 5
1 2 3 4 5 0 0 0 0 0
3、add(int index,Eelement):在index位置插入element。将指定元素插入此列表中的指定位置。将当前位于该位置的元素(如果有)和任何后续元素向右移动(向其索引添加一个元素)。
/**
* 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) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
注:使用System.arraycopy()进行数组复制。
public static void (Object src,
int srcPos,
Object dest,
int destPos,
int length)
src:源数组;srcPos:源数组要复制的起始位置;dest:目的数组; destPos:目的数组放置的起始位置;length:复制的长度。
int[] fun ={0,1,2,3,4,5,6};
System.arraycopy(fun,0,fun,3,3);
则结果为:{0,1,2,0,1,2,6};
- 读取:这个方法就比较简单了,ArrayList能够支持随机访问的原因也是很显然的,因为它内部的数据结构是数组,而数组本身就是支持随机访问。该方法首先会判断输入的index值是否越界,然后将数组的 index 位置的元素返回即可。
public E get(int index) {
rangeCheck(index);
return (E) elementData[index];
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 动态调整数组长度;从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容有两个方法,==其中开发者可以通过一个 public 的方法ensureCapacity(int minCapacity)来增加ArrayList的容量,而在存储元素等操作过程中,如果遇到容量不足,会调用priavte方法private void ensureCapacityInternal(int minCapacity)==实现。
public void ensureCapacity(int minCapacity) {
if (minCapacity > 0)
ensureCapacityInternal(minCapacity);
}
private void ensureCapacityInternal(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
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);
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5 倍(从int
newCapacity = oldCapacity+(oldCapacity>>1)这行代码得出)。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以避免数组扩容的发生。
- 删除:ArrayList 提供了根据下标或者指定对象两种方式的删除功能。需要注意的是该方法的返回值并不相同,如下,根据下标进行删除返回删除的元素,根据指定对象输出返回boolean值:
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; // Let gc do its work
return oldValue;
}
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;
}
- 采用 fail-fast机制
- 特点:底层基于数组实现,实现随机访问接口,随机访问方便,add(int index,E object)及remove()时都会进行元素的移动,采用system.Arraycopy()方法,且每次扩容采用Arrays.copyof()方法,都产生新的数组。
Vector(class、数组、随机访问、线程安全)
LinkedList(class、链表、线程不安全)
- LinkedList 和 ArrayList一样,都实现了List接口,但其内部的数据结构有本质的不同==。LinkedList是基于链表实现的==(通过名字也能区分开来),所以它的插入和删除操作比ArrayList更加高效。但也是由于其为基于链表的,所以随机访问的效率要比 ArrayList 差。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}
在LinkedList中除了本身自己的方法外,还提供了一些可以使其作为栈、队列或者双端队列的方法。这些方法可能彼此之间只是名字不同,以使得这些名字在特定的环境中显得更加合适。是fail-fast的。
LinkedList 也是 fail-fast 的
Stack(class、栈、)
Map
HashMap
- HashMap 是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用 null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。
- HashMap的数据结构:在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是指针(引用),HashMap就是通过这两个数据结构进行实现。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。迭代 collection 视图所需的时间与HashMap实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高或将加载因子设置得太低。也许大家开始对这段话有一点不太懂,不过不用担心,当你读完这篇文章后,就能深切理解这其中的含义了。
-构造函数:有三个,分别为
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为initialCapacity,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;//保证初始容量是2的n次方。
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
- HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}
Entry 是一个static class,其中包含了key和value,也就是键值对,另外还包含了一个 next 的 Entry 指针。我们可以总结出:Entry 就是数组中的元素,每个 Entry 其实就是一个 key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
- 存储:(根据对象的hash值得到数组中的位置,根据对象的key值得到链表中的位置,如果key已存在,调用equals算法看两个key是否相等,相等则将oldvalue换成newvalue,返回oldvalue)
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
//其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
if (key == null)
return putForNullKey(value);
//通过调用hash方法对key进行哈希,得到哈希之后的数值。该方法实现可以通过看源码,其目的是为了尽可能的让键值对可以分不到不同的桶中
int hash = hash(key);
//根据上一步骤中求出的hash得到在数组中是索引i
int i = indexFor(hash, table.length);
//如果i处的Entry不为null,则通过其next指针不断遍历e元素的下一个元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
我们看一下方法的标准注释:在注释中首先提到了,当我们 put 的时候,如果 key 存在了,那么新的value会代替旧的value,并且如果key存在的情况下,该方法返回的是旧的 value,如果key不存在,那么返回null。从上面的源代码中可以看出:当我们往 HashMap 中 put 元素的时候,先根据 key 的hashCode重新计算 hash 值,根据 hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。addEntry(hash,key,value,i)方法根据计算出的 hash 值,将 key-value 对放在数组 table 的 i 索引处。
我们可以看到在 HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用hash(inth)方法所计算得到的hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(inth,intlength)方法来计算该对象应该保存在table数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
这段代码保证初始化时 HashMap 的容量总是2的n次方,即底层数组的长度总是为 2 的 n 次方。当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效 率。
HashMap提高效率的方式:
(1)长度设成2^n,保证快速得到元素所在数组的下标,有效避免哈希冲突。
(2)利用红黑树管理链表。
在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度,
LinkedHashMap
LinkedHashMap和HashMap的区别在于它们的基本数据结构上,看一下LinkedHashMap的基本数据结构,也Entry:
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
}
列一下Entry里面有的一些属性吧:
1、K key
2、V value
3、Entry<K, V> next
4、int hash
5、Entry<K, V> before
6、Entry<K, V> after
存储
LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法==void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),==提供了自己特有的双向链接列表的实现。
//这个方法应该挺熟悉的,如果看了HashMap的解析的话
2 public V put(K key, V value) {
3 //key为null的情况
4 if (key == null)
5 return putForNullKey(value);
6 //通过key算hash,进而算出在数组中的位置,也就是在第几个桶中
7 int hash = hash(key.hashCode());
8 int i = indexFor(hash, table.length);
9 //查看桶中是否有相同的key值,如果有就直接用新值替换旧值,而不用再创建新的entry了
10 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
11 Object k;
12 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
13 V oldValue = e.value;
14 e.value = value;
15 e.recordAccess(this);
16 return oldValue;
17 }
18 }
19
20 modCount++;
21 //上面度是熟悉的东西,最重要的地方来了,就是这个方法,LinkedHashMap执行到这里,addEntry()方法不会执行HashMap中的方法,
22 //而是执行自己类中的addEntry方法,
23 addEntry(hash, key, value, i);
24 return null;
25 }
void addEntry(int hash, K key, V value, int bucketIndex) {
2 //调用create方法,将新元素以双向链表的的形式加入到映射中
3 createEntry(hash, key, value, bucketIndex);
4
5 // Remove eldest entry if instructed, else grow capacity if appropriate
6 // 删除最近最少使用元素的策略定义
7 Entry<K,V> eldest = header.after;
8 if (removeEldestEntry(eldest)) {
9 removeEntryForKey(eldest.key);
10 } else {
11 if (size >= threshold)
12 resize(2 * table.length);
13 }
14 }
void createEntry(int hash, K key, V value, int bucketIndex) {
2 HashMap.Entry<K,V> old = table[bucketIndex];
3 Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
4 table[bucketIndex] = e;
5 //将该节点插入到链表尾部
6 e.addBefore(header);
7 size++;
8 }
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
LinkedHashMap读取元素
LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时(即按访问顺序排序),先将当前节点从链表中移除,然后再将当前节点插入到链表尾部。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
https://www.cnblogs.com/xiaoxi/p/6170590.html