Java中如何实现深浅拷贝
在 Java 中,实现浅拷贝的方法主要有以下几种:
-
实现
Cloneable
接口并重写clone()
方法: -
使用构造函数复制:
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 中,实现深拷贝的方法主要有以下几种:
-
使用序列化和反序列化:
-
通过手动复制对象的所有字段:
-
使用第三方库,如 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):多个线程可以共享资源,如
CountDownLatch
和Semaphore
。
AQS 提供了 acquire
和 release
方法来获取和释放锁,具体实现需要重写以下方法:
-
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 的实现和使用较为复杂,需要理解其内部工作机制。
-
对于简单的同步需求,直接使用
ReentrantLock
或Semaphore
可能更简单。
总结
-
CAS(Compare-And-Swap) 是一种无锁操作,通过比较和交换来保证原子性,适用于实现高效的并发数据结构。
-
AQS(AbstractQueuedSynchronizer) 是一个用于构建锁和同步器的基础框架,通过维护同步状态和等待队列来管理线程的访问。
CAS 和 AQS 各有其应用场景和优缺点,在实际开发中需要根据具体需求选择合适的并发控制手段。
简述JDK1.8中HashMap的原理,什么是扩容,链化和树化,分别发生在什么场景?
JDK1.8中HashMap的原理是使用数组加链表/红黑树的方式实现。扩容发生在当HashMap中的元素数量超过负载因子(0.75)与数组长度的乘积时,会重新调整数组大小,并将原有元素重新分布到新的数组中。链化发生在当插入新元素时,如果该位置已经存在元素(也就是发生哈希冲突),则以链表的形式进行存储。树化发生在当链表长度达到一定阈值(链表长度为8,数组长度>=64)时,将链表转换为红黑树,提高查询效率,并且树还会在树上节点小于6时再次链化成为链表
什么是spring三级缓存,三级缓存解决了什么问题,可能导致哪些问题
三级缓存是指用于管理 Bean 对象创建过程中不同阶段的缓存机制。
- 一级缓存(singletonObjects):存储已经完全初始化的单例 Bean 对象。
- 二级缓存(earlySingletonObjects):存储已经实例化但尚未完全初始化的单例 Bean 对象。
- 三级缓存(singletonFactories):存储 Bean 对象的创建工厂,用于在创建过程中检测循环依赖。
意义:
-
-
- 提高性能: 通过缓存已经创建的 Bean 对象,Spring 可以在后续的请求中直接返回缓存的对象,避免重复创建,从而提高了系统的性能和响应速度。
- 解决循环依赖: 三级缓存中的三级缓存(singletonFactories)用于解决循环依赖问题。当 A Bean 依赖于 B Bean,而 B Bean 又依赖于 A Bean 时,Spring 可以在创建 A Bean 的过程中将其提前放入三级缓存,以解决循环依赖的问题。
- 实现懒加载: 二级缓存(earlySingletonObjects)可以实现 Bean 的懒加载,即在 Bean 第一次被请求时才进行初始化,而不是在容器启动时就立即创建所有的 Bean 对象。
- 保证单例: 通过一级缓存(singletonObjects)中存储的已初始化的单例 Bean 对象,Spring 可以保证在应用程序中只存在一个实例,实现了单例模式的效果。
-
从 Spring 的缓存相关模块(如 Spring Cache)的角度来看,也存在一些可能的问题以及解决方法。
缓存数据不一致性问题:
- 问题:当使用 Spring Cache 缓存方法的返回结果时,如果在缓存数据过期前发生了数据变更(如数据库更新),则缓存中的数据与实际数据不一致。
- 解决方法:
手动清除缓存:在数据变更操作后,手动清除相应的缓存,保持缓存与数据库数据的一致性。
使用缓存刷新策略:在 Spring Cache 中,可以通过配置缓存刷新策略,定期或在特定触发条件下刷新缓存,使缓存中的数据保持最新。
缓存击穿问题:
1.问题:当某个缓存项过期时,同时有大量并发请求访问该缓存项,可能导致大量请求直接访问底层数据源(如数据库),增加系统负载。
2.解决方法:
- 设置合适的缓存过期时间:根据业务场景和系统负载情况,设置合适的缓存过期时间,避免大量缓存同时失效。
- 使用互斥锁机制:在缓存项失效时,使用互斥锁机制保证只有一个线程能够重新加载缓存项,其他线程等待该线程加载完数据后再从缓存中获取。
缓存雪崩问题:
1.问题:当大量缓存项同时失效时,可能导致大量请求直接访问底层数据源,从而造成系统压力过大。
2.解决方法:
- 使用分布式缓存:如果系统是分布式的,考虑使用分布式缓存技术,将缓存分布在多个节点上,避免单点故障。
- 设置随机过期时间:在设置缓存过期时间时,可以稍微随机一些,避免大量缓存同时失效。
- 使用熔断机制:当系统压力过大时,可以使用熔断机制暂时关闭对底层数据源的访问,避免系统崩溃。
SpringMVC中过滤器与拦截器的区别
1. 拦截器是基于java的反射机制,而过滤器是基于函数回调的。
2. 过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。
拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
- 因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务。
而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。
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对象解析比较方便。