Java8新特性(一) Lambda表达式、函数式接口与方法引用

导航

引例

Lambda表达式

格式

格式简化

函数式接口

@FunctionalInterface

四大核心函数式接口

Predicate

Consumer

Supplier

Function

改进

Lambda表达式与变量捕获

方法引用

格式

三类方法引用

静态方法引用

实例方法引用

构造方法引用


引例

有这样一位农场主,他经营着一片苹果园。某天这位农场主突发奇想,他想找出果园里所有的绿苹果。这种简单的要求,我们可以很轻松的帮他实现:

public class Apple {
	 
    private String color;   // 颜色
    private int weight;     // 重量
 
    public Apple(String color, int weight) {
        this.color = color;
        this.weight = weight;
    }

    public String getColor() {
    	return color;
    }
    public void setColor(String color) {
    	this.color = color;
    }
    public int getWeight() {
    	return weight;
    }
    public void setWeight(int weight) {
    	this.weight = weight;
    }
    
    @Override
    public String toString() {
    	return "Apple [color=" + color + ", weight=" + weight + "]";
    }
    
}
public class FindApple1 {
	// 果园
	public static List orchard = Arrays.asList(new Apple("green", 150), 
			new Apple("green", 200), new Apple("yellow", 150),new Apple("red", 170));
	
	public static void main(String[] args) {
		List basket = new ArrayList<>();
		// 找到所有的绿苹果
		for (Apple apple : orchard) {
			if("green".equals(apple.getColor())) {
				basket.add(apple);
			}
		}	
		System.out.println(basket);
	}
	
}

然而这位农场主是一个善变的人,突然他改变了主意——找出果园里所有的红苹果而不是绿苹果。为了应对需求的变更,同时考虑到这位善变的农场主以后可能会想要找其他颜色的苹果,我们对程序做出相应的修改:

public class FindApple2 {
	
	public static void main(String[] args) {
		List basket = appleFilter(FindApple1.orchard, "red");
		System.out.println(basket);
	}
	
	// 找到指定颜色的苹果
	private static List appleFilter(List orchard, String color) {
		List temp = new ArrayList<>();
		for (Apple apple : orchard) {
			if(color.equals(apple.getColor())) {
				temp.add(apple);
			}
		}
		return temp;
	}
	
}

使用修改之后的程序,即使农场主再次改变主意——找出果园里所有的黄苹果,仍然可以应对需求的变更。然而农场主的确又改变主意了,只不过这一次他要找的并不是黄苹果,而是重量大于150g的苹果。这样一来,我们修改过的程序又不适用于新的需求了。为了一劳永逸,我们把程序调整成这样:

public interface AppleCheck {
	
	boolean test(Apple apple);
	
}
public class FindApple3 {
	
	public static void main(String[] args) {
		List basket = appleFilter(FindApple1.orchard, new AppleCheck() {
			@Override
			public boolean test(Apple apple) {
				return apple.getWeight() > 150;
			}	
		});
		System.out.println(basket);
	}
	
	// 根据指定条件找苹果(条件可以随意变更)
	private static List appleFilter(List orchard, AppleCheck appleCheck) {
		List temp = new ArrayList<>();
		for (Apple apple : orchard) {
			if(appleCheck.test(apple)) {
				temp.add(apple);
			}
		}
		return temp;
	}
}

使用最终版的程序,不管农场主再冒出什么新的想法,我们只需要修改找苹果的方式(AppleCheck的具体实现)就可以应对需求变更。

 

Lambda表达式

引例的最后,我们通过引入接口成功的解决了农场主不断变化需求的问题。但是使用匿名内部类实现接口的做法让我们的代码看起来很笨重,不够简洁。那么还有什么更好的做法吗?答案是肯定的,下面使用Lambda表达式来修改程序:

public class FindApple4 {
	
	public static void main(String[] args) {
		// 使用Lambda表达式代替匿名内部类
		List basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
		System.out.println(basket);
	}

	private static List appleFilter(List orchard, AppleCheck appleCheck) {
		List temp = new ArrayList<>();
		for (Apple apple : orchard) {
			if(appleCheck.test(apple)) {
				temp.add(apple);
			}
		}
		return temp;
	}

}

使用Lambda表达式之后的代码看上去是不是很优雅,很简洁?我们只用一行代码就完成了匿名内部类的工作。 那么Lambda表达式应该如何使用呢?先不要着急,我们先了解一下Lambda表达式的格式。

格式

在FindApple4中出现的Lambda表达式是这样的:

apple -> apple.getWeight() > 150                                   //  简化版Lambda表达式

上面的Lambda表达式是简化之后的样子,完整版是这样的:

(Apple apple) -> { return apple.getWeight() > 150;}        // 完整版Lambda表达式

一个完整的Lambda表达式的格式如下所示:

(参数列表) -> { 方法实现 }

然而通常情况下我们不会书写完整的Lambda表达式,而是会进行适当的简化。

格式简化

Lambda表达式可以根据传入参数自动推导该参数的类型(类型推导),所以参数列表中的的参数类型可以省略。参数列表简化规则如下:

  1. 参数类型可以省略
  2. 若参数列表只存在一个参数,参数列表的括号可以和参数类型一起省略
  3. 若参数列表为空,则参数列表的括号不可以省略

下面通过几个例子演示上述规则:

(a, b) -> { System.out.println(a+b); }         // 正确
a, b -> { System.out.println(a+b); }           // 错误:不可以存在两个参数并省略括号
    
a -> { System.out.println(a); }                   // 正确
String a -> { System.out.println(a); }        //  错误:不可以存在参数类型并省略括号

() -> { System.out.println("no paramter"); }    // 正确
   -> { System.out.println("no paramter"); }    //  错误:参数列表为空,不可以省略括号

方法实现简化规则如下:

  1. 方法实现有多条语句,方法体必须使用花括号
  2. 方法实现只有一条语句,方法体可以省略花括号(若省略花括号,语句末尾的分号也要一起省略)
  3. 方法实现只有一条语句且这条语句包含return,则在规则2的基础上还需要省略return

规则演示:

(a, b) -> { System.out.println(a);  System.out.println(b);}      // 正确
(a, b) -> System.out.println(a);   System.out.println(b);        // 错误:方法实现有多条语句要用花括号包起来

a -> System.out.println(a)        // 正确
a -> System.out.println(a);       // 错误:方法实现只有一条语句,省略花括号时分号必须一起省略

a -> {return a;}     // 正确
a -> a                   // 正确
a -> return a        // 错误:方法实现只有一条包含return的语句,省略花括号时return必须一起省略

Tip:上面罗列的规则可能没有考虑到所有的情况,总之大家多尝试一下,不正确的格式是无法通过编译的。

 

函数式接口

当然,Lambda表达式是有使用条件的,能够使用Lambda表达式的前提是函数式接口——只声明一个抽象方法的接口。再去看我们创建的AppleCheck接口,你就会发现它是一个函数式接口。

我们可以将Lambda表达式作为函数式接口的一个具体实现,例如这样:

AppleCheck appleCheck = apple -> "green".equals(apple.getColor());

@FunctionalInterface

我们可以使用Lambda表达式表示函数式接口的一个具体实现,但是在实际开发中,别人可能并不知道某个接口是一个函数式接口,并向其中添加了新的抽象的方法,那么你之前的使用Lambda表达式作为该接口的具体实现的代码就会报错。因此为了表示某个接口是一个函数式接口,我们可以使用@FunctionalInterface注解该接口。

使用@FunctionalInterface注解的接口只能声明一个抽象方法。若接口声明两个抽象方法则无法通过编译。

使用@FunctionalInterface注解的接口一定是函数式接口,不使用@FunctionalInterface注解的接口也可以是函数式接口(只要能保证该接口中只存在一个抽象方法)

下面将AppleCheck修改为函数式接口:

@FunctionalInterface			    // 注解为函数式接口
public interface AppleCheck {
	
	boolean test(Apple apple);
	
//	Apple getApple();			// 函数式接口中只允许存在一个抽象方法
	
	boolean equals(Object obj);		// Object类中public抽象方法
	
	default void getAppleByDefault() {			// default方法
		System.out.println("getAppleByDefault");
	}
	
	static void getAppleByStatic() {			// 静态方法
		System.out.println("getAppleByStatic");
	}
	
}

看到上面的代码你可能会觉得很奇怪,不是说函数式接口只能声明一个抽象方法吗?怎么我们定义的函数式接口存在这么多方法?确实函数式接口只允许声明一个抽象方法,但是除抽象方法之外函数式接口中还可以声明以下两种方法

  1. default方法和静态方法(具体可以参考这篇文章:Java8新特性(五) default)
  2. Object类中public抽象方法

关于上述第二点,在FunctionalInterface的JavaDoc中有如下描述:

If an interface declares an abstract method overriding one of the public methods of {@code java.lang.Object}, that also does not count toward the interface's abstract method count.

即接口声明的抽象方法重写了Object类中public抽象方法,该抽象方法不计入抽象方法总数。

JDK8中可以定义为函数式接口的接口都加上了@FunctionalInterface注解,如Comparator接口:

@FunctionalInterface
public interface Comparator {
    int compare(T o1, T o2);
    ...
}

四大核心函数式接口

如果仅仅为了使用Lambda表达式而特意定义一个函数式接口,未免得不偿失。其实JDK8已经为我们预先定义了大量的函数式接口,下面是四大核心函数式接口:

Predicate

Predicate(断言)接口声明抽象方法test(),该方法接收一个泛型对象并返回一个布尔值。看到这个接口你可能会觉得似曾相识,没错,Predicate接口就是我们定义的AppleCheck接口的泛型版本。

@FunctionalInterface
public interface Predicate {
    boolean test(T t);    
    default Predicate or(Predicate other) { ... }
    default Predicate and(Predicate other) { ... }
    default Predicate negate() { ... }
    ...
}                 

除了抽象方法test()之外,Predicate还提供三个默认方法:and(),or()和negate()。这三个方法的返回值都是Predicate类型,通过这三个方法可以构建更为复杂的Predicate。看下面一个例子:

public class TestPredicate {
	
	public static void main(String[] args) {
		// 条件1
		Predicate redApple = apple -> "red".equals(apple.getColor());
		// 条件2
		Predicate heavyApple = apple -> apple.getWeight() > 150;
		// 条件3
		Predicate greenApple = apple -> "green".equals(apple.getColor());
		// 条件组合
		Predicate complexPredicate = redApple.and(heavyApple).or(greenApple);
		for (Apple apple : FindApple1.orchard) {
			if(complexPredicate.test(apple)) System.out.println(apple);;
		}
	}
	
}

这三个方法的作用相当于逻辑运算符&&、||和!,complexPredicate的判断逻辑相当于这样:

(redApple && heavyApple) || greenApple

除了Predicate之外JDK8还提供特殊版本的Predicate接口:IntPredicate、LongPredicate、DoublePredicate等。

Consumer

Consumer(消费者)接口声明抽象方法accept(),该方法接收一个泛型对象,没有返回值。

@FunctionalInterface
public interface Consumer {
    void accept(T t);
    default Consumer andThen(Consumer after) { ... }
}

Consumer提供一个默认方法:andThen(),该方法的返回值为Consumer类型,可以组合多个Consumer,串联调用。

public class TestConsumer {
	
	public static void main(String[] args) {
		// 操作1
		Consumer applePrinter1 = 
				apple ->  System.out.print("apple color: " + apple.getColor());
		// 操作2
		Consumer applePrinter2 = 
				apple ->  System.out.println(", apple weight: " + apple.getWeight());
		// 组合操作
		Consumer applePrinter = applePrinter1.andThen(applePrinter2);
		for (Apple apple : FindApple1.orchard) {
			applePrinter.accept(apple);
		}
	}
	
}

同Predicate一样,除了Consumer之外JDK8还提供IntConsumer、LongConsumer、DoubleConsumer、BiConsumer等接口。

Supplier

Supplier(供应商)接口声明抽象方法get(),该方法返回一个泛型对象。

@FunctionalInterface
public interface Supplier {
    T get();
}

public class TestSupplier {
	
	public static void main(String[] args) {
		// 提供200以下的随机数
		Supplier weight = () -> new Random().nextInt(200);
		// 提供重量在200一下的红苹果
		Supplier appleCreator = () ->  new Apple("red", weight.get());
		System.out.println(appleCreator.get());
	}
	
}

除了Supplier之外JDK8还提供IntSupplier、LongSupplier、DoubleSupplier、BooleanSupplier等接口。

Function

 Function(函数)接口声明抽象方法apply(),该方法接收一个泛型对象并返回一个泛型对象。

@FunctionalInterface
public interface Function {
    R apply(T t);
    default Function compose(Function before) { ... }
    default Function andThen(Function after) { ... }
    ...
}

正如其名,调用Function接口类似数学中的函数:r=f(t),给出入参t,经过f()运算之后,得到出参r。

Function接口提供两个默认方法:compose()和andThen(),它们的返回值都是Function类型。通过这两个方法可以将多个Function进行组合调用。

public class TestFunction {
	
	public static void main(String[] args) {
		// 函数f()
		Function f = x -> x + 1;
		// 函数g()
		Function g = x -> x * 2;
		// 先进行函数g()运算,再开始函数f()运算
		Function compose1 = f.compose(g);
		// 运算顺序和compose1相反
		Function compose2 = f.andThen(g);
		System.out.println(compose1.apply(3));    // f(g(3)) : 7
		System.out.println(compose2.apply(3));    // g(f(3)) : 8	
	}
	
}

当然,JDK8也提供了IntFunction、LongToDoubleFunction、BiFunction等接口。

改进

下面使用JDK8提供的函数式接口来修改FindApple4,这里提供两种思路:

public class FindApple5 {
	
	public static void main(String[] args) {
		List basket = appleFilter(FindApple1.orchard, apple -> apple.getWeight() > 150);
		System.out.println(basket);
	}
	
	private static List appleFilter(List orchard, Predicate predicate) {
		List temp = new ArrayList<>();
		for (Apple apple : orchard) {
			if(predicate.test(apple)) {
				temp.add(apple);
			}
		}
		return temp;
	}
	
}
public class FindApple6 {
	
	public static void main(String[] args) {
		List basket = new ArrayList<>();
		appleFilter(FindApple1.orchard, basket, (result, apple) -> {
			if(apple.getWeight() > 150)	
				result.add(apple);
		});
		System.out.println(basket);
	}
	
	private static void appleFilter(List orchard, List basket, 
			BiConsumer, Apple> biConsumer) {
		for (Apple apple : orchard) {
			biConsumer.accept(basket ,apple);
		}
	}
	
}

 

Lambda表达式与变量捕获

其实说到这儿,好像也没明说Lambda表达式是个啥。不过你可能已经发现了,Lambda表达式本质上就是特定匿名内部类的简写形式。所以既然如此,匿名内部类存在的问题——匿名内部类中访问的局部变量需要修饰为final类型,Lambda表达式也一并继承了下来。

Java8之前,如果在匿名内部类中访问局部变量,需要显式的将此变量声明为final类型,Java8中则会隐式的将匿名内部类中访问的局部声明为final类型:在Lambda表达式中访问局部变量的操作,称之为变量捕获一旦局部变量被Lambda表达式捕获,那么该变量会被隐式声明成final类型。见下面一个例子:

public class TestLocalVariable1 {
	
	public static void main(String[] args) {
		int num = 3;		
		IntConsumer consumer = (n) -> {
			System.out.println(num + n);  // 此时num已经被声明为 final int num = 3
		};
		consumer.accept(2);
	}
	
}

被Lambda表达式捕获的变量,我们不能修改它的值——不论是Lambda表达式内还是外。

对于前者,由于被捕获的变量已经隐式声明为final类型,所以我们不能再去修改它的值,故而无法通过编译;对于后者,由于在Lambda表达式外该变量的值发生了变化,所以这个变量无法被隐式声明为final类型,因此报错。见下面一个例子:

public class TestLocalVariable2 {
	
	public static void main(String[] args) {
		int num = 3;		
		IntConsumer consumer = (n) -> {
//			num++;   // 无法修改final类型变量的值
			System.out.println(num + n);
		};
//		num++;		// 变量的值发生变化,无法被隐式声明为final类型
		consumer.accept(2);
	}
	
}

不过对于引用类型变量而言,我们可以Lambda表达式中修改该引用指向的对象。因为引用类型变量中存放的是地址值,用final修饰引用类型变量表示的含义是该引用不可以指向其他的对象。

public class TestLocalVariable3 {

	public static void main(String[] args) {   
		Apple apple = new Apple("red",150);
		Consumer consumer = color -> {
//			apple = new Apple("green", 170);		// 错误:不可修改引用指向的对象
			apple.setColor(color);
		};
		consumer.accept("green");
		System.out.println(apple);
	}
	
}

 

方法引用

当我们使用Lambda表达式去实现某个功能时,若恰巧存在某个方法可以实现这个功能,就可以用方法引用来表示这个方法。

从上面的定义不难看出来,方法引用的本质是特定Lambda表达式的表现形式。是一种语法糖。方法引用并未定义新的功能,只是Lambda表达式的一种更简洁的表达,具有更强的可读性。

从名字上来看,方法引用属于引用的一种。而我们知道引用类型数据代表的是对实际值的引用,其本身并不存放任何实际值,方法引用也是如此。方法引用表示对某个方法的引用,其本身并不具有该方法的功能实现

格式

方法引用的格式如下: 

类名(对象名):: 方法名

::是域操作符,表示对方法的引用。方法名后面不需要括号。下面通过一个例子来演示方法引用:

public class Student {
	
	private String name;
	private int score;
	
	public Student(String name, int score) {
		super();
		this.name = name;
		this.score = score;
	}	

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getScore() {
		return score;
	}
	public void setScore(int score) {
		this.score = score;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", score=" + score + "]";
	}

	public static int compareByScore(Student s1, Student s2) {
		return s1.getScore() - s2.getScore();
	}
	
}
public class TestMethodReference1 {

	public static List students = Arrays.asList(new Student("zhangsan", 64), 
			new Student("lisi", 85), new Student("wangwu", 71), new Student("zhaoliu", 82));
	
	public static void main(String[] args) {
		students.sort(Student::compareByScore);
		System.out.println(students);
	}
	
}

上面的例子中,我们想要调用list的sort()方法对集合中的元素排序,而sort()方法使用Comparator接口作为参数:

public interface List extends Collection {
    default void sort(Comparator c) { ... }
    ...
}

之前提到过:JDK8将Comparator注解为函数式接口,所以我们可以使用Lambda表达式表示Comparator接口的一个实现:

students.sort((Student s1, Student s2) -> s1.getScore() - s2.getScore());

而此时Student类中恰好存在compareScore()方法,可以实现上面Lambda表达式的功能,那么就可以用方法引用来引用该方法:

students.sort(Student::compareByScore);

三类方法引用

方法引用可以分为三类:静态方法引用、实例方法引用和构造方法引用。

静态方法引用

格式:   类名 :: 静态方法名

这类方法引用比较好理解——相当于把调用静态方法的.替换成::(注意,这里的用词是相当于,方法调用和方法引用之间没有任何关系,它们是两种完全不相同的东西),TestMethodReference1中使用的就是此类方法引用。

实例方法引用

格式1:  对象名 :: 实例方法名

这类方法引用也很容易理解——相当于把调用对象实例方法的.替换成::。

JDK8中Iterable接口新增forEach()方法,该方法使用Consumer接口作为参数:

public interface Iterable {
    default void forEach(Consumer action) { ... }
    ...
}

下面我们来试着使用该方法来打印集合:

public class TestMethodReference2 {
	
	public static void main(String[] args) {
		TestMethodReference1.students.forEach(System.out::println);
	}

}

看到上面的例子,你可能会觉得奇怪:这里并没有出现对象,为什么可以使用方法引用呢?其实out就是定义在System类中一个对象:

public final class System {
    public final static PrintStream out = null;
    ...
}

 而println()方法正是PrintStream类中的成员方法:

public class PrintStream extends FilterOutputStream implements Appendable, Closeable {
    public void println(Object x) { ... }
    ...
}

这里我们希望有一个方法可以实现打印对象的功能,而实例对象out正好可以提供println()方法,因此可以使用方法引用。

格式2:  类名 :: 实例方法名

 这类方法引用是比较难理解的,我们通过一个例子讲解。向Student类中添加下面的方法:

public int compareByScore2(Student s1) {
      return this.getScore() - s1.getScore();
}

public class TestMethodReference3 {
	
	public static void main(String[] args) {
		TestMethodReference1.students.sort(Student::compareByScore2);
		TestMethodReference1.students.forEach(System.out::println);
	}
	
}

通过TestMethodReference1的例子,我们知道sort()方法使用Comparator接口作为参数。然而Comparator接口的compare()方法有两个参数,而Student类的compareByScore2()方法却只有一个参数,这里为什么可以使用方法引用表示compareByScore2()方法呢?

这就是这类方法引用难以理解的地方。首先实例方法肯定需要通过对象来调用,那么这个对象是从哪儿来的呢?我们知道方法引用对应Lambda表达式,Lambda表达式的第一个参数就会成为调用实例方法的对象,其余参数则会作为该实例方法的参数传递。如下图所示:

Java8新特性(一) Lambda表达式、函数式接口与方法引用_第1张图片

下面再演示一个例子强化理解:

public class TestMethodReference4 {
	
	public static void main(String[] args) {
		List list = Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu");
		list.sort(String::compareToIgnoreCase);
		list.forEach(System.out::println);
	}
	
}

 String类的compareToIgnoreCase()方法定义如下:

public final class String implements java.io.Serializable, Comparable, CharSequence {
    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }
    ...
}

构造方法引用

格式1:  类名 :: new

这类方法引用只能用于构造方法。下面通过一个例子演示:

public class TestMethodReference5 {
	
	public static void main(String[] args) {
//		Supplier supplier = Student::new;    // 报错: Student类中没有无参构造方法
		BiFunction bf = Student::new;
		Student student = bf.apply("zhangsan", 64);
		System.out.println(student);
	}
	
}

上面的例子中,试图通过方法引用表示Student类的无参构造方法,但是由于我们在Student类定义了有参构造方法,所以该类中不存在无参构造方法。因此这里使用方法引用表示Student类的无参构造方法会出现错误信息,方法引用只能表示指定类的无参构造方法

格式2:  类名[] :: new

这类方法引用是数组专属的。

其实可能你都没发现,到现在为止你都没有见过数组的构造方法。在Java中并不存在数组这个类,它是一种即时创建的类型。数组的构造方法只有一个int类型参数,该参数表示数组的长度。下面的例子是数组构造方法引用:

public class TestMethodReference6 {
	
	public static void main(String[] args) {
		IntFunction fun = int[]::new;
		int[] arr = fun.apply(5);    // 创建长度为5的数组 
		System.out.println(arr.length);
	}
	
}

到此为止,关于Lambda表达式、函数式接口和方法引用就全部介绍完了。不过这些只是所有Java8新特性的基础,下一篇文章将进入Java8新特性最重要部分的学习——流。

 

参考:

https://blog.csdn.net/yangyifei2014/article/details/80068265

https://blog.csdn.net/sun_promise/article/details/51190256

https://segmentfault.com/a/1190000012269548

你可能感兴趣的:(Java,Java8新特性)