1、常见数据容器有哪些,各有什么优缺点?
2、哪些是线程安全的,具体是怎么实现的
常见数据容器
- 数组
- Collection
- Map
- others
数组
因为数组分配在一块连续的内存空间,所以实例化一个数组必须指定数组的大小。
优点: 内存是连续的,所以访问、查找元素的效率很高
缺点:删除慢,大小固定 ,有一定局限性。
Collection
- List
- ArrayList
- LinkeList
- Vector
- CopyOnWriteArrayList
- Set
- HashSet
- LinkedHashSet
- CopyOnWriteArraySet
ArrayList
ArrayList是List接口动态数组的实现方式。默认长度是10,也可以自定义。扩容规则:新数组大小为原长度的1.5倍。
数组的特点是其中的元素在内存中的地址是连续的。所以ArrayList的优点在于遍历快,get、set的效率高,时间复杂度为常数。缺点是在ArrayList中插入和删除效率较低,由于每插入/删除一项,都需要移动后续所有项的位置,时间复杂度为O(N)。
线程不安全
LinkeList
LinkedList是通过双向列表实现的。链表元素除了含有自身的值以为,还含有上一个元素和下一个元素的引用。
对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。但是缺点就是查找非常麻烦,需要从第一个或者最后一个索引开始
线程不安全
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
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;
}
get(int index)方法
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
CopyOnWriteArrayList
CopyOnWriteArrayList是线程安全的。实现原理是在写的时候加锁,并且拷贝当前数据到新的数组,操作新的数组,最后把当前数组指向新的数组。这样的好处就是在读的时候不用加锁,从而提升效率。
我们看下add()方法,其他remove等方法都是类似的方法。
public boolean add(E e) {
synchronized (lock) {
//获取当前的数组
Object[] elements = getArray();
int len = elements.length;
//把当前数组Copy到一个长度加1的新的数组里
Object[] newElements = Arrays.copyOf(elements, len + 1);
//加入新数据
newElements[len] = e;
//把当前数组的引用指向新的数组
setArray(newElements);
return true;
}
}
final void setArray(Object[] a) {
elements = a;
}
Vector
Vector和ArrayList比较类似,但不同的是Vector中的很多重要方法都是用synchronized实现同步,保证线程安全。在多线程下如果要保证线程安全,那么使用Vector比较好,但是在保证线程安全的同时效率也会下降。
线程安全
- Map
- HashMap
- Hashtable
- TreeMap
- ConcurrentHashMap
- ArrayMap
HashMap
先从put开始看
public V put(K key, V value) {
//如果table数组为空数组{}
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
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++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
//用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 计算目标位置
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) { //传入新的长度
Entry[] oldTable = table; //引用扩容前的Entry数组,扩容时重新计算位置再保存
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(1 << 30)
threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int) (newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K, V> e = src[j];
if (e != null) {
src[j] = null;//旧的置空
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
通过addEntry()我们发现HashMap扩容都是之前的两倍,再通过扩容的代码可以看到,是每个元素都后重新计算位置,保存到新的数组里。这将会是一个比较耗时的过程。
再看看get()
public V get(Object key) {
//如果key为null,则直接去table[0]处去检索即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
HashTable
1、HashTable主要是synchronized来保证线程安全
2、HashTable不支持Key为null
LinkedHashMap
1、LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。
ConcurrentHashMap
1、HashTable容器使用synchronized来保证线程安全效率会非常低。ConcurrentHashMap使用锁分段技术。适用于高并发
具体参考:https://www.cnblogs.com/ITtangtang/p/3948786.html
ArrayMap
1、ArrayMap是安卓特有的,针对内存方面做了优化
2、ArrayMap主要有两个数组存数据 int[] mHashes; Object[] mArray; 其中mHashes存的是key的HashCode,mHashes就比较有意思了,他存的是key和value两个值,[key1,value1,key2,value2…] 。所以在看代码时经常看到index<<1,左移一位增大两倍。
3、虽然内存方面会比HashMap有所优化,但是在大量数据的时候ArrayMap的查找和插入等操作都会比较耗时,主要是二分查找的锅。按照HashCode大小进行排序,插入或者删除操作都要移动数据。
public V put(K key, V value) {
final int osize = mSize;
final int hash;
int index;
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
//如果拿到Key的位置,直接覆盖
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
//取反值变成正数
index = ~index;
//判断是否要扩容
if (osize >= mHashes.length) {
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//判断是否释放过渡的两个数据
freeArrays(ohashes, oarray, osize);
}
//插入数据,index反面的数据要往后移
if (index < osize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) {
throw new ConcurrentModificationException();
}
}
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
if (N == 0) {
return ~0;
}
//二分查找
int index = binarySearchHashes(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
if (key.equals(mArray[index<<1])) {
return index;
}
// Search for a matching key after the index.
//找到一个Key的hashcode相同,但是key不是同一个
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
if (key.equals(mArray[end << 1])) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
if (key.equals(mArray[i << 1])) return i;
}
// Key not found -- return negative value indicating where a
// new entry for this key should go. We use the end of the
// hash chain to reduce the number of array entries that will
// need to be copied when inserting.
return ~end;
}
public V get(Object key) {
final int index = indexOfKey(key);
return index >= 0 ? (V)mArray[(index<<1)+1] : null;
}
最后去看一波Map相关的问题,看看是否都能回答上来。
http://www.importnew.com/31278.html
Set
Set接口和List接口最大的不同就是List中的元素是可以重复的,而Set中的元素不能重复,所以Set中的元素必须定义equals方法来保证元素的唯一性。还有就是,List接口放入元素是有序的,在使用时可以通过下标来找到对应位置的元素,而Set是离散的,当然,Set也是有维护了元素顺序的实现类的。
- Set
- HashSet
- LinkedHashSet
- TreeSet
最后还有一些安卓特有的
- SparseArray
- SparseIntArray
1、他们内部都是两个数组 private int[] mKeys; private Object[] mValues;来实现的。
2、都是通过二分查找法找到对应的位置
3、键值都是int,避免了自动装箱的key