Java 8 Lambda 表达式被编译成了什么?
点击上方“朱小厮的博客”,选择“设为星标”
后台回复"书",获取个gui
在了解了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表达式的转换策略推迟到运行时, 这也意味着我们现在编译的代码在将来的转换策略改变的情况下也能正常运行。
编译器在编译的时候, 会将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<String> 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.classLast modified Nov 6, 2014; size 1401 bytesMD5 checksum fe2b2d3f039a9ba4209c488a8c4b4ea8Compiled from "Lambda1.java"
public class com.colobu.lambda.chapter5.Lambda1SourceFile: "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;)VInnerClasses:public static final #68= #64 of #66; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandlesminor version: 0major version: 52flags: 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 <init>#6 = Utf8 ()V#7 = Utf8 Code#8 = Methodref #3.#9 // java/lang/Object."<init>":()V#9 = NameAndType #5:#6 // "<init>":()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<Ljava/lang/String;>;#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_PUBLICCode:stack=1, locals=1, args_size=10: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V4: return LineNumberTable:line 7: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/colobu/lambda/chapter5/Lambda1;public static void main(java.lang.String[]);flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=10: invokedynamic #19, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;5: astore_1 6: aload_1 7: ldc #20 // String hello lambda9: invokeinterface #22, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V14: return LineNumberTable:line 10: 0line 11: 6line 12: 14LocalVariableTable:Start Length Slot Name Signature0 15 0 args [Ljava/lang/String;6 9 1 c Ljava/util/function/Consumer;LocalVariableTypeTable:Start Length Slot Name Signature6 9 1 c Ljava/util/function/Consumer<Ljava/lang/String;>;private static void lambda$0(java.lang.String);flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETICCode:stack=2, locals=1, args_size=10: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream;3: aload_0 4: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/String;)V7: return LineNumberTable:line 10: 0LocalVariableTable:Start Length Slot Name Signature0 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<String> c = (Consumer<String> & 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();}
实际是由InnerClassLambdaMetafactory
的buildCallSite
来生成。生成之前会调用validateMetafactoryArgs
方法校验目标类型(SAM)方法的参数/和产生的方法的参数/返回值类型是否一致。
metaFactory
方法的参数:
caller: 由JVM提供的lookup context
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<String> c = s -> System.out.println(s);System.out.println(c.hashCode());
}
但是下面的代码会生成两个lambda对象, 因为它会生成两个invokedynamic
指令。
Consumer<String> c = s -> System.out.println(s);
System.out.println(c.hashCode());
Consumer<String> c2 = s -> System.out.println(s);
System.out.println(c2.hashCode());
生成的类名
既然LambdaMetafactory会使用asm
框架生成一个匿名类, 那么这个类的类名有什么规律的。
Consumer<String> 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()。后缀/<NN>
中的数字是一个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<String> 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<String> 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<String> 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
想知道更多?扫描下面的二维码关注我
后台回复"技术",加入技术群
当当实付满200-40优惠码:NBHH2P
【精彩推荐】
超清晰的DNS入门指南
深入理解Java Stream流水线
如何用ELK搭建TB级的日志系统
深度好文:Linux系统内存知识
日志系统新贵Loki,确实比笨重的ELK轻
日志采集系统都用到哪些技术?
面试官:为什么HashMap的加载因子是0.75?
原创|OpenAPI标准规范
Linux系统内存知识总结
深度好文|奈飞微服务架构解析
耗时3天,上亿数据如何做到秒级查询
点个赞+在看,少个 bug ????
Java 8 Lambda 表达式被编译成了什么?相关推荐
- 这是一个有趣的问题,Java 8 Lambda 表达式被编译成了什么?
在了解了Java 8 Lambda的一些基本概念和应用后, 我们会有这样的一个问题: Lambda表达式被编译成了什么? 这是一个有趣的问题,涉及到JDK的具体的实现.本文将介绍OpenJDK对Lam ...
- 10个Java 8 Lambda表达式经典示例
Java 8 刚于几周前发布,日期是2014年3月18日,这次开创性的发布在Java社区引发了不少讨论,并让大家感到激动.特性之一便是随同发布的lambda表 达式,它将允许我们将行为传到函数里.在J ...
- Java 8 Lambda 表达式详解
版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009186509 1. 引言 在 Java 8 以前,若我们想要把某些功能传递给某 ...
- java 8 lambda表达式中的异常处理
文章目录 简介 处理Unchecked Exception 处理checked Exception 总结 java 8 lambda表达式中的异常处理 简介 java 8中引入了lambda表达式,l ...
- Java 8 Lambda表达式10个示例【存】
PS:不能完全参考文章的代码,请参考这个文件http://files.cnblogs.com/files/AIThink/Test01.zip 在Java 8之前,如果想将行为传入函数,仅有的选择就是 ...
- Java中Lambda表达式的使用
目录 1 简介 2 Lambda表达式的组成 2.1 Lambda表达式的函数式接口 2.2 对接口的要求 3 Lambda使用 3.1 基本使用 3.2 使用Lambdas和Streams 4 ...
- 深入浅出 Java 8 Lambda 表达式
摘要:此篇文章主要介绍 Java8 Lambda 表达式产生的背景和用法,以及 Lambda 表达式与匿名类的不同等.本文系 OneAPM 工程师编译整理. Java 是一流的面向对象语言,除了部分简 ...
- Java中Lambda表达式和stream的使用
Java中Lambda表达式和stream的使用 转自 [*https://www.cnblogs.com/franson-2016/p/5593080.html*] 简介 (译者注:虽然看着很先进, ...
- Java的Lambda表达式
语雀链接:Lambda表达式 文章目录 1.体验Lambda表达式 1.1函数式编程思想概述 1.2体验Lambda表达式 2.Lambda表达式的标准格式 2.1匿名内部类中重写run()方法的代码 ...
最新文章
- Sql Server 中锁的概念
- [微信小程序]给data的对象的属性赋值
- 《用于物联网的Arduino项目开发:实用案例解析》—— 3.4 小结
- Qt最新版5.14在Windows环境静态编译安装和部署的完整过程 VS 2019-Qt static link build Windows 32 bit/64 bit
- gis根据行政区计算栅格数据计算_Java中根据半径计算圆的周长和面积
- 装×失败的后果。。。 | 今日最佳
- Django REST framework 源码解析
- mysql在恢复数据时出现“table full”报错
- Linux Basics command
- Linux单用户下提示权限不够,Linux_/dev/null 权限不足,/dev/null文件设备的权限不对, - phpStudy...
- 【SeaJS】【1】初识SeaJS
- vs2015编译ffmpeg
- 操作系统原理:文件系统
- Udacity DNN
- 数据安全被篡改的风险分析解决方案
- 阳光直射对计算机有影响吗,办公电脑需要注意的风水
- 自定义View 实现圆形seekbar (音量旋钮)
- onsubmit和submit事件处理函数怎么不生效呢?
- Bps,bps,pps 等的计算
- 有种速度让你望尘莫及 | 手机QQ及Qzone速度优化实践
热门文章
- oracle exp 二进制,Oracle备份之exp自动逻辑备份(二)
- 今天在海淀黄庄地铁站真实经历 写下来希望我的好友可以看一下!
- CodeForces - 1368D AND, OR and square sum(位运算+贪心)
- 洛谷 - P3980 [NOI2008]志愿者招募(最小费用最大流+思维建边)
- 中石油训练赛 - Isomorphic Inversion(哈希+贪心)
- atlas mysql 读写分离_MySQL读写分离工具Atlas
- 下列哪个滤波器是非线性的_数字图像处理复习题(选择题及相应答案)
- m3u8文件在手机上用什么软件看_如何用手机从一个网页下载视频
- python自动点击脚本_[Python] 【Python3】教你写页游自动化Python脚本 3.取色,大漠识别和后台点击...
- 0459-Repeated Substring Pattern(重复的子字符串)