【重难点】【Java基础 03】重写hashCode() 和equals()、

文章目录

  • 【重难点】【Java基础 03】重写hashCode() 和equals()、
  • 一、hashCode() 和 equals()
    • 1.对比
  • 二、代理模式
    • 1.介绍
  • 参考链接

一、hashCode() 和 equals()

1.对比

我们来看看官方文档对这两个方法的解释

官方文档对这个规范翻译得会有点拗口,我们来简化一下

  1. hashCode 和 equals 返回值应该是稳定的,不应该具有随机性
  2. 两个对象用运算符 == ,如果结果为 true,则对这两个对象用 equals() 也应该返回 true
    事实上,如果没有重写 equals(),它的源码就只有一句:return (this == obj);其中 obj 是形式参数
  3. 如果对两个对象用 equals() 返回 true,那么这两个对象的 hashCode 也应该相等
    equals() 方法的源码我点进去看的时候发现没有方法体(后来知道 hashCode() 是一个原生函数),我想知道的是 equals() 在重写之前,到底返回的是什么。好在后来找到了:hashCode() 返回什么具体要看 JVM 是怎么处理的,而 JVM 版本不同返回的东西也不同。确实有的 JVM 是直接返回对象的存储地址,但是大多数情况不是这样的,甚至和存储地址毫无关联,比如说 HotSpot 返回的就不是内存地址,OpenJDK 里计算 hashCode() 也根本没用到存储地址
    HotSpot 不是什么陌生的版本,我们电脑装的就是 HotSpot,也是目前绝对的主流版本,其他还有 J9 VM 、JRockit 等等
    当大家说起“Java性能如何如何”、“Java有多少种GC”、“JVM如何调优”云云,经常默认说的就是特指 HotSpot VM
  4. == 比较的是两个对象的地址

解释一下什么原生函数,一个方法是原生函数,也就是说这个方法的实现不是用 Java 语言实现的,而是使用 C/C++ 实现的,并且被编译成了 DDL(Dynamic Link Library,动态链接库),由 Java 去调用,而 JDK 源码中并不包含。对于不同的平台它们是不同的,Java 在不同的操作系统调用不同的 native 方法实现对操作系统的访问,因为 Java 语言没有指针,所以不能直接访问操作系统底层
这种方法调用的过程:

  1. 在 Java 中声明 native 方法,然后编译
  2. 用 javah 产生一个 .h 文件
  3. 写一个 .cpp 文件实现 native 导出方法,其中需要包含第二步产生的 .h 文件(其中又包含了 JDK 带的 jni.h 文件)
  4. 将 .cpp 文件编译成动态链接库文件
  5. 在 Java 中用 System.loadLibrary() 文件加载第四步产生的动态链接库文件,然后这个 native 方法就可被访问了

对于 hashCode() 还有一些比较冷门的知识:

  1. 默认情况下,对象的 hashCode 方法返回值永远大于等于 0
  2. 默认情况下,对象的 hashCode 方法返回值不是对象的地址
  3. hashCode 相同,不一定是同一个对象,必须限定为同一个类

什么情况下需要重写 hashCode() 和 equals() ?

当我们希望两个对象的某些属性值相同就认为他们是相同对象时,而不是严格要求它们完完全全就是同一对象,比如说两个字符串,我们只需要它们的值相同,就可以认为他们相等。因此我们要重写 equals()

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

这是由于 Java API 文档里的规范,虽然不按照规范不会报检查性异常,但是没有特殊情况最好遵守

如何重写才能符合规范?

首先是重写 equals(),需要进行以下三步

  1. 判断是否等于自身
  2. 使用 getClass() 判断传入对象是否为同类型的对象
  3. 比较类中定义的字段,根据自己的需要,判断是否相等,只有全部相等时才能认为两个对象相等

然后是重写 hashCode(),只需要将 equals() 里选择的字段作为参数,传入到 hash() 方法中计算哈希值并返回就好了

实例

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

二、代理模式

1.介绍

你可能会对这个设计模式感到陌生,但是你对 Spring 一定不陌生,而 Spring AOP 就是代理模式的一种实现,因此它的重要性不言而喻

简而言之,代理模式是设置一个中间代理来控制访问被代理对象,以达到增强源被代理对象的功能和简化访问方式

代理模式 UML 类图

代理模式分为静态代理和动态代理,其中动态代理又分为 JDK 代理和 CGB 代理

1、静态代理

静态的代理在使用时,需要定义一个接口,然后让被代理对象和代理对象一起实现这个接口

下面举个例子,设计一个 Tank 类作为代理类,再设计两个代理类,以达到可以在不修改被代理类的情况下为其添加日志和计时功能

第一步,定义一个接口

interface Movable{void move();
}

第二步,定义一个被代理类

class Tank implements Movable{@Overridepublic void move(){System.out.println("Moving......");}try{Thread.sleep(new Random().nextInt(10000));}catch(InterruptedException e){e.printStackTrace();}
}

第三步,定义一个代理类

class TankTimeProxy() implements Movable{Movable m;public TankTimeProxy(Movable m){this.m = m;}@Overridepublic void move(){long start = System.currentTimeMillis();super.move();long end = System.currentTimeMillis();System.out.println("运行了 " + end - start + " ms");}
}

第四步,再定义一个代理类

class TankLogProxy{Movable m;public TankLogProxy{this.m = m;}@Overridepublic void move(){System.out.println("start...");m.move();System.out.println("stopped!");
}

第五步,写 main 方法

public static void main(String args[]){new TankLogProxy(new TankTimeProxy(new Tank;)).move();
}

对于代理类来讲,它们都实现了 Movable 接口并且聚合了一个 Movable 对象,它们能代理的类型也实现了 Movable 接口,这是它们能够互相嵌套的前提

在 main 方法里,我们先 new 一个被代理类,然后把这个被代理类作为参数 new 一个 代理类,然后再把这个被代理类作为参数 new 另一个代理类,最后再调用 move() 方法,这样就实现了在不修改被代理类的情况下为其添加日志和计时功能

现在来思考一个问题,如果我想让 LogProxy 可以重用,不只是用来代理 Tank,而是让它可以代理其他任何类型

但是静态代理只能代理继承了 Movable 接口的被代理类,那样的话就只能继承 Object 类,但是那样就很不方便

这个问题从本质上来讲,是想实现代理行为和代理对象分离。但是我们之前把代理对象写死了,只能代理实现 Movable 接口的类

现在我们希望能代理各种各样的类,如果仍然使用静态代理的话就很麻烦,因为无从得知即将代理的是什么类,也无从得知类的方法有哪些。之前的 Tank 类,我们知道它实现了 Movalbe 接口,所以我们知道它有一个 move 方法,所以我们能通过重写 move 方法然后嵌套,这样就可以为 move 方法扩展新的功能

现在的问题就是我们不知道被代理类实现了什么接口,万一被代理没有实现接口该怎么办

这个时候我们就需要动态代理

在介绍动态代理之前我们现总结一下静态代理的问题:

  1. 代码冗余,由于代理类要实现与被代理对象一致的接口,所以会产生过多的代理类
  2. 不易维护,一旦接口增加方法,被代理类和代理类都要进行修改

2、动态代理

动态代理是什么意思呢?就是说代理类的代码不是我们自己写的,而是在运行时后台动态生成的

动态代理有很多种实现方式,我们先从 JDK 代理入手

我们仍然举 Tank 类的例子

前两不是定义接口和定义 Tank 类,和静态代理一致,这里就不重复写了

第三步,定义一个调用时处理器,里面写需要扩展的功能

class LogHandler implements InvocationHandler{Tank tank;public LogHandler(Tank tank){this.tank = tank;}@Overridepublic Object invoke(Object proxy,Method method,Object[] args) throw Throwable{System.out.println("Method" + method.getName() + "start...");Object o = method.invoke(tank,args);System.out.println("method" + method.getName() + "end!");return o;}
}

第四步,写 main 方法

public static void main(String argps[]){Tank tank = new Tank();//创建代理实例//静态代理是明确生成了一个 TankLogProxy 对象//这里的 newProxyInstance 相当于是直接在运行时生成了一个 TankLogProxy,连代理类都不用我们写,只需要写扩展功能//newProxyInstance() 有三个参数//1、ClassLoader loader 你要使用哪一个 ClassLoader,使用把你 new 出来的代理对象加载到内存时得 ClassLoader 即可//2、Class<?> [] interfaces 你希望代理类实现哪些接口//3、InvocationHandler h 调用时处理器Movable m = (Movable)Proxy.mewProxyInstance(Tank.class.getClassLoader(),new Class[]{Movable.class},new LogHandler(tank));m.move();

运行结果:

method move start…
Moving…
method move end!

这里有一个奇怪的现象,我们认真地看一下 main 方法,我们会发现,没有任何语句调用了 InvocationHandler 的 invoke 方法,我们只调用了 move 方法,但是 invoke() 里写的日志功能也被打印出来了

因此我们可以合理地推测,是我们在调用 move() 的过程中,在一个我们看不见的地方隐式地调用了 invoke 方法

我一步一步地解释一下整个过程

在执行 newProxyInstance()方法时,会生成一个 $Proxy() 类的.class 文件,这个类就是根据我们传入的参数按照一定规则生成的代理类

这个代理类继承了一个父类叫 Proxy,这个父类里有一个聚合对象叫 InvocationHandler h,跟我们的第三个参数是同一类型的

这个父类的构造函数是有参构造,传入的参数为 InvovationHandler h,并且令 this.h = h,也就是用这个参数给聚合对象 h 赋值

代理类作为它的子类也有一个有参构造,参数为 InvocationHandler var1,只是它不像它的父类有一个 InvocationHandler h

它的构造函数里只有一句 super(var1),即让 var1 作为参数调用父类构造

这样,父类的聚合对象 h 就被指定为我们传入的第三个参数,也就是我们自己定义的调用时处理器

我们再来仔细分析一下我们调用的 move() 是哪个类的方法?是被代理类的吗?

显然不是,我们创建的 m 对象实际上是 $Proxy0 的实例,因此我们调用的是 $Proxy0 的方法

这样就破案了,原来是 $Proxy0 做的手脚,它在自己的 move 方法里调用了 InvcationHandler 的 invoke 方法

因此,我们的扩展功能得以实现

动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。动态代理除了通过 JDK 反射来实现之外还有其他各种各样的方式

比如 Instrument,它是利用一个钩子函数,在 class 加载到内存之前将其拦截,并且对其进行一些定制,使其具有扩展功能
但是这种方式用的太少了,因为太过复杂,你必须理解 class 文件的二进制码中的每一个 0 和 1 的实际含义

在介绍 cglib 之前,我们总结一下 JDK代理和静态代理的区别:

  1. 静态代理在编译时就已经实现,编译完成后代理类是一个实际的 class 文件
  2. 动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时动态生成类字节码,并加载到 JVM 中。而且,动态代理类不需要实现接口,但是要求被代理类要实现接口

还有一种常用且简单的方式,cglib(Code Generation Library ),也叫子类代理

cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口

它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 dynaop,为他们提供方法的 interception(拦截)

cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类

不鼓励直接使用 ASM,因为它需要你对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉

第一步,定义一个被代理类,不实现任何接口

class Tank{public void move(){System.out.println("Moving......");try{Thread.sleep(new Random.nextInt(10000));}catch(InterruptedException e){e.printStackTrace();}}
}

第二步,定义一个拦截器,扩展功能

class TimeMethodInterceptor implements MethodInterceptor{@Overridepublic Object intercept(Object o,Method method,Object[] objects,MethodProxy methodProxy){System.out.println("before");Object result = null;result = methodProxy.invokeSuper(o,objects);System.out.println("after");return result;}
}

第三步,写 main 方法

public static void main(String args[]){Enhancer enhancer = new Enhancer();enhancer.setSuperclass(Tank.class);           //把 enhancer 的父类设置为 Tank//设定回调函数,TimeMEthodInterceptor 是一个拦截器,相当于 InvocationHandlerenhancer.setCallback(new TimeMethodInterceptor());Tank tank = (Tank)enhancer.create();        //生成动态代理tank.move();                                //调用 move(),同时会调用到 intercept() 方法,执行我们的操作

我们来思考一下,使用 cglib 确实简单,但是它是怎么实现的呢

enhancer.setSuperclass(Tank.class);

我们关注一下这条语句,我们可以知道生成的动态代理类是被代理类的一个子类

cglib 代理就是在内存中构建一个子类对象,并对子类进行增强,从而实现对被代理类功能的扩展

具体一点,就是生成了一个被代理类的子类,那么就可以重写被代理类的方法,然后就能实现对被代理类功能的扩展

我们总结一下 cglib 代理和 JDK 代理的区别:

  1. JDK 代理的类必须实现接口
  2. cglib 代理的类无需实现接口,但是不能是 final 类,否则无法生成子类

总结

  1. 静态代理实现简单,只要代理类对被代理类进行包装即可。静态代理在编译时生成 class 字节码文件,可以直接使用,效率高。但是静态代理只能为一个被代理类服务,如果被代理类过多,就会产生很多代理类,从而导致代码冗余。其次不易维护,如果接口改变,被代理类和代理类都需要修改
  2. JDK 代理,只有被代理类需要实现接口,而代理类只需要实现 InvocationHandler 接口。但是 JDK 代理需要使用反射,比较消耗系统性能。
  3. cglib 代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题。但是 cglib 需要继承被代理类重写方法,因此被代理类不能被 final 修饰

参考链接

结合以下链接理解,会有更加深入的认知

5分钟理解 hashCode() 和 equals()
Java Object.hashCode()返回的是对象内存地址?
目前主流的 Java 虚拟机有哪些?
Java Object.hashCode()返回的是对象内存地址?
为什么要重写hashCode()方法和equals()方法以及如何进行重写

70分钟入门代理模式
100分钟强化代理模式概念
CGLIB动态代理
Java的三种代理模式
Java三种代理模式:静态代理、动态代理和cglib代理
面试怎么回答代理模式

【重难点】【Java基础 03】hashCode() 和 equals()、代理模式相关推荐

  1. php byte stringbuffer,重拾java基础(十三):String姐妹StringBuffer、StringBuilder总结

    重拾java基础(十三):String姐妹StringBuffer.StringBuilder总结 一.StringBuffer类概述buffer:缓冲 2. 字符串缓冲区,跟String非常相似,都 ...

  2. [重学Java基础][Java IO流][Exter.2]IO流中几种不同的读写方法的区别

    [重学Java基础][Java IO流][Exter.2]IO流中几种不同的读写方法的区别 Read 读入方法 read(): 一般是这种形式 public int read() 1.从流数据中读取的 ...

  3. Java基础-Integer的==和equals方法

    Java基础-Integer的==和equals方法 1.首先说下 equals 方法: ​ equals 方法接受的参数为 Object 类型 equals(Object obj),首先会判断参数中 ...

  4. 【重难点】【JUC 02】volitale 常用模式 、JUC 下有哪些内容 、并发工具类

    [重难点][JUC 02]volitale 常用模式 .JUC 下有哪些内容 .并发工具类 文章目录 [重难点][JUC 02]volitale 常用模式 .JUC 下有哪些内容 .并发工具类 一.v ...

  5. java基础语法(二)--单列模式

    java基础语法(二)--单列模式 /*** 功能:单列模式* @author Administrator**/ public class SingletonTest {public static v ...

  6. 【Java】接口使用原则及代理模式

    接口: 开发原则:接口优先原则,在一个场景既可以使用抽象类也可以使用接口的时候,优先考虑使用接口. 定义:接口中只有全局常量和抽象方法(JDK8之前),接口使用interface定义接口. 2.使用原 ...

  7. java中的静态、动态代理模式以及Spring中的CgLib动态代理解读(面试必问)

    java中的静态.动态代理模式以及Spring中的CgLib动态代理解读(面试必问) 静态代理 动态代理 CgLib动态代理     基础知: 反射知识 代理(Proxy)是一种设计模式,提供了对目标 ...

  8. Java设计模式之结构型:代理模式

    前言: 我们一般在租房子时会去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做:再比如我们打官司需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们 ...

  9. Java基础提升篇:equals()与hashCode()方法详解

    概述 java.lang.Object类中有两个非常重要的方法: public boolean equals(Object obj) public int hashCode() Object类是类继承 ...

最新文章

  1. 下列哪项不属于以太网交换机的特点_网络测试作业题
  2. java投票输出票数最高前三名,给你喜欢的作品投票,票数前三名获本平台赠送书一本。|诗花朵朵...
  3. c# 链接mongDB集群实战开发
  4. dms系统与mysql_关于MySQL与DMsql探寻
  5. 版本控制系统优缺点比较
  6. C#如何测试代码运行时间
  7. 荣耀变鸿蒙系统,鸿蒙系统首批升级机型曝光!荣耀手机遗憾缺席,原因很简单...
  8. 制作 小 linux 教程,【NanoPi NEO Plus2开发板试用体验】编译uboot和linux制作最小根文件系统制作刷机包---详细教程...
  9. SQL HQL JPQL CQL的对比
  10. [收藏]ASP数据库操作类(上)
  11. 从零实现深度学习框架——逻辑回归中的数值稳定
  12. 商业银行会计学内容概述
  13. 【AUTOSAR-COM】-10.4-发送的IPDU Callout(Com_TxIpduCallout)的使用小结
  14. MySQL数据库 1067号错误的解决办法
  15. 2021 谷歌游戏出海峰会精彩内容回放 | 跨界破圈 赢在未来
  16. CentOS 7下载地址(ISO文件)安装
  17. 何为五笔输入的最高境界?
  18. 在 Windows 10 中更改默认浏览器
  19. 来自飞机座椅的实测数据
  20. 成都天瑞地安谈VR虚拟增强技术能否取代修图软件的意见

热门文章

  1. matlab第七章符号对象,MATLAB语言:第七章 MATLAB符号计算
  2. 平常代码练习报错问题解决
  3. JVM 一套卷,助你快速掌握优化法则
  4. Linux下chkconfig命令介绍
  5. C++ 模板何时被实例化
  6. 分享经验,让更多的人受益
  7. 患者数据库mysql_关系型数据库之MySQL基础总结_part1
  8. (80)FPGA面试题-请画出序列“1101 “检测状态转移图
  9. (100)FPGA RAM实现(V实现)
  10. (69)Verilog HDL测试激励:时钟激励2