为了账号安全,请及时绑定邮箱和手机立即绑定

最近的资深Java开发工程师面试经历

咱们来聊聊这个Java面试的技术部分。

正在准备Java开发者的面试吗?

在我的书《Java开发者面试指南》中可以找到 Gumroad (PDF )和Amazon (Kindle电子书).

可以在这里找到《Spring-Boot微服务面试指南》 Gumroad (PDF )和Amazon (Kindle电子书).

可以在这里找到《前端开发者面试指南》 Gumroad (PDF )和Amazon (Kindle电子书).

下载样章:

Java开发者面试指南[免费样章]

Spring-Boot微服务面试指南[免费样章]

前端开发者面试指南[免费样章]

需要个性化指导?请使用以下1:1链接——https://topmate.io/ajay_rathod11

在这次技术面试里,我们能注意到一个明显的提问模式。

  1. 核心 Java:他们从 Java Stream API 的基础知识和概念开始。
  2. Spring 框架:接着重点介绍了 Spring 框架的核心概念。
  3. 编码挑战:然后测试了候选人的编程能力,可能包括 SQL 查询。
  4. 前端:如果简历中有提到,可能会问一些关于 Angular 或 React 这样的前端框架的问题。
  5. Spring 和微服务:最后深入讨论了 Spring 和微服务的核心概念。

要在这些面试中表现突出,以下几点很重要:掌握这些内容才行。

  • 核心 Java(包括其最新特性)
  • Spring
  • Spring Boot 及微服务

另外,像 FAANG 这样的大公司也开始经常问系统设计相关的问题,我建议资深人士对此做好准备,就像准备面试一样。

强烈推荐给想找系统设计面试题资料的人

Alex Xu 的系统设计面试课 — 这门课程涵盖了他著名的《系统设计面试》(第一卷和第二卷)的所有内容。

这些领域对于成功来说非常重要。现在,我们来探讨一下问答环节,深入讨论这些问题。

Java 8中的流API

解释一下Java 8 Streams是什么,以及它们与传统的集合方式有什么不同。

答案: Java 8 Streams 是一种新的方式,用于以函数式风格处理数据。与集合不同,Streams 不存储数据,而更像是对数据源的一系列操作。这些操作可以被链式调用来处理数据,比如 map、filter 和 reduce,这种方式可以以延迟处理、声明式的方式来执行数据处理任务。

你能在流上做哪些操作?

答案: 流主要有两种操作方式:

  • 中间操作:这些操作返回一个流,并且可以链接在一起,例如筛选(filter())、映射(map())、排序(sorted())。
  • 终止操作:这些操作会产生结果或者有副作用,结束流的处理,例如收集(collect())、遍历(forEach())、缩减(reduce())。
如何使用Stream API从整数列表中筛选出所有的奇数?(注:Stream API 是一种处理数据流的技术) 回答

```List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());
// 创建了一个整数列表numbers,包含数字1到6。然后,我们使用流处理创建了一个新的列表evenNumbers,该列表仅包含numbers中所有的偶数。


## **你能说说 Stream API 中 map 和 flatMap 的区别吗?**

**答案:** map 会将每个元素转换成另一个对象,保持一一对应关系的不变性。flatMap 则是在一个元素可以变成多个元素,或者需要处理流中的嵌套集合时使用,将嵌套的结构扁平化为一个单一的流。

* map: Stream.of(\"a\", \"bb\", \"ccc\").map(s -> s.length()) 结果是一个包含 [1, 2, 3] 的列表。
* flatMap 操作: Stream.of(\"a\", \"bb\", \"ccc\").flatMap(s -> s.chars().boxed()) 结果是一个包含每个字符编码的流。

## **如何根据特定的属性对对象流(对象流可以理解为一系列连续的对象)排序?你通常会怎么做?**

## **答:**

```List<Person> people = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25)); List<Person> 按年龄排序的列表 = people.stream().sorted(Comparator.comparing(Person::getAge)).collect(Collectors.toList());

在Stream API中,collect方法是用来做什么的?

答案: collect 是一个终端操作符,它使用收集器将流中的元素聚合到一个结果容器(如 List、Set 或 Map)中。例如,

// 将流转换为字符串列表\nList<String> result = stream.collect(Collectors.toList());

举个例子说明一下reduce函数。

答案: reduce 函数会把流中的这些元素通过某种二元操作结合起来,进行缩减。例如:

  • Optional<Integer> sum = Stream.of(1, 2, 3, 4).reduce(Integer::sum);
    这行代码使用Java Stream API计算了一个整数列表的总和。

这会将流中的所有数字加起来,并返回一个 Optional<Integer> 值,因为流可能是空的。

Optional 的目的是什么,在Java 8中它是如何与流一起使用的?

答案: Optional 用来表示一个可能不存在的值,减少了对 null 检查的需要。在流(Streams)中,它通常由 findFirst()、reduce() 或 min() 等方法返回,以表明结果可能不存在的情况。

  • 可选的整数最大值 max = 流.of(1, 2, 3).最大值(按照整数比较);
默认功能界面: Java 8中的函数式接口是什么?

答案: 函数式接口是指只包含一个抽象方法的接口,这样的接口可以由 lambda 表达式或方法引用来实现。

你能列举并解释几个Java 8中的核心函数接口及其作用吗?

答案是:

  • Predicate<T>: 用于筛选操作,返回一个布尔值。
  • Consumer<T>: 接受一个参数并执行一个操作而不返回任何值,用于forEach。
  • Function<T, R>: 将输入转换为输出,用于map。
  • Supplier<T>: 不需要输入即可提供类型T的结果,用于延迟加载。
  • UnaryOperator<T> 和 BinaryOperator<T>: Function的特例,特别适用于输入和输出类型相同的场景。
如何用谓词过滤流?
  • List<String> nonEmptyStrings = Arrays.asList("", "a", "b", "").stream() .filter(Predicate.not(String::isEmpty)) .collect(Collectors.toList());

这个代码片段创建了一个字符串列表,过滤掉空字符串,只保留非空字符串。

如何使用一个 Consumer和流一起。
  • List<String> myList = Arrays.asList("A", "B", "C"); myList.stream().forEach(System.out::println); // System.out::println 是一个消费器。_// List<String> 表示一个字符串列表,Arrays.asList 表示将一组字符串转换为列表_
BiFunction 和函数有什么区别?

回答:函数接收一个参数并返回结果,而双函数接收两个参数并返回结果。

  • Function<Integer, String> 可能将一个 Integer 转换为其字符串形式。
  • BiFunction<Integer, Integer, String> 可能将两个整数连接成一个字符串。

在流操作中,如何使用 Supplier?

回答:

  • Stream.generate(() -> new Random().nextInt(100)).limit(10).forEach(System.out::println);

这段代码会生成一个包含10个随机整数的流,并将每个数打印出来。

这将生成一个包含10个随机的整数的列表。

能举一个使用 "UnaryOperator" 处理 Stream 的例子吗?

答案:

  • 这段代码创建了一个包含字符串 "hello" 和 "world" 的列表。然后,它将列表中的每个字符串转换为大写,并将结果存储在新的列表中。

  • List<String> strings = Arrays.asList("hello", "world"); List<String> upperCase = strings.stream().map(String::toUpperCase).collect(Collectors.toList());

在这里,String::toUpperCase 是一个 UnaryOperator<String>

Spring核心

问题1:这个Spring中的@Qualifier注解的目的何在,它是如何与@Autowired注解一起使用的呢?

回答:

在 Spring 中,@Qualifier 注解用于解决应用上下文中存在多个相同类型 bean 的歧义问题。当使用 @Autowired 进行依赖注入并且存在多个相同类型的 bean 时,Spring 就不知道该注入哪个 Bean。这时 @Qualifier 就派上用场了:

  • 用法 :@Qualifier 注解通常与 @Autowired 一起使用,用于指明在有多个候选 bean 时要注入的 bean。您只需在要注入的 bean 的位置上加上 @Qualifier 注解,并提供该 bean 的名称或限定符名称即可。
    // 在配置类或组件类中
    @Bean("specialDataSource")
    public DataSource dataSource() {
        return new DataSource();
    }

    // 在你需要注入的类中
    @Autowired
    @Qualifier("specialDataSource")
    private DataSource dataSource;

通过在@Qualifier中标注“specialDataSource”,这样Spring就知道要注入名为“specialDataSource”的bean,而不是其他可能存在的DataSource bean。

说明 @Transactional 注解在 Spring 中是如何工作的。应该注意哪些关键属性?

答案:

Spring 中的 @Transactional 注解实现了声明式的事务管理,这意味着你可以不需在业务逻辑中显式编码来处理事务,即可以应用于类或方法来定义事务的范围。

  • 它是怎么工作的:在方法或类上使用 @Transactional 注解时,Spring 会将这个方法的调用包裹在一个事务中。如果在方法中抛出了异常,事务将会回滚;否则,在方法执行完成后会提交事务。

属性:

  • 传播行为(propagation):定义了当一个事务调用另一个事务时,该事务应如何表现。常见的值包括 REQUIRED(默认值)、REQUIRES_NEW(新开事务)、NESTED(在现有事务中嵌套的新事务)。
  • 隔离级别(isolation):指定了事务的隔离级别,以防止脏读、不可重复读和幻读。可选的选项有 READ_COMMITTED、READ_UNCOMMITTED、REPEATABLE_READ、SERIALIZABLE。
  • 回滚异常(rollbackFor):指定了应当触发事务回滚的异常类型。默认情况下,只有运行时异常会触发回滚,检查型异常不会。
  • 只读(readOnly):如果设置为 true,则提示事务为只读,这可以优化某些事务资源的性能表现。
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED,   
                    rollbackFor = Exception.class, readOnly = false)  
    public void saveData(MyData data) {  
        // 这里写业务逻辑,  
    }

在这个例子中,我们定义了一个事务,它具有特定的传播规则和隔离级别,并且会因任何异常而回滚,而不仅仅是运行时发生的异常。

在一个方法调用链中使用了多个 @Transactional 注解,会怎样?

回答:

当一个方法中有多个@Transactional注解时:

  • 传播:行为取决于每个@Transactional注解的传播特性。如果一个带有@Transactional(propagation = Propagation.REQUIRES_NEW) 注解的方法被另一个事务方法调用,将会为该方法创建一个新的物理事务,与外部事务无关。
  • 嵌套:如果方法是用@Transactional注解的但没有使用REQUIRES_NEW,它们会默认加入现有事务,除非另有指定。但是,如果使用NESTED,Spring会在同一事务中设置保存点,允许部分回滚。
  • 结果:如果异常发生,事务的行为(提交或回滚)通常取决于最外层事务的设置,除非内部事务被配置为REQUIRES_NEW或使用了带有保存点的NESTED模式。

这种设置可能导致复杂的交易边界问题,理解事务如何传递和相互作用对于确保应用程序中的数据一致性非常重要。

关于React的前端问题:

生产环境与开发环境的考虑

问题: React应用的开发环境和生产环境主要有哪些区别?

所以,答案就是:

  • 性能:生产构建针对性能进行了优化;资源被最小化,代码通常被打包和压缩。
  • 错误处理:在生产环境中,错误边界对于优雅退化至关重要;在开发环境中,错误信息更详细,便于调试。
  • 环境变量:使用 process.env.NODE_ENV 进行条件逻辑,例如在生产环境中禁用 PropTypes 验证。
  • 源映射:开发环境中使用源映射以便于调试;生产环境中可能不会包含这些,以增强安全性。
严格的相等比较(== vs ===):这里的“==”是类型转换相等,“===”是严格相等,不进行类型转换。

「问题:」解释 JavaScript 中 == 和 === 的区别,并在 React 中何时使用它们?

答:

==(松散相等):如果类型不同,则会进行类型转换,只比较值。
===(严格相等):不仅比较值,还比较类型,但不会进行类型转换。

在 React 中,你应该始终使用 === 进行状态比较或条件渲染,以避免因类型转换导致的意外行为。例如,在检查 props 或状态值是否相等时,务必使用 === 而不是 ==。

在React中的渲染过程:

问题: 在 React 中,render() 函数和 ReactDOM.render() 和有什么不同?

答案是:

  • render(): 类组件中的一种方法,用来返回应该显示在 DOM 上的内容。它是组件生命周期中的一个环节。
  • ReactDOM.render(): 用于将 React 元素渲染到特定 DOM 容器中的函数。它用来启动整个应用或应用某个部分的渲染。

在 React 中搞定不同的 props 可能有点麻烦:

问题: 如何将不同类型的数据传递给一个 React 组件中,以及如何管理和使用这些数据?

答案是:

  • 基本类型属性:如数字、字符串、布尔值,直接传递。
  • 对象类型属性:传递对象或数组引用。
  • 函数类型属性:用于回调或事件处理程序。

通过在开发模式中定义 propTypes 来进行类型检查,并通过在组件中解构 props 使代码更清晰,来管理它们。

const MyComponent = ({ name, age, onClick }) => { return <div onClick={onClick}>{name}, {age}</div>; };

在 React 中调用 API:

在遵循状态管理最佳实践的情况下,怎样在React组件中从API的获取数据?

答:

使用 useEffect 来处理像 API 调用之类的副作用等。

import React, { useState, useEffect } from 'react'; function FetchData() { const [data, setData] = useState(null); useEffect(() => { fetch('your-api-endpoint') .then(response => response.json()) .then(data => setData(data)) .catch(error => console.error('错误:', error)); }, []); // 这样可以确保副作用只在组件挂载时运行一次,以确保其只执行一次 return ( <div> {data ? JSON.stringify(data) : '正在加载...'} </div> ); }

  • 处理加载状态以及错误,使用 useCallback 进行缓存化,如果需要将 fetch 函数作为属性传递以优化重新渲染。
问题1:实现一个自定义线程队列

问题: 编写一个基本的Java线程池实现。你的线程池应该能够处理固定数量的线程,排队并执行任务。包含提交任务、关闭线程池和处理任务的方法,如所需。特别地,应提供终止线程池的功能。

答:

    import java.util.concurrent.BlockingQueue;  
    import java.util.concurrent.LinkedBlockingQueue;  
    import java.util.concurrent.TimeUnit;  

    public class 自定义线程池类 {  
        private final int 线程数;  
        private final 自定义线程工作者[] 线程;  
        private final BlockingQueue<Runnable> 任务队列;  

        public 自定义线程池类(int 线程数) {  
            this.线程数 = 线程数;  
            任务队列 = new LinkedBlockingQueue<>();  
            线程 = new 自定义线程工作者[线程数];  

            for (int i = 0; i < 线程数; i++) {  
                线程[i] = new 自定义线程工作者();  
                线程[i].start();  
            }  
        }  

        public void 提交任务(Runnable 任务) throws InterruptedException {  
            任务队列.put(任务);  
        }  

        public void 关闭线程池() throws InterruptedException {  
            for (自定义线程工作者 工作者 : 线程) {  
                工作者.停止工作线程();  
            }  

            for (自定义线程工作者 工作者 : 线程) {  
                工作者.join();  
            }  
        }  

        private class 自定义线程工作者 extends Thread {  
            private volatile boolean 运行中 = true;  

            public void 停止工作线程() {  
                运行中 = false;  
            }  

            @Override  
            public void run() {  
                while (运行中) {  
                    try {  
                        Runnable 任务 = 任务队列.take();  
                        任务.run();  
                    } catch (InterruptedException e) {  
                        // 重新中断当前线程  
                        Thread.currentThread().interrupt();  
                    }  
                }  
            }  
        }  

        public static void main(String[] args) throws InterruptedException {  
            // 创建一个包含两个线程的工作线程池  
            自定义线程池类 池 = new 自定义线程池类(2);  

            for (int i = 0; i < 5; i++) {  
                int 任务ID = i;  
                池.提交任务(() -> {  
                    System.out.println("任务 " + 任务ID + " 来自 " + Thread.currentThread().getName());  
                    try {  
                        TimeUnit.SECONDS.sleep(1);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                });  
            }  

            池.关闭线程池();  
        }  
    }

解释: 这个示例演示了一个简单的线程池,其中任务在一个队列中被处理,固定数量的线程来执行这些任务。实现中包括一个关闭机制,以优雅地停止所有线程。

问题2:实现一个最近未使用缓存功能

问题: 实现一个具有O(1)时间复杂度的LRU缓存,使得 getput 操作的时间复杂度均为 O(1)。当超出容量时,应该移除最近最少使用的项目。

回答:

答案。

    import java.util.HashMap;  
    import java.util.Map;  

    class LRUCache {  
        private class Node {  
            int key, value;  
            Node prev, next;  

            Node(int key, int value) {  
                this.key = key;  
                this.value = value;  
            }  
        }  

        private Map<Integer, Node> cache;  
        private int capacity;  
        private Node head, tail;  

        public LRUCache(int capacity) {  
            this.capacity = capacity;  
            cache = new HashMap<>();  
            head = new Node(0, 0);  
            tail = new Node(0, 0);  
            head.next = tail;  
            tail.prev = head;  
        }  

        public int get(int key) {  
            if (!cache.containsKey(key)) return -1;  
            Node node = cache.get(key);  
            removeNode(node);  
            addToHead(node);  
            return node.value;  
        }  

        public void put(int key, int value) {  
            if (cache.containsKey(key)) {  
                removeNode(cache.get(key));  
            }  
            if (cache.size() >= capacity) {  
                removeNode(tail.prev);  
            }  
            Node node = new Node(key, value);  
            addToHead(node);  
            cache.put(key, node);  
        }  

        private void removeNode(Node node) {  
            node.prev.next = node.next;  
            node.next.prev = node.prev;  
        }  

        private void addToHead(Node node) {  
            node.next = head.next;  
            node.prev = head;  
            head.next.prev = node;  
            head.next = node;  
        }  

        public static void main(String[] args) {  
            LRUCache cache = new LRUCache(2);  
            cache.put(1, 1);  
            cache.put(2, 2);  
            System.out.println(cache.get(1)); // 打印输出 1  
            cache.put(3, 3); // 移除键 2  
            System.out.println(cache.get(2)); // 打印输出 -1 (找不到)  
            cache.put(4, 4); // 移除键 1  
            System.out.println(cache.get(1)); // 打印输出 -1 (找不到)  
            System.out.println(cache.get(3)); // 打印输出缓存的 get(3)  
            System.out.println(cache.get(4)); // 打印输出缓存的 get(4)  
        }  
    }

解释: 这种实现方式使用了双向链表(Doubly Linked List)来维护顺序,并使用一个 HashMap 来实现 O(1) 时间访问节点。当访问或添加一个项目时,该项目会被移到链表的头部,确保最近最少使用的项目始终位于链表尾端,准备在缓存满时被移除出缓存。

Spring 和微服务架构

问题一:解释Spring Boot在微服务架构中的角色:

问题: 什么是Spring Boot,它又是如何方便开发微服务架构的?

Spring Boot 是 Spring 框架的一个扩展,它简化了设置、配置和运行独立的、生产就绪的 Spring 应用程序的过程,只需最少的配置即可。

  • 自动配置:Spring Boot 尽可能自动配置 Spring 和第三方库,减少常见用例(如设置数据库、web 服务器等)的样板代码。
  • 预设配置:它带有提供各种功能默认配置的“启动器”依赖项,使使用一致的技术栈构建微服务变得容易。
  • 嵌入式服务器:Spring Boot 支持使用像 Tomcat、Jetty 或 Undertow 这样的嵌入式服务器运行 web 应用程序,这对于独立部署微服务非常关键。
  • 微服务支持

  • 服务发现:与 Eureka 或 Consul 等服务发现工具集成,支持动态服务注册与发现。

  • 分布式配置:使用 Spring Cloud Config 实现集中配置管理。

  • 断路器:使用 Hystrix 或 Resilience4j 实现断路器等弹性机制。

  • API 网关:与 Spring Cloud Gateway 等工具配合,进行路由和负载均衡。

  • 生产就绪特性:提供健康检查、指标和外部化配置,这对于生产环境中的微服务来说必不可少。
第2题:谈谈微服务架构中API网关的重要性

为什么在微服务架构中的API网关很重要,Spring Cloud Gateway是如何解决这些问题的?

回答:

在微服务架构中,API网关扮演了几个关键角色:

  • 单一入口点:它们充当所有客户端请求的唯一入口,简化了客户端的交互,隐藏了服务架构的复杂性。
  • 请求路由:根据路径、头信息等标准将请求路由到相应的后端服务。
  • 负载均衡:将请求分发到各个服务实例,以管理负载并确保高可用性。
  • 安全:实现认证、速率限制,并可以作为服务的安全防线。
  • 横切关注点:在一个地方处理日志记录、监控和度量收集等事项,而不需要在每个服务中实现。

Spring Cloud网关,更重要的是,Spring Cloud网关增强了微服务生态系统的功能。

  • 路由设定:支持通过外部配置定义路由,使其动态且可管理,无需更改代码。
  • 条件和过滤器:提供丰富的内置条件集,用于路由,以及用于修改请求/响应的过滤器,支持复杂的路由逻辑和转换。
  • 与Spring生态系统集成:无缝集成其他Spring Cloud组件,包括服务发现、配置管理和弹性。
  • 响应式编程:基于Project Reactor,支持响应式流,使其在处理微服务环境中常见的高并发和反压情况时更加高效。
在Spring Cloud微服务中,服务发现是如何实现的?

问题: 在使用Spring Cloud的微服务架构中,服务发现是如何工作的,你能描述一下吗?它能带来哪些好处,你能说一下吗?

回答:

服务发现功能涉及到以下内容:

  • 注册:当服务实例启动时,会将自己注册到一个服务注册中心(如 Eureka 或 Consul),这包括其网络位置、健康状况等信息。
  • 查找:客户端或其它服务可以查询这个注册中心来找到他们需要通信的服务实例。
  • 动态更新:当服务被添加或移除时,注册中心会实时更新信息,确保客户端能找到活跃的服务。

福利:

  • 解耦性:服务不需要知道彼此的位置;它们通过逻辑服务名称来通信,减少耦合。
  • 可扩展性:通过允许添加或删除新实例而不更改客户端配置来支持水平扩展性。
  • 弹性:如果某个服务实例宕机了,客户端可以自动发现健康的实例,提高系统的容错能力。
  • 负载均衡:负载均衡通常与客户端的负载均衡功能集成,将请求在多个服务实例之间分配。

Spring Cloud 简化这一点,如下所示:

  • Eureka 或 Consul 集成:内置的服务注册表支持。
  • DiscoveryClient:服务用于与注册表交互的抽象类,抽象了底层的发现机制。
  • Ribbon(适用于较旧版本)或 LoadBalancerClient:用于客户端负载均衡,确保请求被均匀分配到各个实例。

这种设置是实现微服务所承诺的弹性和可扩展性的基础,同时也是实现自治性的基础。

就这样了,兄弟们。

谢谢阅读
  • 👏 给这个故事点个 👏 吧,并关注我 👉
  • 📰 在我的 Medium 上看更多内容(Java开发者面试的21篇文章)

可以在这里找到我的书

  • 清晰的 Java 开发者面试指南 这里Amazon (Kindle 电子书) 和_Gumroad(PDF 格式).
  • 清晰的 Spring-Boot 微服务面试指南 这里Gumroad(PDF 格式) 和_Amazon(Kindle 电子书).
  • 🔔 来关注我: LinkedIn | Twitter |YouTube
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
205
获赞与收藏
1009

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消