记录第一次面试——momenta
简历投递
遥想年初2月就想着找实习,拖拖拉拉到了9月,期间学了Java又放,中途看着开题困难师门延毕,只能先去琢磨论文,侥幸在AI的指导下写出了些东西也算发出去了,能不能录用就算听天由命。
给拖延症一直想着“没准备好”的自己一个ddl,就是9月1号,开始挨家挨户官网投递
但一周过去了都没消息,无奈只能转战boss,在和40+hr沟通之后,终于momenta的面试官发出了邀约,并且问我北京去不去。
我和其他在mmt实习的同学了解了一下,大概日薪300+,餐补90+,还是很合适的,而且我确实没任何面试经验,急需一个真正的环境让我来检验自己的准备程度。
正式面试
面试官很和善,没我想象中那种【拷打】或是上压力,在简单了解我基本信息以及实习的时长之后,就开始了正式的面试环节,全程1小时。
简短自我介绍
我准备了一个大概一分钟的自我介绍,说明了自己基本情况和准备的项目(基本就是外卖+点评)我在点评的基础上加了点AI的东西想着弯道超车做点创新,面试官对这个挺感兴趣,聊了不少。
八股问答
八股我答的是磕磕绊绊,现在记录总结一下,问问AI看正确答案
1、 创建一个list,里面包含a,b,c三个元素,有几种方式?如果想要这个list不可变,如何实现?
我回答是用final修饰,但面试官示意我别的方法,以下是我现在用AI问出的答案
方式1:new创建ArrayList或LinkedList,然后逐个add放入
方式2:使用Arrays的函数asList,注意只能更改元素,但不能增减元素→ 固定大小,可以修改元素。
List<String> list = Arrays.asList("a", "b", "c"); list.set(0, "x"); // ✅ 可以 list.add("d"); // ❌ UnsupportedOperationException
方式3:Java9+引入List.of,真正的不可变
List<String> list = List.of("a", "b", "c");
方式4:Stream收集
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
关于不可变
方案1 不能增删改,但原始 modifiable
变了,unmodifiable
也会跟着变(只是不能通过它直接修改)
List<String> modifiable = new ArrayList<>(Arrays.asList("a", "b", "c")); List<String> unmodifiable = Collections.unmodifiableList(modifiable);
方案2 List.of List<String> list = List.of("a", "b", "c");
真正不可变,底层是immutable
方案3 Google Guava 提供的不可变集合,在一些老项目里经常见到
List<String> list = ImmutableList.of("a", "b", "c");
2、 使用哪些相关库,将Java对象序列化为json字符串,如何实现序列化与反序列化
1、Jackson
import com.fasterxml.jackson.databind.ObjectMapper; public class Demo { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); // 定义一个对象 User user = new User("Tom", 20); // 序列化:对象 -> JSON 字符串 String json = mapper.writeValueAsString(user); System.out.println(json); // {"name":"Tom","age":20} //反序列化 String json = "{\"name\":\"Tom\",\"age\":20}"; User user = mapper.readValue(json, User.class); System.out.println(user.name); // Tom } } class User { public String name; public int age; public User(String name, int age) { this.name = name; this.age = age; } }
2. Gson
import com.google.gson.Gson; Gson gson = new Gson(); User user = new User("Tom", 20); // 序列化 String json = gson.toJson(user); System.out.println(json); // {"name":"Tom","age":20} // 反序列化 User user = gson.fromJson(json, User.class); System.out.println(user.name); // Tom
3. Fastjson
import com.alibaba.fastjson.JSON; User user = new User("Tom", 20); // 序列化 String json = JSON.toJSONString(user); System.out.println(json); // {"name":"Tom","age":20} //反序列化 User user = JSON.parseObject(json, User.class); System.out.println(user.name); // Tom
3、 Java8中引入Stream流,有哪些常见API?对于一个list包含{1,2,3,4,5},使用stream如何让它转为为一个map,其中元素是key,value是key的平方,比如value分别变成1,4,9,16,25。如果list是{1,2,3,3,4}在转化为map过程中包含了重复的key3,怎么解决?
🔹 创建流
- stream():从集合创建流
- Stream.of(...):从一组值创建流
🔹 中间操作(返回新的流,惰性执行)
- filter(Predicate):过滤
- map(Function):映射(元素变形)
- flatMap(Function):扁平化映射
- distinct():去重
- sorted():排序
- limit(n) / skip(n):截断
- peek():调试时查看元素
🔹 终止操作(触发执行,返回结果)
- forEach():遍历
- collect(Collectors.toList()):收集为集合
- collect(Collectors.toMap()):收集为 Map
- reduce():归约(累计计算)
- count():计数
- anyMatch() / allMatch() / noneMatch():匹配判断
- findFirst() / findAny():查找
List→Map的方式
import java.util.*; import java.util.stream.Collectors; public class Demo { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); Map<Integer, Integer> map = list.stream() .collect(Collectors.toMap( key -> key, // key: 元素本身 value -> value * value // value: 元素平方 )); System.out.println(map); // 输出: {1=1, 2=4, 3=9, 4=16, 5=25} } }
处理重复的key
Map<Integer, Integer> map = list.stream() .collect(Collectors.toMap( key -> key, value -> value * value, (oldValue, newValue) -> oldValue // 遇到重复 key 时,保留旧值 ));
(oldValue, newValue) -> oldValue
的意思是:
- 如果 key 重复,优先保留第一个。
- 也可以改成 newValue,保留后者。
- 还可以自定义,比如 Math.max(oldValue, newValue)。
另一种方式:使用 groupingBy
如果业务需要保留所有值,可以收集成 key → List
Map<Integer, List<Integer>> map = list.stream() .collect(Collectors.groupingBy( key -> key, Collectors.mapping(value -> value * value, Collectors.toList()) )); //结果 {1=[1], 2=[4], 3=[9, 9], 4=[16]}
4、 讲讲关键字volatile的使用场景
1 作用
- 保证可见性一个线程修改了变量值,其他线程能立即看到最新值。因为 volatile 禁止了线程缓存和寄存器缓存,每次都直接从主内存读取。
- 禁止指令重排序(部分有序性)编译器和 CPU 在执行指令时可能会“乱序优化”,volatile 会在读写时插入内存屏障,保证前后指令的顺序性(有限制)。
⚠️ 注意:volatile 不能保证原子性,例如 count++
在多线程下还是线程不安全。
2 使用场景
2.1 状态标记(最经典)
比如多线程要通过一个共享变量来控制任务是否继续。
class Task implements Runnable { private volatile boolean running = true; public void stop() { running = false; } @Override public void run() { while (running) { // do something } } }
如果没有 volatile
,可能 stop()
修改了 running=false
,但其他线程一直看不到,循环停不下来。
2.2 单例模式(双重检查锁 DCL)
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 可能被重排序! } } } return instance; } }
👉 这里必须加 volatile
,防止指令重排序导致拿到“半初始化”的对象。
2.3 开关、配置热更新
- 配置类中的某些字段加 volatile,可以在运行时动态修改,其他线程立即生效。
- 比如在线系统中,可以用 volatile 变量来控制某个功能是否启用。
2.4 轻量级读多写少的场景
如果一个变量大多数时候只是被读取,偶尔被修改,而且对原子性要求不高,可以用 volatile
,性能比 synchronized
好。
5、Java中保证线程安全有哪些方式?除了加锁之外还有什么手段?
1. 加锁(重量级方案)
- synchronized
- ReentrantLock(更灵活,可中断,可定时,加条件队列)
👉 这是最直观的方式,但可能带来性能损耗。
2. 使用原子类(CAS 乐观锁)
- AtomicInteger、AtomicLong、AtomicReference 等
- 底层用 CAS(Compare-And-Swap) + 自旋实现
- 优点:不用真正加锁,性能好
- 适合 高并发的计数器、开关
比喻:就像买票时,先看票还在不在(期望值),还在就抢走(修改),不在就再试一次。
3. volatile 保证可见性
- 适合 状态标志、配置开关
- 保证变量修改能被立即看见,避免死循环
- ⚠️ 不能保证原子性
4. 线程安全的类/集合
- 早期:Vector、Hashtable(内部方法全加 synchronized)
- 现代:ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
- 内部使用 分段锁 / CAS / 写时复制 等机制提升性能
5. 不可变对象(Immutability)
- String、Integer、LocalDateTime 都是不可变的
- 思路:只读不写,天然线程安全
- 如果业务能用不可变对象,基本不用担心并发修改
👉 比喻:好像每个人拿到的都是复印件,改动不会影响别人。
6. ThreadLocal(线程隔离)
- 每个线程都有自己独立的变量副本
- 避免了多个线程同时读写同一个变量
- 常见场景:保存用户上下文、数据库连接、事务信息
👉 比喻:每个人用自己的水杯喝水,而不是共用一个杯子。
7. 无锁并发(Lock-Free 数据结构)
- 利用 CAS、自旋等实现
- Java 提供了很多 java.util.concurrent 包里的无锁结构
- 例如:ConcurrentLinkedQueue
8. 限制并发访问(协作机制)
- 使用 Semaphore 控制同时访问某资源的线程数
- 使用 CountDownLatch 或 CyclicBarrier 协调线程执行顺序
- 使用 ReadWriteLock 区分读写操作
6、有些原子类可以保证线程安全,讲讲你知道的这些原子类
原子类大体分为四类:
- 基本类型原子类AtomicInteger:原子更新 intAtomicLong:原子更新 longAtomicBoolean:原子更新 boolean✅ 常用于计数器、状态标识等场景。
- 数组类型原子类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray✅ 支持对数组中的某个索引元素进行原子更新。
- 引用类型原子类AtomicReference<T>:原子更新引用对象AtomicStampedReference<T>:带有版本号的引用,解决 ABA 问题AtomicMarkableReference<T>:带布尔标记的引用✅ 用于解决 CAS 中的 ABA问题。
- 字段更新器AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater✅ 可以针对某个对象的 volatile 字段 进行原子更新。
7、讲讲原子类和包装类的区别。如果有个Boolean对象a和一个小boolean对象b,直接用a给b赋值会出现什么问题?
1 区别
特点 | 包装类(如 | 原子类(如 |
可变性 | 不可变( | 可变(内部 |
线程安全 | 不保证线程安全 | 保证线程安全(通过 CAS 实现) |
典型用途 | 存储值,作为集合 key,缓存优化 | 多线程环境下的计数器、标志位 |
性能 | 操作简单,但多线程需加锁 | 无锁方案,性能更高 |
示例 |
|
|
👉 包装类适合做“只读值”;
👉 原子类适合做“多线程共享变量”。
2 Boolean → boolean 赋值问题
Boolean a = ...; // 包装类对象 boolean b = a; // 基本类型
这里会发生 自动拆箱(unboxing):
- Java 编译器会翻译成 b = a.booleanValue();
- 如果 a 不是 null,一切正常,比如:
- 如果 a = null,那么调用 a.booleanValue() 时会触发 NullPointerException。
8、HTTP协议中4种请求讲解一下
幂等性
1. GET
- 语义:获取资源(查)
- 特点:参数放在 URL 上(?key=value)幂等(同一个请求调用 1 次和 100 次,结果一样)一般不会修改服务器数据
- 应用场景:页面查询、获取商品详情、搜索数据
- 比喻:去饭店菜单上看看菜,不点菜、不改东西,只是“读”。
2. POST
- 语义:提交资源(增)
- 特点:参数放在请求体(body)里,不在 URL 上不幂等(提交一次和多次,结果不同,比如下单多次产生多笔订单)常用于创建数据、表单提交
- 应用场景:用户注册、提交订单、评论发布
- 比喻:你填好点菜单交给服务员,餐厅就会真的给你做菜了(动作有副作用)。
3. PUT
- 语义:更新资源(改,整体替换)
- 特点:参数放在请求体里幂等(同样的更新请求执行 1 次或 100 次,结果一样)适合 整体替换一个资源(比如替换用户信息)
- 应用场景:修改用户资料,上传文件覆盖已有文件
- 比喻:你觉得菜做错了,要求餐厅整个换掉一盘新菜。
4. DELETE
- 语义:删除资源(删)
- 特点:参数可以在 URL,也可以在 body幂等(删一次和删 100 次,最终结果都是资源不存在)
- 应用场景:删除订单、删除用户、删除文章
- 比喻:你直接跟服务员说:“这道菜不要了,拿走。”
9、GET 、 POST这些请求发往后端,肯定有请求头head,包含哪些常见内容?
一个完整的 HTTP 请求报文大致如下:
请求行(Request Line) Header(请求头) 空行 Body(请求体,可选)
- 请求行:包含方法(GET/POST)、URL、HTTP 版本
- 请求头:描述请求的元信息
- 请求体:POST/PUT 方法常用,GET 一般不带
✅ 2. 常见请求头(Header)
| 指定服务器域名和端口 |
|
| 客户端信息(浏览器/应用) |
|
| 告诉服务器客户端可接受的内容类型 |
|
| 支持的压缩方式 |
|
| 可接受的语言 |
|
| 请求体类型(POST/PUT) |
|
| 请求体长度 |
|
| 认证信息 |
|
| 携带 Cookie 信息 |
|
| 缓存控制 |
|
| 是否保持长连接 |
|
✅ 3. GET 与 POST 请求头差异
- GET一般不带请求体,所以常用请求头:Host、User-Agent、Accept、Cookie
- POST通常有请求体,需要 Content-Type、Content-Length其他头和 GET 类似
10、假设客户端向后端发送了一个POST请求,里面包含了一个文件,后端用什么方式来接受这个文件?
方法一:使用 MultipartFile
import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/upload") public class FileController { @PostMapping public String upload(@RequestParam("file") MultipartFile file) throws Exception { String filename = file.getOriginalFilename(); // 保存到服务器 file.transferTo(new java.io.File("D:/upload/" + filename)); return "上传成功: " + filename; } }
- MultipartFile 代表客户端上传的文件
- 可以获取文件名、大小、输入流、字节数组
- file.transferTo() 可以直接保存到磁盘
方法二:使用 @RequestPart
(支持 JSON + 文件混合上传)
@PostMapping("/mixed") public String uploadMixed( @RequestPart("file") MultipartFile file, @RequestPart("metadata") SomeDto metadata) { // 处理文件和 JSON 元数据 }
适合 前端同时上传文件 + 表单 JSON 数据 的场景
11、写一个接收POST请求的接口作为service,前端会传递一个表单数据,如何解析,涉及到哪些注解?
✅ 1. 前端表单提交方式
常见两种方式提交 POST 表单数据:
- application/x-www-form-urlencoded(最常见的普通表单)例如:
- multipart/form-data(带文件上传的表单)如果表单里有 <input type="file">,必须用这个类型
✅ 2. Spring Boot 接收表单数据的方法
假设前端提交普通表单数据(application/x-www-form-urlencoded
),后台解析:
方法一:使用 @RequestParam
(逐个接收字段)
import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/user") public class UserService { @PostMapping("/submit") public String submitForm(@RequestParam("username") String username, @RequestParam("age") Integer age) { return "用户名: " + username + ", 年龄: " + age; } }
- @RequestParam("字段名")将请求参数绑定到方法参数可以设置 required=false,或者 defaultValue="默认值"
方法二:使用对象绑定(推荐)
public class UserDTO { private String username; private Integer age; // getter / setter } @PostMapping("/submit2") public String submitForm2(UserDTO user) { return "用户名: " + user.getUsername() + ", 年龄: " + user.getAge(); }
- Spring MVC 会自动将表单字段映射到对象属性(前提是字段名和对象属性名一致)
- 适合字段多、维护方便
方法三:接收 JSON 表单(application/json
)
如果前端发送 JSON,而不是普通表单,需要用 @RequestBody
:
@PostMapping("/submit-json") public String submitJson(@RequestBody UserDTO user) { return "用户名: " + user.getUsername() + ", 年龄: " + user.getAge(); }
✅ 3. 相关注解总结
| 接收单个请求参数 | 普通表单、query string |
| 接收请求体 JSON | JSON API |
| 接收 multipart/form-data 的某个 part | 文件 + JSON 混合上传 |
| 将请求参数绑定到对象 | 表单绑定到 DTO |
| 返回 JSON 响应 | REST API |
| 映射 POST 请求 | Controller 方法映射 |
/
| 数据校验 | 表单或 DTO 校验 |
12、判断一个字符串是否为空有哪些方式?
1 基础判空方式
String str = ...; // 方式 1:直接判断 null if (str == null) { ... } // 方式 2:判断空串 if ("".equals(str)) { ... } if (str.length() == 0) { ... }
2 简化方式
- String.isEmpty()
if (str != null && str.isEmpty()) { ... }
等价于 str.length() == 0
。
- String.isBlank()(Java 11+)
if (str != null && str.isBlank()) { ... }
区别:isBlank
会把 " "
(全是空格)也当作空。
3 企业级开发常用工具库
在实际企业项目中,大家一般不会自己写,而是用工具库的方法,既简洁又可读性好:
- Apache Commons Lang
StringUtils.isEmpty(str); // null 或 "" → true StringUtils.isBlank(str); // null、""、空格 → true
- Spring Framework
org.springframework.util.StringUtils.hasLength(str); // 长度 > 0 org.springframework.util.StringUtils.hasText(str); // 包含非空白字符
13、你熟悉哪些数据库?MongoDB了解吗?讲解一下
MongoDB 是一个文档型 NoSQL 数据库,使用 BSON 文档存储数据,支持灵活的 schema、高并发读写和水平扩展。在 Java 中常用 Spring Data MongoDB 集成,通过 @Document
、MongoRepository
操作文档。
14、快速排序有什么特点,讲解一下
概念
特点
时间复杂度 | 平均 O(n log n),最坏 O(n²)(当数组已经有序且每次选的 pivot 最大或最小) |
空间复杂度 | O(log n)(递归栈空间) |
排序方式 | 不稳定排序 (相等元素的相对顺序可能改变) |
算法思想 | 分治法、递归 |
原地排序 | 是的,不需要额外数组(只需要递归栈) |
手撕算法
从HTTP请求开始我就答的磕磕绊绊,面试官感觉是没耐心了,最后让我手撕了一下快排,我这段时间都在看dp、图论,上次看排序算法还是2年前考研的时候,实在是想不起来了,只能勉强记得找个较大值,找个较小值交换,10分钟没手撕出来,实在羞愧难当。
最后面试官还是和善地给我点评了一下,全程1小时结束。
这么简单的手撕都没写出来,实在不好意思问hr进度了,愿各位能吸取我的教训,回头看看简单的知识点
#面试#