Java基础(中)核心知识:集合、异常、多线程与泛型实战指南 1. 为什么说“基础(中)”才是Java最关键的承重墙很多人学Java有个错觉语法看完了、Hello World跑通了、循环判断也会写了就觉得自己“基础已经打完了”。然后直接冲进Spring Boot跟着教程抄了一堆代码项目也能跑但一遇到报错就懵、一涉及性能就抓瞎、面试一问就露馅。问题不是出在后面的框架而是出在基础序列里最容易被跳过的那一段——也就是标题里说的“JAVA基础(中)”。这一段的定位很明确它处在纯语法和框架之间的过渡地带核心知识包括面向对象思想的实战化运用、集合框架、异常处理、泛型、I/O、多线程入门。说白了语法是单词和句型这一段才是真正的“语法组织能力”。它能把你从“会写代码”变成“能写出像样的代码”顺便把你从“只能看懂Demo”提升到“能看懂别人项目里为什么这么写”。聊点实际的。我见过不少刚入行的同事ArrayList用得飞起但你问他什么时候该用LinkedList他愣一下HashMap天天用但你问他哈希冲突是什么、为什么重写equals就得重写hashCode他开始翻手机异常try-catch包了一整层却不知道checked和unchecked在设计上有什么本质区别。这些问题全部集中在“基础(中)”你要是带着这些问题去学框架学到的全是表面功夫换个场景立刻失效。所以这篇内容不打算像教科书那样罗列知识点我把自己在实际开发和带新人过程中反复遇到的、最值得花时间的部分拆开讲清楚每一块都会说明“为什么需要它”和“实际工程里怎么用”最后再给出一套实在的自学路线和排坑经验。不管你是刚学完Java语法的大学生还是转行入门不久、想补地基的开发者这段内容都能帮你把知识串成体系而不是散落一地的碎片。2. 这段路到底要啃什么八大核心知识地图2.1 包装类型与内存模型性能问题的第一现场先聊一个很多人压根没当回事的东西——包装类型。int和Integer平时写代码几乎无感但这里藏着两个非常实际的问题自动装箱/拆箱和缓存范围。自动装箱是Java编译器帮你做的它在底层调用了Integer.valueOf()拆箱则是intValue()。代价是什么每次装箱拆箱都是一次对象创建和方法调用在循环里累积起来非常可观。我之前帮一个同事排查接口慢的问题定位到一段代码里用Long做了百万次累加每次循环都在装箱拆箱改成基本类型long之后耗时直接降了一个数量级。这种问题不复杂但你看不到“对象创建”这个隐形成本就很容易忽视。包装类型的另一个坑是缓存。Integer默认缓存了-128到127之间的对象意味着这个范围内的两个Integer用比较会相等超出范围就不相等。很多人用比较包装类型线上偶发bug就是这么来的。规则很简单包装类型之间比数值一律用equals或者拆成基本类型再比。这个习惯养成之后能帮你省掉无数个深夜排查的头发。内存模型这块我建议趁这个阶段把“栈管运行、堆管存储”这个基本认知立起来。局部变量在栈帧里new出来的对象在堆里方法之间的引用传递传的是地址值。理解了这个后续学JVM调优、学线程安全才有个能挂靠的底子。2.2 集合框架90%的业务代码都和它打交道集合是Java日常开发里出场率最高的部分没有之一。但很多人对它的理解停留在“会用”我建议至少把下面几个决策点想透。ArrayList和LinkedList怎么选ArrayList底层是数组随机访问O(1)尾部插入快LinkedList底层是双向链表头部插入删除快但随机访问是O(n)。实际业务里90%以上的场景用ArrayList就够了LinkedList真正发挥优势的场景其实很窄。HashMap的实现原理数组加链表加红黑树默认容量16、负载因子0.75扩容时容量翻倍。你要搞清楚的是hash寻址、哈希冲突时怎么链化、为什么链表长度超过8且数组容量达到64时会转红黑树。这些东西面试常问但更重要的是它能帮你理解“为什么HashMap的key最好选不可变对象比如String和Integer”。TreeMap和LinkedHashMapTreeMap按key排序底层红黑树LinkedHashMap维护插入顺序可以用作LRU缓存的雏形。选型时别只看“能用”要看“这个集合的语义是否符合我的业务场景”。HashMap有一个高频大坑并发修改。它本身线程不安全多个线程同时put可能造成数据覆盖JDK8之后虽然不会像老版本那样扩容时形成环形链表导致死循环但数据丢失和size不准确这类问题依然存在。并发场景老老实实用ConcurrentHashMap它的分段锁设计JDK8是CAS加synchronized锁桶保证了读多写少场景下的高性能。再补一个容易被忽略的细节HashSet底层其实就是一个HashMap只是只用key不用value。理解了这个很多“为什么”就自动通了。2.3 异常体系把“出错”变成一种设计异常这块新手最容易走两个极端要么全部catch吞掉要么完全不处理直接throws。两者都是灾难。Java的异常分两大类受检异常checked和非受检异常unchecked。受检异常强制你处理比如IOException非受检异常是RuntimeException的子类比如NullPointerException、IllegalArgumentException编译器不强制处理。设计原则是什么如果你的代码调用方可以通过合理操作避免这个异常就做成非受检如果异常属于外部环境问题、调用方必须面对就做成受检。比如文件不存在属于外部环境问题用受检异常合理参数传错了属于调用方问题用非受检异常更合理。实际工程里我见过最可怕的操作是把一大段业务逻辑包在try-catch里catch住异常后只写了一行log.error然后继续往下走。这种操作等于把一个错误活生生藏起来等到数据对不上账的时候日志里只有一条孤零零的报错根本还原不出上下文。正确的做法是catch住异常后要么处理它——重试、降级、给用户一个明确提示要么往上抛让上层统一处理绝不悄无声息吞掉。自定义异常也是这个阶段该掌握的。不要什么错误都抛Exception业务异常单独建一个BizException通过错误码区分类型前端可以据此提示后端可以据此排查。这个习惯越早养成项目越大的时候越受益。2.4 泛型编译期的“强制约定”泛型看起来只是尖括号里加个字母但它解决的问题很实际让代码在编译期就发现类型错误而不是等到运行期才抛ClassCastException。比如List 和ListInteger用起来是两回事但如果不加泛型List里什么都能塞取出来的时候类型转换就出问题。泛型把这个风险前移到编译期编译器帮你盯着。泛型有个经典难点是通配符和PECS原则。PECS即“Producer Extends, Consumer Super”——如果你要从集合里读取数据生产者用? extends如果你要往集合里写入数据消费者用? super。记不住也没关系写代码的时候只要出现编译报错往这两个方向调整就能解决问题。关键是理解为什么有这个限制——Java的泛型是不变的List 不是List还有一点容易被忽略泛型在运行期会被擦除。这意味着你不能用instanceof去判断一个泛型的具体类型也不能直接new T()。写法上要绕一下比如传入Class 类型参数。这个知识点比较细节但面试和实际写框架代码时都会遇到。2.5 I/O与NIO从阻塞到非阻塞的思维跳跃I/O这块很多初学者觉得会读个文件写个文件就够了但我建议至少把模型看清楚。传统的阻塞I/O里一个线程处理一个连接读数据时线程在那干等着连接多了线程数量就爆炸。NIO引入了Channel、Buffer、Selector这三个核心组件一个线程可以用Selector管理多个Channel只有Channel真正可读可写时才去处理这就是非阻塞的核心思路。理解NIO的价值不是为了让你立刻去写一个网络框架而是为了后续看Netty源码、理解Redis单线程模型为什么能扛住高并发打下基础。你至少要知道Selector.select()是干什么的、Buffer的flip()和compact()为什么这样设计以及为什么NIO代码写起来比传统I/O啰嗦得多——因为它把控制权交给你了。项目里如果只是普通的需求比如读配置文件、上传下载文件直接用Java NIO的Files、Paths那套就够别硬上Netty。技术选型讲究匹配度不是为了炫技。但是作为基础学习NIO的思维模型必须建立。2.6 多线程基础并发入门的第一道坎多线程是“基础(中)”里最硬核的一部分也是很多人的分水岭。这里的入门要求其实不高但要精炼。先别急着背各种锁的八股文。先理解线程是什么、为什么需要多线程、并发和并行的区别。然后掌握创建线程的两种主流方式继承Thread类和实现Runnable接口。我个人推荐实现Runnable因为Java是单继承继承了Thread就不能继承别的了而且Runnable把任务和线程本身解耦语义更清晰。至于Callable和FutureTask它们能返回结果适合需要拿到线程计算结果的场景。线程安全的核心是“可见性、原子性、有序性”这三个问题。synchronized可以同时解决这三者——加锁保证原子性加锁内存屏障保证可见性锁的互斥特性保证有序性。volatile只保证可见性和有序性不保证原子性所以“volatile变量做计数器”这种操作是错的。记不住原理的时候记住这个结论就够了多线程共享变量做读写要么加锁要么用原子类。线程池也是这个阶段必须接触的。别自己new Thread裸奔有现成的ThreadPoolExecutor。核心参数要能说出来核心线程数、最大线程数、空闲存活时间、任务队列、拒绝策略。用的时候先估算任务的类型——CPU密集型还是I/O密集型前者核心线程数推荐CPU核数加一后者可以设大一些。拒绝策略默认的AbortPolicy会直接抛异常实际业务里更常用的是CallerRunsPolicy让提交任务的线程自己把任务跑了避免任务丢失。3. 实操路线与自我检测怎么知道自己真的会了3.1 用一个小型项目把所有知识点串起来我发现很多人学Java的方式是“看书-做题-下一章”知识点都是独立的学完集合忘了异常学完多线程忘了集合。要解决这个问题我建议做一个能把所有基础点串起来的小项目——图书管理系统就是一个很好的载体不需要Spring纯Java就能写。这个项目可以这样分层演化每一层对应不同的知识点第一版用数组或ArrayList存图书数据实现增删改查用异常处理输入校验。这一版练的是集合和异常。第二版把图书、读者、借阅记录分别设计成类用继承和接口抽象公共行为。这一版练的是面向对象设计。第三版用泛型写一个通用的仓储类Repository 避免每个实体都重复一套CRUD代码。这一版练泛型。第四版借阅时用多线程模拟多个读者同时借书用synchronized或ReentrantLock保证库存不超卖用volatile标记借阅状态。这一版练并发。第五版把操作日志用NIO的Files写入本地文件体验一下缓冲流和字符集的细节。这一版练I/O。每一版做完你都要能回答一个问题“我为什么这样设计换了另一种写法会有什么问题”能答出来说明你真的会了。答不出来就回去翻书翻到能用自己的话说清楚为止。3.2 提高阶段的练习设计用自己的语言复述这里有个很笨但很有效的方法学完一个知识点之后合上书假装你在给一个没学过Java的人讲课把“这是什么、为什么存在、怎么用、有什么坑”讲一遍。讲不出来的部分就是你没掌握的部分。这个方法的好处是逼你把“模糊的感觉”变成“清晰的语言”。比如你能说出“HashMap为什么查询快因为它通过哈希函数直接定位桶的位置冲突了再用链表或红黑树解决”而不是“反正它快”这个知识点才算真的过手了。3.3 推荐的学习节奏与参考路径我个人的建议是正式学习基础语法之后给“基础(中)”分配四到六周时间。第一周主攻集合和泛型第二周主攻异常和I/O第三周主攻多线程入门第四周做综合项目串联。每天保持两小时的专注学习比周末突击十小时效果好得多。参考书不用贪多一本经典的Java入门书加一本源码解析的补充读物就够了。但注意书是用来查的不是用来从头背到尾的。遇到一个知识点先去IDE里写代码验证再去翻书找原理让代码跑起来的声音比书的厚度更让人信服。4. 新手最容易踩的7个坑与排查实战4.1 到处都是空指针先做防御再谈设计空指针是Java世界里出现频率最高的运行时异常没有之一。源头是调用了null对象的方法或属性。排查流程我建议固定下来看报错行号定位到具体变量往前追它的赋值路径找到它可能在哪个分支里变成了null。防御的手段有几个层次。第一层是做事前判断对象可能为null时先判空再使用。第二层是使用Objects.requireNonNull让null在入口处就暴露。第三层是用Optional表达“可能为空”的语义但要注意Optional不是万能药你用它包装一个本来就不可能为空的值纯粹是给自己添堵。原则是可能为null的才用Optional不可能为null的不要硬包。4.2 HashMap在多线程环境里直接“裸奔”这个坑我在4.2里已经提醒过这里再给一个具体排查案例。有一次线上系统偶发性出现某个用户数据丢失日志里没有任何异常。后来分析发现是一段缓存刷新逻辑里用了HashMap多个线程同时写入。因为扩容时桶位置重新分配两个线程同时写同一个桶后写覆盖了先写的数据。换成ConcurrentHashMap之后问题消失。排查这类问题有个技巧如果bug是偶发的、跟并发强相关的、没有稳定复现路径的优先怀疑共享的集合类。不要一上来就怀疑数据库很多时候问题就出在你以为“Java集合自己会处理线程安全”这个误解上。4.3 自动装箱导致的性能黑洞前面提过Long累加的例子。这里再补一个细节每次循环里发生装箱都会创建一个新的Long对象如果有100万次循环就是100万个对象的创建和回收。这个开销平时不明显但放到高频接口里就是性能瓶颈。排查时可以看火焰图或JFRJava Flight Recorder的分配采样会发现大量对象分配集中在某个循环。定位之后改成基本类型瓶颈立刻消失。这个问题用代码审查就能发现关键是脑子里要有“装箱拆箱有成本”这个弦。4.4 受检异常的“处理幻觉”很多人以为try-catch包住就算“处理”了这是最危险的误解。catch住IOException之后打印一行日志跟没处理没有本质区别。真正的处理方式是要么有恢复策略——比如重试、切换数据源、让用户重新上传要么向上抛出让上层知道——比如转换成一个业务异常带着上下文继续往外走。我写代码的习惯是catch住异常之后先问自己“我能不能做点什么让它恢复正常”能就写恢复逻辑不能就抛出去让该处理的人处理。这种思维习惯是区分“会用异常”和“会设计异常”的分界线。4.5 资源未关闭导致句柄泄漏用传统的FileInputStream、OutputStream时如果你忘记在finally里close操作系统层面的文件句柄就会一直占着。短时间不明显跑久了就会报“Too many open files”整台机器上的其他程序都会遭殃。JDK7之后的try-with-resources语法能自动关闭实现了AutoCloseable的资源这是标准写法不要再手动去写那些容易遗漏的finally close了。但有个细节多个资源一起用的时候要在一个try里依次声明关闭顺序会按声明的逆序自动执行。这个设计是合理的——你先开的外层资源后关后开的内层资源先关符合资源依赖的释放顺序。4.6 乐观锁失败时的重试策略用乐观锁做并发控制很常见比如更新库存时用版本号判断。但很多人只实现了“检测冲突”没有设计“冲突之后怎么办”。版本不一致直接返回失败用户体验很差无脑重试三次在高并发下可能把数据库打死。更合理的做法是冲突后先判断重试是否有意义比如库存被扣了一部分需要重新查询最新库存并重新计算这种重试有价值如果业务条件是“只能按这个价格买”而价格已经变了重试就没意义应该直接返回明确错误。重试次数要有限制且要有退避策略比如第一次等10毫秒第二次等50毫秒避免同时并发重试导致的惊群效应。4.7 重载与重写混淆导致意外调用重载是同一个类里方法名相同但参数列表不同重写是子类重新实现父类的方法。两者最直观的区别重载看编译期的引用类型重写看运行期的实际对象类型。如果你定义了一个父类引用指向子类对象调用一个父类里不存在但子类里重载了的方法编译器会直接报错或者调用的是父类的方法。这个知识点做题容易但实际工程里容易出问题的场景是泛型和重载组合的复杂情况。比如一个方法接收Object参数和一个方法接收String参数传null进去时编译器会选择最具体的String版本。这类边角规则不需要背但要知道编译器的“最具体匹配原则”存在遇到诡异的重载调用时往这个方向想。5. 给正在学这段内容的朋友几点实在建议我自己带新人时最常说的就是基础(中)这段内容学的时候“没什么成就感”因为不像做网页那样能立刻看到效果但它的价值全部体现在后续编程生涯的每一个细节里。你写的每一行业务代码、看的每一段框架源码、排查的每一个线上问题背后都是这段地基在托底。建议一一定动手写代码光看书等于白看。哪怕是把书上的示例代码自己手动敲一遍、改几个参数看看结果也比纯阅读强十倍。代码是肌肉记忆不是脑内活动。建议二遇到问题先自己查查不到再问。这不是让你死磕一整天而是至少要有“看报错信息、拆解问题、缩小范围”的排查过程。这个过程本身就是能力积累。建议三别急着冲框架。Spring Boot当然要学但只要你基础(中)这块儿还没落到实处——比如让你说说Java集合的体系结构、异常的处理原则、线程安全的三个特性你不能立刻组织出清晰的回答——框架学得再花哨也是空中楼阁。基础是一辈子的事什么时候回来补都不晚但越早补收益越大。