拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java核心知识系统回顾:语法、集合、反射代理与并发实践

我先说个真实的片段。前阵子帮一个老朋友看代码他写 Java 写了四年结果被一道面试题问住了Integer a 100; Integer b 100; System.out.println(a b);输出什么他犹豫了很久说“应该是 false 吧对象比较要用 equals”。实际上输出是true因为Integer在-128到127之间走了缓存。这个事让我意识到一个问题我们平时写业务代码框架帮我们挡住了太多底层细节很多 Java 的核心知识其实一直处于“用过但没懂”的状态。于是我做了一次完整的 Java 回顾把语法、集合、并发、代理机制全部重新过了一遍这篇文章就是这次回顾的完整记录。无论你是准备面试、从其他语言转回 Java还是写了好几年业务代码想补地基这篇文章都值得你从头到尾看一遍。我不会按教科书顺序讲而是按照“环境准备 → 语法细节 → 集合与算法 → 反射与代理 → 并发协作”这条实际复盘的路径走每部分都会带上我在排查问题时的真实经历。1. 为什么现在值得把 Java 重新翻出来过一遍1.1 语言在变知识体系也在膨胀很多人对 Java 的印象还停留在 Java 8实际上从 Java 9 开始这个语言的发布节奏已经从“几年一更”变成了“半年一更”。Java 17、Java 21 都是长期支持版本LTS新的特性如record、sealed class、虚拟线程都在逐步落地。但问题是绝大多数生产项目还是跑在 Java 8 或者 Java 11 上这就造成了一种“语言在进化开发者的知识体系还停在原地”的错位。我回顾的时候先做了一件事把最近几年的热门搜索词拉出来看了一遍。java基础、java面试八股文、java动态代理、java线程等待都完成、java环境变量配置这些词出现的频率特别高。这说明什么说明大量开发者其实一直在“补课”。不是说这些知识多难而是日常工作里很少有机会系统地用一遍等真要面试或者排查问题的时候就卡住了。1.2 回顾不是重学是给知识做一次重构我特别反对一种做法从《Java 编程思想》第一页开始重新啃。那是重学不是回顾。重学的效率太低因为你已经写过大量业务代码很多基础概念你其实已经会用了只是没有形成体系。回顾的正确姿势是先画一张自己的知识地图把过去几年写过的代码、踩过的坑、看过的源码片段全部映射到这张地图上。语法层面、集合层面、并发层面、JVM 层面、框架层面每一层你都知道大概有哪些东西然后针对自己的薄弱点去定点突破。我在这次回顾里就发现自己对集合框架里的底层数据结构理解很浅对动态代理只停留在“Spring AOP 用了它”这个层面这次正好补上了。1.3 这次回顾适合谁先说清楚这篇文章不适合完全零基础的人如果你连class和main方法都还没写过建议先去跟一套完整的入门课程。这篇文章适合以下几类人写过一到两年 Java 业务代码但觉得自己基础不扎实的准备跳槽面试需要快速把八股文和实际代码对应起来的前端转后端刚刚开始接触 Java 生态想建立整体认知的很久没写 Java需要快速捡起来的老开发者。下文所有内容都是可落地的代码和可复现的排查思路不是概念堆砌。2. 环境这关不过后面全是坑安装、环境变量与常见报错2.1 JDK 版本怎么选别被“最新版”带偏很多人一上来就问“是不是装 JDK 21 最好”我的回答是看你的目标。如果是为了学新特性、做个人项目装最新的 LTS 版本没问题如果是为了找工作那大概率跑不掉 Java 8 或 Java 11 的项目建议本机至少保留一个 JDK 8 和一个 JDK 17用的时候切换。别装什么“最新预览版”预览特性在生产环境根本没人用学了也是浪费时间。JDK 的下载也顺便说一句认准官方渠道下载.tar.gz或.msi安装包。安装路径不要带中文和空格这是我见过最多人踩的坑。我见过有人装在C:\Program Files\Java\jdk-17然后环境变量配置一直报错最后发现就是路径里的空格在搞鬼。虽然理论上加引号能解决但何必给自己添堵。2.2 环境变量配置JAVA_HOME、PATH 与 CLASSPATH 的误区环境变量这块是搜索热词里的常客java环境变量配置详细教程每个月都有人搜。我复盘的时候发现很多教程还在教配置CLASSPATH这其实是个历史遗留误区。从 JDK 5 开始CLASSPATH就不需要手动设置了如果当前目录下能找到类文件JVM 默认会去当前目录找。手动设置CLASSPATH反而容易引发各种奇奇怪怪的 ClassNotFoundException。现在的标准做法只有两个变量# Linux / macOS编辑 ~/.bashrc 或 ~/.zshrc export JAVA_HOME/usr/local/jdk-17 export PATH$JAVA_HOME/bin:$PATH# Windows系统属性 - 环境变量 JAVA_HOME C:\Program Files\Java\jdk-17 PATH %JAVA_HOME%\bin;...配置完在终端里敲java -version javac -version两个命令都能正常输出版本号就说明环境没问题了。注意java和javac都要验证只验证一个不够因为有些 IDE 会自带 JRE导致命令行能跑java但编译不了。2.3 两个高频报错的实际排查exit code-1 与乱码搜索热词里有一条myeclipse2020 java was started but returned exit code-1这种报错我印象很深。它本质上不是 Java 代码的错误而是 JVM 启动失败。常见原因有两个第一JDK 版本和 IDE 不匹配。比如 MyEclipse 2020 默认要求 JDK 8 以上但你配置的 JRE 是 JDK 11 甚至更高某些老版本插件会崩溃。第二eclipse.ini里的内存参数配置过大电脑物理内存不够JVM 起不来。排查方法很简单打开 IDE 安装目录下的eclipse.ini看-Xmx后面的数值如果比你的物理内存一半还大调小它。另一个高频问题是vscode运行java报错乱码。这个几乎都是编码不一致导致的。VS Code 默认用 UTF-8 读取文件但 Windows 的控制台和某些老项目用的是 GBK于是中文注释全变成了乱码。解决方式是在.vscode/settings.json里显式声明{ java.debug.settings.consoleEncoding: UTF-8, terminal.integrated.defaultProfile.windows: Command Prompt, files.encoding: utf8 }如果是老项目源码本身是 GBK 编码那就别强行用 UTF-8 打开改为 GBK 读取并重新保存。编码问题核心就一句话保证文件编码、编译器编码、控制台编码三者一致。3. 语法地基标识符、运算符与枚举八股文的常客也是漏网之鱼3.1 标识符命名规则面试第一题的隐藏考点搜索热词里有java标识符命名规则这种题看起来简单实际很多人会栽。我回顾的时候重新梳理了一次规则其实就五条标识符由字母、数字、下划线_、美元符$组成不能以数字开头不能是 Java 关键字或保留字比如class、int、goto、const严格区分大小写Name和name是两个完全不同的标识符理论上来讲汉字可以作为标识符但你最好不要这么写没有任何好处。面试里经常变的考法是问“下面哪个是合法标识符”比如123abc不合法_abc合法$abc合法class不合法。这些都是死规则背下来就行。但我更想说的是规则是底线规范才是日常该遵守的。类名用大驼峰方法名和变量名用小驼峰常量用全大写下划线分隔。不要为了省事把变量命名为a、b、tmp回顾代码的时候你会发现最贵的代码不是写出来的是读出来的。3.2 运算符与表达式、equals和位运算的细节运算符这块我重点回顾了三类第一与equals的区别。在比较引用类型时比较的是对象地址equals默认也是比较地址但String、Integer这些类重写了它变成比较值。开头说的Integer缓存就是最典型的陷阱。记住基本类型用对象比较一律用equals拿不准的时候用Objects.equals它可以帮你处理 null 的情况。**第二、||与、|的区别。**很多人知道前者有短路效应却不知道后者按位运算时不会短路。例如int a 5; int b 10; // 短路左边为 false右边不执行 boolean result1 (a b) (b 0); // 非短路左边为 false右边依然执行 boolean result2 (a b) (b 0);代码里如果出现、|做逻辑判断而且右侧有方法调用执行次数会不符合预期。我在代码评审里抓过好几次这种问题。第三位运算。、、分别对应左移、右移、无符号右移。面试八股里常问“如何用位运算判断奇偶”“如何用位运算交换两个数”。实际业务中位运算主要用在权限标记、状态位合并这些场景。比如用一个int同时表示多个布尔状态每个 bit 对应一个开关这在写配置类的时候非常实用。3.3 枚举不是简单的常量列表它是一等公民搜索热词里java枚举类型的使用也是高频我这次回顾的最大收获之一就是重新认识了枚举。很多人写枚举就是列几个常量public enum Status { PENDING, APPROVED, REJECTED }但枚举本质上是继承自java.lang.Enum的类它可以有字段、构造方法、抽象方法甚至可以实现接口。我在实际项目里比较推荐这种写法枚举内部维护描述和操作码方便前端展示和后端逻辑复用。public enum OrderStatus { PENDING(0, 待支付) { Override public boolean canCancel() { return true; } }, PAID(1, 已支付) { Override public boolean canCancel() { return false; } }; private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public abstract boolean canCancel(); public static OrderStatus fromCode(int code) { for (OrderStatus status : OrderStatus.values()) { if (status.code code) { return status; } } throw new IllegalArgumentException(未知状态码: code); } }这种写法的好处是状态本身的逻辑内聚在枚举里调用方不需要写一堆if判断。还有一个很多人不知道的细节枚举天然适合实现单例因为 JVM 保证了枚举实例只会被加载一次而且是线程安全的。用枚举实现单例是最简洁也最安全的方式。4. 集合框架与 Stream从“会用 API”到“看懂设计”4.1 Collection 与 Map 的整体脉络Java 集合框架是面试必考、日常必用、但也最容易被当成“字典”使用的部分。回顾的时候我画了一张脑图把集合分成两条主线一条是Collection下面分List有序可重复、Set无序不可重复、Queue队列。另一条是Map负责键值对存储HashMap、TreeMap、LinkedHashMap各有所长。很多人背概念背得很熟一写代码就乱用。比如需要保持插入顺序却选了HashSet结果遍历顺序每次都不一样。我建议记三条经验需要有序且允许重复用ArrayList需要去重用HashSet如果还要保持插入顺序用LinkedHashSet需要按键排序用TreeMap如果还要保持插入顺序用LinkedHashMap。这玩意儿没什么高深的就是用的时候多想一步“我这个场景的核心诉求是顺序、去重还是查找速度”。4.2 数据结构和算法从 ArrayList 到 HashMap 的底层选择搜索热词里冒泡排序java和快速排序java实现都是常客说明大家面试前确实在突击算法。但我回顾之后最大的感触是如果理解了底层数据结构手写排序算法根本不是背的是推出来的。ArrayList底层是数组所以按索引访问是 O(1)但中间插入删除要移动元素是 O(n)。LinkedList底层是双向链表插入删除快但按索引访问是 O(n)。这两个选型搞明白了很多性能问题其实不用等压测就能预判。HashMap更值得聊。JDK 8 之后它的底层是“数组 链表 红黑树”。默认初始容量是 16负载因子是 0.75。当哈希冲突严重、链表长度超过 8 且数组长度超过 64 时链表会转成红黑树把查询时间从 O(n) 降为 O(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 temp arr[i]; arr[i] arr[j]; arr[j] temp; } } arr[left] arr[i]; arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }写的时候注意边界条件特别是i j这个判断漏掉会数组越界。我建议你在自己的 IDE 里跑一遍打断点看每一轮移动的位置这个比背十遍八股文都管用。4.3 Stream 里最容易翻车的 toArray 与排序实现搜索热词里有java list.stream().toarray这个点看着小翻车率却极高。我直接说结论ListString list Arrays.asList(a, b, c); Object[] objArray list.stream().toArray(); // 返回 Object[] String[] strArray1 list.stream().toArray(String[]::new); // 推荐 String[] strArray2 list.toArray(new String[0]); // 推荐toArray()无参版本返回的是Object[]强转成String[]会抛ClassCastException。很多人查半天才发现是这里的问题。正确姿势是传入String[]::new让流在收集时就知道目标类型。Stream 的排序也顺带回顾一下。stream().sorted()默认自然序字符串按字典序数字按数值序。自定义排序要传Comparatorlist.stream() .sorted(Comparator.comparing(User::getAge).reversed()) .collect(Collectors.toList());这个链式写法要注意reversed()的位置它是作用于前一个Comparator的结果所以放在最后就是对年龄倒序。还有一点sorted()不会改变原集合它返回一个新的流如果用完不接收结果等于白排序。5. 反射与动态代理读懂 Spring 的钥匙5.1 反射机制的三个操作层级java动态代理是搜索热词里的常客但很多教程上来就直接讲代理把反射给跳过了。我回顾的心得是代理只是反射的一个应用不理解反射的底层看动态代理代码就像看天书。反射的操作层级我分为三层第一层拿到Class对象。三种方式类名.class、对象.getClass()、Class.forName(全限定名)。forName会触发类的静态初始化面试里常考这个副作用。第二层拿到构造方法、方法和字段。getDeclaredMethod可以拿到私有方法但调用前要设置setAccessible(true)否则会抛IllegalAccessException。注意getMethod只能拿 public 方法包括继承来的getDeclaredMethod只能拿本类声明的所有方法不包括父类的。第三层真正调用。method.invoke(obj, args)如果方法本身是静态的第一个参数传 null 就行。我写一个典型的反射调用代码Class? clazz Class.forName(com.example.User); Object user clazz.getDeclaredConstructor().newInstance(); Method setName clazz.getDeclaredMethod(setName, String.class); setName.invoke(user, 张三); Field nameField clazz.getDeclaredField(name); nameField.setAccessible(true); System.out.println(nameField.get(user));这里有个很容易踩的坑clazz.getDeclaredConstructor().newInstance()是 JDK 9 之后推荐的写法以前写的clazz.newInstance()在新版本已经被标记为废弃而且它要求类必须有 public 无参构造方法。5.2 JDK 动态代理与 CGLIB 的选型逻辑动态代理有两种实现这是面试高频点。JDK 动态代理基于接口核心接口是InvocationHandler核心类是Proxy。CGLIB 基于继承通过生成目标类的子类来代理。JDK 代理的代码模板基本是固定的public interface UserService { void addUser(String name); } public class UserServiceImpl implements UserService { Override public void addUser(String name) { System.out.println(添加用户: name); } } public class LogProxy implements InvocationHandler { private final Object target; public LogProxy(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(调用前: method.getName()); Object result method.invoke(target, args); System.out.println(调用后: method.getName()); return result; } } // 使用 UserService service new UserServiceImpl(); UserService proxyInstance (UserService) Proxy.newProxyInstance( service.getClass().getClassLoader(), service.getClass().getInterfaces(), new LogProxy(service) ); proxyInstance.addUser(张三);CGLIB 的代码形态不同但核心逻辑一样都是生成一个代理类在调用目标方法时插入增强逻辑。Spring 的选择逻辑是如果目标类实现了接口默认用 JDK 动态代理如果没有实现接口就用 CGLIB。Spring Boot 2.x 之后默认强制使用 CGLIB原因很简单就算有接口也用 CGLIB可以避免很多类型转换的坑。我自己回顾的时候手动写了一遍 JDK 动态代理又对比着看了一遍 CGLIB 的字节码生成逻辑瞬间就理解了为什么 Spring AOP 对接口代理和类代理的行为会有差异。这东西光看不动手永远只能背结论。5.3 动态代理的经典应用场景动态代理不只是面试题框架里到处都是它的影子Spring AOP事务管理、日志切面、权限校验底层就是动态代理MyBatisMapper 接口没有实现类却能直接注入调用就是因为 MyBatis 用 JDK 动态代理生成了接口的代理实现Retrofit网络请求框架里把接口方法转换成 HTTP 请求也是动态代理数据源路由、分布式锁的注解切面基本都是同一套路。我实际项目中用得最多的场景是给老系统加操作日志又不愿意在每个方法里手动写日志代码就写了一个代理切面统一处理。这让我深刻体会到一个道理动态代理的本质是对方法的包装它的价值不在于你写了多少代理代码而在于你把“和业务无关的横切逻辑”从业务代码里抽了出去。6. 线程等待的几种姿势join、CountDownLatch 与 CompletableFuture6.1 join最朴素的等待却经常被忽略搜索热词里有一条java线程等待都完成这个场景很典型主线程要等几个子线程全部执行完再继续往后走。最原始的写法就是Thread.join()。Thread t1 new Thread(() - System.out.println(任务1完成)); Thread t2 new Thread(() - System.out.println(任务2完成)); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(所有任务执行完毕);join()的作用是让当前线程阻塞等待被调用的线程终止。它的实现原理是调用了wait()所以会抛出InterruptedException。这里有个细节join()有个重载版本join(long millis)可以指定最多等多久防止某个线程卡死导致主线程永远等下去。我建议在实际项目里一律使用带超时时间的版本别问为什么问就是线上线程卡死过。6.2 CountDownLatch一批任务等一个信号join理解起来容易但适用场景有限。如果线程是由线程池管理的你根本拿不到线程对象就调不了join。这时候更通用的方案是CountDownLatch。int taskCount 5; CountDownLatch latch new CountDownLatch(taskCount); ExecutorService executor Executors.newFixedThreadPool(3); for (int i 0; i taskCount; i) { executor.submit(() - { try { // 模拟任务处理 Thread.sleep(500); System.out.println(子任务完成); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(5, TimeUnit.SECONDS); System.out.println(所有任务完成或等待超时);用CountDownLatch时最大的坑是把countDown()放在finally里。因为如果任务抛异常计数器不减主线程就会一直阻塞。await也建议用带超时的版本道理和join一样。我还遇到过一种情况任务总数和实际提交数不一致比如循环里提前return了导致计数器永远归不了零这种 bug 排查起来相当隐蔽必须靠日志和超时兜底。6.3 CompletableFuture现代 Java 并发的最佳入口如果你还在用原生线程和CountDownLatch写并发我建议你升级到CompletableFuture。它是 Java 8 引入的把“异步任务编排”这件事做得非常优雅。同样一个“等所有任务完成”的场景用CompletableFuture写是这样ListCompletableFutureString futures new ArrayList(); for (int i 0; i 5; i) { int taskId i; CompletableFutureString future CompletableFuture.supplyAsync(() - { // 模拟耗时任务 try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return 任务 taskId 完成; }); futures.add(future); } CompletableFutureVoid allDone CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); // 阻塞等待也可以加超时 allDone.get(5, TimeUnit.SECONDS); // 等全部完成后汇总每个任务的结果 ListString results futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList());这里有个容易犯的错直接对单个future.get()会导致按顺序阻塞失去并发效果。正确的做法是先用allOf统一等待再通过join取结果。allOf返回的也是CompletableFuture它本身不携带结果结果要从每个子 future 里取。CompletableFuture真正强大的是链式编排比如thenApply、thenCompose、exceptionally可以写出一条清晰的异步流水线。我回顾后把项目里很多“回调嵌套 手动计数”的代码都重构成了CompletableFuture代码量减少一半可读性大幅提升。7. 回顾之后我给自己搭了一套知识地图7.1 按“地基—核心—进阶—应用”四层组织这次回顾到最后我深刻体会到一件事知识不梳理就是一盘散沙。于是我按四层结构给自己做了一张知识地图第一层地基。变量、数据类型、运算符、流程控制、数组、方法。这一层是纯语法花不了多少时间但必须确保没有盲点比如运算符优先级、基本类型和包装类型的区别。第二层核心。面向对象封装、继承、多态、接口、抽象类、异常机制、常用类库String、集合框架、日期时间。这一层是业务代码里用得最多的面试题也基本围绕这一层展开。第三层进阶。泛型、反射、注解、IO/NIO、多线程与并发、JVM 内存模型、垃圾回收。这一层决定了你到底是“会写 Java”还是“懂 Java”。第四层应用。Spring 全家桶、MyBatis、数据库、缓存、消息队列。这一层是框架生态更新换代快但要记住框架是建立在第二层和第三层之上的地基不稳框架学得再多也是空中楼阁。7.2 输出倒逼输入Demo、笔记和面试题整理回顾过程中我发现光看和光听的记忆留存率极低真正有效的是“输出”。我这次的产出物有三个第一每个核心知识点都写了一个最小可运行的 Demo。比如动态代理、枚举加状态机、CompletableFuture编排每个 Demo 都在本地跑过跑通之后贴上关键注释。这些代码以后就是我的“代码备忘手册”。第二把自己容易混淆的点做了一张对照表。比如HashMap和Hashtable、ArrayList和LinkedList、sleep和wait、和equals、submit和execute每次回顾只需要看这张表就能快速唤醒记忆。第三把面试题当成项目来拆解。搜热词的时候看到java面试八股文和java面试大全及答案我没有直接去背而是挑了几道核心题比如“HashMap 的 put 流程”“说一下动态代理的实现原理”“如何让多个线程按顺序执行”然后自己在 IDE 里敲代码验证。事实证明能讲清楚和能写出来是两码事。7.3 我的几个实操建议最后分享几个这次回顾得出的实操建议。第一别贪多。每天专注一个点比如今天只搞懂HashMap的底层结构明天只写一个动态代理一个月下来能积累 30 个扎实的知识点。比一个月看完三本书要强得多。第二一定要动手跑代码。看十遍动态代理的博客不如自己写一个InvocationHandler调一次。很多以为自己懂了的知识一动手就露馅。第三善用官方文档和源码。遇到不懂的类先看 Java API 文档再直接跳进源码看实现不要先搜博客。源码是最准确的一手资料博客反而可能带着作者自己的理解偏差。第四定期复习。这个事别只做一次。我建议每半年花一周时间做一次回顾把半年里写的代码、踩的坑重新映射到知识地图上看看哪里又出现了盲区。这次系统回顾 Java对我来说最大的收获不是记住了多少知识点而是重新建立起了对这门语言的整体掌控感。写业务代码的时候不再只是知道“这样写能跑”而是知道“为什么这样写是合理的、它的底层发生了什么”。这种感觉和单纯的熟练是完全不同的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门