一文带你认识Java中的Object类和深浅拷贝

Java
294
0
0
2023-05-13
目录
  • 前言
  • 一.初识Object类
  • 1.Object类接收所有子类实例
  • 2.Object类部分方法介绍
  • ①.Object内的toString方法
  • ②.Object内的equals和hashCode方法
  • ③.Object类的getClass方法
  • ④.Object类的clone方法
  • 二.认识深拷贝和浅拷贝
  • 1.什么是深浅拷贝?
  • 2.实现深拷贝
  • 三.Object类和深浅拷贝总结

前言

本文介绍了Object类以及Object类部分方法,toString方法,equals和hashCode方法(重写前和重写后的对比),getClass方法,clone方法,以及拷贝新对象时会出现的深浅拷贝,内容较长,耗时一天,建议收藏后观看~

一.初识Object类

Object类是Java默认提供的一个类。而这个类是Java里所有类的顶级父类,即在继承体系下,除了Object以外的所有类都可能有自己的父类,但Object类没有父类,
并且所有的类同时也都默认继承Object类…
Object类是在Java.lang 包中的,默认已经导入了此类

有了Object类,可以使方法的形参接受任何类的对象,也可以使返回类型返回任意类的对象,可以使所有类都继承一些Object共有的方法,故Object类是非常重要的类!!!

1.Object类接收所有子类实例

也就是说,不管是Java库里的类还是自己定义的类,不管它们是否有自己的父类,都默认继承着Object类…

Object类的存在,使所有类之间有了联系,根据向上转型的用法:父类引用可以接收子类对象地址…

那么就可以使用Object类型的引用接受所有类的实例对象

示例:

class Student{//自定义学生类
    String name;
    Student(String name){
        this.name=name;
    }
}
class Animal{//自定义动物类
    String name;
    Animal(String name){
        this.name=name;
    }
}
public class Text {


    public static void function(Object obj) {
        System.out.println(obj);
    }

    public static void main(String[] args) {
        function(new Animal("动物"));
        function(new Student("学生"));
    }
}

Person和Animal类都是自定义的类,其没有extends显示继承其他类,但也默认继承Object类,那么可以通过调用function方法形参用Object类型的引用obj接受每个对象,而此时发生向上转型,而通过输出方法println输出obj,最后输出了两个对象的内容!!

思考:

通过传递的对象不同,输出的内容也不同,那这种实现是否是多态呢?

多态即调用同一方法但不同对象会体现出不同的状态,这里看似是不同状态,但是多态实现条件没有满足,因为并没有发生重写和动态绑定

输出的内容本应是每个对象的名字,但为什么会是一堆字母串?

以demo6.Animal@1b6d3586为例 demo6.Animal即是类的全路径(包名+类名)

@是分隔符, 而1b6d3586是一串十六进制数的字符可以暂时理解为对象的地址

那println方法底层是怎么输出这些的呢?

下面是println的一些源码分析:

此方法是用Object 类型引用接受 即可以接收任意类的实例,即所有对象都能输出

可见最关键的是String.valueOf方法,即是将对象内容转换为字符串形式

再进入到此方法进行分析↓

valueOf方法 返回的即是对象的字符串形式,而当obj接受的是null时,直接返回null

当不为空时,即通过toString方法获取到对象的内容的字符串形式返回…

重点来了:

toString方法在定义Person,和Animal时并没有写此方法,而这两个类也没有显示继承任何的类,而obj指向的对象是Person或者Animal,说明toString方法只能是Object类里面的

故这两个类没有重写toString方法,也就没有发生动态绑定,调用和执行的一直是Object类的toString方法,故也没有发生多态

简单了解 在Object类里的toString方法

getClass()是获取到Person或者Animal的类示例,.然后getName()获取到其类所在的全路径的以字符串形式返回

@是分隔符

hashCode()得到的是Person或者Animal实例的hash值,是一个整数(主要用于区分不同的对象,但可能会存在不同的对象hash值相同的情况(哈希冲突) )

而Integer.toHexString方法即是将获取到的hash值转换为十六进制的字符串形式

以此最后得到类似于demo6.Animal@1b6d3586 这种输出格式

注意: hashCode 和getClass()底层都是native修饰的本地方法,底层由C或者C++代码实现,先明白此方法的大概用途即可

2.Object类部分方法介绍

Object是所有类的父类,其内部定义了一些成员方法,这些方法都会被子类继承,一些通常会被子类重写来使用,学会使用这些方法是很有必要的

以下是Object内的成员方法简单介绍,

简单介绍下被圈起来的成员方法,而其他的方法需要在线程方面用到…

①.Object内的toString方法

toString方法主要就是针对对象的内容,即将对象的内容转换成字符串形式返回

在上面介绍到,当想输出某个对象内容可以通过调用println输出方法,println方法的形参是Object类型接受,然后会调用String.valueOf方法 将对象内容转换为字符串,而此方法内会调用toString方法,最后输出的是:类的全路径@hashCode的十六进制形式

但是这并不符合我们想要输出的对象内容,当Person对象和Animal对象想要输出的内容是对象内的成员变量name要怎么做?

此时就要重写toString方法↓

1.可以根据Objec里的toString的方法在对应的子类里写同样的权限修饰符返回值类型 方法名(需满足重写的要求) 然后具体方法体根据自己实现

public String toString(){

  //重写的内容...
}

2.使用IDEA快捷键或者右击,选择Generate ->toString 自动生成重写的toString

@Override
    public String toString() {
        return "Animal{" +   //自动生成的内容  ,也可以根据需求自己修改
                "name='" + name + '\'' +
                '}';
    }

当在Person和Animal类里重写了toString方法后,再次调用上面function方法其内部输出obj接受的对象内容即能输出每个对象的指定内容

class Animal{
    String name;
    Animal(String name){
        this.name=name;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                '}';
    }
}
class Student {
    String name;

    Student(String name) {
        this.name = name;
    }

    @Override
    public String toString() {  //重写内容
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }

}
public class Text {


    public static void function(Object obj) {
        System.out.println(obj);
    }

    public static void main(String[] args) {
        function(new Animal("动物"));
        function(new Student("学生"));
    }
 }

obj接受Person或者Animal对象,调用println方法,而内部最后会调用Object类里的toString方法得到对象内容的字符串形式,但是因为发生了子类重写Object类的toString方法

此时调用Object类的toString方法执行的是子类自己重写的Object类的toString方法(多态)

而Person和Animal两个类都重写了toString,此时则发生了多态…

最后根据自己设置的对象内容输出了对应的对象…

总结:

toString没有重写时输出的是对象其类的全路径@十六进制形式的hashCode值
toString重写时输出的是自己重写的指定对象内容 (更常用)

②.Object内的equals和hashCode方法

equals是比较当前对象和指定对象是否相等,hashCode是获取当前对象的哈希值
二者通常是成对出现的!

1.Object内的equals方法和hashCode方法

Object内的equals方法:

this引用表示当前对象的引用存放的当前对象的地址,而obj引用接受的是要比较的对象的地址…

故Object内的equals方法比较两个对象时,比较的是两个对象的地址,而对象不同则对象地址一定是不相等,即绝大部分返回的值都是false

而我们大部分情况下判断对象是否相等并不是看对象地址,而是看对象的内容是否相等(成员变量内的值),

要实现这种要求,就要重写equals方法! (根据内容比较对象是否相等)

Object内的hashCode方法:

此方法是native修饰的本地方法,无法直接看到源码.但大致就是通过对象地址根据对应规则生成一个整数即哈希值

简单了解hashCode的用途:

hashCode方法在作用在哈希桶处体现,所谓哈希桶就是一个链表数组,用来动态的搜索数据的数据结构.,主要作用用来搜索 故哈希桶内相同的对象只会存放一份!

当你要存放对象在哈希桶内时,即是对应到数组某个下标的空间内存放该对象,而对象怎么得到一个整形下标,即是通过了hashCode方法生成的一个整数,最后通过该整数与数组长度取余映射到数组的对应下标!

但是hashCode生成的整数跟对象地址有关,那么不同对象会生成不同的整数,但是实际情况判断一个对象是否相同不是看对象地址,而是看对象内容,当内容完全一样,而hashCode又不同,会发生在一个哈希桶内存放着两个内容一样的对象,

或者当又new了一个新对象其内容和之前对象完全相同,可是通过调用hashCode方法根据对象地址生成的哈希值不同映射到不同数组下标,而在此下标对应的空间内又没有找到和之前对象内容完全相同的,则会出现在哈希桶内查找对象时,找不到内容相同的对象但是哈希桶内又存在这样的对象,则达不到我们所实现的要求…

而要实现映射的数组下标即要使获得的hashCode相同,那么就要重写hashCode方法(根据对象内容生成哈希值)

===============================================================

2.重写equals方法和hashCode方法

上面介绍在Object内的equals方法和hashCode方法不满足实际需求,此时需要在子类里根据实际需求重写这两个方法,重写之后即会实现动态绑定 调用我们重写的方法

重写equals方法和hashCode方法可以根据其Object内的方法在子类里写满足重写要求的方法,方法体根据自己需求而改, 要考虑实际情况重写,hashCode也要自己定一套根据不同内容生成对应的整数的哈希方法

而在IDEA里提供了快捷生成的重写方法,右击 Generate 点击equals and hashCode 一路Next即可 自动根据对象内容生成 equals方法和hashCode方法

@Override
    public boolean equals(Object o) {
        if (this == o) return true;// 对象地址相等返回true
        if (o == null || getClass() != o.getClass()) 
        return false; 要比较的对象为null或者 两个对象是不同的类实例 返回false
        
        Animal animal = (Animal) o;
        return Objects.equals(name, animal.name);
        //根据Objects类的equals方法 传指定内容比较是否相等返回结果
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);//根据Objects类的hash方法传指定参数 即根据其参数内容生成hashCode
    }

自动生成的equals方法和hashCode方法底层都调用了Objects类的方法,而Objects类即是Object类的api,里面提供的都是静态方法,主要用于当重写Object类时,根据子类对象的内容调用Objects对应的方法,分化成子类内容之间进行操作

equals(name, animal.name);

接受当前对象的name和指定比较对象的name, 而此时的a.equals(b) 转换为了name之间的比较,而name是String类型,其类已经封装好了对equals的重写(即判断每个字符是否相等),通过此方法得出对应的内容是否相等,当有多个参数比较则会依次调用多个此方法

===============================================================

Objects.hash(name);

其内部的hash方法形参是Object…values

而这个语法叫做可变参数:

在 Java 5 中提供了变长参数,允许在调用方法时传入不定长度的参数。变长参数是 Java 的一个语法糖,本质上还是基于数组的实现:
在定义方法时,在最后一个形参后加上三点 …,就表示该形参可以接受多个参数值,多个参数值被当成数组传入。上述定义有几个要点需要注意:
可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数
由于可变参数必须是最后一个参数,所以一个函数最多只能有一个可变参数
Java的可变参数,会被编译器转型为一个数组
变长参数在编译为字节码后,在方法签名中就是以数组形态出现的。这两个方法的签名是一致的,不能作为方法的重载。如果同时出现,是不能编译通过的。可变参数可以兼容数组,反之则不成立

使用了可变参数说明hash()实参可以有多个,根据实际情况而定,最后都会被接受放在Object数组里,此时再调用其内部封装的hashCode , 按相应的规则,调用数组每一个元素的hashCode方法,因为每一个元素都是指定对象的内容 可能是字符串类型或者是Integer类型等, 其内部都封装了hashCode方法,有自己根据自身内容生成哈希值的方法,最后合并得到一个哈希值返回给Objects.hash方法调用方得到一个根据对象指定内容生成的哈希值

故使用编译器自动生成的重写equals和hashCode方法,会编写好相应的方法体,在我们使用的时候,根据需求对对象相应内容要比较的参数传上去,根据指定内容或者全部内容以此判断对象是否相等以及生成相应的hash值!

为什么equals和hashCode需要一起重写?

因为当只重写了equals表示会根据相应内容和指定对象进行比较是否相等,此时没有重写hashCode则在哈希桶里仍然会出现指定内容相同的对象出现多份或者有指定内容相等的对象但是没有找到此对象的情况

而只重写hashCode是没有意义的…哈希桶里即便出现了对象内容完全一样的,也会因为对象地址不同而判定为两个不同的对象…

所以二者一定要有关联,且要一起重写…

equals和hashCode的关系

equals判断两个对象相等其hashCode一定相等嘛?

equals判断两个对象完全相等,要么地址相等要么指定内容相等,对应的如果重写了equals也就会根据内容重写hashCode 那么hashCode一定会相等

hashCode相等 equals判断两个对象一定会相等嘛?

hashCode相等只能说明在指定的获取哈希的函数里两个对象的指定内容或者地址最后生成的哈希值是相同的,但是也有可能内容地址不同却出现相同的哈希值(哈希冲突),但是此时equals并不会相同!

总结:

equals相同,hashCode一定相同, hashCode相同 equals不一定相同,

equals不同,hashCode可能相同,hashCode不同则equals一定不相同

③.Object类的getClass方法

getClass方法是Java中获取类实例的一种方法,而类实例主要用于反射中使用

简单了解下反射机制:

Java文件被编译后,生成了.class文件,JVM此时就要去解读.class文件
,被编译后的Java文件.class也被JVM解析为一个对象,这个对象就是 java.lang.Class .
这样当程序在运行时,每个java文件就最终变成了Class类对象的一个实例。我们通过Java的反射机制应用到这个实例,就可以去获得甚至去添加改变这个类的属性和动作,使得这个类成为一个动态的类

而一个类文件 只有一个类实例,一个类实例化多个对象,通过这多个对象调用getClass获得的类实例都是同一个

在使用快捷命令重写equals方法生成的代码中使用了getClass方法,

即是获得当前对象的类实例,

和Object类型的o引用接受的对象调用getClass获得的类实例(即o指向的对象的类实例)判断是否相等,如果是同一个类实例化的对象则获取的类实例是同一个,如果是不同 的类的实例对象 则会返回false…此处即是为了判断两个对象是否是同一个类实例而来

也可以使用 if ( ! o instanceof Person) return false;判断 o指向的对象是否是Person的实例,从而判断两个对象是否是同一个类实例而来

④.Object类的clone方法

Object类里的clone方法是native修饰的本地方法底层由C/C++实现是直接供对象调用的方法,当对象调用clone方法会在堆区创建一份和原对象一模一样的对象返回(即属性和行为都一样)

虽然clone方法可以直接调用,但并不是字面意思上直接调用

class Person{
    String name;
    int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
}

当有了上面这个类,我们是否可以在main方法中直接创建一份Person对象,再直接调用clone方法再用一个Person引用接受克隆的新对象呢?

此时会编译报错, 在Java中clone被看成一种公共特性,即提供了一个Cloneable接口,其是一个空接口,表示一个标准规范,只有实现了此接口的类才能调用clone方法进行克隆,如果没有实现,则会报CloneNotSupportedException异常

class Person implements Cloneable{
    String name;
    int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
}

当Person实现了Cloneable具备了克隆特性 是否可以克隆了呢?

注意! clone方法在Object类是protected修饰的!!!

当被protected修饰的成员,在当前包里可以直接被访问,但是在不同包里,只能在其对应的子类内被访问!

即调用的是Person对象继承的Object类的clone方法,Person是Object的子类,那么就要在Person类内调用clone方法,Test类虽然也是Object的子类但是调用的是Person,所以在Test类里是不能调用到Person类对象的clone方法

为了实现接口方法统一使在Test类里能够调用到Person的clone方法,这里也要在Person类里重写clone方法,但这个重写只是给类外提供调用clone的方法,并没有重写clone本身的方法

通过 在Person 里重写 clone方法 方法体 是调用其父类Object类的clone方法,但是会抛出一个编译时异常,此异常为了提醒程序员需要处理此异常,即必须try-catch处理或者throws 声明异常抛给上层调用方

使用throws层层声明异常后↓

到这里最后一步,能够调用Person的clone方法 克隆一份一样的对象,但是clone方法的返回类型是Object类型的,不能直接用Person接受,向下转型需要这里强转

最终的写法↓

class Person implements Cloneable{//Person实现Cloneable 支持克隆
    String name;
    int age;
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class Text {//Cloneable是一个接口 空接口 是一个标记接口 标准规范 唯一作用 表示当前类实例化的对象是可以被克隆的,如果没有实现这个接口就不能被克隆
    //实现cloneable接口 当前对象可以克隆 但是需要重写object类的clone方法,但是这个重写只能为了调用父类clone方法!!! 没有实现这两种会报编译时异常!

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person = new Person("张三", 18);
        Person clonePerson = (Person) person.clone();
        
    }

}

为什么clone返回类型是Object类型?

一个方法只有一个返回类型,而实现cloneable接口的对象一般都支持克隆,但是既然可以克隆很多对象,那么返回类型是不确定具体返回的对象类是哪个,而根据Object类的特性,其是所有类的父类,那么就可以达成通用, 借助向上转型返回对象,在外层由程序员自己进行向下转型 从而实现对应的类型接受对应的克隆对象

二.认识深拷贝和浅拷贝

在认识Object的clone方法后,我们能直接拷贝一份和对象一模一样的新对象出来,但是在拷贝对象时,因为引用变量存放的是对象地址,故在拷贝时还要区分深拷贝和浅拷贝!

1.什么是深浅拷贝?

浅拷贝即是当修改拷贝的对象内容时, 原被拷贝对象的内容也会随之被修改,即两个对象共用同一块内容,看似拷贝了一份全新的对象,但是这个对象的成员和原对象的成员仍然共用同一份空间
深拷贝即当修改拷贝对象内容时,原拷贝对象内容不会改变,即两个对象的所有内容也是独立被拷贝的!
class Money implements Cloneable{
    public double m=3.14;

    @Override
    public String toString() {
        return "Money{" +
                "m=" + m +
                '}';
    }
}
public class Student implements Cloneable {
    Money money=new Money();
    public String name;
    public int age=18;
    public Student(String name){
        this.name=name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
    return super.clone();
    }

    @Override
     public String toString() {
        return "Student{" +
                "money=" + money +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public static void main(String[] args) throws CloneNotSupportedException{
        Student student=new Student("666");
        Student student1=(Student) student.clone();
        System.out.println(student);
        System.out.println(student1);
        student1.age=20;// 基本数据类型存放的是原对象的数据 此时修改基本数据类型, 原对象并不会受到影响
        student1.name="000";//name虽然指向的是同一个对象 但是后面直接是实例化另一个对象给克隆的引用变量本质上
        // 克隆对象的name一开始指向的和原对象的name指向的字符串对象一样
        student1.money.m=100.0;   //会克隆一份对象 里面成员变量值和方法是和原对象一样的
        //引用数据类型存放的是原对象的对象地址 这就造成 克隆后对象内的引用变量指向的是同一块对象地址
        //此时通过引用变量修改对象内容 原来的和克隆的占用的是同一份 浅拷贝!!! money指向的同一块对象里面的m成员 共用一块空间
        System.out.println("更改后===========");
        System.out.println(student);
        System.out.println(student1);
    }

}

通过上面代码运行后 发现 只对克隆的对象内容进行修改后,但有些内容原对象也跟着改变,这即是发生浅拷贝, 拷贝的程度浅,对象拷贝了一份但内容却没有真正拷贝.

age是基本数据类型,拷贝的对象内也存有一份age数据,修改这个age不会影响原对象的age值,name是引用类型数据 指向字符串对象, 而修改拷贝对象内的字符串对象本质是创建了一个新字符串,故也没有影响原对象,

但是Menoy引用指向的一份对象,经过拷贝后 新对象的Menoy引用同样存放着原Menoy对象的地址,此时通过新对象修改Menoy指向的对象内的值,原对象内的Menoy对象内的值也会发生改变

此时的拷贝形式正是浅拷贝,对象并没有完全被拷贝,而如何实现深拷贝呢?

2.实现深拷贝

浅拷贝即拷贝了新对象,但是新对象的内容可能和原对象共用一块空间,故要实现深拷贝,要在原来拷贝的基础上,对可能共用一块空间的内容进行再拷贝一份

在上面代码基础上,使Menoy引用指向不同对象则要对Menoy对象单独进行克隆

class Money implements Cloneable{
    public double m=3.14;


    @Override
    public String toString() {
        return "Money{" +
                "m=" + m +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();   //这里用于给 money克隆一份
    }
}
public class Student implements Cloneable {
    Money money=new Money();
    public String name;
    public int age=18;
    public Student(String name){
        this.name=name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        student.money=(Money) this.money.clone(); //调用 money的clone方法为其克隆出一份 
        return student; //自动向上转型
    }

    @Override
    public String toString() {
        return "Student{" +
                "money=" + money +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

对象内主要是Menoy对象是同一个空间,那么在克隆对象时,先克隆出新Student对象,再根据指定Menoy对象克隆出新的Menoy对象(Menoy也要支持clone)给克隆对象的Menoy引用,最后返回克隆的新对象, 最后实现了深拷贝,两个对象的内容是独立不互不影响的

注意: 除了clone能拷贝对象以外还有其他方法能对对象进行拷贝,如Arrays.copyOf方法能够对指定数组对象进行拷贝,对数组进行拷贝 也会出现深浅拷贝的现象,拷贝的新数组每个引用可能指向的原数组的每个引用指向的对象…

故在拷贝时要注意需要的是深拷贝还是浅拷贝…

三.Object类和深浅拷贝总结

本篇博客介绍了Object类 以及其内部的一些方法:toString(重写前:获取其类的全路径@对象地址,重写后将对象内容转换为字符串形式返回),
equals和hashCode(重写前:判断对象的地址是否相等和根据对象地址生成哈希值,重写后:判断对象指定内容是否相等和根据指定内容获取对象生成的哈希值),
getClass (获取类实例),clone(克隆对象需要注意权限和异常)
以及跟克隆相关的深浅拷贝(对新对象内容进行修改是否会影响原对象的内容)