Java 源码剖析——动态代理的实现原理

本贴最后更新于 2695 天前,其中的信息可能已经天翻地覆

在本篇博客中,博主将和大家一起深入分析 Jdk 自带的动态代理的实现原理。如果有同学对代理模式静态代理动态代理这些概念比较模糊,请先阅读博主的另一篇文章《一步一步学设计模式——代理模式》

为了方便讲解,我们继续使用代理模式中的购票例子,下面是这个例子的主要代码:

  • 首先我们先建立一个接口:
package com.wxueyuan.DesignPettern.StaticProxy; public interface Operation { void buyTicket(Ticket t); }
  • 接着我们建立一个学生类并实现上面的接口,表示学生需要购票:
package com.wxueyuan.DesignPettern.StaticProxy; public class Student implements Operation{ @Override public void buyTicket(Ticket t) { // TODO Auto-generated method stub System.out.println("学生买到一张票,票价为"+t.getPrice()); } }
  • 然后我们建立一个 TicketOperationInvocationHandler 实现 InvocationHandler 接口:
package com.wxueyuan.DesignPettern.DynamicProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class TicketOperationInvocationHandler implements InvocationHandler { //将需要代理的委托对象传入Handler中 private Object target; public TicketOperationInvocationHandler(Object target) { this.target = target; } //获得帮助购票者买票的代理 public Object getProxy() { return Proxy.newProxyInstance(Thread.currentThread() .getContextClassLoader(), target.getClass().getInterfaces(), this); } //实际上黄牛执行的购票操作 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub System.out.println("黄牛收取购票者的钱"); System.out.println("黄牛连夜排队"); Object ret = method.invoke(target, args); System.out.println("黄牛将票交给购票者"); return ret; } }
  • 最后是测试类
package com.wxueyuan.DesignPettern.DynamicProxy; import com.wxueyuan.DesignPettern.StaticProxy.Operation; import com.wxueyuan.DesignPettern.StaticProxy.Student; import com.wxueyuan.DesignPettern.StaticProxy.Ticket; public class Test { public static void main(String[] args) { // TODO Auto-generated method stub //学生需要购买的ticket实例 Ticket studentTicket = new Ticket(200); //创建为学生买票的代理 Operation studentProxy = (Operation) new TicketOperationInvocationHandler(new Student()).getProxy(); studentProxy.buyTicket(studentTicket); } }

执行结果为:
黄牛收取购票者的钱
黄牛连夜排队
购票者买到一张票,票价为 200.0
黄牛将票交给学生

下面我们就一步一步地分析这个简单的动态代理例子的原理:

首先我们先看我们是如何获得学生的代理的:

Operation studentProxy = (Operation) new TicketOperationInvocationHandler(new Student()).getProxy();

其中的关键就在于我们自定义的 InvocationHandler 中的 getProxy()方法,现在我们就进入这个方法看一下:

public Object getProxy() { return Proxy.newProxyInstance(Thread.currentThread() .getContextClassLoader(), target.getClass().getInterfaces(), this); }

这个方法的核心就是使用 Proxy 类的静态方法 newProxyInstance(),我们具体看一下这个方法究竟在干什么,我们以 Jdk1.8 的源码为例,首先来看一下这个方法的注释:

/** * Returns an instance of a proxy class for the specified interfaces * that dispatches method invocations to the specified invocation * handler. * @param loader the class loader to define the proxy class * @param interfaces the list of interfaces for the proxy class * to implement * @param h the invocation handler to dispatch method invocations to 这个方法用来返回一个实现了一个或多个指定接口的代理类的实例,这个代理类能够将其实现的方法的调用传递给指定的方法调用处理器。 参数: loader:加载这个代理类的类加载器 interfaces:这个代理类实现的所有接口 h:将方法调用传至的调用处理器

newProxyInstance()这个方法生成实例的核心代码有以下几句:

//获得代理类的class类 Class<?> cl = getProxyClass0(loader, intfs); ... //获取代理类的构造函数 final Constructor<?> cons = cl.getConstructor(constructorParams); ... //根据构造函数生成一个代理类的实例,至于为什么代理类的构造方法中的参数是方法参数h,稍后我们就会知道了 return cons.newInstance(new Object[]{h});

从 newProxyInstance()方法中的三行核心代码可以看出,如何获取代理类的 class 类是重中之重,因为获取 class 类之后,我们就可以利用 Java 反射获取构造函数并生成实例了。由于反射并不是本篇博客的主题,我们现在就来着重关注一下 getProxyClass0()方法是如何工作的:

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } //如果代理类的class在缓存中存在,则直接获取,否则的话,通过ProxyClassFactory来创建 return proxyClassCache.get(loader, interfaces); }

这里的 proxyClassCache 是在 Proxy 类中声明的 WeakCache 的实例,那我们就一起来看看这个 WeakCache 是什么:

/** * Cache mapping pairs of (key, sub-key) -> value. * Keys and values are weakly but sub-keys are strongly referenced. * Keys are passed directly to get method which also takes a parameter. * Sub-keys are calculated from keys and parameters using the subKeyFactory function * passed to the constructor. * Values are calculated from keys and parameters using the valueFactory function passed * to the constructor. */ //这个WeakCache能够将一组(key,sub-key)的值映射成value的值。其中Key的值是直接通过参数传入的。 //sub-key的值是通过构造方法中的subKeyFactory生成的,value的值是通过构造方法中的valueFactory生成的。 //它的构造方法是: final class WeakCache<K,P,V> { ... public WeakCache(BiFunction<K, P, ?> subKeyFactory,BiFunction<K, P, V> valueFactory) { this.subKeyFactory = Objects.requireNonNull(subKeyFactory); this.valueFactory = Objects.requireNonNull(valueFactory); } ... }

知道了 WeakCache 的构造方法之后,我们一起来看一下我们在 getProxyClass0 方法中使用到的 WeakCache 的 get 方法,它的核心代码如下:

public V get(K key, P parameter) { //通过subKeyFactory的apply方法生成subKey Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter)); ... //通过subKey从valuesMap中取出可能存在的缓存提供者supplier Supplier<V> supplier = valuesMap.get(subKey); ... //如果供应者不为空,就调用supplier.get()方法,get方法的返回值就是我们这个方法的返回值 if (supplier != null) { // 这里的供应者有可能是一个factory或者是一个缓存实例 V value = supplier.get(); if (value != null) { return value; } } ... //如果factory没有成功创建,我们此时创建一个factory if (factory == null) { factory = new Factory(key, parameter, subKey, valuesMap); } ... }

看到这大家也许会问,上面代码中 factory 成功创建之后,如果 supplier 就是 factory,该如何通过 factory 的 get()方法来返回我们需要的 value 值,这个 value 值又是怎么计算得到的呢?原来 Factory 这个类也实现了 Supplier 这个函数式接口,因此它也实现了自己的 get 方法:

private final class Factory implements Supplier<V> { @Override public synchronized V get() { ... V value = null; try { //通过我们的valueFactory的apply方法生成value value = Objects.requireNonNull(valueFactory.apply(key, parameter)); } ... return value; } }

知道了 WeakCache 中 get()方法的实现后,我们看一下在 Proxy 类中,是如何定义这个 WeakCache 类型的 proxyClassCache 的呢?

//根据WeakCache的构造函数可知,KeyFactory就是在get方法中生成sub-key的subKeyFactory; //ProxyClassFactory就是get方法中生成value的valueFactory private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

看到这里读者们应该知道 WeakCache 中的 get 方法是怎么工作的了吧?它利用 subKeyFactory(在 Proxy 类中就是 KeyFactory)来生成 subKey,再利用 valueFactory(在 Proxy 类中就是 ProxyClassFactory)的 apply 方法生成并返回 value 值。那么我们一起来看一下,KeyFactory 是如何生成 subKey 的:

//其实很简单就是根据参数intefaces的数量,来生成不同的subKey对象 private static final class KeyFactory implements BiFunction<ClassLoader, Class<?>[], Object> { @Override public Object apply(ClassLoader classLoader, Class<?>[] interfaces) { switch (interfaces.length) { case 1: return new Key1(interfaces[0]); // 代理类只实现了一个接口 case 2: return new Key2(interfaces[0], interfaces[1]);//代理类实现了两个接口 case 0: return key0;//代理类没有实现接口 default: return new KeyX(interfaces);//代理类实现了三个及以上的接口 } } }

然后是 ProxyClassFactory 是如何生成 value,也就是我们这里需要的代理类的 class 类的:

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> { //代理类名字的前缀为$Proxy private static final String proxyClassNamePrefix = "$Proxy"; //为了生成唯一的代理类名的计数器 private static final AtomicLong nextUniqueNumber = new AtomicLong(); @Override public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { ... String proxyPkg = null; // 代理类的包名 //对于非公共接口,代理类的包名与接口的包名相同 for (Class<?> intf : interfaces) { int flags = intf.getModifiers(); if (!Modifier.isPublic(flags)) { accessFlags = Modifier.FINAL; String name = intf.getName(); int n = name.lastIndexOf('.'); String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); if (proxyPkg == null) { proxyPkg = pkg; } else if (!pkg.equals(proxyPkg)) { throw new IllegalArgumentException( "non-public interfaces from different packages"); } } } } if (proxyPkg == null) { // 如果没有非公共的接口,就使用com.sun.proxy作为包名 proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } //默认生成的公共代理类的全限定名为com.sun.proxy.$Proxy0,com.sun.proxy.$Proxy1,以此数字递增 long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; //生成代理类的字节码 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { //根据上面产生的字节码产生Class实例并返回,至此我们终于获得了代理类的Class实例 return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); } }

ProxyGenerator.generateProxyClass 这个方法的源码并没有公开,我们可以反编译 class 文件,然后简单看一下:

public static byte[] generateProxyClass(final String var0, Class[] var1) { ProxyGenerator var2 = new ProxyGenerator(var0, var1); final byte[] var3 = var2.generateClassFile(); // 这里根据参数配置,决定是否把生成的字节码(.class文件)保存到本地磁盘,默认是不保存的 if(saveGeneratedFiles) { AccessController.doPrivileged(new PrivilegedAction() { public Void run() { try { FileOutputStream var1 = new FileOutputStream(ProxyGenerator.dotToSlash(var0) + ".class"); var1.write(var3); var1.close(); return null; } catch (IOException var2) { throw new InternalError("I/O exception saving generated file: " + var2); } } }); } return var3; }

我们可以通过设置 sun.misc.ProxyGenerator.saveGeneratedFiles 这个 boolean 值的属性,来使方法默认将 class 文件保存到磁盘。那么我们就来修改一下我们的 Test 代码,来将生成的 proxy 文件保存到磁盘上:

public static void main(String[] args) { // TODO Auto-generated method stub //学生需要购买的ticket实例 Ticket studentTicket = new Ticket(200); //将是否在系统属性修改为true,使字节文件保存到磁盘上 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //显示生成的代理类的全限定名 System.out.println(Proxy.getProxyClass(Operation.class.getClassLoader(), Operation.class)); //创建为学生买票的黄牛代理 Operation studentProxy = (Operation) new TicketOperationInvocationHandler(new Student()).getProxy(); studentProxy.buyTicket(studentTicket); }

执行结果为:
class com.sun.proxy.$Proxy0
黄牛收取购票者的钱
黄牛连夜排队
学生买到一张票,票价为 200.0
黄牛将票交给购票者
同时,在 com/sun/proxy 下生成了Proxy0 代理类的包名会和接口类的包名相同哦)。下面我们将这个字节码反编译看一下:

package com.sun.proxy; import com.wxueyuan.DesignPettern.StaticProxy.Ticket; import java.lang.reflect.UndeclaredThrowableException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import com.wxueyuan.DesignPettern.StaticProxy.Operation; import java.lang.reflect.Proxy; public final class $Proxy0 extends Proxy implements Operation { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(final InvocationHandler invocationHandler) { super(invocationHandler); } public final boolean equals(final Object o) { try { return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o }); } catch (Error | RuntimeException error) { throw; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } public final String toString() { try { return (String)super.h.invoke(this, $Proxy0.m2, null); } catch (Error | RuntimeException error) { throw; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } public final void buyTicket(final Ticket ticket) { try { super.h.invoke(this, $Proxy0.m3, new Object[] { ticket }); } catch (Error | RuntimeException error) { throw; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } public final int hashCode() { try { return (int)super.h.invoke(this, $Proxy0.m0, null); } catch (Error | RuntimeException error) { throw; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } } static { try { $Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); $Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]); $Proxy0.m3 = Class.forName("com.wxueyuan.DesignPettern.StaticProxy.Operation").getMethod("buyTicket", Class.forName("com.wxueyuan.DesignPettern.StaticProxy.Ticket")); $Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]); } catch (NoSuchMethodException ex) { throw new NoSuchMethodError(ex.getMessage()); } catch (ClassNotFoundException ex2) { throw new NoClassDefFoundError(ex2.getMessage()); } } }

还记得我们之前的 return cons.newInstance(new Object[]{h});这句代码么,因为生成的 Proxy 类的构造函数的参数就是就是 InvocationHandler,因此我们将 newProxyInstance()中的参数 h 传递过来,用来调用它的 invoke()方法。

分析了这么多的源码,我们来总结一下 Java 动态代理的流程吧:

  1. Proxy.newProxyInstance()方法返回一个代理类的实例,需要传入 InvocationHandler 的实例 h
  2. 当新的代理实例调用指定方法时,本质上是 InvocationHandler 实例调用 invoke 方法,并传入指定的 method 类型的参数。

根据我们生成 $Proxy0 代理类,我们能够总结出:

  1. 所有生成的代理类都继承了 Proxy 类,实现了需要代理的接口。正是由于 java 不能多继承,所以 JDK 的动态代理不支持对实现类的代理,只支持接口的代理。
  2. 提供了一个使用 InvocationHandler 作为参数的构造方法。这个参数是由 Proxy.newProxyInstance()方法的参数传入的。当代理类实例调用某个方法时,本质上是 InvocationHandler 实例以该方法的 method 类型作为参数调用 invoke 方法。

Java 的动态代理其实有很多应用场景,比如 Spring 的 AOP 或者是最近很火的 RPC 框架,里面都涉及到了动态代理的知识,因此从原理上分析一下动态代理的源码还是很有帮助的,那么这次的源码分析就到这里了,我们下次再见 ~。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 163 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3196 引用 • 8215 回帖
  • 动态代理实现原理
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • MongoDB

    MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是一个基于分布式文件存储的数据库,由 C++ 语言编写。旨在为应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。

    90 引用 • 59 回帖 • 6 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    227 引用 • 476 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    141 引用 • 946 回帖
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    92 引用 • 752 回帖
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    124 引用 • 74 回帖 • 1 关注
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖 • 18 关注
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖
  • 单点登录

    单点登录(Single Sign On)是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    9 引用 • 25 回帖 • 7 关注
  • PWA

    PWA(Progressive Web App)是 Google 在 2015 年提出、2016 年 6 月开始推广的项目。它结合了一系列现代 Web 技术,在网页应用中实现和原生应用相近的用户体验。

    14 引用 • 69 回帖 • 175 关注
  • TensorFlow

    TensorFlow 是一个采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即张量(tensor)。

    20 引用 • 19 回帖 • 1 关注
  • SEO

    发布对别人有帮助的原创内容是最好的 SEO 方式。

    35 引用 • 200 回帖 • 27 关注
  • SendCloud

    SendCloud 由搜狐武汉研发中心孵化的项目,是致力于为开发者提供高质量的触发邮件服务的云端邮件发送平台,为开发者提供便利的 API 接口来调用服务,让邮件准确迅速到达用户收件箱并获得强大的追踪数据。

    2 引用 • 8 回帖 • 493 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    133 引用 • 796 回帖
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 704 关注
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    107 引用 • 153 回帖
  • jsDelivr

    jsDelivr 是一个开源的 CDN 服务,可为 npm 包、GitHub 仓库提供免费、快速并且可靠的全球 CDN 加速服务。

    5 引用 • 31 回帖 • 103 关注
  • Access
    1 引用 • 3 回帖 • 3 关注
  • 一些有用的避坑指南。

    69 引用 • 93 回帖 • 1 关注
  • 强迫症

    强迫症(OCD)属于焦虑障碍的一种类型,是一组以强迫思维和强迫行为为主要临床表现的神经精神疾病,其特点为有意识的强迫和反强迫并存,一些毫无意义、甚至违背自己意愿的想法或冲动反反复复侵入患者的日常生活。

    15 引用 • 161 回帖 • 1 关注
  • JRebel

    JRebel 是一款 Java 虚拟机插件,它使得 Java 程序员能在不进行重部署的情况下,即时看到代码的改变对一个应用程序带来的影响。

    26 引用 • 78 回帖 • 678 关注
  • IBM

    IBM(国际商业机器公司)或万国商业机器公司,简称 IBM(International Business Machines Corporation),总公司在纽约州阿蒙克市。1911 年托马斯·沃森创立于美国,是全球最大的信息技术和业务解决方案公司,拥有全球雇员 30 多万人,业务遍及 160 多个国家和地区。

    17 引用 • 53 回帖 • 146 关注
  • jsoup

    jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

    6 引用 • 1 回帖 • 487 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    693 引用 • 537 回帖
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 2 关注
  • Laravel

    Laravel 是一套简洁、优雅的 PHP Web 开发框架。它采用 MVC 设计,是一款崇尚开发效率的全栈框架。

    20 引用 • 23 回帖 • 738 关注
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 82 关注