1. 访问控制权限
- 访问权限控制符不能修饰局部变量。
- 类中的属性和方法访问权限共有四种:private、缺省、protected和public。
- private:私有的,只能在本类中访问。
- 缺省:默认的,同一个包下可以访问。
- protected:受保护的,子类中可以访问。(受保护的通常就是给子孙用的。)
- public:公共的,在任何位置都可以访问。
- 类的访问权限只有两种:public 和 缺省。
2. abstract
abstract 关键字是非
访问修饰符,用于类和方法。
抽象类
::用 abstract 修饰的类
抽象方法
: 用 abstract 修饰,且方法体没有具体实现的方法就叫做抽象方法是没有实现的方法,必须由子类提供具体的实现。且只能在抽象类中使用,
2.1 抽象类
- 抽象类和具体类的区别
- 抽象类中可以,且至少应当定义一个抽象方法(就算没有,编译也能通过,但这是没有意义的),当抽象类中定义了抽象方法,则该抽象类必须被继承,所有抽象方法必须被重写,否则编译不通过
- 抽象类不能被实例化
- 抽象类唯一的作用是被继承(自然不能与 final 联用)
- 相应的,如果子类继承了抽象类,必须要实现父类所有抽象方法
abstract class Shape {
// 抽象方法,子类需要实现
abstract double area();
}
class Rectangle extends Shape {
private double width;
private double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
}
class Circle extends Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
- 抽象类唯一的目的就是被继承,且抽象类仅允许单继承,一个具体类只能继承一个抽象类。
- 抽象类作为接口和具体类的一种
过渡
。
接口中只能有抽象方法,具体类中只能有具体方法,抽象类作为一种过渡,在至少含有一个抽象方法的前提下,可以同时含有抽象与非抽象方法,为其子类提供了一些通用的实现,同时又要求子类提供特定的实现。
2.2 抽象方法
- 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
抽象方法没有方法体
:即方法的声明以分号结束,而不是包含实现代码。因此,抽象方法只是声明了方法的存在,但不提供具体的实现。
abstract double area();
访问权限
:抽象方法的访问权限可以是 private以外
的任何类型。因为抽象方法必须在子类中被实现,而私有方法无法被继承和重写。返回类型和参数列表
:子类中实现的抽象方法的返回类型和参数列表必须与父类中的抽象方法一致。
2.3 接口与抽象类如何选择
- 抽象类有构造方法,接口没有构造方法,但两者都不能实例化。
- 抽象类内部可能包含非 final 的变量,但是在接口中存在的变量一定是 final,public,static 。
2.3.1 抽象类是半抽象的,接口是完全抽象的。
抽象类中可以存在非抽象方法,因此,抽象类可以为子类提供通用的方法实现;接口中只有抽象方法,每一个实现接口的子类,都要重新实现所有抽象方法。
更准确地说:抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个抽象父类,在该抽象父类中编写公共的代码。同时,如果有一些方法无法在该类中实现,可以延迟到子类中实现。
2.3.2 接口是一个纯粹的规范
接口是比抽象类更抽象的存在,作为一个存粹的规范,使用接口后续二次开发的灵活性会更好
因为如果使用了抽象类,该抽象类中势必包含一些公共方法/属性,当扩展功能时,该功能可能并不需要这些提供好的公共方法/属性
2.3.2 开发的角度
- Java 不像 C++ 一样支持多继承,所以实际开发中只能通过实现多个接口来弥补这个局限。
- 就抽象类来说,抽象类已经固定了一定的形态,接口只固定一定交流方式。因为忽视内容的特性导致了接口在工程中维护修改中更加舒服~~
而抽象类则定型了另外一个类,就显得不这么灵活了~ - 实际开发中,接口和抽象类的选用并不是绝对的,通常来说建议优先使用接口,它是纯粹的规范集合。但如果在实现的过程中,你发现多个实现类都有相同的部分,那么可以引入抽象类,当然采用组合的方式也可以。
3. interface 接口
接口定义了一组抽象方法和常量,用来描述实现这个接口的类应该具有哪些行为和属性。
换句话说,接口是一个规范
,作用是让大家都知道这个是做什么的,但是具体不用知道具体怎么做。
- 接口和类一样,也是一种引用数据类型。
- 接口怎么定义?
[修饰符列表] interface 接口名{}
- 接口中只能定义:常量+抽象方法。
- 接口中的常量的 static final 可以省略。
- 接口中的抽象方法的 abstract 可以省略。
- 接口中所有的方法和变量都是 public 修饰的。
- 接口和接口之间可以多继承。
- 实现接口时,必须实现接口中
所有
的抽象方法,否则必须声明为抽象类,不然编译报错 - 一个类可以同时实现多个接口。
3.1 面向接口编程
- 面向接口调用的称为:接口调用者
- 面向接口实现的称为:接口实现者
调用者和实现者通过接口达到了解耦合。
也就是说调用者不需要关心具体的实现者,实现者也不需要关心具体的调用者,双方都遵循规范,面向接口进行开发。
面向接口编程实质也是一种多态
举例:
消费者通过一次性纸质菜单点菜,厨房收到菜单后进行制作
将一次性纸质菜单设计为接口,消费者设计为调用者,厨房设计为实现者。
3.2 正反面对照
例如定义一个 Usb 接口,提供 read() 和 write() 方法,通过 read() 方法读,通过 write() 方法写:
定义一个电脑类 Computer,
Usb接口的实现可以有很多,例如:打印机(Printer),硬盘(HardDrive)。他们都是调用者,面向 Usb 接口来调用。
3.2.1 当面向对象编程时:
这个程序没有使用接口。分析存在哪些缺点?
- 违背OCP开闭原则。
假设现在新增了一个设备,那么要做的工作显然是:编写实体类 --> 在电脑类中新增一个重载 conn 方法,以实现连接操作。而修改电脑类的行为,显然违反了 OCP 原则 - 程序的耦合度太高,Computer 类的扩展力差。
Computer 类中使用了 HardDrive r类,以及 Printer 类。导致三者耦合度太高。
3.2.2 当面向接口编程时
这个程序没有使用接口。分析提升了哪些方面?
- 电脑类与具体设备完全解耦。
现在电脑类方法中,定义的形参是 Usb 对象,后续可以在测试类传入任何实现了 Usb 接口的对象
;
进而,在 conn 方法中,usb.read() ,实际上是调用了传入的 Usb 对象的 read() 方法。这里涉及到 Java 中的动态绑定机制(dynamic binding)。
当你传入不同的实现了 Usb 接口的对象时,conn() 方法内部调用的 read() 方法会根据传入对象的实际类型来确定。即,在运行时,Java 虚拟机会根据对象的实际类型去调用相应的方法实现。
- 新增设备时不会再违反 OCP 原则了
假设现在新增了一个耳机,那么要做的工作显然是:
- 编写耳机类继承 Usb 接口,并实现抽象方法。这是实现者的视角。
- 电脑类不必做任何改变,因为从调用者的视角,其面向接口调用,而接口没有任何改变,电脑类自然不应当有任何改变。因此,不会违背 OCP 原则。
- 测试类中,实例化一个耳机对象,并将该对象传入 电脑类的 conn 方法中即可
public class Test {
public static void main(String[] args) {
// 多态
/*Usb usb1 = new HardDrive();
Usb usb2 = new Printer();*/
// 创建硬盘对象
HardDrive hardDrive = new HardDrive();
// 创建电脑对象
Computer computer = new Computer();
// 连接设备
computer.conn(hardDrive);
// 创建打印机对象
Printer printer = new Printer();
// 连接设备
computer.conn(printer);
//MyClass.m();
}
}
3.3 JDK 8 为接口带来的改变
一直以来的认识是:接口比抽象类更抽象,因为接口中的方法必须全部是抽象的。
但这句话从 JDK 8 开始就不合适了。
JDK 8 接口中允许添加默认方法和静态方法
,并且可以同时有多个。
3.3.1 接口中的默认方法
- 默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
- 接口中的方法名前面加个 default 关键字即为默认方法。
为什么要有默认方法?
JDK 8 之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程
缺陷是:假设我们老早就写好了一个接口,这个接口有很多实现类。一年以后,要在一个接口中添加一个抽象方法,那所有的接口实现类都要去实现这个方法,不然就会编译错误,而某些实现类根本就不需要实现这个方法也被迫要写一个空实现,改动会非常大。
所以,接口中的默认方法就是为了解决这个问题,只要在一个接口添加了一个默认方法,所有的实现类就自动继承,不需要改动任何实现类,也不会影响业务,爽歪歪。
并且更进一步,接口中的默认方法也可以被实现类重写,这样就提供了充分的灵活性。
接口中提供默认方法的潜在问题
因为接口默认方法可以被继承并重写,如果继承的多个接口都存在相同的默认方法,那就存在冲突问题。
比如我们来看下在 JDK API 中 java.util.Map 关于接口默认方法和静态方法的应用。
引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。所有实现接口的类都必须实现这些抽象方法。这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时,这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。
引入的静态方法只能使用本接口名来访问,无法使用实现类的类名访问。
所有的接口隐式的继承 Object。因此接口也可以调用 Object 类的相关方法。
3.3.2 接口中的静态方法
JDK 8 新增接口静态方法和默认方法的目的十分类似。
二者的区别只是:有些时候,我们可能希望禁止接口中的定义的默认方法被实现类重写,那么就将其定义为静态,这样实现类就无法重写。
4.内部类
- 什么是内部类?
- 定义在一个类中的类。
- 就 Java 而言,内部类的存在,是为了面向对象编程(OOP)中一个重要特性存在的:封装。 JDK 源码中 HashMap 的实现中就使用了很多内部类,比如 Node,EntrySet,KeySet。
- 什么时候使用内部类?
- 一个类用到了另外一个类,而这两个类的联系比较密切,但是如果把这两个类定义为独立的类,不但增加了类的数量,也不利于代码的阅读和维护。
- 内部类可以访问外部类的私有成员,这样可以将相关的类和接口隐藏在外部类的内部,从而提高封装性。
- 匿名内部类是指没有名字的内部类,通常用于定义一个只使用一次的类,比如在事件处理中。
- 内部类包括哪几种?
- 静态内部类:和静态变量一个级别
- 实例内部类:和实例变量一个级别
- 局部内部类:和局部变量一个级别
- 匿名内部类:特殊的局部内部类,没有名字,只能用一次。
4.1 静态内部类
- 静态内部类如何实例化
- 无法直接访问外部类中实例变量和实例方法。
4.2 实例内部类
- 实例内部类如何实例化:
OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
- 可以直接访问外部类中所有的实例变量,实例方法,静态变量,静态方法。
4.3 局部内部类
局部内部类方外类外部的局部变量时,局部变量需要被 final 修饰。
从 J8 开始,不需要手动添加 final 了,因为 JVM 会自动添加。
4.4 匿名内部类
匿名内部类通常用于实现接口或继承一个类,并且只需要创建该类的一个实例的场景。
匿名内部类一个非常重要的作用就是实例化函数式接口
在这个例子中,匿名内部类实现了 MyInterface 接口,并覆盖了 doSomething 方法。
这个匿名类的实现是直接在 new MyInterface() 之后定义的,并被赋值给了 myFunc 变量。
// 假设我们有一个函数式接口:
interface MyInterface {
void doSomething();
}
// 我们可以使用匿名内部类来创建一个 MyInterface 的实例:
public class TestFunctionnalInterface {
public static void main(String[] args) {
MyFunctionalInterface myFunc = new MyFunctionalInterface() {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
};
myFunc.doSomething(); // out: Doing something...
}
}
4.5 具体场景分析
4.5.1 当某个类除了它的外部类,不再被其他的类使用时
我们说这个内部类依附于它的外部类而存在,可能的原因有:1、不可能为其他的类使用;2、出于某种原因,不能被其他类引用,可能会引起错误。等等。
这个场景是我们使用内部类比较多的一个场景。
下面我们以一个大家熟悉的例子来说明。
在我们的企业级 Java项目开发过程中,数据库连接池是一个我们经常要用到的概念。
虽然在很多时候,我们都是用的第三方的数据库连接池,不需要我们亲自来做这个数据库连接池。
但是,作为我们 Java 内部类使用的第一个场景,这个数据库连接池是一个很好的例子。
为了简单起见,以下我们就来简单的模拟一下数据库连接池。
首先,我们定义一个接口,将数据库连接池的功能先定义出来,如下:
public interface Pool extends TimerListener
{
//初始化连接池
public boolean init();
//销毁连接池
public void destory();
//取得一个连接
public Connection getConn();
//还有一些其他的功能,这里不再列出
……
}
有了这个功能接口,我们就可以在它的基础上实现数据库连接池的部分功能了。
我们首先想到这个数据库连接池类的操作对象应该是由 Connection 对象组成的一个数组,既然是数组,我们的池在取得 Connection 的时候,就要对数组元素进行遍历,看看 Connection 对象是否已经被使用,所以数组里每一个 Connection 对象都要有一个使用标志。
我们再对连接池的功能进行分析,会发现每一个 Connection 对象还要一个上次访问时间和使用次数。
通过上面的分析,我们可以得出,连接池里的数组的元素应该是由对象组成,该对象的类可能如下:
public class PoolConn
{
private Connection conn;
private boolean isUse;
private long lastAccess;
private int useCount;
//省略 get 和 set 方法
}
我们可以看到这个类的核心就是 Connection,其他的一些属性都是Connection 的一些标志。可以说这个类只有在连接池这个类里有用,其他地方用不到。
这时候,我们就该考虑是不是可以把这个类作为一个内部类呢?
而且我们把它作为一个内部类以后,可以把它定义成一个私有类,然后将它的属性公开,这样省掉了那些无谓的get和set方法。下面我们就试试看:
public class ConnectPool implements Pool
{
//存在 Connection 的数组
private PoolConn[] poolConns;
//连接池的最小连接数
private int min;
//连接池的最大连接数
private int max;
//一个连接的最大使用次数
private int maxUseCount;
//一个连接的最大空闲时间
private long maxTimeout;
//同一时间的Connection最大使用个数
private int maxConns;
//定时器
private Timer timer;
// 初始化 PoolConn 数组
public boolean init() {}
//……省略其他方法
// 定义一个实例内部类,该内部类专门用来保存连接池的属性
private class PoolConn
{
// 连接池的一些属性
public Connection conn;
public boolean isUse;
public long lastAccess;
public int useCount;
}
}
PoolConn 类不大可能被除了 ConnectionPool 类的其他类使用到,把它作为 ConnectionPool 的私有内部类不会影响到其他类。同时,我们可以看到,使用了内部类,使得我们可以将该内部类的数据公开,ConnectionPool 类可以直接操作 PoolConn 类的数据成员,避免了因 set 和 get 方法带来的麻烦。
上面的一个例子,是使用内部类使得你的代码得到简化和方便。
还有些情况下,你可能要避免你的类被除了它的外部类以外的类使用到,这时候就不得不使用内部类来解决问题了。
4.5.2 多算法场合
其实逻辑跟上个场景差不多,某个算法可能只被当前类使用到,不如将其作为内部类封装进本类中。
4.5.3 解决一些非面向对象的语句块
这些语句块包括 if…else ,case,try catch
5. Object 类
- Object 类是所有类的父类。
所有类要么直接,要么间接,都继承了 Object
- 子类可以使用 Object 的所有方法。
- Object 类位于 java.lang 包中,编译时会自动导入。我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类。
先看以下代码:
/* E 类中明明没有方法定义,但 E 的实例却可以调用 toString
* 这是因为:如果一个类没有继承任何自定义类,那么会隐式地继承 Object 类
* 而 Object 类中已经写好了一些方法
*/
public class TestObject {
public static void main(String[] args) {
E e = new E();
e.toString();
e.hashCode();
}
}
class E { }
5.1 Object 自带的方法
由于还未涉及多线程,对于 getClass、notify 等方法暂时还不深究
//方法返回对象的哈希值
int hashCode()
// final 方法,返回 Class 类型的对象,反射来获取对象。
Class<?> getClass()
// 将对象转换为字符串返回。
String toString()
// 该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
protected void finalize()
// 比较两个对象的引用是否相同
boolean equals(Object obj)
5.1.1 hashcode
- 由于哈希值是根据对象的地址计算的,因此不同对象的返回值一般不同
- 这个方法在一些具有哈希功能的 Collection 中用到。
5.1.2 toString
- toString() 方法默认打印的是
类的正名 + @ + 哈希码值的十六进制
- 实际开发中,通常需要将对象转为字符串进行传输,所以一般子类都会重写该方法,用来输出该对象的所有属性。
- 直接输出一个对象时,默认调用 toString 方法。
// 输出的内容完全一样,都是 Animal@3b07d329
// 即上述所提:直接输出一个对象时,默认调用 toString 方法。
public class toString_ {
public static void main(String[] args) {
//创建Animal类对象
Animal animal_0 = new Animal();
//toString() 方法的返回值类型为 String 类型(也可不做接收)
String animal_0_string = animal_0.toString();
System.out.println(animal_0_string);
System.out.println(animal_0);
}
}
class Animal {}
5.1.3 equals
- 如果重写了 equals,为了避免某些问题,建议将 hashcode 也一起重写了
- 实际开发中,我们往往需要比较得更具体,比如对象的某个属性/方法(而不仅仅是对象的地址),因此,该方法也常常需要在子类中重写。
- 下面演示实际开发中重写 equals 的例子
public class Student {
public static void main(String[] args) {
//利用带参构造创建不同Animal类对象
Animal animal_0 = new Animal("猫");
Animal animal_1 = new Animal("狗");
Animal animal_2 = new Animal("猫");
//通过重写后的equals方法比较不同对象的信息是否相同
System.out.println("animal_0对象和animal_1对象信息相同吗?" + animal_0.equals(animal_1));
System.out.println("animal_0对象和animal_2对象信息相同吗?" + animal_0.equals(animal_2));
}
}
class Animal {
//成员变量
private String species;
//构造器
public Animal() {}
public Animal(String species) {
this.species = species
}
//省略 getter,setter方法
//重写 equals 方法
/**
* 第一行判断“this == o”中,== 比较运算符如果判断引用类型,判断的是二者的地址是否相同;
* 因此这里是先确定 调用 equals 方法的对象 与 传入 equals 方法的对象是否为同一个
* 如果判断为true,意思就是你拿一个对象和它自己比去了。因此直接return true,不再进行后续的比较。
* 第二行判断中,第一部分“o == null”,是判断传入的对象是否为空。
* 一个在堆空间中真正存在的对象和一个空对象 null 相比没有意义. 因此,如果传入的对象为空,return false
* 第二部分“getClass() != o.getClass())”。第一个getClass() 前面隐含了“this.”。
* 这部分判断是看传入的对象和调用该方法的对象是否是同一类对象。
* 因为一个类只对应一个字节码文件,因此同一个类的字节码文件对象类型一定相同。
* 显然,至少两个对象要是同一类才能比,比如拿一条狗和一棵树比显然不合适。
* 综合来看,第二行需要同时满足传入的对象不为空且两个对象是同一类才能比较
* 如果第一第二行两个 if 条件语句都没有把 o 对象拦下来后
* 就能确定传入的对象既不是调用方法的对象本身,也不为空,并且和调用方法的对象是同一类对象。
* 这时候来一个强制转型 “Animal animal = (Animal) o;”
* 原因是:现在的 o 对象是一个 Object 类型的对象,根据多态的弊端,父类引用不能直接使用子类对象的特有成员。
* 因此,这里要把 o 对象进行强制向下转型,以改变 o 对象的编译类型,
* 使得做接收后的新的引用变量可以调用要判断类型(此处为Animal类)的特有成员,以便进行后续的判断。
* 最后就可以开始判断了。IDEA 也很干脆,直接 return 了一个 boolean 类型。
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Animal animal = (Animal) o;
return species_name.equals(animal.species_name);
}
}
5.1.4 clone
Clone (),简单来说就是创建一个和已知对象一模一样的对象,属于创建对象的五种方法之一。
Object 类中提供的默认 clone 方法属于浅克隆
尽管日常编码过程中用的不多,但了解深拷贝和浅拷贝的原理,对于 Java 中的所谓值传递或者引用传递将会有更深的理解。
简单来说,选择深浅克隆主要取决于克隆的对象中是否有引用数据类型(String 除外)
5.1.4.1 浅克隆
- 拷贝:创建一个新对象,然后将旧对象的非静态字段复制到该对象。
如果字段类型是基本类型/ String ,那么对该字段进行复制;
如果字段是引用类型的,则只复制该字段的引用而不复制引用指向的对象(也就是只复制对象的地址)。
- 原对象与新对象是两个不同的对象。
- 拷贝出来的新对象与原对象内容、属性完全一致
- 基本类型和 String 类型(字符串常量池机制的作用)的改变不会影响到原始对象的改变。
5.1.4.2 深克隆
深克隆: 被克隆对象的所有变量都含有原来的对象相同的值,引用变量也重新复制了一份
【不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象】
某种意义上说,深克隆包含了浅克隆,因为深克隆克隆了所有东西,自然包含了只克隆基本数据类型的浅克隆。
5.1.4.3 使用 clone 方法
使用 clone 方法有两个前提
:
- 实现 Cloneable 接口,否则会抛出 CloneNotSupportedException 异常
- (第二点非必要,但强烈建议)。在实现此接口的类中,用 Public 修饰的方法重写 clone 方法。
默认的 clone 方法只能实现浅克隆,如果克隆的对象内含有引用类型,那么在后续操作中可能两个引用操作同一个数据的情况。
通过重写 clone 方法,可以实现深克隆,确保引用数据类型也得到克隆。
clone 方法有几点需要注意:
- clone 方法返回的是 Object 类型,所以必须强制转换得到克隆后的类型
- clone 方法是一个 native 方法,而 native 的效率远远高于非 native 方法
- clone 方法被 Protected 修饰,所以任何类使用 clone 方法前必须继承 Object 类,同时,由于 Object 类是所有类的基类,也就是说所有的类都可以使用 clone 方法。
- clone 方法必须被包裹在 try-catch 语句块中。
public class Person implements Cloneable{
public void testClone(){
try {
// 未重写 clone 方法,通过 super 直接调用 Object 内置的 clone,只能实现浅克隆
super.clone();
System.out.println("克隆成功");
} catch (CloneNotSupportedException e) {
System.out.println("克隆失败");
e.printStackTrace();
}
}
public static void main(String[] args) {
Person p = new Person();
p.testClone();
}
}
5.1.4.4 对象中有引用数据类型的深克隆:
本次实现深克隆的方法是,先调用父类 clone 完成整个类的浅克隆,再根据需要,自行完成引用数据类型的深克隆。
在这个例子中,人对象具有两个变量,分别是 String 存储的 name 和 List 存储的 hobbies
我们创建一个旧人,具有两个爱好,深度克隆后一个新人后,将新人改名,并将爱好增加到3个
然后输出两个人的名字与爱好,观察是否改变。
关键在于 clonedPerson.hobbies = new ArrayList<>(this.hobbies);
如果注释掉这句代码变成完全的浅克隆,则最后输出时,两个人的爱好都会变成 3 个,
即对新人的引用数据的修改影响到了旧人
/**
* Person 类包含一个 name 字段和一个 hobbies 字段,hobbies 是一个字符串列表。
* 在 clone 方法中,我们首先调用 super.clone() 执行对象的浅克隆,然后创建一个新的 ArrayList 对象来存储 hobbies 字段的深度克隆。
* 这样就确保了克隆后的对象和原始对象完全独立,修改其中一个对象不会影响另一个对象。
*/
public class Person implements Cloneable {
private String name;
private List<String> hobbies;
public Person(String name, List<String> hobbies) {
this.name = name;
this.hobbies = hobbies;
}
// Deep clone method
@Override
public Object clone() {
try {
// Perform shallow clone for the object itself
Person clonedPerson = (Person) super.clone();
// Perform deep clone for the ArrayList of hobbies
clonedPerson.hobbies = new ArrayList<>(this.hobbies);
return clonedPerson;
} catch (CloneNotSupportedException e) {
// This should never happen, as Person implements Cloneable
throw new AssertionError();
}
}
public static void main(String[] args) {
// Original person object
List<String> hobbies = new ArrayList<>();
hobbies.add("Reading");
hobbies.add("Hiking");
Person original = new Person("Alice", hobbies);
// Clone the person object
Person clonedPerson = (Person) original.clone();
// Modify the cloned object
clonedPerson.name = "Bob";
clonedPerson.hobbies.add("Cooking");
// Print original and cloned objects
System.out.println("Original: " + original.name + ", Hobbies: " + original.hobbies);
System.out.println("Cloned: " + clonedPerson.name + ", Hobbies: " + clonedPerson.hobbies);
}
}
5.1.4.5 对象中有对象的深克隆,使用 Object 中的 clone
本例需要克隆的 person 对象中定义了 adress 对象。
person.address = (Address) address.clone() 完成对 person 对象中 adress 对象的深克隆、
- 首先,address.clone() 调用了 Address 类的 clone() 方法,因为 Address 类实现了 Cloneable 接口,所以它的 clone() 方法是合法的。在 Address 类的 clone() 方法中,它会调用 super.clone(),即 Object 类的 clone() 方法,这会创建并返回一个新的 Address 对象,其中的字段值与原对象相同,实现了浅拷贝。
- 然后,(Address) 强制类型转换将浅拷贝的 Address 对象转换为 Address 类型。
- 最后,将这个深拷贝后的 Address 对象赋值给了新的 Person 对象的 address 属性,这样新的 Person 对象就拥有了一个与原对象中 address 属性内容相同但是不同引用的 Address 对象,实现了深拷贝。
class Person implements Cloneable
{
private int age;
private String name;
private Address address;
//省略构造方法
@Override // clone 重载
protected Object clone() throws CloneNotSupportedException
{
Person person = (Person) super.clone();
//手动对address属性进行clone,并赋值给新的person对象
person.address = (Address) address.clone();
return person;
}
}
class Address implements Cloneable
{
private String province;
private String street;
//省略构造方法
// 深拷贝时添加
@Override
protected Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}
5.1.4.6 对象中有对象的深克隆,序列化的方法
通过 Object的 clone 方法实现深克隆比较麻烦, 因此引出了另外一种方式:序列化实现深克隆。
- 序列化:把对象写到流里
- 反序列化:把对象从流中读出来
通过序列化实现深克隆中,常常先使对象实现 Serializable 接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
public class Teacher implements Serializable{
private String name;
private int age;
private Student student;
// 省略构造方法
// 深克隆
public Object deepClone() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
// 省略 get/set
}
public class Student implements Serializable {
private String name;
private int age;
// 省略构造方法
//省略 get/set
}