🔥 广州小厂Java实习面经(爱奇创新):从笔试到面试,线程池、设计模式、Spring IOC、Redis签到与ES分词全解析
发布时间:2026年1月15日
字数:约9200字
阅读时长:27分钟
适用人群:Java实习生、计算机相关专业应届生、准备初级后端岗位面试的在校学生
关键词:Java实习面试、广州小厂面经、线程池、Thread.sleep(0)、设计模式、Spring IOC、BeanFactory vs ApplicationContext、饿汉式懒汉式、Redis签到、Elasticsearch分词、搜索二维矩阵、SQL多表查询、CompletableFuture
在广州众多中小型科技公司中,“爱奇创新”虽非大厂,但其Java实习岗位的考察内容却兼具广度与深度——不仅有扎实的笔试基础题,更有贴近工程实践的场景设计题。本文基于一位候选人的真实经历,完整还原“笔试 + 面试”全流程,采用“面试官提问 + 候选人专业回答”的对话形式,深入剖析10道笔试题与5大面试连环问,涵盖:
- Java 异常体系与线程模型
- 线程池原理与最佳实践
- 设计模式与单例实现
- Spring IOC 容器核心机制
- Redis 位图签到方案
- Elasticsearch 分词优化策略
- 手撕代码(Switch、SQL、算法)
无论你正在备战实习面试,还是希望系统梳理 Java 后端知识体系,本文都将为你提供极具参考价值的实战指南。
一、笔试环节:10道题直击Java核心基础
到达公司后,HR先让我填写基本信息表,随后发放一份纸质笔试卷,共10题,限时40分钟。题目如下:
1. Exception 和 Error 都继承自 Throwable,有什么区别?
标准答案:
- Error:表示 JVM 无法处理的严重系统错误,如
OutOfMemoryError、StackOverflowError。程序通常不应捕获,因为无法恢复。- Exception:表示程序运行中可能出现的异常情况,可分为:
- Checked Exception(受检异常):编译器强制要求处理,如
IOException;- Unchecked Exception(非受检异常):即
RuntimeException及其子类,如NullPointerException,可不显式处理。
💡关键区分:是否需要
try-catch或throws声明。
2. 线程的Thread.sleep(0)有什么意义?有什么替代方法?
标准答案:
Thread.sleep(0)的作用是主动让出当前 CPU 时间片,触发线程调度器重新进行线程优先级评估,使其他同优先级或更高优先级的线程有机会执行。它常用于高频率循环中避免“独占”CPU。替代方法:
Thread.yield():提示调度器让出 CPU,但不保证一定切换;- 使用
LockSupport.parkNanos(1)实现更精确的微等待;- 更推荐使用并发工具类(如
CountDownLatch、Semaphore)替代忙等待。
⚠️注意:
sleep(0)并非“无操作”,它会触发一次完整的线程状态切换(RUNNABLE → TIMED_WAITING → RUNNABLE),有一定开销。
3. 线程池的意义是什么?你会怎么创建线程池?(使用Executors有什么缺陷?)
标准答案:
线程池的意义:
- 降低资源消耗(复用线程)
- 提高响应速度(任务到来时无需创建线程)
- 便于统一管理(控制并发数、拒绝策略等)
正确创建方式:
不要直接使用Executors工具类!因其存在严重缺陷:
newFixedThreadPool/newSingleThreadExecutor:使用无界LinkedBlockingQueue,可能导致 OOM;newCachedThreadPool:最大线程数为Integer.MAX_VALUE,可能创建过多线程导致系统崩溃。推荐手动创建:
ThreadPoolExecutorexecutor=newThreadPoolExecutor(2,// corePoolSize4,// maximumPoolSize60L,TimeUnit.SECONDS,// keepAliveTimenewLinkedBlockingQueue<>(100),// 有界队列newThreadFactoryBuilder().setNameFormat("my-pool-%d").build(),newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略);
✅最佳实践:始终使用有界队列 + 明确拒绝策略,避免资源耗尽。
4.shutdown()之后,线程池已经提交的任务会被执行吗?
标准答案:
会。shutdown()的作用是平滑关闭线程池:
- 不再接受新任务(调用
submit()会抛出RejectedExecutionException);- 但已提交的任务(包括队列中的)会继续执行完毕。
若需立即停止,应使用
shutdownNow(),它会尝试中断所有正在执行的任务,并返回未执行的任务列表。
5. Java 的设计模式有哪些?
标准答案(分类列举):
创建型:单例(Singleton)、工厂(Factory)、抽象工厂、建造者(Builder)、原型(Prototype)
结构型:适配器(Adapter)、代理(Proxy)、装饰器(Decorator)、外观(Facade)、组合(Composite)
行为型:策略(Strategy)、观察者(Observer)、责任链(Chain of Responsibility)、模板方法(Template Method)、命令(Command)
💡重点掌握:单例、工厂、代理、观察者、策略——这五种在 Spring 和日常开发中最常见。
6. UUID 是 32 位的 16 进制编码,怎么转换成 Base64?写出计算方式。
标准答案:
UUID 本质是一个128 位(16 字节)的二进制数。标准字符串形式(如550e8400-e29b-41d4-a716-446655440000)是 32 位十六进制 + 4 个连字符,共 36 字符。转换步骤:
- 去掉连字符,得到 32 位 hex 字符串;
- 将 hex 字符串转为 byte 数组(16 字节);
- 对 byte 数组进行 Base64 编码。
Java 示例:
StringuuidStr="550e8400-e29b-41d4-a716-446655440000";Stringhex=uuidStr.replace("-","");byte[]bytes=newBigInteger("0"+hex,16).toByteArray();// 去掉可能的符号位(BigInteger 补0导致多1字节)if(bytes.length==17&&bytes[0]==0){bytes=Arrays.copyOfRange(bytes,1,17);}Stringbase64=Base64.getEncoder().encodeToString(bytes);System.out.println(base64);// VQ6EAOKbQdSnFkRmVUQAAA==长度对比:
- UUID 字符串:36 字符
- Base64 编码:24 字符(128 bits / 6 ≈ 21.3 → 向上取整为 24,含 padding)
✅优势:Base64 更短,适合 URL 或存储空间敏感场景。
7. Java 的饿汉式和懒汉式有什么区别?
标准答案:
两者都是单例模式的实现方式。
饿汉式:类加载时就创建实例,线程安全,但可能造成资源浪费。
publicclassSingleton{privatestaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;}}懒汉式(双重检查锁 DCL):首次调用
getInstance()时才创建,节省资源,需加volatile防止指令重排序。publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}returninstance;}}现代推荐:使用静态内部类或枚举实现单例,更简洁安全。
8. 对 Spring 的 IOC 的理解
标准答案:
IOC(Inversion of Control,控制反转)是 Spring 的核心思想。传统编程中,对象由我们自己new;而 IOC 将对象的创建、依赖注入和生命周期管理交给 Spring 容器。我们只需通过配置(XML/注解)“声明”需要什么,容器自动完成装配。好处:解耦、便于测试、统一管理 Bean。
9.BeanFactory和ApplicationContext这两个 Spring 的 IOC 容器的区别
标准答案:
- BeanFactory:Spring 最底层的 IOC 容器接口,提供基本的 Bean 管理功能(如
getBean()),懒加载(使用时才创建 Bean)。- ApplicationContext:
BeanFactory的子接口,提供更多企业级特性:
- 自动注册 BeanPostProcessor、BeanFactoryPostProcessor
- 国际化支持(MessageSource)
- 事件发布机制(ApplicationEvent)
- 资源访问(ResourceLoader)
- 启动时预初始化所有单例 Bean(非懒加载)
实际开发中,几乎 always 使用
ApplicationContext(如AnnotationConfigApplicationContext)。
10. 算法题:LeetCode 搜索二维矩阵 II(LC 240)
题目:编写一个高效算法,在 m x n 矩阵中搜索目标值 target。该矩阵具有如下特性:
- 每行的整数从左到右升序排列
- 每列的整数从上到下升序排列
最优解(O(m+n)):
publicbooleansearchMatrix(int[][]matrix,inttarget){if(matrix==null||matrix.length==0)returnfalse;introw=0,col=matrix[0].length-1;// 从右上角开始while(row<matrix.length&&col>=0){if(matrix[row][col]==target){returntrue;}elseif(matrix[row][col]>target){col--;// 当前值太大,向左移动}else{row++;// 当前值太小,向下移动}}returnfalse;}思路:利用矩阵的有序性,从右上角(或左下角)出发,每次排除一行或一列。
二、面试环节:手撕代码 + 场景设计连环问
笔试结束后,技术面试官带我进入会议室,开启高强度连环追问。
面试官提问:
“请手写一个 switch 语句。”
候选人回答:
intday=3;StringdayStr;switch(day){case1:dayStr="Monday";break;case2:dayStr="Tuesday";break;case3:dayStr="Wednesday";break;default:dayStr="Unknown";}System.out.println(dayStr);// 输出 Wednesday注意点:
- Java 7+ 支持
String作为 switch 条件;- 必须加
break,否则会穿透(fall-through);default可放在任意位置,但建议放最后。
面试官提问:
“有 user 和 phone 两张表,user(id, name),phone(id, user_id, number)。请写 SQL 查询 phone 表中有一条及以上记录的 user。”
候选人回答:
这是典型的“存在性查询”,可用EXISTS或IN+ 子查询,但更高效的是GROUP BY + HAVING:-- 方法1:使用 EXISTS(推荐,性能好)SELECTu.*FROMuseruWHEREEXISTS(SELECT1FROMphone pWHEREp.user_id=u.id);-- 方法2:使用 INNER JOIN(去重需 DISTINCT)SELECTDISTINCTu.*FROMuseruINNERJOINphone pONu.id=p.user_id;-- 方法3:使用 IN(注意 NULL 问题)SELECT*FROMuserWHEREidIN(SELECTuser_idFROMphoneWHEREuser_idISNOTNULL);最佳选择:
EXISTS,因为它在找到第一条匹配记录后即可停止子查询,效率最高。
面试官提问:
“根据你的项目,假设有 A、B、C 三个任务,C 必须等待 A 和 B 都完成后才能执行,怎么实现?”
候选人回答:
在 Java 中,有多种方式实现任务依赖:方案1:使用
CountDownLatchCountDownLatchlatch=newCountDownLatch(2);newThread(()->{// 任务Alatch.countDown();}).start();newThread(()->{// 任务Blatch.countDown();}).start();// 任务Clatch.await();// 阻塞直到计数归零System.out.println("A and B done, start C");方案2(推荐):使用
CompletableFuture(更现代、灵活)CompletableFuture<Void>taskA=CompletableFuture.runAsync(()->{// 任务A逻辑});CompletableFuture<Void>taskB=CompletableFuture.runAsync(()->{// 任务B逻辑});// 等待A和B都完成,再执行CCompletableFuture.allOf(taskA,taskB).thenRun(()->{// 任务C逻辑System.out.println("A and B done, start C");}).join();优势:
CompletableFuture支持链式调用、异常处理、自定义线程池,是 Java 8+ 的首选。
面试官提问:
“要实现每月签到功能,怎么设计?”
候选人回答:
我会使用Redis 的 Bitmap(位图)来实现,这是业界标准方案。设计思路:
- 以用户 ID + 年月 作为 key,例如
sign:1001:202601- 每个月最多 31 天,用一个 31 位的 bitmap 表示
- 第 1 天对应 offset=0,第 31 天对应 offset=30
操作命令:
- 签到:
SETBIT sign:1001:202601 0 1(1号签到)- 查询是否签到:
GETBIT sign:1001:202601 0- 统计当月签到天数:
BITCOUNT sign:1001:202601- 获取连续签到天数:需结合
BITFIELD或程序逻辑计算优势:
- 极省空间:31 天仅需 4 字节(31 bits)
- 高性能:O(1) 时间复杂度
- 原子性:Redis 单命令原子
面试官追问:
“那你 int 要存储到哪里去?”
候选人回答:
这里的 “int” 指的是位偏移量(offset),它不需要单独存储。我们通过日期计算得出 offset:intdayOfMonth=LocalDate.now().getDayOfMonth();// 如 15intoffset=dayOfMonth-1;// 0-basedredisTemplate.opsForValue().setBit(key,offset,true);
面试官再追问:
“那 Redis 里存储的是什么数据?”
候选人回答:
Redis 中存储的是一个二进制位序列(bitmap),对外表现为一个 string 类型的 value。例如,如果用户在 1 号、3 号、5 号签到,则 bitmap 为:位索引: 0 1 2 3 4 5 ... 30 值: 1 0 1 0 1 0 ... 0Redis 内部将其紧凑地存储为字节数组,极大节省内存。
面试官提问:
“你项目里用了 Elasticsearch,那它的分词器怎么工作?比如歌手名字叫‘一二’,会不会被分成‘一’和‘二’?怎么保证准确搜索?”
候选人回答:
默认的中文分词器(如 standard analyzer)会将“一二”按单字切分,确实会导致过度分词,影响搜索准确性。解决方案:
1. 使用专门的中文分词器:
- ik_smart / ik_max_word:支持词典,可识别“周杰伦”为一个词;
- jieba:社区版中文分词插件。
2. 自定义词典:
在 ik 分词器的IKAnalyzer.cfg.xml中添加自定义词典文件,将“一二”加入词库,确保不分词。3. 使用 keyword 类型字段:
对于歌手名、专辑名等精确匹配字段,应设置 mapping 为keyword类型:{"mappings":{"properties":{"artist":{"type":"text","fields":{"keyword":{"type":"keyword"}}}}}}搜索时,对精确匹配使用
artist.keyword字段:{"term":{"artist.keyword":"一二"}}4. 搜索时指定 analyzer:
查询时可临时指定不分词的 analyzer,如keyword。总结:“文本搜索用 text + ik,精确匹配用 keyword”是 ES 中文搜索的最佳实践。
三、总结与建议
这场来自广州小厂“爱奇创新”的实习面试,充分体现了“基础扎实 + 场景落地”的考察导向:
- 笔试聚焦 Java 核心(线程、异常、设计模式、JVM)
- 面试强调工程能力(SQL、并发、Redis、ES)
✅ 给实习生的三大建议:
- 基础题必须零失误:如线程池、单例、异常体系,这些是“送分题”,答错直接扣印象分。
- 场景题要有方案思维:不要只说“用 Redis”,要说清“为什么用 Bitmap”、“key 如何设计”、“命令是什么”。
- 手撕代码要规范:变量命名、边界处理、注释(口头说明)都要体现专业素养。
最后寄语:
小厂面试未必简单,反而更看重动手能力和解决问题的思路。扎实的基础 + 清晰的表达 + 真实的项目经验,是你脱颖而出的关键。
如果你觉得这篇面经对你有帮助,欢迎点赞、收藏!也欢迎在评论区分享你的面试故事,我们一起进步!