vivo ⼀⾯
vivo Java ⾯试
最近看到 vivo 校招薪资也开奖了,相⽐去他互 联⽹公司给的不算多,不少拿到 vivo offer 的同学
看到开奖的薪资之后直接拒了。

⽩菜档:17k x 15 + 1.5k x 12 = 27.3w
sp 档:20k x 15 + 1.5k x 12 = 31.8w⽬前没看到其他更⾼的薪资了,所以没写 ssp 档,其中 1.5k x 12 这个是⼀年的房补,也就是每个
⽉ 1.5k 的房补
vivo 和 OPPO 都属于步步 ⾼集团旗下的⼿机公司,互⽆从属关系,严格来说不是同⼀家公司。
不过从2025 届校招薪资开奖的情况来看,OPPO 的薪资整体是⽐ vivo ⾼好⼏ k 的。下⾯是 OPPO
今年的校招薪资:

那 vivo ⾯试难度如何?
下⾯就分享⼀位校招同学 vivo Java 后端岗位的⾯经,这是⼀⾯,主要就问了Java 和⽹络的⼏个⼋
股,不算多,⽽且都是⽐较经典的问题,没有太刁钻的问题。除了⼋股之外,还考察了⼀个场景
题,还有 2 个算法题。

vivo ⼀⾯
说说 你对Java 多态怎 么理解
多态是指允许不同类的对象对同⼀消息作出响应。即同⼀个接⼝,使⽤不同的实例⽽执⾏不同操
作。多态性 可以分为编译时多态(重载)和运⾏时多态(重写)。它使得程序具有良好的灵活性和
扩展性。
⽅法重载是指在同⼀个类中可以定义多个⽅法,它们的名称相同但参数列表不同(包括参数个
数、类型或顺序)。在编译时,Java 编译器会根据传⼊的参数类型或数量来决定调⽤哪个⽅法。
class MathUtils {
public int add (int a, int b) {
return a + b;
}
public double add (double a, double b) {
return a + b;
}
return a + b + c;
}
}⽅法重写是指⼦类可以重写⽗类的⽅法,实现不同的⾏为。当我们通过⽗类的引⽤来调⽤被重写
的⽅法时,实际执⾏的是⼦类中的⽅法。这种特性是“运⾏时多态”的实现,Java 通过动态绑定来实
现这⼀点。
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Cat meows");
}
}
// 使用
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.sound(); // 输出: Dog barks
myCat.sound(); // 输出: Cat meowsJava 怎么实现线程同步 ?
synchronized 关键字:可以使 ⽤ synchronized 关键字来同步代码块或⽅法,确保同⼀时刻只有
⼀个线程可以访问这些代码。对象锁是通过 synchronized 关键字锁定对象的监 视器( monitor )
来实现的。
public synchronized void someMethod () { /* ... */ }/* ... */
}
}volatile 关键字: volatile 关键字⽤于变量,确保所有线程看到的是该变量的最新值,⽽不是可
能存储在本地寄存 器中的副本。
public volatile int sharedVariable;Lock 接⼝和ReentrantLock 类: java.util.concurrent.locks.Lock 接⼝提供了⽐ synchronized
更强⼤的锁定机制, ReentrantLock 是⼀个实现该接⼝的例⼦,提供了更灵活的锁管理和更⾼
的性能。
private final ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {/* ... */
} finally {
lock.unlock();
}
}原⼦类:Java 并发库( java.util.concurrent.atomic )提供了原⼦类,如 AtomicInteger 、
AtomicLong 等,这些类提供了原⼦操作,可以⽤于更新基本类型的变量⽽⽆需额外的同步。⽰
例:
AtomicInteger counter = new AtomicInteger(0);
int newValue = counter.incrementAndGet();线程局部变量: ThreadLocal 类可以为每个线程提供独⽴的变量副本,这样每个线程都拥有⾃⼰
的变量,消除了竞争条件。
ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();并发集合:使⽤ java.util.concurrent 包中的线程安全集合,如 ConcurrentHashMap 、
ConcurrentLinkedQueue 等,这些集合内部已经实现了线程安全的逻辑。
JUC ⼯具类: 使⽤ java.util.concurrent 包中的⼀些⼯具类可以⽤于控制线程间的同步和协作。
例如: Semaphore 和 CyclicBarrier 等。
Java 有哪些集合,分别 作⽤于哪些场景 ?

List 是有序的Collection ,使⽤此接⼝能够精确的控制每个元素的插⼊位置,⽤⼾能根据索引访问
List 中元素。常⽤的实现List 的类有LinkedList ,ArrayList ,Vector ,Stack 。
ArrayList 是容量可变的⾮线程安全列表,其底层使⽤数组实现。当⼏何扩容时,会创建更⼤的
数组,并把原数组复制到 新数 组。ArrayList ⽀持对元素的快速随机访问,但插⼊与删除速度很
慢。
LinkedList 本质是⼀个双向链表,与ArrayList 相⽐,,其插⼊和删除速度更快,但随机访问速度更
慢。
Set 不允许存在重复的元素,与List 不同,set 中的元素是⽆序的。常⽤的实现有HashSet ,
LinkedHashSet 和TreeSet 。
HashSet 通过HashMap 实现,HashMap 的Key 即HashSet 存储的元素,所有Key 都是⽤相同的
Value ,⼀个名为PRESENT 的Object 类型常量。使⽤Key 保证元素唯⼀性,但不保证有序性。由
于HashSet 是HashMap 实现的,因此线程不安全。
LinkedHashSet 继承⾃HashSet ,通过LinkedHashMap 实现,使⽤双向链表维护元素插⼊顺序。
TreeSet 通过TreeMap 实现的,添加元素到集合时按照⽐较规则将其插⼊合适的位置,保证插⼊
后的集合仍然有序。
Map 是⼀个键值对集合,存储键、值和之间的映射。Key ⽆序,唯⼀;value 不要求有序,允许重
复。Map 没有继承于 Collection 接⼝,从 Map 集合中检索元素时,只要给出键对象,就会返回对
应的值对象。主要实现有TreeMap 、HashMap 、HashTable 、LinkedHashMap 、
ConcurrentHashMap
HashMap :JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主
要为了 解决哈希冲突⽽存在的(“拉链法”解决冲 突),JDK1.8 以后在解决哈希冲突时有了较⼤的
变化 ,当链表⻓度⼤于阈值(默认为 8)时,将链表转化为红⿊树,以减少搜索时间
LinkedHashMap :LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结
构即由数组和链表或红⿊树组成。另外,LinkedHashMap 在上⾯结构的基础上,增加了⼀条双
向链表,使得上⾯的结构可以保 持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现
了访问顺序相关逻辑。
HashTable :数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了 解决哈希冲突⽽
存在的
TreeMap :红⿊树(⾃平衡的排序⼆叉树)
ConcurrentHashMap :Node 数组+链表+红⿊树实现,线程安全的(jdk1.8 以前Segment 锁,
1.8 以后volatile + CAS 或者 synchronized )
说出你最熟悉的⼀种设计 模式,并运⽤在哪些场景 ?
TCP 三次握⼿过程说⼀下?
TCP 是⾯向连接的协议,所以使 ⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏
的。三次握⼿的过程如下图:

⼀开始,客⼾端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状
态

客⼾端会随机初始化序号(client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把
SYN 标志位置为 1,表⽰ SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表⽰向服务端发起
连接,该报⽂不包含应⽤层数据,之后客⼾端处于 SYN-SENT 状态。

服务端收到客⼾端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn ),将此序
号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1,接着把 SYN 和 ACK 标志位置为 1。最后把该报⽂发给客⼾端,该报⽂也不 包含应⽤层数据,之
后服务端处于 SYN-RCVD 状态。

客⼾端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部
ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报 ⽂发送给服务
端,这次报⽂可以携带客⼾到服务端的数据,之后客⼾端处于 ESTABLISHED 状态。
服务端收到客⼾端的应答报⽂后,也进⼊ ESTABLISHED 状态。
从上 ⾯的过程可以发现第三次握⼿是可以携带数据的,前两次握⼿是不可以携带数据的,这也是
⾯试常问的题。
⼀旦完成三次握⼿,双⽅都处于 ESTABLISHED 状态,此时连接就已建⽴完成,客⼾端和服务端就
可以相互发送数据了。
在浏览器填⼊地址 后会发⽣什么 操作 ?

解析URL :分析 URL 所需要使⽤的传输协议和请求的资源路径。如果输⼊的 URL 中的协议或者
主机名不合法,将会把地址 栏中输⼊的内容传递给搜索引擎。如果没有问题,浏览器会检查
URL 中是否出现了⾮法字符,则对⾮法字符进⾏转义后在进⾏下⼀过程。
缓存判断:浏览器会判断所请求的资源是否在缓存⾥,如果请求的资源在缓存⾥且没有失效,
那么就直接使⽤,否则向服务器发起新的请求。
DNS 解析:如果资源不在本地缓存,⾸先需要进⾏DNS 解析。浏览器会向本地DNS 服务器发送
域名解析请求,本地DNS 服务器会逐级查询,最终找到对应的IP 地址 。
获取MAC 地址 :当浏览器得到 IP 地址 后,数据传输还 需要知道⽬的主机 MAC 地址 ,因为应⽤
层下发数据给传输层,TCP 协议会指定源端⼝号和 ⽬的端⼝号,然后下发给⽹络层。⽹络层会
将本机 地址 作为源地址 ,获取的 IP 地址 作为⽬的地址 。然后将下发给数据链路层,数据链路层
的发送需要加⼊通信双⽅的 MAC 地址 ,本机 的 MAC 地址 作为源 MAC 地址 ,⽬的 MAC 地址
需要分情况处理。通过将 IP 地址 与本机 的⼦⽹掩码相结合,可以判断是否与请求主机在同⼀个
⼦⽹⾥,如果在同⼀个⼦⽹⾥,可以使 ⽤ APR 协议获取到⽬的主机的 MAC 地址 ,如果不在⼀
个⼦⽹⾥,那么请求应该转发给⽹关,由它代为转发,此时同样可以通过 ARP 协议来获取⽹关
的 MAC 地址 ,此时⽬的主机的 MAC 地址 应该为⽹关的地址 。
建⽴TCP 连接:主机将使⽤⽬标 IP 地址 和⽬标MAC 地址 发送⼀个TCP SYN 包,请求建⽴⼀个TCP
连接,然后交给路由器转发,等路由器转到⽬标服务器后,服务器回复⼀个SYN-ACK 包,确认
连接请求。然后,主机发送⼀个ACK 包,确认已收到服务器的确认,然后 TCP 连接建⽴完成。
HTTPS 的 TLS 四次握⼿:如果使⽤的是 HTTPS 协议,在通信前还存在 TLS 的四次握⼿。
发送HTTP 请求:连接建⽴后,浏览器会向服务器发送HTTP 请求。请求中包含了⽤⼾需要获取的
资源的信息,例如⽹⻚的URL 、请求⽅法(GET 、POST 等)等。
服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进⾏相应的处理。例如,
如果是请求⽹⻚,服务器会读取相应的⽹⻚⽂件,并⽣成HTTP 响应。
场景题:你认为你所在的城市有多少个加油站
我所 在的城市是北京,⾸先,明确问题:求北京地区加油站的数量。
然后建⽴拆解公式,加油站是供给⽅,⽽有⻋⼀族是需求⽅,假设市场供需平衡,我们可以从需
求⽅去估算加油站数量。
加油站的数量 = 每天需要加油的⻋辆数/每个加油站每天可加油的⻋辆数`,接着⼜可以进⼀步拆
分。
每天需要加油的⻋辆数与北京⻋辆数和⻋的加油周期有 关,北京⻋辆数可以通过北京常住⼈⼝数
估算得到,北京⼈⼝数已知(约2000 万),假设每个家庭有4个⼈,每个家庭有⼀辆⻋,每辆⻋五
天加⼀次油,则每天需要加油的⻋辆数 = 2000W/4/5 = 100 。当然如果要考虑再细⼀点,不同家庭⼈数可以分层,按⽐例计算,每个家庭⻋辆数也可以深究,
每辆⻋⼏天加⼀次油也可以分层,这些条件及假设,在过程中也 可以提出和表明。
每个加油站每天可加油⻋辆数可以通过加油站加油桩数量,加油效率,作业时间来估算,如每个
桩加⼀次油需要5分钟,每天可以作 业14 ⼩时,根据时间段可拆为8⼩时加油桩利⽤率为80% ,另
外6⼩时加油桩利⽤率为30% ,加油站平均有4个加油桩,则每个加油站每天可加油⻋辆数 =8/(5/60) 0.8 4+6/(5/60) 0.3 4=392 个
据此可以估 算加油站的数量 = 100 万/392 = 2551 个估算得出结果后,需要进⾏合理复查,⽐如独⽣家庭⽐较多,⼀个家庭是否平均是4个⼈?加油桩
的补油时间有未 考虑,每天能⼯作14 个⼩时吗?
算法:字符串反转
可以⽤双指针来解。
对于⻓度为 N 的待被反转的字符数组,我们可以观察反转前后下标的变化 ,假设反转前字符数组
为 s[0] s[1] s[2] ... s[N - 1] ,那么反转后字符数组为 s[N - 1] s[N - 2] ... s[0] 。⽐较反转前后下标变化很容易得出 s[i] 的字符与 s[N - 1 - i] 的字符发⽣了交 换的规律,因此我们可以得出如下双指针的解
法:
将 left 指向字符数组⾸元素,right 指向字符数组尾元素。
当 left < right :
交换 s[left] 和 s[right] ;
left 指针右移⼀位,即 left = left + 1 ;
right 指针左移⼀位,即 right = right - 1 。
当 left >= right ,反转结束,返回字符数组即可。代码如下:
class Solution {
public void reverseString(char[] s) {
int n = s.length;
for (int left = 0, right = n - 1; left < right; ++left, --right) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
}
}
}复杂度分析:
时间复杂度:O(N) ,其中 N 为字符数组的⻓度。⼀共执⾏了 N/2 次的交换。
空间复杂度:O(1) 。只使⽤了常数空间来存放若⼲变量。算法:在不引⽤第三个 变量前提下使两个 整数 类型交换值
在 Java 中,可以通过多种⽅法在不使⽤第三个 变量的情况下交 换两个 整数 的值。以下是两种常⽤
的⽅法:
- 使⽤加法和减法
这种⽅法利⽤加法和减法的特性来交换两个 值:
public class SwapExample {
public static void main(String[] args) {
int a = 5;
int b = 10;
System.out.println("Before Swap: a = " + a + ", b = " + b);
a = a + b; // 将 a 赋值为 a + b
b = a - b; // 将 b 赋值为原来的 a
a = a - b; // 将 a 赋值为原来的 b
System.out.println("After Swap: a = " + a + ", b = " + b);
}
}注意:这种⽅法可能会导致溢出,如果 a 和 b 数值很⼤时,建议使⽤其他⽅法。
2、使⽤异或运算
使⽤位运算(异或运算)是⼀种更安全的交换两个 值的⽅法:
public class SwapExample {
public static void main(String[] args) {
int a = 5;
int b = 10;
System.out.println("Before Swap: a = " + a + ", b = " + b);
a = a ^ b; // 步骤 1
b = a ^ b; // 步骤 2
a = a ^ b; // 步骤 3
System.out.println("After Swap: a = " + a + ", b = " + b);
}
}
\1. a = a ^ b; 现在 a 存储的是 a 和 b 的异或结果。
\2. b = a ^ b; 通过将 b 与新的 a 进⾏异或,得到原始的 a 值。
\3. a = a ^ b; 再次异或得到原始的 b 值。