0

0

Java8 Lambda表达式的揭秘详解

黄舟

黄舟

发布时间:2017-04-01 10:43:40

|

1952人浏览过

|

来源于php中文网

原创

再了解了java 8 lambda的一些基本概念和应用后, 我们会有这样的一个问题: lambda表达式被编译成了什么?。 这是一个有趣的问题,涉及到jdk的具体的实现。 本文将介绍openjdk对lambda表达式的转换细节, 读者可以了解java 8 lambda表达式背景知识。

Lambda表达式的转换策略

Brian Goetz是Oracle的Java语言架构师, JSR 335(Lambda Expression)规范的lead, 写了几篇Lambda设计方面的文章, 其中之一就是Translation of Lambda Expressions。 这篇文章介绍了Java 8 Lambda设计时的考虑以及实现方法。

他提到, Lambda表达式可以通过内部类, method handle, dynamic proxy等方式实现, 但是这些方法各有优劣。 真正要实现Lambda表达式, 必须兼顾两个目标: 一是不引入特定策略,以期为将来的优化提供最大的灵活性, 二是保持类文件格式的稳定。 通过Java 7中引入的invokedynamic (JSR 292), 可以很好的兼顾这两个目标。

invokedynamic 在缺乏静态类型信息的情况下可以支持有效的灵活的方法调用。主要是为了日益增长的运行在JVM上的动态类型语言, 如Groovy, JRuby。

invokedynamic将Lambda表达式的转换策略推迟到运行时, 这也意味着我们现在编译的代码在将来的转换策略改变的情况下也能正常运行。

立即学习Java免费学习笔记(深入)”;

编译器在编译的时候, 会将Lambda表达式的表达式体 (lambda body)脱糖(desugar) 成一个方法,此方法的参数列表和返回类型和lambda表达式一致, 如果有捕获参数, 脱糖的方法的参数可能会更多一些, 并会产生一个invokedynamic调用, 调用一个call site。 这个call site被调用时会返回lambda表达式的目标类型(functional interface)的一个实现类。 这个call site称为这个lambda表达式的lambda factory。 lambda factory的Bootstrap方法是一个标准方法, 叫做lambda metafactory。

编译器在转换lambda表达式时, 可以推断出表达式的参数类型,返回类型以及异常, 称之为natural signature, 我们将目标类型的方法签名称之为lambda descriptor, lambda factory的返回对象实现了函数接口, 并且关联的表达式的代码逻辑, 称之为lambda object

转换举例

以上的解释有点晦涩, 简单来说

  • 编译时

    • Lambda 表达式会生成一个方法, 方法实现了表达式的代码逻辑

    • 生成invokedynamic指令, 调用bootstrap方法, 由java.lang.invoke.LambdaMetafactory.metafactory方法实现

  • 运行时

    • invokedynamic指令调用metafactory方法。 它会返回一个CallSite, 此CallSite返回目标类型的一个匿名实现类, 此类关联编译时产生的方法

    • lambda表达式调用时会调用匿名实现类关联的方法。

最简单的一个lambda表达式的例子:

public class Lambda1 {
	public static void main(String[] args) {
		Consumer c = s -> System.out.println(s);
		c.accept("hello lambda");
	}
}

使用javap查看生成的字节码 javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class:

[root@colobu bin]# javap -c -p -v com/colobu/lambda/chapter5/Lambda1.class 
Classfile /mnt/eclipse/Lambda/bin/com/colobu/lambda/chapter5/Lambda1.class
  Last modified Nov 6, 2014; size 1401 bytes
  MD5 checksum fe2b2d3f039a9ba4209c488a8c4b4ea8
  Compiled from "Lambda1.java"
public class com.colobu.lambda.chapter5.Lambda1
  SourceFile: "Lambda1.java"
  BootstrapMethods:
    0: #57 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;
    Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;
    Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #58 (Ljava/lang/Object;)V
        #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #62 (Ljava/lang/String;)V
  InnerClasses:
       public static final #68= #64 of #66; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             //  com/colobu/lambda/chapter5/Lambda1
   #2 = Utf8               com/colobu/lambda/chapter5/Lambda1
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          //  java/lang/Object."":()V
   #9 = NameAndType        #5:#6          //  "":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/colobu/lambda/chapter5/Lambda1;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = NameAndType        #17:#18        //  accept:()Ljava/util/function/Consumer;
  #17 = Utf8               accept
  #18 = Utf8               ()Ljava/util/function/Consumer;
  #19 = InvokeDynamic      #0:#16         //  #0:accept:()Ljava/util/function/Consumer;
  #20 = String             #21            //  hello lambda
  #21 = Utf8               hello lambda
  #22 = InterfaceMethodref #23.#25        //  java/util/function/Consumer.accept:(Ljava/lang/Object;)V
  #23 = Class              #24            //  java/util/function/Consumer
  #24 = Utf8               java/util/function/Consumer
  #25 = NameAndType        #17:#26        //  accept:(Ljava/lang/Object;)V
  #26 = Utf8               (Ljava/lang/Object;)V
  #27 = Utf8               args
  #28 = Utf8               [Ljava/lang/String;
  #29 = Utf8               c
  #30 = Utf8               Ljava/util/function/Consumer;
  #31 = Utf8               LocalVariableTypeTable
  #32 = Utf8               Ljava/util/function/Consumer;
  #33 = Utf8               lambda$0
  #34 = Utf8               (Ljava/lang/String;)V
  #35 = Fieldref           #36.#38        //  java/lang/System.out:Ljava/io/PrintStream;
  #36 = Class              #37            //  java/lang/System
  #37 = Utf8               java/lang/System
  #38 = NameAndType        #39:#40        //  out:Ljava/io/PrintStream;
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Methodref          #42.#44        //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #42 = Class              #43            //  java/io/PrintStream
  #43 = Utf8               java/io/PrintStream
  #44 = NameAndType        #45:#34        //  println:(Ljava/lang/String;)V
  #45 = Utf8               println
  #46 = Utf8               s
  #47 = Utf8               Ljava/lang/String;
  #48 = Utf8               SourceFile
  #49 = Utf8               Lambda1.java
  #50 = Utf8               BootstrapMethods
  #51 = Methodref          #52.#54      //  java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;
  Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;
  Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #52 = Class              #53            //  java/lang/invoke/LambdaMetafactory
  #53 = Utf8               java/lang/invoke/LambdaMetafactory
  #54 = NameAndType        #55:#56        //  metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
  Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #55 = Utf8               metafactory
  #56 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;
  Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #57 = MethodHandle       #6:#51    //  invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;
  Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;
  Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #58 = MethodType         #26            //  (Ljava/lang/Object;)V
  #59 = Methodref          #1.#60         //  com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
  #60 = NameAndType        #33:#34        //  lambda$0:(Ljava/lang/String;)V
  #61 = MethodHandle       #6:#59         //  invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
  #62 = MethodType         #34            //  (Ljava/lang/String;)V
  #63 = Utf8               InnerClasses
  #64 = Class              #65            //  java/lang/invoke/MethodHandles$Lookup
  #65 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #66 = Class              #67            //  java/lang/invoke/MethodHandles
  #67 = Utf8               java/lang/invoke/MethodHandles
  #68 = Utf8               Lookup
{
  public com.colobu.lambda.chapter5.Lambda1();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #8                  // Method java/lang/Object."":()V
         4: return        
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/colobu/lambda/chapter5/Lambda1;
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokedynamic #19,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1      
         6: aload_1       
         7: ldc           #20                 // String hello lambda
         9: invokeinterface #22,  2           // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
        14: return        
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      15     0  args   [Ljava/lang/String;
               6       9     1     c   Ljava/util/function/Consumer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            6       9     1     c   Ljava/util/function/Consumer;
  private static void lambda$0(java.lang.String);
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0       
         4: invokevirtual #41                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return        
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       8     0     s   Ljava/lang/String;
}

可以看到, Lambda表达式体被生成一个称之为lambda$0的方法。 看字节码知道它调用System.out.println输出传入的参数。

原lambda表达式处产生了一条invokedynamic #19, 0。它会调用bootstrap方法。

0: #57 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #58 (Ljava/lang/Object;)V
        #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #62 (Ljava/lang/String;)V

如果Lambda表达式写成Consumer c = (Consumer & Serializable)s -> System.out.println(s);, 则BootstrapMethods的字节码为

BootstrapMethods:
    0: #108 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
    Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #109 (Ljava/lang/Object;)V
        #112 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
        #113 (Ljava/lang/String;)V
        #114 1

它调用的是LambdaMetafactory.altMetafactory,和上面的调用的方法不同。#114 1意味着要实现Serializable接口。

如果Lambda表达式写成``,则BootstrapMethods的字节码为

BootstrapMethods:
  0: #57 invokestatic java/lang/invoke/LambdaMetafactory.altMetafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
  Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #58 (Ljava/lang/Object;)V
      #61 invokestatic com/colobu/lambda/chapter5/Lambda1.lambda$0:(Ljava/lang/String;)V
      #62 (Ljava/lang/String;)V
      #63 2
      #64 1
      #65 com/colobu/lambda/chapter5/ABC

#63 2意味着要实现额外的接口。#64 1意味着要实现额外的接口的数量为1。

字节码的指令含义可以参考这篇文章:Java bytecode instruction listings。

可以看到, Lambda表达式具体的转换是通过java.lang.invoke.LambdaMetafactory.metafactory实现的, 静态参数依照lambda表达式和目标类型不同而不同。

LambdaMetafactory.metafactory

现在我们可以重点关注以下 LambdaMetafactory.metafactory的实现。

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {返回值类型
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

实际是由InnerClassLambdaMetafactorybuildCallSite来生成。 生成之前会调用validateMetafactoryArgs方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。

metaFactory方法的参数:

  • caller: 由JVM提供的lookup context

    Quicktools Background Remover
    Quicktools Background Remover

    Picsart推出的图片背景移除工具

    下载
  • invokedName: JVM提供的NameAndType

  • invokedType: JVM提供的期望的CallSite类型

  • samMethodType: 函数式接口定义的方法的签名

  • implMethod: 编译时产生的那个实现方法

  • instantiatedMethodType: 强制的方法签名和返回类型, 一般和samMethodType相同或者是它的一个特例

上面的代码基本上是InnerClassLambdaMetafactory.buildCallSite的包装,下面看看这个方法的实现:

CallSite buildCallSite() throws LambdaConversionException {
       final Class innerClass = spinInnerClass();
       if (invokedType.parameterCount() == 0) {
		..... //调用构造函数初始化一个SAM的实例
           return new ConstantCallSite(MethodHandles.constant(samBase, inst));
       } else {
           UNSAFE.ensureClassInitialized(innerClass);
               return new ConstantCallSite(
                       MethodHandles.Lookup.IMPL_LOOKUP
                            .findStatic(innerClass, NAME_FACTORY, invokedType));
       }
   }

其中spinInnerClass调用asm框架动态的产生SAM的实现类, 这个实现类的的方法将会调用编译时产生的那个实现方法。
你可以在编译的时候加上参数-Djdk.internal.lambda.dumpProxyClasses, 这样编译的时候会自动产生运行时spinInnerClass产生的类。

你可以访问OpenJDK的bug系统了解这个功能。 JDK-8023524

重复的lambda表达式

下面的代码中,在一个循环中重复生成调用lambda表达式,只会生成同一个lambda对象, 因为只有同一个invokedynamic指令。

for (int i = 0; i<100; i++){
	Consumer c = s -> System.out.println(s);
	System.out.println(c.hashCode());
}

但是下面的代码会生成两个lambda对象, 因为它会生成两个invokedynamic指令。

Consumer c = s -> System.out.println(s);
System.out.println(c.hashCode());
Consumer c2 = s -> System.out.println(s);
System.out.println(c2.hashCode());

生成的类名

既然LambdaMetafactory会使用asm框架生成一个匿名类, 那么这个类的类名有什么规律的。

Consumer c = s -> System.out.println(s);
System.out.println(c.getClass().getName());
System.out.println(c.getClass().getSimpleName());
System.out.println(c.getClass().getCanonicalName());

输出结果如下:

com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680
Lambda3$$Lambda$1/640070680
com.colobu.lambda.chapter5.Lambda3$$Lambda$1/640070680

类名格式如 .$$Lambda$/.

number是由一个计数器生成counter.incrementAndGet()。

后缀/中的数字是一个hash值, 那就是类对象的hash值c.getClass().hashCode()

Klass::external_name()中生成。

sprintf(hash_buf, "/" UINTX_FORMAT, (uintx)hash);

直接调用生成的方法

上面提到, Lambda表达式体会由编译器生成一个方法,名字格式如Lambda$XXX

既然是类中的实实在在的方法,我们就可以直接调用。当然, 你在代码中直接写lambda$0()编译通不过, 因为Lambda表达式体还没有被抽取成方法。

但是在运行中我们可以通过反射的方式调用。 下面的例子使用发射和MethodHandle两种方式调用这个方法。

public static void main(String[] args) throws Throwable {
	Consumer c = s -> System.out.println(s);
	Method m = Lambda4.class.getDeclaredMethod("lambda$0", String.class);
	m.invoke(null, "hello reflect");
	MethodHandle mh = MethodHandles.lookup().findStatic(Lambda4.class, "lambda$0", MethodType.methodType(void.class, String.class));
	mh.invoke("hello MethodHandle");
}

捕获的变量等价于’final’

我们知道,在匿名类中调用外部的参数时,参数必须声明为final

Lambda体内也可以引用上下文中的变量,变量可以不声明成final的,但是必须等价于final

下面的例子中变量capturedV等价与final, 并没有在上下文中重新赋值。

public class Lambda5 {
	String greeting = "hello";

	public static void main(String[] args) throws Throwable {

		Lambda5 capturedV = new Lambda5();
		Consumer c = s -> System.out.println(capturedV.greeting + " " + s);
		c.accept("captured variable");
		//capturedV = null; //Local variable capturedV defined in an enclosing scope must be final or effectively final
		//capturedV.greeting = "hi";
	}
}

如果反注释capturedV = null;编译出错,因为capturedV在上下文中被改变。

但是如果反注释capturedV.greeting = "hi"; 则没问题, 因为capturedV没有被重新赋值, 只是它指向的对象的属性有所变化。

方法引用

public static void main(String[] args) throws Throwable {

	Consumer c  = System.out::println;
	c.accept("hello");
}

这段代码不会产生一个类似”Lambda$0″新方法。 因为LambdaMetafactory会直接使用这个引用的方法。

BootstrapMethods:
  0: #51 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;
  Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;
  Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #52 (Ljava/lang/Object;)V
      #59 invokevirtual java/io/PrintStream.println:(Ljava/lang/String;)V
      #60 (Ljava/lang/String;)V

#59指示实现方法为System.out::println


相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
主流快递单号查询入口 实时物流进度一站式追踪专题
主流快递单号查询入口 实时物流进度一站式追踪专题

本专题聚合极兔快递、京东快递、中通快递、圆通快递、韵达快递等主流物流平台的单号查询与运单追踪内容,重点解决单号查询、手机号查物流、官网入口直达、包裹进度实时追踪等高频问题,帮助用户快速获取最新物流状态,提升查件效率与使用体验。

0

2026.02.02

Golang WebAssembly(WASM)开发入门
Golang WebAssembly(WASM)开发入门

本专题系统讲解 Golang 在 WebAssembly(WASM)开发中的实践方法,涵盖 WASM 基础原理、Go 编译到 WASM 的流程、与 JavaScript 的交互方式、性能与体积优化,以及典型应用场景(如前端计算、跨平台模块)。帮助开发者掌握 Go 在新一代 Web 技术栈中的应用能力。

1

2026.02.02

PHP Swoole 高性能服务开发
PHP Swoole 高性能服务开发

本专题聚焦 PHP Swoole 扩展在高性能服务端开发中的应用,系统讲解协程模型、异步IO、TCP/HTTP/WebSocket服务器、进程与任务管理、常驻内存架构设计。通过实战案例,帮助开发者掌握 使用 PHP 构建高并发、低延迟服务端应用的工程化能力。

0

2026.02.02

Java JNI 与本地代码交互实战
Java JNI 与本地代码交互实战

本专题系统讲解 Java 通过 JNI 调用 C/C++ 本地代码的核心机制,涵盖 JNI 基本原理、数据类型映射、内存管理、异常处理、性能优化策略以及典型应用场景(如高性能计算、底层库封装)。通过实战示例,帮助开发者掌握 Java 与本地代码混合开发的完整流程。

0

2026.02.02

go语言 注释编码
go语言 注释编码

本专题整合了go语言注释、注释规范等等内容,阅读专题下面的文章了解更多详细内容。

61

2026.01.31

go语言 math包
go语言 math包

本专题整合了go语言math包相关内容,阅读专题下面的文章了解更多详细内容。

52

2026.01.31

go语言输入函数
go语言输入函数

本专题整合了go语言输入相关教程内容,阅读专题下面的文章了解更多详细内容。

25

2026.01.31

golang 循环遍历
golang 循环遍历

本专题整合了golang循环遍历相关教程,阅读专题下面的文章了解更多详细内容。

10

2026.01.31

Golang人工智能合集
Golang人工智能合集

本专题整合了Golang人工智能相关内容,阅读专题下面的文章了解更多详细内容。

7

2026.01.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
极客学院Java8新特性视频教程
极客学院Java8新特性视频教程

共17课时 | 3.8万人学习

极客学院Java8新特性视频教程
极客学院Java8新特性视频教程

共17课时 | 3.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号