集合框架
集合的底层原理 (上层建筑,"经济"基础)
一、HashMap底层
- HashMap底层原理?
HashMap存储的元素是key,value格式的。用的是数组加链表的结合,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的.在每个数组元素上都有一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上.jdk1.8之后,当链表长度大于8之后,将链表转为红黑树,以减少搜索时间
eg:map.put("美团","小美");系统将调用"美团"这个key的hashCode()方法得到其hashCode值(该方法适用于每个Java对象),然后再通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞.当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高.如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞.那么通过什么方式来控制map使得Hash碰撞的概率又小,哈希桶数组(Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制.
疑问:HashMap的值到底是怎么存储的?
解答:HashMap存的时候,先根据 Hash算法把key存储下来,具体方法是把key的值存储到对应的数组下标位置,当存放的位置发生了哈希碰撞,就把这个key放到数组下标对应的链表上面。我老是在想的是key,value,到底是咋存的,一直有疑惑。
- HashMap的长度设计?
为了能让HashMap存取高效,尽量减少碰撞,也就是尽量要把数据分配均匀.如果Hash映射得比较均匀松散,一般应用很难出现碰撞.但是Hash值的范围,长度是40亿.这样的数组,内存是无法放下的.因此,用之前需要对数组的长度取模运算,得到的余数才能用来存放的位置也就是对应的数组下标取余(%)操作中如果除数是2的幂次则等价于于其除数减1的与(&)操作.并且采用二进制位操作&,相对于%能够提高运算效率这就解释了HashMap的长度为什么是2的幂次方.
为啥是取模呢?定一个数组的长度,hash算法 结合完,可能就是这样的结果
- HashMap默认初始化长度为16,之后每次扩容,容量为原来的2倍,创建时如果给了初始值,会将其扩为2的n次幂(“数字其实都是2的次幂”)
- HashMap和HashTable的区别?
线程是否安全:HashMap的非线程安全的,HashTable是线程安全的,HashTable内部的方法都经过synchronize修饰(如果要保证线程安全使用ConcurrentHashMap)
效率:因为线程安全的问题,HashMap比HashTable效率高一点.HashTable已经被淘汰不被使用.
对null key和null value的支持:HashMap中null可以作为键,这样的键只能有一个,可以有一个或者多个键对应的值为null.但是在HashTable中put进的键只要有一个是null,直接抛出NullPointerExcepiton - ConcurrentHashMap和HashTable的区别?
底层数据结构:
JDK1.7 ConcurrentHashMap 底层采分分段的数组+链表实现,JDK1.8 采用的数据结构是数组+链表/红黑二叉树.
Hashtable的底层数据结构是采用数组+链表的形式.
- 实现线程安全的方式:
JDK1.7 ConcurrentHashMap采用分段锁对整个桶数据进行了分段分隔,每一把锁只锁容器中一部分数据,多线程访问容器里不同数据就不会存在锁竞争,提高并发访问率
1.8的时候已经摒弃了分段分隔的概念,而是直接使用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized 和CAS 来操作,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍.HashTable使用的是同一把锁,效率非常低下.
多线程选择ConcurrentHashMap
二、HashSet底层原理
- HashMap和HashSet的区别?
如果你看过HashSet源码的话就应该知道:HashSet底层就是基于HashMap实现的.HashSet的源码非常非常少,因为除了clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用HashMap 中的方法。
HashMap | HashSet |
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put添加元素 | 调用add添加元素 |
实现了Map接口 | 实现了Set接口 |
HashMap使用键key计算hashCode(key值的唯一) | 使用成员对象计算HashCode值(、HashSet元素的唯一) |
- HashSet如何检查重复?
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值做比较,如果没有相符的hashcode,HashSet会假设对象没有出现过,但是如果发现相同的hashcode值的对象,这时候会调用equals方法检查hashcode相等的对象是否真的相同.如果两者相同,HashSet就不会让其加入成功.
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个equals方法法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值.如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据).
==与equals的区别
- ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
- ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
- ==指引用是否相同 equals()指的是值是否相同
三、ArrayList和LinkedList底层原理
- ArrayList和LinkedList的区别?
是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全
底层数据结构:ArrayList是Object数组,LinkedList是双向链表
内存空间占用:ArrayList空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存储直接前驱和直接后继以及数据) - ArrayList和Vector的区别?为什么要用ArrayList取代Vector?
Vector类的所有方法都是同步的.可以由两个线程安全的访问一个Vector对象,但是一个线程访问Vector的话,代码要在同步操作上面耗费大量的时间.
ArrayList不是同步的,所以在不需要保证线程安全时建议使用ArrayList - ArrayList和LinkedList是线程不安全的,多线程的情况需要使用哪个?
Vector > SynchronizedList > CopyOnWriteArrayList
Vector已经弃用了,不允许使用
- ArrayList的扩容机制?
是拷贝一个新的扩容的数组,并将之前的值也拷贝过来 (HashMap的扩容一样)
list集合说明:
ArrayList实现了RandomAcces接口,说明他具有快速访问功能,RandomAccess只是标识,并不是说ArrayList实现andomAccess接口才具有快速随机访问功能的
四、集合的遍历
- 各种遍历方式的性能?
传统的for循环遍历
基于计数器的,因为是基于元素的位置,按位置读取.所以我们可以知道,对于顺序存储,因为读取特定位置元素的平均时间复杂度是O(1),所以遍历整个集合的平均时间复杂度为O(n).而对于链式存储,因为读取特定位置元素的平均时间复杂度是O(n),所以遍历整个集合的平均时间复杂度为n的平方
- 迭代器遍历,Iterator
因为Iterator内部维护了当前遍历的位置,所以每次遍历,读取下一个位置并不需要从集合的第一个元素开始查找,只要把指针向后移一位就行了,这样一来,遍历整个集合的时间复杂度就降低为O(n);
- foreach循环遍历
分析Java字节码可知,foreach内部实现原理,也是通过Iterator实现的,只不过这个Iterator是Java编译器帮我们生成的,所以我们不需要再手动去编写.但是因为每次都要做类型转换检查,所以花费的时间比Iterator略长.时间复杂度和Iterator一样.
- 各遍历方式适用于什么场合?
传统的for循环遍历,基于计数器的:
顺序存储:读取性能比较高。适用于遍历顺序存储集合。
链式存储:时间复杂度太大,不适用于遍历链式存储的集合。
- 迭代器遍历,Iterator:
顺序存储:如果不是太在意时间,推荐选择此方式,毕竟代码更加简洁,也防止了Off-By-One的问题。
链式存储:意义就重大了,平均时间复杂度降为O(n),还是挺诱人的,所以推荐此种遍历方式。
- foreach循环遍历:
foreach只是让代码更加简洁了,但是他有一些缺点,就是遍历过程中不能操作数据集合(删除等),所以有些场合不使用。 而且它本身就是基于Iterator实现的,但是由于类型转换的问题,所以会比直接使用Iterator慢一点,但是还好,时间复杂度都是一样所以怎么选择,参考上面两种方式,做一个折中的选择。
- 链表的大size的数据用什么遍历?
Iterator,千万不能选择普通for循环遍历,效率贼低
如何选择集合
- 说说List,Set,Map三者的区别?
List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
Set(注重独一无二的性质): 不允许重复的集合.不会有多个元素引用相同的对象.
Map(用Key来搜索的专家): 使用键值对存储.Map会维护与Key有关联的值.两个Key可以引用相同的对象,但Key不能重复 - 集合框架底层数据结构总结
Collection
List
ArrayList:Object数组
Vector:Object数组
LinkedList:双向链表
Set
HashSet(无序,唯一):基于HashMap实现,底层采用HashMap存储元素
LinkedHashSet:LinkedHashSet继承于HashSet,并且其内部是通过LinkedHashMap来实现的.有点类似于我们之前说的
LinkedHashMap其内部是基于HashMap实现一样,不过还是有一点点区别的
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)
Map (键值对)
HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成.另外LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序.同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
HashTable:数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap:红黑树(自平衡的排序二叉树)
- 如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用
other
集合 --从实用角度去考虑
深入了解集合框架的整体结构以及各个集合类的实现原理,并灵活使用各个集合对编码有很大帮助
知识是层层递进的,你知道递进关系之后,还可以将这种递进彻底掌握,掌握包括知道底层,知道上层,由上层推底层,由底层推上层。(eg:HashMap、HashSet以及ArrayLsit等 彼此之间的关系融汇贯通)
知道的东西,原本以为已经都知道了,会发现后面还会有新东西的
哪里不清楚哪里有含糊,将问题描述清楚,这样后面终会解决