前言
今天在读源码过程中,看到一段比较抽象的代码,一时理解不了,停下来琢磨了一会儿。查阅相关资料发现这块代码涉及JDK的Method reference(方法引用),特做此记录,方便日后回顾。下面直接来看该源码中的写法:
// 接口定义
public interface NotifyListener {
void notify(List<String> urls);
}
// 接口使用,当时看到这块代码内心有无数个❓
final AtomicReference<List<String>> reference = new AtomicReference<>();
NotifyListener listener = reference::set;
listener.notify(Arrays.asList("hello world"));
对比通常的匿名内部类写法:
//通常情况下匿名内部类的写法
NotifyListener listener1 = new NotifyListener() {
@Override
public void notify(List<String> urls) {
reference.set(urls);
}
};
明显源码中的写法更为简洁,易读性也没有受影响,那么,这种写法有依据吗?当然有,就是今天我们要说的Method reference,为了更清楚理解方法引用 "::" 的使用,扒了下官网对Method reference的介绍:在lambda表达式的基础上,方法引用可以帮助我们构建更为简洁、紧凑、易读的代码。下面我们从嵌套类(NestClass)开始,了解如何一步步将代码变得更为简洁。
一、嵌套类(NestedClass)
Java语法允许在一个类的内部定义类,这个内部定义类称之为嵌套类,嵌套类通常分为内部类和静态嵌套类两种。例如:
class OuterClass {
...
// 内部类
class InnerClass {
...
}
//静态嵌套类
static class StaticNestedClass {
...
}
}
内部类
与实例方法和变量一样,内部类与其外部类的实例相关联,并可以直接访问外部类实例对象的方法和字段。实例化内部类时,必须先初始化外部类。外部类实例中创建内部类实例的语法如下:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
静态嵌套类
与类方法和变量一样,静态嵌套类与其外部类相关联。和静态类方法一样,静态嵌套类不能直接引用外部类中定义的实例变量或方法:它只能通过对象引用来使用它们。静态嵌套类与外部类的交互方式与其他顶级类一样,事实上,静态嵌套类的行为与在同一个包下的其他顶级类一样。静态嵌套类实例化也与顶级类一样:
StaticNestedClass staticNestedObject = new StaticNestedClass();
屏蔽(shadowing)
特定作用域(内部类或者方法)内的类型声明(成员变量或者参数名)与封闭域(通常指代嵌套类所在的类或者方法作用域)内的其他声明名称相同,那么前者会屏蔽后者,也就是说此时变量值优先从该作用域取,我们来看个例子:
public class ShadowTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// x = 23
System.out.println("x = " + x);
// this.x = 1
System.out.println("this.x = " + this.x);
// ShadowTest.this.x = 0
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
}
}
public static void main(String... args) {
ShadowTest st = new ShadowTest();
ShadowTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
序列化
强烈建议不要序列化内部类(包括局部类和匿名类)。当Java编译器编译某些特定结构,比如内部类时,会创建合成结构;这些合成结构在源代码中没有相应结构的类、方法、字段和其他结构。合成构造使Java编译器能够在不更改JVM的情况下实现新的Java语言特性。然而,合成结构可能在不同的Java编译器实现中有所不同,这意味着.class文件也可能在不同的实现中有所不同。因此,如果您先序列化一个内部类,然后再用不同的JRE实现反序列化它,则可能会出现兼容性问题。
二、内部类(InnerClass)
内部类即一般声明在某个类内部,有明确的名称和修饰符,除了常见的内部类(这里指非方法、语句、代码块中的类,非静态嵌套类)以外,还有两种常见内部类,即局部类(LocalClass)和匿名类(AnonymousClass)。
三、局部类(LocalClass)
局部类一般声明在方法、语句、代码块内部,可以有明确的名称和修饰符,也可以没有,此时称之为匿名类。局部类使用过程中有几个点需要注意:
1、局部类可以声明在方法内部、for循环、if语句等任意代码块;
2、从JDK8开始,局部类可以直接访问所在方法、statement、block的变量和参数(变量和参数必须是final或等效于final,即初始化后值不再发生改变);
3、局部类不能是static(因为局部类需要访问封闭类中的变量),且局部类内部不能有static变量、方法、static初始化代码块,局部类内部不能声明接口,因为接口天生就是static的。
4、方法(static或者非static)内部的局部类只能访问封闭类的静态变量。
5、静态内部类中可以出现static常量,即 static final 修饰。
6、局部类中的变量声明会屏蔽封闭域内的同名变量。
7、其他类若要实现一个类的某内部接口,可以通过直接实现外部类名.内部接口名的方式实现。
来看官网给的例子:
public class LocalClassExample {
// 必须为static,否则静态方法内的局部类无法访问
static String regularExpression = "[^0-9]";
public static void validatePhoneNumber(String phoneNumber1, String phoneNumber2) {
//本地变量须为final
final int numberLength = 10;
// Valid in JDK 8 and later:
// 或等效final,值不再发生改变
// int numberLength = 10;
class PhoneNumber {
String formattedPhoneNumber = null;
PhoneNumber(String phoneNumber){
// 必须是final或final等效,不能改变值,编译报错
// numberLength = 7;
String currentNumber = phoneNumber.replaceAll(regularExpression, "");
if (currentNumber.length() == numberLength)
formattedPhoneNumber = currentNumber;
else
formattedPhoneNumber = null;
}
public String getNumber() {
return formattedPhoneNumber;
}
// Valid in JDK 8 and later:可以访问封闭方法的参数
// public void printOriginalNumbers() {
// System.out.println("Original numbers are " + phoneNumber1 +
// " and " + phoneNumber2);
// }
}
PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
// Valid in JDK 8 and later:
// myNumber1.printOriginalNumbers();
if (myNumber1.getNumber() == null)
System.out.println("First number is invalid");
else
System.out.println("First number is " + myNumber1.getNumber());
if (myNumber2.getNumber() == null)
System.out.println("Second number is invalid");
else
System.out.println("Second number is " + myNumber2.getNumber());
}
public static void main(String... args) {
validatePhoneNumber("123-456-7890", "456-7890");
}
四、匿名类(AnonymousClass)
与局部类相比,匿名类可以让代码更简洁,声明一个类的同时实例化。与本地类不同的是,匿名类没有名字,在仅使用一次本地类的场景可以考虑使用匿名类。
匿名类的声明
如果说本地类是声明类,那么,匿名类就是表达式,即在表达式内部定义类。来看官网的例子:
public class HelloWorldAnonymousClasses {
// 局部接口
interface HelloWorld {
public void greet();
public void greetSomeone(String someone);
}
public void sayHello() {
// 本地类
class EnglishGreeting implements HelloWorld {
String name = "world";
public void greet() {
greetSomeone("world");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hello " + name);
}
}
HelloWorld englishGreeting = new EnglishGreeting();
// 匿名类1
HelloWorld frenchGreeting = new HelloWorld() {
String name = "tout le monde";
public void greet() {
greetSomeone("tout le monde");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Salut " + name);
}
};
// 匿名类2
HelloWorld spanishGreeting = new HelloWorld() {
String name = "mundo";
public void greet() {
greetSomeone("mundo");
}
public void greetSomeone(String someone) {
name = someone;
System.out.println("Hola, " + name);
}
};
englishGreeting.greet();
frenchGreeting.greetSomeone("Fred");
spanishGreeting.greet();
}
public static void main(String... args) {
HelloWorldAnonymousClasses myApp =
new HelloWorldAnonymousClasses();
myApp.sayHello();
}
}
匿名类语法
匿名类表达式的语法类似调用构造方法,不同点在于,表达式中同时有类的定义。匿名类表达式由以下几部分组成:new操作符、实现或的接口或者继承的类、括号+构造方法参数、body(允许声明方法、字段、代码块,但是不允许使用语句)
封闭域内局部变量的访问、匿名类声明与访问
1、匿名类可以访问所在闭包类的成员变量;
2、匿名类不能访问闭包范围内的非final(或等效final)本地变量;
3、与嵌套类类似,匿名类内部声明的变量名会覆盖闭包类内部的同名变量;
4、匿名类内部不能执行静态初始化,不能声明接口;
5、同样的,匿名类内部可以有静态常量;
6、匿名类内部可以声明Filed、非父类method、实例初始化、本地类
五、lambda表达式
匿名类存在的问题是,当匿名类实现的接口非常简单,比如只有一个方法时,匿名类的语法会显得笨拙且混乱。这种情况下,我们往往希望将函数当作方法参数传递,就像点击了按钮就会执行某种动作一样。lambda表达式可以帮助我们实现这个目标,把函数作为参数,或者把代码当作数据。所以,当类只有一个方法时,匿名类也有点包装过度又复杂,lambda表达式可以让你更简洁的表示一个单方法类的实例。
演进样例
官网给出了使用lambda表达式的理想场景样例,可以看到匿名类一步一步到lambda表达式的简化过程,下面分别来做说明:
// 基础model类
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
1、创建方法,搜索满足某种特征的成员
比如说,输出满足年龄超过35岁的人员信息,通常情况下,我们会创建如下方法:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
这种方法设计的相对比较脆弱,举例来说,当需要引入update功能时,就没法使用了。比如,修改了Person类的结构,修改了age的类型和算法, 你就必须重新设计api。而且这种方法设计本身就有一定局限性,如果我要输出年龄小于35岁的人员要怎么搞呢。
2、创建更为通用的搜索方法
接着来看一个比printPersonsOlderThan更通用的方法:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
同样的,这个方法也有局限性。如果我想输出某个性别或者符合某种性别与年龄组合的成员该怎么搞?如果我要改变Person类的数据结构,比如新增、删除某些属性,这个时候又该怎么搞?尽管通用性比上一个方法好,但是,尝试为每种查询创建单独的方法这种设计仍然十分脆弱。那么,我们把查询条件单独封装到一个类中是不是会更好一些呢?
3、局部类封装查询条件
我们把查询条件单独封装到一个局部类中,把条件组装和查询本身分开,代码如下:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
// 定义条件接口
interface CheckPerson {
boolean test(Person p);
}
//年龄查询条件封装
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
// 使用的时候调用printPersons方法即可
printPersons(roster, new CheckPersonEligibleForSelectiveService());
虽然设计上来看,相对没有那么不堪一击,但是,局部类这种设计,代码一坨一坨的看起来比较糟心,尝试用匿名类来代替。
4、在匿名类中指定搜索条件
直接将CheckPersonEligibleForSelectiveService替换为匿名类,只需要调整printPersons方法,代码如下:
printPersons( roster,new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这种情况减少了很多代码量,因为你无需为每次查询都创建一个新类。但是,考虑到CheckPerson这个接口只有一个方法,匿名类的语法又显得过于笨拙,我们可以尝试用lambda表达式来进一步简化。
5、lambda表达式指定搜索条件
首先,CheckPerson这个接口是一个函数式接口。函数式接口指一个接口仅包含一个抽象方法(当然,函数式接口内部也可能有一个或多个default 方法或者static方法)。因为函数式接口只有一个抽象方法,所以在实现的时候可以直接省略方法名称,改造后代码如下:
printPersons(roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
接下来,JDK已经封装了一批函数式接口,那么我们是不是可以利用JDK提供的标准函数式接口来代替CheckPerson,来减少更多的代码呢。
6、使用lambda表达式的标准函数式接口
先来回忆一下CheckPerson接口的定义,再对比JDK的标准函数式接口,可以发现,与Predicate接口非常相像。
interface CheckPerson {
boolean test(Person p);
}
interface Predicate<Person> {
boolean test(Person t);
}
我们尝试用Predicate<Person>来代替CheckPerson接口,代码如下:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
// 同样利用lambda表达式进一步简化
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
当然这不是可以用lambda表达式的第一个点,你可以扩展到整个应用。
7、整个应用内使用lambda表达式
重新审视printPersonsWithPredicate方法,确认是否有其他地方可以用lambda表达式代替。方法中,输出符合条件的人员信息,那么人员信息输出的动作,就符合Consumer这个函数式接口的语义,我们可以把这个动作用Consumer代替,代码如下:
public static void processPersons(
List<Person> roster, Predicate<Person> tester,Consumer<Person> consumer) {
for (Person p : roster) {
if (tester.test(p)) {
consumer.accept(p);
}
}
}
// 同样的,对printPersonsWithPredicate的使用也要做调整
processPersons( roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果此时不仅仅是输出符合条件的人员信息,还需要对人员做额外处理,那么这个时候可以引入Function接口,如下:
public static void processPersons(
List<Person> roster, Predicate<Person> tester,Function<Person,String> applyer,Consumer<String> consumer) {
for (Person p : roster) {
if (tester.test(p)) {
String data = appler.apply(p);
consumer.accept(data);
}
}
}
// 同样的,对printPersons的使用也要做调整
processPersons( roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getName(),
p -> p.printPerson()
);
8、泛型扩展
重新考虑processPersons方法,引入泛型扩展:
public static <S, R> void processElements(
Iterable<S> source,
Predicate<S> tester,
Function <S, R> mapper,
Consumer<R> block) {
for (S p : source) {
if (tester.test(p)) {
R data = mapper.apply(p);
block.accept(data);
}
}
}
// 同样的,使用方法调整为processElements
processElements( roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getName(),
p -> p.printPerson()
);
方法执行以下操作:从容器中取源对象、利用predicate过滤源对象、利用Function 对源对象进行转换、执行cosumer操作。这四个操作完全可以用一个聚合操作来代替,进一步简化代码
9、使用接受Lambda表达式作为参数的聚合操作
这里就需要借助Stream操作了,对此我们非常熟悉。上面的代码可以改为一行代码:
// 直接省略了范型方法的定义和调用
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
以上9个步骤,见证了代码从普通笨拙到简单灵活的蜕变,不得不说lambda表达式真的是太好用了。
lambda语法
a) 圆括号内以逗号分隔的形式参数列表,参数可以有一个或多个,当只有一个参数时,括号可以省略。
b) 然后是箭头 ->
c)单个表达式或语句块组成的语句体,如果指定单个表达式,那么Java运行时将计算该表达式,然后返回其值;你可以用return语句代替,如下:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
返回语句不是一个表达式;lambda表达式中,所有声明必须放在"{}"中,但是void方法的调用可以不用 "{}",如下:
email -> System.out.println(email)
封闭作用域内局部变量的访问
与局部类和匿名类类似,lambda表达式可以捕获变量,对封闭作用域内局部变量的访问权限也一样。不同之处在于,lambda不存没有变量覆盖的问题,这是因为,lambda是词法范围内的应用,也就是说lambda不会即成任何父类型的名称或者引入新的作用域级别。lambda表达式中对声明的解释与封闭作用域内的声明一致。同样的,lambda表达式只能访问封闭作用域内final(或者等效final)类型的参数或者变量。
目标类型推定(Target Typing)
如何推定lambda表达式的类型呢?仍以上面的代码为例
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
// 下面两个方法都会用到该lambda表达式
public static void printPersons(List<Person> roster, CheckPerson tester);
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
上面的代码中,当JVM调用printPersons时,期望的数据类型是CheckPerson,那么lambda表达式的类型就是CheckPerson;当调用printPersonsWithPredicate时,期望的数据类型是Predicate<Person> tester,那么lambda表达式的类型就是Predicate<Person> tester。我们把方法期望的类型称之为目标类型,Java编译器会根据lambda表达式所在的上下文或者环境的目标类型来确定lambda表达式的类型。而且,只有在Java编译器可以确定目标类型的场景,才可以使用lambda表达式,这是基本规则。这些场景包括:
- Variable declarations 变量声明
- Assignments 赋值
- Return statements 返回语句
- Array initializers 数组初始化
- Method or constructor arguments 方法或者构造器参数
- Lambda expression bodies lambda表达式主体
- Conditional expressions,
?:
条件表达式 - Cast expressions 转型表达式
目标类型和方法参数
对于方法参数来说,Java编译器主要根据两个语言特性来完成类型的推定,分别是重载解析(overload resolution )和类型参数推断(type argument inference),以Callable和Runnable接口为例
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
// 有两个重载方法
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
// 调用的将会是 invoke(Callable<T> c)
String s = invoke(() -> "done");
上面的例子中,() -> "done"就是Callable
序列化
当lambda表达式的参数或者目标类型实现了序列化接口,那么lambda也支持序列化,但是,与内部类一样,强烈建议不要使用lambda的序列化。
六、方法引用(MethodReference)
通常情况下,使用lambda表达式来创建匿名方法,但是,大多数情况下lambda表达式除了调用现有方法以外什么也不做。这种情况下,通过名称引用现有的方法通常更清晰,方法引用可以做到这一点。方法引用是用于已经有名称的方法的紧凑、易于阅读的lambda表达式
JDK官网中介绍的Method reference(方法引用)有四种类型:
类型 | 语法 | 样例 |
---|---|---|
Reference to a static method(静态方法引用) | *ContainingClass*::*staticMethodName* |
Person::compareByAge MethodReferencesExamples::appendStrings
|
Reference to an instance method of a particular object(指定实例方法引用) | *containingObject*::*instanceMethodName* |
myComparisonProvider::compareByName myApp::appendStrings2
|
Reference to an instance method of an arbitrary object of a particular type(特定类型的任意实例对象方法引用) | *ContainingType*::*methodName* |
String::compareToIgnoreCase String::concat
|
Reference to a constructor(构造方法引用) | *ClassName*::new |
HashSet::new |
这是官网给的例子,有兴趣的同学可以对照四种类型自己实践一下,辅助类Person,
public class Person {
LocalDate birthday;
public int getAge() {}
public String getName() {}
public LocalDate getBirthday() { return birthday;}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
}
静态方法引用
静态方法引用比较简单,常见如Collections::sort的用法,这里就不再做过多叙述。
指定对象的实例方法引用
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
// 引用myComparisonProvider对象的实例方法
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
rosterAsArray是一个Person数组,compareByName实例方法是参数是<Person,Person>,可以看到用法也比较简单。
特定类型任意对象的实例方法的引用
String[] stringArray = { "Barbara", "James", "Mary", "John","Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
样例也比较简单,不做过多说明
构造方法引用
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>> DEST transferElements(SOURCE sourceCollection,Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
上面的样例中,Supplier内部是一个get方法,没有参数,只有一个返回值。通常情况下,采用lambda表达式调用transferElements的方式如下:
// 初始状态
Set<Person> rosterSetLambda = transferElements(roster, () -> { return new HashSet<>(); });
// 进一步简化
Set<Person> rosterSet = transferElements(roster, HashSet::new);
// 编译器发现你希望创建一个元素类型是Person的容器,指定元素类型
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
七、使用嵌套类(NestedClass)、内部类(InnerClass)、匿名类(AnonymousClass)还是lambda表达式?
应该怎么确定在哪些场景使用那种类呢,比较简单的办法是根据各种类型类的可用场景来选择,官网同样给出了建议:
LocalClass当需要创建一个类的多个实例、访问其构造方法或者引入一个新的命名类型(比如,稍后你需要调用其他方法)时,可以使用。
AnonymousClass需要声明单独field或者方法时
Lambda表达式当需要传递单个行为单元给某代码时;或者需要使用函数式接口的简单实例而且以上均不满足的情况下。
NestedClass类似于LocalClass的需求,但是希望该类型更广泛地可用,且不需要访问局部变量或方法参数时。注意:如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果不需要,请使用静态嵌套类。
总结
回顾完以上知识点,我们再回到本文开头提到的问题。四种方法引用都介绍完了,你可能想问,并没有看到这四种方法引用跟开头讲的例子有什么关系啊,别急,慢慢来看。先把接口定义拎出来:
// 接口定义,函数式接口
public interface NotifyListener {
void notify(List<String> urls);
}
我们从常用的匿名类写法开始:
public static void main(String[] args) {
final AtomicReference<List<String>> reference = new AtomicReference<>();
NotifyListener listener = new NotifyListener() {
@Override
public void notify(List<String> urls) {
reference.set(urls);
}
};
listener.notify(urls);
List<String> result = reference.get();
result.forEach(System.out::println);
}
NotifyListener是函数式接口(仅定义了一个抽象方法),可以利用lambda表达式进行简化:
public static void main(String[] args) {
final AtomicReference<List<String>> reference = new AtomicReference<>();
// urls是函数式接口中抽象方法的参数
NotifyListener listener = urls -> reference.set(urls);
listener.notify(urls);
List<String> result = reference.get();
result.forEach(System.out::println);
}
然后在利用方法引用(实例方法引用)进一步简化:
public static void main(String[] args) {
final AtomicReference<List<String>> reference = new AtomicReference<>();
NotifyListener listener = reference::set;
listener.notify(urls);
List<String> result = reference.get();
result.forEach(System.out::println);
}
最终得到源码中的写法。在阅读源码过程中,如果遇到类似难以理解的代码,可以参考进行反向推断。不正之处,多多指教。