互联网大厂面试:Java核心知识、框架与中间件全方位考察
王铁牛怀揣着紧张与期待,坐在了互联网大厂的面试房间里,对面坐着表情严肃的面试官,一场关于Java技术的严峻考验即将拉开帷幕。
第一轮提问 面试官:“首先问几个基础问题。Java里的多态是如何实现的?” 王铁牛:“多态主要通过继承、接口实现和方法重写来实现。子类继承父类,重写父类的方法,这样在调用时可以根据实际对象类型调用不同的方法;实现接口也是类似,不同的类实现同一个接口,重写接口里的方法。” 面试官:“回答得不错。那说说ArrayList和LinkedList的区别。” 王铁牛:“ArrayList是基于数组实现的,它的优点是随机访问速度快,通过索引可以直接访问元素;缺点是插入和删除操作效率低,因为可能需要移动大量元素。LinkedList是基于双向链表实现的,插入和删除操作效率高,但是随机访问速度慢,需要从头或尾开始遍历链表。” 面试官:“很好。那HashMap的底层数据结构是什么?” 王铁牛:“HashMap在JDK 1.8之前是数组 + 链表的结构,JDK 1.8及以后是数组 + 链表 + 红黑树。当链表长度超过8且数组长度大于64时,链表会转化为红黑树,以提高查找效率。”
第二轮提问 面试官:“看来基础还挺扎实。那聊聊多线程吧。创建线程有几种方式?” 王铁牛:“有四种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口并结合FutureTask使用,还有使用线程池。” 面试官:“不错。那线程池的核心参数有哪些,分别有什么作用?” 王铁牛:“线程池的核心参数有corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程空闲存活时间)、unit(时间单位)、workQueue(任务队列)、threadFactory(线程工厂)和handler(拒绝策略)。核心线程数是线程池一直保持的线程数量,最大线程数是线程池最多能容纳的线程数量。任务队列用于存放等待执行的任务,当核心线程都在工作时,新任务会放入队列。线程空闲存活时间是指当线程空闲超过这个时间就会被销毁。线程工厂用于创建线程,拒绝策略是当任务队列满了且线程数达到最大线程数时,对新任务的处理方式。” 面试官:“回答得很全面。那JUC里的CountDownLatch是做什么用的?” 王铁牛:“CountDownLatch可以让一个或多个线程等待其他线程完成操作。通过构造函数设置一个初始计数值,当线程完成任务时调用countDown()方法将计数值减1,等待的线程调用await()方法,当计数值减到0时,等待的线程才会继续执行。”
第三轮提问 面试官:“多线程掌握得不错。接下来谈谈框架。Spring的IOC和AOP是什么?” 王铁牛:“IOC是控制反转,它把对象的创建和依赖关系的管理交给Spring容器,而不是由对象本身来控制,这样降低了代码的耦合度。AOP是面向切面编程,它可以在不修改原有代码的情况下,对程序进行增强,比如实现日志记录、事务管理等功能。” 面试官:“理解得还可以。那Spring Boot和Spring有什么区别和联系?” 王铁牛:“Spring Boot是基于Spring的,它简化了Spring应用的开发。Spring需要大量的配置文件,而Spring Boot通过自动配置和starter依赖,减少了配置的工作量,让开发人员可以更快速地搭建项目。它们的核心功能是相通的,Spring Boot本质上还是使用了Spring的IOC和AOP等功能。” 面试官:“有点意思。那MyBatis的一级缓存和二级缓存是怎么回事?” 王铁牛:“呃……这个嘛,好像是和数据的缓存有关。一级缓存是在一个SqlSession里的缓存,二级缓存是全局的缓存,但是具体怎么实现和使用我有点不太清楚。” 面试官:“看来这方面掌握得不太好。那Dubbo和RabbitMQ分别有什么作用?” 王铁牛:“Dubbo好像是个分布式服务框架,能实现服务的调用和管理;RabbitMQ是个消息队列,能实现异步通信,但是具体细节我不太能说清楚。” 面试官:“那xxl - job和Redis你了解多少?” 王铁牛:“xxl - job好像是个分布式任务调度框架,Redis是个内存数据库,不过它们具体的使用场景和原理我就不太明白了。”
面试官:“通过这几轮的提问,我能看出你对Java的一些基础核心知识掌握得还可以,像多态、集合类、多线程和线程池这些方面回答得比较准确,对Spring和Spring Boot也有一定的理解。但是在一些中间件和框架的深入应用上,比如MyBatis的缓存、Dubbo、RabbitMQ、xxl - job和Redis等方面,你的知识储备还不够完善,回答得不够清晰和准确。回去之后你可以再深入学习一下这些内容。你先回家等通知吧。”
问题答案详解
-
Java里的多态是如何实现的:
- 多态是指同一个行为具有多个不同表现形式或形态的能力。在Java中,多态主要通过以下几种方式实现:
- 继承:子类继承父类,并重写父类的方法。例如,有一个父类Animal,有一个子类Dog,Animal类有一个方法叫sound(),Dog类重写了这个方法,当通过父类引用指向子类对象时,调用sound()方法会根据实际对象类型调用Dog类的sound()方法。
class Animal { public void sound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override public void sound() { System.out.println("Dog barks"); } } public class Main { public static void main(String[] args) { Animal animal = new Dog(); animal.sound(); } }
- 接口实现:不同的类实现同一个接口,并重写接口中的方法。例如,有一个接口Shape,有两个类Circle和Rectangle实现了这个接口,并重写了接口中的area()方法,通过接口引用指向不同的实现类对象,调用area()方法会根据实际对象类型调用相应类的area()方法。
- 方法重写:是实现多态的关键,在子类中对父类的方法进行重新定义,要求方法名、参数列表和返回值类型都相同。
- 多态是指同一个行为具有多个不同表现形式或形态的能力。在Java中,多态主要通过以下几种方式实现:
-
ArrayList和LinkedList的区别:
- 数据结构:ArrayList是基于动态数组实现的,而LinkedList是基于双向链表实现的。
- 随机访问:ArrayList支持随机访问,通过索引可以直接访问元素,时间复杂度为O(1);LinkedList不支持随机访问,需要从头或尾开始遍历链表,时间复杂度为O(n)。
- 插入和删除操作:ArrayList在中间插入或删除元素时,需要移动大量元素,时间复杂度为O(n);LinkedList在插入和删除元素时,只需要修改节点的指针,时间复杂度为O(1)。
- 内存占用:ArrayList的内存占用主要是数组的空间,可能会有一定的空间浪费;LinkedList的内存占用除了存储元素外,还需要存储节点的指针,相对占用更多的内存。
-
HashMap的底层数据结构是什么:
- 在JDK 1.8之前,HashMap的底层数据结构是数组 + 链表。数组是HashMap的主体,每个数组元素是一个链表的头节点,当发生哈希冲突时,新的元素会插入到链表中。
- 在JDK 1.8及以后,HashMap的底层数据结构是数组 + 链表 + 红黑树。当链表长度超过8且数组长度大于64时,链表会转化为红黑树,以提高查找效率。红黑树是一种自平衡的二叉搜索树,查找、插入和删除操作的时间复杂度为O(log n)。
-
创建线程有几种方式:
- 继承Thread类:创建一个类继承Thread类,并重写run()方法,然后创建该类的对象并调用start()方法启动线程。
class MyThread extends Thread { @Override public void run() { System.out.println("Thread is running"); } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } }
- 实现Runnable接口:创建一个类实现Runnable接口,并重写run()方法,然后将该类的对象作为参数传递给Thread类的构造函数,最后调用start()方法启动线程。
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running"); } } public class Main { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); } }
- 实现Callable接口并结合FutureTask使用:创建一个类实现Callable接口,并重写call()方法,该方法有返回值。将该类的对象作为参数传递给FutureTask类的构造函数,再将FutureTask对象作为参数传递给Thread类的构造函数,最后调用start()方法启动线程。可以通过FutureTask的get()方法获取线程执行的结果。
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { return 1 + 2; } } public class Main { public static void main(String[] args) throws Exception { MyCallable callable = new MyCallable(); FutureTask<Integer> futureTask = new FutureTask<>(callable); Thread thread = new Thread(futureTask); thread.start(); System.out.println(futureTask.get()); } }
- 使用线程池:通过Executors工具类创建不同类型的线程池,将实现了Runnable或Callable接口的任务提交给线程池执行。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyRunnable implements Runnable { @Override public void run() { System.out.println("Task is running"); } } public class Main { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); MyRunnable runnable = new MyRunnable(); executorService.submit(runnable); executorService.shutdown(); } }
-
线程池的核心参数有哪些,分别有什么作用:
- corePoolSize(核心线程数):线程池一直保持的线程数量,当提交的任务数小于核心线程数时,线程池会创建新的线程来执行任务。
- maximumPoolSize(最大线程数):线程池最多能容纳的线程数量,当任务队列满了且线程数达到核心线程数时,线程池会创建新的线程,直到达到最大线程数。
- keepAliveTime(线程空闲存活时间):当线程空闲超过这个时间就会被销毁,前提是线程数大于核心线程数。
- unit(时间单位):keepAliveTime的时间单位,如秒、毫秒等。
- workQueue(任务队列):用于存放等待执行的任务,常用的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
- threadFactory(线程工厂):用于创建线程,可以自定义线程的名称、优先级等。
- handler(拒绝策略):当任务队列满了且线程数达到最大线程数时,对新任务的处理方式。常用的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
-
JUC里的CountDownLatch是做什么用的:
- CountDownLatch是一个同步工具类,它可以让一个或多个线程等待其他线程完成操作。通过构造函数设置一个初始计数值,当线程完成任务时调用countDown()方法将计数值减1,等待的线程调用await()方法,当计数值减到0时,等待的线程才会继续执行。
import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int n = 3; CountDownLatch latch = new CountDownLatch(n); for (int i = 0; i < n; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " is working"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " has finished"); latch.countDown(); }).start(); } latch.await(); System.out.println("All threads have finished, main thread can continue"); } }
-
Spring的IOC和AOP是什么:
- IOC(控制反转):是一种设计原则,它把对象的创建和依赖关系的管理交给Spring容器,而不是由对象本身来控制。通过依赖注入(DI)的方式,将对象的依赖关系注入到对象中,降低了代码的耦合度。例如,有一个UserService类依赖于UserDao类,在传统的方式中,UserService类需要自己创建UserDao对象,而在Spring中,通过配置或注解的方式,将UserDao对象注入到UserService类中。
- AOP(面向切面编程):是一种编程范式,它可以在不修改原有代码的情况下,对程序进行增强。AOP的核心概念包括切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)等。切面是一个包含了通知和切点的模块,连接点是程序执行过程中的某个点,切点是一组连接点的集合,通知是在切点处执行的代码,包括前置通知、后置通知、环绕通知等。例如,在进行日志记录或事务管理时,可以使用AOP来实现,将这些通用的功能从业务逻辑中分离出来。
-
Spring Boot和Spring有什么区别和联系:
- 联系:Spring Boot是基于Spring的,它继承了Spring的核心功能,如IOC和AOP等。Spring Boot本质上还是使用了Spring的框架来构建应用程序。
- 区别:Spring需要大量的配置文件,如XML配置文件或Java配置类,开发人员需要手动配置各种组件和依赖关系。而Spring Boot通过自动配置和starter依赖,减少了配置的工作量。Spring Boot提供了默认的配置,当开发人员引入相应的starter依赖时,Spring Boot会自动配置相关的组件,让开发人员可以更快速地搭建项目。
-
MyBatis的一级缓存和二级缓存是怎么回事:
- 一级缓存:是基于SqlSession的缓存,在同一个SqlSession中,执行相同的查询语句时,会先从缓存中查找,如果缓存中有则直接返回,否则从数据库中查询,并将查询结果存入缓存。当SqlSession关闭或执行了增删改操作时,一级缓存会被清空。
- 二级缓存:是全局的缓存,多个SqlSession可以共享二级缓存。需要在MyBatis的配置文件中开启二级缓存,并在Mapper.xml文件中配置相应的缓存策略。当执行查询操作时,会先从二级缓存中查找,如果缓存中有则直接返回,否则从数据库中查询,并将查询结果存入二级缓存。二级缓存的作用范围更大,能提高查询效率,但需要注意缓存的一致性问题。
-
Dubbo和RabbitMQ分别有什么作用:
- Dubbo:是一个高性能的分布式服务框架,主要用于解决分布式系统中的服务调用和管理问题。它提供了服务注册与发现、远程调用、负载均衡、集群容错等功能。通过Dubbo,服务提供者可以将自己的服务注册到注册中心,服务消费者可以从注册中心获取服务提供者的地址,并进行远程调用。Dubbo支持多种协议,如Dubbo协议、HTTP协议等,能提高分布式系统的开发效率和性能。
- RabbitMQ:是一个开源的消息队列中间件,主要用于实现异步通信和系统解耦。生产者将消息发送到RabbitMQ的交换器,交换器根据路由规则将消息路由到相应的队列,消费者从队列中获取消息并进行处理。RabbitMQ支持多种消息模型,如点对点模型、发布 - 订阅模型等,能提高系统的吞吐量和可靠性,同时降低系统之间的耦合度。
-
xxl - job和Redis你了解多少:
- xxl - job:是一个分布式任务调度框架,它提供了简单易用的任务调度功能。可以通过Web界面进行任务的配置、管理和监控,支持任务的定时调度、分片广播、失败重试等功能。xxl -