18、Java基础教程之面向对象·第十讲

  • 本节学习目标
  • 1️⃣ 基本定义
  • 2️⃣ 使用
    • 2.1 equals()方法
  • 2.2 hashCode()方法
  • 2.3 toString()方法
  • 2.4 finalize()方法
  • 2.5 getClass()方法
  • 2.6 clone()方法
    • * 深拷贝和浅拷贝的区别?
  • 2.7 wait()、notify()和 notifyAll()方法
  • 3️⃣ 使用场景
  • * 总结
  • * 本文源码下载地址

*

本节学习目标

  • 掌握 Object类的主要特征及实际应用;

1️⃣ 基本定义

利用继承与对象多态性的概念可以解决子类对象与父类对象的自动转型操作,但是如果要想统一开发中的参数类型,就必须有一种类可以成为所有类的父类,而这个类就是 Object类。

Java是一种面向对象的编程语言,所有类都直接或间接地继承自Object类。Object类是Java中最基本的类之一,它提供了一些通用的方法,可以被任何Java类使用。 本文将详细介绍Java的Object类及其重要方法。

Object类位于Java的根层次结构中,即所有类都直接或间接地继承自它。每个对象在内存中都有一个对应的Object类实例,该实例包含了对象的状态信息以及操作该对象的方法。Object类的存在使得Java具有了统一访问对象的能力。

Object 类是所有类的父类,也就是说任何一个类在定义时如果没有明确地继承一个父类,那它就是 Object 类的子类,也就是说以下两种类定义的最终效果是完全相同的。

//	范例 1
class Book{
   
     
	...
}

class Book extends Object{
   
     
	...
}

既然Object 类是所有类的父类,那么最大的一个好处就在于:利用 Object 类可以接收全部类的对象,因为可以向上自动转型。

//	范例 2: 利用Object类来接收对象
class Book extends Object {
   
     
}

public class TestDemo {
   
     
	public static void main(String args[]){
   
     
		Object obja = new Book(); 	//向上转型,接收Book 子类对象
		Object objb = "hello";		//向上转型,接收String子类对象
		Book b = (Book) obja;		//向下转型
		String s = (String) objb;	//向下转型
	}
}

上边程序为了测试 Object 可以接收任意类对象,使用 Book 类与 String 类实现了向上与向下转型操作。所以在设计代码时,如果在不确定参数类型时,使用Object 类应该是最好的选择。

2️⃣ 使用

下面是一些Object类的基本方法:

2.1 equals()方法

实际上对于 equals()方法大家应该并不陌生,这个方法在 String 类中使用过,而 String 也是 Object 类的子类,所以 String 类的 equals()方法就是覆写 Object 类中的 equals()方法。在 Object 类中,默认的 equals()方法实现比较的是两个对象的内存地址数值,但是并不符合真正的对象比较需要,因为还需要比较对象内容,所以此方法也需要继承类自己覆写给出比较逻辑实现。

所以,equals()方法用于比较两个对象是否相等。默认情况下,equals()方法使用的是 "==" 运算符进行比较。我们可以通过在子类中重写该方法来实现自定义的相等比较逻辑。

以下是一个Java代码示例,定义了一个Person类和一个Interest 类,并重写了equals()方法:

//	范例 3: 重写equals()方法
public class Person {
   
     
    // 姓名
    private String name;
    // 年龄
    private int age;
    // 兴趣爱好
    private Interest interest;

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

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

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

    // Getter and Setter methods

    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 Interest getInterest() {
   
     
        return interest;
    }

    public void setInterest(Interest interest) {
   
     
        this.interest = interest;
    }
}

public class Interest {
   
     
    private List<String> interestList;

    public List<String> getInterestList() {
   
     
        return interestList;
    }

    public void setInterestList(List<String> interestList) {
   
     
        this.interestList = interestList;
    }

    @Override
    public String toString() {
   
     
        return "Interest{" +
                "interestList=" + interestList +
                '}';
    }

    @Override
    public boolean equals(Object o) {
   
     
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Interest interest = (Interest) o;
        return Objects.equals(interestList, interest.interestList);
    }

    @Override
    public int hashCode() {
   
     
        return Objects.hash(interestList);
    }
}

使用案例:

//	范例 4: 使用equals()方法
public class UseDemo{
   
     
    public static void main(String[] args) {
   
     
        Person person1 = new Person("John", 25);
        Person person2 = new Person("John", 25);
        Person person3 = new Person("Jane", 30);

        System.out.println(person1.equals(person2));  // true
        System.out.println(person1.equals(person3));  // false
    }
}

运行结果:

true
false

在上面的例子中,Person类重写了equals()方法,比较两个Person对象是否相等。根据实现,只有当两个Person对象的姓名、年龄和兴趣爱好都相同时,它们才被认为是相等的。

Main类中,我创建了三个Person对象,并测试了它们之间的相等性。第一个和第二个对象具有相同的姓名和年龄,所以它们被认为是相等的。然而,第一个和第三个对象具有不同的姓名和年龄,所以它们被认为是不相等的。

2.2 hashCode()方法

hashCode()方法用于计算对象的哈希码值。在Java中,哈希码用于加快查找和比较对象的速度。Object类中的hashCode()方法返回对象的内存地址的整数表示形式。

以下是一个Java代码示例,定义了俩个Person类对象,并调用了其hashCode()方法:

//	范例 5: 使用hashCode()方法
...
	public static void main(String[] args) {
   
     
      	Person person1 = new Person("John", 25);
       	Person person2 = new Person("Panda", 25);
       	System.out.println(person1.hashCode()); // 打印HashCode值
       	System.out.println(person2.hashCode()); // 打印HashCode值
	}
...

运行结果:

-2070664751
867568288

2.3 toString()方法

toString()方法用于将对象转换为字符串表示形式。默认情况下,该方法返回一个包含类名和哈希码的字符串。我们可以通过在子类中重写该方法来提供有意义的字符串表达方式。

以下是一个Java代码示例,Person类重写了Object类的toString() 方法:

//	范例 6: 重写Object类的toString() 方法
...
	@Override
    public String toString() {
   
     
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", interest=" + interest +
                '}';
    }
... 

使用案例:

//	范例 7: 通过重写的toString() 方法获取对象信息
import com.xiaoshan.demo1.Interest;
import com.xiaoshan.demo1.Person;
import java.util.Arrays;

public class UseDemo {
   
     
    public static void main(String[] args) {
   
     
        Person person1 = new Person("John", 25);
        Person person2 = new Person("xiaoshan", 27);
        Interest interest = new Interest();
        interest.setInterestList(Arrays.asList("画画","做运动"));
        Person person3 = new Person("xiaoyu", 28,interest);

        System.out.println(person1);
        System.out.println(person2);
        System.out.println(person3);
    }
}

运行结果:

Person{name='John', age=25, interest=null}
Person{name='xiaoshan', age=27, interest=null}
Person{name='xiaoyu', age=28, interest=Interest{interestList=[画画, 做运动]}}

此程序在 Person类中覆写了 toString()方法,而在进行对象输出时,就可以发现会自动调用对象所在类的 toString()方法将对象变为字符串后输出。

可以发现,由于 toString()方法是在Object类中定义,所以所有的类中都可以使用这个方法,那么也就意味着,所有的类都会有一个功能,即将对象转换为String类对象。

2.4 finalize()方法

finalize()方法在垃圾回收器将对象从内存中清除之前被调用。我们可以在子类中重写该方法,进行垃圾回收前的资源释放操作。

* finalize()方法在垃圾标记过程中的作用?
在JVM的应用中,垃圾标记过程使用可达性分析算法,而实际上宣告一个对象真正死亡,会存在两次标记过程:

  • 第一次标记: 如果对象在进行可达性分析后发现没有 GCRoots 相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
    当对象未覆写finalize()方法,或者方法已经被虚拟机调用过,虚拟机会将这两种情况都视为没有必要执行。如果对象被判定为有必要执行,则会被放到一个 F-Queue队列;
  • 第二次标记:finalize()方法是对象跳脱死亡命运的最后一次机会,稍后GC将对F-Queue中对象进行第二次小规模标记,如果对象要在finalize()中重新拯救自己:只要重新与引用链上的任何一个对象建立关联即可,例如将自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出即将回收的对象集合。

以下是一个Java代码示例,Person类重写了Object类的finalize() 方法:

//	范例 8: 重写Object类的finalize() 方法
...
 	@Override
    public void finalize() {
   
     
        // 释放占用的资源或执行清理操作的代码
        System.out.println("正在执行finalize()方法");
    }
...

使用案例:

//	范例 9: 演示finalize() 方法的作用
public class UseDemo {
   
     
    public static void main(String[] args) {
   
     
        // 创建对象,并将其分配给一个变量
        Person person = new Person("xiaoshan", 27);

        // 将该对象设置为null,以便使其称为垃圾
        person = null;

        // 强制进行垃圾回收
        System.gc();
    }
}

运行结果:

正在执行finalize()方法

需要注意的是,如今已经不鼓励使用finalize()方法,因为它的行为不可预测且不确定性很大。最好的做法是显式地在不再需要对象时进行资源释放和清理操作,如使用try-finally块或利用AutoCloseable接口来处理资源回收。

2.5 getClass()方法

getClass()方法返回对象所属的类的Class对象。

public final native Class<?> getClass();

使用案例:

//	范例 10: getClass()方法的使用
public class UseDemo {
   
     
    public static void main(String[] args) {
   
     
        Person person = new Person("xiaoshan", 27);

        // 获取对象的类类型
        Class<? extends Person> objClass = person.getClass();

        // 输出类的名称
        System.out.println("类的名称:" + objClass.getName());

        // 输出类的简单名称(不包含包名)
        System.out.println("类的简单名称:" + objClass.getSimpleName());

        // 输出类的完整名称(包含包名)
        System.out.println("类的完整名称:" + objClass.getCanonicalName());
    }
}

运行结果:

类的名称:com.xiaoshan.demo1.Person
类的简单名称:Person
类的完整名称:com.xiaoshan.demo1.Person

2.6 clone()方法

clone()方法用于创建并返回当前对象的一个副本。对象的克隆是通过实现Cloneable接口以及重写clone()方法来实现的。

protected native Object clone() throws CloneNotSupportedException;

以下是一个Java代码示例,Person类实现了Cloneable接口以及重写clone()方法:

//	范例 11: 实现Cloneable接口以及重写clone()方法
public class Person implements Cloneable {
   
     
	// 中间代码省略

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

使用案例:

//	范例 12: 使用clone()方法
import com.xiaoshan.demo1.Interest;
import com.xiaoshan.demo1.Person;
import java.util.Arrays;

public class UseDemo {
   
     
    public static void main(String[] args) {
   
     
        try {
   
     
            // 创建一个Person对象
            Interest interest = new Interest();
            interest.setInterestList(Arrays.asList("打游戏","看电视"));
            Person person1 = new Person("John", 25, interest);

            // 克隆person1对象
            Person person2 = (Person) person1.clone();

            // 修改person2的属性
            person2.setName("Alice");
            person2.setAge(30);
            interest.setInterestList(Arrays.asList("唱歌","画画"));
            person2.setInterest(interest);

            // 输出person1和person2的信息
            System.out.println(person1);
            System.out.println(person2);
        } catch (CloneNotSupportedException e) {
   
     
            e.printStackTrace();
        }
    }
}

这段代码展示了如何使用clone()方法来克隆一个对象,并展示了克隆对象与原始对象之间的关系。

我先创建了一个Person对象person1,并通过构造函数传入名字"John"、年龄25和一个Interest对象并设置其兴趣列表为[“打游戏”,“看电视”],然后使用clone()方法克隆person1,获取到克隆后的person2对象。

再修改了person2的属性,即将名字修改为"Alice",年龄修改为30,还将兴趣列表改为[“唱歌”,“画画”],最后打印输出person1person2的信息。

运行结果:

Person{name='John', age=25, interest=Interest{interestList=[唱歌, 画画]}}
Person{name='Alice', age=30, interest=Interest{interestList=[唱歌, 画画]}}

由运行结果可以发现,对于拷贝出的新对象的引用类型属性Interest做的修改也影响到了原对象,所以clone()方法实现的是一种浅拷贝。

* 深拷贝和浅拷贝的区别?

深拷贝和浅拷贝是在进行数据拷贝时的两个概念,它们的主要区别在于***拷贝出来的对象是否与原来的对象共享同一块内存空间***。

浅拷贝***是指将原对象中的数据复制到一个新的对象中,新对象的所有变量都含有与原对象相同的值,而***新对象中的引用型变量地址仍指向原对象中的引用型变量。也就是说,如果修改了新对象中的数据,则原对象中的相应数据也会被修改,反之亦成立。浅拷贝通常是通过使用赋值操作符或者拷贝构造函数来实现的。

***深拷贝***是指将原对象中的数据复制到一个新的对象中,新对象与原对象有各自独立的内存地址。 也就是说,如果修改了新对象中的数据,原对象中的相应数据不会被修改。深拷贝通常是通过使用拷贝构造函数或者重载赋值操作符,并在其中手动复制对象的内存空间来实现的。

总之, 浅拷贝只是复制了指针或引用,而深拷贝则是复制了整个对象。在需要修改原对象数据时,使用深拷贝可以避免对原对象的影响。 深拷贝相比于浅拷贝速度较慢并且花销较大。

实现深拷贝的关键在于使用 Java 序列化和反序列化。使用 ObjectOutputStream 将原始对象写入一个 ByteArrayOutputStream 中,然后使用 ByteArrayInputStream 和 ObjectInputStream 读取这个流并创建一个新的对象。

2.7 wait()、notify()和 notifyAll()方法

Object类提供了一些基本的方法,包括wait()notify()notifyAll(),用于实现线程之间的同步和协作。

  • wait()方法用于使调用该方法的线程等待,直到其他线程调用同一对象上的notify()或notifyAll()方法来唤醒它。

  • 在调用wait()方法之前,线程必须先获得该对象的锁。若未获得锁,则会抛出IllegalMonitorStateException异常;

  • 一旦线程处于等待状态,它将释放对象的锁,允许其他线程访问该对象;

  • 被唤醒的线程将继续从wait()方法返回,并尝试重新获取对象的锁,然后继续执行。

  • notify()方法用于通知一个正在等待对象监视器的线程,以便它可以继续执行。

  • 如果多个线程在一个对象上等待,那么只有其中一个线程能被唤醒;

  • 调用notify()方法并不会立即释放锁,它将在相应的synchronized代码块执行完后才会释放。

  • notifyAll()方法用于通知所有正在等待对象监视器的线程,以便它们可以继续执行。

  • 如果多个线程在一个对象上等待,那么所有的线程都将被唤醒,每个线程将竞争获取锁;

  • 调用notifyAll()方法并不会立即释放锁,它将在相应的synchronized代码块执行完后才会释放。

这些方法通常与synchronized关键字一起使用,以提供线程之间的同步处理和协作。通过使用wait()notify()notifyAll(),可以实现生产者-消费者问题、线程间的消息传递和线程池等各种场景下的线程同步与通信。

下面是一个简单的Java代码示例,展示了wait()notify()notifyAll() 方法的使用案例:

//	范例 13: wait()、notify()和notifyAll()方法的使用—— 定义Message 类
public class Message {
   
     
    private String content;

    public Message(String content) {
   
     
        this.content = content;
    }

    public String getContent() {
   
     
        return content;
    }

    public void setContent(String content) {
   
     
        this.content = content;
    }
}

//	范例 13: wait()、notify()和notifyAll()方法的使用—— 定义消息发送器
public class Sender implements Runnable {
   
     
    private final Message message;

    public Sender(Message message) {
   
     
        this.message = message;
    }

    @Override
    public void run() {
   
     
        String[] messages = {
   
     "Message 1", "Message 2", "Message 3", "END"};

        synchronized (message) {
   
     
            for (String msg : messages) {
   
     
                message.setContent(msg);
                System.out.println("Sender: " + msg);

                // 通知Receiver线程可以从等待状态中唤醒继续执行
                message.notify();

                try {
   
     
                    // 当前线程进入等待状态,释放对象锁,并等待被唤醒
                    message.wait();
                } catch (InterruptedException e) {
   
     
                    e.printStackTrace();
                }
            }
        }
    }
}

//	范例 13: wait()、notify()和notifyAll()方法的使用—— 定义消息接收器
public class Receiver implements Runnable {
   
     
    private final Message message;

    public Receiver(Message message) {
   
     
        this.message = message;
    }

    @Override
    public void run() {
   
     
        synchronized (message) {
   
     
            while (!message.getContent().equals("END")) {
   
     
                try {
   
     
                    // 当前线程进入等待状态,释放对象锁,并等待被唤醒
                    message.wait();
                } catch (InterruptedException e) {
   
     
                    e.printStackTrace();
                }

                System.out.println("Receiver: " + message.getContent());

                // 通知Sender线程可以从等待状态中唤醒继续执行
                message.notify();
            }
        }
    }
}

//	范例 13: wait()、notify()和notifyAll()方法的使用—— 定义消息发送接收线程
public class WaitNotifyDemo {
   
     
    public static void main(String[] args) {
   
     
        Message message = new Message("Initial message");

        Thread senderThread = new Thread(new Sender(message));
        Thread receiverThread = new Thread(new Receiver(message));

        receiverThread.start();
        senderThread.start();
    }
}

在上面的代码中,我定义了一个Message类表示要传递的消息内容。SenderReceiver是两个线程分别负责发送和接收消息。

Sender线程负责将预定义的消息串依次发送给Message对象,每发送一条消息后调用notify()方法唤醒处于等待状态的Receiver线程,然后自己进入等待状态调用wait()方法,直到被Receiver线程唤醒。

Receiver线程首先进入等待状态调用wait()方法,等待Sender线程发送消息唤醒它,然后打印接收到的消息后调用notify()方法唤醒Sender线程继续发送下一条消息。这样两个线程交替进行,直到接收到"END"消息为止。

这个示例展示了wait()notify()notifyAll()方法的基本用法,通过对象的锁实现线程间同步和通信。调用wait()方法会使当前线程进入等待状态,释放对象锁,而notify()notifyAll()方法则用于唤醒处于等待状态的线程。

运行结果:

Sender: Message 1
Receiver: Message 1
Sender: Message 2
Receiver: Message 2
Sender: Message 3
Receiver: Message 3
Sender: END
Receiver: END

3️⃣ 使用场景

Object类是Java中所有类的父类,它是一个通用的、基础的对象类型。因此,使用Object类的场景非常广泛。下面是一些使用Object类的常见场景:

1、 泛型:当我们需要定义一个可以存储任意类型对象的集合或容器时,可以使用Object类作为元素的类型通过将对象添加到该集合中,我们可以在需要时将其取出并进行合适的类型转换;
2、 方法参数和返回值:在某些情况下,我们需要编写一个可以接受和返回不同类型对象的方法这种情况下,我们可以使用Object类作为参数类型或返回值类型然后,在方法内部可以使用强制类型转换操作来处理具体类型的对象;
3、 equals()和hashCode()方法重写:当我们需要比较两个对象是否相等时,可以通过重写equals()方法来实现自定义的相等规则在equals()方法内部,通常会使用instanceof关键字检查对象的类型以及类型转换操作同时,为了正确地使用对象作为Map的键或集合的元素,我们还需要重写hashCode()方法;
4、 适配器模式:当我们需要将一个特定类型的对象转换成Object类型时,可以使用适配器模式适配器模式允许我们把一个类的接口转换成客户希望的另一个接口,从而使得原本不兼容的类能够协同工作;
5、 多态性:当面对不同类型的对象时,可以使用Object类来实现多态性通过将具体类型的对象赋值给Object类型的变量,可以实现对不同对象的一致操作;

总之,Object类的使用场景非常广泛,它提供了一种通用的方式来处理和操作任意类型的对象。

* 总结

Java的Object类是所有类的根类,提供了一系列通用的方法,用于操作和比较对象。

本文介绍了Object类的基本方法,包括equals()hashCode()toString()finalize()等。此外,还提到了其他重要方法,如getClass()clone()。以及包括wait()notify()notifyAll(),用于实现线程之间的同步和协作。

熟悉Object类的使用对于Java开发者来说是非常重要的,在编写和理解Java代码中都会经常用到Object类的相关方法。通过深入了解Object类,我们可以更好地利用Java面向对象编程的特性。

* 本文源码下载地址

Java的Object类讲解案例代码 equals()、hashCode()、finalize()、clone()、wait()
*


[* ]nbsp_nbsp 2