这周遇到的几道面试题

Java中如何实现深浅拷贝

在 Java 中,实现浅拷贝的方法主要有以下几种:

  1. 实现 Cloneable 接口并重写 clone() 方法:

  2. 使用构造函数复制:

1. 实现 Cloneable 接口并重写 clone() 方法

这种方法最为常见,通过实现 Cloneable 接口,并重写 clone() 方法,实现浅拷贝。

示例代码:
class Address implements Cloneable {
    String city;
    String country;
​
    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
​
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
​
class Person implements Cloneable {
    String name;
    Address address;
​
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
}
​
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address address = new Address("Shanghai", "China");
        Person person1 = new Person("Tom", address);
        Person person2 = (Person) person1.clone();
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Shanghai
​
        person2.address.city = "Beijing";
​
        System.out.println(person1.address.city); // 输出:Beijing
        System.out.println(person2.address.city); // 输出:Beijing
    }
}

2. 使用构造函数复制

通过构造函数实现浅拷贝,即在构造函数中复制对象的所有字段,但引用类型字段仍指向相同的对象。

示例代码:
class Address {
    String city;
    String country;
​
    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}
​
class Person {
    String name;
    Address address;
​
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    Person(Person person) {
        this.name = person.name;
        this.address = person.address; // 浅拷贝
    }
}
​
public class Main {
    public static void main(String[] args) {
        Address address = new Address("Shanghai", "China");
        Person person1 = new Person("Tom", address);
        Person person2 = new Person(person1);
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Shanghai
​
        person2.address.city = "Beijing";
​
        System.out.println(person1.address.city); // 输出:Beijing
        System.out.println(person2.address.city); // 输出:Beijing
    }
}

在 Java 中,实现深拷贝的方法主要有以下几种:

  1. 使用序列化和反序列化:

  2. 通过手动复制对象的所有字段:

  3. 使用第三方库,如 Apache Commons Lang 的 SerializationUtils

1. 使用序列化和反序列化

这是最常见的深拷贝方式,通过将对象写入字节流然后再读出来,从而创建一个全新的对象。

示例代码:
import java.io.*;
​
class Address implements Serializable {
    String city;
    String country;
​
    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}
​
class Person implements Serializable {
    String name;
    Address address;
​
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    public Person deepCopy() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteOut);
        out.writeObject(this);
​
        ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
        ObjectInputStream in = new ObjectInputStream(byteIn);
        return (Person) in.readObject();
    }
}
​
public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Address address = new Address("Shanghai", "China");
        Person person1 = new Person("Tom", address);
        Person person2 = person1.deepCopy();
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Shanghai
​
        person2.address.city = "Beijing";
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Beijing
    }
}

2. 通过手动复制对象的所有字段

这种方法需要手动复制每个字段,特别是引用类型字段需要递归地进行深拷贝。

示例代码:
class Address implements Cloneable {
    String city;
    String country;
​
    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
​
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return new Address(this.city, this.country);
    }
}
​
class Person implements Cloneable {
    String name;
    Address address;
​
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
​
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = (Address) this.address.clone();
        return cloned;
    }
}
​
public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address address = new Address("Shanghai", "China");
        Person person1 = new Person("Tom", address);
        Person person2 = (Person) person1.clone();
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Shanghai
​
        person2.address.city = "Beijing";
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Beijing
    }
}

3. 使用第三方库,如 Apache Commons Lang 的 SerializationUtils

这种方式使用第三方库提供的工具类来实现深拷贝,代码简洁但需要引入依赖。

示例代码:
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
​
class Address implements Serializable {
    String city;
    String country;
​
    Address(String city, String country) {
        this.city = city;
        this.country = country;
    }
}
​
class Person implements Serializable {
    String name;
    Address address;
​
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}
​
public class Main {
    public static void main(String[] args) {
        Address address = new Address("Shanghai", "China");
        Person person1 = new Person("Tom", address);
        Person person2 = SerializationUtils.clone(person1);
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Shanghai
​
        person2.address.city = "Beijing";
​
        System.out.println(person1.address.city); // 输出:Shanghai
        System.out.println(person2.address.city); // 输出:Beijing
    }
}

总结:

  • 使用序列化和反序列化:通过将对象写入字节流再读出来,创建全新的对象,适用于对象结构较复杂的情况。

  • 通过手动复制对象的所有字段:需要手动实现每个字段的复制,适用于对象结构较简单且明确的情况。

  • 使用第三方库:使用现有库提供的工具类,如 Apache Commons Lang 的 SerializationUtils,实现简单但需要引入依赖。

什么是CAS,什么是AQS

在并发编程中,CAS(Compare-And-Swap)和 AQS(AbstractQueuedSynchronizer)是两个非常重要的概念,它们在多线程环境中起到了关键作用。下面对这两个概念进行详细解释。

CAS(Compare-And-Swap)

概念

CAS 是一种硬件支持的原子操作,通常用于实现无锁算法。它提供了一种原子性的操作,允许一个线程将变量的当前值与预期值进行比较,如果匹配则将变量的值更新为新值。整个过程是原子的,不会被其他线程打断。

工作原理

CAS 操作需要三个操作数:

  • 内存位置(V)

  • 预期值(A)

  • 新值(B)

执行CAS操作时,如果内存位置V的值等于预期值A,则将内存位置的值更新为新值B,否则什么也不做。

public class CASExample {
    private volatile int value;
​
    public int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    }
}
优点
  • 无需加锁,避免了线程阻塞和上下文切换,提高了性能。

  • 适用于实现高效的并发数据结构,如无锁队列、无锁栈等。

缺点
  • 可能会出现ABA问题,即一个变量在两次读取之间,其值可能发生了变化然后又变回原值。可以通过版本号或 AtomicStampedReference 来解决。

  • 对于复杂的操作(如多变量的操作),CAS 可能不适用,因为它只能保证单个变量的原子性。

AQS(AbstractQueuedSynchronizer)

概念

AQS 是 Java 并发包(java.util.concurrent)中的一个基础框架,用于构建锁和同步器。它通过一个 FIFO 队列来管理获取锁和释放锁的线程,并利用内部的一个原子性状态变量来表示同步状态。

工作原理

AQS 通过维护一个同步状态(一个整数)和一个等待队列来实现锁的获取和释放。主要包括两个操作模式:

  • 独占模式(Exclusive Mode):一个线程独占资源,如 ReentrantLock

  • 共享模式(Shared Mode):多个线程可以共享资源,如 CountDownLatchSemaphore

AQS 提供了 acquirerelease 方法来获取和释放锁,具体实现需要重写以下方法:

  • tryAcquire(int arg):尝试以独占模式获取资源。

  • tryRelease(int arg):尝试以独占模式释放资源。

  • tryAcquireShared(int arg):尝试以共享模式获取资源。

  • tryReleaseShared(int arg):尝试以共享模式释放资源。

  • isHeldExclusively():当前同步器是否被独占。

示例代码
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
​
public class MyLock {
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
​
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
​
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
​
    private final Sync sync = new Sync();
​
    public void lock() {
        sync.acquire(1);
    }
​
    public void unlock() {
        sync.release(1);
    }
}
优点
  • AQS 提供了一个通用的框架,可以方便地构建不同类型的同步器。

  • 通过内部队列管理线程,能够有效处理并发访问。

缺点
  • AQS 的实现和使用较为复杂,需要理解其内部工作机制。

  • 对于简单的同步需求,直接使用 ReentrantLockSemaphore 可能更简单。

总结

  • CAS(Compare-And-Swap) 是一种无锁操作,通过比较和交换来保证原子性,适用于实现高效的并发数据结构。

  • AQS(AbstractQueuedSynchronizer) 是一个用于构建锁和同步器的基础框架,通过维护同步状态和等待队列来管理线程的访问。

CAS 和 AQS 各有其应用场景和优缺点,在实际开发中需要根据具体需求选择合适的并发控制手段。

简述JDK1.8中HashMap的原理,什么是扩容,链化和树化,分别发生在什么场景?

JDK1.8中HashMap的原理是使用数组加链表/红黑树的方式实现。扩容发生在当HashMap中的元素数量超过负载因子(0.75)与数组长度的乘积时,会重新调整数组大小,并将原有元素重新分布到新的数组中。链化发生在当插入新元素时,如果该位置已经存在元素(也就是发生哈希冲突),则以链表的形式进行存储。树化发生在当链表长度达到一定阈值(链表长度为8,数组长度>=64)时,将链表转换为红黑树,提高查询效率,并且树还会在树上节点小于6时再次链化成为链表

什么是spring三级缓存,三级缓存解决了什么问题,可能导致哪些问题

三级缓存是指用于管理 Bean 对象创建过程中不同阶段的缓存机制。

  1. 一级缓存(singletonObjects):存储已经完全初始化的单例 Bean 对象。
  2. 二级缓存(earlySingletonObjects):存储已经实例化但尚未完全初始化的单例 Bean 对象。
  3. 三级缓存(singletonFactories):存储 Bean 对象的创建工厂,用于在创建过程中检测循环依赖。

意义:

      1. 提高性能: 通过缓存已经创建的 Bean 对象,Spring 可以在后续的请求中直接返回缓存的对象,避免重复创建,从而提高了系统的性能和响应速度。
      2. 解决循环依赖: 三级缓存中的三级缓存(singletonFactories)用于解决循环依赖问题。当 A Bean 依赖于 B Bean,而 B Bean 又依赖于 A Bean 时,Spring 可以在创建 A Bean 的过程中将其提前放入三级缓存,以解决循环依赖的问题。
      3. 实现懒加载: 二级缓存(earlySingletonObjects)可以实现 Bean 的懒加载,即在 Bean 第一次被请求时才进行初始化,而不是在容器启动时就立即创建所有的 Bean 对象。
      4. 保证单例: 通过一级缓存(singletonObjects)中存储的已初始化的单例 Bean 对象,Spring 可以保证在应用程序中只存在一个实例,实现了单例模式的效果。

从 Spring 的缓存相关模块(如 Spring Cache)的角度来看,也存在一些可能的问题以及解决方法。

缓存数据不一致性问题:

  1. 问题:当使用 Spring Cache 缓存方法的返回结果时,如果在缓存数据过期前发生了数据变更(如数据库更新),则缓存中的数据与实际数据不一致。

  1. 解决方法:

手动清除缓存:在数据变更操作后,手动清除相应的缓存,保持缓存与数据库数据的一致性。

使用缓存刷新策略:在 Spring Cache 中,可以通过配置缓存刷新策略,定期或在特定触发条件下刷新缓存,使缓存中的数据保持最新。

缓存击穿问题:

1.问题:当某个缓存项过期时,同时有大量并发请求访问该缓存项,可能导致大量请求直接访问底层数据源(如数据库),增加系统负载。

2.解决方法:

  1. 设置合适的缓存过期时间:根据业务场景和系统负载情况,设置合适的缓存过期时间,避免大量缓存同时失效。
  2. 使用互斥锁机制:在缓存项失效时,使用互斥锁机制保证只有一个线程能够重新加载缓存项,其他线程等待该线程加载完数据后再从缓存中获取。

缓存雪崩问题:

1.问题:当大量缓存项同时失效时,可能导致大量请求直接访问底层数据源,从而造成系统压力过大。

2.解决方法:

  1. 使用分布式缓存:如果系统是分布式的,考虑使用分布式缓存技术,将缓存分布在多个节点上,避免单点故障。
  2. 设置随机过期时间:在设置缓存过期时间时,可以稍微随机一些,避免大量缓存同时失效。
  3. 使用熔断机制:当系统压力过大时,可以使用熔断机制暂时关闭对底层数据源的访问,避免系统崩溃。

SpringMVC中过滤器与拦截器的区别

1. 拦截器是基于java的反射机制,而过滤器是基于函数回调的。

2. 过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

  1. 因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务。

过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。

JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的?

1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。

解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。

2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。

解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。

3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。

解决: Mybatis自动将java对象映射至sql语句。

4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值