十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java基础复习指南:从HashMap到反射动态代理的底层原理与实战排查

Java基础复习指南:从HashMap到反射动态代理的底层原理与实战排查 复习Java基础这件事最尴尬的不是“不会”而是“好像会了但一追问就露馅”。我前段时间给一位做Java开发两年多的朋友做面试模拟特意挑了几道看起来人畜无害的题比如“String和StringBuilder到底差在哪”“HashMap的get方法链路是什么”结果发现平时写业务写得很顺手的人问到这些底层细节时照样会卡壳。这其实不怪他日常开发里CRUD占大头我们记住的往往是“能用就行”而不是“为什么这么用”。但越往后面试、越往架构方向走底层基础就越是硬通货。这篇Java基础复习的内容我按自己实际带人和复习的经验重新梳理了一遍没有打算把大学教材搬上来而是围绕那些面试题、八股文、以及日常开发里真正容易踩的坑来倒推基础知识点。涵盖运算符、字符串、面向对象、集合容器、反射与动态代理外加一套从环境配置到JVM报错的实际排查链路。既有原理层面的解释也有可以直接照着操作的部分适合正在准备Java面试的人也适合工作中想系统补课的同学。1. 为什么我建议用“先找坑再补课”的方式复习Java基础很多人的复习路径是打开一本书从第一章看到最后一章或者收藏一篇“几千道面试题”从头背到尾。我试过效果很差。前几章语法还能看进去到了集合、并发、反射就开始走神最后记下的只有零散的术语遇到真实问题照样反应不过来。后来我换了思路复习的节奏从“按章节推进”改成“按问题反查”。你先去看面试题里高频出现的那些关键词比如java动态代理、java锁面试题、HashMap底层、反射怎么用、RedisTemplate的increment为什么会报错然后顺着这些关键词回到基础语法和编码实践里找答案每解决一个问题就等于把一个知识点彻底钉死了一次。这种方式的好处是你始终带着“我在解决一个真实疑问”的状态去复习而不是被动接收信息。真正把Java基础复习做透的人普遍遵循三条主线语法层运算符、表达式、字符串、标识符、流程控制这是写代码的工具箱。对象层面向对象三大特性、接口与抽象类、JavaBean规范这是组织代码的方法论。框架层集合、反射、动态代理、排序算法、并发锁机制这是理解所有框架和中间件的敲门砖。这三条线不是平行的而是层层支撑。集合要用到equals和hashCode反射要理解类加载动态代理要理解接口和多态锁要理解对象头和内存模型。复习的时候如果能顺着这条依赖链往下走你回头看自己写的业务代码会突然看懂很多框架自动帮你做的事这是基础复习最有复利效应的部分。另外别小看那些“看起来很简单”的点。热搜里那些报错——NoClassDefFoundError、OutOfMemoryError、数组越界、Lombok编译器版本不匹配、JavaBean大写字母属性JSON序列化变成小写——每一个背后都对应一个基础知识点。它们是基础不牢的“漏电保护器”你每遇到一次就是一次免费的复习机会。把这套“遇坑→反查→归档”的流程跑熟了比单纯刷题高效太多。2. 运算符、字符串与基础语法先避开那些“自以为会了”的地雷语法层面最容易翻车的不是写不出来而是写出来之后行为和预期不一致。这里说的不是冷门写法而是我们天天在用的东西。2.1 运算符优先级与表达式陷阱java运算符和表达式这一块面试出现的频率极高而且经常是那种“你觉得很简单但其实有坑”的题。最容易踩的坑有这几个赋值运算符不换行a a这类写法结果是a没变因为的后置运算是在赋值之后才自增的。短路与和短路或和||一旦确定了最终结果右边的表达式就不再执行很多空指针就是从这省出来的。反过来和|作为位运算时是两边都会计算完的用途完全不同。位运算和逻辑运算混淆if (flag 1 0)这种写法看着能跑但语义上是位运算符用在boolean上虽然可以可读性却很差代码评审时容易引发争议。字符串拼接用的本质是每次拼接都new一个StringBuilder循环里用拼接大量字符串会严重影响性能。我在复习时通常会让对方手写一个“用三目运算符嵌套实现等级判断”或者“交换两个变量且不使用临时变量”这类小题。表面看是考语法实际是在考运算符的优先级、结合方向、以及位运算的功底。真正要把Java基础复习到位运算符不能只停留在知道 - * /而是要清楚%对负数的行为、和与的区别以及在比较引用和比较数值时的语义差异。2.2 字符串多行写法与String家族的选择“Java字符串多行写法”能成为热搜词说明很多人已经遇到Java13之前面对长文本时那种反人类的转义拼接了。其实从Java 15开始文本块Text Blocks成为正式功能用三个双引号包裹内容可以保留换行和缩进写SQL、JSON模板再也不用一行一个加号。复习字符串时我建议把重点放在三类对象的选择逻辑上String不可变适合常量、不频繁修改的场景。StringBuilder非线程安全适合方法内部的字符串拼接单线程环境下性能最优。StringBuffer线程安全通过同步方法保证安全但性能比StringBuilder差日常单线程场景基本用不上它。另外一定要搞清楚intern()和常量池的关系。很多人理解String a abc和String b new String(abc)的区别但说不清常量池里的字符串是如何被复用的。这个点是后面理解HashMap的hashCode、equals逻辑的基础也是很多坑的源头。还有一个小点经常被忽略字符串的不可变性带来的安全问题。你永远不用担心一个被传入方法里的String在方法内被修改这就是为什么它适合作为HashMap的key。反观如果你自己定义的可变对象做keyhashCode变了HashMap就再也get不到原来的value了。这也是为什么面试官总喜欢追问“为什么String适合作为HashMap的key”。2.3 数组越界、标识符命名与lambda函数数组越界异常ArrayIndexOutOfBoundsException几乎是所有Java新手遇到的第一个运行时异常。它不只是“索引写大了”这么简单背后是对数组内存结构的理解数组在JVM中是一段连续内存索引本质是内存偏移量所以索引小于0或者大于等于length都会触发越界。复习时最好顺手看一下for-each循环在遍历数组时的实现原理以及Arrays.asList()返回的对象为什么不能调用add方法这些细节都属于基础语法但又是高频面试点。Java标识符命名规则也很基础但不少人已经记混了标识符可以以字母、下划线、美元符号开头不能以数字开头不能是Java关键字且大小写敏感。很多新手在变量名里用中文拼音缩写、数字开头虽然有些在JVM层面也许能编译通过比如“$”开头合法但不推荐但在团队协作中却是灾难。更严谨的规则是遵循驼峰命名、见名知义这已经不光是语法问题而是职业素养。lambda函数这块Java基础复习一定要把它放到“函数式接口”的上下文去理解不要只记()-{}这种语法。Lambda表达式的本质是“把方法当作参数传递”它在集合stream操作、线程Runnable创建、Optional处理里用得极多。复习到lambda时建议顺便把java.util.function包下的常用接口过一遍尤其是Function、Supplier、Consumer、Predicate后面看Spring、MyBatis源码时你会发现到处都是它们的影子。3. 面向对象与JavaBean规范继承、多态以及那个“大写属性变小写”的坑面向对象是Java最核心的基础面试时几乎每个问题最后都能绕回到封装、继承、多态。很多人在面试前能背出定义可一旦结合项目场景就不知道怎么表达。我建议复习时就事论事把每一个特性放到真实问题里去理解。3.1 封装、继承、多态的工程视角封装的核心是“隐藏实现细节暴露稳定接口”。为什么JavaBean要求私有字段、提供getter/setter因为它对外暴露的是一套规范化的访问方式而不是直接暴露字段本身这样内部字段类型或存储方式变化时不会影响调用方。继承是面向对象设计里最容易被滥用的一块。过度继承会导致类层次过深牵一发而动全身。工程上公认的原则是“组合优于继承”。比如一个PlayerService如果既想记录日志又要做权限校验与其让它继承一个BaseService把日志和校验逻辑塞进去不如用装饰器模式或AOP把横切逻辑剥离出来。这也是Spring框架为什么能大行其道的原因——它就是为解决这种继承爆炸而生的。多态要理解的不是“父类引用指向子类对象”这句话而是分派机制编译期看的是声明类型运行期看的是实际对象类型。Java动态绑定的背后是方法表和方法分派这也是为什么接口编程能提高扩展性的原因。写代码时面向接口、不面向实现换实现类时调用方代码一行不用改就是这个原理落在工程里的价值。3.2 重载与重写别再傻傻分不清面试题里几乎必问重载和重写的区别但很多人回答得不够准确重载同一个类里方法名相同、参数列表不同返回值不影响重载判定它发生在编译期。重写子类对父类方法重新实现方法名和参数列表必须一致返回值可以协变访问权限不能比父类更严格它发生在运行期。还有一个容易混淆的点调用重载方法时编译期就会确定调用的具体是哪个方法而重写方法则要到运行时根据对象实际类型来决定。很多人以为Overload也是多态严格来说它属于编译期的静态分派不算运行时多态。复习到这一步建议动手写一点代码验证一下比干背定义牢固得多。3.3 JavaBean属性大小写陷阱与JSON序列化问题热搜里有条“Java Bean大写字母开头的变量JSON时就变成小写了”这绝对是面向对象复习里最值钱的实战问题。原因在于JavaBeans规范当一个属性名是name时默认的getter方法叫getName()setter叫setName()但如果字段是URL这种全大写缩写JavaBeans规范规定属性名的判定规则会取“首字母大写如果第二个字母也大写则保留原样”的逻辑导致Introspector识别出来的属性名变成URL和uRL不统一处理一旦走默认规则Jackson序列化时就容易把字段名转成小写。解决办法有好几种字段上使用JsonProperty(URL)显式指定JSON字段名或者配置Jackson的命名策略。这个坑在对接第三方接口时极其常见因为对方的字段全是全大写的缩写。看起来是工具使用问题根子上却是不理解JavaBean规范。复习面向对象时能把这个点讲明白比背十道“封装继承多态”概念题都有说服力。3.4 接口与抽象类的选择逻辑接口和抽象类的区别八股文里爱列成一条条的对比表其实核心就一句话抽象类是“是一个”关系接口是“具备某种能力”关系。更准确地说抽象类把公共的一段代码提取出来让子类复用接口定义的一组契约要求实现类必须提供这些能力。Java 8之后接口里可以写default方法这两者的边界又近了一步但设计语义仍然不同。在选择上我的经验是如果你要做的是模板方法比如一套流程中某一步固定、某一步需要子类自定义优先考虑抽象类如果你要定义一组能力让不同业务方向各自实现优先接口。实际项目中“抽象类实现接口”的组合也很常见比如Spring的ApplicationListener多个适配器类先实现一个空接口再继承一个Adapter抽象类这就是接口加抽象类配合的经典用法。4. 集合与容器从ArrayList到HashMap把八股文翻译成代码直觉java集合和java容器是Java基础复习里信息量最大的一块。一方面因为它太常用了另一方面因为集合框架的设计本身就是Java基础的大汇总——泛型、数组、链表、红黑树、哈希、迭代器、快速失败全都融在里面。4.1 Collection体系与实现选择集合框架分成两大派系Collection和Map。Collection下又有List、Set、Queue。面试里高频的对比大概就这几组对比对象底层结构适用场景关键区别ArrayList vs LinkedList动态数组 vs 双向链表随机访问多选ArrayList频繁插入删除在头部两段选LinkedListArrayList扩容倍数1.5LinkedList额外内存开销大HashSet vs LinkedHashSet vs TreeSetHashMap vs LinkedHashMap vs TreeMap去重、维护插序、排序一个无序、一个按插入序、一个按自然序/比较器HashMap vs Hashtable vs ConcurrentHashMap哈希表单线程、同步、高并发null键值支持、线程安全、锁粒度完全不同复习集合的时候不要只背结论一定要看源码。比如ArrayList的扩容机制默认容量10添加时如果容量不够会扩容为原来的1.5倍用的是Arrays.copyOf这种知识光靠背是记不牢的每次遇到数组越界、OOM时想一想可能和元素数量膨胀有关才是真正掌握了。我在面试模拟中发现很多人对LinkedList的理解有偏差提到它就说“增删快、查询慢”但实际测试中ArrayList在尾部添加并不比LinkedList差因为尾部添加也只是数组写入而LinkedList的“增删快”也不是绝对的需要遍历到指定位置时它反而更慢。能把这些结论讲清楚才算真正理解容器的数据结构。4.2 HashMap的底层原理是绕不开的硬核java八股文里HashMap几乎是必考必问而且问得非常细。我把复习要点整理成一套自问自答的清单HashMap的数据结构是什么数组加链表Java 8之后链表长度超过8且数组长度大于等于64时转红黑树。哈希是怎么算的key的hashCode高16位和低16位做异或扰动之后减少哈希碰撞。索引怎么定的(n - 1) hash前提是容量是2的幂次。扩容机制是什么默认加载因子0.75元素数量超过容量乘以加载因子就扩容为原来的两倍扩容时节点会重新分配位置。为什么线程不安全并发put可能造成数据覆盖JDK7在并发扩容时还可能形成环形链表导致死循环JDK8修复了但依然不能用于并发场景。为什么允许null键因为它对null有特殊处理null键的hash固定为0。能用代码把“put过程”和“get过程”演示一遍面试这关基本稳了。我建议这样练手动写一个简化版HashMap不要求红黑树只做数组加链表把hash计算、取模、冲突插入、扩容迁移都实现一遍。这个练习做完你会发现自己对HashMap的所有“背过但不知道”的结论突然都通了。4.3 list的CRUD与快速失败机制ArrayList遍历时如果直接调用remove很容易出现ConcurrentModificationException。这个异常背后的机制是modCount——集合结构被修改的次数。迭代器维护一个expectedModCount在迭代时每次next()都会对比一旦发现集合的modCount被改过就立即抛出异常。这就是“快速失败”设计与其继续迭代产生不可预期结果不如尽早报错。正确删除集合元素的方式要么使用迭代器自身的iterator.remove()方法要么用Java 8的removeIf()。写代码时用for-each循环删除元素是最常见的错误操作因为for-each本质就是迭代器的语法糖正好触发快速失败。复习到这里建议顺手看一下mq里“发消息时遍历集合并批量更新”这种场景的写法避免线上才暴露问题。在复习集合的时候也可以结合两个常见的排序算法一起练冒泡排序和快速排序。冒泡排序适合理解“比较相邻元素并交换”的思想但时间复杂度O(n^2)注定只适合教学场景。快速排序用“分治”的方式把数组拆成两部分递归排序其算法思想和Java里Arrays.sort对基本类型采用双轴快速排序的思路是一脉相承的。手写一遍快排再对照Arrays.sort源码会对排序的工程实现有更深理解。5. 反射与动态代理看懂了它们就看懂了一半框架反射和动态代理在业务代码里用得少但是所有主流框架的基石。Spring的IoC容器要创建对象、MyBatis的Mapper要生成代理实现、AOP要织入日志和事务背后全是反射与动态代理。Java基础复习如果只复习业务常用API而跳过这一块那看框架源码会非常吃力。5.1 反射的获取、调用与防坑反射的核心是Class对象有三种获取方式Class.forName(全类名)、实例.getClass()、类名.class。拿到Class后可以获取字段、方法、构造器然后通过setAccessible(true)绕过访问权限调用私有方法或读写私有字段。反射最大的问题是什么呢性能比直接调用慢因为每一步都涉及类型检查和方法查找。另一个问题是安全问题强转Class类型失败会抛ClassCastException因为反射就发生在运行期很多编译期能发现的问题被推迟到运行时才暴露。用反射写代码时务必做好异常捕获和类型校验。我在工作中最常用反射的几个场景写通用导出工具时根据注解获取字段别名、做ORM映射时自动把结果集转成对象、编写测试Mock时动态生成代理。这些东西其实Spring已经封装得很好了但你理解原理后排查问题会快很多比如启动时Bean创建失败、接口方法调用莫名走了AOP代理都是反射和动态代理层面的问题。5.2 JDK动态代理和CGLIB的区别动态代理面试题里必问“JDK代理和CGLIB有什么区别”。我的理解是这样的JDK动态代理要求目标类必须实现接口它通过实现同一个接口的方式生成代理类而CGLIB是通过继承目标类、生成子类覆盖方法的方式实现代理所以目标类没有实现接口时只能用CGLIB而CGLIB无法代理被final修饰的类和方法。Spring的AOP默认策略是目标类有接口时用JDK代理没有接口时用CGLIB代理也可以通过配置强制使用CGLIB。为什么框架这么热衷于动态代理因为代理可以在不修改原业务代码的情况下在方法调用前后增加统一逻辑。这正是AOP的切入点思想日志、事务、权限、缓存这些横切逻辑都可以通过代理织入。理解了这个再去理解Spring的Transactional为什么能自动开启和回滚就像拨开云雾见青天了。复习动态代理时强烈建议手写一个JDK动态代理的实例定义一个接口写一个实现类作为目标再写一个InvocationHandler通过Proxy.newProxyInstance生成代理对象并在invoke里打印日志。整个流程跑通后再思考一下Spring的AOP是怎么在其上封装的效率比背概念高得多。5.3 类加载与一个诡异的NoClassDefFoundError热搜词里有一条“uncaught exception java.lang.noclassdeffounderror: java/applet/applet in thread main”这是用来集成javac或早期Java工具时踩过的坑。NoClassDefFoundError和ClassNotFoundException是完全不同的两个错前者是类在编译期存在、运行期找不到定义通常是ClassPath配置问题或依赖缺失后者是调用Class.forName这种显式加载时找不到类。而java.applet.Applet在JDK 9之后已经被移除了如果你的程序还需要它说明当前环境或者依赖里还在用很老的API需要升级代码而不是换ClassPath。这一块还关联到一个基础点ClassLoader的父委派机制。JVM加载类时先让父加载器尝试加载父加载器加载不到才轮到子加载器这样保证核心API不会被应用类覆盖。很多“找不到类”的问题最后排查下来都是因为同一个类在不同层级加载器里重复出现或者某依赖被不同版本反复引入。了解类加载机制对排查启动类报错有立竿见影的效果这是Java基础复习里值得多花时间的一节。6. 从环境变量到RedisTemplate把基础能力铺到真实开发的一线基础复习不能只停留在“能写出代码”还得能在真实环境里解决“为什么跑不起来”。开发中很多报错看似是工具问题根子其实也在Java基础上——类加载机制、JVM内存模型、版本兼容性、容器封装全都是基础知识的实际应用场景。6.1 Java环境变量配置与编译运行全链路Java安装和环境变量配置是初学者第一道坎儿。很多人之前只会在IDE里点运行一旦上了命令行就懵。环境变量配置的核心其实就两个变量JAVA_HOME指向JDK安装目录PATH里加上%JAVA_HOME%\bin这样系统才能找到java.exe和javac.exe。CLASSPATH在JDK 1.5之后默认包含当前目录一般不手工配置了。我建议复习时把这段流程完整走一遍用文本编辑器写一个HelloWorld.java命令行javac编译生成.class再java运行。这个简单的过程涉及类路径、包结构、主类全限定名等知识。然后再试试打jar包、用java -jar运行并指定Main-Class顺便看看-D系统属性和-Xmx堆内存参数怎么传。搞定这些后面部署Linux服务器时的很多误解会自动消失。6.2 Lombok与JDK版本不匹配的排查思路热搜里那条“You arent using a compiler supported by Lombok, so Lombok will not work”也很典型。Lombok的工作原理是注解处理器在编译阶段修改AST把getter/setter方法生成进字节码。一旦你用的JDK版本过新Lombok版本对它的编译接口不兼容就会直接罢工或生成失败。遇到这种报错我的排查步骤是固定的先确认当前JDK版本用java -version。确认Lombok的版本看pom或gradle依赖。去Lombok的changelog看该版本支持的最高JDK版本。升级Lombok到适配版本不要盲目升级JDK。这个问题看着和Lombok强相关其实背后是对Java版本迭代和软件兼容性的理解。Java更新节奏快每个大版本都有编译器接口变化三方库总会滞后一段时间搞清楚这些再遇到其它类似的注解处理器报错也就知道怎么处理了。6.3 OutOfMemoryError与JVM参数调优的实验方法Java进程报OutOfMemoryError很多人第一反应加-Xmx但这只是堵漏洞不是找原因。要分清楚到底是堆内存不够还是元空间不够还是直接内存溢出。java.lang.OutOfMemoryError: Java heap space表示堆空间不足Metaspace表示类元数据过多Insufficient memory则可能跟操作系统层面或容器限制有关容器里跑Java时如果不设置-XX:MaxRAMPercentageJVM默认按宿主机内存算反而会被容器kill掉。复习JVM基础不需要一开始啃整本调优手册先把最常见的参数搞清楚就够了-Xms、-Xmx、-Xmn、-XX:MaxMetaspaceSize、-XX:UseG1GC。然后再学会用jmap -dump和jvisualvm导出堆转储用MAT或JProfiler看看到底哪个对象占用最多。这套链路走通后再回头看OutOfMemoryError就会感觉它从“灵异事件”变成了“数据问题”。6.4 RedisTemplate的increment报错与类型认知“Java中Redis使用RedisTemplate的increment()报错不是integer or out of range”这个很值得展开。Redis的INCR命令只对整数字符串有效如果你的key对应的value已经存成了字符串比如abc或者是一个二进制序列化后的对象默认JDK序列化那执行INCR时Redis会直接提示值不是整数。很多人的第一反应是“Redis命令有问题”其实是对Redis内部类型机制不理解。解决办法通常是把value字段改成Redis的String类型并确保序列化器配置一致valueSerializer用StringRedisSerializer而不是JdkSerializationRedisSerializer。在实际项目中用RedisTemplate做计数器、库存扣减非常常见每次increment前先确认key里存的值能不能被Redis解释为整数可以省掉很多排查时间。顺着这个报错往深处复习你还能顺带搞懂RedisTemplate和StringRedisTemplate的区别以及Redis的二进制安全特性。6.5 锁面试题的推导式复习法锁相关的内容是java面试八股文的重灾区什么偏向锁、轻量级锁、重量级锁、锁粗化、锁消除背得头大。我的复习方法是先建立一条推导链为什么会有锁 - 最初用synchronized重量级锁 - 但大多数场景没有竞争重量级锁走用户态内核态切换太慢 - 于是引入偏向锁只有一个线程访问时不加锁- 再有竞争时升级为轻量级锁CAS自旋- 自旋过多再膨胀为重量级锁。这种推导式复习的好处是你不用死记“什么时候升级为什么锁”而是顺着性能优化的逻辑自然推演出来。同时可以把synchronized和ReentrantLock做对比一者是JVM层面的监视器锁一者是JDK层面的Lock接口实现支持超时、可中断、多条件等待也都是可重入的。再配合volatile的可见性和禁止重排特性把JMM里主内存和工作内存的关系也理一遍并发基础就到点上了。复习锁的时候我还会顺手写两个demo一个是用synchronized实现线程安全的计数器另一个是用ReentrantLock Condition实现简单的生产者消费者模型。写完之后你对wait/notify和await/signal的区别会有很直观的体会再遇到“锁唤醒丢失”这类八股回答起来也有代码基础支撑。如果一定要我分享一条最想说的经验那就是别把Java基础复习当成一件“突击”的事。它更像给代码能力打地基地基决定了你后面能盖多高的楼。我在实际带人的过程中发现基础扎实的人遇到问题时会先自己排查套路而不是第一时间搜报错基础薄弱的人同样的报错可能经历三四次才会记住。建议你现在就把自己最近遇到的那个“诡异报错”翻出来顺着它往回查一查一定能查到一个你早该复习的Java知识点。这个过程积累多了你会有一种“从前看框架是黑盒现在看框架是半透明”的感觉那才是复习Java基础真正收获回报的时刻。
返回列表