【百度面经】提前批Java一面|0721

原文链接:【百度面经】提前批Java一面|0721_牛客网 (nowcoder.com)

本文仅作为个人学习使用。


Spring中面向切面编程,说一说AOP

在 Spring 中,面向切面编程(Aspect-Oriented Programming, AOP)是一种编程范式,它可以让我们将横切关注点(如日志、安全、事务管理等)从业务逻辑中分离出来,从而使代码更加模块化和可维护。

AOP 的主要概念包括:

  • 切面(Aspect):定义了一个关注点及其实现。切面可以包含多个通知。
  • 通知(Advice):定义了当特定连接点出现时应该采取的行为。通知有以下几种类型:
    • 前置通知(Before)
    • 后置通知(After)
    • 返回通知(AfterReturning)
    • 抛出异常通知(AfterThrowing)
    • 环绕通知(Around)
  • 连接点(Joinpoint): 程序执行过程中的某个特定位置,如方法的调用或特定异常的处理。
  • 切入点(Pointcut): 定义了一组相关的连接点,用于确定通知应该被应用的位置。
  • 目标对象(Target Object): 被通知的对象,即包含主要业务逻辑的对象。
  • AOP 代理(AOP Proxy): AOP 框架创建的对象,用于实现切面契约(包括通知方法执行等)。

AOP常用场景

AOP(Aspect Oriented Programming)在Spring框架中有很多常用的场景,主要包括以下几种:

  • 日志记录:

    • 使用AOP在方法执行前后记录日志信息,如方法调用时间、参数、返回值等。
    • 可以帮助开发者更好地监控和诊断系统运行状况。
  • 性能监控:

    • 使用AOP计算方法的执行时间,并进行性能分析和优化。
    • 可以帮助开发者发现系统瓶颈,提高应用程序的性能。
  • 安全控制:

    • 使用AOP实现方法级别的身份验证和授权,确保只有经过身份验证和授权的用户才能访问方法。
    • 可以提高系统的安全性,降低安全风险。
  • 事务管理:

    • 使用AOP在方法执行前后添加事务控制逻辑,确保方法执行要么全部成功,要么全部回滚。
    • 可以简化事务管理的代码,提高事务处理的一致性。
  • 缓存管理:

    • 使用AOP在方法执行前检查缓存,如果缓存命中则直接返回结果,否则执行方法并将结果缓存。
    • 可以提高系统的响应速度,减轻数据库的压力。
  • 异常处理:

    • 使用AOP捕获方法抛出的异常,并进行统一的异常处理和错误信息返回。
    • 可以提高系统的健壮性,增强用户体验。
  • 数据校验:

    • 使用AOP在方法执行前对输入参数进行校验,确保数据的合法性和完整性。
    • 可以减少代码重复,提高开发效率。

AOP的实现

AOP(Aspect Oriented Programming)的实现主要包括以下几个核心概念和机制:

  • 连接点(Joinpoint):

    • 连接点是程序执行过程中能够插入切面的一个点,如方法的调用、异常的抛出等。
    • 在 Spring AOP 中,连接点通常指方法的执行。
  • 切点(Pointcut):

    • 切点定义了在哪些连接点上应用通知。
    • 切点通常使用表达式语言来描述,如 execution(* com.example.service.*.*(..))
  • 通知(Advice):

    • 通知定义了在切点上要执行的操作,如方法调用前、调用后、抛出异常时等。
    • 通知包括前置通知、后置通知、环绕通知、异常通知等。
  • 切面(Aspect):

    • 切面是通知和切点的组合,定义了在何时何地执行何种操作。
    • 切面可以由多个通知和切点组成。
  • 织入(Weaving):

    • 织入是把切面应用到目标对象从而创建代理对象的过程。
    • 在 Spring AOP 中,织入通常在运行时完成,使用动态代理技术。
  • 代理(Proxy):

    • 代理是AOP的核心实现机制,是目标对象的增强版本。
    • 代理对象会拦截目标对象的方法调用,并在调用前后执行切面逻辑。

Spring AOP 的实现主要基于 Java 动态代理和 AspectJ 两种技术:

  • Java 动态代理:

    • 当目标对象实现了接口时,Spring AOP 使用 JDK 动态代理创建代理对象。
    • 代理对象会拦截接口方法的调用,并执行切面逻辑。
  • AspectJ:

    • 当目标对象没有实现接口时,Spring AOP 使用 AspectJ 的字节码植入技术创建代理对象。
    • AspectJ 可以在编译期或加载期修改目标类的字节码,实现切面织入。

 查数据库响应慢怎么排查?

  • 确定问题的具体表现

    • 哪些查询语句或者功能比较慢?
    • 是整体响应慢还是某些特定查询比较慢?
    • 是否只有在特定时间段或者并发访问较高时才出现问题?
  • 收集相关信息

    • 查看数据库的整体性能监控指标,如 CPU、内存、磁盘IO等使用情况。
    • 获取慢查询日志,查看哪些SQL语句执行时间较长。
    • 分析查询执行计划,了解数据库如何执行查询。
  • 定位问题的根源

    • 检查SQL语句本身是否存在问题,如没有使用索引、使用了全表扫描等。
    • 分析表结构是否存在设计问题,如字段类型不合理、缺乏索引等。
    • 检查数据库配置是否合理,如内存分配、连接池设置等。
    • 考虑是否存在瓶颈资源,如磁盘IO、网络带宽等。
  • 优化和调整

    • 针对SQL语句问题,可以优化查询条件、添加索引等。
    • 对表结构进行优化,如调整字段类型、增加合理的索引。
    • 优化数据库配置,如调整内存分配、连接池大小等。
    • 考虑使用缓存技术,如Redis缓存查询结果。
    • 水平扩展数据库,如增加节点或者分库分表。
  • 验证优化效果

    • 再次测试关键SQL语句,查看执行时间是否有明显改善。
    • 监控数据库整体性能指标,确认瓶颈问题是否得到缓解。
    • 持续优化,直到查询响应时间达到预期要求。

为什么加索引可以加快扫描范围 ?

  • 索引结构优化查询

    • 索引是按照特定的数据结构(如B+树)组织的,可以提供有序的数据访问路径。
    • 通过索引查找目标数据时,可以快速定位到相应的数据块,而不需要进行全表扫描。
  • 减少磁盘IO操作

    • 全表扫描时需要读取整个表的数据,会产生大量的随机磁盘IO操作,效率较低。
    • 使用索引查找时,只需要根据索引定位到相应的数据块,然后顺序读取少量的数据,大大减少了磁盘IO操作。
  • 利用索引过滤数据

    • 索引中存储了列值及其对应的行指针,可以先在索引上进行过滤和查找。
    • 找到目标行后,只需要从表中取出需要的列数据,避免了全表扫描。
  • 支持范围查找

    • 索引是有序的,可以高效地进行范围查找,如 WHERE column BETWEEN a AND b
    • 全表扫描则需要逐行检查每条记录是否满足条件,效率较低。
  • 提高查询并发性

    • 使用索引可以减少每个查询的IO操作,从而提高并发查询的处理能力。
    • 全表扫描会产生大量随机IO,容易成为数据库的性能瓶颈。

Java集合类有哪些,介绍一下HashMap底层数据结构?

集合类

  • List接口

    • ArrayList: 基于数组实现的动态数组
    • LinkedList: 基于双向链表实现的列表
  • Set接口

    • HashSet: 基于HashMap实现的集合
    • TreeSet: 基于红黑树实现的有序集合
    • LinkedHashSet: 基于链表和哈希表实现的有序集合
  • Map接口

    • HashMap: 基于哈希表实现的键值对集合
    • TreeMap: 基于红黑树实现的有序键值对集合
    • LinkedHashMap: 基于链表和哈希表实现的有序键值对集合

HashMap底层数据结构

  • 数组

    • HashMap内部使用一个数组作为存储结构的基础,称为"桶"(bucket)。
    • 当向HashMap中添加元素时,会根据key的哈希值计算出索引,将元素存储在对应的桶中。
  • 哈希算法

    • HashMap使用哈希算法计算key的哈希值,通过哈希值确定元素存储在数组的哪个位置。
    • 常见的哈希算法有直接定址法、数字分析法、平方取中法等。
  • 链表

    • 如果某个桶中存储的元素过多,会在桶内形成一条链表,以解决哈希冲突。
    • 当查找时,如果发生哈希冲突,则需要遍历链表来找到对应的元素。
  • 红黑树

    • 从Java 8开始,当某个桶中的元素超过8个时,该桶会转换为红黑树,以提高查找效率。
    • 红黑树是一种自平衡二叉搜索树,可以保证查找、插入、删除的时间复杂度为O(logn)。

为什么arraylist检索快增删慢?

  • 数组结构

    • ArrayList 内部使用一个数组来存储元素,这使得随机访问(get/set)的时间复杂度为 O(1),即非常快速。
    • 但是在向中间插入或删除元素时,需要移动大量元素,导致时间复杂度为 O(n),效率较低。
  • 动态扩容

    • 当 ArrayList 容量不足时,需要进行动态扩容操作,通常是扩容到原来容量的 1.5 倍。
    • 扩容过程中需要重新分配内存并拷贝数据,对于大容量 ArrayList 来说,这个操作代价很高。
  • 数组元素移动

    • 在 ArrayList 中插入或删除元素时,需要移动大量元素来保持数据的连续性。
    • 移动元素的时间复杂度为 O(n),随着元素数量的增加而变慢。
  • 装箱和拆箱

    • 对于基本数据类型,ArrayList 会进行自动装箱和拆箱操作,带来一定的性能开销。

相比之下,LinkedList 的实现原理是基于双向链表,对于增删操作的时间复杂度为 O(1),但随机访问的时间复杂度为 O(n)。

因此,如果应用场景中需要频繁进行增删操作,则 LinkedList 可能会更加适合。但如果主要是进行大量的随机访问操作,则 ArrayList 会更加高效。


红黑树和链表的查询效率对比?

  • 红黑树

    • 红黑树是一种自平衡二叉搜索树,具有良好的性能特性。
    • 在平均情况下,红黑树的查找时间复杂度为 O(log n)。这意味着,随着元素数量的增加,查找时间只会缓慢增长。
    • 红黑树能够保持平衡,即使在最坏情况下,查找时间复杂度也只会退化到 O(log n)。
  • 链表

    • 链表是一种线性数据结构,元素之间通过指针链接。
    • 在链表中查找元素的时间复杂度为 O(n),这意味着随着元素数量的增加,查找时间会呈线性增长。
    • 链表的查找效率随元素数量的增加会显著下降,这是其主要缺点之一。

总的来说,红黑树的查询效率要远远高于链表。在大规模数据集中,红黑树的优势会更加明显。


多线程情况下想使用hashmap怎么办?

  • 使用 ConcurrentHashMap

    • ConcurrentHashMap 是 Java 并发包提供的一个线程安全的 Map 实现。它使用了分段锁的机制,提高了并发性能。
    • 相比于使用 Collections.synchronizedMap(new HashMap<>()) 来包装 HashMap,ConcurrentHashMap 的性能更好。
  • 使用 synchronized 关键字

    • 可以手动在 HashMap 的关键操作上加 synchronized 关键字,来保证线程安全。
    • 这种方式粒度比较粗,可能会影响并发性能,不太推荐。
  • 使用 ReadWriteLock

    • 可以使用 ReentrantReadWriteLock 来实现对 HashMap 的读写操作的并发控制。
    • 读操作可以并发执行,写操作需要独占锁。

 对hashmap加锁和直接用ConcurrentHashMap有什么区别?

  • 性能

    • 使用 synchronized 关键字或 Lock 锁来保护整个 HashMap 会导致较高的锁竞争,从而降低性能。
    • ConcurrentHashMap 采用了分段锁的机制,允许多个线程并发地访问和修改不同段的数据,从而提高了并发性能。
  • 并发性

    • synchronized 和 Lock 锁会导致整个 HashMap 被锁定,阻塞其他线程对 HashMap 的访问。
    • ConcurrentHashMap 通过分段锁可以实现对不同段的并发访问,大大提高了并发性。
  • 扩展性

    • 随着线程数的增加,synchronized 和 Lock 锁会成为性能瓶颈。
    • ConcurrentHashMap 可以通过调整并发级别(concurrency level)来适应不同的并发场景,拥有更好的扩展性。
  • 安全性

    • 使用 synchronized 和 Lock 需要开发人员手动管理加锁和释放锁,容易出现死锁等问题。
    • ConcurrentHashMap 内部使用了各种并发控制机制,可以确保线程安全,降低了开发者的编程难度。
  • 功能性

    • ConcurrentHashMap 提供了更丰富的线程安全操作,如 putIfAbsent、replace 等原子操作,使编程更加简单和安全。
    • 使用 synchronized 和 Lock 需要自己实现这些复杂的线程安全操作。

综上所述,在多线程环境下使用 ConcurrentHashMap 相比于在 HashMap 上加 synchronized 锁或 Lock 锁,能够提供更好的性能、并发性、扩展性、安全性和功能性。这也是 ConcurrentHashMap 被广泛使用的主要原因。


常见多线程的使用方式(创建线程的方式)

  • 继承 Thread 类

    • 创建一个继承 Thread 类的子类,并重写 run() 方法。
    • 实例化子类并调用 start() 方法启动线程。
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程要执行的任务
    }
}

MyThread thread = new MyThread();
thread.start();
  • 实现 Runnable 接口

    • 创建一个实现 Runnable 接口的类,并重写 run() 方法。
    • 将 Runnable 实例传递给 Thread 构造函数,创建并启动线程。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程要执行的任务
    }
}

Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
  • 使用线程池

    • 通过 Executor 框架创建线程池,避免频繁创建和销毁线程。
    • 可以使用 ExecutorService 接口及其实现类,如 ThreadPoolExecutor。
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
    // 线程要执行的任务
});
executor.shutdown();
  • 使用 Future 和 FutureTask

    • 使用 Future 接口表示异步计算的结果。
    • 使用 FutureTask 包装 Callable 接口实现,提交给线程池执行。
Callable<String> callable = () -> {
    // 线程要执行的任务
    return "Result";
};
FutureTask<String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
String result = futureTask.get();
  • 使用 CompletableFuture

    • Java 8 引入的 CompletableFuture 可以方便地处理异步任务,支持链式调用。
    • 可以使用静态工厂方法创建 CompletableFuture 实例,并定义异步任务。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 线程要执行的任务
    return "Result";
});
String result = future.get();

以上是 Java 中创建和使用多线程的常见方式。选择合适的方式取决于具体的需求和场景,如线程的复用、任务的性质(CPU 密集型还是 I/O 密集型)、异步处理的需求等。


场景题1(内连接与外连接)

现在有两张表,A表和B表,第一张表里有两行数据,分别是A和B;第二张表里也有两行数据,是B和C。现在对这两张表进行join,分别是内连接和左连接,join完后结果分别有几行

对于两张表 A 表和 B 表:

A 表:

行 1: A

行 2: B

B 表:

行 1: B

行 2: C

内连接 (INNER JOIN):

内连接会只保留两个表中匹配的行。

结果集将包含 1 行, 因为 B 表中只有一行与 A 表中的 B 行匹配。

A | B
--+--
B | B

左连接 (LEFT JOIN):

左连接会保留左表(A 表)的所有行,并尽可能匹配右表(B 表)的行。

结果集将包含 2 行:

第 1 行: A | null

第 2 行: B | B

A | B
--+--
A | null
B | B

总结:

  • 内连接后的结果集有 1 行
  • 左连接后的结果集有 2 行

在数据库中,表之间的连接方式主要有以下几种:

具体可参考:图解MySQL 内连接、外连接、左连接、右连接、全连接……太多了-CSDN博客

  • 内连接 (INNER JOIN):

    • 只返回两个表中满足连接条件的记录。
    • 也可以使用简单的 SELECT ... FROM A, B WHERE A.col = B.col 语法。
  • 左连接 (LEFT JOIN 或 LEFT OUTER JOIN):

    • 返回左表的所有记录,以及右表中满足连接条件的记录。
    • 对于左表中没有匹配的记录,右表的列将显示为 NULL。
  • 右连接 (RIGHT JOIN 或 RIGHT OUTER JOIN):

    • 返回右表的所有记录,以及左表中满足连接条件的记录。
    • 对于右表中没有匹配的记录,左表的列将显示为 NULL。
  • 全连接 (FULL JOIN 或 FULL OUTER JOIN):

    • 返回左右两个表的所有记录。
    • 对于没有匹配的记录,相应的列将显示为 NULL。
  • 交叉连接 (CROSS JOIN):

    • 返回左表中的每一行与右表中的每一行的笛卡尔积。
    • 没有 ON 子句,因为是无条件连接。
  • 自连接 (SELF JOIN):

    • 一个表与自身进行连接,通常用于查找表内部的关系。
    • 需要为表指定别名,以区分不同的实例。

场景题2(大文件top10)

一个大文件记录着一个服务来源的所有访问的IP地址,大文件远超过所处理的机器的内存,如何找出访问次数排名前十的IP地址

对于一个大文件,远远超过机器的内存大小,要找出访问次数排名前十的 IP 地址,可以采用以下步骤:

  • 分块读取文件:

    • 将大文件分成多个小块,每次读取一个小块到内存中处理。
    • 这样可以避免一次性将整个文件读入内存,节省内存开销。
  • 使用HashMap统计IP出现次数:

    • 对于每个小块,使用 HashMap 来统计 IP 地址出现的次数。
    • HashMap 的键为 IP 地址,值为出现次数。
  • 合并统计结果:

    • 遍历处理完所有小块后,将各个 HashMap 中的统计结果合并起来。
    • 可以使用另一个 HashMap 来汇总所有 IP 地址的出现次数。
  • 排序并输出前十:

    • 将合并后的 HashMap 中的数据按照出现次数排序。
    • 输出排名前十的 IP 地址。

下面是一个简单的 Java 代码示例:

public class IPCounter {
    public static void findTopTenIPs(String filePath, int blockSize) {
        Map<String, Integer> ipCount = new HashMap<>();

        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String ip = line.trim();
                ipCount.merge(ip, 1, Integer::sum);

                if (ipCount.size() >= blockSize) {
                    List<Map.Entry<String, Integer>> sortedIPs = ipCount.entrySet().stream()
                            .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                            .limit(10)
                            .collect(Collectors.toList());

                    sortedIPs.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
                    ipCount.clear();
                }
            }

            // 处理最后一个小块
            List<Map.Entry<String, Integer>> sortedIPs = ipCount.entrySet().stream()
                    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                    .limit(10)
                    .collect(Collectors.toList());
            sortedIPs.forEach(entry -> System.out.println(entry.getKey() + ": " + entry.getValue()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        findTopTenIPs("path/to/your/file.txt", 1000);
    }
}

该代码将文件分成 1000 行一个小块进行处理,使用 HashMap 统计每个 IP 地址出现的次数,并最终输出访问次数排名前十的 IP 地址。

当处理一个大文件,内存无法完全加载时,还有其他一些方法可以解决这种问题,比如:

  • 使用外部排序:

    • 将文件划分为多个小文件,对每个小文件进行内存内排序。
    • 然后通过多路归并的方式,将排好序的小文件合并为一个有序的大文件。
    • 最后在有序的大文件上进行查找前十名 IP 地址。
  • 使用 MapReduce 框架:

    • 利用 MapReduce 的分布式处理能力,可以将任务拆分到多台机器上并行执行。
    • Map 阶段统计每个 IP 地址的出现次数,Reduce 阶段汇总并排序得到前十名。
  • 使用 Streaming 技术:

    • 利用流处理的方式,将输入文件逐行读取并处理。
    • 可以使用 Python 的 heapq 模块维护一个大小为 10 的最大堆,动态更新前十名 IP 地址。
  • 使用数据库:

    • 将文件中的 IP 地址批量插入到数据库表中。
    • 然后在数据库端执行 SELECT ip, COUNT(*) AS count FROM table GROUP BY ip ORDER BY count DESC LIMIT 10; 查询,获取前十名 IP 地址。
  • 使用 Spark 或 Flink 等大数据框架:

    • 利用这些分布式计算框架的并行处理能力,可以更高效地统计 IP 地址出现次数并排序。
    • 这种方式需要有相应的大数据处理环境,但可以处理更大规模的数据。

选择合适的方法时,需要考虑系统的硬件条件、数据量的大小、性能要求等因素。大文件处理的最佳方法因场景而异,需要权衡各种方法的优缺点进行选择。


算法题(多个值为m的元素下标,二分法)

在长度为N的有序数组中快速查找所有值为M的元素下标(M可能重复出现)。

在长度为 N 的有序数组中快速查找所有值为 M 的元素下标,可以使用二分查找的思想。以下是 Java 代码实现:

public static List<Integer> findAllIndexes(int[] arr, int target) {
    List<Integer> result = new ArrayList<>();
    int left = 0, right = arr.length - 1;

    // 找到第一个出现 target 的索引
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) {
            result.add(mid);
            // 向左找更早出现的 target
            int leftIndex = mid - 1;
            while (leftIndex >= 0 && arr[leftIndex] == target) {
                result.add(0, leftIndex);
                leftIndex--;
            }
            break;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    // 如果找不到 target,直接返回空列表
    if (result.isEmpty()) {
        return result;
    }

    // 向右找更晚出现的 target
    int rightIndex = result.get(result.size() - 1) + 1;
    while (rightIndex < arr.length && arr[rightIndex] == target) {
        result.add(rightIndex);
        rightIndex++;
    }

    return result;
}

这个方法的主要步骤如下:

  1. 先使用二分查找找到数组中第一次出现 target 的索引。
  2. 从这个索引开始向左查找,直到找到所有值为 target 的索引,并将它们加入结果列表。
  3. 从第一次出现 target 的索引开始向右查找,直到找到所有值为 target 的索引,并将它们加入结果列表。
  4. 最后返回包含所有 target 元素下标的结果列表。

这个算法的时间复杂度为 O(log n + k),其中 n 是数组的长度,k 是数组中值为 target 的元素个数。这是因为二分查找的时间复杂度为 O(log n),而向左右查找的时间复杂度为 O(k)。

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值