专栏首页Bennyhuo一个你可能没听说过的 Java 语法

一个你可能没听说过的 Java 语法

当然这肯定也是标题党了,比如群里面的大佬 Glavo 就是反例,怎么可能有 Glavo 没听说过的 Java 语法呢。

所以说这是什么语法呢?

首先我是在提出这个问题之后自己撕烤的时候突然脑补出的这个语法。

问题中首先摆出了这么一个语法:

void f(@NotNull List<@NotNull String> strings) {
}

函数 f 的参数的类型是 @NotNullList<@NotNullString>,表示这个参数本身不能为 null ,而它作为一个 List,它的成员也都不能是 null 。 这个看起来非常好理解,因为它实际上就是它看起来那样,很符合直觉。

其实还有这种操作:

<E> void f(@NotNull WhatEver<@NotNull ? extends @NotNull List<@NotNull E>> whatEver) {
}

但是如果这个参数是一个数组呢?

void f(@NotNull String[] /* emmm... */ strings) {
}

这个时候,我甚至不知道这个 @NotNull 注解的对象是什么(是参数?是 String?是 String[]?)。 在 Kotlin 中,我们可以写 Array<String?>Array<String>?,分别是本身不能为 null 但成员可以为 null 的数组和本身可以为 null 但成员不能为 null 的数组,这样的两种不同的类型在 Java 里面又应该怎么表达呢。

在 SO 提问之余,我就自己研究了一下。 我猜测,可能 [] 前面也能写东西?于是我试了一下:

void f(@NotNull String @NotNull [] strings) {
}

这个代码居然编译过了(提醒一下读者:不是所有注解都可以这么用的,如果你在使用自己写的注解尝试这个例子,请给你使用的注解加上 @Target({ElementType.TYPE_USE})。)。 我很是震精,于是我开始试图了解它背后的含义。这个时候最方便的测试方法当然就是看 @NotNull 系列注解在 Kotlin 里的表现啦。 首先我们写一个这样的函数:

import org.jetbrains.annotations.Nullable;

public class A {
  public static void main(@Nullable String[] args) {
  }
}

然后我在 Kotlin 里面调用它,发现它的签名是这样的:

说明 Kotlin 把这个注解同时应用到了 ArrayString 上。

而如果把注解写在我之前猜的那个位置的话:

import org.jetbrains.annotations.Nullable;

public class A {
  public static void main(String @Nullable [] args) {
  }
}

Kotlin 就直接无视了它(感叹号表示 Platform Type,是『未被标注为 @NotNull 或者 @Nullable 的意思』):

别急,在不知道这个东西的语义的时候先不要急着批判 Kotlin。 我们编译一下这个代码里的两个函数,看看字节码吧:

import org.jetbrains.annotations.Nullable;

public class A {
  public static void main(String @Nullable [] args) {
  }
  public static void main(@Nullable Number [] args) {
  }
}

然后使用这个命令看看字节码( javap-v 参数表示输出额外信息,这里不需要 -c(显示方法体)和 -p(显示 private 的东西)):

$ gradle assemble
$ javap -v A.class

看到 javap 输出了以下结果(已经省略了 80% 对本文无意义的内容了):

... 省略 ...
Constant pool:
... 省略 ...
  #16 = Utf8               Lorg/jetbrains/annotations/Nullable;
... 省略 ...
  public static void main(java.lang.String[]);
... 省略 ...
    RuntimeInvisibleTypeAnnotations:
      0: #16(): METHOD_FORMAL_PARAMETER, param_index=0

  public static void main(java.lang.Number[]);
... 省略 ...
    RuntimeInvisibleTypeAnnotations:
      0: #16(): METHOD_FORMAL_PARAMETER, param_index=0, location=[ARRAY]
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #16()
}
... 省略 ...

在常量池里面我们可以看到 #16 就是 @Nullable 注解:

Constant pool:
  #16 = Utf8               Lorg/jetbrains/annotations/Nullable;

然后在两个测试函数中,可以看到 #16 注解在不同的地方生效了。 首先是 String@Nullable[]args 的第一个函数:

RuntimeInvisibleTypeAnnotations:
  0: #16(): METHOD_FORMAL_PARAMETER, param_index=0

然后是 @NullableNumber[]args 的第二个函数:

RuntimeInvisibleTypeAnnotations:
  0: #16(): METHOD_FORMAL_PARAMETER, param_index=0, location=[ARRAY]
RuntimeInvisibleParameterAnnotations:
  0:
    0: #16()

呃。。。好吧,首先很明显第二个 @Nullable 同时生效于类型和参数本身了,而第一个只在类型上生效了。 不过我还是不知道他们各自在类型上生效时的字节码的意思(看不懂字节码真是对不起呢),于是就使用控制变量法,再写两个函数对比一下(之所以使用两个不同的 List 实现,是因为 List 和数组不一样,擦除了就一样了所以 JVM 签名就冲突叻):

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedList;

public class A {
  public static void main(ArrayList<@Nullable String> args) {
  }
  public static void main(@Nullable LinkedList<Number> args) {
  }
}

字节码出来是这样的(已经省略了 90% 对本文无意义的内容了):

... 省略 ...
Constant pool:
... 省略 ...
  #20 = Utf8               Lorg/jetbrains/annotations/Nullable;
... 省略 ...
  public static void main(java.util.ArrayList<java.lang.String>);
... 省略 ...
    RuntimeInvisibleTypeAnnotations:
      0: #20(): METHOD_FORMAL_PARAMETER, param_index=0, location=[TYPE_ARGUMENT(0)]

  public static void main(java.util.LinkedList<java.lang.Number>);
... 省略 ...
    RuntimeInvisibleTypeAnnotations:
      0: #20(): METHOD_FORMAL_PARAMETER, param_index=0
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #20()
}

和我想的差不多,写在整个参数前面( @NullableList<String> 或者 @NullableString[])就是对外部的类型和参数同时进行注解,而写在类型参数或者数组的 [] 前面( List<@NullableString> 或者 String@Nullable[])就是对类型参数进行注解。

再看看对于泛型类型,Kotlin 的处理方法吧。首先就是刚才那个 Java 代码,Kotlin 表示:

原来你丫不仅认识对参数的注解,还认识对类型参数的注解啊。

好了,谜底揭晓 ~ 于是我们可以说是 Kotlin 对这个语法的处理是错误的啦。 至于 Kotlin 是否能对二进制的 Java 代码中的这个语法正确处理呢,我已经没有耐心去测试了(Kotlin 的 Java 和 JVM bytecode 前端就是 IntelliJ IDEA 的 Java 和 JVM bytecode 前端,但我也不想再去看了)。

关于 Kotlin 的这个问题我已经在 YouTrack 上开 issue 了,大家可以去围观或者 upvote(逃

这个写法可以扩展到这些情况:

  • String@Nullable[]@Nullable[]
  • String@Nullable...

都是合法的 Java 代码哦。

其实最靠谱的参考还是 Java 标准 (https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.7.4)里对这个 case 的说明啦。


本文分享自微信公众号 - Kotlin(KotlinX),作者:千里冰封

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Kotlin 新版本也有了交叉类型和联合类型?

    Kotlin 1.4-m1 发布之后,我曾整理了一下官方博客中提到的语法更新,见 Kotlin 1.4 新特性预览。除了前面的文章中提到的变化,新类型推导算法对...

    bennyhuo
  • Kotlin 1.4 新特性预览

    Kotlin 1.4 的第一个里程碑版本发布了,具体发布信息可以参考1.4-M1 ChangeLog[1]。

    bennyhuo
  • 快速上手 Kotlin 11 招

    这篇文章主要是写给需要快速上手 Kotlin 的 Java 程序员看的,这时候他们关注的是如何 Kotlin 写出类似某些 Java 的写法,所以本文基本不涉及...

    bennyhuo
  • scala 学习笔记(03) 参数缺省值、不定个数参数、类的属性(Property)、泛型初步

    继续学习,这一篇主要是通过scala来吐槽java的,同样是jvm上的语言,差距咋就这么大呢? 作为一个有.NET开发经验的程序员,当初刚接触java时,相信很...

    菩提树下的杨过
  • 一文了解Java反射和应用

    静态编译:在编译的时候进确定类型,如果绑定对象成功,new 是静态加载类,就编译通过。

    Java技术江湖
  • Retrofit2.0通俗易懂的学习姿势,Retrofit2.0 + OkHttp3 + Gson + RxJava

    Retrofit,因为其简单与出色的性能,也是受到很多人的青睐,但是他和以往的通信框架还是有点区别,不过放心,因为他本身还是挺简单的,所有我相信你看完这篇文章,...

    非著名程序员
  • 在Java EE7框架中使用MongoDB

    中心点创建应用程序的执行在企业环境中,应用程序必须安全、便携和高可用性。它还必须能够与不同的系统交互,但可控的从一个最好的位置。JEE7合并是一个重要的框架的所...

    用户1289394
  • XStream进行xml和bean互转

    老梁
  • Android使用后端云Bmob实现登录、注册及失物招领

    最近在使用后端云Bmob对数据进行存储,目的是在不搭建服务器的前提下,能对Android应用的数据进行操作处理。

    SoullessCoder
  • Android中网络框架简单封装的实例方法

    Android作为一款主要应用在移动终端的操作系统,访问网络是必不可少的功能。访问网络,最基本的接口有:HttpUrlConnection,HttpClient...

    砸漏

扫码关注云+社区

领取腾讯云代金券