模块一Java环境与运行机制知识点全景JDK是整个Java的开发工具包里面内置了JRE和javac编译器JRE是运行环境包含了JVM和Java标准类库JVM是执行字节码的引擎它本身并不跨平台Windows和Linux的JVM版本不同但能识别统一的.class字节码从而实现Java程序的跨平台。我们写的.java源文件先由javac编译成.class字节码运行时JVM通过类加载器载入内存由解释器逐行翻译为机器码执行热点代码会被JIT即时编译器缓存为本地机器码以提升效率。执行java命令时只能跟类名绝对不加.class后缀若依赖外部JAR包必须用-cp指定类路径否则JVM找不到类会报ClassNotFoundException。面试官追问与回答问Java是编译型还是解释型语言答两者结合。先javac编译成字节码再通过JVM解释执行热点代码由JIT编译为本地机器码属于“半编译半解释”型。问java Hello和java Hello.class有什么区别答前者正确JVM会去加载Hello类后者错误JVM会把.class误认为类名的一部分导致找不到类。模块二Java语法基石类型、运算符、流程控制知识点全景Java中byte、short、char参与算术运算时自动提升为int若操作数中出现long、float或double则所有操作数向其中精度最高的类型对齐运算结果就是该最高精度类型。赋值给变量时如果左侧变量范围小于结果类型必须显式强转否则编译报错如果左侧范围更大则自动隐式转换。唯一特例是复合赋值运算符如Java规定其底层自带隐式强转所以short s1; s1合法而ss1报错。逻辑与和逻辑或||具有短路特性左边决定结果时右边直接跳过。自增无论前置还是后置变量本身都会立即自增1区别在于表达式返回旧值还是新值以jj为例JVM会先暂存旧值再将变量自增1最后把暂存的旧值赋值回去覆盖自增结果所以最终j不变。switch语句中冒号语法有穿透效应需手动break箭头语法-自动终止无穿透支持类型包括int及以下整型、String和枚举但不支持long、float、double。面试官追问与回答问float f 3.4;对吗答不对字面量3.4默认为double需加F后缀float f 3.4F;。问和的区别是什么答有短路功能左边false右边不执行无短路两边都执行且可用于位运算。问switch能作用于long类型吗答不能。只支持int及以下byte,short,char、String和enum。实战类比形容类型转换就像不同容量的杯子——大杯long装小杯int的水自动兼容小杯硬装大杯的水必须手动倒掉多余部分显式强转否则溢出报错而复合赋值就像带漏斗的杯子会自动帮你截断多余的水隐式强转。模块三面向对象OOP封装、继承、多态、抽象类/接口、初始化顺序知识点全景封装通过private隐藏成员变量只暴露getter/setter保护数据完整性访问修饰符控制可见范围private本类、default同包、protected同包子类、public完全公开。继承通过extends复用父类非私有成员子类构造器首行必须用super()调用父类构造器重写必须遵守“两同两小一大”方法名参数同、返回/异常小于等于父类、访问权限大于等于父类。多态中非静态成员方法遵循“编译看左运行看右”的动态绑定而静态方法和成员变量属于静态绑定编译看左。抽象类用于定义同一类事物的公共身份和默认行为如所有门都有开关门动作接口用于定义跨类别的扩展能力如报警功能一个类可继承一个抽象类同时实现多个接口复用代码与解耦能力兼得。类初始化顺序遵循“先父后子先静后动先块后构”父类静态块→子类静态块→父类构造块→父类构造器→子类构造块→子类构造器。面试官追问与回答问重载和重写的区别答重载Overload在同一类中方法名同参数列表不同属于编译时多态重写Override在子父类中方法签名完全相同属于运行时多态。问为什么静态方法不能被重写答静态方法属于类编译期就根据引用类型确定了调用目标不涉及运行时动态绑定所以只能被隐藏子类定义同名静态方法但不会被重写。问抽象类和接口怎么选答如果要复用代码或维护状态成员变量用抽象类如果只定义行为规范且需要多实现用接口。实际开发中两者常结合使用。实战类比形容多态就像LOL中所有英雄都有Q技能父类定义行为契约盖伦和伊泽瑞尔各自重写了Q的具体效果子类各自实现。外部调用者只面向Skill接口编程按Q键时JVM自动根据当前选中的英雄执行对应的技能逻辑。抽象类与接口就像LOL中的防御塔体系所有防御塔继承AbstractTower抽象类获得攻击小兵、承受伤害等通用代码外塔额外实现TowerPlatingable接口获得镀层能力而高地塔不需要则无需实现该接口。这体现了“抽象类管身份is-a接口管能力can-do”的设计原则。初始化顺序new Son()时JVM严格按“祖辈先静态、父辈先静态、祖辈先构造、父辈后构造”的族谱规矩执行顺序固定为父静→子静→父块→父构→子块→子构。模块四字符串与常量池知识点全景Java为了优化内存和提升性能在方法区或堆中JDK版本不同位置有异维护了一个字符串常量池。所有通过双引号直接赋值的字符串字面量在类加载时会被放入常量池相同内容的字面量全局共享同一份对象因此用比较两个相同字面量的引用会返回true。而通过new String(...)创建的对象无论常量池中是否已有相同内容都会在堆内存中强制生成一个全新的独立对象所以不同new出来的对象即便内容相同比较也必定为false判断字符串内容是否相等必须使用equals()方法String已重写equals比较字符序列。经典面试题new String(abc)的执行过程是如果常量池中没有abc则先在池中创建1个对象再在堆中创建1个新对象共计2个若池中已有则只创建堆中的1个对象。此外String被设计为不可变类任何修改操作如concat、replace都会返回新对象原对象保持不变这保证了线程安全和常量池的有效复用。面试官追问与回答问String s new String(hello)创建了几个对象答如果常量池中已有hello则只创建1个堆对象如果常量池中没有则先在常量池创建1个再在堆中创建1个总共2个。问和equals()在String中的区别是什么答比较的是两个引用的内存地址即是否指向同一个对象而equals()比较的是两个字符串的字符序列是否完全一致。在比较字符串内容时必须使用equals()否则会出现内容相同但地址不同的false结果。问String为什么设计成不可变答主要基于安全防止被恶意篡改、线程安全无需同步、常量池复用相同字面量共享和性能哈希值可缓存四点考虑是不可变类的经典代表。实战类比形容字符串常量池就像召唤师峡谷的泉水公共商店所有通过双引号直接写的字面量如多兰剑都是商店货架上现成的共享装备你去拿一次后面所有人拿到的都是同一把所以比较返回true而new String(暴风大剑)就像你不仅参考了商店里的图纸还额外花钱在泉水旁亲手打了一把一模一样但属于你自己的独立新剑每次new都造一把独立的所以两把独立新剑的比较永远是false。内容比较用equals()就好比比较两把剑的属性是否完全相同而不是看它们是不是同一把实物。模块五集合框架重点ArrayList 底层与扩容机制1. 知识点全景ArrayList底层基于Object[]数组实现JDK 1.8 后采用延迟初始化策略无参构造时底层指向空数组容量为0首次调用add()时才将数组扩容至默认容量10以此节约内存。此后每次扩容都严格按1.5 倍增长oldCapacity (oldCapacity 1)并将旧数组元素通过System.arraycopy()拷贝至新数组该操作是ArrayList插入性能消耗的主要来源。ArrayList随机访问get/set时间复杂度为O(1)但中间插入或删除add(index, elem)/remove(index)需要移动大量元素时间复杂度为O(n)。size记录实际元素个数底层数组长度length通常大于sizetrimToSize()可将容量缩减至当前size以释放多余内存。2. 面试官追问与回答问ArrayList无参构造初始容量是多少第一次add后容量是多少答无参构造后底层数组长度为0延迟初始化第一次add时容量直接扩容为默认值10。问扩容机制的具体倍数是多少如何计算的答新容量 旧容量 旧容量 1即 1.5 倍。这是通过位运算实现的效率高于浮点除法且int右移一位天然等于除以2。问ArrayList和LinkedList的根本区别答ArrayList基于数组适合随机访问和末尾增删LinkedList基于双向链表适合频繁的头部或中间增删。但实际开发中中间增删因需遍历寻址LinkedList并不一定优于ArrayList需结合CPU缓存和内存占用综合考量。3. 实战类比形容就像游戏中的“背包系统”你用new ArrayList()创建背包时并没有立刻准备10个格子延迟初始化而是先给个空袋子。当你第一次拾取装备add时系统才一口气给你划分出10个默认格子。随着你捡装备越来越多当第11件装备要放入时系统发现10个格子满了于是直接按1.5倍扩张10格变15格并原封不动地把旧装备全部搬进新背包。这种一扩就是一半的策略平衡了“频繁搬家拷贝浪费性能”与“扩充太小导致反复搬家”的矛盾。LinkedList 底层与特性1. 知识点全景LinkedList底层基于双向链表实现由一系列Node节点每个节点包含prev前驱、item数据、next后继链接而成。它同时实现了List和Deque接口因此既可以作为普通列表使用也可以作为双端队列栈/队列使用。由于不需要连续内存空间LinkedList在头部或尾部插入、删除元素的效率极高时间复杂度O(1)但执行get(index)或add(index, elem)进行随机访问或中间插入时必须从链表头部或尾部遍历到指定索引位置该遍历操作的时间复杂度为O(n)即便后续的插入/删除本身是O(1)整体开销仍由遍历主导。此外它没有像ArrayList那样的“扩容”概念内存是随用随分配的因此不存在一次性大搬迁的开销。2. 面试官追问与回答问既然LinkedList中间插入很快修改指针为什么实际开发中大多数场景还是用ArrayList答因为LinkedList在中间插入前必须先从头/尾遍历到该位置O(n)寻址加上需要频繁创建Node对象的内存开销实际总耗时往往高于ArrayList的System.arraycopy()后者基于 CPU 内存拷贝指令且对缓存友好。只有在头部插入/删除频繁如用作队列或增删操作发生在迭代器遍历过程中时LinkedList才具有明显优势。问LinkedList是否支持快速随机访问即实现RandomAccess接口答不支持。RandomAccess是标记接口ArrayList实现了它表示随机访问性能稳定而LinkedList未实现因为其随机访问需要链式遍历时间复杂度为O(n)。问LinkedList是线程安全的吗答不是。ArrayList和LinkedList都是线程不安全的。多线程环境下若需使用列表可用Collections.synchronizedList()包装或使用CopyOnWriteArrayList读多写少场景。3. 实战类比形容ArrayList就像电影院的连排座位给你连续编号你找第 5000 号观众直接走过去就行随机访问O(1)但想在中间加一把椅子后面所有人连带爆米花都得往后挪扩容或中间插入开销大。而LinkedList就像游乐园里排队玩过山车的队伍每个人只认识自己前后两个人双向链表。领班想叫队伍中间第 5000 个人回答问题必须从队头或队尾一个个数过去遍历O(n)极其费时但如果只是让队尾的人离队或者给队头加个人那只需要动一下前后的绳子头尾操作O(1)非常迅速。HashMap 初始化与首次扩容1. 知识点全景HashMap在 JDK 1.8 后采用懒加载延迟初始化策略无参构造时只设置负载因子为 0.75内部的NodeK,V[] table数组此时为null并不占用连续内存空间。当第一次执行put操作时JVM 触发resize()方法将table数组初始化为默认容量16这是规范中定义的初始容量并同时计算出扩容阈值threshold 16 * 0.75 12。HashMap的容量始终为 2 的整数次幂如 16、32、64这是为了通过位运算(n - 1) hash快速计算元素在数组中的索引位置从而替代低效的取模运算。当哈希表中存储的元素个数size超过当前容量与负载因子的乘积阈值时HashMap会进行resize将容量扩大为原来的2 倍如 16 - 32并重新计算所有已有元素在新数组中的位置rehash这是一个相对耗时的操作因此初始化容量若设置过小会导致频繁扩容影响性能。HashMap的整个生命周期构造 → 首次 put 初始化 → 持续 put 树化 → 超过阈值扩容2. 面试官追问与回答问HashMap无参构造时table数组是否立即被初始化长度是多少答不初始化。无参构造只设置了负载因子table数组此时为null。只有第一次调用put时才会触发resize()将数组初始化为长度 16。问负载因子为什么是 0.75答这是时间和空间的权衡结果。负载因子过大如 1空间利用率高但哈希冲突概率增加导致链表或红黑树查找变慢负载因子过小如 0.5冲突减少但数组过于稀疏浪费内存。0.75 是经统计学计算得出的较优值使得数组在达到 3/4 容量时触发扩容。问为什么HashMap的容量必须保证是 2 的整数次幂答计算元素在数组中的索引时采用(n - 1) hash代替hash % n位运算效率远高于取模。只有当n是 2 的幂时(n - 1)的二进制低位全为 1才能保证哈希值均匀分布且不产生哈希冲突激增。问扩容时为什么不重新计算hash % newCap来定位答因为重新计算哈希取模运算对 CPU 来说是除法指令效率极低。利用e.hash oldCap一次位运算就能将原链表拆分为高低两条链既保证了正确性又大幅提升了大数据量下的扩容性能是典型的“空间换时间”思想的延伸。问如果扩容后某个桶中既有低位链又有高位链它们是如何被分开的答resize()会遍历原数组的每个桶针对桶上的链表或红黑树分别构造loHead低位头节点和hiHead高位头节点两个临时链表。遍历过程中依据(e.hash oldCap) 0将节点分配到不同链表遍历结束直接让新数组的[i]指向loHead[ioldCap]指向hiHead。问红黑树在扩容时也会拆分成高低两条链吗答是的。红黑树节点TreeNode在扩容时也会先拆分为低位链和高位链。如果拆分后某条链的长度小于等于 6该链会由红黑树退化为普通链表以平衡树操作带来的额外内存开销。3. 实战类比形容货运站初期并不会直接摆满货架站长只在管理系统中预设一条全局铁律“当仓库所有货架上存放的包裹总数占到当前货架总排数的75%时立即申请扩建货架排数”负载因子0.75当第一个包裹入库首次put时后勤才实际布置出16排开放式货架初始化容量16并设定警戒线只要全仓库16排货架上的包裹总数超过12个阈值16×0.7512次日立刻扩建为32排货架扩容2倍——特别注意这“12个包裹”是全仓库的总包裹警戒数绝非“每排货架限放12个”哪怕16个包裹全堆在第1排货架上其他15排全空着只要总数没到13个站长也坚决不扩建。当第13个包裹入库触发扩建时原第1排货架上的包裹不需要重新扫码称重重hash分拣员只看包裹运单号二进制的第5位因为从16排扩容到32排多了一位二进制参与货架分配是0的留在原地第1排是1的直接搬到对面新划出来的第17排原索引旧容量16整个过程只需扫一眼单号末位一次位运算就把拥堵的第1排瞬间拆成两堆分放到两排货架上高效得不行而关于智能立体库的改造源码级细节是当某一排货架上堆积的包裹数≥8个时站长不会立刻拍板改造而是先看一眼全仓库总排数——如果总排数已经≥64排仓库足够大才会把这排普通货架升级为智能立体库红黑树找包裹从逐个翻找O(n)变成按电子标签快速定位O(log n)如果总排数还不到64排仓库太小站长会优先选择扩建货架排数扩容而不是花钱装智能库因为总排数太少桶数少扩大整体货架数量比改造单个货架更能根治拥挤问题。HashMap put 方法执行链路1. 知识点全景put方法的执行严格遵循“寻址 - 判空 - 对比 - 挂链/覆盖”的四步逻辑。首先计算键的哈希值扰动函数使得高位参与低位运算通过(n - 1) hash定位桶索引。若该桶为空直接创建节点插入若不为空则遍历桶上的链表或红黑树通过hash值和equals()方法逐个判断键是否已存在如果找到相同键则用新值覆盖旧值并将旧值作为方法返回值返回如果遍历结束仍未找到相同键则采用尾插法将新节点追加到链表末尾JDK 1.8 后改为尾插法解决了 1.7 头插法在多线程扩容时的死循环问题。插入完成后若链表长度达到 8且数组长度 64链表会转换为红黑树以提升查找效率若数组长度不足 64则优先触发resize()扩容。最后如果插入新节点导致size threshold也会触发扩容。2. 面试官追问与回答问两次put相同键返回值是什么答第二次返回第一次存入的旧值第一次返回null。这是Map接口约定的标准行为用于感知键是否被覆盖。问链表转红黑树的阈值是 8为什么选这个数字答基于泊松分布统计学得出当哈希冲突极严重时链表长度达到 8 的概率极低小于千万分之一。此长度下红黑树查询O(log n)相对于链表查询O(n)具有明显性能优势因此以 8 为分水岭。问JDK 1.7 和 1.8 在插入链表时有什么不同答1.7 采用头插法新节点插在链表头部在多线程并发扩容时可能形成循环链表导致 CPU 飙升1.8 改为尾插法新节点追加在尾部同时优化了扩容时元素的重新分布算法避免了死循环风险。3. 实战类比形容put流程就像学校分班级桶。招生办根据学生姓名哈希算出所属班级号寻址。到了班级门口如果教室里没人空桶新生直接进教室坐好新建节点。如果教室里有学生哈希冲突老师会依次问“你是张三吗”hash与equals比较如果找到叫张三的键相同就把新来的李四安排在这个座位上而原来的张三被赶走并作为“旧值”返还给招生办覆盖返回旧值如果问了一圈都不是张三键不同就让新生在教室后面“接龙”坐下链表尾插。如果这个班排队超过 8 个人且学校教室总数超过 64 间校长就会把这个班整编成红黑树让老师能快速点名查询从O(n)降到O(log n)。Hash冲突解决第一层预防手段从源头降低冲突概率——扰动函数在计算出键的hashCode()后HashMap不会直接使用它而是执行扰动函数即hash key.hashCode() ^ (key.hashCode() 16)。这会将哈希值的高16位和低16位进行异或混合让高位的特征也参与到低位的运算中。这样做的目的是让哈希值在最终取模寻址时分布得更均匀尽可能从源头减少两个不同键落入同一个桶的概率。第二层存储手段冲突后如何存放——链地址法拉链法这是HashMap解决冲突的核心机制。当两个不同键计算出相同的桶索引时不会覆盖彼此而是将新节点以链表形式挂载在该桶的已有节点后面。JDK 1.8 采用尾插法新节点追加到链表尾部。当某个桶的链表长度超过8且数组长度大于等于64时该链表会树化为红黑树TreeNode将查找时间复杂度从O(n)优化为O(log n)从而应对极端哈希冲突下的性能退化。第三层兜底手段冲突过于严重时扩容——扩容Resize如果某个桶的链表很长说明要么哈希函数设计不佳要么数组容量太小桶位不够。当整体元素数量超过阈值容量 × 负载因子0.75时HashMap会触发resize()将数组长度扩容为原来的 2 倍。扩容后所有已有元素会重新计算索引rehash原本挤在同一个桶里的元素会被分散到新的桶中从而从根本上化解严重的冲突。此外如果链表长度超过8但数组长度小于64HashMap不会树化而是优先扩容因为此时扩充桶数量比转成红黑树更能解决问题。扩展知识面试加分项HashMap采用的是“链地址法”但 Java 中还有另一种经典解决方案叫“开放寻址法”例如ThreadLocalMap就使用线性探测法来解决冲突——即如果计算出的桶被占用就继续找下一个空桶。HashMap不采用这种方法是因为开放寻址法在元素大量聚集时插入和查找效率会急剧下降且删除元素需要特殊标记null无法区分空槽位和已删除槽位维护成本高。LinkedHashMap和ConcurrentHashMapLinkedHashMap:① 它的底层是HashMap 双向链表② 构造方法里有个参数可以开启“访问顺序”accessOrder可以用来实现LRU缓存算法。ConcurrentHashMap:知道它是线程安全的比Hashtable性能好因为Hashtable锁整张表它锁桶或节点。