宝安网站建设制作,dede s网站地图调文章,专业微信网站建设公司首选公司,东莞艺美网站建设知识不回顾是会被遗忘的#xff01;
网上看了一些相关文章#xff0c;这里记录一下#xff0c;仅供参考
Java语言从JDK1.8开始引入了函数式编程。 函数式编程的核心特点是#xff0c;函数作为一段功能代码#xff0c;可以像变量一样进行引用和传递#xff0c;以便在有需…知识不回顾是会被遗忘的
网上看了一些相关文章这里记录一下仅供参考
Java语言从JDK1.8开始引入了函数式编程。 函数式编程的核心特点是函数作为一段功能代码可以像变量一样进行引用和传递以便在有需要的时候进行调用。
1. FunctionalInterface与“函数类型” Java对函数式编程的支持本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口然后对接口冠以FunctionalInterface注解那么这个接口就可以作为“函数类型”可以接收一段以Lambda表达式或者方法引用予以承载的逻辑代码。例如
FunctionalInterface
interface IntAdder {int add(int x, int y);
}IntAdder adder (x, y) - x y;
IntAdder 就可以看成是一个“函数类型”。Lambda表达式和方法引用的介绍见后文。
概念如此需要思考的有几点
为什么必须是只声明一个方法的接口 显然这个方法就是用来代表“函数类型”所能执行的功能一个函数一旦定义好它能执行的功能是确定的就是调用和不调用的区别。接口中声明的方法就是和函数体定义一一对应的。 事实上FunctionalInterface下只能声明一个方法多一个、少一个都不能编译通过 。覆写Object中toString/equals的方法不受此个数限制。
比如Comparator接口就声明了2个方法
// Comparator.java
FunctionalInterface
public interface ComparatorT {int compare(T o1, T o2);boolean equals(Object obj);//...
} 严格地说FunctionalInterface下只能声明一个未实现的方法default方法和static方法因为带有实现体所有不受此限制。 FunctionalInterfacepublic interface IAddT, R {R add(T t1, T t2);default R test1(T t1, T t2) {//可以额外定义default方法return null;}static T,R R test2(T t1, T t2) {//可以额外定义static方法return null;}} 关于interface中声明default/static方法有疑虑的话可以查阅博主另一篇文章java接口里面可以有成员变量么 FunctionalInterface注解不是必须的不加这个注解的接口前提是只包含一个方法一样可以作为函数类型。不过显而易见的是加了这个注解表意更明确、更直观是更被推荐的做法。 要定义清楚一个函数类型除了函数名称必须明确规定函数的参数个数和类型、返回值类型这些信息都是包含于接口中声明的方法。 2. JDK提供的“函数类型”
java.util.function包下预定义了常用的函数类型包括
FunctionalInterface
public interface ConsumerT {void accept(T t); //接收一个类型为T泛型的参数无返回值所以叫消费者
}
FunctionalInterface
public interface BiConsumerT, U {void accept(T t, U u);//接收2个参数无返回值
}
FunctionalInterface
public interface SupplierT {T get();//无参数有返回值所以叫提供者
}
//注意没有BiSupplier因为返回值只能有1个不会有2个
FunctionalInterface
public interface FunctionT, R {R apply(T t);//一个输入参数一个输出返回值
}
FunctionalInterface
public interface BiFunctionT, U, R {R apply(T t, U u);//两个输入T和U一个输出R
}
FunctionalInterface
public interface UnaryOperatorT extends FunctionT, T {static T UnaryOperatorT identity() {//一元操作输入原样返回给输出return t - t;}
}
FunctionalInterface
public interface BinaryOperatorT extends BiFunctionT,T,T {//二元操作输入输出类型相同public static T BinaryOperatorT minBy(Comparator? super T comparator) {Objects.requireNonNull(comparator);return (a, b) - comparator.compare(a, b) 0 ? a : b;//传入比较器返回较小者}public static T BinaryOperatorT maxBy(Comparator? super T comparator) {Objects.requireNonNull(comparator);return (a, b) - comparator.compare(a, b) 0 ? a : b;//传入比较器返回较大者}
} 这些个定义都是在参数个数(0,1,2)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口比如Predicate、LongSupplier等等。
FunctionalInterface
public interface PredicateT {boolean test(T t);//输入1个参数返回boolean就好比是预言家预言你这个输入是真还是假
}
FunctionalInterface
public interface LongSupplier {long getAsLong();//没有输入输出long类型long类型的提供者
}
3. Lambda表达式 上面弄清楚了函数类型FunctionalInterface那么函数类型能接收怎么样的函数实现体呢怎么接收呢该Lambda出场了。 Lambda表达式能赋值给一个变量也就能当作参数传给函数。这个Lambda形式的变量/参数的类型是它所实现的那个接口所包含的方法体便是这个接口抽象方法的实现。以后看到调用方法的参数是一个SAM类型接口的时候就可以考虑使用Lambda表达式替换匿名内部类来写。
作用
①减少代码量突出代码意图②对集合数据 Collection 操作更简便③使变量记住一段逻辑 任务逻辑传递传递一段运算逻辑给执行者 回调逻辑传递简化接口回调的时候 new匿名类后实现抽象方法的模版代码
将一个方法写成Lambda表达式只需要关注参数列表和方法体。 语法组成
(参数类型 参数名) - { 方法体 return 返回值
}
①形式参数最前面的部分是一对括号里面是参数无参数就是一对空括号②箭头中间的是 - 用来分割参数和body部分读作“ goes to”③方法体body部分可以是一个表达式或者一个代码块。
简写
①可选类型声明不用声明参数类型编译器可以自动识别。②可选参数括号一个参数无需定义括号多个参数需要定义。③可选大括号如果body部分只包含一个语句或表达式就不需要使用大括号括起来。④可选 return 关键字如果body部分是一个表达式表达式的值会被作为返回值返回如果是代码块需要用 return 指定返回值 Java8 之前创建接口实现类总会有很多冗余的模版代码接口中定义的抽象方法越多每次实现的模版代码就越多而很多时候这个接口实现类只需要用到一次。 变量作用域
①局部变量引用的局部变量不可被修改值即必须是final所修饰的或者不被后面代码更改的隐形final属性。②成员变量可以修改类成员变量非 final 修饰的和静态变量。③在Lambda表达式中不允许声明一个与局部变量同名的参数或者局部变量。④在Lambda表达式中使用this调用的是该Lambda表达式所属方法的所属实例的this。⑤在Lambda表达式中无法访问到自身接口的默认方法不存在实例。 Lambda表达式用来定义函数实现体。有很多种写法都是为了简化书写但核心是通过-连接参数和实现代码
(入参)-{实现代码}
//无返回值的时候 (int x)-{System.out.println(x);} (x)-{System.out.println(x);}//参数类型自动推断 x-{System.out.println(x);}//只有一个参数的时候可以省略小括号 x-System.out.println(x);//实现体只有一个表达式可以省略大括号,System.out.println本身无返回值
//有返回值的情况 (int x)-{return x*x;} (x)-{return x*x;} //x-return x*x; //错误不能这么写!! x-x*x; 说了这么多来实操一把
IntConsumer ic x-System.out.println(x); IntFunctionInteger ifi1 x-{return x*x;}; IntFunctionInteger ifi2 x-x*x; ic.accept(100);//100 System.out.println(ifi1.apply(5));//25 System.out.println(ifi2.apply(5));//25 好了函数类型–Lambda表达式说明白了再来看看方法引用是怎么回事。
4. 四种方法引用 文章开头说过了函数类型可以接收一段Lambda表达式或者对方法的引用。方法引用就是对一个类中已经存在的方法加以引用分4中类型以Test类为例
对类构造方法的引用如Test::new。对类静态方法的引用如Test::staticMethodName对对象实例方法的引用如new Test()::instanceMethod是2和3的结合如Test::instanceMethod2但要求函数类型声明和函数调用的时候其第一个参数必须是Test类的实例。
第4种比较难以说清楚看看下面的例子吧
public class Test {private String name ;public Test() {System.out.println(构造方法无参数);}public Test(String name) {this.name name;System.out.println(构造方法参数name);}public static void staticMethod(String str) {System.out.println(static method: input str);}public void instanceMethod(String str) {System.out.println(instance method: input str);}public static void main(String[] args) {SupplierTest s1 Test::new;//对无参构造器的引用无参构造器其实就是一个对象的Supplier提供者s1.get();//调用构造方法无参数FunctionString, Test f1 Test::new;//引用有一个String参数的构造器f1.apply(Test);//调用构造方法参数TestConsumerString c1 Test::staticMethod;//对静态方法引用c1.accept(1);//static method: input1ConsumerString c2 new Test()::instanceMethod;//对实例方法的引用c2.accept(2);//instance method: input2//第4种BiConsumerTest, String bc1 Test::instanceMethod;bc1.accept(new Test(), 3);//instance method: input3}
}
第4中方法引用本质上是对实例方法的引用只不过是在调用的时候才传入那个实例对象。
5. andThen链式表达
JDK中很多函数类型都实现了default的andThen方法可以将多个函数体Lambda表达式、方法引用串起来方便进行链式调用。 调用链上的任何一个抛出异常整个调用链会提前结束异常由调用者处理。
/*** 通过andThen()进行链式操作*/
Test
public void testLinkConsumer() {IntConsumer action x- System.out.print(x);action action.andThen(x-System.out.print(--tail1)).andThen(x-System.out.print(--tail2));//100--tail1--tail2action.accept(100);
}
6. 最后
小插曲Callable和Runnable到底什么区别
//java.util.concurrent.Callable
FunctionalInterface
public interface CallableV {V call() throws Exception;
}
//java.lang.Runnable
FunctionalInterface
public interface Runnable {public abstract void run();
}
这两者也都是JDK预定义的函数接口两者都不接收参数主要用于多线程编程。
Runnable无返回值一般用于new一个新线程的时候在新线程中执行代码。
Callable一样一般用于在新线程中执行只不过执行成功后有返回值如果执行失败还会抛异常。
最后一起分析
CallableInteger c1 ()-1; CallableInteger c2 ()-c1.call(); c1引用了一个Lambda表达式
c2引用了一个新的Lambda表达式表示式的实现代码中调用了c1提供的call()方法并将call()方法的返回值返回。
CallableInteger c1 ()-1;
CallableInteger c2 ()-c1.call();
CallableInteger c3 ()-{System.out.println(c3 call c1);return c1.call();
};try {System.out.println(c1.call());//1System.out.println(c2.call());//1System.out.println(c3.call());//c3 call c1//1
} catch (Exception e) {e.printStackTrace();
}
注意c3和c2本质上是一样的只不过方法实现上多加了一行打印代码。
7、Lambda 表达式的各种形态和使用场景 Lambda 表达式是 Java 8 中添加的功能。引入 Lambda 表达式的主要目的是为了让 Java 支持函数式编程。 Lambda 表达式是一个可以在不属于任何类的情况下创建的函数并且可以像对象一样被传递和执行。
Java lambda 表达式用于实现简单的单方法接口与 Java Streams API 配合进行函数式编程。 在前几篇关于 List、Set 和 Map 的文章中我们已经看到了这几个 Java 容器很多操作都是通过 Stream 完成的比如过滤出对象 List 中符合条件的子集时会使用类似下面的 Stream 操作。
ListA list aList.filter(a - a.getId() 10).collect(Colletors.toList); 其中filter方法里用到的a - a.getId() 10就是一个 Lambda 表达式前面对用到 Lambda 的地方知识简单的说了一下如果你对各种 Stream 操作有疑问可以先把本篇 Lambda 相关的内容学完接下来再仔细梳理 Stream 时就会好理解很多了。 Lambda 表达式和函数式接口 上面说了 lambda 表达式便于实现只拥有单一方法的接口同样在 Java 里匿名类也用于快速实现接口只不过 lambda 相较于匿名类更方便些在书写的时候连创建类的步骤也免去了更适合用在函数式编程。 举个例子来说函数式编程经常用在实现事件 Listener 的时候 。 在 Java 中的事件侦听器通常被定义为具有单个方法的 Java 接口。下面是一个 Listener 接口示例
public interface StateChangeListener {public void onStateChange(State oldState, State newState);
} 上面这个 Java 接口定义了一个只要被监听对象的状态发生变化就会调用的 onStateChange 方法这里不用管监听的是什么举例而已。 在 Java 8 版本以前监听事件变更的程序必须实现此接口才能侦听状态更改。
比如说有一个名为 StateOwner 的类它可以注册状态的事件侦听器。
public class StateOwner {public void addStateListener(StateChangeListener listener) { ... }
}
我们可以使用匿名类实现 StateChangeListener 接口然后为 StateOwner 实例添加侦听器。
StateOwner stateOwner new StateOwner();stateOwner.addStateListener(new StateChangeListener() {public void onStateChange(State oldState, State newState) {// do something with the old and new state.System.out.println(State changed)}
}); 在 Java 8 引入Lambda 表达式后我们可以用 Lambda 表达式实现 StateChangeListener 接口会更加方便。 现在把上面例子接口的匿名类实现改为 Lambda 实现程序会变成这样
StateOwner stateOwner new StateOwner();stateOwner.addStateListener((oldState, newState) - System.out.println(State changed)
);
在这里我们使用的 Lambda 表达式是
(oldState, newState) - System.out.println(State changed) 这个 lambda 表达式与 StateChangeListener 接口的 onStateChange() 方法的参数列表和返回值类型相匹配。如果一个 lambda 表达式匹配单方法接口中方法的参数列表和返回值比如本例中的 StateChangeListener 接口的 onStateChange 方法则 lambda 表达式将转换为拥有相同方法签名的接口实现。 这句话听着有点绕下面详细解释一下 Lambda 表达式和接口匹配的详细规则。
匹配Lambda 与接口的规则 上面例子里使用的 StateChangeListener 接口有一个特点其只有一个未实现的抽象方法在 Java 里这样的接口也叫做函数式接口 Functional Interface。将 Java lambda 表达式与接口匹配需要满足一下三个规则
接口是否只有一个抽象未实现方法即是一个函数式接口lambda 表达式的参数是否与抽象方法的参数匹配lambda 表达式的返回类型是否与单个方法的返回类型匹配
如果能满足这三个条件那么给定的 lambda 表达式就能与接口成功匹配类型。
函数式接口 只有一个抽象方法的接口被称为函数是式接口从 Java 8 开始Java 接口中可以包含默认方法和静态方法。默认方法和静态方法都有直接在接口声明中定义的实现。这意味着Java lambda 表达式可以实现拥有多个方法的接口——只要接口中只有一个未实现的抽象方法就行。 所以在文章一开头我说lambda 用于实现单方法接口是为了让大家更好的理解真实的情况是只要接口中只存在一个抽象方法那么这个接口就能用 lambda 实现。 换句话说即使接口包含默认方法和静态方法只要接口只包含一个未实现的抽象方法它就是函数式接口。比如下面这个接口
import java.io.IOException;
import java.io.OutputStream;public interface MyInterface {void printIt(String text);default public void printUtf8To(String text, OutputStream outputStream){try {outputStream.write(text.getBytes(UTF-8));} catch (IOException e) {throw new RuntimeException(Error writing String as UTF-8 to OutputStream, e);}}static void printItToSystemOut(String text){System.out.println(text);}
}
即使这个接口包含 3 个方法它也可以通过 lambda 表达式实现因为接口中只有一个抽象方法 printIt没有被实现。
MyInterface myInterface (String text) - {System.out.print(text);
};
Lambda VS 匿名类 尽管 lambda 表达式和匿名类看起来差不多但还是有一些值得注意的差异。 主要区别在于匿名类可以有自己的内部状态--即成员变量而 lambda 表达式则不能。
public interface MyEventConsumer {public void consume(Object event);
}
比如上面这个接口通过匿名类实现
MyEventConsumer consumer new MyEventConsumer() {public void consume(Object event){System.out.println(event.toString() consumed);}
};
MyEventConsumer 接口的匿名类可以有自己的内部状态。
MyEventConsumer myEventConsumer new MyEventConsumer() {private int eventCount 0;public void consume(Object event) {System.out.println(event.toString() consumed this.eventCount times.);}
}; 我们给匿名类加了一个名为 eventCount 的整型成员变量用来记录匿名类 consume 方法被执行的次数。Lambda 表达式则不能像匿名类一样添加成员变量所以也成 Lambda 表达式是无状态的。
推断 Lamdba 的接口类型 使用匿名类实现函数式接口的时候必须在 new 关键字后指明实现的是哪个接口。比如上面使用过的匿名类例子
stateOwner.addStateListener(new StateChangeListener() {public void onStateChange(State oldState, State newState) {// do something with the old and new state.}
}); 但是 lambda 表达式通常可以从上下文中推断出类型。例如可以从 addStateListener() 方法声明中参数的类型 StateChangeListener 推断出来Lambda 表达式要实现的是 StateChangeListener 接口。
stateOwner.addStateListener((oldState, newState) - System.out.println(State changed)
); 通常 lambda 表达式参数的类型也可以推断出来。在上面的示例中编译器可以从StateChangeListener 接口的抽象方法 onStateChange() 的方法声明中推断出参数 oldState 和 newState 的类型。
Lambda 的参数形式 由于 lambda 表达式实际上只是个方法因此 lambda 表达式可以像方法一样接受参数。Lambda 表达式参数根据参数数量以及是否需要添加类型会有下面几个形式。
如果表达式的方法不带参数那么可以像下面这样编写 Lambda 表达式:
() - System.out.println(Zero parameter lambda);
如果表达式的方法接受一个参数则可以像下面这样编写 Lambda 表达式
(param) - System.out.println(One parameter: param);
当 Lambda 表达式只接收单个参数时参数列表外的小括号也可以省略掉。
param - System.out.println(One parameter: param); 当 Lambda 表达式接收多个参数时参数列表的括号就没法省略了。 如果编译器无法从 Lambda 匹配的函数式接口的方法声明推断出参数类型出现这种情况时编译器会提示则有时可能需要为 Lambda 表达式的参数指定类型。
(Car car) - System.out.println(The car is: car.getName());
Lambda 的方法体
lambda 表达式的方法的方法体在 Lambda 声明中的 - 右侧指定
(oldState, newState) - System.out.println(State changed)
如果 Lambda 表达式的方法体需要由多行组成则需要把多行代码写在用{ }括起来的代码块内。
(oldState, newState) - {System.out.println(Old state: oldState);System.out.println(New state: newState);
}
Lamdba 表达式的返回值 可以从 Lambda 表达式返回值就像从方法中返回值一样。只需在 Lambda 的方法体中添加一个 return 语句即可
(param) - {System.out.println(param: param);return return value;
} 如果 Lambda 表达式所做的只是计算返回值并返回它我们甚至可以省略 return 语句。
(a1, a2) - { return a1 a2; }
// 上面的可以简写成不需要return 语句的
(a1, a2) - { a1 a2; } Lambda 表达式本质上是一个对象跟其他任何我们使用过的对象一样 我们可以将 Lambda 表达式赋值给变量并进行传递和使用。
public interface MyComparator {public boolean compare(int a1, int a2);}---MyComparator myComparator (a1, a2) - a1 a2;boolean result myComparator.compare(2, 5); 上面的这个例子展示 Lambda 表达式的定义以及如何将 Lambda 表达式赋值给给变量最后通过调用它实现的接口方法来调用 Lambda 表达式。
外部变量在 Lambda 内的可见性 在某些情况下Lambda 表达式能够访问在 Lambda 函数体之外声明的变量。 Lambda 可以访问以下类型的变量
局部变量实例变量静态变量
Lambda 内访问局部变量Lambda 可以访问在 Lambda 方法体之外声明的局部变量的值
public interface MyFactory {public String create(char[] chars);
}String myString Test;MyFactory myFactory (chars) - {return myString : new String(chars);
};
Lambda 访问实例变量Lambda 表达式还可以访问创建了 Lambda 的对象中的实例变量。
public class EventConsumerImpl {private String name MyConsumer;public void attach(MyEventProducer eventProducer){eventProducer.listen(e - {System.out.println(this.name);});}
} 这里实际上也是 Lambda 与匿名类的差别之一。匿名类因为可以有自己的实例变量这些变量通过 this 引用来引用。但是Lambda 不能有自己的实例变量因此 this 始终指向外面包裹 Lambda 的对象。
Lambda 访问静态变量Lambda 表达式也可以访问静态变量。这也不奇怪因为静态变量可以从 Java 应用程序中的任何地方访问只要静态变量是公共的。
public class EventConsumerImpl {private static String someStaticVar Some text;public void attach(MyEventProducer eventProducer){eventProducer.listen(e - {System.out.println(someStaticVar);});}
}
把方法引用作为 Lambda 如过编写的 lambda 表达式所做的只是使用传递给 Lambda 的参数调用另一个方法那么 Java里为 Lambda 实现提供了一种更简短的形式来表达方法调用。比如说下面是一个函数式数接口
public interface MyPrinter{public void print(String s);
}
接下来我们用 Lambda 表达式实现这个 MyPrinter 接口
MyPrinter myPrinter (s) - { System.out.println(s); };
因为 Lambda 的参数只有一个方法体也只包含一行所以可以简写成
MyPrinter myPrinter s - System.out.println(s); 又因为 Lambda 方法体内所做的只是将字符串参数转发给 System.out.println() 方法因此我们可以将上面的 Lambda 声明替换为方法引用。
MyPrinter myPrinter System.out::println;
注意双冒号 :: 向 Java 的编译器指明这是一个方法的引用。引用的方法是双冒号之后的方法。而拥有引用方法的类或对象则位于双冒号之前。
我们可以引用以下类型的方法
静态方法参数对象的实例方法实例方法类的构造方法
引用类的静态方法
最容易引用的方法是静态方法比如有这么一个函数式接口和类
public interface Finder {public int find(String s1, String s2);
}public class MyClass{public static int doFind(String s1, String s2){return s1.lastIndexOf(s2);}
}
如果我们创建 Lambda 去调用 MyClass 的静态方法 doFind
Finder finder (s1, s2) - MyClass.doFind(s1, s2);
所以我们可以使用 Lambda 直接引用 Myclass 的 doFind 方法。
Finder finder MyClass::doFind;
引用参数的方法
接下来如果我们在 Lambda 直接转发调用的方法是来自参数的方法
public interface Finder {public int find(String s1, String s2);
}Finder finder (s1, s2) - s1.indexOf(s2);
依然可以通过 Lambda 直接引用
Finder finder String::indexOf; 这个与上面完全形态的 Lambda 在功能上完全一样不过要注意简版 Lambda 是如何引用单个方法的。 Java 编译器会尝试将引用的方法与第一个参数的类型匹配使用第二个参数类型作为引用方法的参数。
引用实例方法
我们还也可以从 Lambda 定义中引用实例方法。首先设想有如下接口
public interface Deserializer {public int deserialize(String v1);
}
该接口表示一个能够将字符串“反序列化”为 int 的组件。现在有一个 StringConvert 类
public class StringConverter {public int convertToInt(String v1){return Integer.valueOf(v1);}
} StringConvert 类 的 convertToInt() 方法与 Deserializer 接口的 deserialize() 方法具有相同的签名。因此我们可以创建 StringConverter 的实例并从 Lambda 表达式中引用其 convertToInt() 方法如下所示
StringConverter stringConverter new StringConverter();Deserializer des stringConverter::convertToInt;
// 等同于 Deserializer des (value) - stringConverter.convertToInt(value) 上面第二行代码创建的 Lambda 表达式引用了在第一行创建的 StringConverter 实例的 convertToInt 方法。
引用构造方法 最后如果 Lambda 的作用是调用一个类的构造方法那么可以通过 Lambda 直接引用类的构造方法。在 Lambda 引用类构造方法的形式如下
ClassName::new
那么如何将构造方法用作 lambda 表达式呢假设我们有这样一个函数式接口
public interface Factory {public String create(char[] val);
}
Factory 接口的 create() 方法与 String 类中的其中一个构造方法的签名相匹配String 类有多个重载版本的构造方法。因此String类的该构造方法也可以用作 Lambda 表达式。
Factory factory String::new;
// 等同于 Factory factory (chars) - String.new(chars);
总结 今天这篇文章把 Lambda 表达式的知识梳理的了一遍相信看完了这里的内容再看到 Lambda 表达式的各种形态就不觉得迷惑了虽然今天的文章看起来有点枯燥不过是接下来 咱们系统学习 Stream 操作的基础以及后面介绍 Java 中提供的几个函数式编程 interface 也会用到 Lambda 里的知识后面的内容可以继续期待一下。 参考
Java 8 (2/6篇) - Lambda表达式 函数式接口FunctionalInterface Lib_java8 lambda函数式接口_Jomurphys的博客-CSDN博客
彻底弄懂FunctionalInterface、Lambda表达式和方法引用_interface 表达式-CSDN博客
Java Lambda 表达式的各种形态和使用场景看这篇就够了 - 知乎