vlambda博客
学习文章列表

JAVA8实战 函数式编程

JAVA8实战 函数编程部分


函数式编程

 JAVA8支持函数作为一等公民,用类::函数名或  Lambda  表达式进行函数传递,jvm会自动传递给最合适的方法。

这样的好处是代码更加简洁,容易读。

e.g  假设你有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples的列表。你可能想要选出所有的绿苹果,并返回一个列表。

java8之前代码
    public static List<Apple> filterGreenApples(List<Apple> inventory){
       List<Apple> result = new ArrayList<>();
       for (Apple apple: inventory){
           if ("green".equals(apple.getColor())) {
               result.add(apple);
          }
      }
       return result;
  }

 但是接下来,有人可能想要选出重的苹果,比如超过150克  ,就容易复制粘贴,改一行代码,写一个新的方法

    public static List<Apple> filterHeavyApples(List<Apple> inventory){
       List<Apple> result = new ArrayList<>();
       for (Apple apple: inventory){
           if (apple.getWeight() > 150) {
               result.add(apple);
          }
      }
       return result;
  }
使用函数式编程

我们发现这两个方法只有一行判断的代码不一样,可以吧这个判断代码做个方法传递进来。这样代码更加简洁

 public static boolean isGreenApple(Apple apple) {
       return "green".equals(apple.getColor());
  }

   public static boolean isHeavyApple(Apple apple) {
       return apple.getWeight() > 150;
  }

   public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
       List<Apple> result = new ArrayList<>();
       for(Apple apple : inventory){
           if(p.test(apple)){
               result.add(apple);
          }
      }
       return result;
  }      

比如要过滤出绿苹果 ,只要把isGreenApple传递,调用这个方法就可以了

 List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
Lambda

也可以用lambda来调用

List<Apple> greenApples2 = filterApples(inventory, (Apple a) -> "green".equals(a.getColor()));

Stream  

Stream API( java.util.stream)解决了这两个问题:集合处理时的套路和晦涩,以及难以利用多核。

 Java 8的设计师几乎可以就此打住了,要是没有多核CPU,可能他们真的就到此为止了。我们迄今为止谈到的函数式编程竟然如此强大,在后面你更会体会到这一点。本来, Java加上filter和几个相关的东西作为通用库方法就足以让人满意了,比如  

static <T> Collection<T> filter(Collection<T> c, Predicate<T> p);

 这样你甚至都不需要写filterApples了,因为比如先前的调用  

filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

 就可以直接调用库方法filter: 

filter(inventory, (Apple a) -> a.getWeight() > 150 );

 但是,为了更好地利用并行, Java的设计师没有这么做。Java 8中有一整套新的类集合API——Stream,它有一套函数式程序员熟悉的、类似于filter的操作,比如map、 reduce,还有我们接下来要讨论的在Collections和Streams之间做转换的方法 。

Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。这里的关键点在于, Stream允许并提倡并行处理一个Stream中的元素。  

默认方法  

可以在接口中使用默认方法,在实现类没有实现方法时提供方法内容。  

 在Java 8里,你现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态方法: 

default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}


第二章 通过行为参数化传递代码

行为参数化  

 行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。 

e.g  从列表中筛选绿苹果的功能  

 第一种解决方案

 public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if("green".equals(apple.getColor())){
result.add(apple);
}
}
return result;
}

 但是现在农民改主意了  ,他想要筛选多种颜色:浅绿色、暗红色、黄色等,这种方法就应付不了了。一个良好的原则是在编写类似的代码之后,尝试将其抽象化。  

 一种做法是给方法加一个参数,把颜色变成参数  

 public static List<Apple> filterApplesByColor(List<Apple> inventory, String color){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(apple.getColor().equals(color)){
result.add(apple);
}
}
return result;
}

 这位农民又跑回来和你说:“要是能区分轻的苹果和重的苹果就太好了。重的苹果一般是重量大于150克。”  

 一种把所有属性结合起来的笨拙尝试  

public static List<Apple> filterApples(List<Apple> inventory, String color,
int weight, boolean flag) {
List<Apple> result = new ArrayList<Apple>();
for (Apple apple: inventory){
if ( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight) ){
result.add(apple);
}
}
return result;
}


 如果这位农民要求你对苹果的不同属性做筛选,比如大小、形状、产地等,又怎么办?而且,如果农民要求你组合属性,做更复杂的查询,比如绿色的重苹果,又该怎么办?你会有好多个重复的filter方法,或一个巨大的非常复杂的方法。  

行为参数化  

首先  定义一个接口来对选择标准建模  

public interface ApplePredicate{
boolean test (Apple apple);
}

 现在你就可以用ApplePredicate的多个实现代表不同的选择标准了  

public class AppleHeavyWeightPredicate implements ApplePredicate{//仅仅选出重的苹果
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{//仅仅选出绿苹果
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}

 你可以把这些标准看作filter方法的不同行为。你刚做的这些和“策略设计模式” 相关,它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。在这里,算法族就是ApplePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。 

利用ApplePredicate改过之后, filter方法看起来是这样的: 

public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
   for(Apple apple: inventory){
       if(p.test(apple)){
           result.add(apple);
      }
}
return result;
}

 现在的代码已经足够灵活了,比如,如果农民让你找出所有重量超过150克的红苹果,你只需要创建一个类来实现ApplePredicate就行了。你的代码现在足够灵活,可以应对任何涉及苹果属性的需求变更了 。

 行为参数化的好处  : 你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的  。

JAVA8实战 函数式编程

使用lambda

 上面的代码在Java 8里可以用Lambda表达式重写为下面的样子: 

List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

 在通往抽象的路上,我们还可以更进一步。目前, filterApples方法还只适用于Apple。你还可以将List类型抽象化,从而超越你眼前要处理的问题: 

public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
   List<T> result = new ArrayList<>();
   for(T e: list){
       if(p.test(e)){
       result.add(e);
      }
}
return result;
}

 现在你可以把filter方法用在香蕉、桔子、 Integer或是String的列表上了。这里有一个使用Lambda表达式的例子:

List<Apple> redApples =filter(inventory, (Apple apple) ->"red".equals(apple.getColor()));

List<Integer> evenNumbers =filter(numbers, (Integer i) -> i % 2 == 0);

第三章 Lambda表达式

下面哪些是正确的表达式

 (1) () -> {}(2) () -> "Raoul"(3) () -> {return "Mario";}(4) (Integer i) -> return "Alan" + i;(5) (String s) -> {"IronMan";}  

 答案:只有4和5是无效的Lambda。(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:public void run() {}。(2) 这个Lambda没有参数,并返回String作为表达式。(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。 

(4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i) -> {return "Alan" + i;}。(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示:(String s) -> "Iron Man"。或者如果你喜欢,可以使用显式返回语句,如下所示:(String s)->{return "IronMan";}  

函数式接口

什么是函数式接口

 接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。 

函数描述符  

 函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。 

常见的函数试接口

Predicate  

例子

@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
   for(T s: list){
       if(p.test(s)){
       results.add(s);
      }
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer  

 接受泛型T的对象,没有返回值。如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。 

例子

@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
   for(T i: list){
c.accept(i);
}
}
forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));
Function  

接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口 。

例子

@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list,
   Function<T, R> f) {
   List<R> result = new ArrayList<>();
   for(T s: list){
  result.add(f.apply(s));
  }
   return result;
}
// [7, 2, 6]
List<Integer> l = map(Arrays.asList("lambdas","in","action"),(String s) -> s.length());

Java 8中的常用函数式接口  

JAVA8实战 函数式编程

JAVA8实战 函数式编程

Lambdas及函数式接口的例子  

JAVA8实战 函数式编程

函数式接口  异常抛出

 任何函数式接口都不允许抛出受检异常( checked exception)。如果你需要Lambda表达式来抛出异常, 有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。 

  1. 自己的函数式接口,并声明受检异常

@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();
  1. 把Lambda包在一个try/catch块中。

    Function<BufferedReader, String> f = (BufferedReader b) -> {
       try {
      return b.readLine();
      }
    catch(IOException e) {
    throw new RuntimeException(e);
    }
    };

类型检查过程

JAVA8实战 函数式编程

 类型检查过程可以分解为如下所示。 首先,你要找出filter方法的声明。 第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。 第三, Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。 第四, test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。 最后, filter的任何实际参数都必须匹配这个要求。这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。 

方法引用  

方法引用例子

方法引用分类

方法引用主要有三类。(1) 指向静态方法的方法引用(例如Integer的parseInt方法, 写作Integer::parseInt)。 

(2) 指 向 任 意 类 型 实 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 写 作String::length)。(3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。

注:第二种和第三种区别在于 第二种调用的方法是传入对象自身的方法,第三种是  Lambda 中 调 用 的是外部对象的方法。 

构造函数引用  

 用它的名称和关键字new来创建它的一个引用:ClassName::new。 

例子

Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

等价于

Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);

谓词复合  

 谓词接口包括三个方法:negate、 and和or,让你可以重用已有的Predicate来创建更复杂的谓词。

e.g

//苹果不是红的
Predicate<Apple> notRedApple = redApple.negate();
//一个苹果既是红色又比较重
redicate<Apple> redAndHeavyApple =redApple.and(a -> a.getWeight() > 150);
//表达要么是重( 150克以上)的红苹果,要么是绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen =redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));

 请注意 and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此, a.or(b).and(c)可以看作(a || b) && c。  

函数复合  

 Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

andThen
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
//数学上会写作g(f(x))
Function<Integer, Integer> h = f.andThen(g);
//输出结果->4
int result = h.apply(1);
compose
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
//数学上会写作f(g(x))
Function<Integer, Integer> h = f.compose(g);
//输出结果 3
int result = h.apply(1);