第十六讲 java进阶-重写equals方法的深度解读
1 接口的补充
-
接口为什么不能new对象?有没有必要new对象?或者是说如果能new对象,new出来的是什么东西?有什么内容,是否符合对象的特征?
- 接口中属性只能是常量,常量并不是对象的成员属性。接口中的方法只能是抽象的方法,没有方法体。
- 对象要有成员属性,每个对象的成员属性值都不一样。要有成员方法,每个对象的成员方法,对象的引用都能调用。
- 如果接口能new对象,那么接口对象的属性是没有的,接口对象的方法都是不能执行的。它违背对象的定义。
- 接口的本身就是一个抽象的定义。
- 抽象类为什么也不能new对象呢?道理是一样的。一旦抽象类中有抽象方法之后,这个方法不能被对象的引用调用。
-
关于接口的应用:
public interface IOpen
{
// 所有的遥控器都有一个开关电视的功能。遥控器上有开关。能打开电视
void open();
}
public interface ITV
{
// 所有的电视都能播放
public void play();
}
public class YaoKongQi implements IOpen
{
// 万能遥控器
// 可以打开小米牌电视,可以打开长虹牌电视,可以打开任何电视
private ITV itv;//昨天说的接口也是引用类型
// ITV itv,这是接口类型的引用,也是引用类型,我们可以看做是父类型的引用。
// 继承:就是扩展。接口说的实现,实现的本质也是扩展。所以,你可以将接口看成是父类。
// 这就满足了多态的机制。接口类型的引用指向接口实现类类型的对象。
// 这也符合多态的语法机制。
public void setTV(ITV itv) {
this.itv = itv;
}
public void open() {
itv.play();
}
}
public class XiaoMiTV implements ITV
{
public void play() {
System.out.println("小米电视play!!");
}
}
public class ChangHongTV implements ITV
{
public void play() {
System.out.println("长虹电视Play");
}
}
public class Test
{
public static void main(String[] args)
{
XiaoMiTV xmtv = new XiaoMiTV();
YaoKongQi ykq = new YaoKongQi();
ykq.setTV(xmtv);
ykq.open();
ChangHongTV chtv = new ChangHongTV();
ykq.setTV(chtv);
ykq.open();
}
}
// 面向对象编程的时候,我们要面向抽象,不要面向具体。要面向接口,不要面向具体的实现。
- 接口隔离
- 我们知道,接口定义之后,实现类一定要实现接口中所有的抽象方法。
- 问题来了,如果一个接口,有5个方法,它的某一个实现类只要用到其中的一个,那么另外4个也一定要实现,否则编译都过不去。那么就会造成代码的冗余。
- 所以,接口要隔离,接口的功能要单一。
2 API 和 API帮助文档
- 什么是API,什么是API帮助文档
- API:application programming Interface 说白了API就是SUN公司提供的java源代码
- API帮助文档,是通过javadoc指令,将java源代码中/** */注释的内容提取出来,形成了这个帮助文档。
- 以后,我们要用的时候,是直接看API还是用帮助文档?文档
- 以后我们开发怎么使用SUN公司提供的API呢?导包、调用方法。导包,创建实例(有的甚至不用),然后“.”方法。
3 Object类探究(equals方法深度解读)
@Override : 这是一个注解。加上这个注解的,说明这个方法是父类中的方法,被子类重写了。这个方法的原型在父类中,子类重写了父类的方法。
- 我们写一个User类,将它作为一个实体类,具体的写法如下:
package com.demo01.pojo;
import java.util.Objects;
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
// 测试类
public class Demo01 {
public static void main(String[] args) {
User user = new User(100, "张三" );
System.out.println(user);
User user1 = new User(100, "张三");
System.out.println(user1);
System.out.println(user.equals(user1));// ?不重写 false 重写后 true
// public boolean equals(Object obj) {
// return (this == obj);
// }
//com.tj.demo01.pojo.User@16d3586
// 我们想要看看user这个对象在内存中到底有哪些属性和值,
// 但是父类中的toString给我们提供的是com.tj.demo01.pojo.User@16d3586
// 这不是我们想要的,因为我们根本看不懂这个东西User@16d3586
// 你能判断赋值成功了吗?对象的成员属性有值还是没值,值是多少?这些是我们能看懂的
// 怎么办?
// 重写父类的toString()方法。
}
}
如果我们不重写父类Object中的toString()方法,那么在测试类中我们希望打印对象的引用,所得结果如下:com.tj.demo01.pojo.User@16d3586
对我们而言,我们希望看到对象的属性和赋值情况,而Object类中的toString()方法只是告诉我们对象创建成功。这不符合我们的预期,因此我们需要在User类中重写toString()方法。
在Object类中有很多native修饰的方法,其具体的实现是用C++实现,具体的实现方式可以找资料阅读。但其提供的方法是可以直接调用的。
toString()方法的重写,我们可以通过IDEA来自动生成,也可以根据我们需要的方式进行重写。
Object类是所有类的superclass,因此该类中的所有成员方法都是其他类所产生的对象直接调用的。如果不符合我们的预期,我们可以对其进行重写。
重点介绍Object类中的equals()方法:
Object类中提供的equals(Object obj)方法,具体的实现如下:
public boolean equals(Object obj) {
return (this == obj);
}
// equals方法的目的是给我们提供比较两个对象是否是一个对象的工具
// Object类中原生的方法实现只能比较两个对象的引用是否相等
// 引用如果相等,说明两个引用指向了同一个对象,这当然是同一个对象
// 如果,一个类的两个对象,拥有同样的属性值,
// 虽然引用不相等,我们也需要将其认定为是相同的对象,是同一个对象
// 那么,Object类中的原生的方式就无法满足我们的需求,因此需要重写
// 一下是重写的equals方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id &&
Objects.equals(name, user.name);
}
// 以上代码满足了我们上述的需求,两个不同引用指向的对象,
// 拥有相同的属性值我们也认定为是相同的对象
// 就好比,一个人的身份证号 和 姓名 与另一个人的身份证号、姓名完全一致
// 我们就可以断定这是同一个人,虽然他可能出现在上海,过段时间出现在北京
// 不管是在上海还是在北京,都是同一个人无疑
// 以上代码要如何阅读呢?
// 第一步:我们看返回值类型,boolean 最后的结果一定是布尔值,即相等为true,不等为false
// 第二步:我们看参数列表,形参是Object o,
// 我们要比较的是User类型的对象,传入的实参一定是User类型的对象引用
// 这里就有多态的语法机制,因为User是Object的子类。
// 即:传参的时候是这样做的:Objcet o = new User();
// 这是父类型的引用指向了子类型的对象,这一点至关重要。
// 那么在equals(Object o)方法被调用的时候,实际上传入的是:
// 指向了子类型User对象的父类型引用Object o
// 该equals(Object o)是成员方法,该方法中还有一个隐藏的参数this,是第一个形参
// 它的原型是这样的equals(Object this, Object o);
// 方法的调用者给这个this传实际值,然后同Object o所指向的类型进行比较
// 第三步:我们看方法体。方法体中第一条语句:if (this == o) return true;
// 这就是Object类中原生的equals()方法,它只是判定了两个对象的引用是不是同一个
// 如果是,就表示是同一个对象,这毫无疑问。
// 栈(堆)内存中的两个引用类型的变量指向了堆内存中的同一个对象,
// 这肯定是同一个对象,好比一个人书名叫张三,它的小名叫二狗子,都是同一个人。
// 第二条语句:if (o == null || getClass() != o.getClass()) return false;
// 如果传入的实参为null,为空意味着它还没有指向堆内存中的对象,这当然没得比。
// 如果不为null,说明堆中已经有了一个对象,
// 如果this所代表的对象的类型和传入的实参对象的类型不一样,
// 都不是一个类,那也没得比较。
// 第四步:最重要也是最难懂的一步:User user = (User)o;
// 看懂这一步,说明对向下转型、多态都有了一定的理解。
// 为什么能这么转型?回答清楚这个问题,这个方法的解读就毫无压力了。
// 首先,我们要知道,o所指向的是子类型的对象,因为O是传入的实参
// 我们在第一步中解释了它的由来。Object o = new User();
// 父类型的引用指向了子类型的对象,这是向上转型,自动类型转换。
// 只有在这个前提下,我们才能进行向下转型,也就是说才能将o这个父类型的引用
// 转为子类型的引用,否则无法转。
// User user = (User)o;
// 这里还要回答另一个问题,就是ClassCastException异常为什么不会发生?
// 我们没有使用instanceof来判定,但这段代码中确保了异常是不会发生的。
// 因为,第三条语句保证了,如果不是一个类型,方法就直接返回false,结束了。
// 所以,我们不用考虑类型的问题。
// 也就是说在类型转换异常的问题处理上,我们又多了一个思路。
// 当然,对于初学者来说,这段代码略显得有些高深。后面再详细讲解。
// 第五步:return id == user.id && Objects.equals(name, user.name);
// 这是最后一条语句,使用了短路与&&
// 这条语句的前局很简单,就是比较两个整型的数据是否相等。
// 如果User这个对象的id属性相等,我们就进行下一步:
// 比较两个String类型的name属性是否相等
// 这一步也蕴含了很多信息,首先我们可以断定,这里调用了一个静态的equals()方法
// 该方法是Objects类中的,是另外一个类。
// 这个类是一个工具类,java.util包下的。因为直接"类名."的方式调用的equals方法
// 经查实,确实是这样的。
// equals(name, user.name)这个方法的原型如下:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
// 我们依然看到了这里的两个形参是两个Object类型的引用
// name 和 user.name作为实际参数值传进来,是怎么回事?
// 这一步理解了,User类中重写的equals()方法阅读也就完成了
// 首先,name 和 user.name是String类型的,String类型也是一个引用类型
// String没有显式的继承任何类,它的父类是Object
// 这里的这种写法,根本上就是父类型的引用指向子类型对象
// 即 Object a = new String(name);
// 所以如果a,b代表的实参都指向了同一个String类型的对象
// 仔细想,这里a==b指的是a、b同时为空,如果同时为空,id又相等
// 那两个User类型的对象当然是同一个
// 如果a不为空,那么我们再看看,a是否与b相同,
// 这里调用的equals方法本质上调用的是String中重写的equals方法。
// 这就是多态的机制。如果想要了解这里如何做的比较,可以去看String类中
// 重写的equals方法。这里不做赘述。
// 再次解释一下:这里为何调用的是String的equals方法,
// 因为运行时,会先去绑定子类型中重写的方法。
// 补充String中重写的equals方法如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}