java基础(7)-lambda

本文要讲几个知识点:

  1. 关于lambda表达式的东西
    • 来由
    • 匿名内部类访问外部变量
    • 什么时候能够使用lambda
  2. 讲讲几个基础的函数式接口的使用
    • Function
    • Predicate
    • Supplier & Consumer
  3. 流的使用
    • 方法引用
    • 流的副作用(side-effects)
    • 流的一些有用却有些陌生的操作

本文不再赘述,lambda表达式的语法,网上有很多哦

1. 关于lambda表达式的东西

1.1 来由

我们在JDK8前(只是想说明,lambda是JDK8引入进来的)常常会看到以下代码

1
2
3
4
5
6
7
8
9
10
11
12
interface MyFunction{
    int add(int a, int b);
}

...

MyFunction mf = new MyFunction() {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
};

上面的内部实现是不是看起来很丑,而lambda让上面的代码看上去更加的紧凑(compactly).将上面的实现,改为lambda表达式

1
2
3
MyFunction mf = (a, b) -> a + b;

// 这样的代码看上去,直观多了;

1.2 匿名内部类访问外部变量

先下结论,匿名内部类可以修改外部类的成员变量,但是不能修改外部类的局部变量

1.2.1 修改外部类的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LambdaScopeTest {
  // 成员变量
    int x = 4;

    public MyFunction getFunction(){
    return new MyFunction() {
        @Override
        public int add(int a, int b) {
          // 这是允许的
            x = 3;
            return x + a + b;
        }
    };
    }

}
interface MyFunction{
    int add(int a, int b);
}

将以上代码编译一下

class LambdaScopeTest$1 implements MyFunction {
    LambdaScopeTest$1(LambdaScopeTest var1) {
        this.this$0 = var1;
    }

    public int add(int var1, int var2) {
        this.this$0.x = 3;
        return this.this$0.x + var1 + var2;
    }
}

我们可以看见,内部类(LambdaScopeTest$1)拿到了外部类(LambdaScopeTest)的引用,只是改变了引用下的属性x, 但是外部类的引用指向的地址并没有改变,这是允许的。

1.2.2 访问局部变量

1
2
3
4
5
6
7
8
9
10
11
12
    public MyFunction getFunction(){
      int x = 4;
      return new MyFunction() {
        @Override
        public int add(int a, int b) {
            // Variable 'x' is accessed from within inner class, 
            // needs to be final or effectively final
            // x = 3; 
            return x + a + b;
          }
        };
     }

使用同样的方法我们编译一下

class LambdaScopeTest$1 implements MyFunction {
    LambdaScopeTest$1(LambdaScopeTest var1, int var2) {
        this.this$0 = var1;
        // 将外部的局部变量直接复制一份到内部来
        this.val$x = var2;
    }

    public int add(int var1, int var2) {
        return this.val$x + var1 + var2;
    }
}

因为,内部的值改变了,外部的值并没有改变,如果该类型是引用类型,其内部类的引用地址改变了,外部的引用地址也不会变的(指向的地址都不同了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    ...
    public static void main(String[] args) {
        TestScope ts = new TestScope();
        TestScope ts1 = ts;
        // ts1指向了一个新的地址
        ts1 = new TestScope();
        System.out.println(ts);
        System.out.println(ts1);
    }
    ... 
    // output:
    // com.ukyu.base.stream.TestScope@449b2d27
    // com.ukyu.base.stream.TestScope@5479e3f

    // 地址并不相同了

从编译的代码我们可以看见,对外部的值进行了拷贝,为了不必去考虑外部与内部变量修改后的可见性,将外部的变量定义为了final,在JDK8之后,不加final也行,java默认将变量修饰为 effectively final (相当于隐式的加了一个final在变量前面)。

1.3 什么时候能够使用lambda

当某个接口上使用了 @FunctionalInterface 就可以用lambda了;functional interface其实就是指的,只包含一个抽象方法的接口,可以包含一个或多个的默认方法或静态方法。 只要接口只包含一个需要去实现的方法(不管这个接口是否被@FunctionalInterface 给标识),就可以省略其方法名,使用lambda

2. 讲讲几个基础的函数式接口的使用

这也是JDK8以后引入的新玩意儿,建议配合lambda一起使用。讲四个函数式的接口,Function、Predicate、Consumer、Supplier,其余的类似的函数式接口都是这四个的一个扩展。

2.1 Function

表示接受一个参数并且生成一个结果的一个函数 –jdk

1
2
3
4
5
6
7
8
9
...
// 将传入的一个参数进行扩大两倍的处理
Function<Integer, Integer> func = a -> a * 2;
log.info(func.apply(3));

...

// output:
// 6

该接口除了apply()方法,还有andThen()’之后’, compose()’之前’,identity(),我们再讲讲identity()这个方法

1
2
3
4
5
    // identity的源码
    static <T> Function<T, T> identity() {
        return t -> t;
    }

identity() 相等于f(x) = x, 传入什么值就返回什么值。恒等函数。 更多的可以看看Usage of Function.identity with Examples

2.2 Predicate

表示一个值的断言(布尔值函数) –jdk

1
2
3
4
5
6
7
    Predicate<Integer> predicate = i -> i > 3;
    // 判断 3 是否大于 3
    log.info(predicate.test(3));
    ...

    // output:
    // false

Predicate还有and、negate、or和一个静态方法isEqual,这里不再多讲。

2.3 Supplier & Consumer

一个提供者,一个消费者,这两个一起写一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
////   ----     Consumer
        Consumer<List<Integer>> consumer = a ->
                a.forEach(i -> System.out.print(i + " "));

////    ---------    Supplier
        List<Integer> list = new ArrayList<>(8);
        Supplier<List<Integer>> s = () -> {
            for(int i = 0; i< 8; i++)
            {
                list.add(i);
            }
            return list;
        };
        consumer.accept(s.get());
        
        // output:
        // 0 1 2 3 4 5 6 7   

3. 流(stream)的使用

先看看下面的例子, 参考lambdaexpressions

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
class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;
    int age;

//    省略getter、setter
}

...
public static void processPersonsWithFunction(
            List<Person> roster,
            Predicate<Person> tester,
            Function<Person, String> mapper,
            Consumer<String> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                String data = mapper.apply(p);
                block.accept(data);
            }
        }
    }
...

// 使用lambda表达式
// 获取18-25岁女性的邮箱
    List<Person> roster = new ArrayList<>(10);
    processPersonsWithFunction(
            roster,
            p -> p.getGender() == Person.Sex.MALE
                    && p.getAge() >= 18
                    && p.getAge() <= 25,
            p -> p.getEmailAddress(),
            email -> System.out.println(email)
    );

我们可以使用’stream’,来替换以上的lambda操作

1
2
3
4
5
6
7
    roster.stream()
            .filter(
                    p -> p.getGender() == Person.Sex.MALE
                            && p.getAge() >= 18
                            && p.getAge() <= 25)
            .map(p -> p.getEmailAddress())
            .forEach(email -> System.out.println(email));

流让我想起了“曲水流觞”的感觉,添加一系列的操作,然后得到我们想要的结果。

An operation on a stream produces a result, but does not modify its source.

下面科普一下关于流的操作可以稍稍看一下

1
2
3
4
5
6
7
8
9
//    流分为中间操作(返回新流的操作)、终端操作
//
//    中间操作都是懒加载,不会立即执行,只有当终端操作执行才会开始执行
//    中间操作分为:
//      1. 无状态  如: filter and map, 跟之前的元素有关
//      2. 有状态  distinct and sorted

//    短路操作: 中间操作对无限的输入,返回一个有限的流时;
//              终端操作,操作无限的流在有限的时间内,这些操作就是短路操作;

3.1 方法引用

若你装了Alibaba Java Coding Guidelines这个插件,上述代码它应该建议你这样写

1
2
3
4
5
6
7
8
    roster.stream()
            .filter(
                    p -> p.getGender() == Person.Sex.MALE
                            && p.getAge() >= 18
                            && p.getAge() <= 25)
            // 这就是方法引用
            .map(Person::getEmailAddress)
            .forEach(System.out::println);

当使用lambda表达式时,若表达式没有做任何其他事却调用了某个方法,最好使用方法引用,看上去更加清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    ...
    .map(p -> p.getEmailAddress())
    //           ↓
    .map(Person::getEmailAddress)

    ...

// 下面是使用方法引用的例子
//
//              Kind	                                                                        Example
//    Reference to a static method	                                                ContainingClass::staticMethodName
//    Reference to an instance method of a particular object	                    containingObject::instanceMethodName
//    Reference to an instance method of an arbitrary object of a particular type	ContainingType::methodName
//    Reference to a constructor	                                                ClassName::new

3.2 流的副作用(side-effects)

我们看看下面的例子

1
2
3
4
5
6
7
8
// 伪代码
        // ArrayList<String> results = new ArrayList<>();
        // stream().filter(s -> s.length() > 5)
        //     .forEach(s -> results.add(s));  // Unnecessary use of side-effects!

        //     List<String> results = 
        // stream.filter(s -> pattern.matcher(s).matches())
        // .collect(Collectors.toList());  // No side-effects!

side-effects就是在操作流的同时,还改变了其他外部的状态(除流之外的);更多的了解,可以查看副作用 (计算机科学)

3.3 流的一些有用却有些陌生的操作

  • peek
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
          // peek(); 主要支持调试,访问一个元素就执行peek中的元素
         Stream.of("one", "two", "three", "four")
                 .filter(e -> e.length() > 3)
                 .peek(e -> System.out.println("Filtered value: " + e))
                 .map(String::toUpperCase)
                 .peek(e -> System.out.println("Mapped value: " + e))
                 .collect(Collectors.toList()).stream();
    
      // output:
      // Filtered value: three
      // Mapped value: THREE
      // Filtered value: four
      // Mapped value: FOUR
    
  • iterate
1
2
3
4
5
6
7
8
9
    // 产生一个无限序列,其规则是调用者自定义的
    // f = n + 1, seed = 0, 以此产生f(seed)、f(f(seed))....
        Stream.iterate(0, n -> n + 1)
                .limit(10)
                .forEach(a -> System.out.print(a + " "));

        // output: 
        // 0 1 2 3 4 5 6 7 8 9 

  • joining
1
2
3
4
5
6
7
8
9
        List<String> l1 = new ArrayList<>();
        l1.add("a");
        l1.add("b");
        l1.add("c");
        l1.add("d");
        // 每个元素之间添加一个空格
        log.info(l1.stream().collect(Collectors.joining(" ")));
        // output:
        // a b c d

流有很多有用的方法,这篇文章也相当于抛砖引玉,感兴趣的可以去看看Stream以及Collectors其余的方法,并拿来写一些小小的demo。


对自己的现状不满意只有付出更多的努力去改变它

如果有不对的地方或建议,请指出,谢谢啦