03 JavaSE-- 访问控制权限、抽象类/方法、interface、内部类、Object 类

本文主要是介绍03 JavaSE-- 访问控制权限、抽象类/方法、interface、内部类、Object 类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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;}@Overridedouble area() {return width * height;}
}class Circle extends Shape {private double radius;Circle(double radius) {this.radius = radius;}@Overridedouble 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 原则了

假设现在新增了一个耳机,那么要做的工作显然是:

  1. 编写耳机类继承 Usb 接口,并实现抽象方法。这是实现者的视角。
  2. 电脑类不必做任何改变,因为从调用者的视角,其面向接口调用,而接口没有任何改变,电脑类自然不应当有任何改变。因此,不会违背 OCP 原则。
  3. 测试类中,实例化一个耳机对象,并将该对象传入 电脑类的 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修饰。
从JDK8开始,不需要手动添加final了,但JVM会自动添加。

4.4 匿名内部类

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 类型。*/@Overridepublic 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@Overridepublic Object clone() {try {// Perform shallow clone for the object itselfPerson clonedPerson = (Person) super.clone();// Perform deep clone for the ArrayList of hobbiesclonedPerson.hobbies = new ArrayList<>(this.hobbies);return clonedPerson;} catch (CloneNotSupportedException e) {// This should never happen, as Person implements Cloneablethrow new AssertionError();}}public static void main(String[] args) {// Original person objectList<String> hobbies = new ArrayList<>();hobbies.add("Reading");hobbies.add("Hiking");Person original = new Person("Alice", hobbies);// Clone the person objectPerson clonedPerson = (Person) original.clone();// Modify the cloned objectclonedPerson.name = "Bob";clonedPerson.hobbies.add("Cooking");// Print original and cloned objectsSystem.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 对象的深克隆、

  1. 首先,address.clone() 调用了 Address 类的 clone() 方法,因为 Address 类实现了 Cloneable 接口,所以它的 clone() 方法是合法的。在 Address 类的 clone() 方法中,它会调用 super.clone(),即 Object 类的 clone() 方法,这会创建并返回一个新的 Address 对象,其中的字段值与原对象相同,实现了浅拷贝。
  2. 然后,(Address) 强制类型转换将浅拷贝的 Address 对象转换为 Address 类型。
  3. 最后,将这个深拷贝后的 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;//省略构造方法// 深拷贝时添加@Overrideprotected 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}

这篇关于03 JavaSE-- 访问控制权限、抽象类/方法、interface、内部类、Object 类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/985673

相关文章

问题:第一次世界大战的起止时间是 #其他#学习方法#微信

问题:第一次世界大战的起止时间是 A.1913 ~1918 年 B.1913 ~1918 年 C.1914 ~1918 年 D.1914 ~1919 年 参考答案如图所示

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

[word] word设置上标快捷键 #学习方法#其他#媒体

word设置上标快捷键 办公中,少不了使用word,这个是大家必备的软件,今天给大家分享word设置上标快捷键,希望在办公中能帮到您! 1、添加上标 在录入一些公式,或者是化学产品时,需要添加上标内容,按下快捷键Ctrl+shift++就能将需要的内容设置为上标符号。 word设置上标快捷键的方法就是以上内容了,需要的小伙伴都可以试一试呢!

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

大学湖北中医药大学法医学试题及答案,分享几个实用搜题和学习工具 #微信#学习方法#职场发展

今天分享拥有拍照搜题、文字搜题、语音搜题、多重搜题等搜题模式,可以快速查找问题解析,加深对题目答案的理解。 1.快练题 这是一个网站 找题的网站海量题库,在线搜题,快速刷题~为您提供百万优质题库,直接搜索题库名称,支持多种刷题模式:顺序练习、语音听题、本地搜题、顺序阅读、模拟考试、组卷考试、赶快下载吧! 2.彩虹搜题 这是个老公众号了 支持手写输入,截图搜题,详细步骤,解题必备

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

电脑不小心删除的文件怎么恢复?4个必备恢复方法!

“刚刚在对电脑里的某些垃圾文件进行清理时,我一不小心误删了比较重要的数据。这些误删的数据还有机会恢复吗?希望大家帮帮我,非常感谢!” 在这个数字化飞速发展的时代,电脑早已成为我们日常生活和工作中不可或缺的一部分。然而,就像生活中的小插曲一样,有时我们可能会在不经意间犯下一些小错误,比如不小心删除了重要的文件。 当那份文件消失在眼前,仿佛被时间吞噬,我们不禁会心生焦虑。但别担心,就像每个问题

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前