大家好,我是小林。
有读者跟我说,看腻了互联网面经,想看看国企的软开面试,想针对性准备一下。
说来就来!这次带大家看看中国移动的面经。
中国移动校招年总包有 20w+,不过可能实际每月到手可能是 1w 多一些,因为很多平摊到奖金和公积金里面了,所以一共加起来20万左右。
中国移动的面试相比互联网中大厂的面试强度会弱一些,通常一场技术面可能是 20 分钟左右,相比互联网中大厂能少 50-60%的强度,所以难度不会太难。
我一般会建议想冲央国企的同学,如果一开始按照中大厂面试难度准备的话,那么后面去面央国企就会感觉很简单,有一种降维打击的感觉了。
这次,我们来看看中国移动 Java 软开的校招一面,主要是问了 Java、Spring、数据结构与算法这三大块知识了,这场面试就问了 8 个技术问题,面试时长是 15 分钟。
添加图片注释,不超过 140 字(可选)
中国移动一面
Java 的优势和劣势是什么?
首先,Java的优势,我记得跨平台应该是一个大点,因为JVM的存在,一次编写到处运行。然后面向对象,这个可能也是优势,不过现在很多语言都支持面向对象,但是Java的设计从一开始就是OOP的。还有强大的生态系统,比如Spring框架,Hibernate,各种库和工具,社区支持大,企业应用广泛。另外,内存管理方面,自动垃圾回收机制,减少了内存泄漏的问题,对开发者友好。还有多线程支持,内置的线程机制,方便并发编程。安全性方面,Java有安全模型,比如沙箱机制,适合网络环境。还有稳定性,企业级应用长期使用,版本更新也比较注重向后兼容。
劣势的话,性能可能是一个,虽然JVM优化了很多,但相比C++或者Rust这种原生编译语言,还是有一定开销。特别是启动时间,比如微服务场景下,可能不如Go之类的快。语法繁琐,比如样板代码多,之前没有lambda的时候更麻烦,现在有了但比起Python还是不够简洁。内存消耗,JVM本身占内存,对于资源有限的环境可能不太友好。还有面向对象过于严格,有时候写简单程序反而麻烦,虽然Java8引入了函数式编程,但不如其他语言自然。还有开发效率,相比动态语言如Python,Java需要更多代码,编译过程也可能拖慢开发节奏。
依赖注入了解吗?怎么实现依赖注入的?
在传统编程中,当一个类需要使用另一个类的对象时,通常会在该类内部通过new关键字来创建依赖对象,这使得类与类之间的耦合度较高。
而依赖注入则是将对象的创建和依赖关系的管理交给 Spring 容器来完成,类只需要声明自己所依赖的对象,容器会在运行时将这些依赖对象注入到类中,从而降低了类与类之间的耦合度,提高了代码的可维护性和可测试性。
具体到Spring中,常见的依赖注入的实现方式,比如构造器注入、Setter方法注入,还有字段注入。
- 构造器注入:通过构造函数传递依赖对象,保证对象初始化时依赖已就绪。
@Service
public class UserService {
private final UserRepository userRepository;
// 构造器注入(Spring 4.3+ 自动识别单构造器,无需显式@Autowired)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
- Setter 方法注入:通过 Setter 方法设置依赖,灵活性高,但依赖可能未完全初始化。
public class PaymentService {
private PaymentGateway gateway;
@Autowired
public void setGateway(PaymentGateway gateway) {
this.gateway = gateway;
}
}
- 字段注入:直接通过 @Autowired 注解字段,代码简洁但隐藏依赖关系,不推荐生产代码。
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}
MyBatis觉得在哪方面做的比较好?
MyBatis 在 SQL 灵活性、动态 SQL 支持、结果集映射和与 Spring 整合方面表现卓越,尤其适合重视 SQL 可控性的项目。
- SQL 与代码解耦,灵活可控:MyBatis 允许开发者直接编写和优化 SQL,相比全自动 ORM(如 Hibernate),MyBatis 让开发者明确知道每条 SQL 的执行逻辑,便于性能调优。
<!-- 示例:XML 中定义 SQL -->
<select id="findUserWithRole" resultMap="userRoleMap">
SELECT u.*, r.role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
WHERE u.id = #{userId}
</select>
- 动态 SQL 的强大支持:比如可以动态拼接SQL,通过 , , 等标签动态生成 SQL,避免 Java 代码中繁琐的字符串拼接。
<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">AND name LIKE #{name}</if>
<if test="status != null">AND status = #{status}</if>
</where>
</select>
- 自动映射与自定义映射结合:自动将查询结果字段名与对象属性名匹配(如驼峰转换)。
<resultMap id="userRoleMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="roles" ofType="Role">
<result property="roleName" column="role_name"/>
</collection>
</resultMap>
- 插件扩展机制:可编写插件拦截 SQL 执行过程,实现分页、性能监控、SQL 改写等通用逻辑。
@Intercepts({
@Signature(type=Executor.class, method="query", args={...})
})
public class PaginationPlugin implements Interceptor {
// 实现分页逻辑
}
- 与 Spring 生态无缝集成:通过 @MapperScan 快速扫描 Mapper 接口,结合 Spring 事务管理,配置简洁高效。
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
// 数据源和 SqlSessionFactory 配置
}
HashMap和HashTable的区别?
- HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
- HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
有哪些可以替代线程不安全的HashMap?
可以通过这些方法来保证:
- 多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
- ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。
String、StringBuffer、StringBuilder的区别和联系
1、可变性 :String 是不可变的(Immutable),一旦创建,内容无法修改,每次修改都会生成一个新的对象。StringBuilder 和 StringBuffer 是可变的(Mutable),可以直接对字符串内容进行修改而不会创建新对象。
2、线程安全性 :String 因为不可变,天然线程安全。StringBuilder 不是线程安全的,适用于单线程环境。StringBuffer 是线程安全的,其方法通过 synchronized 关键字实现同步,适用于多线程环境。
3、性能 :String 性能最低,尤其是在频繁修改字符串时会生成大量临时对象,增加内存开销和垃圾回收压力。StringBuilder 性能最高,因为它没有线程安全的开销,适合单线程下的字符串操作。StringBuffer 性能略低于 StringBuilder,因为它的线程安全机制引入了同步开销。
4、使用场景 :如果字符串内容固定或不常变化,优先使用 String。如果需要频繁修改字符串且在单线程环境下,使用 StringBuilder。如果需要频繁修改字符串且在多线程环境下,使用 StringBuffer。
对比总结如下:
特性 | String | StringBuilder | StringBuffer |
---|---|---|---|
不可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是(因不可变) | 否 | 是(同步方法) |
性能 | 低(频繁修改时) | 高(单线程) | 中(多线程安全) |
适用场景 | 静态字符串 | 单线程动态字符串 | 多线程动态字符串 |
例子代码如下:
// String的不可变性
String str = "abc";
str = str + "def"; // 新建对象,str指向新对象
// StringBuilder(单线程高效)
StringBuilder sb = new StringBuilder();
sb.append("abc").append("def"); // 直接修改内部数组
// StringBuffer(多线程安全)
StringBuffer sbf = new StringBuffer();
sbf.append("abc").append("def"); // 同步方法保证线程安全
hashcode和equals方法有什么关系?
在 Java 中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定:
- 一致性:如果两个对象使用 equals 方法比较结果为 true,那么它们的 hashCode 值必须相同。也就是说,如果 obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 必须等于 obj2.hashCode()。
- 非一致性:如果两个对象的 hashCode 值相同,它们使用 equals 方法比较的结果不一定为 true。即 obj1.hashCode() == obj2.hashCode() 时,obj1.equals(obj2) 可能为 false,这种情况称为哈希冲突。
hashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。
红黑树和AVL树相比查询性能好还是插入性能好一些?
操作 | AVL 树 | 红黑树 |
---|---|---|
查询 | ⭐⭐⭐⭐⭐(更快) | ⭐⭐⭐⭐ |
插入/删除 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐(更快) |
平衡开销 | 高 | 低 |
1、查询性能的对比:
- AVL 树:AVL 树是严格的平衡二叉搜索树,它要求每个节点的左右子树的高度差(平衡因子)不超过 1。这种严格的平衡特性使得 AVL 树的高度始终保持在 O(log n),其中 n)是树中节点的数量。在进行查询操作时,由于树的高度相对较低且较为均匀,所以查找任意节点的时间复杂度稳定为 O(log n)。这意味着在理想情况下,AVL 树的查询效率非常高,能快速定位到目标节点。
- 红黑树:红黑树是一种弱平衡的二叉搜索树,它通过颜色标记和特定的规则(如每个节点要么是红色,要么是黑色;根节点是黑色;每个叶子节点(NIL 节点,空节点)是黑色;如果一个节点是红色的,则它的两个子节点都是黑色的;对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点)来维持大致的平衡。红黑树的高度通常比 AVL 树略高,其高度上限为 2log(n + 1),因此查询操作的时间复杂度同样为 (log n),但在实际应用中,由于树的高度相对较高,其查询性能可能会略逊于 AVL 树。
在查询性能上,AVL 树由于其严格的平衡特性,表现会稍好于红黑树,但差距通常不大。
2、插入性能的对比:
- AVL 树:在插入新节点后,AVL 树可能会破坏原有的平衡结构,需要通过旋转操作(单旋转或双旋转)来重新平衡树。由于 AVL 树对平衡的要求非常严格,插入操作后可能需要进行多次旋转来恢复平衡,特别是在树的高度较高时,插入操作可能会引发较多的旋转操作,导致插入性能受到一定影响。插入操作的平均时间复杂度虽然也是 O(log n),但由于旋转操作的开销,实际插入效率相对较低。
- 红黑树:红黑树在插入新节点后,同样可能会破坏树的平衡,但它只需要进行少量的颜色调整和最多两次旋转操作就能恢复平衡。红黑树的平衡规则相对宽松,使得在插入操作时不需要像 AVL 树那样频繁地进行旋转操作,因此插入性能相对较好。插入操作的平均时间复杂度同样为 O(log n),但由于减少了旋转操作的次数,实际插入效率更高。
在插入性能上,红黑树由于其弱平衡特性,表现优于 AVL 树。
在实际应用中,如果查询操作频繁,对查询性能要求较高,且插入和删除操作相对较少,可以选择 AVL 树;如果插入和删除操作较为频繁,对插入性能有较高要求,同时查询性能也能接受一定的损耗,则红黑树是更好的选择。例如,Java 中的 TreeMap 和 TreeSet 底层使用的就是红黑树,以兼顾插入、删除和查询操作的性能。
数组长度为N,找出最大的前K个值,怎么设计这个算法?时间复杂度是多少
取常见的解决方案有几种:
- 排序法:对整个数组进行排序,然后取前K个元素。比如使用快速排序,时间复杂度是O(N log N)。不过如果K远小于N,这种方法可能效率不高,因为排序整个数组没有必要。代码实现如下:
import java.util.Arrays;
public class TopKBySorting {
public static int[] topK(int[] arr, int k) {
Arrays.sort(arr);
int n = arr.length;
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = arr[n - k + i];
}
return result;
}
public static void main(String[] args) {
int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int k = 3;
int[] topK = topK(arr, k);
for (int num : topK) {
System.out.print(num + " ");
}
}
}
- 堆(优先队列):使用一个最小堆,维护K个最大的元素。遍历数组,当堆的大小小于K时直接加入,否则比较当前元素和堆顶,如果更大就替换堆顶,并调整堆。这样时间复杂度是O(N log K),因为每次堆操作是O(log K),需要进行N次。这种方法适合处理大数据流的情况,因为不需要一次性加载所有数据到内存。代码实现如下:
import java.util.PriorityQueue;
public class TopKByMinHeap {
public static int[] topK(int[] arr, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int num : arr) {
if (minHeap.size() < k) {
minHeap.offer(num);
} else if (num > minHeap.peek()) {
minHeap.poll();
minHeap.offer(num);
}
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = minHeap.poll();
}
return result;
}
public static void main(String[] args) {
int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int k = 3;
int[] topK = topK(arr, k);
for (int num : topK) {
System.out.print(num + " ");
}
}
}
- 快速选择算法:基于快速排序的partition思想,找到第K大的元素,然后取前K个。平均时间复杂度是O(N),最坏情况下是O(N²),但可以通过随机化选择pivot来优化,使得最坏情况概率很低。这种方法在数据可以全部放入内存时效率很高,尤其是当K比较大时。代码实现如下:
import java.util.Arrays;
public class TopKByQuickSelect {
public static int[] topK(int[] arr, int k) {
quickSelect(arr, 0, arr.length - 1, k);
int[] result = Arrays.copyOfRange(arr, arr.length - k, arr.length);
Arrays.sort(result);
return result;
}
private static void quickSelect(int[] arr, int left, int right, int k) {
if (left < right) {
int pivotIndex = partition(arr, left, right);
if (pivotIndex > arr.length - k) {
quickSelect(arr, left, pivotIndex - 1, k);
} else if (pivotIndex < arr.length - k) {
quickSelect(arr, pivotIndex + 1, right, k);
}
}
}
private static int partition(int[] arr, int left, int right) {
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right);
return i + 1;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int k = 3;
int[] topK = topK(arr, k);
for (int num : topK) {
System.out.print(num + " ");
}
}
}
具体怎么选,可以根据具体需求选择合适的方法:
-
如果 K 很小,推荐使用 最小堆 方法,其时间复杂度为 O(NlogK)。
-
如果 K 较大或需要线性时间复杂度,推荐使用 快速选择 方法,其平均时间复杂度为 O(N)。
-
如果对实现复杂度要求较低且 K 接近 N,可以直接使用 排序法 ,时间复杂度为 O(NlogN)。
更多优质文章
- 《图解网络》 :500 张图 + 15 万字贯穿计算机网络重点知识,如HTTP、HTTPS、TCP、UDP、IP等协议
- 《图解系统》:400 张图 + 16 万字贯穿操作系统重点知识,如进程管理、内存管理、文件系统、网络系统等
- 《图解MySQL》:重点突击 MySQL 索引、存储引擎、事务、MVCC、锁、日志等面试高频知识
- 《图解Redis》:重点突击 Redis 数据结构、持久化、缓存淘汰、高可用、缓存数据一致性等面试高频知识
- 《Java后端面试题》:涵盖Java基础、Java并发、Java虚拟机、Spring、MySQL、Redis、计算机网络等企业面试题
- 《大厂真实面经》:涵盖互联网大厂、互联网中厂、手机厂、通信厂、新能源汽车厂、银行等企业真实面试题