Java SE8 之 Lambda 表達(dá)式詳解

lambda.jpg

原文地址:http://stackoverflow.com/documentation/java/91/lambda-expressions#t=201701170111285810613

簡(jiǎn)介

Lambda表達(dá)式用一個(gè)表達(dá)式提供了一個(gè)實(shí)現(xiàn)單個(gè)接口方法(函數(shù)式接口)的簡(jiǎn)潔明了的方式。他允許你減少你必須創(chuàng)建和維護(hù)的代碼數(shù)量,它經(jīng)常被用作匿名內(nèi)部類的替代。

介紹

函數(shù)式接口

Lambdas 只能在一個(gè)僅包含一個(gè)抽象方法的函數(shù)式接口上操作。函數(shù)式接口可以有任意的default或著static方法。(為此, 函數(shù)式接口有時(shí)候是說(shuō)具有單個(gè)抽象方法的接口, 或著SAM interfaces)。

interface Foo1 {
    void bar();
}

interface Foo2 {
    int bar(boolean baz);
}

interface Foo3 {
    String bar(Object baz, int mink);
}

interface Foo4 {
    default String bar() { // default so not counted
        return "baz";
    }
    void quux();
}

當(dāng)聲明 函數(shù)式接口時(shí)@FunctionalInterface注解可以被加上,雖然它沒有明確的作用, 但是如果一個(gè)注解被用于非函數(shù)式接口一個(gè)compiler error 將會(huì)產(chǎn)生,因此充當(dāng)一個(gè)接口不應(yīng)該被改變的提醒者。

@FunctionalInterface
interface Foo5 {
    void bar();
}

@FunctionalInterface
interface BlankFoo1 extends Foo3 { // inherits abstract method from Foo3
}

@FunctionalInterface
interface Foo6 {
    void bar();
    boolean equals(Object obj); // overrides one of Object's method so not counted
}

相反的, 它不是一個(gè)函數(shù)式接口, 因?yàn)樗恢褂幸粋€(gè)抽象方法。

interface BadFoo {
    void bar();
    void quux(); // <-- Second method prevents lambda: which one should be considered as lambda?
}

它也不是一個(gè)函數(shù)式接口, 因?yàn)樗鼪]有任何方法。

interface BlankFoo2 { }

Java 8 也在Java.util.function中提供了很多基本的模版函數(shù)式接口, 例如, 內(nèi)置的接口Predicate<T>包含了一個(gè)單個(gè)方法, 輸入一個(gè)值T并且放回一個(gè)boolean。

Lambda Expressions

Lambda表達(dá)式的基本結(jié)構(gòu)是:
lambda 表達(dá)式結(jié)構(gòu)

fi將會(huì)持有一個(gè)實(shí)現(xiàn)了FunctionalInterface接口的匿名類的實(shí)例,匿名類中一個(gè)方法的定義為{System.out.println("Hello"); }。 換句話說(shuō),等價(jià)于:

FunctionalInterface fi = new FunctionalInterface() {
    @Override
    public void theOneMethod() {
        System.out.println("Hello");
    }
};

你不能在使用lambda時(shí)明確一個(gè)方法名,反而根本不需要,因?yàn)楹瘮?shù)式接口必須有一個(gè)抽象方法, 所以java重寫了它。
一旦lambda的類型不確定,(e.g. 重寫方法)你可以給lambda添加一個(gè)強(qiáng)轉(zhuǎn)型告訴編譯器它的類型,就像:

Object fooHolder = (Foo1) () -> System.out.println("Hello");
System.out.println(fooHolder instanceof Foo1); // returns true

如果函數(shù)式接口的單個(gè)方法包含參數(shù),它們的本地變量名應(yīng)該出現(xiàn)在lambda的方括號(hào)中。沒有必要去聲明參數(shù)的類型或著返回值的類型,因?yàn)樗麄兡軓慕涌诙x中推理得出(當(dāng)然如果你想聲明參數(shù)類型, 這也不是一個(gè)錯(cuò)誤)。因此,如下兩個(gè)樣例是等價(jià)的:

Foo2 longFoo = new Foo2() {
    @Override
    public int bar(boolean baz) {
        return baz ? 1 : 0;
    }
};
Foo2 shortFoo = (x) -> { return x ? 1 : 0; };

如果函數(shù)只有一個(gè)參數(shù),參數(shù)兩邊的圓括號(hào)可以省略:

Foo2 np = x -> { return x ? 1 : 0; }; // okay
Foo3 np2 = x, y -> x.toString() + y // not okay

隱式返回

如果被放在lambda中的代碼是一個(gè)java 表達(dá)式而不是一個(gè)聲明,它就會(huì)被當(dāng)作一個(gè)返回這個(gè)表達(dá)式值的方法,因此,下面這兩個(gè)是等價(jià)的:

IntUnaryOperator addOneShort = (x) -> (x + 1);
IntUnaryOperator addOneLong = (x) -> { return (x + 1); };

訪問(wèn)本地變量(值閉包)

因?yàn)閘ambdas 是匿名內(nèi)部類的簡(jiǎn)化寫法,它們遵循在一個(gè)閉合的域中訪問(wèn)本地變量相同的規(guī)則;變量必須被當(dāng)作final并且在lambda表達(dá)式中不能夠被修改。

IntUnaryOperator makeAdder(int amount) {
    return (x) -> (x + amount); // Legal even though amount will go out of scope
                                // because amount is not modified
}

IntUnaryOperator makeAccumulator(int value) {
    return (x) -> { value += x; return value; }; // Will not compile
}

如果以這種方式包含一個(gè)可改變的變量是必要的, 一個(gè)包含此變量的拷貝的合法對(duì)象應(yīng)該被使用, Read more in Closures with lambda expressions.

接收l(shuí)ambdas

因?yàn)閘ambda是一個(gè)接口的實(shí)現(xiàn),去使一個(gè)方法接收l(shuí)ambda并沒有什么特別的要做:任何函數(shù)只要是函數(shù)式接口都能夠接收一個(gè)lambda。

public void passMeALambda(Foo1 f) {
    f.bar();
}
passMeALambda(() -> System.out.println("Lambda called"));

使用lambda表達(dá)式去排序一個(gè)集合

在java 8 之前, 當(dāng)排序一個(gè)集合的時(shí)候, 用一個(gè)匿名(或著 有名字)類去實(shí)現(xiàn)java.util.Comparator接口是必要的:

Java SE 1.2
Collections.sort(
    personList,
    new Comparator<Person>() {
        public int compare(Person p1, Person p2){
            return p1.getFirstName().compareTo(p2.getFirstName());
        }
    }
);

從java 8 開始, 匿名內(nèi)部類能夠被lambda表達(dá)式替代, 注意到p1和p2參數(shù)能夠被忽略, 因?yàn)榫幾g器能夠自動(dòng)的推斷出它們。

Collections.sort(
    personList, 
    (p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName())
);

這個(gè)例子能夠被簡(jiǎn)化通過(guò)使用Comparator.comparing
和method references(方法引用), 用::(雙冒號(hào))符號(hào)來(lái)表達(dá):

Collections.sort(
    personList,
    Comparator.comparing(Person::getFirstName)
);

靜態(tài)導(dǎo)入允許我們更加簡(jiǎn)明的去表達(dá)它, 但是對(duì)于是否能夠提高整體可讀性是備受爭(zhēng)論的。

import static java.util.Collections.sort;
import static java.util.Comparator.comparing;
//...
sort(personList, comparing(Person::getFirstName));

Comparators構(gòu)建這種方式可以用來(lái)鏈?zhǔn)秸{(diào)用。例如, 通過(guò)名字比較之后, 如果有一些人具有相同的名字, 那么thenComparing方法將會(huì)根據(jù)性別來(lái)接著比較

sort(personList, comparing(Person::getFirstName).thenComparing(Person::getLastName));

方法引用

方法引用允許提前定義的靜態(tài)或著實(shí)例方法去綁定到一個(gè)合適的函數(shù)式接口來(lái)當(dāng)作參數(shù)傳遞,而不是用一個(gè)匿名的lambda表達(dá)式。
假設(shè)我們有一個(gè)模型:

class Person {
    private final String name;
    private final String surname;

    public Person(String name, String surname){
        this.name = name;
        this.surname = surname;
    }

    public String getName(){ return name; }
    public String getSurname(){ return surname; }
}

List<Person> people = getSomePeople();

實(shí)例方法引用(對(duì)于一個(gè)任意的實(shí)例)

people.stream().map(Person::getName)

等價(jià)的lambda:

people.stream().map(person -> person.getName())

在這個(gè)例子中, 對(duì)于一個(gè)Person類的實(shí)例方法getName()的一個(gè)方法引用被傳遞。因?yàn)樗划?dāng)作一個(gè)集合類型, 實(shí)例上的方法(之后被察覺)將會(huì)被調(diào)用 。

實(shí)例方法引用(對(duì)于一個(gè)特定類型)

people.forEach(System.out::println);

因?yàn)镾ystem.out是一個(gè)PrintStream的實(shí)例,對(duì)這個(gè)特定的實(shí)例的一個(gè)方法引用被當(dāng)作一個(gè)參數(shù)傳遞。等價(jià)的lambda表達(dá)式:

people.forEach(person -> System.out.println(person));

靜態(tài)的方法引用

對(duì)于轉(zhuǎn)換流,我們能夠使用靜態(tài)方法引用

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().map(String::valueOf)

這個(gè)例子傳遞了一個(gè)String類型的valueOf()靜態(tài)方法引用, 因此valueOf()
在集合中的實(shí)例對(duì)象中被當(dāng)做一個(gè)參數(shù)傳遞。等價(jià)的lambda:

numbers.stream().map(num -> String.valueOf(num))

構(gòu)造器引用

List<String> strings = Arrays.asList("1", "2", "3");
strings.stream().map(Integer::new)

Collect Elements of a Stream into a Collection看看如何收集元素到集合中。唯一的一個(gè)Integer的String參數(shù)構(gòu)造器在這里被使用, 通過(guò)一個(gè)被當(dāng)作參數(shù)提供的String來(lái)構(gòu)造一個(gè)整數(shù),只要這個(gè)string代表一個(gè)數(shù)字, 流將會(huì)被轉(zhuǎn)化為整數(shù)。等價(jià)的lambda:

Collect Elements of a Stream into a Collection

備忘單

方法參考格式 代碼 等價(jià)于
Static method TypeName::method (args) -> TypeName.method(args)
Non-static method (from instance) instance::method (args) -> instance.method(args
Non-static method (no instance) TypeName::method (instance, args) -> instance.method(args)
Constructor TypeName::new (args) -> new TypeName(args)

實(shí)現(xiàn)多個(gè)接口

有時(shí)候你想使lambda表達(dá)式實(shí)現(xiàn)多個(gè)接口,使用標(biāo)記式接口(例如java.io.Serializable)是很有用的, 因?yàn)樗麄儾惶砑尤魏纬橄蠓椒ā@缒阆胧褂靡粋€(gè)客戶自定義Comparator創(chuàng)建一個(gè)TreeSet, 接著序列化它, 并通過(guò)網(wǎng)路發(fā)送它 。一般方法:

TreeSet<Long> ts = new TreeSet<>((x, y) -> Long.compare(y, x));

并不生效, 因?yàn)閷?duì)Comparator的lambda沒有實(shí)現(xiàn)Serialization
, 你能夠修正它通過(guò)使用交叉類型, 并且顯式的明確這個(gè)lambda是需要序列化的:

TreeSet<Long> ts = new TreeSet<>(
    (Comparator<Long> & Serializable) (x, y) -> Long.compare(y, x));

如果你平凡的使用交叉類型(例如, 你正在使用一個(gè)譬如幾乎所有東西都必須序列化的Apache Spark 框架), 你能夠創(chuàng)建一個(gè)空的接口并在你的代碼中使用它們。

public interface SerializableComparator extends Comparator<Long>, Serializable {}

public class CustomTreeSet {
  public CustomTreeSet(SerializableComparator comparator) {}
}

這樣你就保證了傳遞的comparator接口將會(huì)被序列化。

lambda表達(dá)式閉包

當(dāng)一個(gè)lambda表達(dá)式引用一個(gè)封閉域(全局或局部)內(nèi)的變量, 一個(gè)lambda閉包被創(chuàng)建。這樣做的規(guī)則和內(nèi)聯(lián)方法以及匿名類是相同的。來(lái)自一個(gè)閉合域中的本地變量在一個(gè)lambda內(nèi)部被使用時(shí)必須是final。在java 8 (最早的支持lambdas的版本)中不需要在外部上下文中聲明final, 但是必須(當(dāng)作final)來(lái)對(duì)待。例如:

int n = 0; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
    int i = n;
    // do something
};

只要值n變量沒有被改變,它就是合法的。如果你嘗試去在lambda的外部或內(nèi)部去改變這個(gè)變量, 你將會(huì)得到下面的編譯錯(cuò)誤:

“l(fā)ocal variables referenced from a lambda expression must be final oreffectively final”.

例如:如果在lambda里面必須使用一個(gè)可改變的變量, 正常的方法是聲明一個(gè)對(duì)此變量的final拷貝,然后使用這個(gè)拷貝。例如:

int n = 0;
final int k = n; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
    int i = k;
    // do something
};
n++;      // Now will not generate an error
r.run();  // Will run with i = 0 because k was 0 when the lambda was created

自然地, lambda的body體里面對(duì)原始變量的改變是不可見的。
注意到j(luò)ava 不支持真正的閉包, 一個(gè)java lambda不能夠以一種能夠看到在它所被實(shí)例化的環(huán)境中的變量的改變的方式被創(chuàng)建 。如果你想實(shí)現(xiàn)一個(gè)能夠?qū)λ沫h(huán)境進(jìn)行觀察或做出改變的閉包, 你應(yīng)該使用一個(gè)合法的類“聚集”它。例如:

// Does not compile ...
public IntUnaryOperator createAccumulator() {
    int value = 0;
    IntUnaryOperator accumulate = (x) -> { value += x; return value; };
    return accumulate;
}

以上將不會(huì)被編譯,由于之前討論的原因。我們能夠繞過(guò)編譯錯(cuò)誤,如下:

// Does not compile ...
public IntUnaryOperator createAccumulator() {
    int value = 0;
    IntUnaryOperator accumulate = (x) -> { value += x; return value; };
    return accumulate;
}

這個(gè)問(wèn)題是IntUnaryOperator接口設(shè)計(jì)契約的打破, 它聲明實(shí)例應(yīng)該是函數(shù)式的并且無(wú)狀態(tài)的。如果一個(gè)閉包被傳遞進(jìn)一個(gè)可以接收函數(shù)式對(duì)象的內(nèi)置函數(shù)式接口, 這是很容易造成沖突和錯(cuò)誤的行為。解封裝的易變狀態(tài)的閉包應(yīng)當(dāng)被實(shí)現(xiàn)為一個(gè)合法類。例如:

// Correct ...
public class Accumulator {
   private int value = 0;

   public int accumulate(int x) {
      value += x;
      return value;
   }
}

Lambda - Listener 示例

匿名類listener

JButton btn = new JButton("My Button");
btn.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button was pressed");
    }
});

Lambda listener

JButton btn = new JButton("My Button");
btn.addActionListener(e -> {
    System.out.println("Button was pressed");
});

在你的函數(shù)式接口中使用Lambda

Lambda 意味著為單個(gè)方法的接口提供一個(gè)內(nèi)聯(lián)實(shí)現(xiàn)代碼和用一種常規(guī)變量的方式來(lái)傳遞它們的能力,正如我們所曾做的。我們把它叫做函數(shù)式接口。
例如, 用一個(gè)匿名類來(lái)寫一個(gè)Runnable, 然后啟動(dòng)一個(gè)線程,就像這樣:

//Old way
new Thread(
        new Runnable(){
            public void run(){
                System.out.println("run logic...");
            }
        }
).start();

//lambdas, from Java 8
new Thread(
        ()-> System.out.println("run logic...")
).start();

現(xiàn)在, 和上面一致, 你們有一些客戶端接口:

interface TwoArgInterface {
    int operate(int a, int b);
}

在你的代碼中你怎么使用Lambda去給出這個(gè)接口的實(shí)現(xiàn)? 和上述

public class CustomLambda {
    public static void main(String[] args) {

        TwoArgInterface plusOperation = (a, b) -> a + b;
        TwoArgInterface divideOperation = (a,b)->{
            if (b==0) throw new IllegalArgumentException("Divisor can not be 0");
            return a/b;
        };

        System.out.println("Plus operation of 3 and 5 is: " + plusOperation.operate(3, 5));
        System.out.println("Divide operation 50 by 25 is: " + divideOperation.operate(50, 25));

    }
}

return 僅僅從Lambda中返回, 而不是外部方法

當(dāng)心這不同于Scala和Kotlin!

void threeTimes(IntConsumer r) {
  for (int i = 0; i < 3; i++) {
    r.accept(i);
  }
}

void demo() {
  threeTimes(i -> {
    System.out.println(i);
    return; // Return from lambda to threeTimes only!
  });
}

當(dāng)嘗試用特有語(yǔ)言結(jié)構(gòu), 這會(huì)導(dǎo)致無(wú)法預(yù)期的異常, 譬如內(nèi)置的結(jié)構(gòu):for
循環(huán)return表現(xiàn)的不同:

void demo2() {
  for (int i = 0; i < 3; i++) {
    System.out.println(i);
    return; // Return from 'demo2' entirely
  }
}

scala 和 Kotlin ,demo和demo2都僅僅打印0, 但這并不是始終如一的, java方法和refactoring和 類的使用是一致的 -return在代碼的頂部和底部表現(xiàn)相同:

void demo3() {
  threeTimes(new MyIntConsumer());
}

class MyIntConsumer implements IntConsumer {
  public void accept(int i) {
    System.out.println(i);
    return;
  }
}

因此, java 的return和類方法和refactoring更為一致, 但和內(nèi)置的for、while不具一致性, 保留了它們的特殊性。由此, 解析來(lái)兩個(gè)案例在java 中是等價(jià)的:

IntStream.range(1, 4)
    .map(x -> x * x)
    .forEach(System.out::println);
IntStream.range(1, 4)
    .map(x -> { return x * x; })
    .forEach(System.out::println);

此外,try-with-resources的使用在java中是安全的:

class Resource implements AutoCloseable {
  public void close() { System.out.println("close()"); }
}

void executeAround(Consumer<Resource> f) {
  try (Resource r = new Resource()) {
    System.out.print("before ");
    f.accept(r);
    System.out.print("after ");
  }
}

void demo4() {
  executeAround(r -> {
    System.out.print("accept() ");
    return; // Does not return from demo4, but frees the resource.
  });
}

將會(huì)打印before accept() after close()。 在Scala 和Kotlin 語(yǔ)義中try-with-resources將不會(huì)被關(guān)閉, 將僅僅打印出before accept()。

Lambdas 和 執(zhí)行-環(huán)繞模式

在一些簡(jiǎn)單的場(chǎng)景中, 作為函數(shù)式接口, 有一些使用lambdas好的樣例, 一個(gè)相對(duì)常見的能夠被lambdas所增強(qiáng)的用例是被稱為Execute-Around模式, 在這個(gè)模式中, 你有一組標(biāo)準(zhǔn)的setup/teardown 代碼, 很多場(chǎng)景需要被用例特定的代碼去環(huán)繞, 一些通用的示例就是file io , database io , try / catch 代碼塊。

interface DataProcessor {
    void process( Connection connection ) throws SQLException;;
}

public void doProcessing( DataProcessor processor ) throws SQLException{
    try (Connection connection = DBUtil.getDatabaseConnection();) {
        processor.process(connection);
        connection.commit();
    } 
}

接著用lambda 來(lái)調(diào)用這個(gè)方法,看起來(lái)像下面這樣:

public static void updateMyDAO(MyVO vo) throws DatabaseException {
    doProcessing((Connection conn) -> MyDAO.update(conn, ObjectMapper.map(vo)));
}

它并不限于I/O操作, 它能夠應(yīng)用于和setup/tear down類似且變量較少的任務(wù)的任何場(chǎng)景。這種模式的主要好處是代碼重用 和 強(qiáng)制DRY(Don’t Repeat Yourself)。

傳統(tǒng)方式 -> Lambda風(fēng)格

Traditional way

interface MathOperation{
    boolean unaryOperation(int num);
}

public class LambdaTry {
    public static void main(String[] args) {
        MathOperation isEven = new MathOperation() {
            @Override
            public boolean unaryOperation(int num) {
                return num%2 == 0;
            }
        };

        System.out.println(isEven.unaryOperation(25));
        System.out.println(isEven.unaryOperation(20));
    }
}

Lambda style

1、移除類名和函數(shù)式接口體

public class LambdaTry {
    public static void main(String[] args) {
        MathOperation isEven = (int num) -> {
            return num%2 == 0;
        };

        System.out.println(isEven.unaryOperation(25));
        System.out.println(isEven.unaryOperation(20));
    }
}

2、可選的類型的聲明

MathOperation isEven = (num) -> {
    return num%2 == 0;
};

3、可選的參數(shù)兩邊括弧, 如果是一個(gè)參數(shù)

MathOperation isEven = num -> {
    return num%2 == 0;
};

4、可選的花括號(hào), 如果在函數(shù)體中只有一行
5、可選的返回值, 如果在函數(shù)體中只有一行

MathOperation isEven = num -> num%2 == 0;

Lambdas 與內(nèi)存利用

因?yàn)镴ava lambda是閉包的, 它們能夠 “捕獲” 在閉合作用域中變量的值, 然而并不是所有的lambda都能捕獲 – 簡(jiǎn)單的lambdas 就像s -> s.length()
什么都沒有捕獲, 被稱作 無(wú)狀態(tài)的 – 捕獲形lambdas 要求一個(gè)臨時(shí)的對(duì)象去持有這個(gè)被捕獲的變量, 在這個(gè)代碼片中, 這個(gè)lambda() -> j是一個(gè)捕獲型lambda, 并且在被使用時(shí)可能造成一個(gè)對(duì)象被分配內(nèi)存。

public static void main(String[] args) throws Exception {
    for (int i = 0; i < 1000000000; i++) {
        int j = i;
        doSomethingWithLambda(() -> j);
    }
}

雖然并不會(huì)很快的變得顯而易見, 因?yàn)閚ew關(guān)鍵字并沒有在任何地方出現(xiàn), 但是這個(gè)代碼創(chuàng)建了1000000000 個(gè)獨(dú)立的 () -> j lambda實(shí)例。

使用lambda條件表達(dá)式來(lái)從列表中獲取某些值

從java 8 開始, 你能夠使用lambda 表達(dá)式 & predicates。例如: 使用lambda 表達(dá)式 & predicates 從列表中獲取某個(gè)值, 在這個(gè)樣例中, 如果他們具有大于18歲的事實(shí)就會(huì)被打印出來(lái), 反之不會(huì)。
Person Class:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() { return age; }
    public String getName() { return name; }
}

內(nèi)置的來(lái)自java.util.function.Predicate 包中的接口Predicate是一個(gè)函數(shù)式接口,并有一個(gè)boolean test(T t)
方法。示例用法:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class LambdaExample {
    public static void main(String[] args) {
        List<Person> personList = new ArrayList<Person>();
        personList.add(new Person("Jeroen", 20));
        personList.add(new Person("Jack", 5));
        personList.add(new Person("Lisa", 19));

        print(personList, p -> p.getAge() >= 18);
    }

    private static void print(List<Person> personList, Predicate<Person> checker) {
        for (Person person : personList) {
            if (checker.test(person)) {
                System.out.print(person + " matches your expression.");
            } else {
                System.out.println(person  + " doesn't match your expression.");
            }
        }
    }
}

這個(gè)print(personList, p -> p.getAge() >= 18);方法采用一個(gè)lambda表達(dá)式(因?yàn)镻redicate 被用于作為一個(gè)參數(shù)), 你能定義自己所需要的表達(dá)式, checker的test方法檢查表達(dá)式正確與否:checker.test(person)。你可以輕易的去把它變成其他的, 例如print(personList, p -> p.getName().startsWith("J")); 它會(huì)檢查人的名字是否以字母“J”開頭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容