最近看到 vivo 校招薪资也开奖了,相比去他互联网公司给的不算多,不少拿到 vivo offer 的同学看到开奖的薪资之后直接拒了。
我整理了 2025 届 vivo 校招技术岗的薪资情况,如下表:
白菜档: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;
}
public int add(int a, int b, int c) {
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 meows
Java怎么实现线程同步 ?
synchronized关键字:可以使用 synchronized
关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized
关键字锁定对象的监视器(monitor)来实现的。
public synchronized void someMethod() { /* ... */ }
public void anotherMethod() {
synchronized (someObject) {
/* ... */
}
}
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<>();
threadLocalVar.set(10);
int value = threadLocalVar.get();
并发集合:使用 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)
说出你最熟悉的一种设计模式,并运用在哪些场景 ?
主要熟悉过 spring 用过的设计模式:
工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。 代理设计模式 : Spring AOP 功能的实现。 单例设计模式 : Spring 中的 Bean 默认都是单例的。 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
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.84+6/(5/60)0.34=392个
据此可以估算加油站的数量 = 100万/392 = 2551个
算法:字符串反转
可以用双指针来解,代码如下:
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 中,可以通过多种方法在不使用第三个变量的情况下交换两个整数的值。以下是两种常用的方法:
1. 使用加法和减法
这种方法利用加法和减法的特性来交换两个值:
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);
}
}
解释:
a = a ^ b;
现在a
存储的是a
和b
的异或结果。b = a ^ b;
通过将b
与新的a
进行异或,得到原始的a
值。a = a ^ b;
再次异或得到原始的b
值。
👇🏻 点击下方阅读原文,获取鱼皮的编程学习路线、原创项目教程、求职面试宝典、编程交流圈子。
往期推荐
我的新书,冲上了京东榜一!
看了我的简历指南,面试率翻倍
不敢相信,Nginx 还能这么玩?
我的编程学习小圈子
我开源了一套 RPC 框架,学爆它!
我做了个闯关游戏,竟难倒了无数程序员。。