文章

Java面向对象基础

Java面向对象基础

面向对象特性

面向对象三大基本特征

面向对象的三个基本特征是:封装、继承、多态

  1. 封装

封装最好理解了。封装是面向对象的特征之一,是对象和类概念的主要特性。封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  1. 继承

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为 “ 子类 “ 或 “ 派生类 “,被继承的类称为 “ 基类 “、” 父类 “ 或 “ 超类 “。

要实现继承,可以通过 “ 继承 “(Inheritance)和 “ 组合 “(Composition)来实现。

  1. 多态 polymorphisn

多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。 实现多态,有两种方式,覆盖和重载。覆盖和重载的区别在于,覆盖在运行时决定,重载是在编译时决定。并且覆盖和重载的机制不同,例如在 Java 中,重载方法的签名必须不同于原先方法的,但对于覆盖签名必须相同。

多态的底层实现?

Java 为什么不支持多继承?

Java 不支持多继承,C++ 支持多继承。多继承存在菱形继承问题:
5pp1h
JDK8.0 后,接口有了 default 默认方法实现,接口是支持多继承的,如果有了 default 方法,那么如果出现两个接口定义了相同的 default 方法,那么就可以认为支持了多继承?

JDK8.0 后,多个接口中有相同的 default 方法,实现时是需要重写该方法的,否则编译出错。

类 Class

属性和方法

静态属性和静态方法是否可以被继承?是否可以被重写?为什么?

Java 中静态属性和静态方法可以被继承,但是不可以被重写而是被隐藏。
原因

  1. 静态方法和静态属性是属于类的,调用的时候直接通过 类名.静态属性/静态方法 来完成的,不需要继承机制即可以完成;父类的静态属性和静态方法通过 父类名.静态属性/静态方法
  2. 多态能够被实现依赖于继承、接口和重写、重载,有了继承和重写就可以实现父类的引用指向不同子类的对象;重写的功能是:重写后的子类优先级要高于父类的优先级
  3. 静态属性、静态方法和非静态的属性都可以被继承和隐藏,而不能被重写,因此不能实现多态;非静态方法可以实现继承和重写,因此可以实现多态

内部类

静态内部类和非静态内部类区别?

  1. 静态内部类不依赖于外部类实例而被实例化;但非静态内部类需要在外部类实例化后才可以被实例化
  2. 静态内部类不需要持有外部类的引用;但非静态内部类需要持有对外部类的引用
  3. 静态内部类不能访问外部类的非静态成员变量和非静态方法。他只能访问外部类的静态成员和静态方法,非静态内部类能够访问外部类的静态和非静态成员和方法

接口和抽象类的区别?

共同点

  1. 上层的抽象层
  2. 都不能被实例化
  3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不提供具体的实现

不同点

  1. 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性;而接口只有有抽象的方法
  2. 多继承:一个类只能继承一个直接父类,这个可以是具体的类也可以是抽象类;但一个类可以实现多个接口
  3. 抽象类可以有默认的方法实现;而接口不存在方法的实现,Java8 后的 default 关键字可声明
  4. 子类实现 extends 继承抽象类;子类用 implements 来实现接口
  5. 构造函数:抽象类可以有构造函数;接口不能有构造函数
  6. 访问修饰符:抽象方法可以有 public、protected 和 default 修饰符;而接口方法默认且只能是 public

对象

Java 对象的创建过程?

  1. 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

  1. 分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存 大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “ 指针碰撞 “ 和 “ 空闲列表 “ 两种,选择那种分配方式由 Java 堆是 否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 选择以上 2 种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是 “ 标记 - 清除 “,还是 “ 标记 - 整理 “(也称作 “ 标记 - 压缩 “),值得注意的是,复制算法内存也是规整的。

  1. 初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

  1. 设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

  1. 执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生 了,但从 Java 程序的视角来看,对象创建才刚开始,init 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

Java 代码 new 一个对象后,JVM 是怎么给它们分配内存的呢?

分配原则:一般来讲,new 一个对象后,内存一般分配在堆空间中,但也有一些例外。有些对象会分配在栈上或者 TLAB 中。如果可以在栈上分配,就直接在栈上分配,不行就会进行 TLAB 分配,再不行就判断是否是大对象,大对象直接进入老年代,再不行就分配到 eden 区,eden 若是空间不够,就会进行一次 MinorGC。
大对象
大对象,就是需要大量的连续内存空间,JVM 会让这种对象直接进入老年代,减少 eden 和两个 survivor 区发生大量的内存复制,提高效率。
栈上分配
在 JVM 中堆是线程共享的,也就是说堆里存的东西对于所有的线程都是可见的,可访问的,虚拟机中的垃圾回收才可以回收堆中的没有被引用的对象。但是,若是有对象的作用域不会逃离方法之外,那么,这个对象就可以分配在栈中。随着方法的结束而销毁,无需回收。这就是栈上分配
TLAB 分配
本地线程分配缓冲 (Thread Local Allocation Buffer 即 TLAB,为每⼀个线程预先分配⼀块内存,JVM 在给线程中的对象分配内存时,⾸先在 TLAB 分配。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。为了保证同一快内存的线程安全 JVM 有两种方式
一:CAS 比较和交换(Compare And Swap): CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。
二:TLAB 但是很多线程同时申请内存时。CAS 效率就会变得低下,所以,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤上述的 CAS 进⾏内存分配。
在给对象分配内存时,每个线程使用自己的 TLAB,这样可以避免线程同步,提高了对象分配的效率。 TLAB 本身占用 eEden 区空间,在开启 TLAB 的情况下,虚拟机会为每个 Java 线程分配一块 TLAB 空间。TLAB 空间的内存非常小, 缺省情况下仅占有整个 Eden 空间的 1%, 由于 TLAB 空间一般不会很大,因此大对象无法在 TLAB 上进行分配,总是会直接分配在堆上。TLAB 空间由于比较小,因此很容易装满。
比如,一个 100K 的空间,已经使用了 80KB, 当需要再分配一个 30KB 的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前 TLAB,这样就会浪费 20KB 空间;第二,将这 30KB 的对象直接分配在堆上,保留当前的 TLAB。

对象初始化流程:对象加载的过程,属性先加载还是方法先加载

class 文件加载完毕,以及为各成员方法区开辟好内存空间后,初始化步骤:

  1. 基类静态代码块,基类静态成员字段(并列优先级,按代码中出现先后顺序执行)
  2. 子类静态代码块,子类静态成员子弹(并列优先级,按代码中出现先后顺序执行)
  3. 基类普通代码块,基类普通成员字段(并列优先级,按代码中出现先后顺序执行)
  4. 基类构造方法
  5. 子类普通代码块,子类普通成员(并列优先级,按代码中出现先后顺序执行)
  6. 子类构造函数

Java 中的 main 方法可以继承吗?

main 方法能重载么?

可以。除了 JVM 规定的作为应用程序入口的 main 方法之外,其他的 main 方法都是比较普通的方法。

main 方法能被其他方法调用么?

即使是作为应用程序入口的 main 方法,也是可以被其他方法调用的,但要注意程序的关闭方式,别陷入死循环了。

main 方法可以继承么?

main 方法也是可以继承的;子类定义自己的 main 方法,会覆盖掉父类中的实现;所以除了 main 方法作为应用程序的入口比较特殊外,其他情况下与正常的静态方法是没什么区别的。

面向对象六大原则 SOLID

  • 单一职责原则 S: Single Responsibility Principle(SRP)

每个模块或类都应该对软件提供的功能的一部分负责,而这个责任应该完全由类来封装。它的所有服务都应严格遵守这一职责。

一个类应该只负责一项职责

  • 开闭原则 O: Opened Closed Principle(OCP)

软件中的对象 (类、模块、函数等) 对扩展是开放的,对修改是封闭的。

对扩展开放,对修改关闭

  • 里氏替换原则 L: Liscov Substitution Principle(LSP)

所有使用基类的地方必须能透明地使用其子类的对象

  • 接口隔离原则 I: Interface Segregation Principle(ISP)

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上

  • 依赖倒转原则 D: Dependency Inversion Principle(DIP)

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象

  • 迪米特原则 Law of Demeter, LoD

最少知识原则 (Principle of Least Knowledge): 一个对象应该对其他对象保持最少的了解

四大引用

cn6mr
强引用

1
String tag = new String("T");  

强引用指向的对象宁愿抛出 OOM 也不会被 GC 回收;强引用只有置空才会被 GC 回收
软引用 SoftReference
SoftReference 所指向的对象,当没有强引用指向它时,会在内存中停留一段的时间,垃圾回收器会根据 JVM 内存的使用情况(内存的紧缺程度)以及 SoftReference 的 get() 方法的调用情况来决定是否对其进行回收
弱引用 WeakReference
当一个对象仅仅被 WeakReference(弱引用)指向,而没有任何其他强引用指向该对象的时候,如果这时 GC 运行,那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
在对象被回收后,会把弱引用对象,也就是 WeakReference 对象或者其子类的对象,放入队列 ReferenceQueue 中(注意不是被弱引用的对象)被弱引用的对象已经被回收了。

LeakCanary 就是利用弱引用来主动检测是否存在内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestWeakRef {
    public static void main(String[] args) {
        User u = new User(1, "King");
        ReferenceQueue<User> queue = new ReferenceQueue<>();
        WeakReference<User> userWeakRef = new WeakReference<>(u, queue);
        u = null; // 干掉了User对象的强引用,确保这个实例只有userWeakRef弱引用着,不干掉强引用,无法回收
        System.out.println("Before gc u=null, userWeakRef=" + userWeakRef);
        System.out.println("userWeakRef.get()=" + userWeakRef.get() + ", queue=" + queue.poll());
        System.gc(); // 进行一次GC垃圾回收,千万不要写在业务代码中。
        System.out.println("After gc");
        // User对象被回收后,会将弱引用对象userWeakRef放到ReferenceQueue队列中去
        System.out.println("userWeakRef.get()=" + userWeakRef.get() + ", queue=" + queue.poll()); // 没有强引用u了,只有弱引用userWeakRef,会被gc回收
    }
}
// 输出
// Before gc u=null, userWeakRef=java.lang.ref.WeakReference@123a439b
// userWeakRef.get()=User [id=1, name=King], queue=null
// After gc
// userWeakRef.get()=null, queue=java.lang.ref.WeakReference@123a439b

虚引用 PhantomReference
PhantomReference 是所有 “ 弱引用 “ 中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。
虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否 即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。

Object

Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法

Object 有哪些常用方法?大致说一下每个方法的含义

  1. clone() 保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常
  2. finalize() 在 GC 准备释放对象占用的空间前,调用该方法,在下次 GC 时真正回收对象占用的内存

JVM 保证调用 finalize 函数之前,这个对象是不可达的。但是 JVM 并不保证这个函数一定会被调用。另外,JVM 保证 finalize 函数最多运行一次。在 finalize 运行之后,该对象可能变成可达的,GC 还要再检查一次该对象是否是可达的。因此,使用 finalize 会降低 GC 的性能

  1. equals Object 中的 equals() 方法是直接用来判断两个对象指向的内存空间是不是同一块。如果是同一块内存地址,则返回 true。
  2. hashcode() 用于返回对象的 hash 值,主要用于查找的快捷性,因为 hashCode 也是在 Object 对象中就有的,所以所有 Java 对象都有 hashCode,在 HashTable 和 HashMap 这一类的散列结构中,都是通过 hashCode 来查找在散列表中的位置的。
  3. wait()、wait(long timeout)、wait(long timeout,int naos)

让当前线程进入等待状态,同时,wait() 也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒并进入 “ 就绪状态 “。

  1. notify()、notifyAll() 唤醒该对象等待的某个 (所有) 线程
  2. getClass() 返回 Object 运行时的类,不可重写
  3. toString() 返回当前对象的信息的字符串(默认返回的是当前对象的类名 +hashCode 的 16 进制数字);子类一般需要重写它
1
2
3
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

equals 和 hashCode()

== 和 equals()

关于== 地址比较
== 运算符是判断两个对象是不是同一个对象,即他们的地址是否相等

  • 基本类型:比较的是值是否相同;
  • 引用类型:比较的是引用是否相同;

关于 equals() 值比较
默认情况下也就是从超类 Object 继承⽽来的 equals ⽅法与 == 是完全等价的,⽐较的都是对象的内存地址 ,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。

两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

不对。hashCode() 并不是完全可靠,有时候不同的对象他们⽣成的 hashcode 也会⼀样(这是⽣成 hash 值的公式可能存在的问题),所以 hashCode() 只能说是⼤部分时候可靠,并不是绝对可靠,所以我们可以得出:

  1. equal() 相等的两个对象他们的 hashCode() 肯定相等,也就是⽤ equal() 对⽐是绝对可靠的
  2. hashCode() 相等的两个对象他们的 equal() 不⼀定相等,也就是 hashCode() 不是绝对可靠的
1
2
3
4
String str1 = "通话";
String str2 = "重地";
System.out.println("str1.hashcode=" + str1.hashCode() + ",str2.hashcode=" + str2.hashCode()); // str1.hashcode=1179395,str2.hashcode=1179395
System.out.println("str1.equals(str2):" + str1.equals(str2)); // str1.equals(str2):false

如果两个对象 equals,那么它们的 hashCode 必然相等,但是 hashCode 相等,equals 不一定相等。

重写 equals 时为什么一定要重写 hashCode?

hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度,如果在重写 equals 时,不重写 hashCode,就会导致在某些场景下,例如将两个相等的自定义对象存储在 Set 集合时,就会出现程序执行的异常,为了保证程序的正常执行,所以我们就需要在重写 equals 时,也一并重写 hashCode 方法才行。

  1. HashSet 底层用的是 HashMap
  2. 对象作为 key 时,在 HashMap 进行 put 时,会先判断 key 的 hash 值,而 hash 值是通过 hashCode() 方法计算出来的;然后还会判断 equals() 方法
1
e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
  1. String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用;

equals 和 hashCode 方法区别?

一个是性能,一个是可靠性。他们之间的主要区别也基本体现在这里。
1、equals() 既然已经能实现对比的功能了,为什么还要 hashCode() 呢?hashCode() 提升性能
因为重写的 equals() 里一般比较的比较全面比较复杂,这样效率就比较低,而利用 hashCode() 进行对比,则只要生成一个 hash 值进行比较就可以了,效率很高。
2、hashCode() 既然效率这么高为什么还要 equals() 呢?equals() 提供可靠性
因为 hashCode() 并不是完全可靠,有时候不同的对象他们生成的 hashcode 也会一样(取决于 hashCode 函数,生成 hash 值得公式可能存在的问题),所以 hashCode() 只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:

  • equals() 相等的两个对象他们的 hashCode() 肯定相等,也就是用 equals() 对比是绝对可靠的
  • hashCode() 相等的两个对象他们的 equals() 不一定相等,也就是 hashCode() 不是绝对可靠的

重写 equals 的原则?

  1. 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
  2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
  3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true, 并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
  4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false, 前提是对象上 equals 比较中所用的信息没有被修改。
  5. 非空性:对于任何非空引用值 x,x.equals(null) 都应返回 false。

final、finally 和 finalize 的区别?

  1. 是 Java 的关键字,final 可以⽤来修饰类、⽅法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 修饰的变量不可以被修改,⽽ final 修饰的⽅法不可以被重写
  2. finally 则是 Java 保证重点代码⼀定要被执⾏的⼀种机制。我们可以使⽤ try-finally 或者 try-catch-finally 来进⾏类似回收资源、保证 unlock 锁等动作
  3. finalize 是基础类 java.lang.Object 的⼀个⽅法,它的设计⽬的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使⽤,并且在 JDK 9 开始被标记为 deprecated。Java 平台⽬前在逐步使⽤ java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利⽤了虚引⽤ (PhantomReference)。利⽤虚⽤和引⽤队列,我们可以保证对象被彻底销毁前做⼀些类似资源回收的⼯作,⽐如关闭⽂件描述符,它比 finalize 更加轻量、更加可靠。由于垃圾回收器(GC)的机制是自动回收,所以垃圾回收的时机具有不确定性,finallize() 也可能自始自终都不被调用。

其他

深拷贝和浅拷贝

深拷贝和浅拷贝定义及区别

浅拷贝指的是如果要拷贝 A 对象,则会重新创建一个 B 对象,并将 A 内部变量全部赋值给 B 对象
深拷贝指的是拷贝后,如果 B 对象中存在引用对象,此时更改这个引用对象不会影响到 A 对象中的引用对象,因为它两所操作的内存并不是同一块内存。而浅拷贝相反,当你操作 B 对象中的某个引用对象时,就会影响到 A 对象。对于基本类型,深拷贝和浅拷贝都是直接赋值,没什么区别
1、浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象;

  • 如果属性是基本类型,拷贝的就是基本类型的值;
  • 如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

Java 中 Object 的 clone() 方法和 Kotlin 中的 data class 的 copy() 方法都是浅拷贝(copy() 和 clone() 返回的对象的引用都会指向被拷贝对象引用)
2、深拷贝
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

深拷贝有哪些方式?

1
2
3
4
5
6
7
8
public class User {  
    private String name;  
    private Address address; 
}
public class Address {  
    private String city;  
    private String country; 
}
  1. 构造函数

通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新 new 一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void constructorCopy() {  

    Address address = new Address("杭州", "中国");  
    User user = new User("大山", address);  

    // 调用构造函数时进行深拷贝  
    User copyUser = new User(user.getName(), new Address(address.getCity(), address.getCountry()));  

    // 修改源对象的值  
    user.getAddress().setCity("深圳");  

    // 检查两个对象的值不同  
    assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());  

} 
  1. 重载 clone 方法

实现 Cloneable 接口,每个引用类型都需要实现 Cloneable 接口,实现 clonse 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Address implements Cloneable {  
    private String city;  
    private String country;  
    @Override  
    public Address clone() throws CloneNotSupportedException {  
        return (Address) super.clone();  
    }
} 
public class User implements Cloneable {  

    private String name;  
    private Address address;  
    @Override  
    public User clone() throws CloneNotSupportedException {  
        User user = (User) super.clone();  
        user.setAddress(this.address.clone());  
        return user;  
    }  
}

super.clone() 其实是浅拷贝,所以在重写 User 类的 clone() 方法时,address 对象需要调用 address.clone() 重新赋值。

  1. GSON 序列化
1
2
3
4
5
6
7
8
9
Address address = new Address("杭州", "中国");  
User user = new User("大山", address);  

// 使用Gson序列化进行深拷贝  
Gson gson = new Gson();  
User copyUser = gson.fromJson(gson.toJson(user), User.class);  

// 修改源对象的值  
user.getAddress().setCity("深圳");
  1. Apache Commons Lang 序列化
  2. Kotlin 反射实现
  3. 三方库 KotlinDeepCopy
本文由作者按照 CC BY 4.0 进行授权