java面试记录

HashMap里的碰撞

在 Java 中,HashMap 是一种常用的哈希表实现,它使用哈希函数来确定键值对的存储位置。当多个键映射到同一个桶(bucket)时,就会发生哈希碰撞(hash collision)。哈希碰撞是哈希表中不可避免的现象,但可以通过合理的设计来最小化其影响。

哈希碰撞的原因

哈希碰撞通常是因为哈希函数产生的哈希值相同,而哈希表的大小有限,导致多个键值对映射到了同一个位置。

处理哈希碰撞的方法

  1. 链地址法(Separate Chaining)

    • 在 HashMap 中,当发生哈希碰撞时,键值对会被添加到同一个桶中的一个链表或树结构中。
    • 例如,如果两个键值对 key1 和 key2 映射到同一个桶,那么它们会在该桶中形成一个链表。
  2. 开放寻址法(Open Addressing)

    • 不使用链表,而是寻找下一个可用的槽位来存放发生碰撞的元素。
    • 通常使用线性探测、二次探测或双散列等策略来寻找空闲的位置。
示例代码

下面是一个简单的示例,展示了在 HashMap 中如何处理哈希碰撞:

1import java.util.HashMap;
2
3public class HashMapCollisionExample {
4
5    public static void main(String[] args) {
6        HashMap<String, Integer> map = new HashMap<>();
7
8        // 添加键值对
9        map.put("one", 1);
10        map.put("two", 2);
11        map.put("three", 3);
12        map.put("to", 4); // 注意这里 "to" 与 "two" 的哈希值可能相同
13        
14        // 查找键值对
15        System.out.println(map.get("one")); // 输出 1
16        System.out.println(map.get("two")); // 输出 2
17        System.out.println(map.get("to"));  // 输出 4
18    }
19}

在这个示例中,假设 "to""two" 的哈希值相同,那么它们会被放在同一个桶中。由于 HashMap 使用链地址法来处理碰撞,因此它们会被添加到同一个桶的链表中。

总结

HashMap 中,哈希碰撞是通过链地址法来处理的,即当多个键值对映射到同一个桶时,它们会被存储在一个链表中。Java 的 HashMap 默认使用链地址法来解决哈希碰撞问题,这种方法简单且有效,但也可能会影响性能,特别是在大量碰撞发生时。为了减少碰撞的影响,可以选择一个好的哈希函数,并且适当调整 HashMap 的容量和负载因子。

java中反射

在 Java 中,反射是一种强大的工具,允许你在运行时检查和操作类、接口、构造函数、方法和字段等。利用反射,你可以动态地获取类的信息并调用其方法。下面是一些常见的使用反射获取类信息的方法:

1. 获取类的信息

示例代码
1import java.lang.reflect.Constructor;
2import java.lang.reflect.Field;
3import java.lang.reflect.Method;
4
5public class ReflectionExample {
6
7    public static void main(String[] args) throws Exception {
8        Class<?> clazz = Class.forName("com.example.MyClass");
9
10        // 获取类名
11        System.out.println("Class Name: " + clazz.getName());
12
13        // 获取所有公共构造函数
14        Constructor<?>[] constructors = clazz.getConstructors();
15        for (Constructor<?> constructor : constructors) {
16            System.out.println("Constructor: " + constructor);
17        }
18
19        // 获取所有方法
20        Method[] methods = clazz.getDeclaredMethods();
21        for (Method method : methods) {
22            System.out.println("Method: " + method);
23        }
24
25        // 获取所有字段
26        Field[] fields = clazz.getDeclaredFields();
27        for (Field field : fields) {
28            System.out.println("Field: " + field);
29        }
30    }
31}

2. 调用方法

示例代码
1import java.lang.reflect.Method;
2
3public class MethodInvocationExample {
4
5    public static void main(String[] args) throws Exception {
6        Class<?> clazz = Class.forName("com.example.MyClass");
7        Object instance = clazz.newInstance();
8
9        // 调用无参方法
10        Method noArgMethod = clazz.getMethod("noArgMethod");
11        noArgMethod.invoke(instance);
12
13        // 调用带参数的方法
14        Method withArgMethod = clazz.getMethod("withArgMethod", String.class);
15        withArgMethod.invoke(instance, "Hello, World!");
16    }
17}

3. 创建实例

示例代码
1import java.lang.reflect.Constructor;
2
3public class InstanceCreationExample {
4
5    public static void main(String[] args) throws Exception {
6        Class<?> clazz = Class.forName("com.example.MyClass");
7
8        // 创建无参构造函数实例
9        Constructor<?> noArgConstructor = clazz.getConstructor();
10        Object instance = noArgConstructor.newInstance();
11
12        // 创建带参数构造函数实例
13        Constructor<?> withArgConstructor = clazz.getConstructor(String.class);
14        Object instanceWithArgs = withArgConstructor.newInstance("Hello, World!");
15    }
16}

4. 修改字段值

示例代码
1import java.lang.reflect.Field;
2
3public class FieldModificationExample {
4
5    public static void main(String[] args) throws Exception {
6        Class<?> clazz = Class.forName("com.example.MyClass");
7        Object instance = clazz.newInstance();
8
9        // 获取字段
10        Field field = clazz.getDeclaredField("fieldName");
11        field.setAccessible(true); // 必须设置为 true 才能访问私有字段
12
13        // 修改字段值
14        field.set(instance, "New Value");
15    }
16}

总结

反射在 Java 中提供了强大的功能,可以用来动态地操作类和对象。但需要注意的是,反射操作通常比普通的方法调用要慢,而且可能会破坏封装性和安全性。因此,在使用反射时需要谨慎,并确保只在必要时使用。

怎么自定义异常

在 Java 中,自定义异常是一种常见的做法,用于表示程序中特定类型的错误情况。自定义异常通常继承自 Exception 类或其子类(如 RuntimeException)。下面是如何创建和使用自定义异常的步骤和示例代码:

创建自定义异常

  1. 继承 Exception 或其子类:为了保持与 Java 异常处理机制的一致性,通常自定义异常类会继承自 Exception 或者 RuntimeException

  2. 提供构造函数:通常至少需要两个构造函数,一个是无参构造函数,另一个接受一个字符串参数,用于传递错误消息。

  3. 提供必要的方法:如果需要,可以添加更多方法来提供关于异常的更多信息。

示例代码

下面是一个简单的示例,展示了如何创建一个自定义异常类:

1public class CustomException extends Exception {
2
3    private int errorCode;
4
5    // 无参构造函数
6    public CustomException() {
7        super();
8    }
9
10    // 带错误消息和错误码的构造函数
11    public CustomException(String message, int errorCode) {
12        super(message);
13        this.errorCode = errorCode;
14    }
15
16    // 获取错误码
17    public int getErrorCode() {
18        return errorCode;
19    }
20}

使用自定义异常

一旦创建了自定义异常类,就可以在程序中抛出和捕获这个异常。下面是一个简单的示例,展示了如何抛出和捕获自定义异常:

1public class Main {
2
3    public static void main(String[] args) {
4        try {
5            throwCustomException();
6        } catch (CustomException e) {
7            System.out.println("Caught CustomException: " + e.getMessage());
8            System.out.println("Error Code: " + e.getErrorCode());
9        }
10    }
11
12    public static void throwCustomException() throws CustomException {
13        // 检查某些条件,如果条件不满足,则抛出自定义异常
14        boolean condition = false;
15        if (!condition) {
16            throw new CustomException("Condition not met", 404);
17        }
18    }
19}

总结

自定义异常可以帮助你更好地控制程序的错误处理流程,并提供更详细的错误信息。在创建自定义异常时,通常会继承自 ExceptionRuntimeException,并提供必要的构造函数和其他方法。

 返回值不同的重载

在 Java 中,方法的重载(overloading)是指在一个类中可以有多个同名的方法,只要它们的参数列表不同即可。这里所说的“参数列表不同”通常是指参数的数量、类型或顺序不同。然而,仅仅返回类型不同不足以构成方法的重载。

方法重载的规则

  1. 方法名称必须相同
  2. 参数列表必须不同(数量、类型或顺序)。
  3. 返回类型可以不同,但这不是区分重载方法的标准。
示例代码

下面是一个简单的例子,展示了如何在 Java 中实现方法的重载:

1public class OverloadExample {
2
3    // 无参数,返回 int
4    public int noParams() {
5        return 42;
6    }
7
8    // 有一个 int 参数,返回 String
9    public String withIntParam(int x) {
10        return "Integer value: " + x;
11    }
12
13    // 有一个 String 参数,返回 double
14    public double withStringParam(String s) {
15        return s.length();
16    }
17
18    public static void main(String[] args) {
19        OverloadExample example = new OverloadExample();
20
21        // 调用不同重载的方法
22        int intValue = example.noParams();
23        String stringValue = example.withIntParam(10);
24        double doubleValue = example.withStringParam("Hello, World!");
25
26        System.out.println("Int Value: " + intValue);
27        System.out.println("String Value: " + stringValue);
28        System.out.println("Double Value: " + doubleValue);
29    }
30}

总结

在 Java 中,方法的重载是通过参数列表的不同来实现的。返回类型不同不会导致方法重载,因为 Java 编译器是根据方法签名(方法名加上参数列表)来决定调用哪个方法的。

redis三大问题,穿透,击穿,雪崩

Redis 在作为缓存系统使用时可能会遇到三种常见的问题:缓存穿透、缓存击穿和缓存雪崩。这些问题都与缓存的有效性和一致性有关,下面是对这三种问题的详细解释及应对策略:

1. 缓存穿透

定义:缓存穿透是指查询一个一定不存在的数据,由于缓存和数据库中都没有该数据,因此每次都直接走到数据库去查询,增加了数据库的压力。

示例:假设用户请求一个不存在的商品 ID,如果缓存中没有该商品 ID 的记录,那么每次请求都会直接到达数据库查询,即使数据库中也没有这条记录。

解决方法

  • 缓存空对象:对于查询不到的数据,可以在缓存中设置一个空对象,并设置一个较短的过期时间,这样可以防止对该数据的重复查询。
  • 布隆过滤器:使用布隆过滤器预先过滤掉一定不存在的数据,减少不必要的数据库查询。

2. 缓存击穿

定义:缓存击穿是指某个热点数据在缓存中失效的时候,大量并发请求同时访问数据库,造成数据库压力瞬间增大。

示例:如果一个非常热门的商品突然从缓存中消失(例如缓存过期),那么在接下来的一段时间内,所有的请求都会直接打到数据库上,造成数据库压力骤增。

解决方法

  • 加锁机制:当缓存失效后,第一个请求可以尝试获取一个分布式锁,成功获取锁后去加载数据并写入缓存,其他请求则等待锁释放。
  • 缓存预热:在应用启动时或定时更新热点数据,确保热点数据始终存在于缓存中。
  • 互斥锁:利用 Redis 的 SETNX 或 SET 命令加上 NX 参数来实现互斥锁,确保只有一个线程能进入数据库更新数据。

3. 缓存雪崩

定义:缓存雪崩是指在一段时间内,大量的缓存数据同时失效,导致大量的并发请求直接打到数据库上,使得数据库压力骤增。

示例:如果大量数据的缓存过期时间设置相同,那么在过期时,所有这些数据的请求会同时打到数据库上,导致数据库负载过高。

解决方法

  • 分散缓存过期时间:不要让缓存集中过期,可以采用随机数等方式为每个缓存设置不同的过期时间。
  • 限流:在数据库请求过多时,可以采用限流的方式,暂时拒绝部分请求。
  • 二级缓存:在主缓存失效的情况下,可以使用二级缓存来缓解压力,比如使用内存中的 Map 结构作为临时缓存。
  • 异步更新缓存:在更新数据时,可以采用异步方式更新缓存,避免数据库请求和缓存更新请求同时发生。

通过上述方法可以有效地减轻或避免这些缓存问题带来的影响。

对于ioc容器

IoC 容器(Inversion of Control Container)是 Spring 框架的核心特性之一,它负责管理应用程序中的对象创建、依赖注入和生命周期管理等任务。下面是对 IoC 容器的基本概念、特点和使用方法的概述。

IoC 容器基本概念

  • 控制反转(Inversion of Control, IoC):是一种设计原则,用来降低组件间的耦合度。在传统的编程模式中,组件自己负责创建依赖的对象。而在 IoC 中,这种控制权被“反转”给了容器,由容器来负责实例化、配置和管理组件之间的依赖关系。
  • 依赖注入(Dependency Injection, DI):是 IoC 的一种实现形式,它通过外部容器将依赖注入到组件中,而不是由组件自己创建或查找依赖对象。DI 主要有三种方式:构造器注入、setter 注入和基于接口的注入。

IoC 容器的特点

  • 对象管理:IoC 容器负责管理应用程序中的对象,包括对象的创建、销毁和生命周期管理。
  • 依赖注入:IoC 容器自动处理对象之间的依赖关系,使得对象之间更加解耦。
  • 配置灵活性:通过 XML 或注解配置文件来定义对象及其依赖关系,可以方便地调整和扩展应用结构。
  • 可测试性:由于依赖注入,使得单元测试更加容易实现,可以轻松地模拟(mock)外部依赖。

IoC 容器的使用

  1. 定义 Bean:Bean 是 IoC 容器管理的对象。在 Spring 中,可以通过 XML 配置文件或注解来定义 Bean。
  2. 配置依赖:定义 Bean 之间的依赖关系。这可以通过 XML 文件中的 <bean> 标签的 ref 属性来完成,或者使用注解如 @Autowired 和 @Resource
  3. 创建 IoC 容器:通过 ApplicationContext 接口来创建 IoC 容器。常用的实现类包括 ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext,它们分别用于读取类路径下的 XML 配置文件和文件系统的 XML 配置文件。
  4. 获取 Bean:使用 getBean 方法从 IoC 容器中获取 Bean 实例。

示例

下面是一个简单的示例,展示了如何使用 XML 配置文件定义 Bean 并注入依赖:

  1. XML 配置文件(applicationContext.xml):

    1<?xml version="1.0" encoding="UTF-8"?>
    2<beans xmlns="http://www.springframework.org/schema/beans"
    3       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    5    
    6    <!-- 定义 Service Bean -->
    7    <bean id="serviceBean" class="com.example.ServiceBean">
    8        <!-- 注入依赖 -->
    9        <property name="daoBean" ref="daoBean"/>
    10    </bean>
    11    
    12    <!-- 定义 DAO Bean -->
    13    <bean id="daoBean" class="com.example.DaoBean"/>
    14</beans>
  2. 创建 IoC 容器并获取 Bean:

    1import org.springframework.context.ApplicationContext;
    2import org.springframework.context.support.ClassPathXmlApplicationContext;
    3
    4public class App {
    5    public static void main(String[] args) {
    6        // 创建 IoC 容器
    7        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    8        
    9        // 获取 Bean
    10        ServiceBean serviceBean = (ServiceBean) context.getBean("serviceBean");
    11        serviceBean.doSomething();
    12    }
    13}

在这个示例中,我们定义了两个 Bean,一个是 ServiceBean,另一个是 DaoBean,并且在 ServiceBean 中注入了 DaoBean 作为其依赖。通过 IoC 容器,我们可以轻松地管理这两个 Bean 的创建和依赖注入过程。

在 Java 中,创建线程

在 Java 中,创建线程主要有以下几种方法:

  1. 继承 Thread

    • 创建一个新的类继承 Thread 类,并重写 run() 方法。
    • 创建该类的实例,并调用 start() 方法来启动线程。

    示例

    1class MyThread extends Thread {
    2    @Override
    3    public void run() {
    4        System.out.println("线程 " + this.getName() + " 正在运行...");
    5    }
    6}
    7
    8public class Main {
    9    public static void main(String[] args) {
    10        MyThread thread = new MyThread();
    11        thread.start();
    12    }
    13}
  2. 实现 Runnable 接口

    • 创建一个实现了 Runnable 接口的新类,并实现 run() 方法。
    • 创建该类的实例,并将其传递给 Thread 类的构造函数,然后调用 start() 方法来启动线程。

    示例

    1class MyRunnable implements Runnable {
    2    @Override
    3    public void run() {
    4        System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行...");
    5    }
    6}
    7
    8public class Main {
    9    public static void main(String[] args) {
    10        Thread thread = new Thread(new MyRunnable());
    11        thread.start();
    12    }
    13}
  3. 实现 Callable 接口并使用 FutureTask

    • 创建一个实现了 Callable 接口的新类,并实现 call() 方法。
    • 使用 Callable 实例创建 FutureTask 对象。
    • 创建 Thread 类的实例,并将 FutureTask 作为参数传入,然后调用 start() 方法来启动线程。
    • 通过 FutureTask 的 get() 方法获取线程执行的结果。

    示例

    1import java.util.concurrent.Callable;
    2import java.util.concurrent.FutureTask;
    3
    4class MyCallable implements Callable<String> {
    5    @Override
    6    public String call() {
    7        return "线程 " + Thread.currentThread().getName() + " 的结果";
    8    }
    9}
    10
    11public class Main {
    12    public static void main(String[] args) {
    13        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    14        Thread thread = new Thread(futureTask);
    15        thread.start();
    16
    17        try {
    18            System.out.println(futureTask.get());
    19        } catch (Exception e) {
    20            e.printStackTrace();
    21        }
    22    }
    23}
  4. 使用 ExecutorServiceThreadPoolExecutor

    • 使用 Executors 工具类创建 ExecutorService 实例。
    • 使用 ExecutorService 的 submit() 方法提交任务。
    • 使用 shutdown() 方法停止线程池。

    示例

    1import java.util.concurrent.ExecutorService;
    2import java.util.concurrent.Executors;
    3import java.util.concurrent.Future;
    4
    5public class Main {
    6    public static void main(String[] args) {
    7        ExecutorService executor = Executors.newSingleThreadExecutor();
    8
    9        Future<String> future = executor.submit(() -> {
    10            return "线程 " + Thread.currentThread().getName() + " 的结果";
    11        });
    12
    13        executor.shutdown();
    14
    15        try {
    16            System.out.println(future.get());
    17        } catch (Exception e) {
    18            e.printStackTrace();
    19        }
    20    }
    21}

这些方法都可以用来创建和启动线程。每种方法都有其适用场景,选择哪种方法取决于您的具体需求。例如,如果您需要从线程获取返回值,则应使用 CallableFutureTask;如果您需要管理一组线程,则 ExecutorService 是一个更好的选择。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值