如果让我只挑一句话来说清楚《Java 程序设计》这门课到底在学什么,我会说:它不是在教你背 Java 语法,而是在训练你用 Java 这门语言去完成从问题拆解、类设计到代码实现的一整套思维流程。很多同学来问我 Java 怎么学、Java 面试题怎么准备、所谓的“八股文”要不要背,我的回答一直很一致——知识点当然要梳理,但更重要的是知道这些知识点为什么存在、用在哪里、坑在哪里。
这篇文章不是教科书式复述,而是我把这些年带新人、准备面试、实际做项目时反复绕不开的 Java 核心知识点做的一次系统梳理。你如果是刚开始接触 Java,可以把它当成一份“先看哪里”的地图;如果你正准备面试或者应对期末考试,可以把它当成查漏补缺的清单;如果你已经写了一段时间代码但总觉得底子虚,那这篇内容正好帮你把那些略过的关键机制补回来。
我把内容切成几个层面:先讲 Java 程序设计的整体学习思路,再铺开基础语法、面向对象和常用 API,接着深入到异常、泛型、Lambda、多线程、反射与动态代理,最后回到环境配置和实战排错。这样梳理的好处是,你看到一个知识点时,既知道它长什么样,也知道它为什么长这样。
1. 先想明白:Java 程序设计到底在考察什么
1.1 从“语法”到“程序设计”的距离
很多初学者会把 Java 程序设计和“背 Java 语法”画等号,这是最需要纠正的认知。语法只是工具,程序设计才是目的。同样是求一个数组里的最大值,刚学三天的同学会写一个双层循环加 if 比较,学过一个月的同学会想到用排序或者直接调 API,学半年的同学会开始思考这个数组是常驻内存还是外部输入,是 int 类型还是对象排序,需不需要考虑并发修改。
我说这些不是鼓励大家一开始就追求设计模式,而是想点明一个事实:Java 核心知识点天然是分层级的。最底层是变量、运算符、流程控制,再往上是类与对象、继承、多态,再往上是集合、异常、IO、多线程,再往上是反射、泛型、函数式编程、JVM 相关的东西。你每往上走一层,都意味着对前面一层有更深入的理解。
很多搜索热词里总出现“Java 基础”“Java 面试题”“Java 面试八股文”,这其实从侧面说明了一个现象:Java 生态庞大,面试官很难通过几个问题就判断出你的真实项目水平,所以只能从这些约定俗成的核心知识点去考察你是否具备基本盘。换句话说,这些被大家调侃为“八股文”的内容,恰恰是 Java 程序设计里最值得被固化的部分。
1.2 不同阶段要达成三个不同层次的目标
我把学习 Java 的人粗略分成三类:应付考试的在校生、准备面试的求职者、已经在项目中写代码的工程师。三类人看核心知识点的侧重点完全不同,但知识本身没有变,变的是深度要求。
对在校生来说,目标是能理解并能默写关键代码。比如字符串的反转、数组的排序、自定义类的比较器,这些题看起来机械,实际上是在检验你对语法 API 的熟悉程度。对求职者来说,目标上升一层,需要能讲清楚原理。比如 HashSet 为什么是无序的、HashMap 的扩容机制是什么、线程池里的核心参数是怎么配合的,这类问题考察的是你是否明白“底层发生了什么”。对工程师来说,目标再上升一层,是能在真实场景里做取舍。同样是做异步任务,什么时候用线程池,什么时候用消息队列;同样是读取配置文件,什么时候用 Properties,什么时候用 YAML。
这篇文章覆盖的目标,是让处在任意阶段的读者都能往前推进一步。我会尽量把原理和实操放在一起讲,而不是只抛出一堆概念。
2. 基础语法与代码习惯:地基打不牢,后面全是坑
2.1 变量、包装类与运算符背后的细节
Java 的变量分为基本类型和引用类型,这是一个几乎所有教材都会讲、但很多人没有真正理解的区别。基本类型有 int、double、boolean、char 等,它们直接存储值;引用类型存储的是对象的地址。理解这个区别,对后续理解“方法传参是不是引用传递”“字符串为什么不可变”“HashMap 的 key 为什么需要重写 hashCode”都非常关键。
以运算符为例,面试和考试都喜欢问 i++ 和 ++i 的区别、短路与和短路或的行为、位运算符的使用场景。这些看起来是死记硬背的知识,实际映射的是底层计算模型。比如短路与if (a != null && a.length() > 0),如果 a 是 null,左边条件为 false 后右边根本不会执行,这就是短路的价值,能避免空指针。你要是把&&写成&,语义就变了,甚至会直接抛异常。毫厘之差,写代码时很难发现,出了问题却非常隐蔽。
包装类是另一大高频考点。int 对应 Integer,boolean 对应 Boolean,Java 提供了自动装箱和自动拆箱机制,让基本类型和包装类可以互相转换。但这里有一个经典陷阱:Integer a = 127; Integer b = 127; a == b的结果是 true,而Integer a = 128; Integer b = 128; a == b的结果是 false。原因是 Integer 在 -128 到 127 之间有缓存,超出这个范围会 new 新对象。这个问题如果不去深挖,写代码时就很容易在比较中踩雷。
public class IntegerCompare { public static void main(String[] args) { Integer a = 128; Integer b = 128; System.out.println(a == b); // false System.out.println(a.equals(b)); // true } }这段代码几乎是 Java 核心知识点里的“送分题”,但也是实际代码中最常见的隐患之一。我的建议很朴素:所有包装类之间的比较,一律用 equals 或者直接比较基本类型,不要依赖 ==。
2.2 流程控制与字符串比较的经典陷阱
流程控制本身不难,if、else、switch、for、while 是任何一门语言都具备的骨架。但 Java 里有两个容易翻车的点:一个是 switch 对字符串的支持是 JDK 7 以后才有的,早期版本只能用整数或枚举;另一个是增强 for 循环底层用的是 Iterator,遍历过程中如果直接修改集合结构会抛出 ConcurrentModificationException。
字符串比较则是我每次带新人必讲的课题。字符串是对象,不是基本类型,所以比较内容要调用 equals,而不是 ==。但很多人不知道的是,String s1 = "abc"; String s2 = "abc";时,s1 == s2 的结果是 true,这是因为字符串常量池的存在。一旦改成String s3 = new String("abc");,s1 == s3 就变成 false 了。这直接引出了字符串不可变性的设计原因:只有字符串不可变,编译器才能安全地把相同字面量放到常量池里共享,运行时才能节省大量内存。
说到字符串,就不得不提字符串拼接的性能问题。在循环里做str += item,每次都会生成新的 String 对象,造成大量不必要的内存开销。正确的做法是使用 StringBuilder,它是一个可变的字符序列,append 方法不会频繁创建对象。这是一个细节,但在大数据量场景下性能差距可以达到几十倍。
StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i).append(","); } String result = sb.toString();我见过不少同学的实验项目因为这种小细节导致页面卡顿,根源不是算法复杂,而是对核心 API 的性质不够了解。所以说基础语法不是背书,而是代码习惯的养成。
3. 面向对象与常用类:Java 的核心舞台
3.1 类、对象与内存模型
Java 是一门面向对象的语言,这句话嘴里都说烂了,但真要问“类到底是什么,对象又是什么”,很多人反而讲不清楚。用最生活化的比喻来说:类就是设计图纸,对象就是按照图纸生产出来的实物。图纸规定了实物有哪些属性以及能做什么动作,但图纸本身不能住人,只有 new 出来的对象才能真正在系统里发挥作用。
这个比喻背后对应的是 JVM 内存模型。用new创建一个对象时,对象本身被分配在堆内存中,而栈内存里存放的是这个对象的引用。引用像一个门牌号,实际内容在堆里。方法里的局部变量如果是基本类型,直接存在栈里;如果是引用类型,栈里存地址,堆里存数据。
理解这一点后,很多问题都能解释了。比如方法传参时,Java 官方说“Java 是值传递”,很多人不理解:传对象进去,方法里修改属性,外部对象也变了,这难道不是引用传递吗?其实,传递的是引用的副本,两个引用都指向同一个对象,所以通过任一个引用修改属性都会影响原对象。但如果方法内部把引用重新指向新对象,外部引用并不会变化。这个机制搞不清楚,写出来的代码就会出现“我以为改了,但没改”的诡异现象。
一个合格的程序设计者,应该能在脑袋里画出代码对应的内存布局。不需要追求特别底层,只要能画出“这个变量在栈里,这个对象在堆里,这里的数组是一片连续空间”,你写代码的信心都会完全不同。
3.2 继承、多态、抽象类与接口
面向对象三大特性是封装、继承、多态。封装讲的是把数据和行为绑定在一起,并对外隐藏内部细节;继承讲的是类与类之间的纵向关系,子类拥有父类的属性和方法;多态讲的是同一个方法在不同对象上有不同表现。三者不是孤立的,多态通常建立在继承或接口实现之上。
我在讲这部分时,习惯用形状类来做例子。定义一个 Shape 抽象类,里面有一个 draw 方法,但 Shape 自己不知道怎么画,所以它应该是抽象方法,Shape 本身也应该是抽象类。Circle 和 Rectangle 继承 Shape 并各自重写 draw。这样,写代码的人只需要面向 Shape 编程,调用 draw 时实际执行的是各个子类自己的实现。
abstract class Shape { abstract void draw(); } class Circle extends Shape { @Override void draw() { System.out.println("画一个圆形"); } } public class Demo { public static void main(String[] args) { Shape shape = new Circle(); shape.draw(); } }这段代码看着简单,但它体现了面向对象设计的核心思想:依赖抽象而不是依赖具体。正因为它重要,面试里才会有大量关于接口与抽象类区别的问题。我的回答通常从四个角度对比:构造方法上,抽象类可以有构造方法,接口不能;继承结构上,类只能单继承,但接口可以多实现;成员变量上,抽象类的变量可以随意定义,接口的变量默认是 public static final;设计目的上,抽象类强调 is-a 关系,接口强调 has-a/能力契约关系。
从设计角度讲,接口更适合定义能力。比如定义一个 Flyable 接口,里面只有 fly 方法,鸟类、飞机类都能实现它,但它们之间没有任何继承关系。这种“能力组合”比“层级强制”更灵活,也是为什么现代 Java 框架普遍喜欢面向接口编程。
3.3 字符串、数组、集合与日期类
字符串在前面提过不可变性,接下来要重点说的是数组和集合的选型。数组是固定长度的连续内存空间,访问速度快,但声明后不能动态增删元素。集合则是数组的进阶版,封装了扩容、迭代、排序等常用操作。实际编写 Java 程序时,集合的使用频率远高于数组,但数组仍然在底层算法、数值计算、性能敏感场景中扮演重要角色。
Java 集合框架的整体结构很简单:Collection 里分为 List、Set、Queue;List 的有序可重复,Set 的去重,Queue 的先进先出;另外还有独立的 Map,以键值对形式存储。选型时不要背文档,要从需求反推。需要按下标访问,选 ArrayList;需要频繁在头部插入删除,选 LinkedList;需要去重,选 HashSet;需要保持插入顺序,用 LinkedHashSet;需要排序,用 TreeSet。Map 的选择也同理,HashMap 是最通用的,需要线程安全就用 ConcurrentHashMap,需要有序就考虑 TreeMap 或 LinkedHashMap。
提到 HashSet 和 HashMap,就绕不开 equals 和 hashCode 方法。Java 规定,两个对象 equals 相等,hashCode 必须相等;hashCode 相等,equals 不一定相等。往 HashSet 里放入自定义对象时,如果只重写 equals 而不重写 hashCode,会出现两个明明相等的对象都能同时加入集合,去重失效。这个问题在很多实验课里都会遇到,面试更是高频中的高频。
日期类也是 Java 知识点里容易被忽略的部分。早期的 java.util.Date 和 java.util.Calendar 设计得很别扭,月份从 0 开始,线程也不安全。Java 8 之后的 java.time 包彻底改进了这个问题,LocalDate 表示日期,LocalDateTime 表示日期时间,DateTimeFormatter 用来格式化。我现在写任何新代码都建议用 java.time,处理日期逻辑直观很多,比如日期加减直接用 plusDays、minusMonths,计算两个日期间隔用 Period 或 Duration。
4. 异常、泛型与函数式编程:把代码写成工程代码
4.1 异常体系与 try-with-resources
可靠运行的 Java 程序不可能是“永不报错”的程序,而是“报错能被正确处理”的程序。Java 的异常体系以 Throwable 为顶层,下面分为 Error 和 Exception。Error 表示 JVM 层面的严重问题,比如 OutOfMemoryError、StackOverflowError,这种情况程序一般无法恢复,也不应该去捕获。Exception 才是程序设计时真正需要关注的,它又分为受检异常和非受检异常。
受检异常(checked exception)发生在编译期,比如 IOException、SQLException,编译器强制要求调用者捕获或者向上抛出。非受检异常(unchecked exception)发生在运行期,比如 NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException,编译器不会强制处理。这个设计在工程上很有意义,它把“可预料的异常”和“编程失误导致的异常”区分开,倒逼开发者在读文件、连数据库时必须考虑失败处理。
异常处理的关键不只是捕获,更重要的是资源释放。以前写文件读取,需要在 finally 里手动关闭流,代码很长也容易漏。Java 7 引入 try-with-resources 语法后,只要资源实现了 AutoCloseable 接口,就能自动关闭。
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) { String line = reader.readLine(); System.out.println(line); } catch (IOException e) { e.printStackTrace(); }这段代码的优雅之处在于,不管 try 块是否抛出异常,reader 最终都会被自动关闭,不用再写烦人的 finally 块。我在代码审查时经常看到同学把异常吞掉,只留一个空 catch,这是比抛出异常更危险的做法。异常信息是排错的唯一线索,至少要把堆栈打印出来,或者打日志,绝对不能默默吃掉。
4.2 泛型的意义与集合安全
泛型是 Java 进阶路上的一道坎。很多初学者会觉得泛型只是给集合加上<String>尖括号,没什么大不了。但泛型的设计目的是把“类型安全”检查从运行期提前到编译期。没有泛型的时候,集合里存进去的是 Object,取出来要强制转型,一旦转型错误,就会在运行期抛出 ClassCastException。有了泛型后,编译器就能提前确认类型匹配,代码同时变得更简洁更安全。
泛型有几个常见考点:泛型类、泛型方法、通配符 ?、上下界限定 extends 和 super。其中通配符最容易和泛型继承搞混。List<String> 并不是 List<Object> 的子类型,所以不能把 List<String> 传给接收 List<Object> 的方法。这时候就需要用到 List<? extends Object> 来表示“任何 Object 子类的 List”。
public static void printAll(List<? extends Number> list) { for (Number n : list) { System.out.println(n); } }注意,这里看起来只是很小的语法差异,但背后是泛型不变性的设计。它保证了对集合进行读写时的类型一致性,否则容易在运行时插入一个不兼容的对象,破坏整个数据结构。写代码时如果不理解这一点,早晚会在拐弯抹角的设计里遇到奇怪的编译报错。
4.3 Lambda、Stream 与常用函数式接口
Java 8 引入的 Lambda 表达式和 Stream API,极大改变了 Java 程序设计的编码风格。传统写法里,排序需要写一个匿名内部类 Comparator;Lambda 出现后,一行就能搞定的排序代码让代码量大幅缩减。
list.sort((a, b) -> a.compareTo(b));Lambda 的本质是函数式接口的实例。函数式接口指的是只包含一个抽象方法的接口,比如 Runnable、Comparator、Function、Predicate、Consumer。一个函数式接口,就能用 Lambda 表达式简洁地创建匿名实现类对象。
Stream 则是把集合操作变成一条流水线,可以链式调用 filter、map、sort、limit、collect 等方法。曾经搜索引擎热词里有“java list.stream().toarray”,说明很多人在用 Stream 时遇到了转数组的疑问。这里我展开解释一下:Stream 的 toArray() 无参版返回 Object[],如果你想要 String[],需要传入一个长度构造器参数,写法是toArray(String[]::new)。
List<String> list = Arrays.asList("java", "stream", "demo"); String[] array = list.stream().filter(s -> s.length() > 3).toArray(String[]::new); System.out.println(Arrays.toString(array));Stream 的价值不只是简化写法,更重要的是它可以配合并行流提高处理速度,但并行流的使用要谨慎,因为它涉及线程安全、任务拆分、结果合并等复杂问题。核心知识点的学习还是要先掌握顺序流,把 filter、map、collect 理解透,再去碰 parallelStream。
5. 进阶核心机制:多线程、反射与动态代理
5.1 多线程:从 Thread 到线程池
Java 程序的并发能力是它能在企业级应用里站稳脚跟的重要原因。多线程的入门从两个方式开始:继承 Thread 类和实现 Runnable 接口。但实际开发中,这两个方式用的都不多,因为频繁创建线程的开销非常大,线程的生命周期管理也很麻烦。工程里更多使用线程池,通过复用有限数量的线程,处理大量异步任务。
线程池的核心参数必须能讲清楚:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。简单理解,核心线程数是池子长期保留的线程数量;工作队列是用来存等待任务的地方;当队列也满了,才会创建非核心线程去处理;再忙不过来,就启动拒绝策略。
ExecutorService pool = Executors.newFixedThreadPool(4); pool.execute(() -> System.out.println("任务执行")); pool.shutdown();网上很多教程会用 Executors 工具类直接创建线程池,但 Java 并发规范更推荐使用 ThreadPoolExecutor 手动指定参数。因为 Executors 提供的一些工厂方法存在隐患,比如 newFixedThreadPool 和 newSingleThreadExecutor 的队列是无界的,高峰时期可能会堆积大量任务,导致内存溢出。本质上是把风险和参数选择权交还给程序员,让你根据业务场景设计合适的线程数量与队列策略。
5.2 让“所有线程都完成”:join、Future 与 CountDownLatch
很多同学在处理多线程任务时都会遇到一个经典需求:主线程要等所有子线程都执行完,再继续做下一步。这个需求如果处理不好,就会出现主线程先跑完、子线程还在后面慢悠悠执行的情况。
最原始的做法是用 Thread.join 方法,让主线程等待指定线程结束。但 join 一次只能等一个线程,如果有一堆线程,写起来很繁琐。更好的方案是使用 CountDownLatch,它是一个计数器,初始值设为子线程的数量,每个子线程执行完就调用 countDown,主线程调 await,直到计数器归零才继续。
CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(() -> { System.out.println("子线程执行"); latch.countDown(); }).start(); } latch.await(); System.out.println("所有线程执行完成");如果在线程池场景中,更常用的是 Future 和 FutureTask。线程池 submit 方法会返回一个 Future 对象,它的 get 方法是阻塞的,可以获取线程执行结果,也天然实现了等待效果。多个任务可以用一个 Future 列表,逐个 get 来保证全部完成。必要时还可以用 CompletableFuture 做更复杂的异步编排,比如前一个任务完成后自动触发下一个任务,这对异步流程设计非常有用。
5.3 反射与动态代理:框架里的隐藏主角
反射是 Java 中非常特别的一种能力,它允许程序在运行期间动态获取类的完整信息,包括类名、方法、字段、构造方法,并且可以调用这些成员。看到这里你可能会问,正常写代码不就能直接 new 对象、调方法吗,为什么还需要反射?答案在于“动态”。很多场景下,代码在编写时根本不知道需要运行的类是什么,比如 Spring 框架要根据 XML 或注解配置去实例化任意类,就必须靠反射读取类路径、构造对象、调用方法。
反射的常见操作包括:Class.forName("com.example.Demo")获取 Class 对象;clazz.getDeclaredMethod获取私有方法;method.setAccessible(true)绕过访问检查;method.invoke(obj, args)动态调用。这套机制强大,但也带来性能损耗,所以不要为了炫技在业务代码里大量使用反射。
动态代理则是在反射基础上发展出来的高级能力。JDK 提供的动态代理要求目标类实现接口,代理对象在运行时动态生成,会拦截接口方法的调用,在调用前后插入增强逻辑。最典型的场景就是 Spring AOP 的事务管理、日志记录、权限校验。理解了动态代理,面试时再遇到“Spring AOP 底层原理”,你就能答出关键点:JDK 动态代理依靠接口,CGLIB 依靠继承。
Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> { System.out.println("调用前"); Object result = method.invoke(target, args); System.out.println("调用后"); return result; } );这段代码初看很抽象,但它是很多框架的基石。学习到这个深度,你再看 Spring 的声明式事务、MyBatis 的 Mapper 代理,就不会觉得是黑魔法,而是很自然的工程延伸。这部分是 Java 核心知识体系里最接近“深入探究”的一层,也是区分“会写 Java”和“懂 Java”的分水岭。
6. 工程落地与常见坑:环境配置、乱码、面试高频点
6.1 环境变量、JDK 安装与命令行运行
很多初学者在安装配置阶段就被劝退了。其实 JDK 安装本身不复杂,复杂的是环境变量的理解。安装完成后,需要配置 JAVA_HOME、Path 和可选的 CLASSPATH。JAVA_HOME 指向 JDK 的安装根目录,很多开发工具和服务器都靠这个环境变量找 JDK。Path 变量里需要加入 JDK 的 bin 目录,这样你在命令行输入 java、javac 时,操作系统才能找到对应的可执行文件。
验证环境是否配好,三步足够:在命令行输入 java -version、javac -version、echo %JAVA_HOME%。如果都能正常输出,说明基本环境没问题。如果 java -version 能执行但 javac 提示找不到,大概率是 Path 里漏了 JDK 的 bin 目录,或者装的是 JRE 而不是 JDK。
我还想特别提一下用命令行直接编译运行 Java 程序的经验。很多人在 IDE 里按钮一点就运行了,但对背后的 javac 和 java 命令没有概念。用命令行跑一个简单的 Main.java,需要先 javac Main.java 生成 Main.class,再 java Main 运行。一定不能加 .class 后缀,否则会报“找不到或无法加载主类”。理解了这个过程,你对 IDE 里各种报错的理解都会更深。
6.2 常见运行报错与排查技巧
我在实际教学中发现,有一类问题集中爆发在学生用 VSCode 运行 Java 时。最常见的是控制台输出乱码。原因往往是源码文件的编码是 UTF-8,而 Windows 控制台默认使用 GBK 解码,两边对不上就会出现中文乱码。解决办法有几种:把编译器编码改成 UTF-8,或者在运行配置里加上-Dfile.encoding=UTF-8,还可以在系统设置里把控制台的代码页改成 65001。
另外一个高频错误是“错误: 找不到或无法加载主类”。我在前面说过,运行 java 命令时不能带 .class 后缀。但要提醒的是,如果类带了 package 声明,就必须先编译到对应的包目录结构里,然后在包目录的上一层执行带完整类名的 java 命令。很多新手在这个细节上反复翻车,并不是不懂代码,而是对包和类路径的机制不熟悉。
再一个常见错误是 ClassNotFoundException 和 NoClassDefFoundError,这通常涉及第三方依赖。IDE 能自动管理依赖,但你在命令行运行时,必须用 -cp 参数把 jar 包加入类路径。我把常见的 Java 运行错误整理成一个简表,方便对照查找。
| 报错信息 | 可能原因 | 检查方向 |
|---|---|---|
| javac 不是内部或外部命令 | 环境变量 Path 未配置 | 检查 JDK bin 是否在 Path 中 |
| 找不到或无法加载主类 | 运行命令带 .class 或类名错误 | 使用 java 完整类名 |
| Exception in thread main java.lang.NullPointerException | 对象没有实例化 | 检查引用是否被赋值 |
| 乱码 | 编码不一致 | 统一 UTF-8 或 GBK |
| 类已被错误加载 | 类路径存在多个版本 | 检查 jar 包冲突 |
6.3 高频考点速查:面试八股中的“核心知识点”
最后,我把这份梳理落回实际考试和面试最常问的考点上。与其说是八股文,不如说是知识点的密度测试。第一个必问题目是“== 和 equals 的区别”,我们前面已经讲过,一个比较地址,一个比较内容。第二个必问题目是“String、StringBuilder、StringBuffer 的区别”,核心是可变性和线程安全性。String 不可变,StringBuilder 线程不安全但性能好,StringBuffer 加了同步锁所以线程安全但性能略低。
第三类是集合相关的追问,比如 HashMap 底层结构、扩容机制、为什么线程不安全。如果你能把数组加链表的结构、扩容因子 0.75、红黑树转换阈值 8 这些细节讲清楚,面试官基本认可你的功底。第四类是并发相关,volatile 与 synchronized 的区别、ThreadLocal 的原理、线程池参数、死锁的产生条件。第五类是 JVM 基础,类的加载过程、双亲委派模型、内存区域划分、垃圾回收算法。
这些内容为什么总是被反复问?因为它们恰恰是 Java 程序设计从入门到进阶的“主干道”。主干道通了,旁支细节自然能顺着挂上去。我给所有准备面试的同学的建议是:不要死记答案,而是每个知识点都自己写一段最小示例代码跑一遍,观察输出结果,再思考背后的机制。代码亲眼跑过一遍,记忆的牢固程度比背诵答案高很多倍。
我个人在实际教学和带项目过程中,最深的体会就是 Java 这门语言的“回报周期”很长,但一旦把核心知识点串成体系,后面吸收新技术会非常快。你不需要在初学阶段就妄想记住所有 API,先把这个骨架立起来,之后每接触一个新框架,心里都会有一条清晰的线索去定位它到底属于哪一层、解决了什么问题。
如果在梳理过程中遇到某个概念怎么都绕不明白,我的建议是停下手里的代码,先画一张简单的关系图,把涉及的类、接口、方法、调用关系标出来。Java 的复杂度大多来自类与类之间的协作关系,图像化思考往往比死磕源码更有效。也希望这篇梳理,能帮你少走一点弯路,把这个庞大的语言体系稳稳地装进脑子里。