文 | 小林coding
出品 | 小林coding(ID:CodingLin )
蔚来 25 届开发岗位的普通档年包在 35w 左右,优秀突出的也能拿到 40w+ 年薪,具体薪资情况如下(目前数据不多,所以只看到这两个档次薪资):
普通 offer:23.5k x(13+1.5) + 630 股,同学 bg 硕士 211 ssp offer:28k x(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区别是什么?
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 如图所示:
比如,我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 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 len1 = expandAroundCenter(s, i, i); // 奇数长度的回文
int len2 = expandAroundCenter(s, i, i + 1); // 偶数长度的回文
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),只使用了常数级别的额外空间。
反问
组里业务是做什么 流程是什么样的
推荐阅读:
推荐阅读: