大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: Java语法 修改讲义

复习:面向函数:函数做变量 / 回调 / 委托 / Lamda / 箭头函数……

但JavaScript可以将箭头函数赋值给任意一个变量,Java呢?


函数式接口

Java用一种特殊(仅有一个方法)的接口变量,作为lambda表达式的赋值对象/数据容器。
interface IMove{
	double move(double speed, int seconds);
}

这种接口被称之为函数()/功能(接口

@试一试@:接口中添加一个方法声明?

演示:F3到Comparator,以下方法不算作接口方法:

  • 默认方法
    default void stop() {}
  • 静态方法
    static void stop() {}
  • 重写Object中已有的方法
    String toString();
函数式接口也可以被当做普通接口使用。

PS:@FunctionalInterface注释

lambda表达式表示函数式接口的实例:

IMove m = (s, t) -> s * t;
Java中lambda使用的是单横线箭头(->)
System.out.println(m.move(2.5, 60));    //实例调用其接口方法

演示:其他规则和JavaScript箭头函数一样

类型推断

一般情况下,lambda类型可以由编译器推断。@想一想@:为什么可以推断?

什么是特殊情况呢?

Lambda表达式最重要的作用就是作为函数参数传递。假设有两个重载的方法,

static void goSchool(IMove move) {
static void goSchool(IWalk walk) {

使用了类似的函数式接口:

interface IWalk {    //和IMove类似
	double move(float speed, long seconds);
}
演示:编译时错误
goSchool((s,t)->s*t);
解决方案:
  • 显式声明参数类型
    //要声明参数类型,所有参数的类型就都要声明
    goSchool((double s,int t)->s*t);
  • 先声明Lambda变量类型
    IMove m = (s, t) -> s * t;
    goSchool(m);
  • 通过强制类型转换声明lambda表达式类型:
    goSchool((IMove)(s, t) -> s * t);

作用域和闭包:移到函数式编程

final修饰

Java允许在lambda参数前加final,以禁止lambda表达式内部修改该参数。

但如果这样,需要显式的声明参数类型:

IMove m = (final double s, int t) -> {
    s *= 2;    //error
    return s * t;
};

泛型

函数式接口可以是泛型的:
interface IMove<T> {
	T move(T speed, int seconds);
}

PS:以上对应C#中delegate


方法引用

Lambda表达式可以视为一种匿名函数

那如果我们需要传递一个已有的命名函数呢?比如把这个方法(注意是方法本身是方法运行结果)传递给IMove,IMove move = ??

static double walk(double speed, int seconds) {
	return speed * seconds;
}
这时候就需要操作符为双冒号(::)的方法引用了:
IMove move = Main::walk;    //静态方法由类名调用
System.out.println(move.move(23.5, 2));
除了静态方法以外,还有:
  • 实例方法和属性
    class Person  {
    	public double walk(double speed, int seconds) {
    IMove m = new Person()::walk;
    m.move();
    
  • 构造函数
    class Person  {
    	public Person() {
    interface IMove {
    	Person getPerson();
    IMove m = Person::new;
    Person fg =  m.getPerson();
  • 数组构造
    interface IMove {
        int[] get(int length);    //注意这个length参数
    
    IMove m = int[]::new;
    int[] students = m.get(10); //得到一个长度为10的数组
  • 类::实例方法
    interface IMove{
    	void move(Person person);    //第一个参数是Person
    class Person{
    	public void walk() {    //这是一个实例方法
    IMove m = Person::walk;    //但仍然可以用类名调用
    //等价于:IMove m = p->p.walk();
    m.move(new Person());
    (一定要注意这个当)接口方法有参数时,可以用它的第一个(且只能是第一个)参数类型,后加双冒号(::)指定它的某一个匹配实例方法。(我个人不喜欢这种写法,宁愿用箭头,避免和静态方法混淆


内置function

@想一想@:每用一次lamda表达式,就去声明一个函数接口,麻烦不麻烦?

当泛型被引入之后,解决了这个(匿名)函数的方法参数和返回值的类型问题,lambda表达式是不是可以被归类?

好像我们不需要太多的内置函数接口?

比如,以下接口是不是可以合并统一

interface IMove<T> {
	void get(T length);
}
interface IWalk<T> { 
	void get(T length);
}

所以Java为我们提供了java.util.function包,里面封装了一些内置的函数接口

中文名
示例
参数
返回
函数
Function<T,R>
T
R
谓词
Predicate<T>
T
boolen
Consumer
Consumer<T>
T
void
Supplier
Supplier<T>

T
演示:F3转到定义

Supplier

只有一个get()方法

复习:泛型参数的实例化

Consumer

“标配的”accept()方法:用于执行lambda表达式
Consumer<Integer> ci = i -> {
	System.out.println(++i);
};
ci.accept();

但多了一个default的andThen()方法:

default Consumer<T> andThen(Consumer<? super T> after) {
着重理解:
  • andThen需要被Consumer<T>实例调用
  • 传入的是一个Consumer<T>实例
  • 返回的还是一个Consumer<T>实例

这样就能够形成连缀

ci = ci.andThen(i -> {
	System.out.println(--i);
});

ci.accept(100);

甚至更进一步:

ci.andThen(i -> {
	System.out.println(--i);
}).accept(100);
断点演示:两个lambda执行的顺序

注意

  • 对andThen()的返回值执行accept()才有连缀的效果
  • accept()可以在andThen()之前调用,accept()被调用不影响andThen()的执行

Predicate

“标配的”test
Predicate<Integer> greaterThan0 = i -> i > 0;
System.out.println(greaterThan0.test(100));
另外三个default方法,套路和Consumer的andThen()一样。为了演示,添加一个:
Predicate<Integer> lessThan100 = i -> i < 100;

  • and:且,&&
    System.out.println(greaterThan0.and(lessThan100).test(-2));
  • or:或,||
    System.out.println(greaterThan0.or(lessThan100).test(80));
  • negate:取反,!
    System.out.println(greaterThan0.negate().test(80));

还有一个static的isEqual()方法:(演示讲解其实现,复习

Predicate.isEqual(32).test(new Integer(22))

@想一想@:搞这么一个方法干嘛?直接用 == 不香么?

Predicate.<Integer>isEqual(32).or(greaterThan0).test(new Integer(22))

演示说明:and(Predicate<? super T> other)

Predicate<Student> greaterThan0 = i -> i.Age > 0;
Predicate<Person> lessThan100 = i -> i.Age < 100;
//假如这样可以允许的话
//Predicate<OnlineStudent> lessThan100 = i -> i.Fee < 100;

Student fg = new Student();

/*
 * 1. 在test的时候,greaterThan0和lessThan100都使用的fg 
 * 2. 我们只能控制and()中的lambda泛型参数, 
 * 3. 确保其至少是Student类,这样predict中使用的都是Student有的类成员
 */
greaterThan0.and(lessThan100).test(fg);

Function

“标配的”apply()方法

两个default方法,在当前function

  • 之后被调用:andThen(),以当前function的返回值为输入
  • 之前被调用:compose(),运行结果作为以当前function的输入
Function<Person, Integer> getAge = p -> p.age;	
		
System.out.println(getAge
//指定i 类型方案1:	.compose((Integer i) -> new Person(i))
//指定i 类型方案2:	.<Integer>compose(i -> new Person(i))
	//还可以使用构造函数方法引用
	.<Integer>compose(Person::new)
	.andThen(a-> a*0.9)
	.apply(40));

演示:执行顺序 apply() -> compose() -> getAge -> andThen()

注意练习阅读其源代码。

其他

还有更多的function(函数接口名),可以适用于更多的情形:

  • 参数或返回值为基本类型的:
    • 添加输入参数类型前缀,比如:DoubleConsumer、IntConsumer、LongPredicate……
    • 添加输出参数和返回类型前缀,比如:IntToDoubleFunction、DoubleUnaryOperator
    作用:减少装箱拆箱,可以些许的提高性能
  • 输入参数为2个的:
    • 引用类型(泛型)添加bi前缀,比如:BiConsumer<T, U>、BiFunction<T, U, R>、BiPredicate<T, U>……
    • 基本类型添加To前缀,比如:ToDoubleBiFunction、ToIntBiFunction<T, U>……

大家开发中根据实际情况具体选择。

PS:以上内置function对应C#中的Func和Action


作业

  1. 参考之前supplier解决泛型参数类型的实例化问题,解决泛型数组不能实例化的问题
    以下皆使用内置function完成
  2. 利用三个lambda表达式连缀,完成邀请人验证功能:
    1. 根据用户名找到某用户:借用UserRepository.GetByName(name)
    2. 根据找到的用户得到其邀请码:直接lambda
    3. 比较该用户的邀请码是否和用户输入相同:直接lambda
  3. 改造User的Register()方法,接收三个函数接口参数:
    1. hasUsername:是否已有当前用户名
    2. isCaptchaCorrect:验证码是否正确
    3. hasTheInviter:邀请人用户名和密码都正确无误(利用第6题
    使用连缀,当123全部满足时,才能继续注册。
    以上用户名、验证码、邀请人用户名和邀请码都由User对象提供

其他见:J&C:Lambda表达式

学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 Java语法 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码