1 集合的分类
集合主要分为单列和双列集合:
1 单列的顶层父类接口Collection,子类分为接口List和接口Set
- a. List :ArrayList, LinkedList, Vector
- b. Set: HashSet, LinkedHashSet
单列集合的区别
List有序且可重复而Set无序且不可重复,但LinkedHashSet是有序的,按照装入数据的顺序迭代
其中ArrayList与Vector底层是数组实现,而LinkedList是链表实现,而HashSet则是HashTable实现
如果需要查阅,ArrayList速度更快(有角标),而当需要增删,则LinkedList更快(只需要修改节点,不需要扩容)
2 双列集合的顶层父类接口为Map
所有Map类都是双列,即储存键值对的集合,key不可重复,且没有顺序
- a HashMap 与 HashTable .
- b LinkedHashMap
双列集合的区别
HashMap与HashTable的主要区别:HashMap线程不安全,但是效率高而HashTable线程安全,效率低(线程与安全的问题涉及到锁,即同步与异步的问题)
LinkedHashMap是HashMap的子类,但是他是有序的,也是按照装入数据的顺序迭代
2 集合的遍历
(1)有角标集合的遍历
ArrayList与Vector这种有角标的有序集合有三种遍历形式,即for循环,for each循环与迭代器
for-each
public static void m1() {
ArrayList<Integer> arrayList =new ArrayList<>();
arrayList.add(1);
arrayList.add(1);
arrayList.add(2);
arrayList.add(4);
arrayList.forEach((x)->System.out.println(x));
}
for 循环
public static void m1() {
ArrayList<Integer> arrayList =new ArrayList<>();
arrayList.add(1);
arrayList.add(1);
arrayList.add(2);
arrayList.add(4);
for(int i=0; i<arrayList.size(); i++){
System.out.println(arrayList.get(i));
}
}
迭代器(见下面)
(2)无序集合的遍历
Map与Set的子类都是无序集合,所以普通for循环无法遍历,只能用for each与迭代器,其中for each与有序集合操作一致
迭代器循环
public static void m2() throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
HashMap<Integer, String> hashMap =new HashMap<Integer, String>();
hashMap.put(12, "sad");
hashMap.put(13, "sad2");
hashMap.put(124, "s21ad");
Set<Integer> enSet =hashMap.keySet();
Iterator<Integer> iterator =enSet.iterator();
while (iterator.hasNext()) {
Integer integer =iterator.next();
System.out.println(hashMap.get(24));
System.out.println(hashMap.get(integer));
}
迭代器循环需注意:
1 迭代器的next()方法用一次边移动到了下一个元素,所以一次循环只能使用一次next()方法
2 一个迭代器只能使用一次,使用完后迭代器中便没有元素了,要再使用必须重新创建迭代器
3 HashMap和HashTable本身都没有迭代器,是使用了entrySet()或者KeySet()方法得到了Set集合,再使用Set中的迭代器来遍历的
3 集合的常见方法啊
(1)增加元素
单列集合用add(),双列集合用put(),其中有角标的可以使用add(index int ,Object obj),在指定位置插入。
(2)删除元素
使用remove()方法,与add类似,需注意的是如果是遍历删除在有角标集合中的元素,for循环应从后往前删除或者使用迭代器删除,否则角标易错
(3)改变元素
单列使用Set方法,双列直接用put()解决
(4)查阅元素
单列用contains()方法,双列分为containsKey()与containsValue()
2019.9.18关于各种集合数据结构的补充
最近有空翻阅了一些关于各类集合数据结构的源代码及相关文章,对集合有了进一步的理解。
(1) 数组实现的数组:ArrayList及Vector
这两个集合都是动态数组实现的,在源代码中都是创建了一个object[],来实现数据的增减。由于Vector源代码中add以及get方法全都使用了sychronized锁住,所以是线程同步,速度较慢,而ArrayList则没有,所以线程不安全但速度快。
(2)链表实现:LinkedList
LinkedList底层是链表,而且是由双向链表实现
(3)数组+链表:hashset,hashmap,hashtable
hashSet的底层数据结构就是hashMap,所以三者的数据结构一样,都是数组加链表,只不过hashSet的数据是储存在hashMap中的key值里的,而value是一个空的object,因为key值不能重复,所以hashSet不能重复
关于hashMap的数据结构,可以理解为底层是一个数组,数组的每一个单位储存的是链表,即一个Entry<K,V>键值对。在使用put方法时,其实首先是调用key.hashcode()得到hashcode返回值,以此得到索引确定数组储存的位置,此时再使用equal方法比较key,如果Key相同则直接覆盖value,但是不同的key值也有可能生成同样的hashcode值,即hash冲突,所以在每一个数组的位置上,如果发生了此类情况,即后来的链表成为插入之前储存在此的链表,成树形增长。而具体调用时,所以用get()方法时,会先匹配hashcode值,然后还要用equal()方法比较key值,才能取得value。
另外,为了尽量让链表的树能均匀分布,而不是在某一个索引上集中,所以hashMap中的Hash算法是 index = key.hashcode() & (hashMap.length-1)
也就是key对数组长度的取模运算,这个运算只有当hashmap的长度为2的n次方时,上述取模运算结果才会达到分布均匀的效果,这也就是为什么hashmap的自动扩容和初始化时,长度永远是2的指数倍,而hashmap中,capacity对应的是桶的容量,也就是数组长度,而不是元素个数,元素个数对应的是映射数量,这也就是为什么hashmap元素数量使用的是size()而不是length。
2019.9.27关于hashmap中hash算法的补充
这是HashMap中的关于key的hash算法,他使用了object.hashcode算法然后还要与自己进行了16位无符号右移后的值进行按位异运算
因为hashcode算法得到的值是32位的,其变化其实主要在高位,但是实际hashmap中数组的长度是远远低于这个数,如果直接在后面的index运算中按位与运算
static int indexFor(int h, int length) {
return h & (length-1);
}
那么因为低位的重复率高,导致整个Index的重复率很高,降低了hashmap效率
而当他使用自己对自己的右移运算按位异运算,等于是高位和低位的异运算,其实是把高位的影响传到了低位,从而大大降低了Index计算的重复率
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果数组中对应index上没有元素,直接储存
else {
//走到此代码块,即已经发生了hash碰撞,这个时候p是hash碰撞的节点
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//index相同,hash值相同,key值相同,则先将e赋值为p,记住这个节点,并将K赋值p.key
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果是红黑树节点,使用红黑树结构储存
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//index相同,key相同,碰撞节点后面没有.next的,直接头前插入
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//TREEIFY_THRESHOLD是等于8的,超过8会变成红黑树节点
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
//找了key完全相等的链表,直接新节点替换旧的
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
//key相同,value新的覆盖旧的
}
}