资讯动态

Java特性清单:从基础语法到JVM的完整知识地图

发布时间:2026/9/15 20:35:45 来源:尧图企业网站定制
如果你和我一样在学Java的路上把“java基础”“java面试题”“java学习路线”“java面试八股文”这些关键词翻来覆去搜了无数遍最后手机里存了十几个书签、收藏夹里躺了几十篇干货可一到面试或者写项目时还是心里没底——那问题多半不在你不够努力而是缺少一张能把知识串起来的清单。这份Java特性清单要做的就是把Java从语法、面向对象、集合、并发到泛型、异常、反射、JVM的这些特性摊开放平标出哪些是必须掌握的、哪些是面试高频的、哪些是实战容易踩坑的。你不需要把它背下来你只需要对照着给自己打分每一行你是“见过”“会写”还是“能讲明白”。能讲的越来越多你离真正掌握Java就越来越近。无论你是刚装好JDK的新手还是准备跳槽的中间件开发这份清单都能帮你快速定位盲区。1. 为什么一张“特性清单”比收藏一百篇教程更管用1.1 你缺的不是资料而是知识地图学Java的人几乎都经历过这个阶段搜“java基础”出来几千篇文章搜“java面试题”又出来几十万条帖子再搜“java学习路线”各路大神给出五花八门的路线图。收藏夹越来越满脑子里却越来越乱。问题出在哪出在知识是碎片化的。今天看一篇讲HashMap的文章明天看一篇讲线程池的文章它们之间没有连线你就永远只能“见过”不能“掌握”。特性清单干的事很简单把Java这门语言按层切开——语法层、面向对象层、集合层、并发层、JVM层——每一层列出它包含的特性每个特性标清楚“是什么、为什么、怎么用、常见坑在哪”。这就像打游戏先看地图再跑图。有了地图每一篇零散教程都能归位到地图上的某个坐标知识才会从点连成网。这也就是为什么“java面试大全及答案”这种资源看起来很全背完还是慌——因为答案是别人的地图才是自己的。1.2 用“三级自检法”代替机械背诵我见过太多人准备面试的方式是打开一篇“java面试八股文”从头背到尾。背的时候觉得自己全会了到了面试现场面试官换一个问法就懵了。原因很简单背诵只停留在记忆层没有进入理解层。我建议你对着清单做三级自评第一级见过看过这个概念能照着例子写出来但独立写会卡壳第二级会写不查资料能独立写出来并且真的在项目里用过第三级能讲能用自己的话讲清楚设计思路能接住面试官的连环追问。比如“HashMap底层是数组加链表加红黑树”这句话第一级的人能背出来第二级的人能画出put流程第三级的人能讲清楚为什么链表长度是8就转树、为什么负载因子是0.75。对每一行特性至少要达到第二级面试高频考点要尽量达到第三级。三级自检法听起来费时间实际上比无脑背八股文效率高得多因为它逼着你把“别人的知识”变成“自己的解释”。1.3 这份清单和“八股文”到底是什么关系老实说“八股文”这个说法被污名化了。八股文本质上是前人把常见面试问题标准化让后人有据可查。问题不在八股文本身在怎么对待它——只背结论的八股文是废纸能讲清楚原理的八股文就是你的知识体系。所以这份特性清单的定位很明确它是一张覆盖java基础、java面试高频考点的自检表你可以把它当成学习路线也可以当成面试前的最后一遍扫盲。后面几章我会按层把特性铺开每一条后面其实都藏着一个或多个八股考点你拿它当清单扫也等于在扫考纲。它的价值不在“背下来”而在“对着每一项问自己一句我真懂了吗”2. 语言基础层标识符、数据类型、运算符与流程控制2.1 标识符命名规则最基础的送分题也最容易栽跟头Java标识符的规则其实就那么几条以字母、下划线、美元符开头后续可以跟字母、数字、下划线、美元符不能用关键字和保留字大小写敏感。注意Java标识符理论上支持Unicode字符也就是说你完全可以用中文当变量名编译能过。但我强烈不建议这么干团队协作里代码可读性比“炫技”重要得多。除了语法规则还有一套“潜规则”比语法更重要类名用大驼峰UserService方法名和变量名用小驼峰getUserById、userList常量用全大写加下划线MAX_RETRY_COUNT。命名规范和语法规则一起构成了Java代码风格的底色。面试的时候如果面试官让你手写代码你的命名是否规范很多时候比代码本身更能说明问题。这里顺带说一个面试必考题main方法为什么必须是public static void main(String[] args)public是为了让JVM能从外部调用static是因为JVM启动时还没有任何对象void是main方法不需要返回状态码要返回状态可以用System.exitString[] args是接收命令行参数。这几个修饰符每一个都有存在的理由能把这四个理由讲清楚就说明你是真的懂而不是背的。2.2 八种基本数据类型与包装类存储、范围和经典缓存陷阱Java有八种基本数据类型记住它们的位数、默认值和范围是基本功类型位数默认值取值范围byte80-128 ~ 127short160-32768 ~ 32767int320-2^31 ~ 2^31-1long640L-2^63 ~ 2^63-1float320.0f约 ±3.4E38double640.0约 ±1.7E308char16\u00000 ~ 65535boolean1JVM实现相关falsetrue / false基本数据类型之外每个类型都有对应的包装类Integer、Long、Boolean等。自动装箱机制让int和Integer可以无缝互转但这里藏了一个经典的坑Integer在-128到127之间有一个缓存Integer a 100和Integer b 100比较返回true而Integer a 200和Integer b 200比较返回false因为200不在缓存范围内每次装箱都会new一个新对象。我见过不少初级开发在项目里拿比较Integer结果环境一换数据一变就出诡异bug。记住包装类比较值一律用equals除非你特别清楚自己在做什么。2.3 运算符与表达式短路、自增和那些年踩过的坑运算符分几大类算术运算符 - * / %、关系运算符 ! 、逻辑运算符 || !、位运算符 | ^ ~ 、赋值运算符 - * /、三元运算符条件 ? 表达式A : 表达式B。有几个点值得单独拎出来说。第一逻辑运算符的短路特性左边为false时右边根本不会执行||相反。这个特性经常被拿来考也偶尔会埋bug——如果右边表达式里有副作用比如方法调用、变量自增短路与否结果完全不同。第二自增自减的赋值陷阱int i 0; i i; 这行代码执行完之后i还是0因为右边的i先返回了0然后i自增成1最后再把0赋回给i。这是Java面试里几乎人手一份的经典题很多人背了答案但没理解其实就是“先取值、再自增、后赋值”的顺序问题。第三运算优先级位运算、逻辑运算的优先级很多人记不住我自己的习惯是能加括号就加括号宁可多写两对括号也不让读代码的人去翻优先级表。2.4 流程控制与数组基础功里最容易忽略的细节流程控制无非是if/else、switch、for、while但有几个细节我建议你注意。switch在Java 7之后支持String和枚举在Java 14之后支持switch表达式可以像表达式一样返回值不用再写一堆break代码干净很多。如果你还在用老版本至少记住switch的case里别忘了break否则会穿透到下一个case这种bug排查起来相当痛苦。数组这块很多人以为数组属于“集合”其实它更像是语言内置的数据结构。数组是对象length是属性不是方法所以是arr.length而不是arr.length()。访问越界会抛ArrayIndexOutOfBoundsException。数组声明可以是int[] arr也可以是int arr[]前者是主流风格。要注意的是数组的复制直接赋值是引用复制两个变量指向同一块内存要真正复制内容得用Arrays.copyOf或者System.arraycopy。这些细节看起来小但在并发环境下数组引用共享导致的问题一点都“不小”。3. 面向对象层抽象类、接口、枚举与动态代理3.1 封装、继承、多态面试官为什么总爱从这里开场面向对象三件套封装修的是“细节隐藏”继承修的是“代码复用”多态修的是“面向抽象编程”。面试官开场问这个表面上是考基础实际是看你能不能把三个概念串起来讲。封装的核心是访问修饰符private、default、protected、public范围从类内到包内再到继承者到最后全开放。但别以为封装就是“字段改成private加上getter/setter”。真正的封装是“隐藏实现细节暴露稳定接口”如果一个getter把内部List直接返回出去调用方就能修改你的内部状态这等于没封装。所以返回集合的时候要么返回不可变集合要么返回拷贝。继承有几个硬规则Java是单继承一个类只能有一个父类Object是所有类的根父类重写父类方法时访问权限不能比父类更严返回值可以是父类返回值的子类型抛出的异常不能比父类更宽。多态则体现在运行时绑定上——编译期你用的是父类引用运行期实际调用的却是子类方法。这也是为什么接口、抽象类设计的代码特别灵活的原因。重写equals的时候必须重写hashCode这是一条铁律。Java的HashMap、HashSet都是先比较hashCode快速定位再通过equals判断是否相同。你只重写equals不重写hashCode对象放进HashSet就会“明明内容一样却算两个元素”。这条规则面试常考实操更是天天用到。3.2 抽象类与接口一道经典选择题的现代版答案“抽象类和接口有什么区别、什么时候用哪个”是Java面试八股文里出现频率最高的题目之一。但如果你还停留在“抽象类是单继承、接口是多实现”这种表面答案那面试官大概率会继续追问问到你说不出话为止。抽象类可以有字段、构造器、普通方法、抽象方法它适合表达“is-a”的关系多个类有共同的属性和行为模板比如Animal这个抽象类定义了name字段和eat方法让Dog、Cat去继承。接口在Java 8之后也变强了可以有default方法和static方法Java 9还加了private方法。接口更适合表达“can-do”的契约一个类可以实现多个接口表示它能做多件事比如一个类既实现了Runnable又实现了Serializable。那到底怎么选我的经验是优先接口。因为接口解耦更彻底Spring这类框架也基本都是面向接口编程。但如果你确实需要共享状态、共用的构造逻辑抽象类仍然是正确的选择。另外要提一个冷知识Java不支持多继承核心原因是避免“菱形继承”问题——如果两个父类都有同一个方法子类该继承哪一个会产生歧义。Java 8接口引入default方法后菱形问题以另一种形式回来了一个类实现两个含有同名default方法的接口时必须显式重写这个方法。这个点面试也很爱问。3.3 枚举类型不只是常量的集合而是轻量级状态机“java枚举类型的使用”是搜索热词估计很多人在项目里把枚举用成了常量类。其实枚举比常量类强大得多它本身就是class可以有构造器、字段、方法甚至可以为每个枚举值单独实现抽象方法。我举一个实际例子订单状态机。如果不考虑复杂状态流转用枚举完全够用public enum OrderStatus { CREATED(1, 已创建) { Override public boolean canCancel() { return true; } }, PAID(2, 已支付) { Override public boolean canCancel() { return false; } }, SHIPPED(3, 已发货); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public boolean canCancel() { return false; } }这种方式把状态、描述、行为封装在同一个类型里比到处写if判断常量值干净得多。枚举还可以配合switch用switch的case可以直接写枚举常量。另外一个隐藏知识点枚举是实现单例模式最推荐的方式因为它天然线程安全、反射无法破坏、序列化也安全。这些特性面试的时候都能当加分项讲出来。3.4 动态代理AOP和ORM框架的底层基石动态代理是“java动态代理”热搜背后的核心考点也是理解Spring AOP、MyBatis Mapper原理的关键。先分清楚静态代理和动态代理静态代理是你在编译期就写好一个代理类手动把增强逻辑写死动态代理是运行期在内存中动态生成代理类。JDK动态代理有两个前置条件目标类必须有接口并且调用Proxy.newProxyInstance动态创建代理对象。代理对象实现了目标接口所有方法调用都会经过InvocationHandler的invoke方法你在invoke里可以做日志、事务、权限等增强。比如最简单的耗时统计public class LogProxy { public static Object create(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) - { long start System.currentTimeMillis(); Object result method.invoke(target, args); System.out.println(method.getName() 耗时 (System.currentTimeMillis() - start) ms); return result; } ); } }如果目标类没有接口JDK动态代理就没办法了这时候需要用CGLIB——它基于继承直接生成目标类的子类作为代理。Spring AOP的默认策略是目标类有接口就用JDK动态代理没有接口就用CGLIB。这个地方面试官常问一个问题为什么JDK动态代理必须要求接口因为生成的代理类已经继承了Proxy类Java是单继承代理类没法再继承目标类只能通过接口扩展功能。能讲清楚这层关系说明你是真理解了代理而不仅仅是背概念。4. 集合与流式处理从数据结构到Lambda的一整条链路4.1 集合框架家族谱先弄清谁是谁Java集合框架分两大家族Collection和Map。Collection下面又分List有序可重复、Set不可重复、Queue队列。List的常见实现有ArrayList、LinkedList、Vector、Stack。Set的常见实现有HashSet、LinkedHashSet、TreeSet分别对应哈希表、哈希表加链表、红黑树。Map的常见实现有HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap。这里有个历史遗留概念容易被考到Vector和Hashtable是早期的线程安全集合方法上直接加了synchronized但性能很差现在基本不推荐用了。线程安全的场景应该用ConcurrentHashMap、CopyOnWriteArrayList这些并发包里的类它们通过更细粒度的锁或者无锁方案做到线程安全性能好得多。4.2 ArrayList与LinkedList别把性能口诀当真理背“ArrayList查询快、增删慢LinkedList查询慢、增删快”——这句话是经典的八股答案但实际工程里要打很多折扣。ArrayList底层是Object[]数组通过System.arraycopy实现扩容和移动。LinkedList底层是双向链表每个节点都要存前驱和后继指针。论随机查询ArrayList是O(1)LinkedList是O(n)这个没有悬念。但论插入删除情况就复杂了如果在头部插入LinkedList确实比ArrayList快如果在中间插入ArrayList要搬移半个数组LinkedList要遍历到中间位置两者都慢如果只讨论尾部添加ArrayList均摊是O(1)LinkedList还要创建Node对象反而更慢。再加上CPU缓存局部性数组的遍历速度远高于链表——实际项目中LinkedList的胜场少之又少。还有一个知识点ArrayList的扩容机制。ArrayList默认容量是10每次扩容为原来的1.5倍用Arrays.copyOf复制旧数组。如果你能预估数据量最好在构造时指定初始容量避免频繁扩容。面试官问ArrayList的时候十有八九会追到扩容这里。4.3 HashMap底层机制面试中的重头戏HashMap是Java集合里被问得最多、也是源码最值得读的类之一。JDK 8之后HashMap的底层结构是“数组 链表 红黑树”先按key的hash定位到数组下标如果这个下标上已经有元素就用链表把冲突的元素串起来当链表长度超过8且数组长度大于等于64时链表会转成红黑树把查询从O(n)降到O(logn)。几个核心细节hash计算有扰动处理key.hashCode()的高16位和低16位做异或目的是减少哈希碰撞数组下标是(n - 1) hashn是数组长度因为n是2的幂n-1的二进制全是1与hash做与运算能均匀分布这就是为什么HashMap的容量必须是2的幂默认初始容量16负载因子0.75元素个数达到12时触发扩容扩容时容量翻倍元素重新散列到新数组。JDK 7用的是头插法并发扩容会出现循环链表导致死循环JDK 8改成了尾插法修掉了这个致命问题但并发put仍然会丢数据。所以结论很明确HashMap不是线程安全的并发场景要用ConcurrentHashMap。ConcurrentHashMap在JDK 8里抛弃了JDK 7的分段锁改成CAS synchronized锁Node节点并发粒度更细。这些细节面试时能讲到这个深度基本就能镇住场面。4.4 Stream流式处理list.stream().toArray()只是一个入口搜“java list.stream().toarray”的人多半是想把流的结果转成数组。直接回答list.stream().toArray()返回的是Object[]如果你想要String[]要写成list.stream().toArray(String[]::new)。但是Stream的能力远不止转数组它是Java 8最值得学的特性之一。Stream的操作分三类创建、中间操作、终止操作。创建方式有集合的stream()方法、Stream.of、Arrays.stream等。中间操作包括filter过滤、map映射、flatMap扁平化、distinct去重、sorted排序、limit和skip截断这些操作是惰性的不调用终止操作就不会真正执行。终止操作包括collect收集成集合、toArray、forEach、count、reduce等只有终止操作才会触发整条管道执行。举个例子一行代码搞定“筛选出金额大于100的订单并按时间排序取前5个”ListOrder top5 orders.stream() .filter(o - o.getAmount() 100) .sorted(Comparator.comparing(Order::getCreateTime).reversed()) .limit(5) .collect(Collectors.toList());用Stream写出来的代码语义非常清晰但要注意两个坑第一Stream是一次性的流被消费之后就关闭了不能反复遍历第二parallelStream并行流虽然可以提升性能但如果操作里有共享可变状态、或者任务本身很小反而会因为线程切换变慢。老老实实先写串行流遇到真正的性能瓶颈再考虑并行。4.5 排序算法冒泡和快排的Java实现与面试考点搜“冒泡排序java”和“快速排序java实现”的人应该都在准备笔试或面试手写代码。排序算法是数据结构的基本功冒泡和快排是其中最常考的两种。冒泡排序思路最简单每一轮把相邻元素两两比较大的往后冒经过n-1轮后数组有序。时间复杂度O(n²)是稳定排序。public static void bubbleSort(int[] arr) { 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; } } }这个优化点记录本轮是否发生交换没交换就提前结束是加分项很多面试官会问“冒泡排序能不能优化”。快速排序则是分治思想选一个基准pivot把小于pivot的放左边、大于的放右边然后递归处理左右两半。平均时间复杂度O(n log n)但不稳定。public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } while (i j arr[i] pivot) { i; } if (i j) { int tmp arr[i]; arr[i] arr[j]; arr[j] tmp; } } arr[left] arr[i]; arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }手写快排最容易出错的地方是边界条件和pivot的落位建议多写几遍形成肌肉记忆。而实际生产中直接用Arrays.sort就行——底层针对基本类型用的是双轴快排针对引用类型用的是TimSort性能已经调得足够好没必要自己造轮子但面试要考你原理你还是要会写。5. 并发与线程创建、等待、可见性与线程池5.1 线程创建方式与生命周期别再只会extends ThreadJava创建线程有三种经典方式继承Thread类、实现Runnable接口、实现Callable接口并用FutureTask包装。三者的区别在于继承Thread简单直接但Java是单继承继承Thread就不能继承别的类而且这种方式把任务和线程耦合在一起实现Runnable更灵活可以把任务对象传给线程Callable相比Runnable的优势是能返回结果、能抛异常。线程生命周期有六种状态NEW新建、RUNNABLE可运行、BLOCKED阻塞、WAITING等待、TIMED_WAITING限时等待、TERMINATED终止。从NEW到RUNNABLE必须调用start()方法注意是start()不是run()——直接调run()只会在当前线程里执行方法体等于没开线程。从RUNNABLE可以通过join、wait、LockSupport.park等方法进入WAITING或TIMED_WAITING持有锁竞争失败会进入BLOCKED。5.2 让所有子线程都完成join、CountDownLatch与CompletableFuture搜“java线程等待都完成”的人一般是在做“主线程要等一组子线程全部结束再继续”的需求。这个需求有至少四种解法我把它们讲清楚你按场景选。第一种join()。在主线程里对每个子线程调用join()主线程就会等它执行完多个子线程就逐次join。适合简单场景缺点是每个线程只能等一次灵活性差。第二种CountDownLatch。设置一个初始计数每个子任务完成后countDown()一下主线程await()等待计数归零。适合“等所有人到齐再出发”的场景。int taskCount 3; CountDownLatch latch new CountDownLatch(taskCount); for (int i 0; i taskCount; i) { new Thread(() - { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() 完成); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }).start(); } latch.await(); // 主线程阻塞在这里直到计数归零 System.out.println(全部完成继续主流程);第三种ExecutorService.invokeAll。可以一次性提交多个Callable任务返回一个Future列表invokeAll本身会等待所有任务完成。第四种CompletableFuture.allOf(...).join()。CompletableFuture是Java 8引入的异步编程神器allOf等待所有Future完成配合join()阻塞还能用thenApply链式处理结果是目前最推荐的方式。实际项目里比如导出报表需要并发查多个数据源再合并我一般用CompletableFuture代码简洁还不会被“线程池爆炸”问题困扰。5.3 volatile、synchronized与Lock三个关键字背后的并发三性并发编程有三个核心概念原子性、可见性、有序性。volatile能保证可见性和有序性但保证不了原子性synchronized和Lock能同时保证三者。最典型的例子volatile修饰一个int count两个线程同时执行count结果仍然可能不对。因为count本质上是三步操作读count、加1、写回count三步之间可能被其他线程插进来volatile能保证每一步的可见性但保证不了“三步作为一个整体”不被拆开。要让count原子化得用AtomicInteger、synchronized或者Lock。volatile的正确使用场景是状态标志比如一个volatile boolean running线程A修改它线程B立刻能看到。这个场景用synchronized当然也行但开销大得多。再说synchronized它是可重入的同一个线程可以多次获得同一把锁。而Lock接口提供了更灵活的玩法tryLock()可以尝试获取锁、获取不到就执行别的逻辑lockInterruptibly()可以响应中断避免死锁导致线程永远阻塞。但用手写Lock有个大坑——unlock()一定要放在finally里否则获取锁之后抛异常锁永远不释放程序直接卡死。5.4 线程池核心参数、执行流程与拒绝策略线程池是并发面试的必考大项也是生产环境最容易出问题的地方。ThreadPoolExecutor有七个核心参数参数含义corePoolSize核心线程数即使空闲也保活maximumPoolSize最大线程数keepAliveTime非核心线程空闲存活时间unit存活时间单位workQueue任务队列threadFactory线程工厂用于给线程命名handler拒绝策略执行流程一句话讲清提交任务时先看线程数是否小于核心线程数小于就创建线程执行大于等于核心线程数就放入任务队列队列满了再看线程数是否小于最大线程数小于就创建非核心线程如果已经到最大值了就触发拒绝策略。拒绝策略有四种AbortPolicy默认直接抛RejectedExecutionExceptionCallerRunsPolicy让提交任务的线程自己去执行相当于变相限流DiscardPolicy静默丢弃DiscardOldestPolicy丢弃队列里最老的任务。生产上我一般用CallerRunsPolicy因为它是“业务方自付代价”不会静默丢数据也不会让系统直接崩掉。还有一个高频考点Executors工具类。newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool用起来很方便但《阿里巴巴Java开发手册》不建议用它们原因是很多实现用了无界队列如LinkedBlockingQueue默认是Integer.MAX_VALUE任务堆积会导致内存暴涨。正确姿势是自己new ThreadPoolExecutor显式指定参数和队列大小线程工厂命名也有讲究方便后续排查问题是哪个线程出了故障。6. 进阶战场泛型、异常、反射与JVM高频考点6.1 泛型的类型擦除与通配符为什么List 传不进List泛型的定义不难难在理解Java泛型的实现机制。Java泛型是“伪泛型”——编译期做类型检查运行期把类型信息擦除掉。List 和List 在运行时是同一个类都是裸的List。这也是为什么你不能用instanceof判断一个List是不是List 为什么不能new T[]创建泛型数组。类型擦除还带来一个经典问题List 为什么不能传给List很多人直觉上觉得String是Object的子类那List 也应该是List的子类。但泛型之间没有继承关系两者是独立的类型。要接收任意类型的List得用通配符比如List?表示“某个具体类型的List”。通配符还有个PECS原则上界通配符extends T表示“读”安全适合生产者下界通配符super T表示“写”安全适合消费者。简单记Producer Extends, Consumer Super。这个点面试基本必问因为光背概念没用面试官会给一个add和get的混合场景让你分析能不能编译通过理解了PECS才能稳。6.2 异常体系受检异常与非受检异常的设计之争Java的异常体系根是Throwable下面分两个大分支Error和Exception。Error表示严重系统错误比如OutOfMemoryError、StackOverflowError程序处理不了也不用捕获。Exception分两类受检异常Checked Exception必须显式捕获或抛出比如IOException、SQLException非受检异常RuntimeException不强制处理比如NullPointerException、IllegalArgumentException。设计上受检异常强迫调用方处理异常看起来很合理但实践中有争议——因为受检异常会强制侵入方法签名很多框架比如Spring干脆把所有检查异常包装成RuntimeException抛出去。我的建议是业务代码里自定义异常优先继承RuntimeException配合全局异常处理器统一处理数据库、IO这类底层受检异常在边界处转换成业务异常即可。try-with-resources是Java 7开始的重要语法凡是实现了AutoCloseable的资源文件流、数据库连接、socket都可以写在try后面的括号里代码块结束自动close不用再手写finally关资源。注意如果try块和close都抛异常close的异常会被try块的异常抑制suppressed想看完整信息要用getSuppressed()拿。6.3 反射框架的基石也是性能的黑洞反射是Java框架的基石Spring的依赖注入、MyBatis的结果集映射、各种ORM的字段绑定底层都靠反射。三个获取Class对象的方式类名.class、实例.getClass()、Class.forName(全限定名)。拿到Class之后可以getDeclaredFields()拿字段、getDeclaredMethods()拿方法、setAccessible(true)绕过访问权限、invoke()调用方法。但反射不是没有代价的。第一是性能反射调用方法比直接调用慢很多因为多了方法查找、安全检查、参数装箱拆箱等环节。第二是安全setAccessible(true)能访问private成员打破了封装。第三是可读性差反射代码通常比较绕不好调试。所以在性能敏感的循环里不要用反射框架层面也做了大量优化比如缓存反射对象、用MethodHandle替代传统反射。Java 7引入的MethodHandle性能更好、语义更清晰Java 9又加了VarHandle。面试问到“反射为什么慢”你能说出这几个优化方向比背一句“很慢”值钱得多。6.4 JVM内存区域、类加载与GCJava面试的下半场聊完语言本身Java面试一定会进入JVM环节。先记运行时数据区程序计数器当前线程执行字节码的行号、虚拟机栈局部变量表、操作数栈、本地方法栈native方法、堆对象实例、方法区类信息、常量、静态变量JDK 8之后改为元空间Metaspace不再使用堆内存。堆是GC的主战场分新生代和老年代。新生代又分Eden区和两个Survivor区S0、S1对象优先在Eden区分配Minor GC之后存活对象在两个Survivor区间来回复制熬过一定次数默认15次晋升到老年代。老年代满了会触发Full GC一般伴随着较长的停顿。现在主流的G1收集器和JDK 11之后出现的ZGC都在尽量缩短停顿时间。这一块不用背太细但至少要能说出对象分配的大致流程和为什么要有Survivor区——它是为了避免把存活对象频繁复制到老年代在新生代内部就把大部分朝生夕死的对象回收掉。类加载机制也是高频考点。一个类从加载到使用经历加载、验证、准备、解析、初始化五个阶段。类加载器分三层启动类加载器Bootstrap负责加载JDK核心类、扩展类加载器ExtensionJDK 9之后被平台类加载器替代、应用类加载器Application负责加载classpath下的类。三层之间是双亲委派模型一个类加载器收到加载请求先把请求委派给父加载器父加载器处理不了才自己加载。为什么要双亲委派两个原因一是避免核心类被篡改比如你自己写一个java.lang.String应用类加载器会先让启动类加载器去加载官方版本你的类根本没机会被执行二是避免同一个类被重复加载。这块是送分题一定要把流程和原因都讲清楚。7. 把清单变成行动环境配置、学习路线和面试自查7.1 环境变量配置第一个劝退点也是第一个检查点很多新手从“java下载安装”到“java环境变量配置详细教程”一路搜过来卡在环境变量上。其实现在的JDK安装包在Windows上会自动配置好一部分环境变量你需要手动确认的主要是JAVA_HOME和PATH。JAVA_HOME指向JDK安装根目录比如C:\Program Files\Java\jdk-17。然后在PATH里加一条%JAVA_HOME%\binWindows或者$JAVA_HOME/binLinux、macOS。配置完之后新开一个命令行窗口输入java -version能看到版本信息输入javac -version能看编译器的版本信息两个都有输出就算配好了。CLASSPATH在JDK 5之后其实不需要手动配了很多老教程还在让新手配CLASSPATH配上反而容易出问题直接忽略那几个步骤。常见的坑第一PATH配置时不小心把原来的值覆盖了导致cmd和系统工具都打不开——修改前先把原有PATH复制备份一份第二配完之后还在用旧窗口环境变量不生效一定记得新开窗口第三电脑里装了多个JDK版本java -version显示的不是你刚装的那个因为PATH里前面还有其他JDK的bin目录。项目里建议统一用Maven管理JDK版本或者用IDEA里给项目单独指定JDK避免环境差异引发的诡异问题。7.2 一份按阶段推进的Java学习路线学一段打一段勾特性清单最好的用法是搭配学习路线逐项打勾。我按自己带新人的经验把Java学习拆成四个阶段第一阶段打地基2到4周。目标是把本文第2、3章的内容过完基础语法、数据类型、运算符、流程控制、数组、面向对象、接口、枚举。这个阶段不要跳每个知识点都要写代码验证。很多人语法没看完就急着学框架后面补起来非常痛苦。第二阶段啃核心3到5周。目标是集合框架、异常、泛型、IO、多线程、Stream流。这个阶段建议开始读源码至少把ArrayList、HashMap、ThreadPoolExecutor这几类的核心方法读一遍。读源码一开始很慢但读完之后面试和写项目的底气都会不一样。第三阶段进框架4到8周。学习Maven和Git这两个工具然后学MySQL、Spring Boot、MyBatis。到这个阶段你会发现前面打的地基全都派上了用场Spring的依赖注入要理解代理和反射MyBatis的Mapper要理解动态代理事务管理要理解AOP。第四阶段持续进阶长期。JVM调优、并发编程实战、设计模式、读Spring源码、刷leetcode。到这个阶段特性清单上的内容已经不再是“学过”而是“用过、调过、能够给别人讲清楚”。每学完一个阶段回来看一眼清单给自己打一遍分这个过程本身就是最好的复习。7.3 面试前的三张自查表对着清单做最后一次扫盲面试前最后三天不建议再看新知识而是用清单做一轮“扫雷”。我习惯把高频考点整理成一张表每个主题对应几个追问逐条自测主题高频追问自评分语法基础标识符规则、与equals、自增陷阱、switch特性1-3级面向对象接口vs抽象类、重写规则、枚举行为、动态代理原理1-3级集合HashMap底层与扩容、ArrayList扩容、Stream用法1-3级并发创建线程方式、等待线程完成、volatile局限、线程池参数1-3级JVM内存区域、对象分配、双亲委派、G1特点1-3级新特性Lambda、Stream、Optional、Java 11到17的关键变化1-3级自评分只有1级见过的回去把对应代码敲一遍升到2级面试高频的再尝试口头讲一遍升到3级。这里分享一个我自己一直在用的办法假装对面坐着一个完全不懂Java的人把清单里的每一条讲给他听讲的过程中卡壳的地方就是你的盲区。这个方法比任何刷题软件都有效因为它逼着大脑做“提取练习”而不是被动的“再认练习”。带过不少新人和社招候选人我自己面试别人的时候最怕的也不是某道八股文没答上来而是对方每句话都是背的一问“为什么”就卡壳。所以这份清单不是拿来背的是拿来一遍遍过、一遍遍给自己挑刺的。挑出来的刺少了你离“真正掌握Java”这个目标也就真的不远了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价