文章目录

  • 简介
  • 使用Lambda表达式的优缺点
  • 基本概念
    • 函数式接口
    • 接口默认方法
    • 嵌套类(Nested Classes)
  • 使用Lambda表达式的前提
  • 基础语法
  • Lambda表达式的重要特征
    • 目标类型与类型推断
    • 作用域
    • 方法引用
      • 静态方法引用
      • 指定对象实例方法引用
      • 特定类型任意对象方法引用
      • 超类方法引用
      • 构造器方法引用
      • 数组构造器方法引用
  • Java8内置的函数式接口
    • 消费型接口
    • 供给型接口
    • 函数型接口
    • 断言型接口

简介

Lambda表达式(也称闭包),是Java8中最受期待和欢迎的新特性之一。Lambda表达式本质是一个匿名函数,但是它并不是匿名类的语法糖,它让 Java 开始走向函数式编程,其实现原理区别于一般的匿名类中的匿名函数。在Java语法层面Lambda表达式允许函数作为一个方法的参数(函数作为参数传递到方法中),或者把代码看成数据。Lambda表达式可以简化函数式接口的使用。函数式接口就是一个只有一个抽象方法的普通接口,像这样的接口就可以使用Lambda表达式来简化代码的编写。

使用Lambda表达式的优缺点

引入Lambda表达式的初衷:如果一个接口只包含一个方法,那么匿名类的语法会变得十分笨拙和不清楚,产生大量的模板代码,归结一下就是:代码冗余是匿名类的最大弊端。在编程的时候,我们很多时候希望把功能作为参数传递到另一个方法,Lambda表达式就是为此而生。

优点

  • 使用Lambda表达式可以简化接口匿名内部类的代码,可以减少类文件的生成,同时引入了强大的类型推断和方法引用特性,简单的功能甚至可以一行代码解决,解放匿名类的束缚。

  • 把功能作为参数向下传递,为函数式编程提供了支持,让 Java 开始走向函数式编程。

缺点

使用Lambda表达式会减弱代码的可读性,而且Lambda表达式的使用局限性比较强,只能适用于接口只有一个抽象方法时使用,不方便调试。

基本概念

函数式接口

  • 有且只有一个抽象方法的接口被为函数式接口
  • 只有函数式接口,才可以转换为lambda表达式
  • 接口默认方法必须予以实现,它们不是抽象方法
  • 函数式接口可以显式的被@FunctionalInterface所表示,当被标识的接口不满足规定时,编译器会提示报错

接口默认方法

接口默认方法的含义可以见Java官方教程中对应的章节,在文末的参考资料可以查看具体的链接:

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

简单来说就是:默认方法允许你在你的类库中向接口添加新的功能,并确保新增的默认方法与这些接口的较早版本编写的代码二进制兼容

接口默认方法(下称默认方法)通过default关键字声明,可以直接在接口中编写方法体。也就是默认方法既声明了方法,也实现了方法。这一点很重要,在默认方法特性出现之前,Java编程语言规范中,接口的本质是抽象方法的集合,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:

public interface DefaultMethod {default void defaultVoidMethod() {}default String sayHello(String name) {return String.format("%s say hello!", name);}static void main(String[] args) throws Exception {class Impl implements DefaultMethod {}DefaultMethod defaultMethod = new Impl();System.out.println(defaultMethod.sayHello("thinkwon"));}
}

如果继承一个定义了默认方法的接口,那么可以有如下的做法:

  • 完全忽略父接口的默认方法,那么相当于直接继承父接口的默认方法的实现(方法继承)。
  • 重新声明默认方法,这里特指去掉default关键字,用public abstract关键字重新声明对应的方法,相当于让默认方法转变为抽象方法,子类需要进行实现(方法抽象)。
  • 重新定义默认方法,也就是直接覆盖父接口中的实现(方法覆盖)。

结合前面一节提到的函数式接口,这里可以综合得出一个结论:函数式接口,也就是有且仅有一个抽象方法的接口,可以定义0个或者N(N >= 1)个默认方法

这一点正是Stream特性引入的理论基础。举个例子:

@FunctionalInterface
public interface CustomFunctionalInterface {public abstract void process();default void defaultVoidMethod() {}default String sayHello(String name) {return String.format("%s say hello!", name);}
}

这里说点题外话。

在写这篇文章的时候,笔者想起了一个前同事说过的话,大意如下:在软件工程中,如果从零做起,任何新功能的开发都是十分简单的,困难的是在兼容所有历史功能的前提下进行新功能的迭代。试想一下,Java迭代到今天已经过去十多年了,Hotspot VM源码工程已经十分庞大(手动编译过OpenJDK Hotspot VM源码的人都知道过程的痛苦),任何新增的特性都要向前兼容,否则很多用了历史版本的Java应用会无法升级新的JDK版本。既要二进制向前兼容,又要迭代出新的特性,Java需要进行舍夺,默认方法就是一个例子,必须舍去接口只能定义抽象方法这个延续了多年在Java开发者中根深蒂固的概念,夺取了基于默认方法实现构筑出来的流式编程体系。笔者有时候也在思考:如果要我去开发Stream这个新特性,我会怎么做或者我能怎么做?

嵌套类(Nested Classes)

嵌套类(Nested Classes),简单来说就是:在一个类中定义另一个类,那么在类内被定义的那个类就是嵌套类,最外层的类一般称为封闭类(Enclosing Class)。嵌套类主要分为两种:静态嵌套类和非静态嵌套类,而非静态嵌套类又称为内部类(Inner Classes

// 封闭类
class OuterClass {...// 静态嵌套类static class StaticNestedClass {...}// 内部类class InnerClass {...}
}

静态嵌套类可以直接使用封闭的类名称去访问例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();,这种使用形式和一般类实例化基本没有区别。

内部类实例的存在必须依赖于封闭类实例的存在,并且内部类可以直接访问封闭类的任意属性和方法,简单来说就是内部类的实例化必须在封闭类实例化之后,并且依赖于封闭类的实例,声明的语法有点奇特:

public class OuterClass {int x = 1;static class StaticNestedClass {}class InnerClass {// 内部类可以访问封闭类的属性int y = x;}public static void main(String[] args) throws Exception {OuterClass outerClass = new OuterClass();// 必须这样实例化内部类 - 声明的语法相对奇特OuterClass.InnerClass innerClass = outerClass.new InnerClass();// 静态嵌套类可以一般实例化,形式为:封闭类.静态嵌套类OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass();// 如果main方法在封闭类内,可以直接使用静态嵌套类进行实例化StaticNestedClass x = new StaticNestedClass();}
}

内部类中有两种特殊的类型:本地类(Local Classes)和匿名类(Anonymous Classes)。

本地类是一种声明在任意块(block)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:

public class OuterClass {static int y = 1;{    // 本地类Aclass A{int z = y;}A a = new A();}static {// 本地类Bclass B{int z = y;}B b = new B();}private void method(){// 本地类Cclass C{int z = y;}C c = new C();}
}

匿名类可以让代码更加简明,允许使用者在定义类的同时予以实现,匿名类和其他内部类不同的地方是:它是一种表达式,而不是类声明。例如:

public class OuterClass {interface In {void method(String value);}public void sayHello(){// 本地类 - 类声明class LocalClass{}// 匿名类 - 是一个表达式In in = new In() {@Overridepublic void method(String value) {}};}
}

嵌套类的类型关系图如下:

Nested Classes- Static Nested Classes- None Nested Classes- Local Classes- Anonymous Classes- Other Inner Classes

使用Lambda表达式的前提

只适用于函数式接口,即接口有且只有一个抽象方法!!!

基础语法

在认识Lambda表达式基础语法之前,先来看一段用两种方式创建线程的代码

// 创建线程
// 匿名类
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("Hello!");}
}).start();// Lambda 表达式
new Thread(() -> System.out.println("Hello!")).start();

Lambda 表达式的基础语法:Java8中引入了一个新的操作符 “->” 该操作符称为箭头操作符或 Lambda 操作符

箭头操作符将 Lambda 表达式拆分成两部分:

左侧:Lambda 表达式的参数列表

右侧:Lambda 表达式中所需实现的功能, 即 Lambda 体

Lambda表达式的重要特征

  • 可选参数类型声明: 不需要声明参数类型,编译器可以统一识别参数值。

    也就说(s) -> System.out.println(s)和 (String s) -> System.out.println(s)是一样的编译器会进行类型推断,所以不需要添加参数类型。

  • 可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。例如:

    1. s -> System.out.println(s) 一个参数不需要添加圆括号。
    2. (x, y) -> Integer.compare(y, x) 两个参数添加了圆括号,否则编译器报错。
  • 可选的Lambda体大括号:如果主体包含了一个语句,就不需要使用大括号。

    1. s -> System.out.println(s) , 不需要大括号.
    2. (s) -> { if (s.equals(“s”)){ System.out.println(s); } }; 需要大括号
  • 可选的返回关键字: 如果Lambda体不加{ }就不用写return,Lambda体加上{ }就需要添加return。

Lambda体不加{ }就不用写return:

 Comparator<Integer> com = (x, y) -> Integer.compare(y, x);

Lambda体加上{ }就需要添加return:

Comparator<Integer> com = (x, y) -> {int compare = Integer.compare(y, x);return compare;
};

目标类型与类型推断

先引入下面的一个场景:

// org.thinkwon.Runnable
@FunctionalInterface
public interface Runnable {void run();static void main(String[] args) throws Exception {java.lang.Runnable langRunnable = () -> {};org.thinkwon.Runnable customRunnable = () -> {};langRunnable.run();customRunnable.run();}
}

定义了一个和java.lang.Runnable完全一致的函数式接口org.thinkwon.Runnable,上面main()方法中,可以看到两个接口对应的Lambda表达式的方法体实现也是完全一致,但是很明显最终可以使用不同类型的接口去接收返回值,也就是这两个Lambda的类型是不相同的。而这两个Lambda表达式返回值的类型是我们最终期待的返回值类型(expecting a data type of XX),那么Lambda表达式就是对应的被期待的类型,这个被期待的类型就是Lambda表达式的目标类型

为了确定Lambda表达式的目标类型,Java编译器会基于对应的Lambda表达式,使用上下文或者场景进行综合推导,判断的一个因素就是上下文中对该Lambda表达式所期待的类型。因此,只能在Java编译器能够正确推断Lambda表达式目标类型的场景下才能使用Lambda表达式,这些场景包括:

  • 变量声明。
  • 赋值。
  • 返回语句。
  • 数组初始化器。
  • Lambda表达式函数体。
  • 条件表达式(condition ? processIfTrue() : processIfFalse())。
  • 类型转换(Cast)表达式。

Lambda表达式除了目标类型,还包含参数列表和方法体,而方法体需要依赖于参数列表进行实现,所以方法参数也是决定目标类型的一个因素

方法参数的类型推导的过程主要依赖于两个语言特性:重载解析(Overload Resolution)和参数类型推导(Type Argument Inference)。

原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference

重载解析会为一个给定的方法调用(Method Invocation)寻找最合适的方法声明(Method Declaration)。由于不同的声明具有不同的签名,当Lambda表达式作为方法参数时,重载解析就会影响到Lambda表达式的目标类型。编译器会根据它对该Lambda表达式所提供的信息的理解做出决定。

如果Lambda表达式具有显式类型(参数类型被显式指定),编译器就可以直接使用Lambda表达式的返回类型;如果Lambda表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略Lambda表达式函数体而只依赖Lambda表达式参数的数量。

举个例子:

// 显式类型
Function<String, String> functionX = (String x) -> x;
// 隐式类型
Function<String, Integer> functionY = x -> Integer.parseInt(x);

如果在依赖于方法参数的类型推导最佳方法声明时存在二义性(Ambiguous),我们则需要:

  • 使用显式Lambda表达式(为参数p提供显式类型)以提供额外的类型信息,ps.stream().map((Person p) -> p.getName());
  • Lambda表达式转型为Function<Person, String>ps.stream().map((Function<Person, String>) p -> p.getName());
  • 为泛型参数R提供一个实际类型。ps.stream().<String>map(p -> p.getName());

举个例子:

public static void main(String[] args) {List<Person> ps = new ArrayList<Person>();Stream<String> names = ps.stream().map(p -> p.getName());
}private static class Person {private final String name;public Person(String name) {this.name = name;}public String getName() {return name;}
}

作用域

关于作用域的问题记住几点即可:

  • <1>Lambda表达式内的this引用和封闭类的this引用相同。
  • <2>Lambda表达式基于词法作用域,它不会从超类中继承任何变量,方法体里面的变量和它外部环境的变量具有相同的语义。
  • <3>Lambda expressions close over values, not variables,也就是Lambda表达式对值类型封闭,对变量(引用)类型开放(这一点正好解释了Lambda表达式内部引用外部的属性的时候,该属性必须定义为final)。

对于第<1>点举个例子:

public class LambdaThis {int x = 1;public void method() {Runnable runnable = () -> {int y = this.x;y++;System.out.println(y);};runnable.run();}public static void main(String[] args) {LambdaThis lambdaThis = new LambdaThis();// 2lambdaThis.method();}}

对于第<2>点举个例子:

public class LambdaScope {public void method() {int x = 1;Runnable runnable = () -> {// 编译不通过 - Lambda方法体外部已经定义了同名变量int x = 2;};runnable.run();}
}

对于第<3>点举个例子:

public class LambdaValue {public void method() {(final) int x = 1;Runnable runnable = () -> {// 编译不通过 - 外部值类型使用了finalx ++;};runnable.run();}
}public class LambdaValue {public void method() {(final) IntHolder holder = new IntHolder();Runnable runnable = () -> {// 编译通过 - 使用了引用类型holder.x++;};runnable.run();}private static class IntHolder {int x = 1;}
}

方法引用

方法引用(Method Reference)是用来直接访问类或者实例已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。

作用

  • 方法引用的唯一用途是支持Lambda的简写。

  • 方法引用提高了代码的可读性,也使逻辑更加清晰。

组成

  • 使用::操作符将方法名和对象或类的名字分隔开。::是域操作符(也可以称作定界符、分隔符)。

常见的方法引用

方法引用 等价的Lambda表达式
String::valueOf x -> String.valueOf(x)
Object::toString x -> x.toString()
x::toString () -> x.toString()
ArrayList::new () -> new ArrayList<>()

方法引用的类型归结如下:

类型 例子
静态方法引用 ClassName::methodName
指定对象实例方法引用 instanceRef::methodName
特定类型任意对象方法引用 ContainingType::methodName
超类方法引用 supper::methodName
构造器方法引用 ClassName::new
数组构造器方法引用 TypeName[]::new

可见其基本形式是:方法容器::方法名称或者关键字

举一些基本的使用例子:

静态方法引用

public class StaticMethodRef {public static void main(String[] args) {Function<String, Integer> function = StaticMethodRef::staticMethod;// 等同于// Function<String, Integer> function1 = (String s) -> StaticMethodRef.staticMethod(s);Integer result = function.apply("10086");// 10086System.out.println(result);}public static Integer staticMethod(String value) {return Integer.parseInt(value);}}

指定对象实例方法引用

public class ParticularInstanceRef {public Integer refMethod(String value) {return Integer.parseInt(value);}public static void main(String[] args) {ParticularInstanceRef ref = new ParticularInstanceRef();Function<String, Integer> function = ref::refMethod;// 等同于// Function<String,Integer> function1 = (String s) -> ref.refMethod(s);Integer result = function.apply("10086");// 10086System.out.println(result);}}

特定类型任意对象方法引用

String[] stringArray = {"C", "a", "B"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
// 等同于
// Arrays.sort(stringArray, (String s1, String s2) -> s1.compareToIgnoreCase(s2));
// [a, B, C]
System.out.println(Arrays.toString(stringArray));

超类方法引用

public class SupperRef {public static void main(String[] args) throws Exception {Sub sub = new Sub();// 10086System.out.println(sub.refMethod("10086"));}private static class Supper {private Integer supperRefMethod(String value) {return Integer.parseInt(value);}}private static class Sub extends Supper {private Integer refMethod(String value) {Function<String, Integer> function = super::supperRefMethod;// 等同于// Function<String,Integer> function1 = (String s) -> super.supperRefMethod(s);return function.apply(value);}}}

构造器方法引用

public class ConstructorRef {public static void main(String[] args) {Function<String, Person> function = Person::new;// 等同于// Function<String,Person> function1 = (String s) -> new Person(s);Person person = function.apply("thinkwon");// dogeSystem.out.println(person.getName());}private static class Person {private final String name;public Person(String name) {this.name = name;}public String getName() {return name;}}}

数组构造器方法引用

Function<Integer, Integer[]> function = Integer[]::new;
// 等同于
// Function<Integer, Integer[]> function1 = (Integer i) -> new Integer[i];
Integer[] array = function.apply(10);
// [null, null, null, null, null, null, null, null, null, null]
System.out.println(Arrays.toString(array));

Java8内置的函数式接口

Java 8 在 java.util.function 包下定义了很多标准函数式接口,主要分为以下几类:

接口 参数 返回值 类别
Consumer T void 消费型接口
Supplier None T 供给型接口
Function T R 函数型接口
Predicate T boolean 断言型接口

消费型接口

Consumer 接口只有一个抽象方法 accept,参数列表只有一个泛型t,无返回值,重点在于内部消费

public class ConsumerTest {public static void main(String[] args) {test("hello", x -> System.out.println(x));}public static <T> void test(T t, Consumer<T> consumer) {consumer.accept(t);}}

如果需要多个参数列表的话,可以考虑使用 ObjLongConsumer

供给型接口

Supplier 只有一个抽象方法 get,参数列表为空,有返回值,返回值得数据类型为T。

public class SupplerTest {public static List<Integer> supply(Integer num, Supplier<Integer> supplier) {List<Integer> list = new ArrayList<Integer>();for (int x = 0; x < num; x++) {list.add(supplier.get());}return list;}public static void main(String[] args) {List<Integer> list = supply(10, () -> (int) (Math.random() * 100));list.forEach(System.out::println);}}

如果需要返回得数据为基本数据类型,可以考虑使用 LongSupplier

函数型接口

Function<T, R> 只有一个抽象方法名为 apply,参数列表只有一个参数为T,有返回值,返回值的数据类型为R。

public class FunctionTest {public static void main(String[] args) {String test = test("hello", x -> x.toUpperCase());System.out.println(test);}public static String test(String str, Function<String, String> function) {return function.apply(str);}}

如果需要多个入参,然后又返回值的话,可以考虑 BiFunction

断言型接口

断言型又名判断型。 Predicate 只有一个抽象方法 test,参数列表只有一个参数为 T,有返回值,返回值类型为 boolean。

public class PredicateTest {public static List<String> filter(List<String> fruit, Predicate<String> predicate) {List<String> f = new ArrayList<>();for (String s : fruit) {if (predicate.test(s)) {f.add(s);}}return f;}public static void main(String[] args) {List<String> fruit = Arrays.asList("香蕉", "哈密瓜", "榴莲", "火龙果", "水蜜桃");List<String> newFruit = filter(fruit, (f) -> f.length() == 2);System.out.println(newFruit);}}

Java8新特性-Lambda表达式相关推荐

  1. Java8新特性----Lambda表达式详细探讨

    Java8新特性 Lambda表达式 入门演示 案例1 如何解决 cannot be cast to java.lang.Comparable问题? 案例2 优化方式一 : 策略设计模式 优化方式二: ...

  2. java8新特性lambda表达式、函数式编程、方法引用和接口默认方法以及内部类访问外部变量

    一提到java是一种什么语言? 大多数人肯定异口同声的说是一门面向对象的语言,这种观点从我们开始学java就已经根深蒂固了,但是学到java8新特性函数式编程的时候,我才知道java并不是纯面向对象的 ...

  3. java8新特性-lambda表达式入门学习

    定义 jdk8发布新特性中,lambda是一大亮点之一.lambda表达式能够简化我们对数据的操作,减少代码量,大大提升我们的开发效率.Lambda 表达式"(lambda expressi ...

  4. Java8新特性——lambda表达式

    什么是lambda表达式? Lambda 表达式是Java 8 的新特性,是一种新的编程语法.lambda语义简洁明了,性能良好,是Java 8 的一大亮点.废话不多说,我们来看个例子. 从内部类到l ...

  5. java8新特性lambda表达式概述

    定义 ​ jdk8发布新特性中,lambda是一大亮点之一.lambda表达式能够简化我们对数据的操作,减少代码量,大大提升我们的开发效率.Lambda 表达式"(lambda expres ...

  6. java8新特性-lambda表达式和stream API的简单使用

    一.为什么使用lambda Lambda 是一个 匿名函数,我们可以把 Lambda表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递).可以写出更简洁.更灵活的代码.作为一种更紧凑的代码风 ...

  7. java compare 返回值_关于Java你不知道的那些事之Java8新特性[Lambda表达式和函数式接口]...

    前言 为什么要用Lambda表达式? Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码,将代码像数据一样传递,这样可以写出更简洁.更灵活的代码,作为一个更紧凑的代码风 ...

  8. 2020.10.20课堂笔记(java8新特性 lambda表达式)

    一.什么是Lambda? 我们知道,对于一个Java变量,我们可以赋给其一个"值". 如果你想把"一块代码"赋给一个Java变量,应该怎么做呢? 比如,我想把右 ...

  9. Java8 新特性 -- Lambda表达式:函数式接口、方法的默认实现和静态方法、方法引用、注解、类型推测、Optional类、Stream类、调用JavaScript、Base64

    文章目录 1. Lambda表达式 1.1 Lambda表达式语法 1.2 Lambda表达式示例 1.3 说明:函数式接口 2. 方法的默认实现和静态方法 3. 方法引用 3.1 方法引用示例 4. ...

  10. Java8 新特性lambda表达式(一)初始

    本篇参考Richard Warburton的 java8 Lambdas :Functional Programming for the Masses 学习lambda表达式之前,需要知道什么是函数式 ...

最新文章

  1. form表单嵌套,用标签的form属性来解决表单嵌套的问题
  2. php ios视频文件上传,iOS实现视频和图片的上传思路
  3. 2019CCPC网络选拔赛签到题题解
  4. 【杂谈】从CV小白到人脸表情识别专栏作者,我与有三AI的故事
  5. Linux基础书籍推荐
  6. 产品经理日常数据分析工作
  7. VS2017创建ASP.NET Core Web程序
  8. 百度SEO Cloud-Platform(后台管理系统) v3.1.0
  9. 后端如何收取多个文件_前段文件分片后后端怎么接收
  10. js中的forEach
  11. BertEmbedding的各种用法
  12. PXE无人值守安装linux后无法启动图形
  13. 如何让jpa 持久化时不校验指定字段
  14. c++如何生成指定范围的随机数
  15. django + mysql 支持表情包
  16. Tomcat9百度云下载
  17. Hive SQL之表与建表
  18. 游戏或制图用的计算机配置单,自己组装电脑配置单6000元左右适合PS制图与吃鸡游戏的电脑配置推荐...
  19. 获取 iOS 设备 UDID
  20. 【转】 Linux那些事儿之我是U盘(16)冰冻三尺非一日之寒

热门文章

  1. Python-----列表,字典,集合生成式,生成器
  2. 《深入理解Windows操作系统》笔记5
  3. 200+套HTML以及HTML5静态网页网站模板收藏
  4. java离线_java8离线版软件下载
  5. 《 免费手机WAP网站大全》
  6. 轴承行业PLM解决方案
  7. 码农小汪-锁 LOCK
  8. 2023计算机毕业设计SSM最新选题之javaOA办公系统y7x0p
  9. 在linux系统下如何下载中文输入法,如何在linux系统下安装中文输入法
  10. 复试口语常见话题整理以及华师18 19年topic