北森客开实习生(实习转正) 2026.10.13
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- 聊一下你参与比较多的项目。
- 聊一下项目的业务,以及实际是怎么开发的。
- 聊一下你的 Java 开发实习。
- ETL 是什么?使用了什么数据库?目标数据库用的哪个?遇到过哪些比较难的类型转换?
- 项目中遇到哪些难点?怎么解决的?
- Java 的 try-catch-finally 中,try 里面有 return,finally 还会执行吗?
- 重载和重写的区别是什么?
- HashMap 是什么?它线程安全吗?ConcurrentHashMap 呢?
- synchronized 和 Lock 的区别是什么?
- 斐波那契数列有哪些解法?(迭代、递归、数学公式)
- 冒泡排序怎么实现?有几层 for 循环?
- 你有什么想问我的?
《参考解析》
try-catch-finally 与 return 的执行顺序,要把三种情况都摆出来:第一,finally 一定会执行(除极少数特例:System.exit、JVM 崩溃、守护线程被杀),即便 try 块里写了 return——执行的顺序是「先算出 return 表达式的值并暂存,再执行 finally,最后才真正返回」。第二,如果 finally 里也写了 return,它会覆盖 try 里的返回值,而且 try 中那次返回的暂存值被直接丢弃;同理 finally 里抛异常也会吞掉 try 的返回或异常。第三,finally 里修改基本类型的局部变量不会影响已暂存的返回值,但如果返回的是引用类型、在 finally 里改了对象的字段,改动是能被看到的。能补一句规范就更好:不要在 finally 里写 return、break、continue 或抛异常,很多静态检查(Sonar、Ali 规约)会直接判为缺陷。此外 try 里发生异常时走的是 catch 分支,catch 里 return 也会先执行 finally。
重载与重写的区别,本质是编译期绑定和运行期绑定的区别:重载(overload)发生在同一个类里,方法名相同而参数列表不同(个数、类型、顺序),与返回值类型和访问修饰符无关,编译器在编译期按静态类型就能确定调哪个,所以它是静态多态。重写(override)发生在父子类之间,子类提供与父类签名相同的方法实现,通过父类引用调用时由对象的实际类型在运行期决定,属于动态多态(Java 里靠虚方法表派发)。重写有几条硬约束要能报出来:访问权限不能比父类更严、不能抛出比父类更宽的受检异常、返回值可以是父类返回值的子类型(协变返回)、final、static、private 方法和构造器都不能被重写(static 方法只能被隐藏)。最后补一句面试常追问的:重载看的是编译期静态类型,所以 null 传参、自动装箱、可变参数都可能让编译器选到「意外」的那个重载,实际编码里要避免嵌套过深的重载。
HashMap 与 ConcurrentHashMap 要答出结构、扩容与并发安全三层:HashMap 是数组 + 链表 + 红黑树(JDK 8 起,链表长度到 8 且数组容量到 64 才树化),通过 hashCode 扰动后取模定位桶,默认容量 16、负载因子 0.75,超过阈值扩容为两倍并做 rehash(JDK 8 用高低位拆分,避免了 JDK 7 头插法在并发扩容下形成环形链表导致死循环的问题,但并发写仍然会丢数据、size 不准,所以它依然不是线程安全的)。线程安全的替代方案有三档:Hashtable 全表加锁、性能最差;Collections.synchronizedMap 只是包一层同样的锁;ConcurrentHashMap 是正解——JDK 7 用分段锁(Segment),JDK 8 起改为 CAS 插入空桶 + synchronized 只锁当前桶的头节点,锁粒度更小,扩容时支持多线程协助迁移(sizeCtl、ForwardingNode),并发计数用 baseCount 加 CounterCell 数组分段累加代替全局计数。还要能提一句它的一致性语义:get 是弱一致的(不加锁,可能看到旧值),size 是估算值,putIfAbsent/computeIfAbsent 这类复合操作才是原子的——键和值都不允许为 null,也是它的一个显著区别。
synchronized 与 Lock 的差别,讲清「谁来实现」和「谁负责释放」就赢一半:synchronized 是 Java 关键字,由 JVM 在字节码层面用 monitorenter/monitorexit 实现,进入同步块加锁、退出(含异常抛出)自动释放,不用担心忘记解锁;它支持锁升级(偏向锁 → 轻量级锁 → 重量级锁)与等待唤醒(wait/notify)。Lock 是 java.util.concurrent.locks 下的接口,ReentrantLock 是它最常用的实现类(不是父类关系,是接口与实现的关系),基于 AQS 的同步队列与状态位实现。用 Lock 换来的能力有几项是 synchronized 给不了的:可中断地获取锁(lockInterruptibly)、带超时的尝试(tryLock)、可配置公平/非公平、以及一个锁上挂多个 Condition 做精准唤醒;代价是必须在 finally 里手动 unlock(),否则一旦异常就会永久持锁。业务里通常这么选:并发量不大、竞争不激烈、只做简单互斥,用 synchronized 最省心;需要超时、可中断、公平性或条件队列(典型如手写阻塞队列、按条件分批唤醒的生产消费模型),才上 ReentrantLock。
算法题考的是「你会几种,以及知道各自的边界」:斐波那契至少准备四种解法:① 朴素递归,时间 O(2^n),指数爆炸,只能当作讲解起点;② 带备忘录的递归或自底向上的迭代,时间 O(n)、空间可压到 O(1)(只存前两项),是实际默认答案;③ 矩阵快速幂,把递推写成 [[1,1],[1,0]] 的幂,用二进制快速幂把时间降到 O(log n),适合 n 极大的场景;④ 通项公式(比内公式),用无理数表达,理论 O(1) 但浮点精度在 n 稍大时就失真,属于「知道有这回事」就行。要主动补一句溢出问题:n 超过 92 时 long 就装不下,题目若要求取模就用矩阵快速幂配合取模。冒泡排序的写法是两层循环:外层控制需要比较的轮数(n-1 轮),内层从头两两比较并在逆序时交换,每轮结束末尾的最大值就位,所以内层可以逐步缩短边界;标准实现时间 O(n²)、空间 O(1)、稳定。加分项是说出两个优化:加一个交换标记,某轮没有发生交换就说明已经有序,直接提前结束(最好情况降到 O(n));以及记录最后一次交换的位置,把下一轮内层边界收窄到那里。面试官问「有几层 for」其实是想确认你知道轮数与边界的关系,答的时候把两端边界写清楚。
从这场的自评看,真正的失分点是项目纵深而不是八股:面试官问「业务怎么开发、难点怎么解决、类型转换难在哪」这类问题,想要的是你对自己参与部分的完整掌握:数据从哪来、经过哪些步骤、为什么这么设计、量级多大、出错怎么兜住。准备时可以画一张数据流图,把每个环节的输入输出、责任边界、异常处理都标上,再对每个环节准备一到两个「为什么」。八股部分都是常规题,但要成体系而不是零散记忆:把 Java 基础(异常、重载重写)→ 集合(HashMap 家族)→ 并发(synchronized/Lock、线程池)→ JVM → 数据库 串成一条线复习,面试时能顺着追问往下答。至于「让 AI 拷打简历」这类方法可以用,但要注意它是自检工具不是背稿机——真正的目标是让你对项目细节形成可复述的结构,遇到没准备过的追问也能靠事实推理,而不是被问穿了就卡住。