1、java容器有哪些?
java 中的集合容器分为两类,一类为Collection,另外一类为Map。
Collection集合主要分为两个类,一类是实现List 接口的 如 LinkedList、ArrayList、Vector 等,实现List接口的集合元素可重复,可填充多个null值,元素顺序跟插入顺序一致,是有序的。另外一类是实现了Set 接口的如 HashSet、TreeSet、LinkedHashSet 等,而实现set接口的集合的元素不可重复,最多可存放一个null值,针对HashSet而言,其实现方式是基于HashMap 实现,将Value值存放在HashMap的key中,因此HashSet中存放的值是不重复且无序的;而LinkedHashSet 是对HashSet 的一个升级版,在进行数据填充时,同时维护一张链表来记录元素的顺序,从而达到有序的效果;针对TreeSet 而言,它也是基于TreeMap 实现的,TreeMap 的底层则是依靠红黑树实现的,排序方式可依靠 值的自然顺序排序,根据映射时提供的comparator进行排序。
Map 集合中根据实现不同,分为 HashMap、HashTable、TreeMap、LinkedHashMap。
2、ArraList和LinkedList 实现原理及区别
ArrayList 和 LinkedList 二者都是 List 接口的实现类,因此二者都有 有序,元素可重复的特性;
java在创建ArrayList 时,默认无参构造初始化一个容量为10的堆内存空间,或者按照传入的数量构造指定容量大小的堆内存空间,或者构造一个含有指定元素的列表;ArrayList 底层利用数组实现,因此,ArrayList 在进行增删改查等操作时,均操作的是数组,因此也继承了数组的特性,即查询、修改快(可直接根据下标index 定位到数据和修改数据),增删慢(进行数据增删时,需要进行元素的移动)的特性。
java在创建LinkedList 时,底层利用双向循环链表实现,初始化后默认是空容量;LinkedList 在进行增删时速度比较快,仅需要移动指针的指向即可,而在查询时,速度较慢。
3、HashMap和TreeMap的实现原理及区别 1.7 和1,8的实现区别
HashMap底层是以数组和链表的结构实现,根据Key 的hashCode 和数组长度计算出entry 存放的位置,如果该位置被占用(出现了hash冲突),则以每次添加的entry为头结点,进行链式存放。而在1.8 中使用Node数组来存放entry ,该Node 数组可能是链表,也可能是红黑树;将当同一位置的entry数量不超过8时,使用于1.7相同的链表结构进行存储,当节点超过 8时,则会调用treeifyBin 方法,将链表转化成红黑树进行存储。
map.get(key) 时,先计算key的hashcode 定位到存放entry的数组下标,然后进行数据key比对,key 比对上了,则返回entry 中的value值;由于HashMap允许k - v 为null ,因此当get返回null 时,并不能说明 HashMap 中不存在该键,判断键存不存在需要利用 containsKey方法。
在创建HashMap 时,建议根据存储数据量指定HashMap的大小,即HashMap中的数组的长度,因为HashMap 的性能与 其容量大小 和负载因子相关。当存储的数据大于 容量大小 * 负载因子时,会触发HashMap的扩容,数组长度会增加到原长度的2倍,并将所有元素进行rehash ,重新进行存储。默认的负载因子大小为 0.75 ,默认容量大小为16 ;在指定HashMap 的容量大小时建议为2 的幂次方,为了降低哈希冲突,提升HashMap的性能
TreeMap 底层利用了红黑树进行实现,红黑树是一种近似平衡的二叉查找树。当进行K-V 键值对存放时,会构造一个entry对象作为存储节点,利用key 的值利用红黑树的存储规则,进行数据比对,节点着色,树形左右旋转得到应用的红黑树结构,并将entry存储在对应的节点上,由于需要利用key值进行大小比对,因此TreeMap 的key 是不允许为null 的,value 可为null 。而在进行map.get(key) 操作时,会利用key 的值进行红黑树遍历,查找对应的entry,最终返回entry的value值。
由于TreeMap 在底层利用红黑树进行存储,在节点的增删时会进行树形的改变,着色修改等,导致其增删效率较差;相对HashMap 而言,其根据hashCode 直接定位到数组下标进行取值,而TreeMap则是排序后再取值,其效率不如HashMap高。但是TreeMap 的内存占用小。
4、HashSet 和 TreeSet的实现原理 及区别
HashSet底层是利用HashMap 实现的,将Value 作为HashMap的key进行存储,组装一个统一的PRESENT 作为HashMap的Value值。由于map中的value都是固定的,在HashSet 进行 value add 时,会先调用map 的containsKey 方法,判断其是否存在,如果存在,则跳过不插入;HashSet 无序,不重复,可存储null 。
TreeSet 底层也是利用TreeMap 进行实现的,其特性为 有序,不重复,不可存储null。
5、ConCurrentHashMap 实现原理 1.7 和1.8 的区别
在1.7 中ConCurrentHashMap 利用的基于Segment+HashEntry数组,默认将其分成了16个Segment,在进行map 操作时,将对应的Segment 锁住,因此相对于HashTable 而言,ConCurrentHashMap 的效率提升了16倍;在进执行put 操作时,需要进行两次定位,首先进行Segment 定位,然后进行数组下标定位,定位后采用自旋锁和膨胀锁进行加锁,整个操作过程中全程加锁,整个segment 无法被其他线程使用;在获取map 的size 时,采用类似乐观锁的逻辑,先不加锁进行size 统计,统计两次的size 值如果相同,则直接返回,如果不同,则再次尝试统计,如果三次尝试 前后统计的size 都不通,则对每个segment 加锁,进行数量统计。
在1.8 中抛弃了Segment ,采用 CAS+Synchronized 保证并发更新的安全,底层还是利用 数组 + Node 数组(链表或红黑树)的模式进行存储;在执行put 方法时,首先判断key 值是否有哈希冲突,如果没有哈希冲突,直接CAS插入,如果出现了哈希冲突,则将冲突的那个Node锁起来,然后再进行插入。
1.7 与1.8 对比,1.7 是锁住一个Segment ,而1.8 是锁住的一个Node,粒度更细,1.8 中加入了红黑树,相对于链表而言,提升了查询速度。
6、HashMap 、HashTable、ConCurrentHashMap 的区别
1.7
HashMap 数组+ 链表实现,线程不安全,所有操作均不上锁,允许K -V 为null ,默认初始大小为16 ,加载因子为 0.75 ,扩容逻辑为 数组占用大小超过 原数组长度* 加载因子 时,长度扩容到原长度的2倍,所有数据进行rehash 重新摆放存储,下标索引计算逻辑为 index = hash & (tab.length – 1)
HashTable 数组+ 链表实现,线程安全,修改数据时锁住整HashTable ,K-V 均不可为null ,初始大小为 11 ,扩容逻辑为 newsize = olesize*2+1 ,下标索引计算逻辑为 (hash & 0x7FFFFFFF) % tab.length
ConCurrentHashMap segment+链表实现,线程安全,修改数据时锁住数据所在的分片,value值以volatile修饰,读取可不加锁,默认分16 个片段,相对HashTable 速度提升16倍,扩容对象为每个segment ,当段内存储的数据超过 段长度的75%,则触发扩容 。
1.8
HashMap 数组+Node 数组实现,与1.7 的区别在于,当Node数组存储的entry 超过 8个,且 数组的长度超过64 时,就会采用红黑树进行数据存储,而在1.8 中以链表存储时,是采用的尾部插入法,保证扩容前后的元素的顺序,避免链表成环。
HashTable 目前暂时未有什么变化
ConCurrentHashMap 改为 数组+Node 数组实现,与1.8 的HashMap 实现保持一致,只是在处理并发安全问题上,抛弃了 分片试锁,采用的CAS + Synchronized 实现,未出现哈希冲突,采用CAS 比较交换插入,出现哈希冲突,针对Node进行加锁然后进行数据修改。
7、java 如何构造线程安全的map
HashTable
ConCurrentHashMap
Collections.synchronizedMap(map)
8、ConcurrentHashMap 如何统计元素的个数?会有什么问题?
1.7
采用了类似乐观锁的逻辑,首先不加锁统计所有分片的数量,两次统计的值相同,则直接返回,不同则再次尝试统计,尝试3次均不同,则将整个Map的所有分片全部锁住,然后进行计数
1.8
map 利用volatile 修饰 baseCount ,每次新增或删除元素时,都会CAS更新baseCount 的值,更新失败的线程会利用CounterCell 记录元素的变化数量,最后将 baseCount 和 CounterCell 中的数值相加即可
9、1.8 中ConcurrentHashMap 中 Node 节点是如何设计的?为什么要用 volatile 修饰 Node 节点的属性?有什么作用?
Node 会根据node 中的数量Map 数组的大小来对应生成其结构,当Node中的数量小于等于8 时,会采用链表形式存储,新插入的元素采用尾插法, 而当Node 中的数量大于8 且 Map 中的数组长度大于64 时,则会将链表转化成红黑树进行存储,降低遍历的深度 。
采用volatile 修饰Node 节点,是为了将线程工作内存中修改的值回刷到主内存中,这样在map 进行元素读取的时候,可以不用加锁
10、HashMap 中的 hash 算法?这样的 hash 算法有什么好处(高低 16 位均参与运算)?如何寻址?
hash算法
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
先将key值取其hashcode 设为 h , 将 h 右移16位高位补0 与原 h 执行亦或运算
好处
让h的低16也具有较好散列性,这样高低16 位均参与计算,较少hash冲突
寻址
hash算法得到的值与数组长度-1 进行按位与计算,得到其存放的数组下标
11、使用 HashMap 会有哪些问题?(1.7 和 1.8 中有什么不同)如何解决?
内存泄露
当key 类没有实现hashcode 和eques 方法时,会产生很多重复的对象,最终导致内存溢出。
多线程下使用HashMap 线程不安全
1.7
hashMap 在扩容的时候链表可能会成环,因为是链表数据插入采用的头插法,扩容会导致链表顺序丢失,而1.8 的链表采用的尾插法
12、如何构建线程安全的List
Collections.synchronizedList() 推荐
new vector()
new CopyOnWriteArrayList() 因为CopyOnWriteArrayList在插入的过程中会创建新的数组
350

被折叠的 条评论
为什么被折叠?



