大家好,我是阿秀。
10 月份开始华为就会开展线下校园招聘会,冲华为的同学不要错过线下面试的机会,线下面试一般流程走得快,有的时候甚至一天就能走完全部的流程。
华为面试面完之后, 就会进入池子,等待开奖,去年的话是等到了 12 月份才发录取 offer 通知,中途可能还会收到 hr 的保温电话,所谓保温行为,是指通过面试后,到最后发offer之前,HR会不时地给通过了面试的求职者打电话,示意可能有offer,先不要签其他家,你排名很还不错,拿到华为 offer 的希望很大。
去年的华为校招薪资大概如下:
13 级别:19-21k * (14-16) 14 级别:22-25k * (14-16) 15 级别:26-31k * (14-16)
华为的薪资主要根据职级来定的,校招开发岗位职级一般是 13级、14 级、15 级,再细节一点,还会分 14a,14b,14c。14 a 是 14 级别里面薪资最高的水平,14c 则是 14 级别里面的普通档的薪资水平。
华为面试流程总共是 3 轮技术面+1 轮 hr 面,在约面之前,还得先进行机试,基本都是算法题,达到150分就算机试通过,然后就进行后面的技术面试。
华为的面试难度相比互联网公司会简单一点,不会问太深的技术原理,问的题目也不会很多,大概都是 10 -20 个问题,相比互联网大厂一场面试动不动就问 30 个问题,确实压力相对小一点。
今天分享一下朋友小林发表的一位同学华为线下一二面的面经,主要问了Java、MySQL、Redis、网络方面的问题,同学还是很优秀,线下一二面直接通关了。
大家可以自己感受一下难度如何?
华为线下一面
C和Java有什么区别?
语言类型:C语言是一种过程式编程语言,主要基于函数的结构,Java是一种面向对象的编程语言,强调对象和类的使用。 编译与解释:C语言通常是完全编译的语言,代码被编译成机器代码直接运行,这使其速度较快。Java代码首先被编译成字节码,然后通过Java虚拟机(JVM)解释执行,这使得Java具有平台无关性。 内存管理:C语言程序员需要手动管理内存,使用 malloc
和free
等函数,容易出现内存泄漏。Java有自动垃圾回收机制,自动管理内存,降低了内存泄漏的风险。错误处理:C语言错误处理主要依赖程序员,通过返回值检查或 errno
等机制来判断。Java使用异常捕获机制,提供更为结构化的错误处理方式。支持的数据类型:C语言支持基本的数据类型(如整数、浮点数、字符等),并且可以直接使用指针。Java有更丰富的对象类型,但不支持指针,避免了由指针引起的复杂性。 适用场景:C语言更接近底层,是对系统资源的直接控制,而Java则更加注重平台的无关性和开发效率,适合于大规模的应用开发
Java有哪些特点,深入讲讲?
Java主要有以下的特点:
平台无关性:Java的“编写一次,运行无处不在”哲学是其最大的特点之一。Java编译器将源代码编译成字节码(bytecode),该字节码可以在任何安装了Java虚拟机(JVM)的系统上运行。 面向对象:Java是一门严格的面向对象编程语言,几乎一切都是对象。面向对象编程(OOP)特性使得代码更易于维护和重用,包括类(class)、对象(object)、继承(inheritance)、多态(polymorphism)、抽象(abstraction)和封装(encapsulation)。 内存管理:Java有自己的垃圾回收机制,自动管理内存和回收不再使用的对象。这样,开发者不需要手动管理内存,从而减少内存泄漏和其他内存相关的问题。
继承封装多态,详细讲讲?
Java面向对象的三大特性包括:封装、继承、多态:
封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。 多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
继承有什么缺点,继承有什么用?
继承的缺点:
强耦合:子类与父类之间存在紧密的耦合关系,这使得父类的改变可能会影响到所有的子类,从而降低了系统的可维护性。 不灵活性:如果一个类需要从多个类继承特性,会由于Java只支持单继承(即一个类只能继承一个父类)而受到限制。这可能导致需要使用接口或组合模式来解决该问题。 潜在的问题:继承可能引入“菱形继承”问题,即多个父类的同一方法在子类中可能会导致不明确的行为,这在Java中用 super
关键字有所区分,但依然可能引发混淆。
熟悉有哪些设计模式?
主要熟悉过 spring 用过的设计模式:
工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。 代理设计模式 : Spring AOP 功能的实现。 单例设计模式 : Spring 中的 Bean 默认都是单例的。 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
Redis跟MySQL什么区别?
数据库类型:Redis是内存数据结构存储系统,通常被称为 NoSQL 数据库,数据以键值对的形式存储,并支持多种数据类型,包括字符串、哈希、列表、集合、有序集合等。MySQL是关系型数据库管理系统,数据存储在表中,使用结构化查询语言(SQL)进行数据操作,并支持复杂的查询、索引、约束和事务等功能。 数据存储和访问方式:Redis主要是将数据存储在内存中,因此读写速度非常快,适合对性能要求高的应用,数据持久化可以选择定期保存到磁盘(RDB)或通过日志(AOF)进行持久化,但主要用途仍然是内存存储。MySQL数据存储在磁盘中,虽然也有内存缓存(如 InnoDB Buffer Pool),但整体性能相对较慢于 Redis,事务支持强大,并提供 ACID(原子性、一致性、隔离性、持久性)特性。
Redis和MySQL通常用在什么时候?
Redis 常被用作缓存层来提高性能,而 MySQL 负责持久化存储,两者结合使用能够获得高性能和高可靠性的优势。
Redis 除了可以用在缓存之外,还可以:
分布式锁: Redis的特性可以用来实现分布式锁,确保多个进程或服务之间的数据操作的原子性和一致性。 排行榜: Redis的有序集合结构非常适合用于实现排行榜和排名系统,可以方便地进行数据排序和排名。 计数器 由于Redis的原子操作和高性能,它非常适合用于实现计数器和统计数据的存储,如网站访问量统计、点赞数统计等。 消息队列: Redis的发布订阅功能使其成为一个轻量级的消息队列,它可以用来实现发布和订阅模式,以便实时处理消息。
Redis和MySQL之间的读写怎么更快?
使用 Redis 作为缓存:将频繁读取但不经常更新的数据存储在 Redis 中,减轻 MySQL 的压力,从而提高读取性能,但是需要考虑缓存一致性的问题。 批量操作:在 Redis 中使用管道(Pipeline)功能,可以将多个命令一次性发送到服务器,减少网络往返时间。在 MySQL 中使用批量插入和更新,将多个操作合并为一次事务,减少数据库连接的开销。 连接池:使用连接池来管理数据库连接,减少连接创建和销毁的开销,从而提高性能。 读写分离:Redis和MySQL都支持主从架构,可以将读操作分发到从服务器,主服务器专注于写操作,从而提高整体读写性能。 数据分片:在 MySQL 中进行水平分割,将数据分散在多个数据库服务器上,从而减轻单个数据库的压力。使用 Redis 集群模式,将数据分散在多个节点上,不仅提高了并发处理能力,还提高了可用性。
Redis一致性怎么保证?
对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。对于写数据,我会选择更新 db 后,再删除缓存。
缓存是通过牺牲强一致性来提高性能的。这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。所以,如果需要数据库和缓存数据保持强一致,就不适合使用缓存。
所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短、或者太长都不好:
太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。 太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。
但是,通过一些方案优化处理,是可以最终一致性的。
针对删除缓存异常的情况,可以使用 2 个方案避免:
删除缓存重试策略(消息队列) 订阅 binlog,再删除缓存(Canal+消息队列)
消息队列方案
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个例子,来说明重试机制的过程。
重试删除缓存机制还可以,就是会造成好多业务代码入侵。
订阅 MySQL binlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:
将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性
MySQL事务有哪些特性?
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样,就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)。 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
持久性是通过 redo log (重做日志)来保证的; 原子性是通过 undo log(回滚日志) 来保证的; 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的; 一致性则是通过持久性+原子性+隔离性来保证;
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次。
mysql多个表同时写,要保持一致性,然后又要保证性能高,怎么做?
开启事务:将所有写操作包裹在一个事务中,这样可以确保所有操作要么成功,要么全部回滚,以保证数据一致性。
START TRANSACTION;
INSERT INTO table1 (col1, col2) VALUES (value1, value2);
INSERT INTO table2 (col1, col2) VALUES (value3, value4);
COMMIT;
批量插入:将多个插入合并成一条语句,减少与数据库的交互次数,提升性能。
INSERT INTO table1 (col1, col2) VALUES (value1, value2), (value3, value4);
spring aop内部实现原理?
Spring AOP的实现依赖于动态代理技术。动态代理是在运行时动态生成代理对象,而不是在编译时。它允许开发者在运行时指定要代理的接口和行为,从而实现在不修改源码的情况下增强方法的功能。
Spring AOP支持两种动态代理:
基于JDK的动态代理:使用 java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口实现。这种方式需要代理的类实现一个或多个接口。基于CGLIB的动态代理:当被代理的类没有实现接口时,Spring会使用CGLIB库生成一个被代理类的子类作为代理。CGLIB(Code Generation Library)是一个第三方代码生成库,通过继承方式实现代理。
其他
问主要学的什么课程,在学校哪些课学的比较好
华为线下二面
一个list怎么去重?
在Java中,要去重一个List
,可以使用几种方法。以下是常见的几种方式:
1、使用 Set:Set
集合不允许重复元素,因此可以通过将List
转换为 Set
来去重,然后再将其转换回 List
。
import java.util.*;
public class ListDeduplication {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
// 使用 HashSet 去重
Set<String> set = new HashSet<>(list);
// 转换回 List
List<String> deduplicatedList = new ArrayList<>(set);
System.out.println(deduplicatedList); // 输出可能顺序不同
}
}
2、使用 Java 8 Stream API:Java 8 引入了 Stream API,可以通过流处理来实现更简洁的去重:
import java.util.*;
import java.util.stream.Collectors;
public class ListDeduplication {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
// 使用 Stream 去重
List<String> deduplicatedList = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(deduplicatedList); // 输出保持顺序
}
}
3、使用 LinkedHashSet:如果需要保持元素的插入顺序,可以使用 LinkedHashSet
,因为它既可以去重又保持插入顺序。
import java.util.*;
public class ListDeduplication {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "apple", "orange", "banana"));
// 使用 LinkedHashSet 去重并保持顺序
Set<String> set = new LinkedHashSet<>(list);
// 转换回 List
List<String> deduplicatedList = new ArrayList<>(set);
System.out.println(deduplicatedList); // 输出保持顺序
}
}
介绍一下数据结构中的栈?怎么用 java 实现?
栈(stack)是一种特殊的线性数据结构,只能够在一端(即栈顶)进行,采用后进先出原则(LIFO, Last In First Out),基本操作有加入数据(push)和输出数据(pop)两种运算。
在Java中,可以通过多种方式实现栈:
数组实现:适合已知最大容量的情况,但可能会导致栈溢出。 链表实现:动态大小,没有溢出的问题,但需要额外的内存来存储指针。 内置 Stack类:简单易用,适合一般情况,但由于其继承自 Vector
,在多线程环境中可能不够高效。
1、使用数组实现栈
class ArrayStack {
private int maxSize;
private int[] stackArray;
private int top;
public ArrayStack(int size) {
maxSize = size;
stackArray = new int[maxSize];
top = -1; // 栈顶指针,初始化为-1表示栈空
}
public void push(int value) {
if (top == maxSize - 1) {
throw new RuntimeException("Stack is full");
}
stackArray[++top] = value;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stackArray[top--];
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stackArray[top];
}
public boolean isEmpty() {
return (top == -1);
}
public int size() {
return top + 1; // 返回栈中当前元素的数量
}
}
2、使用链表实现栈
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
class LinkedListStack {
private Node top;
public void push(int value) {
Node newNode = new Node(value);
newNode.next = top;
top = newNode;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
int value = top.data;
top = top.next;
return value;
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return top.data;
}
public boolean isEmpty() {
return (top == null);
}
}
3、使用 Java 内置的 Stack 类
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Top element: " + stack.peek()); // 输出 3
System.out.println("Popped element: " + stack.pop()); // 输出 3
System.out.println("Is stack empty? " + stack.isEmpty()); // 输出 false
}
}
计算机网络ping的底层过程
接下来,我们重点来看 ping
的发送和接收过程。
同个子网下的主机 A 和 主机 B,主机 A 执行ping
主机 B 后,我们来看看其间发送了什么?
ping 命令执行的时候,源主机首先会构建一个 ICMP 回送请求消息数据包。
ICMP 数据包内包含多个字段,最重要的是两个:
第一个是类型,对于回送请求消息而言该字段为 8
;另外一个是序号,主要用于区分连续 ping 的时候发出的多个数据包。
每发出一个请求数据包,序号会自动加 1
。为了能够计算往返时间 RTT
,它会在报文的数据部分插入发送时间。
然后,由 ICMP 协议将这个数据包连同地址 192.168.1.2 一起交给 IP 层。IP 层将以 192.168.1.2 作为目的地址,本机 IP 地址作为源地址,协议字段设置为 1
表示是 ICMP
协议,再加上一些其他控制信息,构建一个 IP
数据包。
接下来,需要加入 MAC
头。如果在本地 ARP 映射表中查找出 IP 地址 192.168.1.2 所对应的 MAC 地址,则可以直接使用;如果没有,则需要发送 ARP
协议查询 MAC 地址,获得 MAC 地址后,由数据链路层构建一个数据帧,目的地址是 IP 层传过来的 MAC 地址,源地址则是本机的 MAC 地址;还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。
主机 B
收到这个数据帧后,先检查它的目的 MAC 地址,并和本机的 MAC 地址对比,如符合,则接收,否则就丢弃。
接收后检查该数据帧,将 IP 数据包从帧中提取出来,交给本机的 IP 层。同样,IP 层检查后,将有用的信息提取后交给 ICMP 协议。
主机 B
会构建一个 ICMP 回送响应消息数据包,回送响应数据包的类型字段为 0
,序号为接收到的请求数据包中的序号,然后再发送出去给主机 A。
在规定的时候间内,源主机如果没有接到 ICMP 的应答包,则说明目标主机不可达;如果接收到了 ICMP 回送响应消息,则说明目标主机可达。
此时,源主机会检查,用当前时刻减去该数据包最初从源主机上发出的时刻,就是 ICMP 数据包的时间延迟。
针对上面发送的事情,总结成了如下图:
当然这只是最简单的,同一个局域网里面的情况。如果跨网段的话,还会涉及网关的转发、路由器的转发等等。
但是对于 ICMP 的头来讲,是没什么影响的。会影响的是根据目标 IP 地址,选择路由的下一跳,还有每经过一个路由器到达一个新的局域网,需要换 MAC 头里面的 MAC 地址。
说了这么多,可以看出 ping 这个程序是使用了 ICMP 里面的 ECHO REQUEST(类型为 8 ) 和 ECHO REPLY (类型为 0)。
其他
实习做了什么 你对部门的产品有什么理解
你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,这是我在校期间的编程学习之路,详细记录了我是如何自学技术以应对第二年的校招秋招的。
毕业后我先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责的项目顺利盈利 300w+。在研三那年就组建了一个阿秀的学习圈,一直持续分享校招/社招跳槽找工作的经验,都是自己一路走过来的经验,目前已经累计服务超过 4000 +人,欢迎点此了解一二。