经典伴读_Java8实战_Lambda

经典伴读系列文章,想写的不是读书笔记,也不是读后感,自己的理解加上实际项目中运用,让大家3-4天读懂这本书

在这里插入图片描述

一、为什么要关心java8

当看到这篇文章或者想要看《java8实战》这本书的时候,说明你可能正在为工作中出现的各种各样“奇怪”的语法结构而感到困惑。不知什么时候起java已经让你有些不认识,或是总听到函数式编程,想弄清楚那到底是什么,没关系,让我们花点时间彻底解决它们。

1、像大多数技术书籍一样,第1章从总体概述开始,介绍java8新特性

  • 方法引用
  • Lambda表达式
  • Stream流
  • default默认方法
  • Optional

这些名字工作中或多或少都听到过,这里先把它们当作新学期的课表,知道我们要学习什么就可以了。

2、对函数的概念要有清晰的认识

编程语言中的函数一词通常是指方法,尤其是静态方法;这是在数学函数,也就是没有副 作用的函数之外的新含义。幸运的是,你将会看到,在Java 8谈到函数时,这两种用法几乎是一 致的。

这里把编程语言中的方法和函数做了明确区分,这里的函数指的是单纯的数学函数,只用于计算或逻辑,没干别的,如:在函数中修改成员变量的值,访问数据库,访问网络等。这些都是所谓的“副作用”。java8希望大家在使用上区分开函数和方法。从面向对象的思维中,开辟出一条新路面向函数。

编程语言的整个目的就在于操作值,要是按照历史 上编程语言的传统,这些值因此被称为一等值

这里定义了可以操作或传递的值就是一等公民,java8和以往最大的不同,就是将方法引用或函数本身作为值,变成一等公民。当然类还是二等。函数式编程的种子从这里开始埋入,像不像小说开头留下的最大伏笔?

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

java8新特性中Lambda表达式最出名,而使用Lambda表达式最大的好处之一,当然就是简化代码。本章旨在激发你的兴趣,直观感受简化代码的过程,因此,不要在意Lambda语法细节,这并不是本章重点。

1、本章标题有些拗口,第一次看我也不明白,其实“行为”就是算法。换句话说就是将算法当做参数传递。那么封装算法成了必要条件,自然联想到设计模式中的策略模式。现在假设我们要选苹果,有时选大的,有时选红的,有时要选又大又红的。每一种选择都是一个算法。

	/*
	 * 一个返回boolean值的函数称为谓词
	 */
	interface Predicate<T> {
		boolean test(T t);
	}
	
	/*
	 * 选大苹果
	 */
	static class AppleWeightPredicate implements Predicate<Apple> {
		@Override
		public boolean test(Apple t) {
			return t.getWeight() > 100;
		}
	}

	/*
	 * 选红苹果
	 */
	static class AppleColorPredicate implements Predicate<Apple> {
		@Override
		public boolean test(Apple t) {
			return t.getColor().equals("red");
		}
	}

	/*
	 * 按照要求过滤苹果
	 */
	static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> p) {
		List<Apple> fas = new ArrayList<>();
		for (Apple apple : apples) {
			if (p.test(apple)) {
				fas.add(apple);
			}
		}
		return fas;
	}

	public static void main(String[] args) {
		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
		
		//我要大苹果
		List<Apple> fas = filterApples(apples, new AppleWeightPredicate());
		System.out.println("大苹果:" + fas);
		
		//我要红苹果
		fas = filterApples(apples, new AppleWeightPredicate());
		System.out.println("红苹果:" + fas);
	}

大苹果:[ [weight=120, color=green], Apple [weight=140, color=red]]
红苹果:[ [weight=80, color=red], Apple [weight=100, color=red], Apple [weight=140, color=red]]

如果这时要又大又红的苹果,是不是得再写一个算法类AppleWeightAndColorPredicate。

2、上面的策略模式其实已省略上下文类Context,现改用Lambda表达式重构,感受下极致的简化(Lambda语法细节先不考虑),这次连算法类AppleWeightPredicate,AppleColorPredicate都直接省略掉。

	public static void main(String[] args) {
		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
		
		//要大苹果
		List<Apple> fas = filterApples(apples, a -> a.getWeight() > 100);
		System.out.println("大苹果:" + fas);
		
		//要红苹果
		fas = filterApples(apples, a -> a.getColor().equals("red"));
		System.out.println("红苹果:" + fas);
		
		//还要又大又红的苹果
		fas = filterApples(apples, a -> a.getWeight() > 100 && a.getColor().equals("red"));
		System.out.println("又大又红:" + fas);
	}

大苹果:[[weight=120, color=green], [weight=140, color=red]]
红苹果:[[weight=80, color=red], [weight=100, color=red], [weight=140, color=red]]
又大又红:[[weight=140, color=red]]

3、上面的例子体现了算法不仅可以封装,还可以当做方法参数传递,Lambda表达式在平常项目开发时,已经有很多“真实例子”,如:Runnable 和 Comparator接口 分别对应"开线程 "和"比较"两种行为。

		new Thread(() -> {
			apples.sort((a1, a2) -> a2.getWeight().compareTo(a1.getWeight()));//按照苹果重量倒序
			System.out.println("倒序:" + apples);
		}).run();

三、Lambda表达式

第一部分的重点,从语法到应用具体分析Lambda表达式。请沉下心理解,上主食。
1、初识
用个大家熟知的概念类比,先可以把Lambda表达式看成是java中匿名内部类的简化。

		//使用匿名内部类
		List<Apple> fas = filterApples(apples, new Predicate<Apple>() {
			@Override
			public boolean test(Apple a) {
				return a.getWeight() > 100; //选大苹果
			}
		});
		
		//使用对等的Lambda表达式
		fas = filterApples(apples, a -> a.getWeight() > 100);
		System.out.println("大苹果:" + fas);

2、Lambda表达式格式
(1)(参数列表) -> 表达式,这里默认返回表达式的值,如:

		(Apple a) -> a.getWeight() > 100

(2)(参数) -> {语句},如:

		(Apple a) -> {
			boolean b = a.getWeight() > 100;
			return b;
		}

2、函数式接口
java8想要引入的函数式编程,把函数当作值一样传递,也就是所谓的一等公民,那么整形的类型是int,字符串的类型是String,函数的类型呢?如何申明函数变量,函数自己被当做参数调用另一个方法时,形参类型又是什么?
如上例中的Predicate,这是一种特殊的接口,只有一个抽象方法,称为函数式接口。可以添加@FunctionalInterface注解,它不仅是一种标志,还能在编译时限制函数式接口不能出现多个抽象方法。(可以有多个default方法,后面再说)

	/*
	 * 一个返回boolean值的函数称为谓词
	 */
	@FunctionalInterface
	interface Predicate<T> {
		boolean test(T t);
	}

有了函数类型,函数就能和普通变量一样,声明定义,

		Predicate<Apple> p1 = a -> a.getWeight() > 100;
		Predicate<Apple> p2 = (Apple a) -> {
			boolean b = a.getWeight() > 100;
			return b;
		};

好了,现在回头思考下Lambda是什么,Lambda并不能完全认为是匿名内部类的语法糖,除了明显简化了代码之外,他们的产生的初衷不同。在一切皆是对象的美好愿望下,匿名内部类,当只需要创建一个对象时使用,本质传递的是对象(有多个方法),是面向对象编程的产物。而Lambda表达式,却是java想要引入函数式编程的基石,虽然表面上也是一个接口类,本质却是函数,Java8的设计师们想让我们把它只当做函数,请忽略它的接口类名吧,奈何受限于JDK的兼容性,不允许函数脱离类单独存在。因此我更想称它为匿名函数,java的闭包。

主角的背景终于被揭开,像不像皇帝流落民间的小儿子。

3、函数描述符
每一个方法都有方法签名,让编译器检查传入的参数类型和返回值类型是否正确。那么对于Lambda表达式或者说匿名函数,编译器如何做类型检查。答案是函数描述符,即函数式接口的抽象方法签名就是Lambda表达式的签名。如:
public boolean test(Apple a) 函数描述符是 (Apple a) -> boolean

由此有了一个想法,以前的java方法主要通过方法名,方法参数区分,但匿名函数的函数描述符中只有参数类型和返回值类型,没有方法的名字。那么是否可以创建一套公用的函数式接口,如:

 /*
	 * 无参数
	 */
	@FunctionalInterface
	interface Function0 {
		void apply();
	}

	/*
	 * 1个参数
	 */
	@FunctionalInterface
	interface Function1<T, R> {
		R apply(T t);
	}

	/*
	 * 2个参数
	 */
	@FunctionalInterface
	interface Function2<T, U, R> {
		R apply(T t, U u);
	}

这样一来不必每次使用Lambda之前还得写个函数式接口,像Predicate这一类的,都用同一套公用接口就好。现在让我们写一个测试方法耗时的工具,如:

/*
	 * 检测方法耗时(无参数的函数式接口)
	 */
	public static void testMethodTime(Function0 func) {
		System.out.println("开始执行...");
		long begin = System.currentTimeMillis();
		func.apply();
		long end = System.currentTimeMillis();
		System.out.println("执行结束,耗时:" + (end - begin) + "ms");
	}

	/*
	 * 按照要求过滤苹果(1个参数的函数式接口)
	 */
	static List<Apple> filterApples(List<Apple> apples, Function1<Apple, Boolean> f) {
		List<Apple> fas = new ArrayList<>();
		for (Apple apple : apples) {
			if (f.apply(apple)) {
				fas.add(apple);
			}
		}
		return fas;
	}

	public static void main(String[] args) {
		testMethodTime(() -> {
			List<Apple> apples = new ArrayList<>();
			apples.add(new Apple(80, "red"));
			apples.add(new Apple(100, "red"));
			apples.add(new Apple(120, "green"));
			apples.add(new Apple(140, "red"));
			
			// 我要大苹果
			List<Apple> fas = filterApples(apples, a -> a.getWeight() > 100);
			System.out.println("大苹果:" + fas);
		});
	}

开始执行…
大苹果:[[weight=120, color=green], [weight=140, color=red]]
执行结束,耗时:1ms

4、使用通用的函数式接口
正当我们高兴之余,发现JDK中有同样一个Function类,深入一看,发现我们重复造了轮子。Java8的设计师们早已内置了通用的函数式接口,全部都在java.util.function包中。可以按照参数个数分类。
(1)单参或无参的函数式接口

  • 用于判断
 @FunctionalInterface
public interface Predicate<T> {
	boolean test(T t); //函数描述符:(T t) -> boolean,
  • 用于消费
@FunctionalInterface
public interface Consumer<T> {
	void accept(T t); //函数描述符: (T t) -> void,
  • 用于加工
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
  • 用于获取
@FunctionalInterface
public interface Supplier<T> {
    T get(); //函数描述符: () -> T

(2)两个参数的函数式接口(单参接口名加Bi前缀,不用记)

@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u); //函数描述符: (T t, U u) -> boolean
    
@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u); //函数描述符:(T t, U u) -> void

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u); //函数描述符:(T t, U u) -> R

那么有三个参数的函数式接口,是否要加Tri前缀?,这个JDK8到没有,我们可以自己扩展。

3)比较器接口

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2); //函数描述符:(T o1, T o2) -> int

另外,UnaryOperator 和 BinaryOperator 用于一元 和 二元的计算,完全可以被Function替代。因此只用记住常用的5个,基本不用再写函数式接口。

5、Lambda表达式tips
(1)Lambda和泛型一样都具有类型推断的特点,如:

(Apple a1, Apple a2) -> a2.getWeight().compareTo(a1.getWeight())

省略参数类型后变成:

(a1, a2) -> a2.getWeight().compareTo(a1.getWeight())

这里的参数类型,从函数式接口的抽象方法推断而来。

(2)Lambda表达式中无法修改局部变量
这就和匿名内部类中只能使用final类型局部变量一样,Lambda表达式实际访问的是局部变量的副本。

	private int x = 0;
	public void add() {
		int y = 0;
		new Thread(() -> {
			x++; //实例变量可以修改+1
			y++; //报错,局部变量默认final
		}).run();
	}

报错原因,书中有一段表达的非常清楚,不再追叙。

你可能会问自己,为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局 部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线 程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它 的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了 这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中 解释,这种模式会阻碍很容易做到的并行处理)。

方法引用

既然匿名函数都可以传递,那么有名字的方法能够传递么?当然,只要满足函数描述符。
1、静态方法引用
格式:类名::静态方法名,下面将引用LambdaTest类的testWeight静态方法,过滤出大苹果。

	/*
	 * java.util.function.Predicate<Apple>的函数描述符是(Apple a) -> boolean
	 */
	public static List<Apple> filterApples(List<Apple> apples, Predicate<Apple> p) {
		List<Apple> result = new ArrayList<Apple>();
		for(Apple apple : apples) {
			if (p.test(apple)) {
				result.add(apple);
			}
		}
		return result;
	}
	/*
	 * 函数描述符:(Apple a) -> boolean
	 */
	public static boolean testWeight(Apple a) {
		return a.getWeight() > 100;
	}
	
	public static void main(String[] args) {
		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
		//满足函数描述符,可以引用的静态方法testWeight
		List<Apple> fas = filterApples(apples, LambdaTest::testWeight); 
		System.out.println("大苹果:" + fas);
	}

2、实例方法引用
格式:实例对象::实例方法名,下面将先创建出LambdaTest类的实例,再引用实例方法,过滤出大苹果。

/*
	 * 函数描述符:(Apple a) -> boolean
	 */
	public boolean testWeightNonStatic(Apple a) {
		return a.getWeight() > 100;
	}
	
	public static void main(String[] args) {
		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
		
		LambdaTest lt = new LambdaTest(); 
        //满足函数描述符,可以引用实例方法testWeightNonStatic
		List<Apple> fas = filterApples(apples, lt::testWeightNonStatic); 
		System.out.println("大苹果:" + fas);
	}

3、内部实例方法引用
格式:类名::实例方法名,需要引用的实例方法,是调用对象内部的方法。这种方式并不容易理解,
我们之前一直通过处理Apple列表,过滤出想要的苹果,而判断苹果的方法都写在LambdaTest类中,无论是静态方法还是实例方法,现在我们不再依赖LambdaTest类,直接将判断的方法写在要处理对象,也就是Apple内部。如:

 	public static class Apple {
		private Integer weight;
		private String color;
		public Apple(Integer weight, String color) {
			this.weight = weight;
			this.color = color;
		}
		/*
		 * 函数描述符:(Apple a) -> boolean
		 */
		public boolean testWeightSelf() {
			return weight > 100; //this.weight
		}
      ...

现在我们要引用Apple类的实例方法testWeightSelf,就得先知道函数描述符,testWeightSelf没有参数,为什么函数描述符是(Apple a) -> boolean?这是因为java方法默认带有一个this指针,这里可以理解为Apple的方法默认都带有Apple a的参数。

	public static void main(String[] args) {
		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
		
            //满足
		List<Apple> fas = filterApples(apples, Apple::testWeightSelf); 
		System.out.println("大苹果:" + fas);
	}

再看个例子,按照苹果重量排序

apples.sort(Comparator.comparing(Apple::getWeight)); //按重量排序

Comparator.comparing的方法签名:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)

看起来好复杂,没错,泛型多了有时会有些晕,参数Function<? super T, ? extends U> keyExtractor,让我们去掉super 和 extends,发现剩下的就是一个参数一个返回值的函数式接口。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t); //函数描述符: (T t) -> R

再看Apple::getWeight的函数描述符是(Apple a)->int ,函数描述符相同,可以传递方法引用。

注意,如果需要引用的内部方法,带有参数(除了默认的this外),如Apple的setWeight方法。

public void setWeight(Integer weight)

这时就需要用多参数的函数式接口类传递方法,如:

BiConsumer<Apple, Integer> bc = Apple::setWeight;

带有参数的实力方法,不建议再使用这种方式传递,会让代码难以理解,更难以维护。

4、构造方法引用
格式:类名::new,之前不知在哪本书中读到构造函数是一种静态方法,也有方法签名,那么自然也能够被引用。给Apple类添加两个子类GoodApple,BadApple,重写toString方法,下面代码不知还算不算是工厂?

	public static class AppleFactory {
		public static Apple create(Supplier<Apple> s) {
			Apple apple = s.get();
			apple.setWeight(100);
			apple.setColor("green");
			return apple;
		}
	}
	 
	public static void main(String[] args) {
		Apple apple = AppleFactory.create(GoodApple::new);
		System.out.println(apple);
		
		apple = AppleFactory.create(BadApple::new);
		System.out.println(apple);
	}

GoodApple[weight=100, color=green]
BadApple[weight=100, color=green]

上面的例子调用的是无参构造函数,接着我们看下多参构造函数如何引用?

  		//无参构造函数,函数描述符:() -> Apple
		Supplier<Apple> s = Apple::new; 
		Apple apple = s.get();
		
		//1参构造函数,函数描述符:(Integer) -> Apple
		Function<Integer, Apple> f = Apple::new; 
		apple = f.apply(100);
		
		//2参构造函数,函数描述符:(Integer, String) -> Apple
		BiFunction<Integer, String, Apple> bf = Apple::new; 
		apple = bf.apply(100, "green");

Lambda表达式复合

1、函数复合
函数既然已经可以作为值一样传递,那么能否将多个函数动态编排成一个新的函数。就像数学中的复合函数一样,如在数学中:
g(x) = x²
f(x) = x+1
复合函数f(g(x)) = x² + 1,当x=2,函数值多少?口答是5,让我们看下对应代码:

 		Function<Integer, Integer> g = (x) -> x * x; //g(x)
		Function<Integer, Integer> f = (x) -> x + 1; //f(x)
		Function<Integer, Integer> f_g = g.andThen(f); //f(g(x))
		Integer y = f_g.apply(2); 
		System.out.println(y);

这里g.andThen(f)还可以写成f.compose(g),只是谁复合了谁的问题,用一个就可以了。
另外,还可以看出函数式接口想要复合,主要依靠的是接口内的default方法,如andThen,compose,它们都是Function中的默认方法。(后面再说)

2、谓词复合
函数都能复合,那能否把谓词(返回boolean值的函数)也复合下,又大又红的苹果它又来了。

 		List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red"));
		apples.add(new Apple(100, "red"));
		apples.add(new Apple(120, "green"));
		apples.add(new Apple(140, "red"));
            
        Predicate<Apple> weightPre = (a) -> a.getWeight() > 100; //大苹果
		Predicate<Apple> colorPre = (a) -> a.getColor().equals("red"); //红苹果
		Predicate<Apple> weightAndColorPre = weightPre.and(colorPre); //又大又红的苹果
		
		List<Apple> fas = filterApples(apples, weightAndColorPre);
		System.out.println("又大又红的苹果:" + fas);

weightPre.and(colorPre),既然有and,当然还有or或,negate非。

3、比较器复合
上文已经提到比较器Comparator也是一种函数式接口,同样也能够复合。我们给Apple类增加甜度sweetness属性。

		public Apple(Integer weight, String color, Integer sweetness) {
			this.weight = weight;
			this.color = color;
			this.sweetness = sweetness;
		}

按照重量倒序排列,如果同样重,甜度优先。

        List<Apple> apples = new ArrayList<>();
		apples.add(new Apple(80, "red", 90));
		apples.add(new Apple(100, "green", 50));
		apples.add(new Apple(100, "red", 80));
		apples.add(new Apple(120, "green", 40));
		apples.add(new Apple(100, "red", 70));
		
		Comparator<Apple> c1 = (o1, o2) -> o2.getWeight().compareTo(o1.getWeight()); //按照重量逆序
		Comparator<Apple> c2 = (o1, o2) -> o2.getSweetness().compareTo(o1.getSweetness()); // 按照甜度逆序
		Comparator<Apple> c3 = c1.thenComparing(c2); //先按照重量逆序,再按照甜度逆序
		apples.sort(c3);
		System.out.println(apples);

[[weight=120, sweetness=40, color=green], [weight=100, sweetness=80, color=red], [weight=100, sweetness=70, color=red], [weight=100, sweetness=50, color=green], [weight=80, sweetness=90, color=red]]

让我们换一种更贴合自然语言的写法

		Comparator<Apple> c1 = Comparator.comparing(Apple::getWeight).reversed(); //按照重量逆序
		Comparator<Apple> c2 = Comparator.comparing(Apple::getSweetness).reversed(); // 按照甜度逆序
		Comparator<Apple> c3 = c1.thenComparing(c2); //先按照重量逆序,再按照甜度逆序
		apples.sort(c3);

至此,Lambda表达式的内容全部学完。包含了《java8实战》这本书主要内容的四分之一,你花了多长时间呢?下一篇,将会看到Lambda真正的用武之地。《经典伴读_java8实战_Stream基础》

未完待续

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值