最近整理Java学习笔记的时候,我翻出了很多刚入门时踩过的坑,也看了不少网上流传的《Java自学路线图(超全超详细)》。说实话,那份路线图内容很长,知识点列得很全,但真要照着学,很多人坚持不到两周就放弃了。问题不在于知识不够多,而在于路线太“散”,不知道哪些该先学、哪些可以缓一缓。所以这份笔记我不打算做成百科式清单,而是以一个从零开始自学、后来做过几个真实项目的一线Java工程师视角,把基础语法、集合容器、并发、JVM、生态工具常见场景都串一遍。适合准备入门的同学、正在复习Java基础准备面试的人,也适合工作中想快速查漏补缺的同事。
1. 学习路线怎么定:先看清Java的森林
1.1 从零基础到能干活,路线其实比你想的短
网上经常看到有人问“Java自学要多久才能找到工作”,我的回答是:如果你只是想写出能跑的代码,两周足够;如果你想进公司干活,建议按三个阶段走。
第一阶段是Java SE基础,包括数据类型、运算符、流程控制、数组、面向对象、集合、IO、异常、泛型、反射、注解。这个阶段的核心是建立“一切皆是对象”的思维。第二阶段是数据库和Web基础,SQL、JDBC、MyBatis、Spring Boot、HTTP协议。这时候能做简单的CRUD系统。第三阶段是项目实战,比如学生管理系统、订单系统、博客系统,重点不是用了什么新技术,而是把事务、权限、日志、异常处理这些东西真正落到代码里。
我见过很多自学资料把“学习路线图”写成了一个大而全的目录,从JDK安装到微服务治理都塞进去。这种“从入门到放弃”的路线说白了就是焦虑贩卖。真正有效的路线应该是短而快的闭环:先写代码,再补理论,最后做项目。每学完一个模块,立刻用代码验证,而不是一直看视频看文档。
1.2 环境准备避坑:JDK选择与WIN11环境变量
环境配置是劝退新手的第一座山。很多问题不是不会装,而是装完发现命令不对、版本冲突。我一般建议用Eclipse Temurin(原AdoptOpenJDK)或者Oracle JDK。版本选择上,企业里JDK 8仍然很多,但JDK 11和17的用户也在快速增长,学习阶段装JDK 8或11都行,不要直接上最新的JDK 21。原因很简单:很多老项目还跑在8上,教材和面试题也大多基于8。
WIN11下配置Java环境大致是下载JDK安装包、设置JAVA_HOME、把%JAVA_HOME%\bin加到Path。装完之后在命令行执行java -version验证。这里有个很隐蔽的坑:如果电脑里装过其他JDK,比如ClickOnce或系统自带的JRE,命令行执行时可能指向了旧版本。先用where java查一下实际路径,再用java -version确认。IDE里也要单独配置JDK,比如IntelliJ IDEA的Project Structure里选一下SDK路径,不然IDEA和命令行用的可能不是同一个JDK。
2. 基础语法与常见坑:写得对,更要写得稳
2.1 数据类型、数组越界与编码
Java有8种基本类型:byte、short、int、long、float、double、char、boolean。对应包装类型是Byte、Short、Integer、Long、Float、Double、Character、Boolean。这里最容易考的问题就是Integer缓存。默认Integer缓存范围是-128到127,所以用Integer a = 100; Integer b = 100;比较a == b会返回true,但如果超过127会返回false。这个坑刷题的时候很容易碰见。
数组越界是新手高频报错,就是ArrayIndexOutOfBoundsException。常见原因是循环边界写错,比如习惯性写了i <= length,数组下标应该是i < length。还有个隐藏问题:数组初始化后,默认值是0或null,如果你遍历时没判空,拿到null一调用方法就会NPE。真正排查越界的时候,别只盯着异常那行,要往上找数组在哪创建、长度是多少、循环用什么变量控制。
编码问题也值得提。Windows环境下经常出现中文乱码,根源是源文件编码、编译编码、运行控制台编码不一致。现在IDE默认UTF-8还好,但老项目或某些命令行工具还是用GBK。建议在Maven或Gradle里显式设置-Dfile.encoding=UTF-8,并在读取文件时指定Charset.forName("UTF-8")。代码里不要依赖操作系统默认编码。
2.2 排序与算法:从冒泡排序看懂工程选择
每个学Java的人都写过冒泡排序,我也写过。冒泡排序的核心就是相邻元素两两比较,大的往后移,一趟下来最大的元素就到位了。时间复杂度O(n²),稳定。实现起来很简单:
public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } for (int i = 0; i < arr.length - 1; i++) { boolean swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) { break; } } }面试时写冒泡排序没问题,但在工程里千万别自己造轮子。Java内置的Arrays.sort()对基本类型用的是双轴快速排序,对对象用的是TimSort,性能比手写冒泡好得多。对象排序时,可以写Comparator.comparing(User::getAge)或Comparator.comparingInt(...),如果要倒序就再reversed()。很多新手容易犯的错误是让实体类实现Comparable,却忘记重写equals和hashCode,导致排序结果正常,但放到Set或Map里行为很奇怪。
2.3 Lambda、内部类与深拷贝
Lambda表达式是Java 8引入的,它本质上是函数式接口的简洁写法。函数式接口就是只有一个抽象方法的接口,比如Runnable、Consumer、Predicate、Function。使用起来很简单:
List<String> names = Arrays.asList("小明", "小红", "小刚"); names.forEach(name -> System.out.println(name));这里有个硬性规则:如果Lambda表达式引用了局部变量,这个变量必须是final或者effectively final(赋值后再没有改过)。否则编译都过不了。原因在于Lambda底层是把局部变量复制一份到匿名类中,如果变量还能被重新赋值,就会造成数据不一致。
内部类的话题也常和Lambda混在一起。Java内部类有静态内部类、成员内部类、局部内部类、匿名内部类。匿名内部类是最常见的,比如new Comparator<Integer>() { ... }。Lambda和匿名内部类有个细微区别:匿名内部类里的this指向匿名类对象,而Lambda里的this指向外部类对象。调试事件监听器时,如果发现this不是你预期的对象,多半就是用了Lambda。
再提一下深拷贝。Object.clone()默认是浅拷贝,只复制基本类型和对引用对象的指针。要实现深拷贝,简单粗暴的办法是序列化,前提是对象所有字段都可序列化:
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(source); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); Object target = ois.readObject();但这种写法性能差,而且有些字段标记了transient就不会被复制。如果只是复制一个业务对象,我建议手动逐个字段拷贝,最可控,也最不会踩坑。
3. 面向对象与设计模式:用对象思维写代码
3.1 封装、继承、多态:不只是背概念
面向对象是Java的灵魂,但很多人的理解停留在“类是模板,对象是实例”这一层。封装的核心是隐藏实现细节,对外暴露稳定的接口。继承描述的是“is-a”关系,多态则是让同一个方法在不同子类上有不同表现。
写代码的时候,访问修饰符要养成习惯:私有字段用private,对外方法用public,受保护的用protected。不要为了图方便把所有字段都设为public,否则封装就没有意义了。方法重写要注意返回值、修饰符、抛异常的限制都比父类更宽松。重载是同一个类里方法名相同、参数列表不同,和多态不是一个概念,面试时经常被混在一起问。
我踩过最深的坑是继承层次太深。比如原来有个BaseEntity,然后子类又派生子类,再叠加接口默认方法,最后找个字段都不知道是哪个层级定义的。经验是:能用接口描述能力就用接口,能用组合就用组合。继承本身不是坏事,但设计时一定要清楚“为什么继承”。
3.2 设计模式在业务中的落地
设计模式是提升代码可读性和扩展性的工具。单例模式是面试高频题,特别是双重检查锁写法:
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }volatile在这里必须加,因为指令重排可能让其他线程拿到一个未完全初始化的对象。这个细节面试官很爱问。
策略模式在业务系统里特别实用。比如支付场景,有微信、支付宝、银行卡,可以定义一个PayStrategy接口,每种支付方式实现一个子类,然后用Map存放实现类,根据参数路由。这样新增一种支付方式时不用改动原有逻辑,只加一个实现类就行。模板方法模式适合那些流程固定、细节可变的场景,比如数据导入:先校验文件,再解析数据,最后入库,校验规则和解析规则由子类实现。
说到行级权限,这是很多系统里隐藏的需求。比如一个后台管理系统里,普通用户只能看到自己创建的数据,部门主管能看到本部门的数据,管理员才能看全部。实现行级权限最简单的方案是在SQL里拼user_id = ?或者dept_id IN (...),而不是查全表再在内存里过滤。后者不仅慢,还容易造成越权。做得好的框架会在MyBatis层面通过拦截器统一拼权限条件,代码里就不需要每个SQL都写一遍了。
4. 集合与并发:容器选择和线程安全
4.1 集合容器底层选型
Java集合是个大话题。我整理笔记时通常把常用容器分三类:List、Map、Set。
ArrayList底层是Object数组,默认初始容量是10,每次扩容为原来的1.5倍。如果提前知道数据量,最好在构造时指定容量,能减少扩容开销。LinkedList底层是双向链表,插入删除快,但随机访问慢,实际项目里用的不多。HashMap底层是数组+链表,当链表长度超过8且数组长度超过64时,链表转为红黑树;负载因子默认0.75。为什么是0.75?这是空间和时间的一个折中。线程不安全,多线程环境下用ConcurrentHashMap。
看源码的时候,重点要看HashMap的hash()方法为什么要把高位异或下来,以及putVal里什么时候扩容、什么时候树化。这些细节能帮你理解“哈希冲突”到底是怎么处理的。不少面试题会问HashMap和Hashtable的区别,最简单的回答是Hashtable是同步的,HashMap不是;但真正重要的是HashMap允许key为null,Hashtable不允许,而且Hashtable的并发性能很差,已经不建议用了。
Set底层大多基于Map实现,比如HashSet内部就是HashMap,利用键的唯一性实现去重。如果要遍历Map,最好用entrySet()而不是keySet()再查一遍,否则多了一次get操作。
4.2 AQS与并发工具:看懂排队规则
AQS全称是AbstractQueuedSynchronizer,是JUC包下很多并发工具的基础。ReentrantLock、Semaphore、CountDownLatch都是基于它实现的。理解AQS的核心,只需盯住两个东西:一个volatile修饰的state状态变量,一个双向CLH等待队列。
当线程尝试获取锁时,会调用tryAcquire尝试修改state。如果成功,就拿到了锁;如果失败,就进入等待队列排队。后来有线程释放锁,会唤醒队列头部的等待线程。ReentrantLock的可重入逻辑就是state累加:同一个线程每加锁一次state加1,每解锁一次state减1,减到0才是真正释放。
很多人背诵AQS源码,但面试时讲不清楚“为什么需要队列”。打个比方:锁就像卫生间,只有一个坑位,来的人发现坑位被占,就在门口排队;当前使用的人出来时,确实进入卫生间,而不是冲进去抢。AQS就是这套排队机制的Java实现。你要是能把这个过程说清楚,比背十行源码都管用。
4.3 数据一致性保障
说到“Java怎么保证数据一致性”,这个问题可以分很多层。单机多线程下,靠的是JMM(Java内存模型)提供的可见性、原子性、有序性保证。synchronized既能保证原子性,也能保证可见性;volatile只能保证可见性和有序性,不能保证原子性。所以用volatile修饰一个计数器,多个线程同时count++仍然会丢值。
乐观锁和悲观锁也是高频考点。悲观锁就是先加锁再访问,适合并发冲突高的场景;乐观锁就是CAS,比较并交换,适合冲突不高的场景。CAS有ABA问题,解决办法是加版本号或时间戳。在Java中,AtomicStampedReference可以解决ABA问题。
单机锁只能解决单进程内的线程安全问题。如果是微服务架构,需要分布式锁。最简单的实现是Redis的SETNX加过期时间,但要注意防止死锁和误删锁的问题。更稳妥的做法是用Redisson的RLock,它会自动续期,避免业务没跑完锁就过期了。我踩过的坑是:业务逻辑里忘了释放锁,结果别的线程一直拿不到锁,后来锁定代码必须写在finally块里,或者用tryLock等方法。
数据库层面还有个常见手段是乐观锁:在表里加version字段,UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?,更新的同时检查版本号。这样能防止并发更新把数据覆盖掉。事务里还要注意事务传播行为,尤其是@Transactional失效的场景:同类内部方法调用、非public方法、异常被捕获等,都会让事务不生效。
5. JVM知识:Java是静态链接的吗?
5.1 类加载与动态链接
网上有个说法“Java是静态链接的”,我每次看到都觉得要澄清一下。C/C++的静态编译是在编译期把被调用的库函数直接拼进可执行文件,我把这种情况叫静态链接。Java则完全不是这样。
Java编译出来的是.class字节码,里面存的符号引用,而不是实际的内存地址。程序运行时,类加载器才把需要的类加载进来,在解析阶段把符号引用替换为直接引用,这个过程是动态的。所以同一个class文件依赖的类库完全可以在运行时更换,只要classpath里有,就能加载到。这也是Java生态里依赖管理能这么灵活的基础。
类加载器也是面试常客,双亲委派模型要理解:当类要被加载时,先让父加载器尝试,父加载器加载不到才轮到子加载器。这样可以保证核心类库不会被篡改。至于为什么会抛出ClassNotFoundException或NoClassDefFoundError,大多数原因是classpath缺少依赖,或者依赖冲突导致类加载不到对应版本。
5.2 内存区域与垃圾回收
JVM内存区域我在笔记里画过无数遍图:堆、栈、方法区(元空间)、程序计数器、本地方法栈。堆是对象分配主战场,又分新生代和老年代;新生代又分伊甸园、From Survivor、To Survivor。绝大多数对象先在伊甸园创建,Minor GC后存活对象被复制到Survivor,年龄增长到一定阈值才进入老年代。
垃圾回收算法常见的三种:复制、标记-清除、标记-整理。复制算法适合年轻代,因为存活对象少,复制成本低;老年代对象存活率高,通常用标记-整理或标记-清除配合处理。现在的G1收集器把堆划分为多个Region,能够做到可预测的停顿时间。
调优方面,新手不用追太多花哨参数,重点理解-Xms和-Xmx。堆大小初始值和最大值如果一样,可以减少运行时扩容带来的开销。线上遇到OOM,先看是堆溢出还是元空间溢出,再配合jmap和jstack分析。这里我有个习惯:写项目前就在IDE里配置好-XX:+HeapDumpOnOutOfMemoryError,出了OOM还能留下dump文件排查。
6. Java生态的实用场景
6.1 用POI操作Word并生成图表
有朋友问“Java POI Word能生成图表吗”,答案是能,但不像Excel图表那样开箱即用。Apache POI里操作Word的类主要是XWPFDocument,支持docx格式。生成图表的API从POI 4.1左右开始提供,可以创建XWPFChart并插入简单的柱状图、折线图等。下面是一个创建Word文档并加段落和表格的简单流程:
try (XWPFDocument document = new XWPFDocument()) { XWPFParagraph paragraph = document.createParagraph(); XWPFRun run = paragraph.createRun(); run.setText("Java学习笔记"); XWPFTable table = document.createTable(3, 3); table.getRow(0).getCell(0).setText("知识点"); table.getRow(0).getCell(1).setText("优先级"); table.getRow(0).getCell(2).setText("掌握进度"); try (FileOutputStream out = new FileOutputStream("note.docx")) { document.write(out); } }如果要生成复杂图表,比如带数据标签、图例、多系列折线图,原生API会比较吃力。我遇到过的做法是事先在Word里做好模板,再用POI替换数据。这种方式可控性强,不用和底层OOXML的XML细节死磕。需要注意POI处理大文档时内存消耗较高,如果并发导出,建议用线程池控制并发数。
6.2 网络编程与数据采集
Java获取DNS相关信息很简单,用InetAddress就行:
InetAddress[] addresses = InetAddress.getAllByName("www.example.com"); for (InetAddress address : addresses) { System.out.println(address.getHostAddress()); }但要注意JVM对DNS解析结果默认有缓存。InetAddress的缓存分为正向解析和反向解析,缓存时间可以通过安全属性networkaddress.cache.ttl控制。如果程序里改了DNS记录,线上还是解析到旧IP,多半就是缓存没刷新。
说到Java在网络编程和采集网关方面的应用,我工作中也写过类似的东西:用定时任务从设备接口轮询数据,解析后批量写入数据库。这种采集网关要注意几个点:第一是超时设置,HTTP调用必须配连接超时和读超时;第二是重试机制,网络抖动是很常见的,但重试要考虑幂等性,防止重复写入;第三是线程池隔离,不能因为一个上游接口变慢,拖垮整个采集进程。使用ExecutorService或者虚拟线程后,可以用虚拟线程处理大量IO等待场景。
Java的应用范围真的不只是互联网后端。很多学习管理系统(LMS)、物联网接入服务、工具类软件,比如Minecraft的启动器PCL,底层都是Java。学Java的时候没必要把自己束缚在“只有Web开发”这个框架里,核心能力扎实了,很多场景都能迁移。
6.3 环境问题与依赖排查
运行Java项目时,最容易遇到的问题是Lombok报错,提示you aren't using a compiler supported by lombok。我第一次遇到时很懵,明明代码没错。后来发现是JDK 16以上版本更新了编译接口,旧版Lombok不兼容。解决办法就三条路:升级Lombok到新版本;把JDK降到8或11;如果用的是IDEA,检查一下Enable annotation processing有没有打开。Maven项目里还可以在maven-compiler-plugin的annotationProcessorPaths里显式声明Lombok版本,避免和自有依赖冲突。
另一个常见场景是Docker容器里提示找不到java。这个通常不是代码问题,而是基础镜像里根本没装JDK。有些精简镜像只包含运行时库,并不包含完整的java命令。解决方法是使用带JDK的基础镜像,比如eclipse-temurin:17-jdk,或者在Dockerfile里安装JDK。还要注意容器里环境变量是否继承,PATH是否包含JDK的bin目录。排查时先在容器内执行which java、java -version,就能定位到是镜像问题还是路径问题。
编码问题也值得一提。不管是Maven项目还是Gradle项目,都要确认编译编码是UTF-8。如果项目的pom.xml里没有设置project.build.sourceEncoding,在Windows上编译,读取中文资源文件时就很容易出现乱码。另一个容易忽略的点是IDE控制台输出乱码,很多时候不是代码问题,而是控制台编码和系统编码不一致。
7. 学习资源与面试准备
7.1 免费资源推荐
Java入门的免费资源其实很多。我推荐路线是:语言基础看廖雪峰Java教程和菜鸟教程,这两个适合快速上手。想系统复习和应对面试,可以看GitHub上的JavaGuide,几乎把Java核心知识点都整理成了问答形式。B站上也有不少高质量全套视频,但看的时候要挑播放量高、更新时间近的,否则很容易学到过时写法。
刷题网站方面,LeetCode力扣是练算法的标配,牛客网有大量Java面试题和公司真题。如果你喜欢刷简单题练手感,可以在力扣按“数组”“字符串”“链表”标签刷,每天两三题,坚持一个月就有感觉。不要一味追求难题,Java后端面试里算法题一般就是中等难度,能写清楚思路比AC更重要。
免费的入门网站还有一个容易被忽略的:Oracle官方Java Tutorial。它是英文的,但内容权威,适合查缺补漏。
7.2 八股文与面试题怎么看
“Java八股文”这个说法很有意思,很多人背得很熟,但一深入问就露馅。我的建议是不要死背答案,把每个题当成一个故事来讲。比如面试官问“HashMap底层实现”,不要回答“数组+链表+红黑树”就完了,而是讲清楚:为什么用数组?哈希冲突怎么办?链表什么时候转红黑树?为什么阈值是8?这样才显得你真的理解。
面试题可以按模块整理:集合、并发、JVM、Spring、MySQL、Redis、消息队列。每个模块准备三到五个核心问题,比如“线程池参数怎么设置”“Spring Bean生命周期”“MySQL索引为什么用B+树”。准备的时候可以自己写一段demo验证,再总结成笔记。等到面试时就能说得既熟练又不背书。
刷题和面试虽然重要,但最终还是要回到项目本身。面试官更想知道的是你在项目里怎么解决过问题,而不是考察你记住了多少标准答案。所以我在笔记里特别强调:每学一个新知识点,一定要造一个小例子或者看看框架源码里的真实用法,否则这个知识只是“知道”,不会“会用”。
结尾:写笔记的意义,在于复盘
整理完这份Java学习笔记,我最大的体会是,知识不是记在本子上就属于你的。很多人收藏了无数资源,却从不打开代码编辑器,最后依然是“一看就会,一写就废”。动手敲一遍、踩一次坑、看一遍报错栈,比看十篇教程都管用。
有件事我一直建议身边新人做:每次遇到报错,别急着复制粘贴到群里,先自己看异常信息的前三行,把准确错误信息、操作步骤、环境版本记录下来。时间长了,你会发现自己排查问题的速度快得惊人。写笔记也是一样,与其抄别人的总结,不如记录自己踩坑后的思考,那样的笔记才真正有用。