ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java面试基础篇:面向对象、集合框架与Java8核心考点全解析

Java面试基础篇:面向对象、集合框架与Java8核心考点全解析 Java 面试八股文之基础篇一从背题库到讲清楚差的不只是熟练度又到了一年金三银四的跳槽季后台和社群里咨询最多的问题永远是同一个“Java 基础到底怎么准备八股文背了忘、忘了背面试时还是被问懵。”说实话Java 面试八股文这个词这几年已经被聊烂了但它的价值依然实打实存在——一面笔试、二面技术面筛的不就是这些基础功底吗这篇先聊基础篇一覆盖面向对象、数据类型与包装类、String 家族、异常体系、集合框架、Java 8 新特性这几大块。内容整理自这些年我面试别人、也参加面试后的真实沉淀每个考点我会先告诉你面试官想听什么再告诉你哪些细节容易踩坑最后补上“为什么”。适合正在准备 Java 校招、社招初到中级岗位的朋友也适合带新人的老手拿去当入职培训提纲。背会只是及格能讲出背后的设计考虑才是加分项这篇就是奔着后者去的。1. 面向对象三大特性面试官的“三板斧”1.1 封装不是“私有化”三个字那么简单面试官问面向对象十有八九第一刀会砍向封装。多数人能答出来“封装就是把属性私有化提供 getter/setter 访问”但这只是表面功夫真正的考点是你知不知道为什么不能直接暴露字段。Java 的访问控制符有 private、default、protected、public 四档封装的核心意义在于控制可变性、统一校验入口。举个例子你做一个用户模块年龄字段如果直接 public int age调用方随手赋一个 -1 可能就直接塞进去了但如果走 setAge(int age) 方法你可以在方法里加校验拒绝非法值。这就是封装的第一个价值——保护数据完整性。第二个价值是降低耦合度。内部字段怎么存、怎么算调用方不需要知道你后面想把 int 改成 long或者从直接存值改成存一个包装对象只要 getter/setter 的签名不变外部代码一行都不用改。很多初级开发觉得写 getter/setter 是“样板代码”是 IDE 自动生成的垃圾其实这是误解——模板化的代码形态不代表它没有设计价值。注意面试时别只说“为了安全”。补充一句“为了控制变更范围和维持类的不变式”面试官会高看你一眼。1.2 继承与组合优先别一上来就 extends继承这块最经典的三个追问是Java 支持多继承吗构造方法能被重写吗为什么阿里规范里常说“组合优先于继承”第一个问题好答Java 类只支持单继承但接口可以多实现。这句话说出口之后要能继续展开——Java 不搞多继承主要是为了规避菱形继承问题也就是两个父类都有同一个方法子类不知道该继承哪一个。接口不一样接口只有抽象方法Java 8 之后加了 default 方法但这属于另一个故事不存在实现的冲突所以接口可以放开让你多实现。第二个问题构造方法不能被重写。原因很简单构造方法名必须和类名一致而子类名字跟父类不同签名就对不上何来重写但子类构造器必须调用父类构造器这就是 super() 的来源——如果父类没有无参构造子类构造器第一行就必须显式 super(参数)。这个考点很基础但大把人栽在“父类构造器带参时子类编译报错”上。第三个问题值得展开说。继承关系是强耦合父类改一个方法签名或改行为所有子类都可能受影响这叫做“脆弱的基类问题”。组合则是把你需要的行为通过接口注入进来类之间只依赖抽象不依赖具体实现。举个常见的例子一个 Cat 类如果 extends Animal 只是为了复用 eat() 方法那后续想给 Cat 加一个 fly() 能力继承就玩不转了但用组合Cat 拥有 EatBehavior 和 FlyBehavior 两个接口引用想加能力就替换实现灵活得多。1.3 多态运行时才能决定的事多态是面试里出题密度最高的部分因为它是面向对象设计的灵魂。关键要理解“编译看左边运行看右边”这句话。多态分为两种编译时多态和运行时多态。编译时多态就是重载Overload同一个类里方法名相同、参数列表不同编译期就能确定调用哪个版本。运行时多态靠的是重写Override和向上转型——父类引用指向子类对象调用被子类重写的方法时JVM 根据实际对象类型去方法表中查找并执行子类版本。这里有个高频陷阱题静态方法能不能被重写答案是不能。静态方法是类级别的跟实例对象无关虽然在子类里写一个同名同参的静态方法编译器不会报错但那叫隐藏Hide不是重写。面试官问到这里多半还会追加一句“那 private 方法能被重写吗”也不能private 方法对子类不可见。还有一种问法是“父类构造器里调用了一个被子类重写的方法会怎样”答案是会调用子类版本但此时子类字段还没初始化很容易拿到 null 或默认值这就是经典面试题《构造器内多态方法调用的坑》。生产环境里如果真想在构造流程里做点初始化建议用 private/final/static 方法别依赖多态分派。2. 数据类型与包装类你以为知道其实没吃透2.1 基本类型与默认值速查Java 有 8 种基本类型byte、short、int、long、float、double、char、boolean。面试官通常会让你报出它们的字节数和默认值这算热身题但只要有一个记错印象分就大打折扣。类型字节数默认值byte10short20int40long80Lfloat40.0fdouble80.0dchar2\u0000boolean1/4JVM 依赖false注意 long 和 float 的默认值要带后缀long 是 0Lfloat 是 0.0f。boolean 的大小在规范里其实是未定义的HotSpot 里单独使用时一般占 1 个字节boolean 数组里可能按位存储这个细节只有面试官较真时才会追到。2.2 自动装箱、拆箱与 Integer 缓存陷阱这个考点几乎每场面试必出现。自动装箱是 Java 5 引入的语法糖本质上是调用 Integer.valueOf(int)自动拆箱则是调用 intValue()。那 Integer 的缓存范围是多少-128 到 127。为什么是这个范围因为 JVM 规范里没有强制要求但 HotSpot 实现很默契地把 valueOf 的小整数缓存池定为 -128 到 127这个区间是 8 位二进制能表示的范围绝大多数业务里的小整数都集中在这里缓存起来能省大量对象创建。于是就有了著名面试题Integer a 127; Integer b 127; System.out.println(a b); // true Integer c 128; Integer d 128; System.out.println(c d); // false第一组是 true因为走了缓存第二组是 false因为 128 超出缓存范围每次都是 new 新对象。实操心得公司代码规范里有一条我特别认同——“包装类比较必须用 equals不允许用 ”。这不是理论洁癖线上真的出过事故接口返回值里两个 Long 型 ID 比较因为没拆箱用 结果 ID 一大全都不相等数据直接错乱。2.3 经典高频追问int 和 Integer 的区别这道题答法讲究层次。第一层类型定位int 是基础数据类型直接存值Integer 是包装类是对象。第二层默认值int 默认为 0Integer 默认为 null这决定了 Integer 可以表达“未赋值”的业务语义比如数据库里某个可选字段没填时用 Integer 更合适。第三层存储位置局部变量的 int 在栈帧里操作数栈中而 Integer 对象本身在堆上引用存栈里。第四层性能Integer 有对象头和缓存机制高频循环场景下若反复装箱拆箱会带来额外开销。有些面试官还会追问“new Integer(100) 和 Integer.valueOf(100) 有什么区别”前者无论如何都会 new 一个新对象后者在缓存范围内复用缓存的实例。这种细节最能检验你是真读过源码还是只看过博客速记。3. String 家族面试必考的一块硬骨头3.1 String 为什么是 final 的String 类用 final 修饰同时里面的 value 数组也是 final 的。这两个 final 层层保护最终目的就是不可变性。面试官问你 String 不可变有什么好处至少要说上三点第一线程安全不可变对象天然可以安全地被多个线程共享第二支持字符串常量池复用创建一万个内容相同的字符串如果都是字面量写法实际可以复用同一个实例第三hashCode 可以缓存String 被大量用作 HashMap 的 key不可变意味着 hash 值永远不会失效。再深入点追问final char[] 和不可变是一回事吗不是。final char[] 只是让 value 引用不能指向新数组但数组里的元素其实可以改虽然 String 内部没有提供修改方法反射可以强行改但属于破坏封装。所以“不可变”是类设计层面约束出来的行为不是 final 数组天然提供的。3.2 字符串常量池与 intern()String s1 abc; 和 String s2 new String(abc); 到底差在哪前者先去字符串常量池里找有没有 “abc”没有就创建并放入池中后者直接在堆上 new 一个 String 对象同时也会在常量池里存放字面量如果之前没有的话。所以 s1 s2 一定是 false两者不是同一个对象只是内容相同。intern() 方法的作用是把字符串内容尝试放入常量池并返回池中的实例。JDK 7 之后常量池挪到了堆里intern() 的逻辑也做过调整如果池中已有相同内容字符串直接返回池中实例如果没有会把当前对象的引用复制一份放进池中返回的还是当前对象。这导致 JDK 版本不同 intern() 行为有细微差异属于进阶八股题懂底层能加分。3.3 StringBuilder vs StringBuffer一个锁引发的分工这道题的核心答案就一句话StringBuffer 的方法是 synchronized 修饰的线程安全但串行化StringBuilder 不加锁单线程下更快。但面试官更想听到的是字符串拼接千万不要用 String 加号。比如在循环里执行 str item每次拼接都会生成一个新的 StringBuilder 对象再 toString循环一万次就是一万次对象创建和 O(n^2) 的字符复制性能惨不忍睹。正确做法是在循环外声明一个 StringBuilder循环内 append。这里有个反直觉的点如果是编译期就能确定常量的拼接比如 String s a b;JVM 编译器会自动优化成 String s ab不会生成 StringBuilder。但一旦有一方是变量编译器才会生成 new StringBuilder().append(...).toString() 的字节码。所以很多人以为“编译器会自动优化拼接”其实只优化了字面量常量拼接。4. 异常体系别再把 Error 和 Exception 混为一谈4.1 Throwable 树形结构速览Java 里所有异常和错误的根类是 Throwable分两大子类Error 和 Exception。Error 表示 JVM 层面的严重问题比如 OutOfMemoryError、StackOverflowError程序一般无法恢复也不需要捕获Exception 才是程序层面可以处理的问题。Exception 内部又分两类受检异常Checked Exception和非受检异常Unchecked Exception。受检异常必须显式捕获或抛出典型有 IOException、SQLException非受检异常主要指 RuntimeException 及其子类比如 NullPointerException、IllegalArgumentException、IndexOutOfBoundsException编译器不强制处理。很多团队规范里会建议业务代码尽量抛 RuntimeException 或自定义运行时异常因为受检异常会把处理逻辑强制铺到每一层调用方导致代码里到处是 try/catch 或者 throws 蔓延反而污染了业务表达。这不是说受检异常没用而是要在框架边界和个人代码里做取舍。4.2 OutOfMemoryError 真的是“异常”吗从热搜词里常能看到java: outofmemoryerror: insufficient memory这种报错很多人第一反应是“我这个异常没捕获导致程序崩了”。这里要澄清一个概念OutOfMemoryError 是 Error 不是 Exception它不在 try/catch 应该关心的范畴里捕获它没有任何意义因为抛出这个错误时 JVM 内存基本已经山穷水尽。面试里关于 OOM 更常被问到的是类型划分堆内存不足是 java.lang.OutOfMemoryError: Java heap space元空间不足是 Metaspace线程栈内存不够是 unable to create new native thread。要能说出一个排查思路——先确认是堆还是非堆堆 OOM 就 dump 堆快照分析大对象和内存泄漏非堆 OOM 则查动态类加载和线程数。4.3 try-with-resources 和 finally 的执行顺序JDK 7 引入了 try-with-resources本质是语法糖底层编译后会自动生成 finally 块并调用资源的 close()。相比传统手写 finally close它最大的优势是异常抑制如果 try 块抛异常同时 close() 也抛异常传统写法 close 的异常会覆盖 try 的异常try-with-resources 会把 close 的异常追加为 suppressed 异常保留主异常信息排查问题友好得多。另一个经典问题是try 块里有 returnfinally 还会执行吗答案是会finally 在 return 执行之前执行。如果 finally 里也有 returnfinally 的返回值会覆盖 try 里的返回值。这是一个大坑所以 Java 官方文档其实也建议不要在 finally 里写 return。还有个更细的考点如果 try 里 return 的是一个基本类型变量finally 里再改这个变量不影响返回值因为返回值在 return 字节码指令执行时已经确定并暂存了但如果 return 的是一个对象引用finally 里修改该对象的属性返回值会跟着变因为这俩是同一个对象。5. 集合框架HashMap 几乎是必考题5.1 HashMap 底层结构与扩容机制Java 面试如果没有 HashMap就相当于吃火锅没有毛肚绝对的不完整。JDK 8 的 HashMap 是数组 链表 红黑树的组合。先说数组长度默认初始容量是 16负载因子是 0.75也就是当元素个数达到 16 * 0.75 12 时触发扩容扩容时数组容量翻倍。为什么要 0.75 这个数字这是空间和时间的一个折中负载因子太大比如 1虽然省空间但哈希冲突会剧增链表变长查询退化到 O(n)负载因子太小比如 0.5冲突少但空间浪费严重频繁扩容也耗性能。0.75 是 Java 开发者在大量实验数据里选出来的平衡点。链表什么时候转红黑树两个条件同时满足链表节点数达到 8且数组长度达到 64。为什么阈值是 8源码注释里引用了泊松分布——在随机哈希码和默认负载因子的情况下一个桶里链表长度达到 8 的概率大约是千万分之六基本属于“天选之桶”才会遇到。达到 8 就树化是为了应对恶意哈希码导致的大量碰撞而不是正常业务下的预期场景。树节点数是 6 时又会退化为链表中间隔着 7是为了防止频繁插入删除导致树化和退化来回震荡。5.2 为什么 HashMap 线程不安全三个典型场景可以说明第一多线程同时 put 导致数据覆盖两个线程都算好了新元素要放在同一个桶后写的一方覆盖先写的一方第二扩容时多线程并发 rehashJDK 7 的链表头插法会形成环形链表下次 get 时无限循环CPU 直接打满JDK 8 改成了尾插法环形链表问题被解决了但数据覆盖和 size 统计不准确仍然存在。第三如果多个线程同时 put 的 key 恰好碰撞链表插入时也可能会丢节点。所以面试答这题时要说“JDK 7 和 JDK 8 的问题不一样”这比笼统说一句“线程不安全”要扎实得多。多线程场景要用 ConcurrentHashMap它的实现细节单独能写一整篇但基础答案至少要知道JDK 7 是分段锁JDK 8 改成了 CAS synchronized 锁桶节点锁粒度更细。5.3 ArrayList、LinkedList 与扩容细节ArrayList 底层是数组默认构造时其实是空数组第一次 add 才初始化成容量 10 的 Object[]。扩容规则是新容量 旧容量 (旧容量 1)也就是 1.5 倍。扩容需要数组复制用的是 System.arraycopy这是 native 方法效率比手动 for 循环高但频繁扩容仍然有成本。所以如果能预估数据量构造时直接传 initialCapacity 是很好的习惯。LinkedList 底层是双向链表麻烦的是每个节点都包装成 Node 对象内存占用比 ArrayList 高。它真的在插入删除时更快吗不见得。如果已经持有某个节点的引用在那附近插入删除确实 O(1)但你要是只存了一个索引下标LinkedList 需要从头遍历到目标位置复杂度 O(n)。所以实际开发里随机访问坚决用 ArrayList只在“已知位置高频增删”这种特殊场景才考虑 LinkedList。6. Java 8 新特性从语法糖到编程思维的转变6.1 Lambda 表达式与函数式接口Java 8 的 Lambda 是这些年最容易问到的“新特性”考点。Lambda 就是匿名内部类的一种简化写法核心前提是目标类型必须是函数式接口——也就是只有一个抽象方法的接口通常用 FunctionalInterface 标注。最常用的四个Supplier无参有返回值、Consumer有参无返回值、Function有参有返回值、Predicate有参返回 boolean。网上很多人说 Lambda 就是语法糖译成匿名内部类这话放到 JDK 8 之前是成立的但 JDK 8 之后强烈不建议这么说。Lambda 编译后是通过 invokedynamic 指令动态生成的并不会直接创建一个匿名内部类对象。两者的性能特性和生命周期都不一样Lambda 不持有外部 this 引用匿名内部类则持有——这也是为什么匿名内部类里用外部变量要求 final 或 effectively final而 Lambda 里也存在这个限制。面试答到这一层基本能镇住场面。有个非常真实的坑得提一嘴有些人电脑上编译项目报错 “源发行版 17 需要目标发行版 17” 或者 “java: internal error in the mapping processor: java.lang.nullpointerexceptio”其实不是代码问题是 Maven/Gradle 编译插件里 source/target 版本没配对IDE 里设置了一个 JDK 版本Maven 配置里又是另一个。用 Lambda 的代码如果编译期版本太低也会报错建议项目里统一在 pom.xml 或 build.gradle 里锁定 Java 版本别依赖 IDE 默认配置。6.2 Stream 流的中间操作与终止操作Stream 的考点在“懒加载”和“流水线”这两个词上。中间操作filter、map、distinct、sorted都是惰性的调用它们只是把操作记录下来不会真正执行只有遇到终止操作collect、forEach、count、reduce时整条流水线才会一次性跑起来。这个设计是为了支持短路优化——比如 limit(5) 遇到终止操作后只要找到 5 个满足条件的元素后面就不继续遍历了。实际工作中我见过不少人滥用 Stream一个业务方法里套四五层 filter 加嵌套 collect可读性极差debug 也没法下断点。不是所有循环都要改成 Streamfor-each 在小数据集和复杂分支里反而更直观。面试官如果问“Stream 和 for 循环谁更快”大胆答多数场景 for 循环略快Stream 的优势在于表达能力和并行能力而不是绝对性能。6.3 Optional治标不治本的防空利器Optional 是 Java 8 带来的又一个高频话题。它的设计原意是让返回值能显式表达“可能为空”的语义逼迫调用方处理空值情况。用它有一个核心原则Optional 是用来做返回值的不要把它当成字段类型也不要把它当成方法参数。这两个误用会让序列化、构造函数、equals/hashCode 全部变得别扭。还有个经典问答Optional.of(null) 会怎样会抛 NullPointerException因为 of 不接受 null而 Optional.ofNullable(null) 会返回一个空的 Optional。所以代码里如果明确知道不该为 null用 of 让程序快速失败如果不确定用 ofNullable 再配 orElse 兜底。注意 orElse 和 orElseGet 的区别前者无论有没有值都执行参数里的表达式后者只在值为空时才执行。如果 orElse 里是个 createNewObject() 此类有副作用的调用每次都会白跑一次浪费资源。7. 备考方法与实战经验聊完具体考点再分享一点准备心法。我见过不少人天天刷题但刷完就忘本质上是缺了一个“输出倒逼输入”的环节。你可以在读这道题的答案时先用自己的话复述一遍录成语音或者写进笔记然后对照原答案找遗漏。一次能顺畅讲出 80% 以上的点才算真正掌握了这道题。学完一个专题比如 String就试着给一个完全不懂 Java 的朋友讲一遍他听懂了说明你的逻辑是通的他听不懂多半是你自己还没理明白。另外八股文之外一定要配一个“故事点”。面试官问你 HashMap你答完底层结构顺手补一句“我之前线上遇到过一台机器的 hash 值分布极端导致的 CPU 飙高后来通过自定义 key 的 hashCode 实现解决了”这一句话的价值抵得上三分钟的纯背诵。八股文只是敲门砖能把知识关联到真实场景才是工作经验和学习能力的证明。这套基础篇先讲到这。集合框架里的 ConcurrentHashMap 细节、JVM 内存模型、类加载机制、反射与代理这些内容量都很大适合单独开篇展开。等下一篇接着聊。
返回列表