1. 项目概述:一份持续更新的Java学习实战笔记
最近在系统性地重温Java,起因是团队里来了几位新人,在带他们上手项目时,发现很多基础概念,比如集合框架里ArrayList和LinkedList的区别、HashMap的扩容机制,大家说起来都头头是道,但一碰到实际场景,比如高并发下的集合线程安全问题、如何设计一个高效的缓存结构,就有点抓瞎。这让我意识到,光看教程、背八股文是远远不够的,必须有一套结合了原理、实战和避坑指南的笔记。于是,我决定以黑马程序员的Java课程为蓝本,结合我过去十多年踩过的坑和项目经验,重新整理一份学习笔记。这份笔记不是对课程内容的简单复述,而是聚焦于“为什么”和“怎么用”,目标是打造一份能直接用于面试复盘和日常开发的实战手册。目前笔记已经更新到了集合框架这一核心部分,这也是面试中“八股文”的重灾区,但更是日常开发效率的基石。无论你是正在入门Java的新手,还是想巩固基础、应对技术面试的开发者,这份笔记里梳理的思路、验证的代码和总结的“坑点”,或许都能给你带来一些不一样的视角。
2. 学习路径设计与核心方法论
2.1 为什么选择“课程+笔记+实战”三位一体模式
单纯观看视频课程很容易陷入被动接收信息的陷阱,感觉听懂了,关上视频却写不出代码。我选择黑马课程作为主线,是因为它的知识结构比较系统,从环境搭建到高级特性,路径清晰。但我的学习方法核心是“笔记驱动”和“问题驱动”。笔记驱动意味着我不是抄PPT,而是每学完一个知识点,立刻用我自己的语言,模拟“给同事讲解”的场景,把概念重新组织并记录下来,重点标注出容易混淆的地方(比如==和equals)。问题驱动则是在学习每个章节时,主动去思考并尝试回答一些高频面试题或实际开发问题,例如学到集合时,我就会问自己:“Arrays.asList()得到的List为什么不能增删?”“HashMap在多线程下为什么会引起死循环?”
这种模式的好处是,笔记最终会成为你个人知识体系的索引。当你在工作中遇到“遍历集合时发生ConcurrentModificationException”的报错时,你能立刻想到笔记中“fail-fast机制”那部分,并找到原因和解决方案。这份笔记的更新过程,其实就是我个人知识库的构建过程。
2.2 环境搭建:不仅仅是配置PATH
很多教程把环境配置讲得很简单,就是下载JDK、设置JAVA_HOME、在Path里添加bin目录。这没错,但对于想深入理解Java生态的开发者来说,仅仅这样还不够。在我的笔记里,环境准备是一个独立章节,我着重强调了以下几点:
- JDK版本选择与隔离:不建议无脑安装最新版。很多老项目可能还在用JDK 8或11。我会建议使用
jEnv、SDKMAN!这类工具进行多版本管理,这在实际企业开发中非常常见。笔记里会记录如何安装和切换不同JDK版本。 - IDE的选择与优化:IntelliJ IDEA是主流,但笔记里不会只教怎么点按钮。我会记录一些提升效率的关键配置,例如:
- 如何调整JVM参数(
-Xms,-Xmx)来避免IDEA本身或运行大型项目时的“Java: OutOfMemoryError: Insufficient memory”错误。 - 如何配置项目的语言级别和SDK,解决“
错误: 不支持发行版本 5”这类问题。 - 推荐安装的关键插件(如
Key Promoter X用于熟悉快捷键,Rainbow Brackets提升代码阅读体验)。
- 如何调整JVM参数(
- 构建工具初接触:即使课程前期不讲Maven/Gradle,我也会在环境篇简单引入Maven的概念,并演示如何创建一个简单的Maven项目,解释
pom.xml的作用。这能让你提前适应企业项目的标准结构,而不是永远用着IDE创建的“简单Java项目”。
注意:环境配置的一次性成功很重要,但更重要的是理解每个配置项的意义。比如
JAVA_HOME指向的是JDK的根目录,而不是bin目录,这是因为很多工具(如Maven、Tomcat)会依赖这个变量来查找完整的Java开发套件,包括tools.jar等。
3. 攻克核心:从数组到集合框架的思维跃迁
3.1 数组的局限性:为什么我们需要集合
课程通常从数组讲起,这是正确的。数组是基础,但它有明显的短板:长度固定。在笔记中,我通过一个简单的场景来凸显这种不便:写一个方法,读取用户输入的一串不定数量的整数。用数组实现,你需要先声明一个“足够大”的数组,或者使用繁琐的扩容拷贝逻辑。这自然引出了ArrayList——一个可以动态扩容的“智能数组”。
但这里我着重对比了性能开销:ArrayList的扩容(通常是1.5倍)涉及数组拷贝,这是一个O(n)操作。我会在笔记里写一段测试代码,分别向一个初始容量为10的ArrayList和每次扩容2倍的ArrayList(自定义实现)添加100万元素,对比耗时,直观感受不当初始容量带来的性能损耗。结论是:如果能预估数据量,尽量使用带初始容量的构造函数new ArrayList<>(initialCapacity)。
3.2 Collection与Map:两大阵营的清晰划分
集合框架的学习最忌混淆。我的笔记用一棵清晰的“思维树”来划分:
Collection(单列集合):存放一个个独立的对象。List(有序、可重复):ArrayList(数组,查询快),LinkedList(链表,增删快),Vector(线程安全但古老)。Set(无序、唯一):HashSet(基于HashMap,最快),LinkedHashSet(维护插入顺序),TreeSet(有序,基于红黑树)。
Map(双列集合):存放键值对(Key-Value)。HashMap(最常用,基于哈希表),LinkedHashMap(维护插入或访问顺序),TreeMap(基于Key排序),Hashtable(线程安全但古老)。
我会强调,Collection和Map是平级的接口,没有继承关系。Arrays.asList()返回的List和new ArrayList<>()的区别,就在这里作为一个“坑点”详细讲解:前者返回的是Arrays内部类,固定大小,不支持结构性修改(增删)。
3.3 深入ArrayList源码:动态扩容的奥秘
看源码不是目的,理解设计思想才是。对于ArrayList,我笔记的核心是grow方法。我会带着问题看源码:
- 何时扩容?
add元素时,发现size + 1 > elementData.length。 - 扩容多少?新容量 = 旧容量 + (旧容量 >> 1),即1.5倍。但会检查是否超过最大数组大小限制。
- 如何扩容?调用
Arrays.copyOf,底层是System.arraycopy这个本地方法,效率较高但仍有成本。
我会在笔记里附上关键源码片段和自己的注释,并总结最佳实践:
- 在构造
ArrayList时指定初始容量,避免多次扩容。 - 慎用
ArrayList存储大量数据并频繁在中间位置插入/删除,此时LinkedList可能更优。
3.4 征服HashMap:面试必考与性能关键
HashMap是集合框架的重中之重。我的笔记从使用深入到原理,再回到使用。
3.4.1 核心结构:数组+链表/红黑树我会画一个简化的结构图(用文字描述):一个Node<K,V>[]数组,每个位置称为一个“桶”(bucket)。通过Key的hashCode()计算哈希值,再经过扰动函数(高16位异或低16位)和(n-1) & hash得到数组下标。如果多个Key的哈希值冲突,它们会以链表形式存放在同一个桶里。当链表长度超过8(且数组总长度>=64),链表会转化为红黑树,以提升查询效率(从O(n)到O(log n))。
3.4.2 扩容机制:为什么容量是2的幂这是高频面试点。容量为2的幂(如16,32)时,(n-1) & hash这个操作等价于hash % n,但位运算的效率远高于取模。扩容时(默认负载因子0.75,即元素数量达到容量*0.75时触发),容量变为2倍,原有元素会重新计算位置。因为n变成了2倍,n-1的二进制高位多了一个1,元素的新位置要么是原位置,要么是“原位置+旧容量”。这个设计非常巧妙,避免了重新计算每个Key的哈希值,只需判断(e.hash & oldCap) == 0即可。
3.4.3 线程安全问题与替代方案HashMap非线程安全。我会在笔记中模拟一个多线程put导致死循环的经典场景(在JDK 1.7及之前,头插法扩容可能导致链表成环)。解决方案:
Hashtable:全表锁,性能差,不推荐。Collections.synchronizedMap(new HashMap()):包装器模式,性能一般。ConcurrentHashMap:推荐方案。JDK 1.7采用分段锁,JDK 1.8改为synchronized锁桶头节点+CAS,并发度更高。笔记会强调它在高并发场景下的首选地位。
4. 迭代、比较与工具类:集合操作的瑞士军刀
4.1 遍历集合:多种方式与性能考量
遍历是集合最常用的操作。笔记会对比几种方式:
- for循环(带索引):仅适用于
List,效率高。 - 增强for循环(for-each):语法简洁,底层是迭代器。但要警惕:在遍历过程中直接调用集合的
remove方法会触发ConcurrentModificationException。 - 迭代器(
Iterator):最标准的方式,可以在遍历时安全地使用迭代器自身的remove方法删除元素。 forEach方法(Java 8+):配合Lambda表达式,代码更简洁。
我会写一个性能测试,对比遍历100万元素的ArrayList和LinkedList,用不同方式的时间消耗,直观展示“LinkedList用索引遍历是灾难”这一结论。
4.2 对象比较与排序:Comparable与Comparator
集合排序(如Collections.sort()或TreeSet)依赖对象间的比较。这是初学者易混点。
Comparable(内部比较器):让对象类实现Comparable<T>接口,重写compareTo(T o)方法。这定义了对象的“自然顺序”。比如String、Integer都实现了这个接口。Comparator(外部比较器):创建一个单独的类实现Comparator<T>接口,重写compare(T o1, T o2)方法。这种方式更灵活,可以在不修改原有类的情况下定义多种排序规则。
笔记会提供一个典型场景:一个Student类,默认按学号排序(实现Comparable),但有时需要按成绩排序。这时就可以创建一个Comparator<Student>的实现类,传给sort方法。Java 8之后,使用Lambda表达式可以更简洁地创建Comparator。
4.3 实用工具类:Collections与Arrays
这两个类提供了大量静态方法,是操作集合和数组的利器。笔记不会罗列所有方法,而是聚焦最常用的:
Collections:sort(list),shuffle(list):排序、洗牌。synchronizedXxx():创建线程安全的集合包装(性能有损耗,如前所述)。unmodifiableXxx():创建不可变集合视图,用于防御性编程。binarySearch():在已排序的List中进行二分查找。
Arrays:asList(T... a):再次强调其返回的是固定大小的List。sort(),binarySearch():对数组排序和查找。toString(),deepToString():方便打印数组内容。
5. 典型应用场景与避坑指南实录
5.1 场景一:实现一个简单的本地缓存
使用LinkedHashMap可以轻松实现一个FIFO或LRU缓存。笔记会展示如何通过继承LinkedHashMap并重写removeEldestEntry方法来实现一个简单的LRU缓存。这会综合运用到Map、泛型、访问顺序等知识。
public class SimpleLRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxCapacity; public SimpleLRUCache(int maxCapacity) { // 设置accessOrder为true,按访问顺序排序 super(maxCapacity, 0.75f, true); this.maxCapacity = maxCapacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { // 当元素数量超过最大容量时,移除最老的条目(最近最少访问) return size() > maxCapacity; } }5.2 场景二:数据去重与统计
给定一个字符串列表,需要找出不重复的字符串及其出现次数。这是HashMap的经典用例。
List<String> list = Arrays.asList("apple", "banana", "apple", "orange", "banana", "apple"); Map<String, Integer> countMap = new HashMap<>(); for (String fruit : list) { // getOrDefault是Java 8的实用方法,避免空指针判断 countMap.put(fruit, countMap.getOrDefault(fruit, 0) + 1); } System.out.println(countMap); // 输出:{orange=1, banana=2, apple=3}5.3 高频“坑点”排查与解决
ConcurrentModificationException:- 现象:在遍历集合(增强for循环或迭代器)时,直接调用集合自身的
add或remove方法。 - 原因:集合的
modCount(修改次数)与迭代器预期的expectedModCount不一致,触发fail-fast机制。 - 解决:使用迭代器的
remove方法;或使用CopyOnWriteArrayList(写时复制,适合读多写少);或在遍历前复制一份新集合。
- 现象:在遍历集合(增强for循环或迭代器)时,直接调用集合自身的
Arrays.asList()的陷阱:- 现象:对
Arrays.asList()返回的List进行add或remove操作,抛出UnsupportedOperationException。 - 原因:返回的是
Arrays$ArrayList,一个固定大小的列表包装器。 - 解决:如果需要可变列表,使用
new ArrayList<>(Arrays.asList(...))。
- 现象:对
HashMap在JDK 1.7下的死循环(面试常问):- 现象:多线程并发
put触发扩容时,可能导致CPU占用100%。 - 原因:JDK 1.7采用头插法转移链表节点,并发下可能形成环形链表,导致
get操作无限循环。 - 解决:升级到JDK 1.8(改为尾插法);或使用
ConcurrentHashMap。
- 现象:多线程并发
集合与泛型擦除:
- 现象:运行时无法获取泛型的具体类型。
- 示例:
List<Integer>在运行时是List,Integer信息被擦除。 - 影响:无法在运行时直接通过反射创建泛型数组(如
T[] array = new T[size]是不允许的),通常需要传入Class<T>类型令牌或使用List代替数组。
6. 性能优化与选型建议
6.1 集合选型决策树
面对具体场景,如何选择集合?我总结了一个简单的决策流程:
- 需要键值对吗?
- 是 -> 用
Map。- 需要排序吗? ->
TreeMap(按Key排序)或LinkedHashMap(按插入/访问顺序)。 - 不需要排序,追求最快速度 ->
HashMap。 - 需要线程安全 ->
ConcurrentHashMap。
- 需要排序吗? ->
- 否 -> 用
Collection。
- 是 -> 用
- 元素允许重复吗?
- 允许 -> 用
List。- 查询多,增删少 ->
ArrayList。 - 增删(尤其在头部/中间)多,查询少 ->
LinkedList。 - 需要线程安全 ->
CopyOnWriteArrayList(读多写少)或Collections.synchronizedList。
- 查询多,增删少 ->
- 不允许 -> 用
Set。- 需要排序 ->
TreeSet。 - 需要保持插入顺序 ->
LinkedHashSet。 - 只需要去重,追求最快速度 ->
HashSet。
- 需要排序 ->
- 允许 -> 用
6.2 初始化容量与负载因子调优
对于ArrayList、HashMap、HashSet等基于数组的集合,初始化容量至关重要。
ArrayList:如果能预估最终大小,直接指定初始容量,避免多次扩容和数据拷贝。HashMap:如果你知道大概要存放1000个元素,默认负载因子0.75,那么可以设置初始容量为(1000 / 0.75) + 1 ≈ 1334,然后取最近的2的幂,即2048。这样可以在放入1000个元素的过程中避免扩容。使用构造函数new HashMap<>(2048)。注意:负载因子(Load Factor)是权衡时间与空间的参数。降低负载因子(如0.5)可以减少哈希冲突,提高查询速度,但会占用更多内存,增加扩容频率。通常使用默认值0.75即可,除非有非常极致的性能要求。
6.3 遍历操作的选择
ArrayList:三种遍历方式(索引for、迭代器、for-each)性能差异不大,for-each最简洁。LinkedList:绝对不要使用索引for循环!它的get(int index)是O(n)操作。应使用迭代器或for-each。HashMap:遍历EntrySet比遍历KeySet再get(value)效率更高,因为后者需要两次查找。
7. 向高阶迈进:Java 8+的流与函数式编程对集合的影响
虽然课程可能还未涉及,但作为一份面向实战的笔记,有必要提前引入Java 8带来的革命性变化——Stream API。它极大地改变了我们操作集合的方式。
例如,上面统计词频的例子,用Stream可以一行搞定:
Map<String, Long> countMap = list.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));再比如,过滤、映射、排序、归约等操作,用Stream链式调用,代码更声明式、更易读。笔记会对比传统循环和Stream操作的写法,并指出Stream的惰性求值、并行流等特性,为后续学习打下伏笔。理解集合是熟练使用Stream的基础,而Stream则是现代化、高效率处理集合数据的利器。
整理这份笔记的过程,也是我自己将零散知识系统化、将理论认知实践化的过程。集合框架就像Java世界的容器工具箱,每种容器都有其特定的用途和性能特征。死记硬背面试题答案或许能通过一轮面试,但只有在项目中真正思考过“为什么这里用ArrayList而不用LinkedList”、“这个HashMap的容量该设多大”,这些知识才会真正内化成为你的开发能力。笔记更新到集合篇,算是完成了一个重要里程碑,后续面向对象、IO、多线程等章节,我也会继续用这种“刨根问底+实战验证”的方式记录下去。