前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在Java 中安全使用接口引用

在Java 中安全使用接口引用

作者头像
小鄧子
发布于 2019-03-05 07:24:20
发布于 2019-03-05 07:24:20
2.1K00
代码可运行
举报
运行总次数:0
代码可运行

Photo by Joseph Maxim Reskp on Unsplash

我使用Java 开发过很多项目,这其中包括一些Web 应用和Android 客户端应用。作为Android 开发人员,Java 就像我们的母语一样,但Android 世界是多元化的,并不是只有Java 才能用来写Android 程序,Kotlin 和Groovy 同样优秀,并且有着大量的粉丝。我在过去的一年中尝试学习并使用它们,它们的语法糖让我爱不释手,我尤其对?. 操作符感到惊讶,它让我写更少的代码,就能够避免空指针异常(NPE)。可惜的是Java 中并没有提供这种操作符,所以本文就和大家聊聊如何在Java 中构造出同样的效果。

由于源码分析与调用原理不属于本文的范畴,只提供解读思路,所以本文不涉及详细的源码解读,仅点到为止。本文所涉及的项目已经开源:interface-buoy

接口隔离原则

软件编程中始终都有一些好的编程规范值得我们的学习:如果你在一个多人协作的团队工作,那么模块之间的关系就应该建立在接口上,这是降低耦合的最佳方式;如果你是一个SDK 的提供者,暴露给客户端的始终应该是接口,而不是某个具体实现类。

在Android 开发中我们经常会持有接口的引用,或者注册事件的监听,诸如系统服务的通知,点击事件的回调等,虽不胜枚举,但大部分监听都需要我们去实现一个接口,因此我们今天就拿注册一个回调监听举例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  private Callback callback;

  public void registerXXXX(Callback callback) {
    this.callback = callback;
  }
  
  ......
  
  public interface Callback {
    void onXXXX();
  }

当事件真正发生的时候调用callback 接口中的相应函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
......

 if (callback != null) {
   callback.onXXXX();
}

这看起来并没有什么问题,因为我们平时就是这样书写代码的,因此我们的项目中存在大量的对接口引用的非空判断,即使有参数型注解@NonNull 的标记,但仍无法阻止外部传入一个null 对象。

说实话,我需要的无非就是当接口引用为空的时候,不进行任何的函数调用,然而我们却需要在每一行代码之上强行添加丑陋的非空判断,这让我的代码看起来失去了信任,变得极其不可靠,而且繁琐的非空判断让我感到十分疲惫 : (

使用操作符 ' ?. '

Kotlin 和Groovy 似乎意识到了上述尴尬,因此加入了非常实用的操作符:

?. 操作符只有对象引用不为空时才会分派调用

我们接下来分别拿Kotlin 和Groovy 举例:

在Kotlin 中使用 ' ?. ' :
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  fun register(callback: Callback?) {
    
    ......

    callback?.on()
  }

  interface Callback {
    fun on()
  }
在Groovy 中使用 ' ?. ' :
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  void register(Callback callback) {

    ......

    callback?.on()
  }

  interface Callback {
    void on()
  }

可以看到使用?. 操作符后我们再也不需要添加if (callback != null) {} 代码块了,代码更加清爽,所要表达的意思也更简明扼要:如果callback 引用不为空则调用on() 函数,否则不做任何处理

我们将在下一个章节介绍操作符 ' ?. ' 的实现原理。

反编译操作符 ' ?. '

我始终相信在代码层面没有所谓的黑魔法,更没有万能的银弹,我们之所以能够使用语法糖,一定是语言本身或者框架内部帮我们做了更复杂的操作。

于是我们现在可以提出一个假设:编译器将操作符?. 优化成了与if (callback != null) {} 效果相同的代码逻辑,无论是Java,Kotlin 还是Groovy,在字节码层面均表现一致

为了验证假设,我们分别用kotlinc 和groovyc 将之前的代码编译成class 文件,然后再使用javap 指令进行反汇编。

编译/反编译KotlinTest.kt
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# $ kotlinc KotlinTest.kt
# $ javap -c KotlinTest.kt

Compiled from "KotlinTest.kt"
public final class KotlinTest {
  public final void register(KotlinTest$Callback);
    Code:
       0: aload_1
       1: dup
       2: ifnull        13
       5: invokeinterface #13,  1           // InterfaceMethod KotlinTest$Callback.on:()V
      10: goto          14
      13: pop
      14: return
    
    ......

}

通过分析register() 函数体中的所有JVM 指令,我们看到了熟悉的ifnull 指令,因此我们可以很快地将代码还原:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  fun register(callback: Callback?) {
    if (callback!=null){
      callback.on()
    }
  }

kotlinc 编译器在编译过程中将操作符?. 完完全全地替换成if (callback != null) {} 代码块。这和我们手写的Java 代码在字节码层面毫无差别。

编译/反编译GroovyTest.groovy
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# $ groovyc GroovyTest.groovy
# $ javap -c GroovyTest.class

Compiled from "GroovyTest.groovy"
public class GroovyTest implements groovy.lang.GroovyObject {

  public void register(GroovyTest$Callback);
    Code:
       0: invokestatic  #19                 // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
       3: astore_2
       4: aload_2
       5: ldc           #32                 // int 0
       7: aaload
       8: aload_1
       9: invokeinterface #38,  2           // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.callSafe:(Ljava/lang/Object;)Ljava/lang/Object;
      14: pop
      15: return

    ......

}

需要注意的是,groovy 文件在编译过程中由编译器生成大量的不存在于源代码中的额外函数和变量,感兴趣的朋友可以自行阅读反编译后的字节码。此处为了方便理解,在不影响原有核心逻辑的条件下做出近似还原:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public void register(GroovyTest.Callback callback) {

    String[] strings = new String[1]
    strings[0] = 'on'

    CallSiteArray callSiteArray = new CallSiteArray(GroovyTest.class, strings)
    CallSite[] array = callSiteArray.array

    array[0].callSafe(callback)
  }

其中CallSite 是一个接口,具体实现类是AbstractCallSite ,:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class AbstractCallSite implements CallSite {

    public final Object callSafe(Object receiver) throws Throwable {
        if (receiver == null)
            return null;

        return call(receiver);
    }

  ......

}

函数AbstractCallSite#call(Object) 之后是一个漫长的调用过程,这其中包括一系列重载函数的调用和对接口引用callback 的代理等,最终得益于Groovy 的元编程能力,在标准GroovyObject对象上获取meatClass ,最后使用反射调用接口引用的指定方法,即callback.on()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
callback.metaClass.invokeMethod(callback, 'on', null);

那么回到文章的主题,在AbstractCallSite#call(Object) 函数中我们可以看到对receiver 参数也就是callback 引用进行了非空判断,因此我们可以肯定的是在Groovy 中操作符?. 和Kotlin 是如出一辙的,这也恰好印证了本段开头的猜想:

编译器将?. 操作符编译成亦或在框架内部调用与if (callback != null) {} 等同效果的代码片段。Java,Kotlin 和Groovy 在字节码层面的处理方式基本相同

为Java 添加' ?. ' 操作符

事情变得简单起来,我们只需要为Java 添加?. 操作符即可

其实与其说为Java 添加?. 操作符不如说是通过一些小技巧达到相同的处理效果,毕竟改变javac 的编译方式成本较大。

面向接口的编程方式,使我们有天然的优势可以利用,动态代理正是基于接口,因此我们可以对接口引用添加动态代理并返回代理后的值,这样callback 引用实际指向了动态代理对象,在代理的内部我们借助反射调用callback 引用中的对应函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  private void register(Callback callback) {
    callback = ProxyHandler.wrap(callback);

    ......

    callback.on();
  }


public static final class ProxyHandler {

  public static <T> T wrap(final T reference) {
    Class<?> clazz = reference.getClass();

    if (clazz.isInterface()) {
      return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
          new InvocationHandler() {
            @Override public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
              if (reference == null) return null;
              return method.invoke(reference, args);
            }
          });
    }
    return reference;
  }
}

通过这样的一层代理关系,我们可以在callback 上安全的使用任何函数调用,而不必关心空指针的发生。也就是说,我们在Java 上通过使用动态代理加反射的方式,构造出了一个约等于?. 操作符的效果

Android gradle plugin (AGP)

我们发现每次使用前都需要手动添加代理关系实在麻烦,能否像javac 或者kotlinc 那样在编译过程或者构建过程中使用自动化的方式代替手动添加呢?

答案是肯定的:构建过程中修改字节码!

通过观察字节码的规则,了解到调用Java 接口中声明的方法使用的是invokeinterface 指令,因此我们只需要找到函数体中invokeinterface 指令所在的位置,在前面添加对接口引用的动态代理并返回代理结果的相关字节码操作。

使用ASM 修改字节码并集成到AGP 中,使其成为Android 构建过程的一部分,我们做到了 : )

总结&讨论

通篇下来,其实我们并没有修改javac ,我们不能也不应该去修改这些编译工具,我们使用Java 平台所提供的动态代理与反射就完成了类似?. 操作符的功能。

可能有人会说反射很慢,套用动态代理后会变得更慢,我倒是认为这种观点是缺乏说服力的,因为在这个级别上担心性能问题是不明智的,除非能够分析表明这个方法正是造成性能损失的源头,否则在没有任何衡量标准的前提下,固执地断定反射和动态代理很慢的观点是站不稳脚的。

为了安全使用定义在接口中的函数,我做了这个小工具,目前已经开源,所有代码都可以通过github 获取,希望这个避免空指针的“接口救生圈”能够让你在Java 的海洋中尽情遨游。

欢迎讨论或在评论区留下您宝贵的建议。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.01.31 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
浅析JVM invokedynamic指令和Java Lambda语法|得物技术
尽管近年来JDK的版本发布愈发敏捷,当前最新版本号已经20+,但是日常使用中,JDK8还是占据了统治地位。
得物技术
2024/08/27
1650
浅析JVM invokedynamic指令和Java Lambda语法|得物技术
在Java 中安全使用接口引用
我使用Java 开发过很多项目,这其中包括一些Web 应用和Android 客户端应用。作为Android 开发人员,Java 就像我们的母语一样,但Android 世界是多元化的,并不是只有Java 才能用来写Android 程序,Kotlin 和Groovy 同样优秀,并且有着大量的粉丝。
程序亦非猿
2019/08/16
2K0
大厂高级工程师面试必问系列:Java动态代理机制和实现原理详解
通过代理层这一中间层,有效的控制对于真实委托类对象的直接访问,同时又可以实现自定义的控制策略,比如Spring中的AOP机制,这样使得在设计上获得更大的灵活性
攻城狮Chova
2022/01/22
3820
大厂高级工程师面试必问系列:Java动态代理机制和实现原理详解
Java动态代理原理及解析
代理:设计模式 代理模式是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个真实对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。 通过
Spark学习技巧
2018/04/17
1.2K1
Java动态代理原理及解析
反射、注解和动态代理
反射是指计算机程序在运行时访问、检测和修改它本身状态或行为的一种能力,是一种元编程语言特性,有很多语言都提供了对反射机制的支持,它使程序能够编写程序。Java的反射机制使得Java能够动态的获取类的信息和调用对象的方法。
良月柒
2019/03/20
5610
反射、注解和动态代理
面试造火箭系列,栽在了cglib和jdk动态代理
“喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗”。声音是从电话那头传来的
程序员老猫
2022/01/04
2510
Kotlin —— 这次入门就不用放弃了
声明:本文是FEELS_CHAOTIC原创,已获其授权发布,未经原作者允许请勿转载
用户2802329
2018/08/07
1.7K0
Kotlin —— 这次入门就不用放弃了
基础篇:深入解析JAVA注解机制
在代码里定义的注解,会被jvm利用反射技术生成一个代理类,然后和被注释的代码(类,方法,属性等)关联起来
潜行前行
2020/12/11
6600
基础篇:深入解析JAVA注解机制
Java字节码深度知多少?
Java真的是长盛不衰,拥有顽强的生命力。其中,字节码机制功不可没。字节码,就像是 Linux 的 ELF。有了它,JVM直接摇身一变,变成了类似操作系统的东西。
猫头虎
2024/04/08
1080
Java字节码深度知多少?
Java动态代理之一CGLIB详解
在上篇文章《Java代理模式及动态代理详解》中我们介绍了Java中的静态代理模式与动态代理模式,并以JDK原生动态代理作为示例进行讲解。本篇文章我们来介绍一下基于CGLIB实现的动态代理,并与原生动态代理进行对比。
程序新视界
2020/03/26
1.9K0
干货 | Kotlin超棒的语言特性
作者简介 何伦,携程度假BU移动端资深研发经理,负责iOS、Android平台上跟团游产品预订流程的前端页面的研发工作。对新技术有着浓厚的兴趣。 自从2017年Google宣布Kotlin成为Android官方开发语言之后,Kotlin受到广大Android开发者的追捧。其强大的安全性,简洁性和与Java的互操作性,为开发者带来了耳目一新的开发体验,也极大提升了Android原生代码的开发效率。 不过大部分开发者对Kotlin的使用,仍然局限于把Java代码逻辑按照Kotlin语法进行转换的层面,其实Ko
携程技术
2018/07/05
1.5K0
是时候使用Kotlin编程了
从事Android开发的童鞋都知道,自从去年的Google I/O大会上Kotlin被定为Android开发的官方语言以来,关于Kotlin就成为每个开发人员学习的目标,的确,Kotlin以它独有的魅力正在吸引这传统的Java程序开发人员。或许很多的童鞋已经对Kotlin进行了深入的学习,甚至已经运用到了自己的项目当中,但是还有较多同学可能只是听过Kotlin或简单了解过,本文将从宏观的角度来介绍Kotlin相关的内容。 在介绍Kotlin之前,先来安利一波,本人去年年底开始写作的关于Kotlin的书下个月就要出版了,有兴趣的可以关注下,目录如下。
xiangzhihong
2022/11/30
3.4K0
无处不在的字节码技术-ASM在cglib和fastjson中的应用
字节码技术在我们常见的各大框架中都有用到. 这篇文章我们将讲解 ASM 在 cglib 和 fastjson 上的实际使用案例。
架构狂人
2023/08/16
3450
无处不在的字节码技术-ASM在cglib和fastjson中的应用
研究学习Kotlin的一些方法
Kotlin是一门让人感到很舒服的语言,相比Java来说,它更加简洁,省去了琐琐碎碎的语法工作,同时了提供了类似Lambda,String template,Null Safe Operator等特性。让开发者用起来得心应手。
技术小黑屋
2018/09/05
6410
研究学习Kotlin的一些方法
Android |《看完不忘系列》之Retrofit
嗨,我是哈利迪~《看完不忘系列》将以从树干到细枝的思路分析一些技术框架,本文将对开源项目Retrofit进行介绍。
Holiday
2020/08/20
9060
Android |《看完不忘系列》之Retrofit
Android平台的Swift—Kotlin
原文链接:http://wetest.qq.com/lab/view/383.html
WeTest质量开放平台团队
2018/05/04
3.6K7
Android平台的Swift—Kotlin
揭密 Java方法调用的底层原理
我们在日常开发中,其实很少去关注字节码层面的东西。但,作为我们的吃饭家伙,个人觉得还是很有必要了解的。
田维常
2021/10/27
1.5K0
揭密 Java方法调用的底层原理
通过字节码分析JDK8中Lambda表达式编译及执行机制【面试+工作】
在Class文件中,方法调用即是对常量池(ConstantPool)属性表中的一个符号引用,在类加载的解析期或者运行时才能确定直接引用。
Java帮帮
2018/09/29
2K0
通过字节码分析JDK8中Lambda表达式编译及执行机制【面试+工作】
深入浅出动态代理
代理模式是为了提供额外或者不同的操作,而插入代替”实际对象”的对象,即代理类,针对代理类的调用操作,都会涉及到与”实际对象”的通信,代理类起到中间人的作用。Java动态代理比代理的思想更进一步,它可以动态的创建代理类并处理对”实际对象”的调用,Java动态代理底层基于Proxy/InvocationHandler相关类和反射技术。
luoxn28
2019/11/06
5040
Java动态代理一览笔录
1、什么是代理? 比较经典的含义如销售代理,签订合同的基础上,为委托人(厂商)销售某些特定产品或全部产品的代理商,对价格、条款及其他交易条件可全权处理。我们从销售代理那里购买产品,通常是不知道销售代理背后的委托人(厂商)是谁,也就是 "委托人" 对于我们来说是不可见的。 代理,简单来说,也就是提供代理人,并有代理人全权处理委托人的事务。 在Java中,代理模式,类似的,也就是为某个对象(即委托人)提供一个代理对象(即代理人),并由代理对象(即代理人)全权控制对于原对象(即委托人)的访问。客户对于委托人不再可
斯武丶风晴
2018/03/01
7270
Java动态代理一览笔录
相关推荐
浅析JVM invokedynamic指令和Java Lambda语法|得物技术
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验