互联网大厂 Java 面试:核心知识、框架与中间件大考验
王铁牛怀揣着紧张又期待的心情,坐在了互联网大厂的面试会议室里,对面的面试官一脸严肃,一场激烈的技术问答即将展开。
第一轮提问 面试官:我们先从 Java 核心知识开始。Java 中基本数据类型有哪些? 王铁牛:这个我知道,有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那你说说 String 是基本数据类型吗? 王铁牛:不是,String 是引用数据类型,它是一个类。 面试官:很好。那 String、StringBuilder 和 StringBuffer 有什么区别呢? 王铁牛:String 是不可变的,每次对 String 进行操作都会生成一个新的 String 对象。StringBuilder 是非线程安全的,效率高,适合单线程环境下使用。StringBuffer 是线程安全的,因为它的方法加了同步锁,效率相对低一些,适合多线程环境。 面试官:非常棒,基础很扎实。那 Java 中的自动装箱和拆箱是什么意思? 王铁牛:自动装箱就是把基本数据类型自动转换为对应的包装类对象,比如 int 转 Integer。拆箱就是把包装类对象自动转换为对应的基本数据类型,像 Integer 转 int。
第二轮提问 面试官:接下来我们聊聊 JUC 和多线程。什么是线程池? 王铁牛:线程池就是预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务,任务执行完后线程不会销毁,而是放回线程池,这样可以减少线程创建和销毁的开销。 面试官:嗯,理解得很对。那线程池有哪些常用的创建方式? 王铁牛:可以通过 Executors 工具类创建,比如 newFixedThreadPool 创建固定大小的线程池,newCachedThreadPool 创建可缓存的线程池。不过阿里巴巴开发手册不建议用 Executors 创建,推荐用 ThreadPoolExecutor 手动创建。 面试官:看来你对规范也有了解。那说说 ThreadPoolExecutor 的几个重要参数。 王铁牛:有 corePoolSize 核心线程数,maximumPoolSize 最大线程数,keepAliveTime 线程空闲时间,TimeUnit 时间单位,workQueue 任务队列,threadFactory 线程工厂,handler 拒绝策略。 面试官:不错。那在多线程环境下,HashMap 会有什么问题? 王铁牛:呃……这个嘛,好像会有线程安全问题,具体的我有点不太清楚了。
第三轮提问 面试官:现在我们来谈谈一些框架和中间件。Spring 的核心特性有哪些? 王铁牛:Spring 的核心特性有 IOC(控制反转)和 AOP(面向切面编程)。IOC 就是把对象的创建和依赖关系的管理交给 Spring 容器,AOP 是在不修改原有代码的基础上,对程序进行增强。 面试官:回答得挺好。那 Spring Boot 有什么优势? 王铁牛:Spring Boot 可以快速搭建项目,它有自动配置功能,能减少很多繁琐的配置,还内置了 Tomcat 等服务器,方便开发和部署。 面试官:对的。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,会把参数部分用占位符 ? 代替,能防止 SQL 注入。{} 是字符串替换,会直接把参数的值替换到 SQL 语句中,有 SQL 注入风险。 面试官:看来基础部分掌握得不错。那 Dubbo 是什么,它有什么作用? 王铁牛:Dubbo 嘛,好像是个分布式服务框架,具体有啥作用我有点说不清楚了。 面试官:那 RabbitMQ 呢,你了解它的工作模式吗? 王铁牛:这个……我只知道它是消息队列,工作模式不太熟悉。
面试官总结:王铁牛,通过这轮面试,能看出你对 Java 的一些基础核心知识掌握得还不错,像 Java 基本数据类型、线程池的概念和参数、Spring 的核心特性等问题都回答得比较准确,说明你有一定的知识储备。但是在面对一些稍微复杂和深入的问题时,比如多线程环境下 HashMap 的问题、Dubbo 的作用以及 RabbitMQ 的工作模式等,你回答得不够清晰甚至不太了解。我们公司对技术的要求比较高,不仅要掌握基础,还需要对各类技术有更深入的理解和实践经验。我们会综合考虑这次面试情况,你先回家等通知吧。
答案详解
- Java 基本数据类型:Java 中有 8 种基本数据类型,分别是 4 种整数类型(byte 占 1 字节,取值范围 -128 到 127;short 占 2 字节;int 占 4 字节,是最常用的整数类型;long 占 8 字节,定义时数值后要加 L),2 种浮点类型(float 占 4 字节,定义时数值后要加 F;double 占 8 字节,是常用的浮点类型),1 种字符类型(char 占 2 字节,用于表示单个字符),1 种布尔类型(boolean 只有 true 和 false 两个值)。
- String 不是基本数据类型:基本数据类型是 Java 语言预先定义好的简单数据类型,而 String 是 Java 中的一个类,属于引用数据类型。它表示字符串,内部使用字符数组存储字符序列。
- String、StringBuilder 和 StringBuffer 的区别
- String:是不可变的,一旦创建,其值不能被改变。每次对 String 进行拼接等操作时,都会创建一个新的 String 对象,会造成内存的浪费。
- StringBuilder:是非线程安全的,它继承自 AbstractStringBuilder 类,内部使用可变的字符数组存储数据。在单线程环境下,对字符串进行频繁的拼接等操作时,使用 StringBuilder 效率更高,因为它不会频繁创建新对象。
- StringBuffer:是线程安全的,它也继承自 AbstractStringBuilder 类,和 StringBuilder 类似,但是它的方法都加了 synchronized 关键字进行同步,保证在多线程环境下操作的安全性,但也因为同步操作会带来一定的性能开销,效率相对 StringBuilder 较低。
- 自动装箱和拆箱
- 自动装箱:是 Java 编译器提供的语法糖,当把一个基本数据类型赋值给对应的包装类对象时,编译器会自动调用包装类的 valueOf 方法进行装箱操作。例如:
Integer i = 10;实际上编译器会转换为Integer i = Integer.valueOf(10); - 拆箱:当把一个包装类对象赋值给对应的基本数据类型时,编译器会自动调用包装类的 xxxValue 方法进行拆箱操作。例如:
Integer i = 10; int j = i;实际上编译器会转换为Integer i = 10; int j = i.intValue();
- 自动装箱:是 Java 编译器提供的语法糖,当把一个基本数据类型赋值给对应的包装类对象时,编译器会自动调用包装类的 valueOf 方法进行装箱操作。例如:
- 线程池:线程池是一种多线程处理形式,它维护了一个线程集合,用于执行提交的任务。预先创建一定数量的线程,当有任务提交时,从线程池中获取空闲线程来执行任务,任务执行完毕后线程不会销毁,而是返回线程池等待下一个任务。这样可以避免频繁创建和销毁线程带来的性能开销,提高系统的性能和资源利用率。
- 线程池常用创建方式
- 通过 Executors 工具类创建
- newFixedThreadPool:创建一个固定大小的线程池,核心线程数和最大线程数相等,当有任务提交时,如果线程池中有空闲线程则执行任务,否则任务会被放入任务队列中等待。
- newCachedThreadPool:创建一个可缓存的线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE。当有任务提交时,如果线程池中有空闲线程则执行任务,否则会创建新的线程来执行任务。如果线程空闲时间超过 60 秒,则会被销毁。
- 使用 ThreadPoolExecutor 手动创建:手动创建可以更灵活地配置线程池的参数,根据实际业务需求进行调整,避免使用 Executors 工具类创建时可能带来的一些问题,比如内存泄漏等。
- 通过 Executors 工具类创建
- ThreadPoolExecutor 的重要参数
- corePoolSize:核心线程数,线程池启动后会一直保持的线程数量,即使这些线程处于空闲状态。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。
- keepAliveTime:线程空闲时间,当线程池中的线程数量超过核心线程数时,空闲线程在超过这个时间后会被销毁。
- TimeUnit:时间单位,用于指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- workQueue:任务队列,用于存储提交的任务,当线程池中的线程都在执行任务时,新提交的任务会被放入任务队列中等待。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- threadFactory:线程工厂,用于创建线程,通过线程工厂可以自定义线程的名称、优先级等属性。
- handler:拒绝策略,当任务队列已满且线程池中的线程数量达到最大线程数时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃任务队列中最老的任务)。
- 多线程环境下 HashMap 的问题:HashMap 不是线程安全的,在多线程环境下使用可能会出现数据不一致和死循环等问题。在 JDK 1.7 中,当多个线程同时进行扩容操作时,可能会导致链表形成环形结构,从而出现死循环。在 JDK 1.8 中,虽然对链表转红黑树等进行了优化,但仍然存在数据覆盖等问题。在多线程环境下可以使用 ConcurrentHashMap 来代替 HashMap。
- Spring 的核心特性
- IOC(控制反转):也称为依赖注入(DI),是一种设计模式,它把对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。通过 Spring 容器来创建和管理对象,对象之间的依赖关系由容器来注入,降低了对象之间的耦合度。例如,一个类需要依赖另一个类的对象,不需要在类内部手动创建该对象,而是通过 Spring 容器注入。
- AOP(面向切面编程):是一种编程范式,它允许在不修改原有代码的基础上,对程序进行增强。通过定义切面(Aspect)、切点(Pointcut)和通知(Advice),可以在程序的特定点(如方法调用前后、异常抛出时等)插入额外的代码逻辑,实现日志记录、事务管理、权限验证等功能。
- Spring Boot 的优势
- 快速搭建项目:Spring Boot 提供了很多 Starter 依赖,只需要在项目中添加相应的 Starter 依赖,Spring Boot 就会自动配置相关的组件,大大减少了项目搭建的时间和工作量。
- 自动配置:Spring Boot 根据项目中引入的依赖和配置,自动进行一些默认的配置,开发者可以根据需要进行少量的配置来覆盖默认配置。例如,引入 Spring Data JPA 的 Starter 依赖后,Spring Boot 会自动配置数据源、JPA 等相关组件。
- 内置服务器:Spring Boot 内置了 Tomcat、Jetty 等服务器,不需要单独部署服务器,直接运行项目的主类就可以启动服务器,方便开发和调试。
- MyBatis 中 #{} 和 ${} 的区别
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会把参数部分用占位符 ? 代替,然后使用 PreparedStatement 进行 SQL 语句的执行。这样可以防止 SQL 注入,因为参数的值是通过预编译的方式传入的,不会和 SQL 语句进行拼接。
- **{} 时,会直接把参数的值替换到 SQL 语句中。如果参数的值是用户输入的,可能会存在 SQL 注入风险,因为恶意用户可以通过构造特殊的输入来改变 SQL 语句的语义。
- Dubbo:是阿里巴巴开源的一个高性能、轻量级的分布式服务框架,用于解决分布式系统中服务之间的远程调用和服务治理问题。它提供了服务注册与发现、远程调用、负载均衡、集群容错等功能。通过 Dubbo,可以将一个大型的系统拆分成多个小的服务,各个服务之间可以独立开发、部署和维护,提高了系统的可扩展性和可维护性。
- RabbitMQ 的工作模式
- 简单模式(Hello World):一个生产者将消息发送到一个队列,一个消费者从该队列中接收消息。
- 工作队列模式(Work Queues):一个生产者将消息发送到一个队列,多个消费者从该队列中竞争消费消息,主要用于任务的分发和负载均衡。
- 发布/订阅模式(Publish/Subscribe):生产者将消息发送到一个交换机(Exchange),交换机将消息广播到所有绑定的队列,多个消费者可以从不同的队列中接收相同的消息。
- 路由模式(Routing):生产者将消息发送到一个交换机,交换机根据消息的路由键(routing key)将消息发送到绑定了相应路由键的队列。
- 主题模式(Topics):和路由模式类似,但是主题模式的路由键可以使用通配符,更加灵活。
- RPC 模式(Remote Procedure Call):用于实现远程过程调用,客户端发送请求消息到队列,服务端从队列中接收请求并处理,然后将响应消息发送回客户端。