Lambda Expressions
匿名类的一大问题是,如果实现很简单,例如单方法接口,那么它的语法显得笨拙且模糊。这种情况下,你通常试图将函数作为参数传递给其它方法,比如用户点击按钮要采取的动作。Lambda [ˈlæmdə]
表达式允许你把函数当成方法参数,或把代码看成数据。
前章 Anonymous Classes 展示了如何不具名实现基类。尽管它比具名类简洁,但对于单方法类,也有点多余和累赘。Lambda 表达式允许你更紧凑地表达单方法类型实例。
本章涵盖如下话题:
Ideal Use Case for Lambda Expressions
假设你在编写社交网络应用。你想编写一个通用函数方便管理员执行操作,比如发送消息给满足特定规则的成员。用例的详细描述见下表:
Field | Description |
---|---|
成员名称 | 在该成员上执行操作 |
主要角色 | 管理员 |
前置条件 | 管理员已登录系统 |
后置条件 | 只在满足特定条件的成员上执行动作 |
主要场景 | 1. 管理员指定执行操作的成员条件 2. 管理员在选定成员上执行指定动作 3. 管理员按下 Submit 按钮 4. 系统寻找所有满足条件的成员 5. 系统在匹配成员上执行动作 |
扩展功能 | 管理员在执行动作或点击提交前可以预览匹配条件的成员 |
使用频次 | 每天多次使用 |
假定社交网络应用的成员使用如下 Person 类表示:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
成员存储在 List<Person>
实例中。我们首先使用原生方法完成用例。随后通过局部匿名类改进,最后使用高效简洁的 lambda 表达式。你可以在 RosterTest 找到完整代码。
Approach 1: Create Methods That Search for Members That Match One Characteristic
最简单的方式是创建几个方法,每个方法搜寻匹配一个特征的成员,比如性别或年龄。如下方法打印年龄大于指定条件的成员:
public static void printPersonOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
[tip type="info" title="备注"]
List 是有序 Collection。Collection
对象是由多个元素组成的单元,用于存储、检索、操作和传递聚合数据。更多信息见 Collections 教程。
[/tip]
这种方式可能让你的应用很 brittle
,它表示由于更新(例如新数据类型)导致应用无法工作的可能性。假设你升级应用改变了 Person
类让它包含不同成员变量,也许以不同的数据类型或算法记录和测量年龄,你必须重写许多 API 适应这种改变。此外,这种方式的限制性太强,比如,如果你想打印小于特定年龄的成员呢?
Approach 2: Create More Generalized Search Methods
下面的方法比 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();
}
}
}
如果你想打印指定性别,或者结合指定性别和年龄范围的成员该怎么办呢?如果决定增加其它属性,例如关系状态或地理位置又该怎么办呢?尽管此类比 printPersonsOlderThan
通用,但试图为每种可能搜索创建单独方法还是会导致代码脆弱。你可以分离搜索条件的代码到不同类中。
Approach 3: Specify Search Criteria Code in a Local Class
以下方法打印匹配特定搜索条件的成员:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
该方法检查 roster
中的每个 Person
,看它是否满足指定的搜索条件 tester
,通过调用 tester.test
方法。如果它返回 true
,就调用 Person
实例的 printPerson
。
要指定搜索条件,你必须实现 CheckPerson
接口:
interface CheckPerson {
boolean test(Person person);
}
下面是一个实现类,它的 test
方法用于过滤适合服兵役的成员:如果 Person
参数是男性并且年龄在 18 到 25 之间,它返回 true
:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
@Override
public boolean test(Person person) {
return person.getGender() == Person.Sex.MALE
&& person.getAge() >= 18
&& person.getAge() <= 25;
}
}
要使用它,创建一个实例,并调用 printPersons
方法:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
虽然这个方法不那么脆弱 —— 更新 Person
结构后,你无需重写方法 —— 但仍需额外代码:每种搜索都需新的接口和局部类。因为 CheckPersonEligibleForSelectiveService
实现了接口,你可以使用匿名类代替局部类,从而省略为每个搜索声明新类。
Approach 4: Specify Search Criteria Code in an Anonymous Class
下面使用匿名类调用 printPersons
,功能同上述局部类:
printPersons(roster,
new CheckPerson() {
@Override
public boolean test(Person person) {
return person.getGender() == Person.Sex.MALE
&& person.getAge() >= 18
&& person.getAge() <= 25;
}
});
这种方式减少了代码量,因为你无需为每个搜索单独创建新类。但是,考虑到 CheckPerson
接口仅含一个方法,匿名类语法还是很笨重。这种情况下,你可以使用 lambda 表达式取代匿名类,就像下面那样。
Approach 5: Specify Search Criteria Code with a Lambda Expression
CheckPerson
是一个 functional interface
。函数接口表示仅含一个 abstract method 的接口。(函数接口可以包含一到多个 default methods 和 static methods)由于函数接口仅含一个抽象方法,实现它时你可以省略方法名称。要这样做,使用 lambda expresstion
代替匿名类,就像以下方法调用的高亮部分:
printPersons(roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);
阅读下方的 Syntax of Lambda Expressions,了解如何定义 lambda 表达式。
你可以使用标准函数接口取代 CheckPerson
,进一步减少代码。
Approach 6: Use Standard Functional Interfaces with Lambda Expressions
重新考虑 CheckPerson
接口:
interface CheckPerson {
boolean test(Person p);
}
它非常简单,是个仅含一个抽象方法的函数接口。该方法接收一个参数,返回一个 boolean
值。它是如此简单以至于不值得单独定义。因此,JDK 定义了几个标准函数接口,你可以在 java.util.function
下发现它们。
例如,你可以使用 Predicate<T>
接口代替 CheckPerson
。该接口包含 boolean test(T t)
方法:
interface Predicate<T> {
boolean test(T t);
}
因此,你可以像下面这样使用它:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
最终,你可以像下面这样查询指定成员:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
此处不是唯一可使用 lambda 表达式的地方。以下示例探讨了其它方式。
Approach 7: Use Lambda Expressions Throughout Your Application
重新考虑 printPersonsWithPredicate
,想象还有什么地方可以使用 lambda 表达式:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
该方法检查 roster
中的每个 Person
,看它是否满足 tester
指定的条件。如果满足,Person
实例的 printPerson
将被调用。
除了调用 printPerson
,你可以使用 lambda 表达式指定不同的动作。假定你想编写一个类似于 printPerson
的 lambda 表达式,它接收一个 Person
类型参数,返回 void。记住,使用 lambda,你需要实现一个函数接口。此处的函数接口需要接收 Person
类型参数,返回 void。Consumer<T>
接口包含 void accept(T t)
方法,它符合这些特征。下例使用该接口调用 accept
取代 p.printPerson()
:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson() /* Person::printPerson */
);
如果你想对成员做更多操作不仅仅是打印它们,那该怎么办呢?假定你想验证成员的资料,或者检索它们的联系人。此时,你需要一个函数接口,它包含一个方法,该方法有返回值。Function<T,R>
接口包含 R apply(T t)
方法。下面的方法获取参数 mapper
指定的数据,随后执行参数 block
指定的动作:
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
下面的调用从 roster
中过滤每个符合兵役条件的成员,获取它们的邮箱并打印:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
Person::getEmailAddress,
System.out::println);
Approach 8: Use Generics More Extensively
重新考虑 processPersonsWithFunction
方法。下面是它的泛型版本,以包含任何数据类型的容器为参数:
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function<X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
它的调用参数和 Approach 7 相同:
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
Person::getEmailAddress,
System.out::println);
该方法调用完成了如下动作:
- 从
source
容器获得对象源。本例中,它从roster
容器获取Person
。注意roster
容器的类型是List
,它也是一个Iterable
对象。 - 过滤匹配
Predicate tester
的对象。本例中,Predicate
是一个 lambda 表达式,指定了成员必须符合兵役规则。 - 将每个过滤对象映射为
Function mapper
指定的值。本例中,Function
是一个 lambda 表达式,它返回成员的邮箱地址。 - 在每个映射对象上执行
Consumer block
指定的动作。本例中,Consumer
是一个 lambda 表达式,它打印一个字符串,该字符串是Function
对象的返回值。
你可以使用聚集操作替代所有这些动作。
Approach 9: Use Aggregate Operations That Accept Lambda Expressions as Parameters
下面的示例使用聚集操作打印 roster
中那些满足兵役条件成员的邮箱地址:
roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(Person::getEmailAddress)
.forEach(System.out::println);
下表是 processElements
与聚集操作之间的映射:
processElements Action | Aggregate Operation |
---|---|
获得对象源 | Stream<E> stream() |
过滤匹配 Predicate 的对象 | Stream<T> filter(Predicate<? super T> predicate) |
映射对象为 Function 指定的值 | <R> Stream<R> map(Function<? super T,? extends R> mapper) |
执行 Consumer 指定的动作 | void forEach(Consumer<? super T> action) |
filter
, map
和 forEach
都是 aggregate operations
。聚集操作处理流中的元素,而非直接从容器获取(这就是为什么第一个调用的方法是 stream
)。stream
是一个元素序列。不像容器,它不是一个储存元素的数据结构。相反,流通过管道,从源,例如容器,搬运数据。pipeline
是流的操作序列,即本例中的 filter-map-forEach
。此外,聚集操作通常接收 lambda 表达式,让你自定义它们的行为。
更全面的聚集操作讨论,见 Aggregate Operations 课程。
Lambda Expressions in GUI Applications
为了处理 graphical user interface (GUI) 应用的事件,如键盘,鼠标点击和滚动动作,你通常需要创建事件处理器,这大多涉及实现特定接口。事件处理器经常是函数接口,它们倾向于只包含一个方法。
在 JavaFX 示例 HelloWorld.java 中(前面的 Anonymous Classes 部分讨论过),你可以将以下匿名类替换成 lambda 表达式:
btn.setOnAction(new EventHandler<ActionEvent>) {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
}
该方法需要一个 EventHandler<ActionEvent>
对象,它包含一个方法 void handle(T event)
,所以是函数接口,你可以使用如下 lambda 表达式取代它:
btn.setOnAction(
event -> System.out.println("Hello World!")
);
Syntax of Lambda Expressions
一个 lambda 表达式由以下部分组成:
- 以逗号分隔,圆括号封闭的形式参数列表。
CheckPerson.test
方法包含一个参数p
,代表Person
类实例。
[tip type="info" title="备注"]
你可以省略表达式中的参数类型。此外,如果只有一个参数,圆括号也可以省略。例如,下面的表达式也是合法的:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
[/tip]
- 箭头标志,
->
- 包含单个表达式或声明块的体。本例使用如下表达式:
p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
如果你指定单个表达式,Java 运行时会执行它并将结果返回。你也可以使用返回声明:
p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
返回声明不是表达式,你必须使用花括号({}
)封闭它。但是,你不需要封闭无返回值方法调用。例如:
email -> System.out.println(email)
注意 lambda 表达式看上去像方法声明,你可以把它看成匿名方法 —— 没有名字的方法。
下面的示例 Calculator 使用了接收多个形式参数的 lambda 表达式:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String[] args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.printf(
"40 + 2 = %d%n", myApp.operateBinary(40, 2, addition));
System.out.printf(
"40 - 2 = %d%n", myApp.operateBinary(40, 2, subtraction));
}
}
Accessing Local Variables of the Enclosing Scope
和局部、匿名类一样,lambda 表达式也能 capture variables,它们对封闭域的局部变量有相同的访问权限。但是,与局部和匿名类不同,lambda 表达式没有任何 shadowing
问题(阅读 Shadowing 了解更多信息)。lambda 表达式有词法作用域,这意味着它不从父类型继承任何名称,或引入新作用域级别。lambda 表达式内的声明就像在封闭环境中被解释一样。下面的例子 LambdaScopeTest 阐释了这点:
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
// The following statement causes the compiler to generate
// the error "local variables referenced from a lambda expression
// must be final or effectively final" in statement A:
//
// x = 99;
Consumer<Integer> myConsumer = (y) ->
{
System.out.println("x = " + x); // Statement A
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println(
"LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String[] args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel f1 = st.new FirstLevel();
f1.methodInFirstLevel(23);
}
}
示例输出如下:
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0
如果你把 myConsumer
声明的 y
换成 x
,那么编译器将产生错误:
Consumer<Integer> myConsumer = (x) -> {
// ...
}
错误提示是:"variable x is already defined in method methodInFirstLevel(int)",因为 lambda 表达式不引入新作用域级别。所以,你可以直接访问封闭域的属性,方法和局部变量。例如,示例可以直接访问 methodInFirstLevel
的参数 x
。要访问封闭类的变量,使用关键字 this
。本例中,this.x
指向成员变量 FirstLevel.x
。
但是,和局部及匿名类一样,lambda 表达式只能访问封闭块中的 final
或 等效 final
局部变量或参数。例如,假设你在 methodInFirstLevel
定义后立马加上以下赋值声明:
void methodInFirstLevel(int x) {
x = 99;
// ...
}
由于该赋值语句,FirstLevell.x
不再是等效 final
变量。最终,Java 编译器将在 lambda 表达式访问 FirstLevel.x
的地方生成类似 "local variables referenced from a lambda expression must be final or effectively final" 的错误信息:
System.out.println("x = " + x);
Target Typing
你如何确定 lambda 表达式的类型?回想选择男性且年龄在 18 到 25 之间成员的 lambda 表达式:
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
该表达式用在了以下两个方法中:
- Approach 3 的
public static void printPersons(List<Person> roster, CheckPerson tester)
- Approach 6 的
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
当 Java 运行时调用 printPersons
方法时,它期待 CheckPerson
类型数据,所以 lambda 表达式就是该类型。而调用 printPersonsWithPredicate
方法时,它期待 Predicate<Person>
,所以 lambda 表达式变为此类型。方法期待的数据类型叫 target type
。Java 编译器使用表达式所在上下文的目标类型确定 lambda 表达式类型。只有在以下 Java 编译器可以确定目标类型的情形下,才可以使用 lambda 表达式:
- 变量声明
- 赋值
- 返回声明
- 数组初始化
- 方法或构造器参数
- Lambda 表达式体
- 条件表达式
?:
- Cast 表达式
Target Types and Method Arguments
对于方法参数,Java 编译器使用其它两个语言特性确定目标类型:重载解析和类型参数推导。
考虑下面两个函数接口(java.lang.Runnable 和 java.util.concurrent.Callable\<V\>):
public interface Runnable {
void run();
}
public interfade Callable<V> {
V call();
}
方法 Runnable.run
没有返回值,Callable.call
有。
假定你有如下重载方法 invoke
(阅读 Defining Methods 详细了解重载方法):
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
下面的声明会调用那个方法呢?
String s = invoke(() -> "done");
invoke(Callable<T>)
会被调用,因为它有返回值。此时,lambda 表达式 () -> "done"
的类型是 Callable<T>
。
Serialization
你可以 serialize 一个 lambda 表达式,如果它的目标类型和捕获参数是可序列化的。但是,和 inner classes 一样,不推荐 lambda 表达式序列化。
Method References
Lambda Expressions 可以创建匿名方法。但是有时,lambda 除了调用已有方法什么也不做。此时,引用已有方法名更清晰。方法引用允许你这样做,它们是紧凑易读的具名方法 lambda 表达式。
重新考虑前面的 Person 类:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public Calendar getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
}
假定社交网络应用的成员存储在数组中,你想让它们按年龄排序,可以使用如下代码(完整代码见 MethodReferencesTest):
Person[] rosterAsArray = roster.toArray(new Person[0]);
class PersonAgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
Arrays.sort(rosterAsArray, new PersonAgeComparator());
被调方法 sort
签名如下:
static <T> void sort(T[] a, Comparator<? super T> c)
Comparator
是一个函数接口。因此,你可以使用 lambda 表达式代替接口实现和实例创建:
Arrays.sort(rosterAsArray,
(Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
}
);
但是,已经存在 Person.compareByAge
,你可以直接调用它代替 lambda 表达式体:
Arrays.sort(rosterAsArray,
(a, b) -> Person.compareByAge(a, b)
);
由于该 lambda 表达式调用已有方法,你可以使用方法引用代替表达式:
Arrays.sort(rosterAsArray, Person::compareByAge);
方法引用 Person::compareByAge
在语义上和 lambda 表达式 (a, b) -> Person.compareByAge(a, b)
等价。都有如下特征:
- 形式参数列表拷贝自
Comparator<Person>.compare
,即(Person, Person)
- 体调用
Person.compareByAge
方法
Kinds of Method References
存在四种方法引用:
Kind | Example |
---|---|
静态方法引用 | ContainingClass::staticMethodName |
具名实例方法引用 | containingObject::instanceMethodName |
任意类型不具名实例方法引用 | ContainingType::methodName |
构造器引用 | ClassName::new |
Reference to a Static Method
Person::compareByAge
便是静态方法引用。
Reference to an Instance Method of a Particular Object
下面是一个具名实例方法引用:
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();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
方法引用 myComparisonProvider::compareByName
调用 myComparisonProvider
对象的 compareByName
方法。JRE 会推断方法参数类型,此处是 (Person, Person)
。
Reference to an Instance Method of an Arbitrary Object of a Particular Type
下例展示了任意特定类型对象的不具名实例方法引用:
String[] stringArray = {
"Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda"
};
Arrays.sort(stringArray, String::compareToIgnoreCase);
String::compareToIgnoreCase
等价 lambda 表达式的形式参数列表为 (String a, String b)
,a
和 b
是为了方便描述随意起的名字。方法引用会调用 a.compareToIgnoreCase(b)
。
Reference to a Constructor
你可以像引用静态方法那样引用构造函数,区别是方法名为 new
。以下方法将一个容器中的元素复制到另一个:
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
result.addAll(sourceCollection);
return result;
}
函数接口 Supplier
包含一个方法 get
,它没有参数,返回一个对象。因此,可以使用如下 lambda 表达式调用 transferElements
:
Set<Person> rosterSet = transferElements(roster, () -> {
return new HashSet<>();
});
你可以使用构造器引用取代 lambda 表达式:
Set<Person> rosterSetLambda = transferElements(roster, HashSet::new);
Java 编译器能推断出你想创建一个包含 Person
的 HashSet
。你可以显示指定元素类型:
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);
When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions
就像 Nested Classes 提到的,嵌套类允许你将只在一处使用的类放到同一逻辑分组,从而创建易读可维护的高内聚代码。局部类,匿名类和 lambda 表达式也具有这些优点,但它们的使用场景更具体:
- Local Classes:使用它如果你需要创建多个实例,访问它的构造器,或引入具名类型(例如,你需要调用对象的额外方法)。
- Anonymous Classes:使用它如果你需要声明属性和附加方法。
- 使用它如果你正在封装传递给其它代码的单个行为单元。例如,当进程完成或遇到错误时,在容器的每个元素上执行特定操作。
- 使用它如果你只需简单的函数接口实例或者已有条件不满足(例如,你不需要构造器,具名类型,属性或附加方法)。
Nested Classes:使用它如果你的需求类似局部类,但你需要在更广范围使用它,而它又不依赖局部变量或方法参数。
- 如果你需要访问封闭类实例的非公有属性和方法,使用非静态嵌套类(或者叫内部类)。如果不需要访问它们,使用静态嵌套类。
本文译自 Lambda Expressions,译者 LOGI。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »