Java8新特性 - lambda

1. java8 中的三种编程思想

1. 流处理
流是一系列数据项,程序可以从输入流中一个一个读取数据项,可以把stream先看成一种花里胡哨的迭代器

好处:
① 可以在更高层次上写程序,思路变成把这样的流变成那样的流(就像数据库查询语句时的思路),而不是一次只处理一个项目
② 透明地并行处理

2. 行为参数化

3. 并行与共享的可变数据

2. 行为参数化 - 通过参数化传递代码

2.1 行为参数化

  • 一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力
  • 是可以帮助你处理频繁变更的需求的一种软件开发模式
  • 传递代码,就是将新行为作为参数传递给方法,但在Java 8之前这实现起来很啰嗦,现在使用lambda表达式
    在这里插入图片描述
    编写一个prettyPrintApple方法,它接受一个AppleList,并可以对它参数化,以多种方式根据苹果生成一个String输出(有点儿像多个可定制的toString方法)。例如,你可以告诉prettyPrintApple 方法, 只打印每个苹果的重量。此外, 你可以让prettyPrintApple方法分别打印每个苹果,然后说明它是重的还是轻的
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){
    for(Apple apple: inventory){
        String output = formatter.accept(apple);
        System.out.println(output);
    }
}

首先,你需要一种表示接受Apple并返回一个格式String值的方法

public interface AppleFormatter{
    String accept(Apple a);
}

现在你就可以通过实现AppleFormatter方法,来表示多种格式行为了

public class AppleFancyFormatter implements AppleFormatter{
    public String accept(Apple apple){
        String characteristic = apple.getWeight() > 150 ? "heavy" :"light";
        return "A " + characteristic +" " + apple.getColor() +" apple";
    }
}
public class AppleSimpleFormatter implements AppleFormatter{
    public String accept(Apple apple){
        return "An apple of " + apple.getWeight() + "g";
    }
}

最后,你需要告诉prettyPrintApple方法接受AppleFormatter对象,并在内部使用它们。你可以给prettyPrintApple加上一个参数:

public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){
    for(Apple apple: inventory){
        String output = formatter.accept(apple);
        System.out.println(output);
    }
}

现在你就可以给prettyPrintApple方法传递多种行为了。为此,你首先要实例化AppleFormatter的实现,然后把它们作为参数传给prettyPrintApple

prettyPrintApple(inventory, new AppleFancyFormatter());

行为参数化就是策略模式的一种体现,只不过这里是将算法族(行为方法)作为参数直接传递给方法而不是作为环境类的成员变量

  • 既然是策略模式的一种体现那也有策略模式的缺点 - 策略类数量会增多,每个策略都是一个类,复用的可能性很小
  • 可以使用内部类或者lambda表达式来解决这个问题

2.2 解决啰嗦

1. 内部类

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple apple){
        return "red".equals(apple.getColor());
    }
});

缺点:在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为

2. lambda

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

2.3 行为参数化符合DRY

软件设计的三大原则

  1. DRY:don‘t repeat yourself 不要做重复的事情
    dry

  2. KISS:Keep It Simple and Stupid 保持简单直接

  • 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
  • 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
  • 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
  1. YAGNI:You Ain’t Gonna Need It 你不会需要它
    不要去设计当前用不到的功能;不要去编写当前用不到的代码;不要过度设计

3. lambda

3.1 什么是lambda表达式

可以把Lambda表达式理解为简洁地表示可传递的匿名函数

  • 匿名:不像普通方法那样有一个明确的名称
  • 函数:我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方 法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递:Lambda表达式可以作为参数传递给方法或存储在变量中
  • 简洁:无需像匿名类那样写很多模板代码

lambda是为函数式编程服务的
编程语言共性之------什么是函数式编程?
函数式编程是一种编程范式,也就是如何编写程序的方法论,主要思想是把运算过程尽量编写成一系列嵌套的函数调用,FP强调“everything is lambda",并且强调在逻辑处理中不变性的重要性

OOP强调“everything is object”,以及object之间的消息传递。通过消息传递改变每个Object的内部状态,但是很多情况代码的编写实际上是用不到对象的,比如,对一组数据做加工,先查询,然后聚合,聚合后排序,再join,再排序,再聚合,再转换(map)得到最终的结果。这个过程,用FP的函数就很自然

result = func1(func2(func3...funcN(x))))

java为了在原先oop的思想上增加函数式编程的使用,在java8上增加了lambda函数的新特性

除此之外,lambda表达式的引入还使得代码更为简洁,可以避免生成过多的污染环境的无用实现类

lambda表达式的引入可以避免生成过多的污染环境的实现类;
在上面为了解决行为参数化的啰嗦的问题我们引入了lambda表达式,解决了策略模式产生很多实现类

3.2 基本语法

//表达式 能写在等号右边的就是一个表达式 表达式就是Lambda的返回值了
(parameters) -> expression 
//语句 否则是语句
(parameters) -> { statements; }

在这里插入图片描述
在这里插入图片描述

3.3哪里使用 - 函数式接口@FunctionalInterface

函数式接口

  • 函数式接口只有一个抽象方法
  • default方法为默认实现,不计入抽象方法
  • 如果接口声明了一个覆盖java.lang.Object的全局方法之一的抽象方法,那么它不会计入接口的抽象方法数量中,因为接口的任何实现都将具有java.lang.Object或其他地方的实现

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)
在这里插入图片描述
也就是说在java中,所有的Lambda的类型都是一个接口,而Lambda表达式本身,需要是这个接口的实现

java.util.function中的几个新的函数式接口
Predicate
在这里插入图片描述
Consumer
在这里插入图片描述
Function
在这里插入图片描述
原始类型特化
除了上面三个泛型函数式接口:Predicate<T>Consumer<T>Function<T,R>。还有些函数式接口专为某些基本类型类型设计,避免拆箱和装箱
在这里插入图片描述
关于异常
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。

3.4 类型检查,类型推断和限制

类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的
参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型
在这里插入图片描述
关于使用局部变量

  • 实例变量和静态变量无要求
  • 局部变量必须被显示声明为final或事实上是final
    实际和内部类一样,Lambda线程访问的是局部变量的一个副本

3.5 方法引用

所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用,在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系
方法引用
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷 写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那好还是用名称 来调用它,而不是去描述如何调用它
在这里插入图片描述
① 指向静态方法的方法引用(例如IntegerparseInt方法,写作Integer::parseInt

② 指向任意类型实例方法的方法引用( 例如Stringlength 方法, 写作String::length

  • 实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:public static int compareTo(this, String o);

③ 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue

第二种和第三种方法引用可能乍看起来有点儿晕。类似于String::length的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUppeCase()可以写作String::toUpperCase。但第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue()可以写作expensiveTransaction::getValue
在这里插入图片描述
比方说你想要对一个字符串的List排序,忽略大小写。Listsort方法需要一个Comparator作为参数。
Comparator描述了 一个具有(T, T) -> int签名的函数描述符。你可以利用String类中compareToIgnoreCase 方法即直接引用这个方法来定义一个Lambda表达式(注意compareToIgnoreCaseString类中预先定义的)

List<String> str = Arrays.asList("a","b","A","B"); 
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); 
//Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可 以用方法引用改写成下面的样子: 
List<String> str = Arrays.asList("a","b","A","B"); 
str.sort(String::compareToIgnoreCase); 
//编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数 式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配

④ 构造函数引用:对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new

它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。 它适合Supplier的签名() -> Apple。你可以这样做:

Supplier<Apple> c1 = Apple::new; //构造函数引用指向默认的Apple()构造函数 
Apple a1 = c1.get(); //调用Supplier的get方法 将产生一个新的Apple
 
//这就等价于: 
 
Supplier<Apple> c1 = () -> new Apple(); //利用默认构造函数创建 Apple的Lambda表达式 
Apple a1 = c1.get(); //调用Supplier的get方法 将产生一个新的Apple 

//如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签 名,于是你可以这样写: 
 
Function<Integer, Apple> c2 = Apple::new;  //指向Apple(Integer weight) 的构造函数引用 
Apple a2 = c2.apply(110); //调用该Function函数的apply方法,并 给出要求的重量,将产生一个Apple  
 
//这就等价于: 
 
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);  //用要求的重量创建一 个Apple的Lambda表 达式 
Apple a2 = c2.apply(110);  //调用该Function函数的apply方法,并给出要 求的重量,将产生一个新的Apple对象 

在下面的代码中,一个由Integer构成的List中的每个元素都通过 map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new); //将构造函数引用 传递给map方法

public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f){     
	List<Apple> result = new ArrayList<>();     
	for(Integer e: list){         
		result.add(f.apply(e));     
	}     
	return result; 
} 

如果你有一个具有两个参数的构造函数Apple(String color, Integer weight),那么 它就适合BiFunction接口的签名,于是你可以这样写

BiFunction<String, Integer, Apple> c3 = Apple::new;   
Apple c3 = c3.apply("green", 110); 
//这就等价于: 
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);  
Apple c3 = c3.apply("green", 110); 

你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么 样才能对具有三个参数的构造函数,比如Color(int, int, int),使用构造函数引用呢?
构造函数引用的语法是ClassName::new,那么在这个例子里面就是 Color::new。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身并没有提 供这样的函数式接口,你可以自己创建一个:

 public interface TriFunction<T, U, V, R>{     
 	R apply(T t, U u, V v); 
 } 

现在你可以像下面这样使用构造函数引用了:

TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new

不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来 将构造函数映射到字符串值。你可以创建一个giveMeFruit方法,给它一个String和一个 Integer,它就可以创建出不同重量的各种水果:

static Map<String, Function<Integer, Fruit>> map = new HashMap<>(); 
static {     
	map.put("apple", Apple::new);     
	map.put("orange", Orange::new);     
	// etc... 
} 
public static Fruit giveMeFruit(String fruit, Integer weight){    
	return map.get(fruit.toLowerCase())   //你用map得到了一个 Function<Integer, Fruit>             
			  .apply(weight);  
} 

4. 复合Lambda表达式

Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用 于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法,这意味着你可以把多个简单的Lambda复合成复杂的表达式

比如, 你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结 果成为另一个函数的输入

这些方法都是默认方法,所以可以在函数式接口中与抽象方法共存

4.1 比较器复合

Comparator具有一个叫作comparing的静态辅助方法, 它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight()); 

所以对于针对数组的排序我们可以写成

import static java.util.Comparator.comparing; 
inventory.sort(comparing(Apple::getWeight)); 

逆序
如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口 有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改 一下前一个例子就可以对苹果按重量递减排序:

inventory.sort(comparing(Apple::getWeight).reversed());

比较器链
如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能 需要再提供一个Comparator来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可 能想要按原产国排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像 comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个 Comparator。你又可以优雅地解决这个问题了:

inventory.sort(comparing(Apple::getWeight)          
		 .reversed()           
		 .thenComparing(Apple::getCountry));

4.2 谓词覆盖

谓词接口包括三个方法:negateandor,让你可以重用已有的Predicate来创建更复杂的谓词
比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

Predicate<Apple> notRedApple = redApple.negate(); 

你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);  

你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

Predicate<Apple> redAndHeavyAppleOrGreen = 
							redApple.and(a -> a.getWeight() > 150)            
								    .or(a -> "green".equals(a.getColor())); 

4.3 函数复合

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

andThen方法
会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组 合成一个函数h,先给数字加1,再给结果乘2:

Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.andThen(g); //g(f(x))
int result = h.apply(1);  

compose方法
先把给定的函数用作compose的参数里面给的那个函 数,然后再把函数本身用于结果

Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); //f(g(x))  
int result = h.apply(1); 

5. Lambda的注意事项

是某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程

  • 匿名类和Lambda表达式中的thissuper的含义是不同的。在匿名类中,this代表的是类自身,但 是在Lambda中,它代表的是包含类
  • 匿名类可以屏蔽包含类的变量,而Lambda表达式不 能(它们会导致编译错误)
  • 在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦 涩
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值