7月18日更:
hashMap 是懒加载 只有put的时候 才创建数组 线程不安全主要体现在rehash,也就是扩容时候出现链表成环。链表插入的时候(jdk1.8是在尾部插入,1.8之前是头部插入)
hashMap的存入过程: Object--->hashCode(hashCode ^ hashCode>>>16 (无符号右移16位 相当于除以2的16次方))高16位和hash自身异或--->hash-->index (hash&(length-1))
扩容时,变成原来的2倍,那么新索引要么和原来的索引一样,要么增加原数组长度,JDK1.8中,判断hash值新增的那一位(扩大2倍,相当于左移一位)是0还是1, 是0新索引不变,是1增加原数组长度 为什么容量是2的次幂?答:减少hash冲突,2的次幂-1 后面每一位都是1 比如8-1 (1000 ——> 0111) 这样h&(length-1)也就是h&0111可以使数组每个位置都能添加到元素,假设是h&0101 那么第二个位置就一定是0 所以对应的数组这个位置也一直为空。
取余(%
)操作中如果除数是2的幂次 则等同于与其除数减一的与(&
)操作。位操作效率比算数运算效率高
当数组中链表长度大于8,并且数组size大于64,链表变成红黑树,当树长度小于6,恢复成链表
Java集合是Java基础里面最重要的知识点之一,刚接触时觉得用的地方不多,而且很累赘,但随着写的代码量的增加,越来越觉得集合类是最重要的基础,做算法题的时候,合理利用集合可以更快更有效的AC,因为它提供了很多的方法,只需直接调用即可。做项目的时候,会频繁的用到集合,比如参数的传递,实体的转化等等,都需要map,list等。想深入理解最好还是去看看源码。这里综合网上的很多文章对常用集合做一个总结。
Java集合可以分为两大类,实现Collection接口的类和实现Map的类,
Collection接口提供的方法有:增(add,addAll),删(remove,removeAll),查(size,contains) Map提供的方法有:增(put,putAll),删(remove),查(size,get,KeySet,ValueSet,EntrySet,contains)
(实际上Collection也继承了Iterable这个接口),实现Collection的又有三个接口,List,Set,Queue。所以集合类基本就分成四部分,Map,list,Set,Queue.
1.List接口:list表示一个列表,可以是数组,链表,栈,队列,元素可重复 常用方法:
ArrayList: 底层维护动态数组,允许为值为null 查找复杂度是 O(1),通过下标可以直接查找,增加或删除时需要移动大量元素
默认初始长度是10,每次通过Arrays.copyOf()方法扩充为原来的1.5倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量)
Arrays。copyOf()最底层调用了本地方法System.arraycopy(),最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多
每次增加元素时,都会调用ensureCapacity方法确保足够容量
线程不安全,可以通过Collections.synchronized(new ArrayList())方法转换为线程安全的
ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常 原因是调用list.remove()方法导致modCount和expectedModCount的值不一致。expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。modCount是AbstractList类中的一个成员变量 初始为0 所以迭代器移除某个元素的时候 用iterator.remove(),而不是list.remove 因为 iteartor.remove 有 expectedModCount = modCount;
线程不安全 主要表现在:1 多个线程进行add操作时可能会导致elementData数组越界
原因是 :add方法分两步 1 检查容量 需要是扩容 2 设置值 索引+1。 如果有两个线程同时检查了扩容 判断都不需要扩容(假设数组容量是10 此时长度是9) 一个线程set值之后 刚好达到最大容量 另一个线程再set就会越界。
2. 一个线程的值覆盖另一个线程添加的值。因为
- elementData[size] = e; 2 size = size + 1; 这两步不是原子操作。A,B线程同时add一个元素,导致的结果可能是size = 1 但是B线程覆盖了A线程添加的值。
代码如下
public static void main(String[] args) throws InterruptedException{ final List<Integer> list = new ArrayList<>(); new Thread(() -> { for (int i = 0 ; i < 1000 ;i ++){ list.add(i); try { Thread.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } } }).start(); new Thread(() -> { for (int i = 1000 ; i < 2000 ;i ++){ list.add(i); try { Thread.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } } }).start(); Thread.sleep(1000); for( int i = 0 ; i < list.size();i++){ System.out.println("第" + (i+1) + "个元素是 " + list.get(i)); } } //最后会出现 第 X 个元素 是 null的结果
如果想要用线程安全的arrayList , 可以使用CopyOnWriteArrayList。内部实现是 在进行add操作的时候 使用 Lock 加锁。
如果想要使用线程安全的linkedList 可以使用 ConcurrentLinkedQueue。
可被序列化,随机访问,能被克隆
参考博客:http://blog.csdn.net/ns_code/article/details/35568011
LinkedList: 1.底层维护双向链表(1.6是双向循环链表,1.7取消了循环 ),允许值为null,在头尾增加删除操作时很方便,内部只维护size,first,last三个变量或指针,当栈 push,pop,peek; 当队列,offer,poll,peek(peek会自动根据添加元素时的方法判断栈还是队列,默认队列)
2.可用作栈,队列,双端队列
3.线程不安全,可被序列化,能被克隆
4.内部单元是Node<E>
E item; Node<E> next; Node<E> prev;
参考博客:http://blog.csdn.net/ns_code/article/details/35787253
vector: 1 和ArrayList一样,线程安全,同Synchronized关键字提供了方法粒度级别的同步机制
2. 不支持序列化,可以克隆
Stack :栈 push ,pop ,peek empty
2.set接口: 无序且不重复
HashSet: 可存储null(占一个位置),线程不安全,适合添加,查询
底层采用hashMap实现,hashSet存储元素实际为hashMap中的Key,map中key不可重复,所以决定了set中元素不可重复,value为一个静态Object对象
可用foreach ,iterator 迭代
TreeSet: 有序集,基于TreeMap,线程不安全
默认排序是升序
treeSet需要额外的红黑树算法来维护集合的次序,性能最次。
方法:first,last,lower(E e):返回小于e的元素中最大元素,higher(E e)同理 subSet(E from, E to)
LinkedHashSet:基于链表的哈希表,具有迭代顺序的Set集合,保证存储顺序和取出顺序一致,线程不安全
迭代访问时效率高
更适合于插入、删除以及遍历操作
EnumSet: 为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值,元素有序
使用EnumSet.of方法新建
EnumSet性能>HashSet性能>LinkedHashSet>TreeSet性能
3.queue接口:队列,FIFO Deque接口是Queue接口的子接口,它代表一个双端队列
PriorityQueue:从小到大排列,本质上动态数组,维护最小堆,顶点是最小值,与ArrayList一致,默认初始容量是11
非线程安全,不允许插入Null
ArrayDeque:底层是循环数组,默认长度16
4.map接口:维护k,V键值对
HashMap: 内部是数组,数组的每个元素是一条单链表的头结点,链表是用来解决冲突的,同的key映射到了数组的同一位置处,就将其放入单链表中,key是唯一的,重复插入同一个键,value会被覆盖,Java8是用红黑树代替链表处理冲突(链表长度大于8,否则仍是链表)
初始容量是16,系统默认加载因子为0.75,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作
HashMap中key和value都允许为null,get方法:如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。
记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。
如果key不为null,则先求的key的hash值,根据hash值找到在table中的索引,在该索引对应的单链表中查找是否有键值对的key与目标key相等,有就返回对应的value,没有则返回null
put方法:如果key为null,则将其添加到table[0]对应的链表中,key不为null,求出key的hash,遍历对应的单链表,如果单链表中存在与目标key相等的键值对,
则将新的value覆盖旧的value,比将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到改单链表的头结点位置
每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。
length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
h&(length-1)的方法来代替取模,同样实现了均匀的散列
内部结构为Node<K,V>
final int hash; final K key; V value; Node<K,V> next;
参考博客:http://blog.csdn.net/ns_code/article/details/36034955
hashtable: 线程安全,支持序列化,能被克隆
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂
Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算
参考博客:http://blog.csdn.net/ns_code/article/details/36191279
TreeMap; 基于红黑树实现的
TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,
就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。
查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap
TreeMap的key不能为null,而HashMap的key可以为null
参考博客:http://blog.csdn.net/ns_code/article/details/36421085
LinkedHashMap: 与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,
因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。
非线程安全的
HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,
所有put进来的Entry都保存在哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,
每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。
说白了就是用两个数据结构保存同一个对象
参考博客:http://blog.csdn.net/ns_code/article/details/37867985