将 脚本之家 设为“星标⭐”
第一时间收到文章更新
文 | 小林coding
出品 | 小林coding(ID:CodingLin )
好家伙,美的现在开始实行强制下班了,自愿加班都不行!
必须在 18 点 20 前把下班这个动作完成,规定不允许18 点 20 后还有人在公司加班,甚至到下班点了,HR 还会催大家抓紧下班了,生怕你不下班。
不仅是美的实行了“反内卷”,大疆和海尔等企业也开始严控员工加班的行为。
大疆要求员工每天21点前下班(大疆上班时间早上十点半开始),海尔从单休调整成强制双休。
为什么这些制造业为主的企业,突然开始“反内卷”了呢?
欧盟 2024 年 12 月通过一个法案,《禁止强迫劳动法案》,按照这个法案,所有进入欧盟市场的商品,生产过程中都不能涉及任何形式的强迫劳动,供应链的各个环节也都得注意。
哪怕只是一个零件,要是生产的时候不符合五天八小时工作制度,或者存在强迫劳动情况,那这商品就没法在欧盟卖了,即使员工自愿加班也不行。
欧盟的这个方案去年已经签署生效,不过还有个过渡期,会在 3 年后开始全面实施,所以各大制造业企业,为了避免被欧盟市场拉黑,就开始严控加班了。
回归我们的正题
之前已经分析过大疆和美的的面经了,那这次我们来看看海尔的面经。
海尔是以家电制造为主,Java 软开的岗位肯定是没有互联网公司多的,我也是找了很久才找到海尔的Java 软开面经,而且还是几年前的,不过面试的问题还是蛮经典,还是值得学习一波。
海尔(一面通过)
Redis 有遇到过缓存击穿和缓存雪崩的情况吗?怎么解决的?
-
缓存雪崩:当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
-
缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
-
缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存雪崩解决方案:
-
均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
-
互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
-
后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
缓存击穿解决方案:
-
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
-
不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透解决方案:
-
非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
-
缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
-
布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
讲讲 Spring Boot 加载初始化的过程?
SpringBoot是一个服务Spring框架的框架,能够简化配置文件,快速构建web应用,内置tomcat,无需打包部署,直接运行。 当我们启动一个SpringBoot应用的时候,都会用到如下的启动类:
@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }
只要加上@SpringBootApplication,然后执行run()方法,就可以启动一个应用程序,启动的流程如下:
首先从main找到run()方法,在执行run()方法之前new一个SpringApplication对象
进入run()方法,创建应用监听器SpringApplicationRunListeners开始监听
然后加载SpringBoot配置环境(ConfigurableEnvironment),然后把配置环境(Environment)加入监听对象中
然后加载应用上下文(ConfigurableApplicationContext),当做run方法的返回对象
最后创建Spring容器,refreshContext(context),实现starter自动化配置和bean的实例化等工作。
平时常用的注解有哪些?
Bean 相关:
-
@Component:将一个类标识为 Spring 组件(Bean),可以被 Spring 容器自动检测和注册。通用注解,适用于任何层次的组件。
-
@ComponentScan:自动扫描指定包及其子包中的 Spring 组件。
-
@Controller:标识控制层组件,实际上是 @Component 的一个特化,用于表示 Web 控制器。处理 HTTP 请求并返回视图或响应数据。
-
@RestController:是 @Controller 和 @ResponseBody 的结合,返回的对象会自动序列化为 JSON 或 XML,并写入 HTTP 响应体中。
-
@Repository:标识持久层组件(DAO 层),实际上是 @Component 的一个特化,用于表示数据访问组件。常用于与数据库交互。
-
@Bean:方法注解,用于修饰方法,主要功能是将修饰方法的返回对象添加到 Spring 容器中,使得其他组件可以通过依赖注入的方式使用这个对象。
依赖注入:
-
@Autowired:用于自动注入依赖对象,Spring 框架提供的注解。
-
@Resource:按名称自动注入依赖对象(也可以按类型,但默认按名称),JDK 提供注解。
-
@Qualifier:与 @Autowired 一起使用,用于指定要注入的 Bean 的名称。当存在多个相同类型的 Bean 时,可以使用 @Qualifier 来指定注入哪一个。
读取配置:
-
@Value:用于注入属性值,通常从配置文件中获取。标注在字段上,并指定属性值的来源(如配置文件中的某个属性)。
-
@ConfigurationProperties:用于将配置属性绑定到一个实体类上。通常用于从配置文件中读取属性值并绑定到类的字段上。
Web相关:
-
@RequestMapping:用于映射 HTTP 请求到处理方法上,支持 GET、POST、PUT、DELETE 等请求方法。可以标注在类或方法上。标注在类上时,表示类中的所有响应请求的方法都是以该类路径为父路径。
-
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping:分别用于映射 HTTP GET、POST、PUT、DELETE 请求到处理方法上。它们是 @RequestMapping 的特化,分别对应不同的 HTTP 请求方法。
其他常用注解:
-
@Transactional:声明事务管理。标注在类或方法上,指定事务的传播行为、隔离级别等。
-
@Scheduled:声明一个方法需要定时执行。标注在方法上,并指定定时执行的规则(如每隔一定时间执行一次)。
MyBatis 有用过吗?说说 MyBatis 有什么特点?
用过,项目中有用到。MyBatis 的特点如下:
-
基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任 何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
-
与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不 需要手动开关连接;
-
很好的与各种数据库兼容,因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持。
-
能够与 Spring 很好的集成,开发效率高
-
提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射 标签,支持对象关系组件维护。
Git 使用熟练吗?
熟悉,项目的代码是用 git 工具来管理的/
git 的工作原理如下图:
几个专用名词的译名如下:
-
Workspace:工作区
-
Index / Stage:暂存区
-
Repository:仓库区(或本地仓库)
-
Remote:远程仓库
工作流程
最基础的工作流程,首先执行 git pull 获取远程仓库的最新代码,进行代码的编写。
完成相应功能的开发后执行 git add . 将工作区代码的修改添加到暂存区,再执行 git commit -m 完成xx功能 将暂存区代码提交到本地仓库并添加相应的注释,最后执行 git push 命令推送到远程仓库。
撤回 git commit 操作
当执行了 git commit -m 注释内容 命令想要撤回,可以使用 git reset --soft HEAD^ 把本地仓库回退到当前版本的上一个版本,也就是刚刚还没提交的时候,代码的改动会保留在暂存区和工作区。
也可以使用 git reset --mixed HEAD^,这样不止回退了刚刚的 git commit 操作,还回退了 git add 操作,代码的改动只会保留在工作区。因为 --mixed 参数是 git reset 命令的默认选项,也就是可以写为 git reset HEAD^。
撤回 git push 操作
当执行了 git push 命令想要撤回,可以使用 git reset HEAD^ 将本地仓库回退到当前版本的上一个版本,代码的修改会保留在工作区,然后使用 git push origin xxx --force 将本地仓库当前版本的代码强制推送到远程仓库。
说说 Java 中的异常有几大类?
Java异常类层次结构图:
Java的异常体系主要基于两大类:Throwable类及其子类。Throwable有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。
-
Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError等。
-
Exception(异常):表示程序本身可以处理的异常条件。异常分为两大类:
-
非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
-
运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。
Java 8 你知道有什么新特性?
下面是 Java 8 主要新特性的整理表格,包含关键改进和示例说明:
Lambda 表达式 |
简化匿名内部类,支持函数式编程 |
(a, b) -> a + b
代替匿名类实现接口 |
函数式接口 |
仅含一个抽象方法的接口,可用 @FunctionalInterface 注解标记 |
Runnable
, Comparator, 或自定义接口 @FunctionalInterface interface MyFunc { void run(); } |
Stream API |
提供链式操作处理集合数据,支持并行处理 |
list.stream().filter(x -> x > 0).collect(Collectors.toList()) |
Optional 类 |
封装可能为 null 的对象,减少空指针异常 |
Optional.ofNullable(value).orElse("default") |
方法引用 |
简化 Lambda 表达式,直接引用现有方法 |
System.out::println
等价于 x -> System.out.println(x) |
接口的默认方法与静态方法 |
接口可定义默认实现和静态方法,增强扩展性 |
interface A { default void print() { System.out.println("默认方法"); } } |
并行数组排序 |
使用多线程加速数组排序 |
Arrays.parallelSort(array) |
重复注解 |
允许同一位置多次使用相同注解 |
@Repeatable
注解配合容器注解使用 |
类型注解 |
注解可应用于更多位置(如泛型、异常等) |
List<@NonNull String> list |
CompletableFuture |
增强异步编程能力,支持链式调用和组合操作 |
CompletableFuture.supplyAsync(() -> "result").thenAccept(System.out::println) |
Lambda 表达式了解吗?
Lambda 表达式它是一种简洁的语法,用于创建匿名函数,主要用于简化函数式接口(只有一个抽象方法的接口)的使用。其基本语法有以下两种形式:
-
(parameters) -> expression:当 Lambda 体只有一个表达式时使用,表达式的结果会作为返回值。
-
(parameters) -> { statements; }:当 Lambda 体包含多条语句时,需要使用大括号将语句括起来,若有返回值则需要使用 return 语句。
传统的匿名内部类实现方式代码较为冗长,而 Lambda 表达式可以用更简洁的语法实现相同的功能。比如,使用匿名内部类实现 Runnable 接口
public class AnonymousClassExample { public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("Running using anonymous class"); } }); t1.start(); }}
使用 Lambda 表达式实现相同功能:
public class LambdaExample { public static void main(String[] args) { Thread t1 = new Thread(() -> System.out.println("Running using lambda expression")); t1.start(); }}
可以看到,Lambda 表达式的代码更加简洁明了。
还有,Lambda 表达式能够更清晰地表达代码的意图,尤其是在处理集合操作时,如过滤、映射等。比如,过滤出列表中所有偶数
import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;public class ReadabilityExample { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); // 使用 Lambda 表达式结合 Stream API 过滤偶数 List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(evenNumbers); }}
通过 Lambda 表达式,代码的逻辑更加直观,易于理解。
还有,Lambda 表达式使得 Java 支持函数式编程范式,允许将函数作为参数传递,从而可以编写更灵活、可复用的代码。比如定义一个通用的计算函数。
interface Calculator { int calculate(int a, int b);}publicclass FunctionalProgrammingExample { public static int operate(int a, int b, Calculator calculator) { return calculator.calculate(a, b); } public static void main(String[] args) { // 使用 Lambda 表达式传递加法函数 int sum = operate(3, 5, (x, y) -> x + y); System.out.println("Sum: " + sum); // 使用 Lambda 表达式传递乘法函数 int product = operate(3, 5, (x, y) -> x * y); System.out.println("Product: " + product); }}
虽然 Lambda 表达式优点蛮多的,不过也有一些缺点,比如会增加调试困难,因为 Lambda 表达式是匿名的,在调试时很难定位具体是哪个 Lambda 表达式出现了问题。尤其是当 Lambda 表达式嵌套使用或者比较复杂时,调试难度会进一步增加。
说说 HashMap 的底层原理?
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。
所以在 JDK 1.8 版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。
Docker 有了解吗?
了解过,大家一起开发一个软件时,每个人的电脑就像不同的房子,里面的家具(软件、工具等环境配置)都不太一样。用 Docker 就可以打造一个标准的 “样板房”,不管谁拿到这个 “样板房”(Docker 镜像),里面的东西都是一样的。这样开发时就不会因为环境不同,出现代码在这个人电脑上能跑,在另一个人电脑上就出错的情况。
docker 底层的隔离实现原理如下:
-
基于 Namespace 的视图隔离:Docker利用Linux命名空间(Namespace)来实现不同容器之间的隔离。每个容器都运行在自己的一组命名空间中,包括PID(进程)、网络、挂载点、IPC(进程间通信)等。这样,容器中的进程只能看到自己所在命名空间内的进程,而不会影响其他容器中的进程。
-
基于 cgroups 的资源隔离:cgroups 是Linux内核的一个功能,允许在进程组之间分配、限制和优先处理系统资源,如CPU、内存和磁盘I/O。它们提供了一种机制,用于管理和隔离进程集合的资源使用,有助于资源限制、工作负载隔离以及在不同进程组之间进行资源优先处理。
Jenkins 有了解吗?讲讲什么是持续集成和持续部署?
Jenkins 是开源的自动化服务器,核心功能是实现 CI/CD 流程的调度和管理。例子,一个软件开发团队使用 Jenkins 作为 CI/CD 工具,开发人员每天多次将代码提交到 Git 仓库,Jenkins 会自动触发构建和测试任务。如果代码通过了所有测试,Jenkins 会将代码自动部署到生产环境,让用户能够尽快使用到新的功能。
持续集成(Continuous Integration,CI)
持续集成是什么?开发人员频繁地将代码变更合并到主分支(如每天多次),每次合并后自动触发构建、测试和代码检查,确保新代码不会破坏已有功能。核心目标:快速发现并修复集成问题,避免“集成地狱”。
关键流程:
代码提交 → 触发自动化流程。
代码编译 → 检查语法错误。
运行单元测试 → 确保新代码不影响现有功能。
代码质量检查 → 如 SonarQube 分析代码规范。
生成报告 → 失败时通知团队,成功则生成可部署的产物(如 Jar 包)。
持续部署(Continuous Deployment,CD)
持续部署是什么?自动化将代码发布到生产环境,只要通过 CI 流程的代码变更,无需人工干预即可直接上线。核心目标:快速、安全地交付用户价值。
关键流程:
从 CI 产物出发 → 如通过测试的 Jar 包。
环境部署 → 自动部署到测试、预发布、生产环境。
自动化验收测试 → 验证功能是否符合需求。
监控与回滚 → 发现生产问题自动回退版本。
假设团队开发一个 Java Web 项目:
-
持续集成(CI):开发者提交代码到 Git → Jenkins 自动拉取代码 → 运行 Maven 构建 → 执行单元测试 → 生成测试报告。如果测试失败,邮件通知负责人;成功则生成 War 包。
-
持续部署(CD):先将 War 包自动部署到 Tomcat 服务器,然后使用 Docker 或 Kubernetes 实现滚动更新,零停机部署。
项目问题
-
你觉得这个项目做起来最难的地方是什么?
-
你们团队有多少人完成这个项目?
-
你的项目最高同时在线人数有多少?项目是怎么部署的?
聊天问题
-
你什么时候开始接触 Java 的?
-
你本科专业不是计算机,本科学过编程吗?
-
你是哪里人?研究生的课题是什么?过来的话打算租房子吗?
面试感受
听同学说,海尔这次的面试体验相当不错。面试官特别靠谱,早早地就进会议了,开场就说别紧张,就当唠唠嗑,语气那叫一个温和亲切。面试的时候,还主动跟同学聊起租房子的事,给了好多实用的建议,这波好感拉满了。
END
看完这篇文章你有什么想说的?欢迎各位评论区聊一聊
留言抽 3位 小伙伴,赠送8.88元现金红包!
下面顺便给大家推荐一个不错的AI工具聚合平台,已接入自部署满血版 DeepSeek-R1,欢迎来体验。
点击长按二维码,即可体验:
👇👇👇
✅传送门:https://www.aijuli.com