《揭秘互联网大厂Java面试:从基础到进阶的核心知识大考察》

53 阅读5分钟

在一间明亮却略显严肃的面试房间里,一位神色专注的面试官正准备对面前的求职者展开一场关于Java技术的深度考察。

面试官:“第一轮,先从基础的Java核心知识开始。第一个问题,Java中多态是如何实现的?”

王铁牛:“多态主要通过继承、重写和向上转型来实现。子类继承父类,重写父类方法,然后可以将子类对象赋值给父类引用,调用方法时就体现出多态了。”

面试官:“回答得不错。第二个问题,ArrayList和HashMap在存储结构上有什么区别?”

王铁牛:“ArrayList是基于数组实现的,有序存储元素;HashMap是基于哈希表实现的,以键值对形式存储,无序。”

面试官:“很好。第三个问题,JVM的内存结构主要有哪些部分?”

王铁牛:“有堆、栈、方法区,还有程序计数器和本地方法栈。堆是存放对象实例的,栈存放局部变量等,方法区存类信息这些。”

面试官:“非常好,基础很扎实。接下来第二轮。第一个问题,在多线程场景下,线程池的核心参数有哪些,分别有什么作用?”

王铁牛:“有核心线程数、最大线程数,嗯……还有队列容量。核心线程数就是一直保留的线程数,最大线程数是能创建的最大线程数,队列容量就是存放任务的队列大小。”

面试官:“还行。第二个问题,Spring框架中,Bean的生命周期是怎样的?”

王铁牛:“嗯……就是先实例化,然后初始化,最后销毁。具体细节我可能说得不太清楚。”

面试官:“好。第三个问题,MyBatis中#{}和${}有什么区别?”

王铁牛:“#{}是预编译处理,{}是字符串替换,#{}能防止SQL注入,{}可能有SQL注入风险。”

面试官:“不错。最后一轮了。第一个问题,Dubbo的服务调用流程是怎样的?”

王铁牛:“嗯……就是服务提供者注册服务,消费者去发现服务,然后就调用了,具体中间细节我不太确定。”

面试官:“第二个问题,RabbitMQ在高并发场景下如何保证消息不丢失?”

王铁牛:“嗯……好像要开启确认机制,还有持久化什么的,但具体怎么做我不太记得清了。”

面试官:“第三个问题,xxl - job是如何实现分布式任务调度的?”

王铁牛:“这个……我不太了解。”

面试官:“好的,面试到这里差不多了。你在基础的Java知识方面掌握得还可以,但对于一些进阶和框架相关的复杂问题,回答得不是特别清晰和全面。我们后续会综合评估所有面试者的情况,你先回去等通知吧,无论结果如何,我们都会在一周内给你回复。感谢你今天来参加面试。”

问题答案

  1. Java中多态是如何实现的?
    • 继承:子类继承父类,获得父类的属性和方法,为多态提供了基础结构。例如,定义一个父类Animal,子类Dog继承Animal
    • 重写:子类重写父类的方法,当通过父类引用调用该方法时,实际执行的是子类重写后的方法。如Animal类有void speak()方法,Dog类重写为void speak() { System.out.println("汪汪汪"); }
    • 向上转型:将子类对象赋值给父类引用,如Animal animal = new Dog();,此时通过animal.speak()调用的就是Dog类重写后的speak方法,体现了多态。
  2. ArrayList和HashMap在存储结构上有什么区别?
    • ArrayList:基于动态数组实现。它在内存中是连续存储的,元素按照插入顺序排列,有索引,可以通过索引快速访问元素。例如ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2);,可以通过list.get(0)获取第一个元素。
    • HashMap:基于哈希表实现,由数组和链表(JDK 1.8后引入红黑树优化)组成。它以键值对(key - value)的形式存储数据,通过对键进行哈希运算确定存储位置。当哈希冲突时,会将冲突的元素以链表或红黑树的形式存储在数组的同一位置。例如HashMap<String, Integer> map = new HashMap<>(); map.put("one", 1);,通过map.get("one")获取对应的值。
  3. JVM的内存结构主要有哪些部分?
    • 堆(Heap):是JVM中最大的一块内存区域,用于存放对象实例,所有线程共享。分为新生代(包含Eden区和两个Survivor区)和老年代,对象一般先在Eden区分配,经过多次垃圾回收后如果还存活会进入老年代。
    • 栈(Stack):每个线程都有自己的栈,用于存放局部变量表、操作数栈、动态链接、方法出口等信息。栈帧随着方法的调用和返回而创建和销毁。例如一个方法中定义的局部变量就存放在栈中。
    • 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它也是所有线程共享的。
    • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,它记录着当前线程执行的字节码的行号,是线程私有的。
    • 本地方法栈(Native Method Stack):与Java栈类似,不过它是为虚拟机使用到的本地(Native)方法服务的,也是线程私有的。
  4. 在多线程场景下,线程池的核心参数有哪些,分别有什么作用?
    • 核心线程数(corePoolSize):线程池中会一直存活的线程数,即使这些线程处于空闲状态,也不会被销毁。当有新任务提交时,如果线程池中的线程数小于核心线程数,就会创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务队列已满,且线程池中的线程数小于最大线程数时,会继续创建新线程来处理任务。
    • 队列容量(workQueue):用于存放等待执行的任务的队列。当线程池中的线程数达到核心线程数后,新提交的任务会被放入队列中等待执行。常见的队列类型有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量Integer.MAX_VALUE)等。
    • 线程存活时间(keepAliveTime):当线程池中的线程数大于核心线程数时,多余的空闲线程在等待新任务到来的时间超过该存活时间后,会被销毁。
    • 时间单位(unit):用于指定keepAliveTime的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
  5. Spring框架中,Bean的生命周期是怎样的?
    • 实例化(Instantiation):通过构造函数创建Bean实例。例如定义一个UserService类,Spring会调用其构造函数创建实例。
    • 属性赋值(Populate):在Bean实例创建后,Spring会为Bean的属性进行赋值,通过@Autowired等注解实现依赖注入。
    • 初始化前(Post - Process Before Initialization):在调用初始化方法前,会执行BeanPostProcessorpostProcessBeforeInitialization方法,可以对Bean进行一些预处理操作。
    • 初始化(Initialization):调用Bean的初始化方法,如通过@PostConstruct注解标注的方法,或者在配置文件中指定的init - method方法,用于完成一些初始化逻辑,比如数据库连接的初始化等。
    • 初始化后(Post - Process After Initialization):在初始化方法调用后,会执行BeanPostProcessorpostProcessAfterInitialization方法,可以对Bean进行一些后处理操作。
    • 使用(Usage):Bean可以被应用程序使用,提供相应的服务。
    • 销毁前(Pre - Destroy):在Bean被销毁前,会执行@PreDestroy注解标注的方法,或者在配置文件中指定的destroy - method方法,用于释放资源,如关闭数据库连接等。
    • 销毁(Destruction):Bean从Spring容器中移除,相关资源被释放。
  6. MyBatis中#{}和${}有什么区别?
    • #{}:是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?,然后使用PreparedStatementset方法来设置参数值,这样可以有效防止SQL注入。例如select * from user where username = #{username},实际执行时会将#{username}替换为?,然后通过PreparedStatement.setString(1, usernameValue)设置参数。
    • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis在处理`{}时,会直接将中的内容替换为变量的值,不会进行预编译。例如selectfromuserwhereusername={}`中的内容替换为变量的值,不会进行预编译。例如`select * from user where username = '{username}',如果username的值为'; drop table user; --,就会导致SQL注入,因为它直接将该值拼接到SQL中。所以在使用${}`时要特别小心,一般用于传入表名、列名等非用户输入的固定值。
  7. Dubbo的服务调用流程是怎样的?
    • 服务注册:服务提供者启动时,将自己提供的服务通过注册中心(如Zookeeper)进行注册,将服务的接口、实现类、地址等信息登记到注册中心。
    • 服务订阅:服务消费者启动时,向注册中心订阅自己需要的服务,注册中心会将服务提供者的地址等信息返回给消费者。
    • 服务调用:消费者根据从注册中心获取的服务提供者地址,通过网络通信(如Netty)调用服务提供者的接口方法。在调用过程中,Dubbo会进行负载均衡,从多个服务提供者中选择一个来处理请求,常见的负载均衡策略有随机、轮询、最少活跃调用数等。同时,Dubbo还支持集群容错,当某个服务提供者出现故障时,能自动切换到其他可用的服务提供者。
  8. RabbitMQ在高并发场景下如何保证消息不丢失?
    • 生产者确认机制(Publisher Confirm):生产者将消息发送到RabbitMQ后,RabbitMQ会给生产者发送一个确认消息,告知消息是否成功接收。生产者可以通过channel.confirmSelect()开启确认模式,然后通过channel.addConfirmListener()监听确认结果。如果消息未被确认,生产者可以进行重试。
    • 消息持久化
      • 队列持久化:通过Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)方法,将durable参数设为true,这样队列在RabbitMQ重启后依然存在。
      • 消息持久化:通过AMQP.BasicProperties.Builder durable(true)构建持久化的消息属性,这样消息会被写入磁盘,即使RabbitMQ重启,消息也不会丢失。
    • 消费者确认机制(Consumer Ack):消费者从RabbitMQ获取消息后,处理完成后需要向RabbitMQ发送确认消息(channel.basicAck(deliveryTag, multiple)),告知RabbitMQ可以将该消息从队列中删除。如果消费者未发送确认消息,RabbitMQ不会删除消息,会重新将消息发送给其他消费者(如果有多个消费者)或在一定时间后再次发送给该消费者。
  9. xxl - job是如何实现分布式任务调度的?
    • 调度中心:是xxl - job的核心,负责任务的管理、调度触发、监控等。它是一个独立的服务,可以部署为集群模式,提高可用性。调度中心通过数据库存储任务信息、执行日志等数据。
    • 执行器:负责实际执行任务,它以微服务的形式存在,可以部署在多个节点上。执行器向调度中心注册自己,调度中心根据任务配置,将任务分配给相应的执行器。
    • 任务触发:调度中心根据任务的调度规则(如Cron表达式),定时触发任务,将任务发送给对应的执行器。执行器接收到任务后,开启线程执行任务,并将执行结果返回给调度中心。
    • 负载均衡:当有多个执行器时,调度中心采用负载均衡策略(如轮询、随机等)将任务分配给不同的执行器,以实现任务的分布式执行。同时,调度中心还会监控执行器的健康状态,当某个执行器出现故障时,会自动将任务分配到其他正常的执行器上。