当前位置:   article > 正文

Lambda表达式_lambda 表达式:

lambda 表达式:

1.什么是Lambda表达式

Lambda表达式,也可称为闭包。类似于JavaScript中的闭包,它是推动Java8发布的最重要的新特性。

2.为什么使用Lambda表达式

我们可以把Lambda表达式理解为一段可以传递的代码(将代码像数据一样进行传递)。Lambda允许把函数作为一个方法的参数,使用Lambda表达式可以写出更简洁、更灵活的代码,而其作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

一个简单示例:

分别使用成员内部类、局部内部类、静态内部类、匿名内部类方式实现Runnable的run()方法并创建和启动线程,如下所示:

public class LambdaDemo {

/**
 * 成员内部类
 */
class MyThread01 implements Runnable{
    @Override
    public void run() {
        System.out.println("成员内部类:用Lambda语法创建线程吧!");
    }
}

/**
 * 静态内部类
 */
static class MyThread02 implements Runnable{
    @Override
    public void run() {
        System.out.println("静态内部类:对啊,用Lambda语法创建线程吧!");
    }
}

public static void main(String[] args) {

    /**
     * 局部内部类
     */
    class MyThread03 implements Runnable{
        @Override
        public void run() {
            System.out.println("局部内部类:用Lambda语法创建线程吧!");
        }
    }

    /**
     * 匿名内部类
     */
    Runnable runnable = new Runnable(){

        @Override
        public void run() {
            System.out.println("匿名内部类:求求你,用Lambda语法创建线程吧!");
        }
    };
    //成员内部类方式
    LambdaDemo lambdaDemo = new LambdaDemo();
    MyThread01 myThread01 =lambdaDemo.new MyThread01();
    new Thread(myThread01).start();
    //静态内部类方式
    MyThread02 myThread02 = new MyThread02();
    new Thread(myThread02).start();
    //局部内部类
    MyThread03 myThread03 = new MyThread03();
    new Thread(myThread03).start();
    //匿名内部类的方式
    new Thread(runnable).start();
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

可以看到上面创建方式,代码量都不少,使用Lambda表达式实现,如下所示:

 //Lambda方式
new Thread(() -> System.out.println("使用Lambda就对了")).start();
12
  • 1
  • 2
  • 3

可以看到代码明显简洁了许多。

3.Lambda表达式语法

Lambda表达式在Java语言中引入了一个操作符**“->”**,该操作符被称为Lambda操作符或箭头操作符。它将Lambda分为两个部分:

左侧:指定了Lambda表达式需要的所有参数
右侧:制定了Lambda体,即Lambda表达式要执行的功能。
像这样:

(parameters) -> expression
或
(parameters) ->{ statements; }

  • 1
  • 2
  • 3
  • 4

以下是lambda表达式的重要特征:

可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
下面对每个语法格式的特征进行举例说明:

(1)语法格式一:无参,无返回值,Lambda体只需一条语句。如下:

  @Test
  public void test01(){
    Runnable runnable=()-> System.out.println("Runnable 运行");
    runnable.run();//结果:Runnable 运行
  }
  • 1
  • 2
  • 3
  • 4
  • 5

(2)语法格式二:Lambda需要一个参数,无返回值。如下:

  @Test
  public void test02(){
    Consumer<String> consumer=(x)-> System.out.println(x);
    consumer.accept("Hello Consumer");//结果:Hello Consumer
  }
  • 1
  • 2
  • 3
  • 4
  • 5

(3)语法格式三:Lambda只需要一个参数时,参数的小括号可以省略,如下:

  public void test02(){
    Consumer<String> consumer=x-> System.out.println(x);
    consumer.accept("Hello Consumer");//结果:Hello Consumer
  }
  • 1
  • 2
  • 3
  • 4

(4)语法格式四:Lambda需要两个参数,并且Lambda体中有多条语句。

  @Test
  public void test04(){
    Comparator<Integer> com=(x, y)->{
      System.out.println("函数式接口");
      return Integer.compare(x,y);
    };
    System.out.println(com.compare(2,4));//结果:-1
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

(5)语法格式五:有两个以上参数,有返回值,若Lambda体中只有一条语句,return和大括号都可以省略不写

  @Test
  public void test05(){
    Comparator<Integer> com=(x, y)-> Integer.compare(x,y);
    System.out.println(com.compare(4,2));//结果:1
  }
  • 1
  • 2
  • 3
  • 4
  • 5

(6)Lambda表达式的参数列表的数据类型可以省略不写,因为JVM可以通过上下文推断出数据类型,即“类型推断”

  @Test
  public void test06(){
    Comparator<Integer> com=(Integer x, Integer y)-> Integer.compare(x,y);
    System.out.println(com.compare(4,2));//结果:1
  }
  • 1
  • 2
  • 3
  • 4
  • 5

类型推断:在执行javac编译程序时,JVM根据程序的上下文推断出了参数的类型。Lambda表达式依赖于上下文环境。

语法背诵口诀:左右遇一括号省,左侧推断类型省,能省则省。

4.函数式接口

4.1 什么是函数式接口

==只包含一个抽象方法的接口,就称为函数式接口。==我们可以通过Lambda表达式来创建该接口的实现对象。
我们可以在任意函数式接口上使用@FunctionalInterface注解,这样做可以用于检测它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。

4.2 自定义函数式接口

按照函数式接口的定义,自定义一个函数式接口,如下:

@FunctionalInterface
public interface MyFuncInterf<T> {
   public T getValue(String origin);
}
  • 1
  • 2
  • 3
  • 4

定义一个方法将函数式接口作为方法参数。

  public String toLowerString(MyFuncInterf<String> mf,String origin){
    return mf.getValue(origin);
  }

  • 1
  • 2
  • 3
  • 4

将Lambda表达式实现的接口作为参数传递。

    @Test
    public void test07() {
        String value = toLowerString((str) -> {
            return str.toLowerCase();
        }, "ABC");
        System.out.println(value);//结果ABC
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
4.3 Java内置函数式接口

四大核心函数式接口的介绍,如图所示:

使用示例:

1.Consumer:消费型接口 void accept(T t)

    public void makeMoney(Integer money, Consumer<Integer> consumer) {
        consumer.accept(money);
    }

    @Test
    public void test01() {
        makeMoney(100, t -> System.out.println("今天赚了" + t));//结果:今天赚了100
        makeMoney(100, System.out::println);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.Supplier:供给型接口 T get()

  /**

产生指定的整数集合放到集合中

Iterable接口的forEach方法的定义:方法中使用到了Consumer消费型接口,

default void forEach(Consumer<? super T> action) {

Objects.requireNonNull(action);

for (T t : this) {

action.accept(t);

}

}
*/
    @Test
    public void test02() {

        List list = addNumInList(10, () -> (int) (Math.random() * 100));
        list.forEach(t -> System.out.println(t));

    }

    public List addNumInList(int size, Supplier<Integer> supplier) {
        List<Integer> list = new ArrayList();
        for (int i = 0; i < size; i++) {
            list.add(supplier.get());
        }
        return list;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

这时候小白肯定就要问了为什么(int) (Math.random() * 100)加return不行,因为他生产的东西不是我们所需要的List最最最主要的原因是因为去掉大括号的同时要去掉return
3.Function<T,R>:函数型接口 R apply(T t)

    /**
     * 使用函数式接口处理字符串。
     */
    public String handleStr(String s, Function<String, String> f) {
        return f.apply(s);
    }

    @Test
    public void test03() {
        System.out.println(handleStr("abc", (String s) -> s.toUpperCase()));
    }
    //结果:ABC
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4.Predicate:断言型接口 boolean test(T t)

     /**

	自定义条件过滤字符串集合

	*/
    @Test
    public void test04() {
        List<String> strings = Arrays.asList("啊啊啊", "2333", "666", "?????????");
        List<String> stringList = filterStr(strings, (s) -> s.length() > 3);
        for (String s : stringList) {
            System.out.println(s);
        }
    }

    public List<String> filterStr(List<String> list, Predicate<String> predicate) {
        ArrayList result = new ArrayList();
        for (int i = 0; i < list.size(); i++) {
            if (predicate.test(list.get(i))) {
                result.add(list.get(i));
            }
        }
        return result;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们来解析一下这个predicate.test()即把这个括号里的参数传入然后进行判断

我们来扩展一下在一个函数当中我们可以设置多个Predicate predicate

    public static boolean checkString(String string, Predicate<String> pre1, Predicate<String> pre2){
//        return pre1.test(string) && pre2.test(string);
        return pre1.and(pre2).test(string);
    }


    public static void main(String[] args) {
        //定义一个字符串
        String s = "abcdef";
        //调用checkString方法,参数传递字符串和两个lambda表达式
        boolean b = checkString(s,(String str)->{return str.length() > 5;},(String str)->{return str.contains("a");});
        System.out.println(b);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

5.方法引用

当要传递给Lambda体的操作,已经有实现的方法了,就可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用的参数列表一致,方法的返回值也必须一致,即方法的签名一致)。方法引用可以理解为方法引用是Lambda表达式的另外一种表现形式。
方法引用的语法:使用操作符“::”将对象或类和方法名分隔开。
方法引用的使用情况共分为以下三种:

对象::实例方法名
类::静态方法名
类::实例方法名
使用示例:
1.对象::实例方法名

    /**
     * PrintStream中的println方法定义
     * <p>
     * public void println(String x) {
     * synchronized (this) {
     * print(x);
     * newLine();
     * }
     * }
     */
    //对象::实例方法名
    @Test
    public void test1() {

        PrintStream out = System.out;
        Consumer<String> consumer = out::println;
        consumer.accept("hello");

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.类::静态方法名

    /**
     * Integer类中的静态方法compare的定义:
     *     public static int compare(int x, int y) {
     *         return (x < y) ? -1 : ((x == y) ? 0 : 1);
     *     }
     */
  @Test
  public void test2(){
    Comparator<Integer> comparable=(x,y)->Integer.compare(x,y);
    //使用方法引用实现相同效果
    Comparator<Integer> integerComparable=Integer::compare;
    System.out.println(integerComparable.compare(4,2));//结果:1
    System.out.println(comparable.compare(4,2));//结果:1
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.类::实例方法名

    @Test
    public void test3() {
        BiPredicate<String, String> bp = (x, y) -> x.equals(y);
        //使用方法引用实现相同效果
        BiPredicate<String, String> bp2 = String::equals;
        System.out.println(bp.test("1", "2"));//结果:false
        System.out.println(bp.test("1", "2"));//结果:false
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.综合练习

class Dog {
    private String name;

    public Dog(String name) {
        this.name = name;
    }


    public static void bark(Dog dog) {
        System.out.println(dog + "叫了");
    }

    public void ect(Dog dog) {
        System.out.println(dog + "吃狗粮");
    }

    @Override
    public String toString() {
        return this.name;
    }
}


public class Demo {

    public static void main(String[] args) {

        //消费者函数
        Consumer<Dog> consumer1 = Dog::bark;
        consumer1.accept(new Dog("哮天犬"));

        Consumer<Dog> consumer2 = new Dog("小")::ect;
        consumer2.accept(new Dog("原因"));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

最后我们打印的是原因吃狗粮

于是我们又添加的一个

Dog dog = new Dog("小");
Consumer<Dog> consumer3 = dog::ect;
consumer3.accept(dog);
  • 1
  • 2
  • 3

证明dog::ect这个只是得到了一个方法

6.构造器引用

格式:类名::new
与函数式接口相结合,自动与函数式接口中方法兼容,可以把构造器引用赋值给定义的方法。需要注意构造器参数列表要与接口中抽象方法的参数列表一致。使用示例:
创建一个实体类Employee:

public class Employee {
  private Integer id;
  private String name;
  private Integer age;

  @Override
  public String toString() {
    return "Employee{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", age=" + age +
            '}';
  }
  public Employee(){

  }

  public Employee(Integer id) {
    this.id = id;
  }

  public Employee(Integer id, Integer age) {
    this.id = id;
    this.age = age;
  }

  public Employee(int id, String name, int age) {
    this.id = id;
    this.name = name;
    this.age = age;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

使用构造器引用与函数式接口相结合

  @Test
  public void test01(){
    //引用无参构造器
    Supplier<Employee> supplier=Employee::new;
    System.out.println(supplier.get());
    //引用有参构造器
    Function<Integer,Employee> function=Employee::new;
    System.out.println(function.apply(21));
    BiFunction<Integer,Integer,Employee> biFunction=Employee::new;
    System.out.println(biFunction.apply(8,24));
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出结果:

Employee{id=null, name='null', age=null}  
Employee{id=21, name='null', age=null}  
Employee{id=8, name='null', age=24}
  • 1
  • 2
  • 3

7.数组引用

数组引用的格式:type[]:new
使用示例:

@Test
public void test02(){
  Function<Integer,String[]> function=String[]::new;
  String[] apply = function.apply(10);
  System.out.println(apply.length);//结果:10
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

8.Lambda表达式的作用域

Lambda表达式可以看作是匿名内部类实例化的对象,Lambda表达式对变量的访问限制和匿名内部类一样,因此Lambda表达式可以访问局部变量、局部引用,静态变量,实例变量。

8.1 访问局部变量

在Lambda表达式中规定只能引用标记了final的外层局部变量。我们不能在lambda 内部修改定义在域外的局部变量,否则会编译错误。

public class TestFinalVariable {


interface VarTestInterface{
    Integer change(String str);
}

public static void main(String[] args) {
   //局部变量不使用final修饰
    Integer tempInt = 1;
    VarTestInterface var = (str -> Integer.valueOf(str+tempInt));
    //再次修改,不符合隐式final定义
    tempInt = 2;
    Integer str =var.change("111") ;
    System.out.println(str);
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

上面代码会出现编译错误,出现如下提示:

表达式变量是最终变量

特殊情况下,局部变量也可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

例如上面的代码确保Lambda表达式后局部变量后面不做修改,就可以成功啦!

public class TestFinalVariable {

interface VarTestInterface{
    Integer change(String str);
}

public static void main(String[] args) {
   //局部变量不使用final修饰
    Integer tempInt = 1;
    VarTestInterface var = (str -> Integer.valueOf(str+tempInt));
    Integer str =var.change("111") ;
    System.out.println(str);
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
8.2 访问局部引用,静态变量,实例变量

Lambda表达式不限制访问局部引用变量,静态变量,实例变量。代码测试都可正常执行,代码:

public class LambdaScopeTest {


/**
 * 静态变量
 */
private static String staticVar;

/**
 * 实例变量
 */
private static String instanceVar;

@FunctionalInterface
interface VarChangeInterface{
    Integer change(String str);
}

/**
 * 测试引用变量
 */
private void  testReferenceVar(){
    ArrayList<String> list = new ArrayList<>();
    list.add("111");
    //访问外部引用局部引用变量
    VarChangeInterface varChangeInterface = ((str) -> Integer.valueOf(list.get(0)));
    //修改局部引用变量
    list.set(0,"222");
    Integer str =varChangeInterface.change("");
    System.out.println(str);
}



/**
 * 测试静态变量
 */
void testStaticVar(){
    staticVar="222";
    VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+staticVar));
    staticVar="333";
    Integer str =varChangeInterface.change("111") ;
    System.out.println(str);
}



/**
 * 测试实例变量
 */
void testInstanceVar(){
    instanceVar="222";
    VarChangeInterface varChangeInterface = (str -> Integer.valueOf(str+instanceVar));
    instanceVar="333";
    Integer str =varChangeInterface.change("111") ;
    System.out.println(str);
}


public static void main(String[] args) {
    new LambdaScopeTest().testReferenceVar();
    new LambdaScopeTest().testStaticVar();
    new LambdaScopeTest().testInstanceVar();
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

Lambda表达式里不允许声明一个与局部变量同名的参数或者局部变量。

//编程报错
        Integer tempInt = 1;
        VarTestInterface varTest01 = (tempInt -> Integer.valueOf(tempInt));
        VarTestInterface varTest02 = (str -> {
            Integer tempInt = 1;
            Integer.valueOf(str);
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
8.3 Lambda表达式访问局部变量作限制的原因

Lambda表达式不能访问非final修饰的局部变量的原因是,局部变量是保存在栈帧中的。而在Java的线程模型中,栈帧中的局部变量是线程私有的,如果允许Lambda表达式访问到栈帧中的变量地址(可改变的局部变量),则会可能导致线程私有的数据被并发访问,造成线程不安全问题。

基于上述,对于引用类型的局部变量,因为Java是值传递,又因为引用类型的指向内容是保存在堆中,是线程共享的,因此Lambda表达式中可以修改引用类型的局部变量的内容,而不能修改该变量的引用。

对于基本数据类型的变量,在 Lambda表达式中只是获取到该变量的副本,且局部变量是线程私有的,因此无法知道其他线程对该变量的修改,如果该变量不做final修饰,会造成数据不同步的问题。

但是实例变量,静态变量不作限制,因为实例变量,静态变量是保存在堆中(Java8之后),而堆是线程共享的。在Lambda表]达式内部是可以知道实例变量,静态变量的变化。

9.Lambda表达式的优缺点

优点:
使代码更简洁,紧凑
可以使用并行流来并行处理,充分利用多核CPU的优势
有利于JIT编译器对代码进行优化
缺点:
非并行计算情况下,其计算速度没有比传统的 for 循环快
不容易调试
若其他程序员没有学过 Lambda 表达式,代码不容易看懂

Integer tempInt = 1;
Integer.valueOf(str);
});


#### 8.3 Lambda表达式访问局部变量作限制的原因

Lambda表达式不能访问非final修饰的局部变量的原因是,局部变量是保存在栈帧中的。而在Java的线程模型中,栈帧中的局部变量是线程私有的,如果允许Lambda表达式访问到栈帧中的变量地址(可改变的局部变量),则会可能导致线程私有的数据被并发访问,造成线程不安全问题。

基于上述,对于引用类型的局部变量,因为Java是值传递,又因为引用类型的指向内容是保存在堆中,是线程共享的,因此Lambda表达式中可以修改引用类型的局部变量的内容,而不能修改该变量的引用。

对于基本数据类型的变量,在 Lambda表达式中只是获取到该变量的副本,且局部变量是线程私有的,因此无法知道其他线程对该变量的修改,如果该变量不做final修饰,会造成数据不同步的问题。

但是实例变量,静态变量不作限制,因为实例变量,静态变量是保存在堆中(Java8之后),而堆是线程共享的。在Lambda表]达式内部是可以知道实例变量,静态变量的变化。

## 9.Lambda表达式的优缺点

优点:
使代码更简洁,紧凑
可以使用并行流来并行处理,充分利用多核CPU的优势
有利于JIT编译器对代码进行优化
缺点:
非并行计算情况下,其计算速度没有比传统的 for 循环快
不容易调试
若其他程序员没有学过 Lambda 表达式,代码不容易看懂

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/505680
推荐阅读
相关标签
  

闽ICP备14008679号