记录第一次面试——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字符串,如何实现序列化与反序列化

  • Jackson(Spring 默认用的)优点:功能强大、Spring Boot 自动集成。用法:简单,注解支持多,性能均衡。
  • Gson(Google 出品)优点:轻量、易上手。缺点:对新特性支持不如 Jackson(比如 Java 8 时间)。
  • Fastjson / Fastjson2(阿里出品)优点:性能快,用于国内项目很多。缺点:历史上有安全漏洞,需要注意版本。
  • 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 作用

    1. 保证可见性一个线程修改了变量值,其他线程能立即看到最新值。因为 volatile 禁止了线程缓存和寄存器缓存,每次都直接从主内存读取。
    2. 禁止指令重排序(部分有序性)编译器和 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、有些原子类可以保证线程安全,讲讲你知道的这些原子类

    原子类大体分为四类:

    1. 基本类型原子类AtomicInteger:原子更新 intAtomicLong:原子更新 longAtomicBoolean:原子更新 boolean✅ 常用于计数器、状态标识等场景。
    2. 数组类型原子类AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray✅ 支持对数组中的某个索引元素进行原子更新。
    3. 引用类型原子类AtomicReference<T>:原子更新引用对象AtomicStampedReference<T>:带有版本号的引用,解决 ABA 问题AtomicMarkableReference<T>:带布尔标记的引用✅ 用于解决 CAS 中的 ABA问题。
    4. 字段更新器AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater✅ 可以针对某个对象的 volatile 字段 进行原子更新。

    7、讲讲原子类和包装类的区别。如果有个Boolean对象a和一个小boolean对象b,直接用a给b赋值会出现什么问题?

    1 区别

    特点

    包装类(如 Integer, Boolean

    原子类(如 AtomicInteger, AtomicBoolean

    可变性

    不可变(valuefinal

    可变(内部 volatile value 可被 CAS 修改)

    线程安全

    不保证线程安全

    保证线程安全(通过 CAS 实现)

    典型用途

    存储值,作为集合 key,缓存优化

    多线程环境下的计数器、标志位

    性能

    操作简单,但多线程需加锁

    无锁方案,性能更高

    示例

    Boolean a = true;

    AtomicBoolean flag = new AtomicBoolean(false); flag.compareAndSet(false, true);

    👉 包装类适合做“只读值”;

    👉 原子类适合做“多线程共享变量”。

    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)

    Host

    指定服务器域名和端口

    Host: www.example.com

    User-Agent

    客户端信息(浏览器/应用)

    User-Agent: Mozilla/5.0 ...

    Accept

    告诉服务器客户端可接受的内容类型

    Accept: text/html,application/json

    Accept-Encoding

    支持的压缩方式

    Accept-Encoding: gzip, deflate

    Accept-Language

    可接受的语言

    Accept-Language: en-US,en;q=0.9

    Content-Type

    请求体类型(POST/PUT)

    Content-Type: application/json

    Content-Length

    请求体长度

    Content-Length: 123

    Authorization

    认证信息

    Authorization: Bearer <token>

    Cookie

    携带 Cookie 信息

    Cookie: JSESSIONID=xxx

    Cache-Control

    缓存控制

    Cache-Control: no-cache

    Connection

    是否保持长连接

    Connection: keep-alive

    ✅ 3. GET 与 POST 请求头差异

    • GET一般不带请求体,所以常用请求头:Host、User-Agent、Accept、Cookie
    • POST通常有请求体,需要 Content-Type、Content-Length其他头和 GET 类似

    10、假设客户端向后端发送了一个POST请求,里面包含了一个文件,后端用什么方式来接受这个文件?

  • POST 上传文件,一般使用 multipart/form-data 类型
  • Spring Boot 常用 MultipartFile 接收
  • 可以获取:文件名、大小、字节流、保存到服务器
  • 如果不用 Spring,也可以用原生 Servlet Part 或 Apache Commons FileUpload
  • 方法一:使用 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 表单数据:

    1. application/x-www-form-urlencoded(最常见的普通表单)例如:
    2. 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. 相关注解总结

    @RequestParam

    接收单个请求参数

    普通表单、query string

    @RequestBody

    接收请求体 JSON

    JSON API

    @RequestPart

    接收 multipart/form-data 的某个 part

    文件 + JSON 混合上传

    @ModelAttribute

    将请求参数绑定到对象

    表单绑定到 DTO

    @RestController

    返回 JSON 响应

    REST API

    @PostMapping

    映射 POST 请求

    Controller 方法映射

    @Valid

    /

    @Validated

    数据校验

    表单或 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 集成,通过 @DocumentMongoRepository 操作文档。

    14、快速排序有什么特点,讲解一下

    概念

  • QuickSort 是一种 分治法(Divide and Conquer) 排序算法
  • 核心思想:选取一个 基准值(pivot)将数组分成 两部分:小于基准值的放左边,大于基准值的放右边递归对左右两部分继续排序
  • 最终数组就是有序的
  • 特点

    时间复杂度

    平均 O(n log n),最坏 O(n²)(当数组已经有序且每次选的 pivot 最大或最小)

    空间复杂度

    O(log n)(递归栈空间)

    排序方式

    不稳定排序

    (相等元素的相对顺序可能改变)

    算法思想

    分治法、递归

    原地排序

    是的,不需要额外数组(只需要递归栈)

    手撕算法

    从HTTP请求开始我就答的磕磕绊绊,面试官感觉是没耐心了,最后让我手撕了一下快排,我这段时间都在看dp、图论,上次看排序算法还是2年前考研的时候,实在是想不起来了,只能勉强记得找个较大值,找个较小值交换,10分钟没手撕出来,实在羞愧难当。

    最后面试官还是和善地给我点评了一下,全程1小时结束。

    这么简单的手撕都没写出来,实在不好意思问hr进度了,愿各位能吸取我的教训,回头看看简单的知识点

    #面试#
    全部评论

    相关推荐

    点赞 评论 收藏
    分享
    评论
    1
    1
    分享

    创作者周榜

    更多
    牛客网
    牛客网在线编程
    牛客网题解
    牛客企业服务