Java面向对象(进阶)-- Object类的详细概述

文章目录

  • 一、如何理解根父类
  • 二、 Object类的方法
    • (1)引子
    • (2)Object类的说明
  • 三、了解的方法
    • (1)clone( )
      • 1、介绍
      • 2、举例
    • (2)finalize( )
      • 1、介绍
      • 2、举例
    • (3)getClass()
    • (4)hashCode()
  • 四、native关键字的理解
  • 五、重点的方法
    • (1)equals( )
      • 1、适用性
      • 2、java.lang.Object类中equals()的定义
      • 3、子类使用说明
      • 4、重写equals()方法
        • 手动实现
        • IDEA自动实现
      • 5、开发中使用说明
      • 6、区分==和equals()
    • (2)toString( )
      • 1、Object类中toString()的定义
      • 2、子类使用说明
      • 3、开发中的使用场景和说明
      • 4、实现toString()方法
        • 手动实现
        • IDEA自动实现
      • 5、总结
  • 六、练习题
    • (1)equals()练习题
      • 1、题一
      • 2、题二
      • 3、题三
    • (2)toString()练习题
      • 1、题一

一、如何理解根父类

类 java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。

类的继承树关系:
Java面向对象(进阶)-- Object类的详细概述_第1张图片

  • Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用
method(Object obj){} //可以接收任何类作为其参数

Person o = new Person();  
method(o);
  • 所有对象(包括数组)都实现这个类的方法。
  • 如果一个类没有特别指定父类,那么默认则继承自Object类。例如:
public class Person {
    ...
}
//等价于:
public class Person extends Object {
    ...
}

二、 Object类的方法

(1)引子

之前说继承性的时候就已经提到Object:

默认的父类:
Java中声明的类,如果没有显式的声明其父类时,则默认继承于java.lang.Object

若一个类没有显示声明,那么它的父类就是Object;若显示声明了,比如B是它的父类,但是B的父类没有显示写,那么也会继承于Object。

所以,任何一个类都直接或间接继承于Object类。Object类中的结构具有通用性,因为子类都能拿到。

(2)Object类的说明

  • 明确:java.lang.Object

我们自己也可以定义一个类,类名叫Object。(当然不建议这样做)

  • 任何一个Java类(除Object类)都直接或间接的继承于Object类
  • Object类称为java类的根父类
  • Object类中声明的结构(属性、方法等)就具有通用性
    • Object类中没有声明属性
    • Object类提供了一个空参的构造器
    • 之前说过子类对象实例化的全过程,造一个子类对象的时候,会直接或间接地调用到父类的构造器,父类又会调用父类的构造器,最终调用到Object类的构造器,Object就会留一个空参的构造器方便我们调用。
    • 重点关注:Object类中声明的方法
    • Object类的方法通过继承性都能被子类拿到,在权限允许的情况下,子类可以直接调用这些方法。那么任何一个类,在条件允许的情况下都可以调用这个方法,所以Object类的方法具备通用性。

看一下API文档:

Java面向对象(进阶)-- Object类的详细概述_第2张图片

三、了解的方法

Java面向对象(进阶)-- Object类的详细概述_第3张图片

  • 重点方法:equals() \ toString()
  • 了解方法:clone() \ finalize()
  • 目前不需要关注:getClass() – 反射 \ hashCode() – 集合 \ notify() \ notifyAll() \ wait() \ wait(xx) \ wait(xx,yy) – 多线程

(1)clone( )

1、介绍

clone():创建并返回当前类的复制品。调用这个方法的对象是this object。

若用x来调用clone()方法,得到一个复制品,把这个复制品与x用==来判断,结果是false。若是true那就麻烦了,因为是true就说明赋值的是地址。

这里复制的不是地址,而是真实得在堆空间中又造了一个对象。它们俩的地址一定不一样!内容(属性)是一样的。

Java面向对象(进阶)-- Object类的详细概述_第4张图片

2、举例

这里还涉及到接口的问题,简单看一下即可。

package com.atguigu07.object;
//Object类的clone()的使用
public class CloneTest {
    public static void main(String[] args) {
        Animal a1 = new Animal("小花");
        try {
            Animal a2 = (Animal) a1.clone();
            a2.setName("毛毛");

            System.out.println("原始对象:" + a1);
            System.out.println("a1[name = " + a1.getName() + "]");
            System.out.println("clone之后的对象:" + a2);
            System.out.println("a2[name = " + a2.getName() + "]");
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

class Animal implements Cloneable{
    private String name;

    public Animal() {
        super();
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


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

}

Animal默认Object为父类:

Java面向对象(进阶)-- Object类的详细概述_第5张图片

这里重写了父类的方法,其实也没有干啥:

Java面向对象(进阶)-- Object类的详细概述_第6张图片

分别打印原来的和clone()之后的对象的地址和name,如下:

Java面向对象(进阶)-- Object类的详细概述_第7张图片

最终输出结果:

Java面向对象(进阶)-- Object类的详细概述_第8张图片

目前来看,创建对象就有两种方式了。第一种new一下构造器来造对象,第二种就是调用clone()。以后还会有其他的方式。

(2)finalize( )

1、介绍

  • 对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
    • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
  • 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
  • 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
    • 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
  • 在JDK 9中此方法已经被标记为过时的。

Java面向对象(进阶)-- Object类的详细概述_第9张图片

2、举例

package com.atguigu07.object;

public class FinalizeTest {
    public static void main(String[] args) {
        Person p = new Person("Peter", 12);
        System.out.println(p);
        p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
        System.gc();//强制性释放空间

        // try {
        //     Thread.sleep(1000);
        // } catch (InterruptedException e) {
        //     throw new RuntimeException(e);
        // }

    }
}

class Person{
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    //此方法调用的时机:当GC(垃圾回收器)要回收此对象时,调用如下的方法:
    //子类重写此方法,可在释放对象前进行某些操作
    //finalize()可能导致内部出现循环引用,导致此对象不能被回收。
    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象被释放--->" + this);	//这里只是方便看一下是不是调用了它而已
    }
}

可以在编译器中看到这根线,其实在JDK9及以后就有这根线了:

Java面向对象(进阶)-- Object类的详细概述_第10张图片

这里重写了,可以看一下Object父类中的finalize()方法:

Java面向对象(进阶)-- Object类的详细概述_第11张图片

若将“项目依赖”改为1.8.0版本:

Java面向对象(进阶)-- Object类的详细概述_第12张图片

并且“语言标准”也改为1.8:

Java面向对象(进阶)-- Object类的详细概述_第13张图片

这个时候finalize上面的横线就没有了,因为它从9才开始过时。

换成17之后,并不是不能用,只是过时了而已。

那什么时候会调用finalize方法呢?回收的时候。那怎么才能让它回收呢?

public class FinalizeTest {
    public static void main(String[] args) {
        Person p = new Person("Peter", 12);
        System.out.println(p);
        p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
        System.gc();//强制性释放空间
    }
}
class Person{
	//...
    
    //此方法调用的时机:当GC(垃圾回收器)要回收此对象时,调用如下的方法:
    //子类重写此方法,可在释放对象前进行某些操作
    //finalize()可能导致内部出现循环引用,导致此对象不能被回收。
    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象被释放--->" + this);	//这里只是方便看一下是不是调用了它而已
    }
}

p指向堆空间的一个对象,将这根连接线抹除,此时堆空间的这个对象就是一个垃圾了。垃圾回收器GC若是此时回收的话,就会调用finalize方法。

Java面向对象(进阶)-- Object类的详细概述_第14张图片

但是不是随时都可以回收的,需要到一定程度才行,比如内存不够用了。

所以这里调用了一个方法:System.gc();强制调用gc。

但是尽管加上了这个方法,也不一定会调用到finalize()方法。

看一下输出结果,并没有显示finalize()方法中的输出语句,说明此时并没有调用它,也就是没有回收:

image.png

虽然System.gc();强制调用gc,但是垃圾回收器要我们等会儿,虽然我们着急要调用,垃圾回收器有个性,它不干,让我们再等会儿,等着等着程序就结束了,所以咱们就没有看到。

我们可以让程序先睡(sleep)上一秒钟等等咱,然后就可以看到GC执行它啦,如下:

package com.atguigu07.object;

public class FinalizeTest {
    public static void main(String[] args) {
        Person p = new Person("Peter", 12);
        System.out.println(p);
        p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
        System.gc();//强制性释放空间

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }
}

class Person{
	//...
    
    //此方法调用的时机:当GC(垃圾回收器)要回收此对象时,调用如下的方法:
    //子类重写此方法,可在释放对象前进行某些操作
    //finalize()可能导致内部出现循环引用,导致此对象不能被回收。
    @Override
    protected void finalize() throws Throwable {
        System.out.println("对象被释放--->" + this);	//这里只是方便看一下是不是调用了它而已
    }
}

运行一下看看:
image.png

finalize有啥用呢?简单来说,就是这个对象临死之前还有什么遗言。

在最后当前对象要回收了,有没有拿着某些资源没有释放呐?你需要释放一下,要不然还回收不了嘞。

有点像之前学过的Scanner(),最后要close()释放一下,防止泄露。

finalize还有一个作用,就是当前对象临死之前要回收了,遗言是将一些数据保存下来,下一次再次创建新的对象的时候,这些数据可以重新拿来使用。

⚡注意

①finalize执行的时机是“临死之前”。

②那么究竟是谁调用的finalize()方法呢?

GC调用的吗?不准确。

自己调用的,自己的方法当然自己调用。当GC要回收的时候,我自己说还想再做点事。

所以是自己调用的,只不过是GC触发的而已。

③怎么给过时了呢?

之所以会过时,是因为它可能会导致一个问题。

finalize()可能导致内部出现循环引用,导致此对象不能被回收。
Java面向对象(进阶)-- Object类的详细概述_第15张图片

面试题:final(关键字) 、 finally(关键字) 、 finalize (方法名)的区别

根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。这里我们主要关注其中的6个。

(3)getClass()

public final Class getClass():获取对象的运行时类型

Java面向对象(进阶)-- Object类的详细概述_第16张图片

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

public static void main(String[] args) {
	Object obj = new Person();
	System.out.println(obj.getClass());//运行时类型
}

结果:

class com.atguigu.java.Person

(4)hashCode()

public int hashCode():返回每个对象的hash值。(后续在集合框架章节重点讲解)
Java面向对象(进阶)-- Object类的详细概述_第17张图片

public static void main(String[] args) {
	System.out.println("AA".hashCode());//2080
    System.out.println("BB".hashCode());//2112
}

四、native关键字的理解

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++等非Java语言实现的,并且被编译成了DLL,由Java去调用。

  • 本地方法是有方法体的,用c语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体
  • 在Java中定义一个native方法时,并不提供实现体。

1. 为什么要用native方法

Java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,例如:Java需要与一些底层操作系统或某些硬件交换信息时的情况。native方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

2. native声明的方法,对于调用者,可以当做和其他Java方法一样使用

native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。

五、重点的方法

(1)equals( )

Java面向对象(进阶)-- Object类的详细概述_第18张图片

1、适用性

任何引用数据类型都可以使用。(基本数据类型不可以用,它和Object连继承关系都没有)

数组可以用:(包括后面要学习的接口)
Java面向对象(进阶)-- Object类的详细概述_第19张图片

2、java.lang.Object类中equals()的定义

举例

【UserTest.java】

package yuyi12;

/**
 * ClassName: UserTest
 * Package: yuyi12
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/12 0012 21:45
 */
public class UserTest {
    public static void main(String[] args) {
        //两个对象的属性值一样
        User u1=new User("Tom",12);
        User u2=new User("Tom",12);

        //u1调用equals()方法,将u2放入,结果是?
        System.out.println(u1.equals(u2));  //false
        //当前User并没有重写equals()这个方法,现在可以调用一定是来自于父类,此时User并没有写父类是谁,那么默认是Object
    }
}

class User{
    //属性
    String name;
    int age;

    //构造器
    public User(){

    }

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

输出结果:

image.png

当前User并没有重写equals()这个方法,现在可以调用一定是来自于父类,此时User并没有写父类是谁,那么默认是Object。

按住Ctrl键点击equals()方法,就会跳转到Object中equals()方法的声明,如下:

Java面向对象(进阶)-- Object类的详细概述_第20张图片

所以,

java.lang.Object类中equals()的定义:

public boolean equals(Object obj) {
    return (this == obj);
}

可以看到,java.lang.Object类中equals()中用到了==,所以这里就是判断一下u1与u2的地址值是否相同,如下:(很显然u1与u2是两个对象,地址值不同,所以输出false)

System.out.println(u1.equals(u2));  //false

3、子类使用说明

  • 自定义的类在没有重写Object中equals()方法的情况下,调用的就是Object类中声明的equals(),比较两个

对象的引用地址是否相同。(或比较两个对象是否指向了堆空间中的同一个对象实体)
平时开发中要是自定义类的话,要是调用equals(),一般也得重写。
比如刚才的例子,若是直接使用Object类中的equals()方法,就和==结果一致了,那还不如直接使用==

System.out.println(u1.equals(u2));  //false
System.out.println(u1==u2);

若平时想调用equals()方法,肯定也是想比较内容的,所以也得重写。

  • 对于像String(字符串的字符数组是否一样)、File(对应文件是不是同一个)、Date(对象是不是指的是同一个时间)和包装类等,它们都重写了Object类中的equals()方法,用于比较两个对象的实体内容是否相等。

【举例】

String

String除了可以正常声明之外,还可以new,比如:

public class UserTest {
    public static void main(String[] args) {
        String str1=new String("hello");
        String str2=new String("hello");
        System.out.println(str1==str2); //false
        System.out.println(str1.equals(str2));  //true
    }
}

输出结果:
image.png

很明显,这里String类调用的并不是Object类中的equals()方法(因为Object类中的equals()方法与==结果一致)。

按住Ctrl键点击equals()方法,就会跳转到String类中对equals()方法的重写,如下:

Java面向对象(进阶)-- Object类的详细概述_第21张图片

随着JDK版本的迭代,这个代码写的越发看不懂了,我们可以看一下JDK8版本的。如下:

public boolean equals(Object anObject){ //形参anObject,这个方法是Object中定义的,Object中定义的形参是Object,所以String重写的时候,名和形参都要与父类一样,所以这里的形参也是Object

    //若两个对象的地址一样,直接返回true
    if(this==anObject){ 
        return true;
    }
    
    //若两个对象的地址不一样,就是两个对象
    if(anObject instanceof String){ //判断一下参数是不是String类型,若是则进入if,否则不执行if直接输出下一条语句,返回false
        String anotherString=(String)anObject;  //强转
        int n=value.length; //字符串(String)在底层存储的是很多字符(char)构成的数组,这个数组叫value
    
        //想要看看两个字符串内容是否相等,就是比较两个数组是不是每一个字符都一样
        if(n == anotherString.value.length){    //先判断两个数组长度是否一致,若不一致直接结束,不用再比较
            char v1[]=value;    //将调用equals()方法的数组拿过来
            char v2[]=anotherString.value;  //将形参anObject的数组拿过来
            int i=0;
            while(n-- !=0){
                if(v1[i] != v2[i]){ //两个数组一个一个比较,只要有一个位置不一样,就false
                    return false;
                }
                i++;
            }
            return true;
        }
    }
}

File

类似String这种场景还是很多的,比如File

public class UserTest {
    public static void main(String[] args) {
        File file1=new File("d:\\abc.txt"); //d盘下这个文件可能不存在,现在只是内存层面来说
        File file2=new File("d:\\abc.txt");
        System.out.println(file1.equals(file2));
    }
}

对于File来说,equals()方法也重写了,比较的就是它们地址值是不是一样,指向的是不是同一个文件。所以此时输出结果是true,如下:
image.png

4、重写equals()方法

平时使用equals()方法时,需要重写一下,否则就和==一致了,并没有调用的必要。

怎么重写呢?

父类Object中的equals()方法如下:

public boolean equals(Object obj) {
	return (this == obj);
}
手动实现

【重写equals()方法】

方式一:

//重写equals()方法

@Override
public boolean equals(Object obj) {
    //若两个对象的地址一样,直接返回true
    if(this==obj){
        return true;
    }
    //若两个对象的地址不一样,就是两个对象
    if(obj instanceof User){    //判断一下参数obj是不是User类型,若是则进入if,否则不执行if直接输出下一条语句,返回false
        User user=(User) obj;//若是User,则强转
        if(this.age==user.age && this.name.equals(user.name)){  //若name与age都相等,则返回true
            return true;
        }else{  //若是name与age有一个不相等,则返回false
            return false;
        }
    }else{
        return false;
    }
}

注意一下这里的写法:(有个地方不小心把user的u写成大写了,不好意思~)

Java面向对象(进阶)-- Object类的详细概述_第22张图片

方式二:

//重写equals()方法

    @Override
    public boolean equals(Object obj) {
        //若两个对象的地址一样,直接返回true
        if(this==obj){
            return true;
        }
        //若两个对象的地址不一样,就是两个对象
        if(obj instanceof User){    //判断一下参数obj是不是User类型,若是则进入if,否则不执行if直接输出下一条语句,返回false
            User user=(User) obj;//若是User,则强转
            
            //方式1:
            /*if(this.age==user.age && this.name.equals(user.name)){  //若name与age都相等,则返回true
                return true;
            }else{  //若是name与age有一个不相等,则返回false
                return false;
            }*/
            
            //方式2:
            return this.age==user.age && this.name.equals(user.name);
        }else{
            return false;
        }
    }

现在再来看一下整体的代码:

package yuyi12;

public class UserTest {
    public static void main(String[] args) {
        //两个对象的属性值一样
        User u1=new User("Tom",12);
        User u2=new User("Tom",12);

        //u1调用equals()方法,将u2放入,结果是?
        System.out.println(u1.equals(u2));  //false--->true
        System.out.println(u1==u2);
        
    }
}

class User{
    //属性
    String name;
    int age;

    //构造器
    public User(){

    }

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

    //重写equals()方法
    @Override
    public boolean equals(Object obj) {
        //若两个对象的地址一样,直接返回true
        if(this==obj){
            return true;
        }
        
        //若两个对象的地址不一样,就是两个对象
        if(obj instanceof User){    //判断一下参数obj是不是User类型,若是则进入if,否则不执行if直接输出下一条语句,返回false
            User user=(User) obj;//若是User,则强转

            //方式1:
            /*if(this.age==user.age && this.name.equals(user.name)){  //若name与age都相等,则返回true
                return true;
            }else{  //若是name与age有一个不相等,则返回false
                return false;
            }*/

            //方式2:
            return this.age==user.age && this.name.equals(user.name);
        }else{
            return false;
        }
    }
}

再次输出结果,是true,如下:
image.png


IDEA自动实现

由于equals()方法在开发中比较常用,所以可以不用自己手写啦,可以用编译器自动生成。

按快捷键“Alt+Insert”,跳出一个框,点击“equals()”:

Java面向对象(进阶)-- Object类的详细概述_第23张图片

直接“下一步”:

Java面向对象(进阶)-- Object类的详细概述_第24张图片

这里我们就当只有两个属性都相同才算“equals()”,可以根据需要选择:

Java面向对象(进阶)-- Object类的详细概述_第25张图片

一直“下一步”,然后“创建”即可:(hashCode()暂时不用管)

Java面向对象(进阶)-- Object类的详细概述_第26张图片

然后就可以自动生成啦,如下:(和我们刚才手写的有点区别,但是功能是一样的)

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return age == user.age && Objects.equals(name, user.name);
}

5、开发中使用说明

  • 实际开发中,针对于自定义的类,常常会判断两个对象是否equals(),而此时主要是判断两个对象的属性值是否相等。

所以:我们要重写Object类的equals()方法。

  • 如何重写:
    • 手动自己实现
    • 调用IDEA自动实现(推荐)

以后开发中,可以自动生成它。

建议大家还是练一练,第一锻炼逻辑,第二以后有个需求是判断两个对象谁大谁小,也需要写类似这里的逻辑。

6、区分==和equals()

= =

==:运算符

使用范围:基本数据类型、引用数据类型

<1>基本数据类型:判断数据值是否相等

举例

int i1 = 10;
int i2 = 10;
sout(i1 == i2);//true

char c1 = 'A';
int i3 = 65;
sout(c1 == i3);//true  char自动类型提升为int

float f1 = 12.0F;
int i4 = 12;
sout(f1 == i4);//true	int类型自动提升为float

用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错。

基本类型比较值,只要两个变量的值相等,即为true。

<2>引用数据类型变量:比较两个引用变量的地址值是否相等。(或比较两个引用是否指向同一个对象实体)

举例

Person p1=new Person();  	    
Person p2=new Person();
if (p1==p2){}

引用类型比较引用(是否指向同一个对象),只有指向同一个对象时,==才返回true。

equals()

equals():方法

  • 使用范围:只能使用在引用数据类型上。(包括数组)
  • 具体使用:对于类来说,重写equals()和不重写equals()的区别。

【数组应用举例】

package yuyi12;

public class UserTest {
    public static void main(String[] args) {
        //数组上使用equals()
        int[] arr=new int[10];
        System.out.println(arr.equals(new int[20]));    //这里没有重写之说,用的就是Object中的==,输出false
        System.out.println(arr.equals(new int[10]));
    }
}

输出结果:
image.png


**equals():**所有类都继承了Object,也就获得了equals()方法。还可以重写。

  • 只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象。

Java面向对象(进阶)-- Object类的详细概述_第27张图片

  • 格式:obj1.equals(obj2)
  • 特例:当用equals()方法进行比较时,对类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;
    • 原因:在这些类中重写了Object类的equals()方法。
  • 当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等
  • 重写equals()方法的原则
    • 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
    • 自反性:x.equals(x)必须返回是“true”。
    • 传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
    • 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
    • 任何情况下,x.equals(null),永远返回是“false”;
      x.equals(和x不同类型的对象)永远返回是“false”。

高频面试题:==equals的区别

从面试的反馈,85%的求职者“理直气壮”的回答错误…

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
  • equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。
  • 具体要看自定义类里有没有重写Object的equals方法来判断。
  • 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

(2)toString( )

Java面向对象(进阶)-- Object类的详细概述_第28张图片

1、Object类中toString()的定义

举例

【ToStringTest.java】

package yuyi16;

/**
 * ClassName: ToStringTest
 * Package: yuyi16
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 16:16
 */
public class ToStringTest {
    public static void main(String[] args) {
        User u1=new User("Tom",12);
        System.out.println(u1.toString());
    }
}


class User{
    String name;
    int age;

    //构造器
    public User() {

    }

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

    //方法
}

输出结果:
image.png

u1在User(继承于Object)没有重写ToString的时候,它调用的就是Object里面的ToString方法。如下:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

解释一下:

Java面向对象(进阶)-- Object类的详细概述_第29张图片

我们可以看到输出结果与直接输出u1的结果一致,如下:

public class ToStringTest {
    public static void main(String[] args) {
        User u1=new User("Tom",12);
        System.out.println(u1.toString());  //yuyi16.User@4eec7777
        System.out.println(u1); //yuyi16.User@4eec7777
    }
}

这是为啥呢?其实可以看一下println内部源码:(虽然这里写的是u1,但是实际上最终调用的也是toString)

Java面向对象(进阶)-- Object类的详细概述_第30张图片

2、子类使用说明

  • 自定义的类,在没有重写Object类的toString()的情况下,默认返回的是当前对象的地址值
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • 像String、File、Date或包装类等Object的子类,它们都重写了Object类的toString(),在调用toString()时,返回当前对象的实体内容

举例1

public class ToStringTest {
    public static void main(String[] args) {
        String s1=new String("hello");
        System.out.println(s1.toString());
    }
}

输出结果:
image.png

可以看到,输出的不是地址值,而是实体内容。为啥呢?很简单,重写了呗。

举例2

package yuyi16;
import java.io.File;

public class ToStringTest {
    public static void main(String[] args) {
        File file =new File("d:\\abc.txt");
        System.out.println(file);
    }
}

输出结果:
image.png
可以看到打印的是文件的路径而不是地址值。

举例3

package yuyi16;

import java.util.Date;

public class ToStringTest {
    public static void main(String[] args) {
        Date date=new Date();
        System.out.println(date);
    }
}

输出结果:
image.png

可以看到,打印的也不是地址值,而是当前生成该对象的时间。

3、开发中的使用场景和说明

  • 平时我们在调用System.out.println()打印对象引用变量时,其实就调用了对象的toString()
  • 习惯上,开发中对于自定义的类在调用toString()时,也希望显示其对象的实体内容,而非地址值。这时候,就需要重写Object类中的toString()。

4、实现toString()方法

手动实现
//手动实现toString方法
    @Override
    public String toString() {
        //习惯上先会留一个类型
        return "User{ name = " + name + ",age = " + age + "}";
    }

整体代码:

package yuyi16;

public class ToStringTest {
    public static void main(String[] args) {
        User u1=new User("Tom",12);
        System.out.println(u1.toString());  //yuyi16.User@4eec7777 --> User{ name = Tom,age = 12}
        System.out.println(u1); //yuyi16.User@4eec7777 --> User{ name = Tom,age = 12}
    }
}

class User{
    String name;
    int age;

    //构造器
    public User() {

    }

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

    //手动实现toString方法
    @Override
    public String toString() {
        //习惯上先会留一个类型
        return "User{ name = " + name + ",age = " + age + "}";
    }
}

输出结果:
image.png

现在一看就知道里面的内容是啥了,一般以后就是这样来做。

IDEA自动实现

按快捷键“Alt+Insert”,跳出一个框,点击“toString()”:

Java面向对象(进阶)-- Object类的详细概述_第31张图片

选择想要显示的即可,点击“确定”:

Java面向对象(进阶)-- Object类的详细概述_第32张图片

然后就会自动生成啦:

//自动生成

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

整体代码:

package yuyi16;

public class ToStringTest {
    public static void main(String[] args) {
        User u1=new User("Tom",12);
        System.out.println(u1.toString());  //yuyi16.User@4eec7777 --> User{ name = Tom,age = 12}
        System.out.println(u1); //yuyi16.User@4eec7777 --> User{ name = Tom,age = 12}
    }
}

class User{
    String name;
    int age;

    //手动实现toString方法
    /*@Override
    public String toString() {
        //习惯上先会留一个类型
        return "User{ name = " + name + ",age = " + age + "}";
    }*/


    //自动生成
    @Override
    public String toString() {
        return "User{" +
        "name='" + name + '\'' +
        ", age=" + age +
        '}';
    }
}

输出结果:
image.png

5、总结

方法签名:public String toString()

① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"

② 在进行String与其它类型数据的连接操作时,自动调用toString()方法

Date now=new Date();
System.out.println(“now=+now);  //相当于
System.out.println(“now=+now.toString());

③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

④ 可以根据需要在用户自定义类型中重写toString()方法

如String 类重写了toString()方法,返回字符串的值。

s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());

例如自定义的Person类:

public class Person {  
    private String name;
    private int age;

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

六、练习题

(1)equals()练习题

1、题一

【Account.java】

package yuyi13;

/**
 * ClassName: Account
 * Package: yuyi13
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 10:04
 */
public class Account {
    //属性
    private double balance; //余额

    //方法
    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //构造器
    public Account() {
    }

    public Account(double balance) {
        this.balance = balance;
    }
}

【Customer.java】

package yuyi13;

/**
 * ClassName: Customer
 * Package: yuyi13
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 10:17
 */
public class Customer {
    //属性
    private String name;
    private int age;
    private Account acct;

    //方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Account getAcct() {
        return acct;
    }

    public void setAcct(Account acct) {
        this.acct = acct;
    }

    //构造器
    public Customer() {
    }

    public Customer(String name, int age, Account acct) {
        this.name = name;
        this.age = age;
        this.acct = acct;
    }
}

【CustomerTest.java】

package yuyi13;

/**
 * ClassName: CustomerTest
 * Package: yuyi13
 * Description:
 *说明:判断两个Customer对象是否equals(),除了Customer类需要重写equals()之外,其内部的类类型的属性
 *     所在的类,也需要重写equals()
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 10:18
 */
public class CustomerTest {
    public static void main(String[] args) {
        Customer c1=new Customer("Tom",12,new Account(2000));
        Customer c2=new Customer("Tom",12,new Account(2000));

        System.out.println(c1.equals(c2));
        System.out.println(c1 == c2);
    }
}

输出结果:
image.png

现在我想比较一下c1与c2的属性是否相同,就需要重写equals()方法。

在【Customer.java】中自动生成:

package yuyi13;

import java.util.Objects;

public class Customer {
    //属性
    private String name;
    private int age;
    private Account acct;

    //重写equals()方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Customer customer = (Customer) o;
        return age == customer.age && Objects.equals(name, customer.name) && Objects.equals(acct, customer.acct);
    }
    //...
}

现在返回【Customer.java】中再次执行,发现结果还是false

Java面向对象(进阶)-- Object类的详细概述_第33张图片

此时equals()方法已经重写过了,为啥结果还是false呢?

Account是我们自定义的类,在equals()中需要调Account中的equals()方法,而现在Account类没有重写equals()方法,所以c1与c2的Account类的属性比较的是地址,地址显然不一样,所以结果是false。

Java面向对象(进阶)-- Object类的详细概述_第34张图片

如果在自定义类里面使用了自定义类的属性,而且想重写equals()方法并且用到它了,那么这个属性也得重写。

所以在【Account.java】中也要对equals()方法重写:

package yuyi13;

public class Account {
    //属性
    private double balance; //余额

    //重写equals()方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return Double.compare(account.balance, balance) == 0;
    }
	//...
}

现在返回【Customer.java】中再次执行,发现结果是true

Java面向对象(进阶)-- Object类的详细概述_第35张图片

判断两个Customer对象是否equals(),除了Customer类需要重写equals()之外,其内部的类类型的属性所在的类,也需要重写equals()。

此例中String本身就已经重写过了,所以我们没有感受到,但我们自定义一个类Account的属性,就需要重写equals()方法。

2、题二

题目描述

编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法,两个参数的构造器,

重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创建的两个对象是否相等。

代码

【Order.java】

package yuyi14;

/**
 * ClassName: Order
 * Package: yuyi14
 * Description:
 *  编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法,两个参数的构造器,
 *  重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创建的两个对象是否相等。
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 14:26
 */
public class Order {
    //属性
    private int orderId;
    private String orderName;

    //构造器
    public Order() {
    }

    public Order(int orderId, String orderName) {
        this.orderId = orderId;
        this.orderName = orderName;
    }

    //方法
    public int getOrderId() {
        return orderId;
    }

    public void setOrderId(int orderId) {
        this.orderId = orderId;
    }

    public String getOrderName() {
        return orderName;
    }

    public void setOrderName(String orderName) {
        this.orderName = orderName;
    }

    //手写equals()方法
    @Override
    public boolean equals(Object obj) { //equals形参之所以是Object类,是因为无法确定后续main中调用时,equals到底和谁进行比较,所以才有Object这个顶层父类比较
        if(this==obj){  //判断当前对象和形参是不是同一个对象,若地址一样,直接返回true即可
            return true;
        }
        if(obj instanceof Order){
            //判断obj是不是Order类型,若是,则强转 (因为obj的引用形式是Object,得强转成引用形式是Order的,才能调用引用形式的方法,否则调用的就是父类Object的方法)
            Order order=(Order)obj; //强转

            //两个属性都相同才算一致
            return this.orderId==order.orderId && this.orderName.equals(order.orderName);   //orderId是基本数据类型,用==即可比较;orderName是String类型,String类型里面的equals方法重写了,比较的是字符串里面每个字符的内容
            //通过orderName属性调用equals()方法时,有可能这个属性的值是null,
            // 若是null调用equals就是空指针了,那么要不要加一个非空的判断?在该属性是非空的情况下去调用equals(使得程序健壮性更好)
        }else{
            return false;
        }

    }
}

【OrderTest.java】

package yuyi14;

/**
 * ClassName: OrderTest
 * Package: yuyi14
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 15:04
 */
public class OrderTest {
    public static void main(String[] args) {
        //直接赋值
        Order order1=new Order(1001,"Jack");
        Order order2=new Order(1001,"Jack");    //String用直接赋值的方式(字面量的方式)
        System.out.println(order1.equals(order2));  //true

        //new两个不同字符串对象
        Order order3=new Order(1002,new String("Luis"));
        Order order4=new Order(1002,new String("Luis"));
        System.out.println(order3.equals(order4));  //true
    }
}

运行结果
image.png

⚡注意

这段代码希望大家可以看懂:

package yuyi14;

public class Order {
    //属性
    private int orderId;
    private String orderName;

    //...
    //手写equals()方法
    @Override
    public boolean equals(Object obj) { //equals形参之所以是Object类,是因为无法确定后续main中调用时,equals到底和谁进行比较,所以才有Object这个顶层父类比较
        if(this==obj){  //判断当前对象和形参是不是同一个对象,若地址一样,直接返回true即可
            return true;
        }
        if(obj instanceof Order){
            //判断obj是不是Order类型,若是,则强转 (因为obj的引用形式是Object,得强转成引用形式是Order的,才能调用引用形式的方法,否则调用的就是父类Object的方法)
            Order order=(Order)obj; //强转

            //两个属性都相同才算一致
            return this.orderId==order.orderId && this.orderName.equals(order.orderName);   //orderId是基本数据类型,用==即可比较;orderName是String类型,String类型里面的equals方法重写了,比较的是字符串里面每个字符的内容
            //通过orderName属性调用equals()方法时,有可能这个属性的值是null,
            // 若是null调用equals就是空指针了,那么要不要加一个非空的判断?在该属性是非空的情况下去调用equals(使得程序健壮性更好)
        }else{
            return false;
        }

    }
}

若这个地方改为了==

Java面向对象(进阶)-- Object类的详细概述_第36张图片

则运行结果是不一样的,因为两个对象的地址值不一样:(第一个输出还是true,因为地址值一样)

Java面向对象(进阶)-- Object类的详细概述_第37张图片

//补充
String str1="AA";
String str2="AA";
System.out.println(str1==str2); //true

字符串常量池,它们使用的是池子中的同一个,所以地址是相同的。输出为true,如下:

Java面向对象(进阶)-- Object类的详细概述_第38张图片

3、题三

题目描述

请根据以下代码自行定义能满足需要的MyDate类,在MyDate类中覆盖equals方法,

使其判断当两个MyDate类型对象的年月日都相同时,结果为true,否则为false。

public boolean equals(Object o)

【EqualsTest.java】

public class EqualsTest {
    public static void main(String[] args) {
        MyDate m1 = new MyDate(14, 3, 1976);
        MyDate m2 = new MyDate(14, 3, 1976);
        if (m1 == m2) {
            System.out.println("m1==m2");
        } else {
            System.out.println("m1!=m2"); // m1 != m2
        }

        if (m1.equals(m2)) {
            System.out.println("m1 is equal to m2");// m1 is equal to m2
        } else {
            System.out.println("m1 is not equal to m2");
        }
    }
}

代码

①没有重写equals方法

【MyDate.java】

package yuyi15;

/**
 * ClassName: MyDate
 * Package: yuyi15
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 15:31
 */
public class MyDate {
    //属性
    private int day;
    private int month;
    private int year;

    //构造器
    public MyDate(){

    }
    public MyDate(int day, int month, int year) {
        this.day = day;
        this.month = month;
        this.year = year;
    }

    //方法
    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }
}

运行结果:
image.png

因为m1与m2的地址不同,而且此时equals没有重写(相当于==),所以输出的是else中的内容。

②重写equals方法

【MyDate.java】

package yuyi15;

public class MyDate {
    //属性
    private int day;
    private int month;
    private int year;

    //手写equals()
    @Override
    public boolean equals(Object obj) {
        if(this==obj){
            return true;
        }
        if(obj instanceof MyDate){
            MyDate mydate=(MyDate) obj;
            return this.getDay()==mydate.day &&this.getMonth()==mydate.month &&this.getYear()==mydate.year;
        }
        return false;
    }
}

运行结果:
image.png

(2)toString()练习题

1、题一

题目描述

定义两个类,父类GeometricObject代表几何形状,子类Circle代表圆形。

写一个测试类,创建两个Circle对象,判断其颜色是否相等;利用equals方法判断其半径是否相等;

利用toString()方法输出其半径。

类关系图:
Java面向对象(进阶)-- Object类的详细概述_第39张图片

代码

【GeometricObject.java】

package yuyi17;

/**
 * ClassName: GeometricObject
 * Package: yuyi17
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 17:28
 */
public class GeometricObject {
    //属性
    protected String color;
    protected double weight;

    //构造器
    public GeometricObject() {
        color="white";
        weight=1.0;
    }

    public GeometricObject(String color, double weight) {
        this.color = color;
        this.weight = weight;
    }

    //方法
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }
}

【Circle.java】

package yuyi17;

/**
 * ClassName: Circle
 * Package: yuyi17
 * Description:
 *
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 17:28
 */
public class Circle extends GeometricObject{
    //属性
    private double radius;


    //构造器
    public Circle() {
        color="white";
        weight=1.0;
        radius=1.0;
    }

    public Circle(double radius) {
        color="white";
        weight=1.0;
        this.radius = radius;
    }

    public Circle(String color, double weight, double radius) {
        super(color, weight);
        this.radius = radius;
    }


    //方法
    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    //计算圆的面积
    public double findArea(){
        return 3.14*radius*radius;
    }

    //重写equals方法,比较两个圆的半径是否相等,若相等则返回true
    //手写
    public boolean equals(Object obj){
        if(this==obj){
            return true;
        }
        if(obj instanceof Circle){
            Circle c=(Circle) obj;  //强转
            return this.radius == c.radius;
        }
        return false;
    }

    //重写toString方法,输出圆的半径
    //手写
    public String toString(){
        return "Circle [radius = "+ radius + "]";
    }

}

【CircleTest.java】

package yuyi17;

/**
 * ClassName: CircleTest
 * Package: yuyi17
 * Description:
 *  写一个测试类,创建两个Circle对象,判断其颜色是否相等;利用equals方法判断其半径是否相等;
 *  利用toString()方法输出其半径。
 * @Author 雨翼轻尘
 * @Create 2023/11/13 0013 22:28
 */
public class CircleTest {
    public static void main(String[] args) {
        Circle c1=new Circle(2.3);
        Circle c2=new Circle("red",2.0,3.4);

        System.out.println("颜色是否相等? " + c1.getColor().equals(c2.getColor()));    //getColor返回的是String类型,String类型是重写过的equals,所以可以直接调用

        System.out.println("半径是否相等? "+ c1.equals(c2));

        System.out.println(c1);
        System.out.println(c1.toString());

    }
}

输出结果
Java面向对象(进阶)-- Object类的详细概述_第40张图片

⚡注意

当写好父类【GeometricObject.java】之后,在子类【Circle.java】中用IDEA自动写入构造器时,首先要选择是哪个父类构造器:(因为此时父类有两个构造器)

Java面向对象(进阶)-- Object类的详细概述_第41张图片

此时若要写子类空参构造器,就直接选择第一个父类空参的构造器GeometricObject(),然后要的是空参的,选择最上面那个,点击“确定”,如下:

Java面向对象(进阶)-- Object类的详细概述_第42张图片

就可以自动生成一个子类空参构造器:

public class Circle extends GeometricObject{
    //属性
    private double radius;

    //构造器
    public Circle() {
        
    }
}

重写方法:

//重写equals方法,比较两个圆的半径是否相等,若相等则返回true
//手写
public boolean equals(Object obj){
    if(this==obj){
        return true;
    }
    if(obj instanceof Circle){
        Circle c=(Circle) obj;  //强转
        return this.radius == c.radius;
    }
    return false;
}

//重写toString方法,输出圆的半径
//手写
public String toString(){
    return "Circle [radius = "+ radius + "]";
}

Java面向对象(进阶)-- Object类的详细概述_第43张图片

你可能感兴趣的:(Java基础,java,面向对象(进阶),Object类)