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

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

一、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{
	@Override
	public 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;
	}

	@Override
	public 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;
	}
	
	@Override
	public 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;
	}
	
	@Override
	public 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{
	@Override
	public 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 是一个拦截器,相当于 InvocationHandler
	enhancer.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代理
面试怎么回答代理模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

313YPHU3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值