⼋股
蔚来 Java ⾯试
昨天跟⼤家说了理想汽⻋ 25 届校招薪资,有同学想看看 蔚来⻋企的薪资情况。
蔚来 25 届开发岗位的普通档年包在 35w 左右,优秀突出的也能拿到 40w+ 年薪,具体薪资情况
如下:
普通 offer :23.5k* (13+1.5 ) + 630 股,同学 bg 硕⼠ 211
ssp offer :28k* (13+1.5 ) + 880 股,同学 bg 硕⼠ 985
蔚来⼀年是发 13 薪资,年终奖是 1.5 个⽉,但是也不 ⼀定能拿满,主要还是看公司的效益,然后
还给了配股,分 5 年发,也就是在公司待满 5 年才能全部拿到。
这次,来看看 蔚来 Java 后端的校招⾯经,主要拷打了Java 、MySQL 这两个 ⽅⾯的问题,最后也有
算法⼿撕环节,整体⾯试问题不算多,难度相⽐互联⽹⼤⼚是⼩了⼀些,但是算法依旧躲不了 。

⼋股
JVM 内存结构介绍⼀下?

根据 JVM8 规范,JVM 运⾏时内存共分为虚拟机栈、堆、元空间、程序计数器、本地⽅法栈五个
部分。还有⼀部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM 的内存结构主要分为以下⼏个部分:
元空间:元空间的本质和永久代类似,都是对JVM 规范中⽅法区的实现。不过元空间与永久代
之间最⼤的区别在于:元空间并不在虚拟机中,⽽是使⽤本地内存。
Java 虚拟机栈:每个线程有⼀个私有的栈,随着线程的创建⽽创建。栈⾥⾯存着的是⼀种叫“栈
帧”的东西,每个⽅法会创建⼀个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引
⽤)、操作数栈、⽅法出⼝等信息。栈的⼤⼩可以固定也可以动态扩展。
本地⽅法栈:与虚拟机栈类似,区别是虚拟机栈执⾏java ⽅法,本地⽅法站执⾏native ⽅法。在
虚拟机规范中对本地⽅法栈中⽅法使⽤的语⾔、使⽤⽅法与数据结构没有强制规定,因此虚拟
机可以⾃由实现它。
程序计数器:程序计数器可以看成是当前线程所执 ⾏的字节码的⾏号指⽰器。在任何 ⼀个确定
的时刻,⼀个处理器( 对于多内核来说是⼀个内核)都只会执⾏⼀条线程中的指令。因此,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要⼀个独⽴的程序计数器, 我们称这类
内存区域为“线程私有”内存。
堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和
数组都在堆上进⾏分配。这部分空间可通过 GC 进⾏回收。当申请不到空间时会抛出
OutOfMemoryError 。堆是JVM 内存占⽤最⼤,管理最复杂的⼀个区域。其唯⼀的⽤途就是存放
对象实例:所有的对象实例及数组都在对上进⾏分配。jdk1.8 后,字符串常量池从永久代中剥离
出来,存放在队中。
直接内存:直接内存并不是虚拟机运⾏时数据区的⼀部分,也不 是Java 虚拟机规范中定义的内
存区域。在JDK1.4 中新加⼊了NIO(New Input/Output) 类,引⼊了⼀种基于通道 (Channel) 与缓冲区(Buffer )的I/O ⽅式,它可以使 ⽤native 函数库直接分配堆外内存,然后通脱⼀个存储在
Java 堆中的DirectByteBuffer 对象作为这块内存的引⽤进⾏操作。这样能在⼀些场景中显著提⾼
性能,因为避免了在Java 堆和Native 堆中来回复制数据。
线程为什么 需要本地内存,不直接读主内存?
Java 线程之间的通信采⽤的是共享内存模型,这⾥提到的共享内存模型指的就是Java 内存模型(简
称JMM) ,JMM 决定⼀个线程对共享变量的写⼊何时对另⼀个线程可⻅。从抽象的⻆度来看,JMM 定义了 线程和主内存之间的抽象关系:线程之间的共享变量存储在主内
存(main memory )中,线程被CPU 执⾏,每个线程都有⼀个私有的本地内存(如CPU 的⾼速缓
存),本地内存中存储了该线程以读/写共 享变量的副本。
本地内存是JMM 的⼀个抽象概念,并不真实存 在;它涵盖了缓存,写缓冲区,寄存 器以及其他的
硬件和编译器优化。

Java 内存模型规定所有的变量都 是存在主存当中(类似于前⾯说的物理内存),每个线程都有⾃⼰
的⼯作内存(⽐如CPU 的⾼速缓存)。
线程对变量的所有操作都必须在⼯作内存中进⾏,⽽不能直接对主存进⾏操作,并且每个线程不
能访问其他线程的⼯作内存。
之所以要⽤⾃⼰的本地内存,主要是利⽤缓存和改变执⾏代码顺序达到程序执⾏效率优化。
例如下⾯代码:
int a1 = x;
int a2 = y;
int a3 = x;可能会被转化为:
int a2 = y;
int a1 = x;
int a3 = x;或者是
int a1 = x;
int a2 = y;
int a3 = a1;这样和最初的代码相⽐,少读x⼀次。
volatile 和synchronized 有什么 区别详细介绍⼀下?
Synchronized 解决了多线程访问共享资源时可能出现的竞态条件和数据不⼀致的问题,保证了线程
安全性。Volatile 解决了变量在多线程环境下的可⻅性和有序性问题,确保了变量的修改对其他线
程是可⻅的。
Synchronized : Synchronized 是⼀种排他性的同步机制,保证了多个线程访问共享资源时的互斥
性,即同⼀时刻只允许⼀个线程访问共享资源。通过对代码块或⽅法添加Synchronized 关键字
来实现同步。
Volatile : Volatile 是⼀种轻量级的同步机制,⽤来保证变量的可⻅性和禁⽌指令重排序。当⼀个
变量被声明为Volatile 时,线程在读取该变量时会直接从内存中读取,⽽不会使 ⽤缓存,同时对
该变量的写操作会 ⽴即刷回主内存,⽽不是缓存在本地内存中。
mysql 和pgsql 区别是什么 ?
int a1 = x; int a2 = y; int a3 = x;
int a2 = y; int a1 = x; int a3 = x;
int a1 = x; int a2 = y; int a3 = a1;MySQL 是应⽤开发者创建出来的DBMS ;⽽PostgreSQL 是由数据库开发者创建出来的DBMS 。
换句话说,MySQL 倾向于使⽤者的⻆度,回答的问题是 “你想解决的是什么 问题”;⽽
PostgreSQL 倾向于理论⻆度,回答的问题是 “数据库应 该如何来解决问题” 。
PostgreSQL ⽀持 JSON 、XML 等现代应⽤程序功能,同时 MySQL 仅⽀持 JSON 。
PostgreSQL 完全符合 ACID 标准,同时 MySQL 仅与 InnoDB 存储引擎 ⼀起使⽤时才符合 ACID
标准。
⽐较 PostgreSQL vs MySQL 性能, PostgreSQL 执⾏复杂查 询时表现良好,⽽ MySQL 在 OLAP
和 OLTP 系统中表现良好。
MySQL ⼀般会将数据合法性验证交给客⼾;PostgreSQL 在合法性难⽅⾯做得⽐较严格。⽐如
MySQL ⾥插⼊ “2012-02-30” 这个时间时,会成功,但结果会是 “0000-00-00” ;PostgreSQL 不
允许插⼊此值。
MySQL 由于⼴泛的应⽤和⽀持,拥有庞⼤的⽤⼾社区和丰富的⼯具包。 PostgreSQL 社区相对
较⼩,但是开发者活跃,技术较为严 谨,适合对安全性、标准化要求⾼的场景
MySQL 更适合快速开发和⾼并发的应⽤,⽽PostgreSQL 则更适合需要⾼度稳定性和⾼级特性
的企业级应⽤
mysql 索引底层数据结构是什么 ?
MySQL InnoDB 引擎是⽤了B+ 树作为了 索引的数据结构。
B+Tree 是⼀种多叉树,叶⼦节点才存放数 据,⾮叶⼦节点只存放索引,⽽且每个节点⾥的数据是
按主键顺序存放的。每⼀层⽗节点的索引值都会出现在下层⼦节点的索引值中,因此在叶⼦节点
中,包括了所有的索引值信息,并且每⼀个叶⼦节点都有两个 指针,分别 指向下⼀个叶⼦节点和
上⼀个叶⼦节点,形成⼀个双向链表。
主键索引的 B+Tree 如图所⽰:

⽐如,我们执⾏了下 ⾯这条查 询语句:
这条语句使⽤了主 键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会⾃顶向下逐层进⾏
查找:
将 5 与根节点的索引数据 (1 ,10 ,20) ⽐较,5 在 1 和 10 之间,所以根据 B+Tree 的搜索逻
辑,找到第⼆层的索引数据 (1 ,4,7) ;
在第⼆层的索引数据 (1 ,4,7) 中进⾏查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数
据(4,5,6);在叶⼦节点的索引数据(4,5,6)中进⾏查找,然后我们找到了索引值为 5 的⾏数据。
数据库的索引和数据都是存储在硬盘的 ,我们可以把读取⼀个节点当作⼀次磁盘 I/O 操作。那么上⾯的整个查询过程⼀共经历了 3 个节点,也就是进⾏了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层⾼度就可以满⾜,这意味着从千万级的表查询⽬标数据最
多需要 3-4 次磁盘 I/O ,所以B+Tree 相⽐于 B 树和⼆叉树来说,最⼤的优势在于查询效率很⾼,
因为即使在数据量很⼤的情况,查询⼀个数据的磁盘 I/O 依然维持在 3-4 次。
你怎么理解最左匹配原则的?
通过将多个字段组合成⼀个索引,该索引就被称为联合索引。
⽐如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name) ,创建联合索引的⽅式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name) 的 B+Tree ⽰意图如下(图中叶⼦节点之间我画了单向链表,但是实际上是双向链表,原图我找 不到了,修改不了 ,偷个懒我不重画了,⼤家脑补成双向链表就⾏)。

可以看到,联合索引的⾮叶⼦节点⽤两个 字段的值作为 B+Tree 的 key 值。当在联合索引查询数据
时,先按 product_no 字段⽐较,在 product_no 相同的情况下再按 name 字段⽐较。
也就是说,联合索引查询的 B+Tree 是先按 product_no 进⾏排序,然后再 product_no 相同的情况再按 name 字段排序。
因此,使⽤联合索引时,存在最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配。在使
⽤联合索引进⾏查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就⽆法利⽤到
索引快 速查询的特性了。
⽐如,如果创建了⼀个 (a, b, c) 联合索引,如果查 询条件是以下这⼏种,就可以匹配上联合索引:
where a=1 ;
where a=1 and b=2 and c=3 ;
where a=1 and b=2 ;
需要注意的是,因为有查 询优化器, 所以 a 字段在 where ⼦句的顺序并 不重要。但是,如果查 询条件是以下这⼏种,因为不 符合最左匹配原则,所以就⽆法匹配上联合索引,联
合索引就会失效:
where b=2 ;
where c=3 ;
where b=2 and c=3 ;
上⾯这些查询条件之所以会 失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再 按b 排序,在 b 相同的情况再 按 c 排序。所以,b 和 c 是全局⽆序,局部相对有序的,这样在没有遵
循最左匹配原则的情况下,是⽆法利⽤到索引的。
联合索引有⼀些特殊情况,并不是查询过程使⽤了联合索引查询,就代表联合索引中的所有字段
都⽤到了联合索引进⾏索引查询,也就是可能存在部分字段⽤到联合索引的 B+Tree ,部分字段没
有⽤到联合索引的 B+Tree 的情况。
这种特殊情况就发⽣在范围查询。联合索引的最左匹配原则会⼀直向右 匹配直到遇到「范围查询」
就会停⽌匹配。也就是范围查询的字段可以⽤到联合索引,但是在范围查询字段的后⾯的字段⽆
法⽤到联合索引。
索引失效情况有哪些?
6 种会发⽣索引失效的情况:
当我们使 ⽤左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种⽅式都会造成索
引失效;
当我们在查询条件中对索引列使⽤函数,就会导致索引失效。
当我们在查询条件中对索引列进⾏表达式计算,也是⽆法⾛索引的。
MySQL 在遇到字符串和数字⽐较的时候,会⾃动把字符串转为数字,然后再进⾏⽐较。如果字
符串是索引列,⽽条件语句中的输⼊参数是数字的话,那么索引列会发⽣隐式类型转换,由于
隐式类型转换是通过 CAST 函数实现的,等同于对索引列使⽤了函数,所以就会导致索引失
效。
联合索引要能正确使⽤需要遵循最左匹配原则,也就是按照最左优先的⽅式进⾏索引的匹配,
否则就会导致索引失效。
在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索
引会失效。
有⼀个⽤⼾表,其中属性有⽤⼾唯⼀Id ,⽤⼾性别,查询条件这两个 属性都
需要⽤到,联合索引应该怎么设置。
建⽴联合索引时的字段顺序,对索引效率也有很⼤影响。越靠前的字段被⽤于索引过滤的概率越
⾼,实际开发⼯作中建⽴联合索引时,要把区分度⼤的字段排在前⾯,这样区分度⼤的字段越有
可能被更多的 SQL 使⽤到。
区分度就是某个字段 column 不同值的个数「除以」表的总⾏数,计算公式如下:

⽐如,性别的区分度就很⼩,不适合建⽴索引或不适合排在联合索引列的靠前的位置,⽽ UUID 这
类字段就⽐较适合做索引或排在联合索引列的靠前的位置。
所以,我会创建(⽤⼾ id ,⽤⼾性别)的联合索引。
算法
⼿撕:最⻓回⽂⼦串
要找到⼀个字符串的最⻓回⽂⼦串,可以使 ⽤多种算法。以下是⼏种常⻅的⽅法:
暴⼒法:检查所有可能的⼦串,判断每个⼦串是否是回⽂。这种⽅法的时间复杂度是 O(n^3) ,其中 nn 是字符串的⻓度。
动态规划:使⽤动态规划来减少重复计算。时间复杂度是 O(n2)O(n2) ,空间复杂度也是
O(n^2) 。
中⼼扩展法:从每个可能的中⼼点向两边扩展,检查是否是回⽂。时间复杂度是 O(n^2) ,空间
复杂度是 O(1) 。
Manacher 算法:⼀种专⻔⽤于解决最⻓回⽂⼦串问题的线性时间算法,时间复杂度是 O(n) 。下⾯是使⽤中⼼扩展法的Java 实现:
// 遍历字符串中的每个字符,以该字符为中心向两边扩展,检查是否是回文。
// 同时考虑奇数长度和偶数长度的回文。
// 记录最长的回文子串的起始和结束位置。
public class LongestPalindromicSubstring {
public String longestPalindrome (String s) {
if (s == null || s.length () < 1) {
return "" ;
}
int start = 0, end = 0;
for (int i = 0; i < s.length (); i++ ) {
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
//从给定的中心点向两边扩展,检查是否是回文。
//返回回文子串的长度。
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
public static void main(String[] args) {
LongestPalindromicSubstring solution = new LongestPalindromicSubstring();
String s = "babad";
System.out.println(solution.longestPalindrome(s)); // 输出 "bab" 或 "aba"
}
}复杂度分析:
时间复杂度: O(n^2) ,其中 nn 是字符串的⻓度。每个字符都可能成为回⽂的中⼼,最多需要扩展 nn 次。
空间复杂度: O(1) ,只使⽤了常数级别的额外空间。反问
组⾥业务是做什么
流程是什么 样的
