安卓面试总结(2)——Java 集合框架

上一篇

上安卓面试总结(1)——Java基础

上一篇写到了 Java 的一些基础知识,这篇将以面试的态度去回顾一些 Java 的集合框架,博文的目的不是想让看的人立马能学会多少东西(更多可以看这篇 Java 集合笔记),更多的是希望能够借精简的内容,去回忆,去发散,加深对知识的理解,或许再看一次,还会生出原来如此的想法,这是很美妙的。

1、概述

安卓Java设置样式_android

Java 容器示意图

容器
  • Collection:储存对象集合
  • Map:储存键值对(两个对象)的映射表(Entry)
Set
  • TreeSet:基于红黑树,支持有序新操作,基于范围查找元素(SortedSet)
  • HashSet:基于哈希表实现,支持快速查找,但不支持有序操作
  • LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的顺序
List
  • ArrayList:基于动态数组实现,支持随机访问(RandomAccess)
  • Vectoy:和 ArrayList 类似,当是线性安全的
  • LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在键表中间插入和删除元素
Queue
  • LinkedList:可以用它实现双向队列(Deque)
  • PriorityQueue:基于堆结构实现,可以用它实现优先队列
Map
  • TreeMap:基于红黑树实现(SortedMap)
  • HashMap:基于哈希表实现
  • HashTable:和 HashMap 类似,但是线程安全的,可用 ConcurrentHashMap
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序,或者是最近最少使用顺序(LRU)

2、迭代器

Collection 继承了 Iterable 接口

Iterable --> Iterator:通过 iterator() 方法实现

Iterable 方法:

  • next()
  • hasNext()
  • remove():删除上次 next 方法是返回的元素(必须配合使用)
  • dafault forEachRemaining(Consumer)

Iterator 的位置实际是在两个元素中间,remove 的是前一个,next 的是后一个

如何遍历数组?

3、视图与包装器

视图:好像创建了一个新集,将内容填进去,并返回这个集(keySet)

  • Arrays.asList(xxx):包装器
  • 子范围
  • 不可修改视图
  • 同步视图:synchronizedMap()
  • 受查视图

4、源码分析

① ArrayList
  • RandAccess,仅标记随机访问,删除代价高
  • 扩容:默认10,不够扩一半(1.5倍),需要复制数组
  • modCount 记录结构变化,迭代、序列化时检查 --> ConcurrentModificationException
  • transient:标记数组,只对数组内有的值进行序列化(手动操作)
② Vector
  • 内部使用了 synchronized 进行同步,也有 modCount,扩容是 2 倍
  • 代替:
  • 用 Collections.synchronizedList<>() 得到线程安全的 ArrayList
  • 使用 CopyOnWriteArrayList
③ CopyOnWriteArrayList
  • 读:在原始数组进行、
  • 写:在一个复制数组进行,有加锁,写完代替原有数组(指针)
  • 优点:大大提高了读操作的效率
  • 缺点:内存加倍,数据不一致,没实时性
④ LinkedList
  • 双向链表实现,不支持随机访问,无需扩容
  • 在任意位置添加删除元素更快
⑤ HashMap
  • 内部包含了一个 Entry 类型的数组 Entry<K,V>[] table,Entry 是一个链表
  • 数组每个位置被当作一个同,一个同放一个链表
  • 拉链法:同一个链表中存放哈希值和散列同取模运算结果相同的 Entry(头插法)
  • HashMao 允许 null 键值对,以第 0 桶存放
  • put 方法
  • 判断当前数组是否需要初始化,需要则对 table 初始化。
  • 如果 key 为空,则 put 一个空值进去。
  • 根据 key 计算出 hashcode,并由 hashcode 定位出所在桶
  • 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖。
  • 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  • 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置。
  • 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树
  • 当调用 addEntry 写入 Entry 时需要判断是否需要扩容
  • get 方法
  • 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
  • 如果桶为空则直接返回 null 。
  • 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。红黑树就按照树的查找方式返回值,不然就按照链表的方式遍历匹配返回值。
  • 扩容:
  • capacity:默认大小16,必须保证为 2 的 n 次方(为什么?求模运算用了 & 优化,index = (n - 1) & hash)
  • threshold = size * loadFactor:装载因子(能够使用的百分比)* 所有元素的数量
  • 扩容后要重新调整数据在桶的位置,桶的数量变了 --> index 也变了 --> 重新拉链法
  • JDK1.8 开始,一个桶的链表长度大于 8 时会将链表为红黑树
  • HashTable
  • 使用 synchronized 来进行同步,不能插入 null 键
  • 迭代器是 fail-fast 迭代器(modCount)
⑥ ConcurrentHashMap
  • 分段锁(Segment):采用分段锁,每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度为锁的个数)
  • Segment 继承 ReentrantLock,默认 16 个,每个 segment 维护一个 count 统计下面个数。size 操作会计算所以 count 和,先不加锁,不行再锁(重试过多)。
  • JDK1.8 抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性 。
下一篇

安卓面试总结(3)——Java 多线程I