大家好,我是小林。
我在图解网站把在公众号发表过的的企业面经收录到了这里。
https://xiaolincoding.com/
目前网站累计收录了 50 多家企业的真实面经,如下图:
刚好发现互联网中厂面经里缺失了「顺丰科技」的,那这次我们来补上!
先来看看顺丰科技的校招薪资如何?
看有 25 届的硕士 211 的同学爆料是开了 20k x 15 (30w 总包),我也有看到有 17.5k 的,所以整体是在 17k-20k 这样,算是互联网中厂薪资的范畴。
不过,看到不少同学都拒了顺丰,倒不是钱的问题,主要是之前有同学说顺丰卡转正,导致风评不是很好,所以大家都害怕发生在自己身上,对选择顺丰就会比较谨慎的态度了。
那顺丰科技的面试难点如何?
这次来看看顺丰科技Java 后端的凉经,同学准备的不是很充分,答的不理想,最后就挂了,拷打还是比较全方位的,Java集合、并发、JVM、Spring、MySQL、Redis 都各问了几个问题。
顺丰科技(一面凉经)ArrayList 和 LinkedList 的使用场景是什么?
ArrayList适用于需要频繁访问集合元素的场景。它基于数组实现,可以通过索引快速访问元素,因此在按索引查找、遍历和随机访问元素的操作上具有较高的性能。当需要频繁访问和遍历集合元素,并且集合大小不经常改变时,推荐使用ArrayList
LinkedList适用于频繁进行插入和删除操作的场景。它基于链表实现,插入和删除元素的操作只需要调整节点的指针,因此在插入和删除操作上具有较高的性能。当需要频繁进行插入和删除操作,或者集合大小经常改变时,可以考虑使用LinkedList。
hashmap的好处是可以以O(1)时间查询复杂度快速查询到数据。
当我们需要频繁访问某些数据,且这些数据的生成或获取成本较高时,可以使用 HashMap作为缓存来提高性能。
例如,在一个计算密集型的应用中,对于一些已经计算过的结果,你可以将其存储在 HashMap中,下次需要使用时直接从 HashMap中获取,而不需要重新计算。
importjava.util.HashMap;
publicclassCacheExample{
privatestaticfinalHashMap
publicstaticintfactorial(intn){
if(n == 0|| n == 1) {
return1;
}
if(cache.containsKey(n)) {
returncache.get(n);
}
intresult = n * factorial(n - 1);
cache.put(n, result);
returnresult;
}
publicstaticvoidmain(String[] args){
intnum = 5;
System.out.println("Factorial of "+ num + " is: "+ factorial(num));
}
}
线程休眠的方法有哪些?有什么区别?
方法 | 所属类 | 是否需要同步块 | 是否释放锁 | 时间单位 | 异常处理 |
---|---|---|---|---|---|
Thread.sleep | Thread 类(静态方法) |
否 |
否 |
毫秒 |
InterruptedException |
TimeUnit 类 |
TimeUnit 枚举类 |
否 |
否 |
多种(如秒、分等) |
InterruptedException |
Object.wait | Object 类(实例方法) |
是 |
是 |
毫秒 |
InterruptedException |
Thread.sleep是 Thread类的静态方法,用于使当前正在执行的线程暂停执行指定的毫秒数。它是 Thread类的静态方法,只能让当前正在执行的线程休眠。会抛出 InterruptedException异常,需要进行异常处理,当线程在休眠期间被其他线程调用 interrupt方法中断时,会抛出该异常。线程休眠期间不会释放对象锁,如果线程持有锁,在休眠期间其他线程无法获取该锁。
TimeUnit是 Java 中一个枚举类,位于 java.util.concurrent包下,提供了更方便的时间单位转换和线程休眠方法。本质上也是调用 Thread.sleep方法实现休眠,同样会抛出 InterruptedException异常。同样不会释放对象锁。
Object.wait是 Object类的实例方法,用于使当前线程进入等待状态,直到其他线程调用该对象的 notify或 notifyAll方法唤醒它。可以指定等待的时间,若不指定则会一直等待。必须在 synchronized代码块或同步方法中调用,因为它是用来协调线程对共享资源的访问。会释放对象锁,当线程调用 wait方法进入等待状态时,会释放它持有的对象锁,其他线程可以获取该锁。同样会抛出 InterruptedException异常,当线程在等待期间被其他线程调用 interrupt方法中断时,会抛出该异常。
第一种方法:通过实例化Thread对象并调用start方法启动线程。
publicstaticvoidmain(String[] args){
// 创建三个线程
Thread t1 = newThread(newTask, "Thread-1");
Thread t2 = newThread(newTask, "Thread-2");
Thread t3 = newThread(newTask, "Thread-3");
// 启动线程
t1.start;
t2.start;
t3.start;
}
staticclassTaskimplementsRunnable{
@Override
publicvoidrun{
System.out.println(Thread.currentThread.getName + " 正在执行");
}
}
}
第二种方法:通过ExecutorService管理线程池,复用线程资源。
importjava.util.concurrent.Executors;
publicclassThreadPoolExample{
publicstaticvoidmain(String[] args){
// 创建固定大小为3的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务
for(inti = 0; i < 3; i++) {
executor.execute(newTask);
}
// 关闭线程池(等待已提交任务完成)
executor.shutdown;
}
staticclassTaskimplementsRunnable{
@Override
publicvoidrun{
System.out.println(Thread.currentThread.getName + " 正在执行");
}
}
}
第三种方法:通过异步编程实现任务的并行执行:
publicclassCompletableFutureExample{
publicstaticvoidmain(String[] args){
CompletableFuture
CompletableFuture
CompletableFuture
// 等待所有任务完成
CompletableFuture.allOf(future1, future2, future3).join;
}
staticvoidtask(String name){
System.out.println(name + " 正在执行,线程:"+ Thread.currentThread.getName);
}
}
JVM内存模型介绍一下
根据 JDK 8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM的内存结构主要分为以下几个部分:
程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的 JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 null。是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域,生命周期与线程相同。
Java 虚拟机栈:每个线程都有自己独立的 Java 虚拟机栈,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会抛出 StackOverflowError 和 OutOfMemoryError 异常。
本地方法栈:与 Java 虚拟机栈类似,主要为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样可能出现 StackOverflowError 和 OutOfMemoryError 两种错误。
Java 堆:是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。从内存回收角度,堆被划分为新生代和老年代,新生代又分为 Eden 区和两个 Survivor 区(From Survivor 和 To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出 OutOfMemoryError 异常。
方法区(元空间):在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有 “非堆” 的别名。方法区可以选择不实现垃圾收集,内存不足时会抛出 OutOfMemoryError 异常。
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。当无法申请到足够内存时,会抛出 OutOfMemoryError 异常。
直接内存:不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致 OutOfMemoryError 异常。
在 JDK 1.8 中,JVM 堆内存依然采用分代式的设计,主要划分为新生代和老年代,不过有一个显著的变化是,取代了 JDK 8 之前的永久代(Permanent Generation),引入了元空间(Metaspace),但元空间并不属于堆内存,它是直接内存。
新生代是新创建的对象最初分配内存的区域,由于大部分对象的生命周期都很短,所以这个区域的垃圾回收操作比较频繁。新生代又进一步细分为Eden 区和 Survivor 区。
老年代主要用于存放经过多次 Minor GC 仍然存活的对象,以及一些大对象。大对象是指需要大量连续内存空间的对象,例如很长的数组等,这些大对象可能会直接被分配到老年代。当老年代的空间不足时,会触发一次 Full GC,Full GC 会对整个堆内存(包括新生代和老年代)进行垃圾回收,其成本比 Minor GC 高得多,因为它需要扫描和回收更多的对象。
如果有个大对象一般是在哪个区域?
大对象通常会直接分配到老年代。
新生代主要用于存放生命周期较短的对象,并且其内存空间相对较小。如果将大对象分配到新生代,可能会很快导致新生代空间不足,从而频繁触发 Minor GC。而每次 Minor GC 都需要进行对象的复制和移动操作,这会带来一定的性能开销。将大对象直接分配到老年代,可以减少新生代的内存压力,降低 Minor GC 的频率。
大对象通常需要连续的内存空间,如果在新生代中频繁分配和回收大对象,容易产生内存碎片,导致后续分配大对象时可能因为内存不连续而失败。老年代的空间相对较大,更适合存储大对象,有助于减少内存碎片的产生。
springboot的好处是什么?
Spring Boot 提供了自动化配置,大大简化了项目的配置过程。通过约定优于配置的原则,很多常用的配置可以自动完成,开发者可以专注于业务逻辑的实现。
Spring Boot 提供了快速的项目启动器,通过引入不同的 Starter,可以快速集成常用的框架和库(如数据库、消息队列、Web 开发等),极大地提高了开发效率。
Spring Boot 默认集成了多种内嵌服务器(如Tomcat、Jetty、Undertow),无需额外配置,即可将应用打包成可执行的 JAR 文件,方便部署和运行。
索引类似于书籍的目录,可以减少扫描的数据量,提高查询效率。如果查询的时候,没有用到索引就会全表扫描,这时候查询的时间复杂度是On。
如果用到了索引,那么查询的时候,可以基于二分查找算法,通过索引快速定位到目标数据, mysql 索引的数据结构一般是 b+树,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。
组合索引的好处是什么?
可以利用覆盖索引查询的优化,来避免回表,提升查询的效率。
比如,联合索引是(a ,b),那么针对下面这个查询:
select a, b fromtablewhere a =? and b =?
是覆盖索引的查询,查询不需要回表。
redis基本数据类型和使用场景说一下?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五种数据类型的应用场景:
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
Hash 类型:缓存对象、购物车等。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
Redis Hash 可以将多个相关的字段存储在一个键下,相比为每个字段单独设置一个键,Hash 结构能更紧凑地存储数据。
例如,在存储用户信息时,如果每个用户有多个属性(如姓名、年龄、地址等),使用 Hash 可以将这些属性存储在一个键下,而不是为每个属性创建一个独立的键,减少了键名的存储开销。
Hash 的字段查找和更新操作的时间复杂度都是 O(1),这意味着无论 Hash 中存储了多少个字段,查找和更新操作的性能都非常高。这使得 Redis Hash 非常适合存储需要频繁读写的数据,如缓存用户信息、配置信息等。
# 快速获取用户的年龄
HGET user:1age
Hash 可以将相关的数据组织在一起,形成一个逻辑上的分组。例如,在一个电商系统中,可以使用 Hash 来存储每个商品的详细信息,包括商品名称、价格、库存等,将这些信息关联在一起,方便管理和查询。
# 存储商品信息
HMSET product:1name "iPhone 14"price 999stock 100
通过将相关数据存储在一个 Hash 中,可以更方便地对这些数据进行管理和维护,例如批量删除、遍历等操作。