您当前的位置:首页 > 计算机 > 编程开发 > Java

读书笔记:Java 8 函数式编程

时间:12-14来源:作者:点击数:
CDSY,CDSY.XYZ

Lambda 表达式

Lambda 表达式的几种格式

// ()表示没有参数
Runnable noArguments = () -> System.out.println("hello");
// Lambda 表达式主体可以是一个表达式,或一个代码段,使用大括号{};
// 参数类型可以不用指定,由javac根据上下文自动进行类型推断
ActionListener oneArgument = event -> {
  System.out.println("button clicked");
  System.out.println("hello");
}
// 有且只有一个参数,可以省略();后接表达式,可省略{}
ActionListener oneArgument = event -> System.out.println("button clicked");
// 多个参数
BinaryOperator<Long> add = (x, y) -> x + y;
// 多个参数并指定参数类型
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

引用值,而不是变量

如果匿名内部类 需要引用它所在方法中的变量,则需要将该变量声明为final,即最终变量;在java8中,虽然放开了这个限制,但是变量必须是事实上的最终变量,也就是说变量不可以多次赋值,否则会编译报错【idea直接提示错误】,如下代码会提示错误

String name = "phenix";
name = "phenix";
Stream.of("1","2","3").forEach(e -> {
  System.out.println(name);
});

编译报错信息:

java: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量

函数接口

函数接口是只有一个抽象方法的接口, 用作 Lambda 表达式的类型。

Java中重要的函数接口

接口 参数 返回类型 示例
Predicate T boolean 这张唱片已经发行了吗
Consumer T void 输出一个值
Function<T,R> T R 获得 Artist 对象的名字
Supplier None T 工厂方法
UnaryOperator T T 逻辑非( !)
BinaryOperator (T, T) T 求两个数的乘积( *)

函数接口的图示:

参数类型-->接口名称-->返回值类型

@Test
public void test004() {
  Predicate<Integer> atLeast5 = x -> x > 5;
  System.out.println(atLeast5.test(1));
  System.out.println(atLeast5.test(10));

  Function<Double, Double> calculator = x -> x + x;
  System.out.println(calculator.apply(5d));

  IntPred atLeast1 = x -> x > 5;
  System.out.println(atLeast1.test(1));
  System.out.println(atLeast1.test(10));

}

类型推断

某些情况下, 用户需要手动指明类型, 建议大家根据自己或项目组的习惯, 采用让代码最便于阅读的方法。

Lambda 表达式中的类型推断, 实际上是 Java 7 就引入的目标类型推断的扩展。 比如Java 7 中的菱形操作符, 它可使 javac 推断出泛型参数的类型。

Map<String, Integer> oldWordCounts = new HashMap<String, Integer>(); 
// 可以简写为:
Map<String, Integer> diamondWordCounts = new HashMap<>();

我们为变量 oldWordCounts明确指定了泛型的类型, 而变量 diamondWordCounts则使用了菱形操作符。 不用明确声明泛型类型, 编译器就可以自己推断出来, 这就是它的神奇之处!

如果将构造函数直接传递给一个方法, 也可根据方法签名来推断类型。 如下代码中我们传入了 HashMap, 根据方法签名已经可以推断出泛型的类型。

useHashmap(new HashMap<>());
...
private void useHashmap(Map<String, String> values);

Java 7 中程序员可省略构造函数中的泛型类型, Java 8 更进一步, 程序员可省略 Lambda 表达式中的所有参数类型。 再强调一次, 这并不是魔法, javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。 程序依然要经过类型检查来保证运行的安全性, 但不用再显式声明类型罢了。 这就是所谓的类型推断。

举例1:

// 通过lambda表达式的主体推断出返回boolean值
Predicate<Integer> atLeast5 = x -> x > 5;
// 输出false
System.out.println(atLeast5.test(1));
// 输出true
System.out.println(atLeast5.test(10));

Lambda表达式实现了 Predicate 接口, 因此它的单一参数被推断为 Integer 类型。 javac 还可检查Lambda 表达式的返回值是不是 boolean, 这正是 Predicate 方法的返回类型。

举例2:

BinaryOperator。 该接口接受两个参数, 返回一个值, 参数和返回值的类型均相同。 实例中所用的类型是 Long。

BinaryOperator<Long> addLongs = (x, y) -> x + y;

如果,我们去掉,即“BinaryOperator addLongs = (x, y) -> x + y”则编译器报错:Operator '+' cannot be applied to 'java.lang.Object', 'java.lang.Object',因为javac无法推断出参数类型,只能认为是Object对象。

练习题

1、请查看 Function 函数的源码,回答问题

public interface Function<T,R> {
  R apply(T t);
}

a. 请画出该函数接口的图示

T --> Function --> R

b. 若要编写一个计算器程序, 你会使用该接口表示什么样的 Lambda 表达式?

Function<Double, Double> calculator = x -> x + x;
Function<Double, Double> calculator = x -> x - x;
Function<Double, Double> calculator = x -> x * x;
Function<Double, Double> calculator = x -> x / x;

System.out.println(calculator.apply(5d));

c. 下列哪些 Lambda 表达式有效实现了

Function<Long,Long> ?
1). x -> x + 1; [ok]
2). (x, y) -> x + 1;
3). x -> x == 1;

2、ThreadLocal Lambda 表达式。 Java 有一个 ThreadLocal 类, 作为容器保存了当前线程里局部变量的值。 Java 8 为该类新加了一个工厂方法, 接受一个 Lambda 表达式, 并产生一个新的 ThreadLocal 对象, 而不用使用继承, 语法上更加简洁。

a. 在 Javadoc 或集成开发环境( IDE) 里找出该方法。

/**
* 当前余额
*/
private ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);
public class NotSafeBank {
  /**
  * 当前余额
  */
  private int balance = 1000;

  /**
  * 存款
  *
  * @param money 存款金额
  */
  public void deposit(int money) {
  String threadName = Thread.currentThread().getName();
  System.out.println(threadName + " -> 当前账户余额为:" + this.balance);
  this.balance += money;
  System.out.println(threadName + " -> 存入 " + money + " 后,当前账户余额为:" + this.balance);
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  }
}

public class SafeBank {
  /**
  * 当前余额
  */
  private ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 1000);

  /**
  * 存款
  *
  * @param money 存款金额
  */
  public void deposit(int money) {
  String threadName = Thread.currentThread().getName();
  System.out.println(threadName + " -> 当前账户余额为:" + this.balance.get());
  this.balance.set(this.balance.get() + money);
  System.out.println(threadName + " -> 存入 " + money + " 后,当前账户余额为:" + this.balance.get());
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  }
}

@Test
public void test005() {
  SafeBank bank = new SafeBank();
  Thread thread1 = new Thread(() -> bank.deposit(200), "user1");
  Thread thread2 = new Thread(() -> bank.deposit(500), "user2");
  Thread thread3 = new Thread(() -> bank.deposit(1000), "user3");
  thread1.start();
  thread2.start();
  thread3.start();
}
/*
user1 -> 当前账户余额为:1000
user1 -> 存入 200 后,当前账户余额为:1200
user2 -> 当前账户余额为:1000
user2 -> 存入 500 后,当前账户余额为:1500
user3 -> 当前账户余额为:1000
user3 -> 存入 1000 后,当前账户余额为:2000
*/
@Test
public void test006() {
  NotSafeBank bank = new NotSafeBank();
  Thread thread1 = new Thread(() -> bank.deposit(200), "user1");
  Thread thread2 = new Thread(() -> bank.deposit(500), "user2");
  Thread thread3 = new Thread(() -> bank.deposit(1000), "user3");
  thread1.start();
  thread2.start();
  thread3.start();
}
/*
不安全的变量bank,导致不同用户使用了同一个账户

user1 -> 当前账户余额为:1000
user1 -> 存入 200 后,当前账户余额为:1200
user2 -> 当前账户余额为:1200
user2 -> 存入 500 后,当前账户余额为:1700
user3 -> 当前账户余额为:1700
user3 -> 存入 1000 后,当前账户余额为:2700
*/

b. DateFormatter 类是非线程安全的。 使用构造函数创建一个线程安全的 DateFormatter对象, 并输出日期, 如“ 01-Jan-1970”。

3、类型推断规则。 下面是将 Lambda 表达式作为参数传递给函数的一些例子。 javac 能正确推断出 Lambda 表达式中参数的类型吗? 换句话说, 程序能编译吗?

a.

Runnable helloWorld = () -> System.out.println("hello world");

b. 使用 Lambda 表达式实现 ActionListener 接口:

JButton button = new JButton();
button.addActionListener(event ->
System.out.println(event.getActionCommand()));

c. 以如下方式重载 check 方法后, 还能正确推断出 check(x -> x > 5) 的类型吗?

interface IntPred {
boolean test(Integer value);
}
boolean check(Predicate<Integer> predicate);
boolean check(IntPred predicate);  
IntPred atLeast1 = x -> x > 5;
System.out.println(atLeast1.test(1));
System.out.println(atLeast1.test(10));

外部迭代和内部迭代

首先,让我们看一个例子:使用for语句计算来自London的艺术家人数

int count = 0;
for (Artist artist : allArtists) {
  if (artist.isFrom("London")) {
  	count++;
  }
}

从上述代码可以看出,for语句使用了固定的模板代码:for xxxx {};这样的方式,代码量多了,同时也不能流畅的表达程序的逻辑。单个for语句可能还好理解,但是多个for嵌套的话,则很难一眼看出程序表达的意思。for语句背后的原理,其实是使用了迭代器iterator。使用Iterator对象来控制整个迭代过程,这就是外部迭代。上述的for语句可以修改为iterator的,如下:

int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
  Artist artist = iterator.next();
  if(artist.isFrom("London")) {
  count++;
  }
}

图示外部迭代:

外部迭代的缺点:很难进行如流一般的抽象操作;本质来讲是一种串行化的操作。

内部迭代需要实现Stream接口,如stream()方法返回的就是一个Stream接口。查找来自London的艺术家数量的功能内部迭代实现如下:

long count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();

内部迭代少了for语句那样的模板代码,同时,从抽象上通过filter过滤出需要的数据,使用count进行数量统计。这样的代码,可读性更高,一眼就可以看出程序的逻辑。

图示内部迭代:

Stream 是用函数式编程方式在集合类上进行复杂操作的工具。

为了找出来自伦敦的艺术家, 需要对 Stream 对象进行过滤: filter。 过滤在这里是指“ 只保留通过某项测试的对象”。 测试由一个函数完成, 根据艺术家是否来自伦敦, 该函数返回 true 或者 false。 由于 Stream API 的函数式编程风格, 我们并没有改变集合的内容, 而是描述出 Stream 里的内容。 count() 方法计算给

定 Stream 里包含多少个对象。 返回的 Stream 对象不是一个新集合, 而是创建新集合的配方。【诡异的翻译】

实现机制

像filter 这样只描述 Stream, 最终不产生新集合的方法叫作惰性求值方法; 而像 count 这样最终会从 Stream 产生值的方法叫作及早求值方法(判断一个操作是惰性求值还是及早求值很简单: 只需看它的返回值。 如果返回值是 Stream,那么是惰性求值; 如果返回值是另一个值或为空, 那么就是及早求值。 )整个过程和建造者模式有共通之处。 建造者模式使用一系列操作设置属性和配置, 最后调用一个 build 方法, 这时, 对象才被真正创建。

由于是惰性求值,如下代码,并不会打印出任何信息:

allArtists.stream().filter(artist -> {
  System.out.println(artist.getName());
  return artist.isFrom("London");
});

要打印出艺术家姓名,那么需要再加一个终止操作的流:

long count = allArtists.stream().filter(artist -> {
  System.out.println(artist.getName());
  return artist.isFrom("London");
}).count();

常用流操作

collect(tolist())

collect(tolist())方法由stream里的值生成一个列表,是一个及早求值操作。

// demo1: 将一串字符转换为List集合
List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList()); 
assertEquals(Arrays.asList("a", "b", "c"), collected);
// demo2: 对象Person,有cardNo,name,age,gender等属性
@Getter
@Sette
@Builder
public class Person() {
  // 身份证
  private String cardNo;
  private String name;
  private Integer age;
  private String gender;
}

// 将Person集合persons转化为Map<String,Person>对象,key为cardNo,value为Person对象。
List<Person> persons = new ArrayList(1);
persons.add(Person.builder().cardNo("xxxxx").name("xxxxx").build());
// Function.identity()返回当前输入参数,这里就是Person对象
persons.stream().collect(Collectors.toMap(Person::getCardNo, Function.identity()));
map

如果有一个函数可以将一种类型的值转换成另外一种类型, map 操作就可以使用该函数, 将一个流中的值转换成一个新的流 。Lambda 表达式必须是 Function 接口的一个实例 。map方法为惰性求值。

// demo1: 小写转换为大写
List<String> collected = Stream.of("a", "b", "hello").map(e->e.toUppperCase()).collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "hello"), collected);
filter

filter用于过滤数据,他接受一个返回true或false的接口函数,即我们前面提到过的Predicate 。结果为true的数据会被保存下来。filter方法为惰性求值。

// 过滤首字母为数字的字符串
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1").filter(value -> isDigit(value.charAt(0))).collect(toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);  
filtermap

flatMap 方法可用 Stream 替换值, 然后将多个 Stream 连接成一个 Stream。flatMap 方法的相关函数接口和 map 方法的一样, 都是 Function 接口, 只是方法的返回值限定为 Stream 类型罢了 。

// 将两个list合并为一个新的list
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
  .flatMap(numbers -> numbers.stream())
  .collect(Collectors.toList());
System.out.println(together);
max & min

Stream 上常用的操作之一是求最大值和最小值。 Stream API 中的 max 和 min 操作足以解决这一问题。

// 获取薪水最高和最低的职员
@Test
public void test009() {
  List<Employee> empList = Arrays.asList(
  new Employee(1, "Jeff Bezos", 100000.0),
  new Employee(2, "Bill Gates", 200000.0),
  new Employee(3, "Mark Zuckerberg", 300000.0)
  );
  Employee maxSalaryEmp = empList.stream().max(Comparator.comparing(Employee::getSalary)).get();
  System.out.println(maxSalaryEmp);
  Employee minSalaryEmp = empList.stream().min(Comparator.comparing(Employee::getSalary)).get();
  System.out.println(minSalaryEmp);
}
/* 输出
Employee(id=3, name=Mark Zuckerberg, salary=300000.0)
Employee(id=1, name=Jeff Bezos, salary=100000.0)
*/

reduce

reduce 操作可以实现从一组值中生成一个值。 在上述例子中用到的 count、 min 和 max 方法, 因为常用而被纳入标准库中。 事实上, 这些方法都是 reduce 操作。 reduce方法的函数接口为BinaryOperator 。

@Test
public void test010() {
  int sum = Stream.of(1, 2, 3).reduce(4, Integer::sum);
  System.out.println(sum);
}
// 输出:10

// 展开reduce操作
@Test
public void test011() {
  BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
  int count = accumulator.apply(accumulator.apply(accumulator.apply(4, 1), 2), 3);
  System.out.println(count);
}
// 输出:10

// 等效于如下代码:
int acc = 4;
for(Integer element : Arrays.asList(1, 2, 3)) {
  acc += element;
}
System.out.println(acc);
peek

peek 方法让你能查看每个值, 同时能继续操作流。

@Test
public void test002() {
  Employee[] arrayOfEmps = {
  new Employee(1, "Jeff Bezos", 100000.0),
  new Employee(2, "Bill Gates", 200000.0),
  new Employee(3, "Mark Zuckerberg", 300000.0)
  };
  List<Employee> empList = Arrays.asList(arrayOfEmps);
  empList.stream()
  .peek(e -> e.salaryIncrement(10))
  .peek(System.out::println)
  .collect(Collectors.toList());
  assertThat(empList, contains(
  hasProperty("salary", equalTo(110000.0)),
  hasProperty("salary", equalTo(220000.0)),
  hasProperty("salary", equalTo(330000.0))
  ));
}
CDSY,CDSY.XYZ
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐