腾讯系列聊完(15篇),进入阿里巴巴面经。阿里面试跟腾讯差异大:腾讯考广度(8方向快速切换),阿里考深度(一个方向往死里挖)。P5初级岗就会问HashMap源码级原理、ArrayList扩容实现、线程安全集合底层区别。
阿里面试还有个特点:算法难度高,P5也出LeetCode Medium。算法后面有专题篇,今天8道题专注Java集合核心考点。
Q1:ArrayList的扩容机制?源码怎么实现的?
ArrayList内部用Object数组存储。默认初始容量10(第一次add时创建),满了就扩容。
扩容过程(grow方法):
- 计算新容量:oldCapacity + (oldCapacity >> 1)——即1.5倍
- 创建新数组:Arrays.copyOf(elementData, newCapacity)
- 把旧数组元素复制到新数组
为什么1.5倍:太大会浪费内存,太小会频繁扩容(每次扩容都是O(n)拷贝)。1.5倍是空间和时间的折中。
优化:如果预知数据量,构造时传初始容量new ArrayList<>(100)避免多次扩容。addAll时如果一次性加的元素多,ArrayList会一次性扩到够大而不是分多次扩。
追问:ArrayList的subList是什么?返回的是原List的视图(内部持有原List引用),对subList的修改会影响原List。但原List结构变化(add/remove)后subList操作会抛ConcurrentModificationException。
Q2:LinkedList和ArrayList区别?各自场景?
ArrayList:连续内存(数组),随机访问O(1),插入删除O(n)。LinkedList:双向链表,随机访问O(n),头尾插入删除O(1)。
选择:99%场景用ArrayList。CPU缓存对连续内存友好(局部性原理),ArrayList遍历比LinkedList快很多。
追问:LinkedList能当队列和栈吗?能。但ArrayDeque性能更好——底层循环数组无链表节点分配开销。
Q3:HashMap的底层原理?JDK 1.8做了什么优化?
底层结构:数组+链表+红黑树(1.8后)。每个数组位置叫bucket,hash冲突的元素用链表存储。链表长度超过8(且数组长度>=64)时转红黑树,查找从O(n)降到O(logn)。
put过程:
- key的hashCode经过扰动函数(高低16位异或,减少hash冲突)
- (n-1) & hash 算出bucket位置(n是数组长度,必须是2的幂)
- bucket为空直接放入,不为空遍历链表/树
- key相同则覆盖value,不同则链表尾插(1.8改尾插,1.7头插会死循环)
扩容:元素数 > capacity * loadFactor(0.75) 时扩容。新容量是2倍,每个元素重新hash到新位置。1.8优化:用hash & oldCap判断元素在原位还是原位+oldCap,避免重新算hash。
Q4:HashMap为什么线程不安全?ConcurrentHashMap怎么解决?
线程不安全场景:两个线程同时put,扩容时可能一个线程在搬数据另一个也在搬,导致数据丢失。1.7版本头插法还会造成链表成环(死循环),1.8改尾插法解决了死循环但数据还是可能丢。
ConcurrentHashMap 1.8实现:
- 抛弃1.7的Segment分段锁设计
- 用CAS+synchronized锁单个bucket
- put时先CAS尝试写入空bucket,不为空则synchronized锁住该bucket的头节点操作链表/树
- 锁粒度从Segment(多个bucket)降到单个bucket,并发度大幅提升
追问:ConcurrentHashMap的size()怎么统计?不锁全表——每个bucket维护一个count,size()时用baseCount + 所有CounterCell的值求和。CounterCell用CAS更新避免竞争,高并发时分散到多个CounterCell减少竞争(类似LongAdder思路)。
Q5:HashMap的key可以是可变对象吗?
技术上可以,实际不应该。HashMap用key的hashCode确定bucket位置。如果put之后修改了key导致hashCode变化,get时用新hashCode去找bucket位置找不到——key就"丢"了。
最佳实践:HashMap的key用不可变对象(String、Integer、枚举)。如果必须用自定义对象做key,override hashCode和equals且确保hashCode在对象生命周期内不变。
追问:equals和hashCode的关系?a.equals(b)为true则a.hashCode() == b.hashCode()必须为true。反过来不要求——hashCode相同equals可以不同(hash冲突)。HashMap先比hashCode再比equals,所以两者必须一致。
Q6:HashSet底层怎么实现的?跟HashMap什么关系?
HashSet内部就是一个HashMap。HashSet.add(e)等价于map.put(e, PRESENT)(PRESENT是一个固定的Object常量)。Set的元素就是Map的key,value统一用PRESENT占位。
所以HashSet的特性继承HashMap:O(1)查找、不保证顺序、允许null、线程不安全。
TreeSet:内部用TreeMap(红黑树),元素有序(自然排序或Comparator),查找O(logn)。需要排序集合用TreeSet,不需要顺序用HashSet。
LinkedHashSet:内部用LinkedHashMap,维护插入顺序(双向链表串联entry)。需要保持插入顺序时用。
Q7:线程安全集合有哪些?各自场景?
Vector:ArrayList线程安全版(synchronized),性能差没人用。Collections.synchronizedList:包装一层锁,读也锁性能差。
CopyOnWriteArrayList:写时复制数组,读不加锁。读多写少场景(监听器列表)性能好,写多不能用。
ConcurrentLinkedQueue:无锁队列(CAS),高并发入队出队。BlockingQueue:ArrayBlockingQueue/LinkedBlockingQueue/PriorityBlockingQueue,生产者-消费者模式用。
Q8:HashMap的hash函数为什么这么设计?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扰动函数:hashCode的高16位和低16位异或。目的:让高位也参与bucket位置计算。
为什么需要:数组长度一般不大(16/32/64),(n-1) & hash只用到低位。如果hashCode高位不同但低位相同就冲突了。扰动后高位信息混入低位,减少冲突。
追问:为什么数组长度必须是2的幂?因为(n-1) & hash这个运算只在n是2的幂时等价于hash % n。用位运算代替取模性能快几倍。如果n不是2的幂,(n-1) & hash的结果分布不均匀(某些bucket永远不会被访问到)。
面试Tips:阿里P5对Java基础要求源码级深度。HashMap从put到扩容到红黑树转换要能讲完整流程。ArrayList扩容的1.5倍策略和具体实现要看过源码。ConcurrentHashMap 1.8的CAS+synchronized设计是区分"背过"和"看过"的关键题。集合框架建议读一遍JDK源码的ArrayList/HashMap/ConcurrentHashMap类——总共就几千行。
下一篇是阿里P6中级Android面经,难度跳一档。设计模式实战、ARouter路由原理、ButterKnife和EventBus源码——P6考的是框架理解深度。
面试被问过HashMap源码的同学评论区报到,面试官追问到了什么深度?
本系列连载中,关注不迷路,下一篇:阿里P6中级Android工程师面试真题
系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。