关于 Lambda 表达式的一些事

DavReinhart 7年前
   <p>从1998年 Java 发布以来,Java 版本从1.1到目前的 Java8,Java一直在升级,在增加新功能。这也就很好的解释了为什么 Java 在编程语言排行榜上一直占据前排位置。不断的创新才是保持优势的正确方式。Java8 在2014年被发布,提供了更多的编程工具和概念,让开发人员以更加简洁、易维护的方式去解决问题。Lambda 表达式作为 Java8 中一个重要的特性,是不可不学的一部分。</p>    <h2>为什么需要 Lambda 表达式</h2>    <h3>怎么解决点外卖的问题</h3>    <p>都说人生有三大难题:早饭吃什么?午饭吃什么?晚饭吃什么?每当到了</p>    <p>饭点,在决定点哪家外卖的时候,都像是在作出人生抉择。我们就从点外卖这件事上来讲一讲为什么需要引入 Lambda 表达式。</p>    <p>首先,我们定义一下我们供我们点外卖的店铺:</p>    <pre>  <code class="language-java">publicclassStore{  privateString name;//商店名称  privatedoubleavePrice;//平均价格  privateintaveDeliveryTime;//平均送餐时间  privateString type;//商店类型    publicStore(){     }    publicStore(String name,doubleavePrice,intaveDeliveryTime,String type){  this.name = name;  this.avePrice = avePrice;  this.aveDeliveryTime = aveDeliveryTime;  this.type = type;   }    @Override  publicStringtoString(){  returnString.format("店铺名称:%s 平均价格:%d 平均送餐时间:%d 店铺类型:%s",  this.name,this.avePrice,this.aveDeliveryTime,this.type);   }  /*省略get和set方法*/  }  </code></pre>    <p>接着,我们开始定外卖,现在到月底了,又没钱了,点外卖也不挑什么了,就找最便宜的,省钱是硬道理。找最便宜的外卖店应该怎么找呢,很自然的就是对店铺的价格进行排序。</p>    <pre>  <code class="language-java">publicstaticvoidmain(String[] args){   List<Store> stores = newArrayList<Store>();   stores.add(newStore("店铺一",20,10,"快餐"));   stores.add(newStore("店铺二",30,20,"烧烤"));   stores.add(newStore("店铺三",10,20,"披萨"));   stores.add(newStore("店铺四",15,30,"炸鸡"));   stores.add(newStore("店铺五",45,20,"甜品"));   Collections.sort(stores, newComparator<Store>() {  publicintcompare(Store o1, Store o2){  returnDouble.compare(o1.getAvePrice(),o2.getAvePrice());   }   });   }  </code></pre>    <p>可以看到这样的确解决了问题,但是当我们有新的需求的时候,比如说我想找出送餐时间最短的店铺,或者是我只想找出卖快餐的店铺时,这个代码就变得不适用了,我们需要更改代码来实现新的功能。这个还是简单的情况,真实开发中情况更加复杂,显然每次遇到新的需要就大改代码这样很不优雅,该怎么解决这个问题。熟悉设计模式的朋友很快就会想到,这很符合策略模式的情况,我们每次只是更改不同的选择店铺策略去选择店铺。</p>    <h3>用策略模式来解决点外卖的问题</h3>    <p>下面我们重新用策略模式来完成我们挑选外卖店铺的任务。我们将挑选店铺的策略抽象出来:</p>    <pre>  <code class="language-java">publicinterfaceSelector{  publicList<Store>select(List<Store> stroes);  }  </code></pre>    <p>然后实现这个选择:</p>    <pre>  <code class="language-java">//将店铺按照价格排序  classPriceSelectorimplementsSelector{  publicList<Store>select(List<Store> stroes){   Collections.sort(stroes, newComparator<Store>() {  publicintcompare(Store o1, Store o2){  returnDouble.compare(o1.getAvePrice(),o2.getAvePrice());   }   });  returnstroes;   }  }  //将店铺按照送餐时间排序  classTimeSelectorimplementsSelector{  publicList<Store>select(List<Store> stroes){   Collections.sort(stroes, newComparator<Store>() {  publicintcompare(Store o1, Store o2){  returno1.getAveDeliveryTime() - o2.getAveDeliveryTime();   }   });  returnstroes;   }  }  //选择所有的快餐店  classTypeSelectorimplementsSelector{  publicList<Store>select(List<Store> stroes){   List<Store> result = newArrayList<Store>();  for(Store store:stroes){  if("快餐".equals(store.getType())){   result.add(store);   }   }  returnresult;   }  }  </code></pre>    <p>之后我们就可以同传递不同的选择方式来选择外卖店:</p>    <pre>  <code class="language-java">publicstaticList<Store>selectStore(List<Store> stores,Selector s){  returns.select(stores);   }    publicstaticvoidmain(String[] args){   List<Store> stores = newArrayList<Store>();   stores.add(newStore("店铺一",20,10,"快餐"));   stores.add(newStore("店铺二",30,20,"烧烤"));   stores.add(newStore("店铺三",10,20,"披萨"));   stores.add(newStore("店铺四",15,30,"炸鸡"));   stores.add(newStore("店铺五",45,20,"甜品"));   Selector s = newTypeSelector();   stores = selectStore(stores,s);  for(Store store : stores){   System.out.println(store.toString());   }   }  </code></pre>    <p>这样一来看上去貌似是解决了我们的问题,当有新的选择方式的时候,我们只需要实现一个新的选择类,用户端基本上不用改变任何代码。但是仔细一考虑,当选择方式越来越多的时候,每次都需要重新实现新的类,而且仔细观察发现每个选择类中真正不同的只有 select() 方法中的代码而已。</p>    <h3>Lambda 在点外卖为题中的应用</h3>    <p>有什么办法可以让我们不用每次都写那么繁琐的代码呢,这个时候就需要 Lambda 表达式登场了。等了这么久,终于写到文章的主题了(我的文章就是这么又臭又长)。为什么 Lambda 表达式可以简化我们的代码呢,因为 Lambda 提出的是一个 <strong>行为参数化</strong> 的概念,首先 <strong>行为</strong> 就是我们上面讲到的 select() 方法中的代码,这是选择的具体行为, <strong>参数化</strong> 就是向方法中传递参数一样。 <strong>行为参数化</strong> 就是定义了一种新的编程方式,让我们可以将代码像参数一样传递到方法中使用。</p>    <p>看一下我们使用 Lambda 表达式完成店铺选择的功能是怎么样的:</p>    <pre>  <code class="language-java">publicstaticvoidmain(String[] args){   List<Store> stores = newArrayList<Store>();   stores.add(newStore("店铺一",20,10,"快餐"));   stores.add(newStore("店铺二",30,20,"烧烤"));   stores.add(newStore("店铺三",10,20,"披萨"));   stores.add(newStore("店铺四",15,30,"炸鸡"));   stores.add(newStore("店铺五",45,20,"甜品"));   stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()));  for(Store store : stores){   System.out.println(store.toString());   }   }  </code></pre>    <p>stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice())); 就这么一行代码代替了我们之前那么多的代码,可以看到 Lambda 表达式可以使得我们的程序变得多么简洁吧。 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()) 这行跟表达式一样的东西就这么传递给了 sort() 方法。这就是所谓的行为参数化。</p>    <h2>什么是 Lambda 表达式</h2>    <p>Lambda 表达式可以理解为 <strong>简洁地表示可传递的匿名函数的一种方式</strong> :他没有名称,但他有参数列表、函数主体、返回类型,还有一个可以抛出的异常列表。</p>    <p>Lambda 表达式由三个部分组成:参数列表、箭头、Lambda 主体。具体的形式表现为 (parameters) -> expression 或 (parameters) -> {statements;} 。</p>    <p>可以看到我们之前的 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()) 是 (parameters) -> expression 的形式, (Store o1,Store o2) 是参数列表 , Double.compare(o1.getAvePrice(),o2.getAvePrice()) 是表达式。当 Lambda 主体中有多条语句的时候,就需要符合 (parameters) -> {statements;} 的形式了( <strong>注意{}</strong> )。</p>    <h2>怎么使用 Lambda 表达式</h2>    <p>在介绍 Lambda 表达式的使用之前,先提出一个概念:函数式接口。函数式接口定义:只定义一个抽象方法的接口。为什么需要这个概念呢,在 Java 中,”123” 是 String 类型; 12.3 是 Double 类型;true 是 boolean 类型…那 Lambda 这个实现行为参数化的新规范是什么类型呢,为了更符合我们的思维方式,Java8 给 Lambda 归为函数式接口类型。这样,我们能以我们更加理解的方式去学习 Lambda。</p>    <h3>明确行为参数化</h3>    <p>这其实就是一个去同存异的步骤,从上面的代码中我们可以发现,真正不同的地方就是选择店铺的策略上,所以我们需要做的就是将 select() 的行为参数化。</p>    <h3>定义函数式接口来传递行为</h3>    <p>知道我们需要参数化的行为之后,我们需要定义一个函数式接口,以便于我们之后传递 Lambda 表达式。</p>    <pre>  <code class="language-java">@FunctionalInterface  publicinterfaceStoreSelect{  booleanselect(Store store);  }  </code></pre>    <p>我们将对店铺的判断逻辑抽取出来,将其进行行为参数化。 @FunctionalInterface 这个注解并不是必须的,但是为了将函数式接口与其他普通的接口区分开来,最好加上这个注解。</p>    <h3>执行行为</h3>    <p>定义完函数式接口之后,我们需要定义一个方法来接受 Lambda 表达式的参数,不然我们应该从哪边传入 Lambda 参数呢。</p>    <pre>  <code class="language-java">publicstaticList<Store>selectStore(List<Store> stores,StoreSelect s){   List<Store> result = newArrayList<>();  for(Store store:stores){  if(s.select(store)){   result.add(store);   }   }  returnresult;   }  </code></pre>    <p>可以看到我们调用了 StoreSelect 接口中的 select() 方法,但是我们并没有哪边实现过这个接口,这就需要我们在下一步中传入 Lambda 表达式来实现这个接口。</p>    <h3>使用 Lambda</h3>    <p>传入 Lambda 表达式,有种类似于用匿名方式实现接口:</p>    <pre>  <code class="language-java">publicstaticvoidmain(String[] args){   List<Store> stores = newArrayList<Store>();   stores.add(newStore("店铺一",20,10,"快餐"));   stores.add(newStore("店铺二",30,20,"烧烤"));   stores.add(newStore("店铺三",10,20,"披萨"));   stores.add(newStore("店铺四",15,30,"炸鸡"));   stores.add(newStore("店铺五",45,20,"甜品"));   stores = selectStore(stores,(Store store) -> store.getAvePrice() <= 20.0);  for(Store store : stores){   System.out.println(store.toString());   }  }  </code></pre>    <p>使用了一个 (Store store) -> store.getAvePrice() <= 20.0 Lambda 表达式,可以看到在传入 Lambda表达式的时候,并不能乱写,我们 Lambda 表达式中的参数列表必须与函数式接口的方法的参数列表一致,Lambda 主体中的返回结果必须与函数式接口中的方法的返回类型一致。Java 程序在编译的过程中会检查我们的代码是否满足这些要求,不然编译不会通过。</p>    <p>Java8 提出了一个新的概念: <strong>方法引用</strong> ,这是一种 Lambda 表达式的简略写法。具体可以参照这篇文章,我在这就不啰嗦了。</p>    <h3>使用 Java8 为我们准备的函数式接口</h3>    <p>如果每次都使用 Lambda 表达式,都需要完成上面的这些步骤,会觉得很麻烦,人都是有惰性的。所以 Java 已经为我们准备好了许多内置的函数式接口,当我们需要函数式接口的时候,只需要寻找一下有没有我们需要的,如果找不到再去自己定义。</p>    <p><a href="/misc/goto?guid=4959718268432097471" rel="nofollow,noindex">Java内置函数式接口</a></p>    <h2>使用复合 Lambda 表达式</h2>    <p>上面的过程中,我们每次使用 Lambda 表达式的时候传递进的只是一个逻辑,当需要的逻辑更加复杂,简单的一句话已经不能描述清楚的时候,应该怎么办,多定义几个函数式接口?并不需要,我们可以对 Lambda 表达式进行复合使用,就像 if 语句中出传入的条件一样,可以传入多个。</p>    <p>在介绍如何复合使用 Lambda 表达式之前,我们先来升华一下 <strong>函数式接口</strong> 这个定义,之前的定义是函数式接口中只有一个抽象方法。Java 中引入一个叫 <strong>默认方法</strong> 的机制,让我们可以在接口中实现方法。只需要在方法前加上 default 关键字就能为方法提供实现了。为什么要讲这个呢,因为默认方法为我们复合使用 Lambda 表达式提供了可能。</p>    <p>Java 中内置的函数式接口中的 Comparator 、 Function 、 Predicate 都提供了允许进行表达式复合的方法,这些方法都是默认方法,已经为我们提供了实现。</p>    <p>Function</p>    <p>T -> R</p>    <p>Predicate T -> boolean</p>    <p> </p>    <h3>比较器复合</h3>    <p>我们可以通过使用静态方法 Comparator.comparing ,根据提取用于比较的 Function 来返回一个 Comparator。 Comparator.comparing((Store store) -> store.getAvePrice()) 这样就可以获得一个根据店铺平均价格进行排序的 Comparator。如果我们想逆序排序,Compator 函数式接口中定义了一个 reversed() 函数,这可以返回一个逆序排序的 Comparator,类似于:</p>    <pre>  <code class="language-java">Comparator.comparing((Store store) -> store.getAvePrice()).reversed();  </code></pre>    <p>当两家店铺的价格一样的时候,我们希望根据送餐时间进行排序又该怎么办呢,Comparator 很贴心的又准备了一个 thenComparing() 方法,用法如下:</p>    <pre>  <code class="language-java">Comparator.comparing((Store store) -> store.getAvePrice())  .reversed()  .thenComparing((Store store) -> store.getAveDeliveryTime());  </code></pre>    <h3>谓词复合</h3>    <p>谓词接口 Predicate 提供了三个方法: and 、 or 、 negate ,分别代表与、或、非。</p>    <p>首先,定义一个 Predicate 接口,选择出所有的快餐店:</p>    <pre>  <code class="language-java">Predicate<Store> typeStores = (Store store) -> "快餐".equals(store.getType());  </code></pre>    <p>那要找出所有的非快餐店呢:</p>    <pre>  <code class="language-java">Predicate<Store> notTypeStores = typeStores.negate();  </code></pre>    <p>找出非快餐店中,价格低于20的呢:</p>    <pre>  <code class="language-java">Predicate<Store> priceAndTypeStores = typeStores.negate()  .and((Store store) -> store.getAvePrice() < 20);  </code></pre>    <p>找出非快餐店中,价格低于20或者送餐时间低于30的呢:</p>    <pre>  <code class="language-java">Predicate<Store> priceAndTypeStores1 = typeStores.negate()  .and((Store store) -> store.getAvePrice() < 20)  .or((Store store) -> store.getAveDeliveryTime() < 30);  </code></pre>    <h3>函数复合</h3>    <p>函数复合和数学中的函数复合基本上概念是一致的,提供了 andThen 和 compose 两个方法:</p>    <pre>  <code class="language-java">Function<String,String> f1 = (String s) -> s.trim();  Function<String,String> f2 = (String s) -> s.substring(4);  </code></pre>    <p>f1.andThen(f2) 表示 f2(f1(s)) , f1.compose(f2) 表示 f1(f2(s)) 。</p>    <h2>小结</h2>    <p>啰啰嗦嗦的讲 Lambda 的基本概念讲完了,最后只想表明一个观点:实践出真知。光看懂 Lambda 表达式的概念,不在实际中使用,这一切都是空谈,Lambda 的出现就是为了简化我们的代码,将它用到平时的代码中去才能说是真正掌握了知识。</p>    <p> </p>    <p>来自:https://win-man.github.io/2016/09/24/关于 Lambda 表达式的一些事/</p>    <p> </p>