大家好,我是小林。
很多同学在面互联网厂的时候,有被打击到,感觉面的问题很深,准备不够充分的情况下,很难过面,于是就会去投国企和银行类公司,结果发现异常的简单,自信心就马上找回来了。
确实是这样,国企和银行公司对于技术不会深问,面试就 20-30 分钟左右,都是问一些基础的内容,偏向应用,而不是底层原理,如果按照冲大厂的路线学习,去面国企发现降维打击了。
国企的面试会比互联网厂晚一点,这个月还是有机会的,秋招的同学们不要放弃,继续投起来!
今天,分享一位同学秋招国企的面经,面试的范围是Java基础 Java并发 Java框架 mysql 网络,无算法。
Java基础
重载与重写有什么区别?
- 重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。
- 重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过@override注解来明确表示这是对父类方法的重写。
重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。
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以后CAS锁)
ArrayList和LinkedList的区别?
数据结构方面:
- ArrayList:内部使用动态数组存储数据。因此,它支持随机访问,通过索引访问元素非常快,时间复杂度为O(1)。
- LinkedList:内部使用双向链表存储数据。这使得在列表的开头或结尾插入、删除元素非常快,时间复杂度为O(1)。
性能方面:
- ArrayList:添加元素时如果需要扩容(即当前数组已满),则需要复制原数组到新的更大的数组,这样的操作时间复杂度为O(n)。而对于非尾部的插入和删除操作,需要移动后面的所有元素,时间复杂度也是O(n)。
- LinkedList:对于非首尾的插入和删除操作,需要从头部或尾部遍历到相应的位置,时间复杂度为O(n)。而访问元素(get和set操作)也需要从头部或尾部遍历到相应的位置,时间复杂度为O(n)。
String、StringBuilder、StringBuffer 区别
String、StringBuilder和StringBuffer都是Java中用于操作字符串的类。
String是不可变的字符序列,每次对String进行修改时都会创建一个新的String对象,因此在大量操作字符串时,使用String会频繁地创建对象,导致性能较低。
StringBuilder和StringBuffer都是可变的字符序列,可以对其进行多次修改而不创建新的对象。两者的区别在于线程安全性,StringBuffer是线程安全的,而StringBuilder是非线程安全的。因为StringBuffer的所有共有方法都是同步的,所以在多线程环境下使用StringBuffer可以保证线程安全,但是会降低性能。而StringBuilder没有同步方法,所以在单线程环境下使用StringBuilder性能更高。
因此,在单线程环境下进行大量的字符串操作时,应该使用StringBuilder,可以获得更好的性能。在多线程环境下,使用StringBuffer可以保证线程安全,但是会牺牲一定的性能。
综上所述,单线程大量操作字符串时应该使用StringBuilder,而在多线程环境下应该使用StringBuffer。
Java 并发
创建多线程方式有哪些?
回答:继承 Thread 类;实现 Runnable 接口;实现 Callable 接口;使用 Executors 工具类创建线程池
追问:如何启动线程?
回答:调用线程对象的start()方法
Java的线程状态变化是怎么样的?
图片
线程池的五个状态如下
- RUNNING: 接收新的任务,并能继续处理 workQueue 中的任务
- SHUTDOWN: 不再接收新的任务,不过能继续处理 workQueue 中的任务
- STOP: 不再接收新的任务,也不再处理 workQueue 中的任务,并且会中断正在处理任务的线程
- TIDYING: 所有的任务都完结了,并且线程数量(workCount)为 0 时即为此状态,进入此状态后会调用 terminated() 这个钩子方法进入 TERMINATED 状态
- TERMINATED: 调用 terminated() 方法后即为此状态
线程池工作原理原理是什么?
线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗。
线程池分为核心线程池,线程池的最大容量,还有等待任务的队列,提交一个任务,如果核心线程没有满,就创建一个线程,如果满了,就是会加入等待队列,如果等待队列满了,就会增加线程,如果达到最大线程数量,如果都达到最大线程数量,就会按照一些丢弃的策略进行处理。
线程池的构造函数有7个参数:
- corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
- maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
- keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
- unit:就是keepAliveTime时间的单位。
- workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
- threadFactory:线程工厂。可以用来给线程取名字等等
- handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。
说出你知道的所有Java里面保证线程安全的方式
Java中保证线程安全的方式包括:
- synchronized关键字:通过synchronized关键字可以实现对代码块或方法的同步,保证同一时刻只有一个线程执行该代码块或方法,从而避免多线程并发访问造成的数据不一致性。
- ReentrantLock:ReentrantLock是显示锁,通过lock()和unlock()方法来实现对临界区的加锁和解锁,提供了比synchronized更灵活的锁操作。
- Atomic类:java.util.concurrent.atomic包下的Atomic类(如AtomicInteger、AtomicLong等)提供了原子操作,保证了对变量的操作是原子性的,从而避免了多线程并发访问带来的数据竞争问题。
- 使用线程安全的集合类:如ConcurrentHashMap、CopyOnWriteArrayList等,这些集合类内部实现了线程安全机制,可以在多线程环境下安全地进行操作。
- 使用ThreadLocal变量:ThreadLocal可以实现每个线程拥有自己独立的变量副本,从而避免多线程之间的共享变量带来的线程安全问题。
什么是乐观锁?
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
这里举一个场景例子:在线文档。
我们都知道在线文档可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档了,这用户体验当然不好了。
那实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。
怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交早,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。
服务端要怎么验证是否冲突了呢?通常方案如下:
- 由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;
- 当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号不一致则提交失败,如果版本号一致则修改成功,然后服务端版本号更新到最新的版本号。
实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
Java 框架
springboot怎么开启事务?
在 Spring Boot 中开启事务非常简单,只需在服务层的方法上添加 @Transactional
注解即可。
例如,假设我们有一个 UserService 接口,其中有一个保存用户的方法 saveUser():
代码语言:javascript复制public interface UserService {
void saveUser(User user);
}
我们希望在这个方法中开启事务,只需在该方法上添加 @Transactional
注解,如下所示:
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
}
这样,当调用 saveUser() 方法时,Spring 就会自动为该方法开启一个事务。如果方法执行成功,事务会自动提交;如果方法执行失败,事务会自动回滚。
spring与springboot的区别
SpringBoot=Spring Boot。没有SpringBoot之前,写Spring程序,维护bean,维护配置文件,维护依赖,开发企业应用会极其麻烦。
SpringBoot就是把Spring这些繁琐的东西,通过自动化配置、代码即配置、约定即配置等方式,封装成了一个脚手架,对开发者屏蔽了大量Spring的配置细节,并实现依赖的自动装配,让你可以快速开始一个应用,并且很好维护。
MySQL
Select a,b,c from t where a = xx and b = xx order by c ASC,怎么建立索引?
建立(a,b,c)联合索引,这样 a 和 b 都能利用联合索引进行索引查询,并且也能避免 c 字段的额外排序,因为在 a 相等并且 b 相等的情况下 ,c 字段是有序的,天然就有序,就不需要额外排序了。
追问:建立索引(a, c, b)的话索引会命中吗?
会命中索引,但是只有 a 和c 字段才能利用索引,b 字段无法利用索引了,因为 b 字段是在 a 相等和
c 相等的情况下,b 才是有序的,这个 sql 并不满足这个条件,所以无法满足联合索引的最左匹配原则。
sql 手撕题
表有如下字段student_id(学号), score(成绩),class(班级)
- 找出每个班中成绩最好的学生的学号
SELECT
class,
MAX(score) AS max_score,
student_id
FROM
student
GROUP BY
class;
Mysql 设置了可重读隔离级后,怎么保证不发生幻读?
尽量在开启事务之后,马上执行 select ... for update 这类锁定读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录,就避免了幻读的问题。
网络
TCP和HTTP对比
- TCP 它位于OSI模型的传输层,负责在网络中的两个端点之间建立可靠的连接,确保数据包的正确顺序传输和错误检测。
- HTTP 是应用层协议,用于在网络上进行信息传输,主要用于万维网(WWW)中网页的传输。HTTP通常运行在TCP之上,依赖TCP提供的可靠交付机制。
TCP和UDP对比
- 连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接;UDP 是不需要连接,即刻传输数据。
- 服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信
- 可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议
- 拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
- 首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。 - 传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
- 应用场景:TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:FTP、HTTP/HTTPS协议。UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,经常用于视频、音频等多媒体通信等。
算法
无算法。。
历史好文:
【后端训练营】这一个月惊喜满满!!
不愧是字节,把我吊打了。。。
不愧是微信啊,问的范围贼广!
有大厂实习,贼加分!