Java8Lambda表达式

Lambda是一个匿名函数,Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),你可以理解为是一段可以传递的代码。使用 Lambda 表达式可以使代码变的更加简洁紧凑,是一种函数式编程方式。首先,我们先了解一下为什么要使用Lambda表达式。

假如现在有一个需求,为农民写个软件,需求是可以在苹果中挑选出所有的红苹果。

第一步,创建Apple类,具有两个属性:颜色和重量,还要get、set方法、重写toString。

public class Apple {
	
	private String color;
	private double weight;
        // 构造器
	// get set 方法
        // 重写toString()方法
	
}

第二步,准备一个集合存放苹果。

List<Apple> apples = Arrays.asList(
				new Apple("red",100.22),
				new Apple("blue",90.42),
				new Apple("red",150.90),
				new Apple("blue",120.56));

第三步,实现在所有苹果中挑选出红苹果的逻辑。

private static List<Apple> filterByRedColor(List<Apple> apples){
	List<Apple> result = new ArrayList<>(); 
	for(Apple apple : apples){
		if("red".equals(apple.getColor())){
			result.add(apple);
		}
	}
	return result;
}

第四步,调用并打印结果。

List<Apple> result = filterByRedColor(apples);
		
for(Apple apple : result){
	System.out.println(apple);
}
Apple [color=red, weight=100.22]
Apple [color=red, weight=150.9]

很简单的实现了这个功能,但是现在需求有变化,还需要查询绿色的苹果,你可能需要在复制一遍方法,把颜色改成blue就行了。

private static List<Apple> filterByBlueColor(List<Apple> apples){
	List<Apple> result = new ArrayList<>(); 
	for(Apple apple : apples){
		if("blue".equals(apple.getColor())){
			result.add(apple);
		}
	}
	return result;
}

如果以后再出现别的颜色的苹果呢,比如黄色,浅绿色等等,这样写明显是有问题的,它违反了DRY(不要重复你自己)的软件设计规则。你可能想到了一个优化办法,将颜色作为参数传入方法进行判断。

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

可是需求再次变更,想要找到重量大于150g的红色苹果,这个方法还是不能满足需求,那我们再次优化。

定义一个接口,checkApple方法用来判断苹果是否符合筛选条件。

public interface ApplePredicate {
	
	boolean checkApple(Apple apple);

}

定义两个实现类,分别是筛选红色的苹果和重量大于150g的苹果。

public class AppleRedColorPredicate implements ApplePredicate{

	@Override
	public boolean checkApple(Apple apple) {
		return "red".equals(apple.getColor());
	}

}

public class AppleHeavyWeightPredicate implements ApplePredicate{

	@Override
	public boolean checkApple(Apple apple) {
		return apple.getWeight() > 150;
	}

}

定义方法,实现对苹果的查询。

private static List<Apple> filter(List<Apple> apples,ApplePredicate ap){
	List<Apple> result = new ArrayList<>(); 
	for(Apple apple : apples){
		if(ap.checkApple(apple)){
			result.add(apple);
		}
	}
	return result;
}

调用方法,并打印结果。

List<Apple> result = filter(apples, new AppleRedColorPredicate());
List<Apple> result2 = filter(apples, new AppleHeavyWeightPredicate());
		
for(Apple apple : result){
	System.out.println(apple);
}
		
for(Apple apple : result2){
	System.out.println(apple);
}
Apple [color=red, weight=100.22]
Apple [color=red, weight=150.9]
Apple [color=red, weight=150.9]

现在采用抽象的思维实现了这个需求,再有新的查询方式只要定义一个类实现ApplePredicate接口就可以了。但是,这样依然很麻烦,如果几十个查询条件岂不是要写几十个实现类,那么我们可以使用匿名内部类来实现。

List<Apple> result = filter(apples, new ApplePredicate(){

	@Override
	public boolean checkApple(Apple apple) {
		return "red".equals(apple.getColor());
	}
			
});

优化并没有到此为止,Java8提供了Lambda表达式来进一步优化代码。

List<Apple> result = filter(apples, (a) -> "red".equals(a.getColor()));
List<Apple> result2 = filter(apples, (a) -> a.getWeight() > 150);

使用Lambda表达式将函数当作参数传入到filter()方法中,你可以发现其实(a)就是匿名内部类重写方法的参数,而->后面的语句则是重写的方法的方法体。此时控制台打印result和result2结果依然是正确的。

Lambda语法

(parameters) -> expression
或
(parameters) ->{ statements; }

其中parameters是参数,参数名随便起,a,b,x,y都行,可以不显示指定参数类型,编译器会根据上下文的环境推断参数类型,如果参数的个数是一个可以不用写小括号。

->是Lambda操作符,也叫箭头操作符。

expression或者statements是Lambda体,如果Lambda体只有一行可以省略大括号,如果只有一行且有返回值的Lambda体也可以省略return关键字,如果是多行的话必须写大括号,且如果有返回值必须显示return。

以下几种都是正确的:

// 1. 不需要参数,返回值为 5  
() -> 5  

// 2. 不需要参数,无返回值
() -> System.out.print(22) 
  
// 3. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 4. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 5. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 6. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

// 7. 接收2个int型整数,返回他们的和 与 差的乘积
(x, y) -> {
    int a = x + y;
    int b = x - y;
    return a * b;
};

函数式接口

使用Lambda表达式有一个要求,就是需要函数式接口的支持,函数式接口指的就是只有一个抽象方法得接口。你可以使用@FunctionalInterface注解声明某个接口是函数式接口,如果接口中超过两个抽象方法就会编译错误。

我们可以通过Lambda表达式创建该接口的对象,若 Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明。

变量作用域

Lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 Lambda 内部修改定义在域外的局部变量,否则会编译错误。

发现weight没有声明为final居然没报错,这是因为编译时Java8会替weight加上final,其实weight还是final修饰的。

如果对weight做运算就会报错。

内置函数接口

此时你发现好像需要先定义一个函数接口才能使用Lambda表达式,其实Java8内置了一些函数接口供我们使用,我们直接使用即可。

四大核心内置接口

其他接口

四大核心内置接口是我们需要重点掌握的,我们可以选择适合业务需求的接口来使用,下面是简单的使用示例。

// 消费型接口 输出内容
	@Test
	public void test1() {
		hello("你好", x -> System.out.println(x));
	}

	public void hello(String message, Consumer<String> consumer) {
		consumer.accept(message);
	}

	// 供给型接口 把数字添加到集合内
	@Test
	public void test2() {
		List<Integer> list = getList(20, () -> (int) (Math.random() * 100));
		for (Integer integer : list) {
			System.out.println(integer);
		}
	}

	public List<Integer> getList(int num, Supplier<Integer> supplier) {
		List<Integer> list = new ArrayList<>();
		for (int i = 0; i < num; i++) {
			list.add(supplier.get());
		}
		return list;
	}

	// 函数型接口 去掉首尾空格
	@Test
	public void test3() {
		String str = strHandler("  你好  ", x -> x.trim());
		System.out.println(str);
	}

	public String strHandler(String str, Function<String, String> function) {
		return function.apply(str);
	}

	// 断言型接口 把符合条件的字符串添加到集合内
	@Test
	public void test4() {
		List<String> strs = Arrays.asList("abc","bcd","acd","ccd");
		List<String> result = strCheck(strs, x -> x.contains("a"));
		for (String str : result) {
			System.out.println(str);
		}
	}

	public List<String> strCheck(List<String> strs, Predicate<String> predicate) {
		List<String> list = new ArrayList<>();
		for(String str : strs){
			if(predicate.test(str)){
				list.add(str);
			}
		}
		
		return list;
	}

方法引用

若 Lambda 体中的功能,已经有方法提供了实现,可以使用方法引用,可以将方法引用理解为 Lambda 表达式的另外一种表现形式。主要有以下三种:

  1. 对象的引用 :: 实例方法名
  2. 类名 :: 静态方法名
  3. 类名 :: 实例方法名

使用操作符 “::” 将方法名和对象或类的名字分隔开来。

使用前提:方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致。

对象的引用 :: 实例方法名

@Test
public void test1(){
	// Lambda表达式
	Consumer<String> con = (x) -> System.out.println(x);
	// 使用方法引用
	Consumer<String> con2 = System.out::println;
	con2.accept("hello");
		
	Apple apple = new Apple("red",150);
	// Lambda表达式
	Supplier<String> sup = () -> apple.getColor();
	// 使用方法引用
	Supplier<String> sup2 = apple::getColor;
		
	System.out.println(sup2.get());
}

第一个例子Lambda体是System.out.println(x)。其中System.out.println(x)是一个已经被实现了的方法,它的参数和返回值与Consumer接口方法的参数和返回值一样,所以可以使用方法引用简写。其中System.out是对象也就是PrintStream,println是方法名。

第二个例子Lambda体是apple.getColor(),返回类型是String,所以可以使用方法引用简写Lambda表达式

类名 :: 静态方法名

@Test
public void test2() {
	Comparator<Integer> com = (x, y) -> Integer.compare(x, y);

	System.out.println("-------------------------------------");

	Comparator<Integer> com2 = Integer::compare;
		
		
	BiFunction<Double, Double, Double> fun = (x, y) -> Math.max(x, y);
	System.out.println(fun.apply(1.5, 22.2));

	System.out.println("--------------------------------------------------");

	BiFunction<Double, Double, Double> fun2 = Math::max;
	System.out.println(fun2.apply(1.2, 1.5));
}

第一个例子中Lambda体是Integer.compare(x, y),这是Integer类提供的静态方法,而且这个静态方法的参数列表和返回值与Comparator接口方法的参数列表和返回值相同,所以可以使用方法引用。

第二个例子同样,只不过参数是两个。

类名 :: 实例方法名

使用这种方式还需要一个前提:当需要引用方法的第一个参数是调用对象,并且第二个参数是实例方法的参数(或无参数)时才可以使用。

@Test
public void test3() {
	BiPredicate<String, String> bp = (x, y) -> x.equals(y);
	System.out.println(bp.test("abcde", "abcde"));

	System.out.println("-----------------------------------------");

	BiPredicate<String, String> bp2 = String::equals;
	System.out.println(bp2.test("abc", "abc"));

}

例子中Lambda体是x.equals(y),其中x是第一个参数,且是调用对象,y是第二个参数且是实例方法的参数,所以可以使用方法引入简写。

构造器引用

格式: ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致!

@Test
public void test4() {
	Supplier<Apple> sup = () -> new Apple();
	System.out.println(sup.get());

	System.out.println("------------------------------------");

	Supplier<Apple> sup2 = Apple::new;
	System.out.println(sup2.get());

	Function<String, Apple> fun = Apple::new;

	BiFunction<String, Double, Apple> fun2 = Apple::new;
}

第一个例子中Lambda体是通过Apple的无参构造器创建对象,所以可以通过Apple::new简写Lambda表达式,它调用的是Apple的无参构造,因为Supplier<Apple> sup = () -> new Apple();并没有传参数。

public Apple() {}

第二个例子传一个String类型的参数给构造器,可以通过Apple::new简写Lambda表达式,它调用的Apple的String参数的构造器来创建对象。

public Apple(String color) {
	this.color = color;
}

第三个例子传入String 和Double类型的参数给构造器,可以通过Apple::new简写Lambda表达式,它调用的Apple的String参数和Double参数的构造器来创建对象。

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

数组引用

格式:Type[]::new

@Test
public void test5() {
	Function<Integer, String[]> fun = (args) -> new String[args];
	String[] strs = fun.apply(10);
	System.out.println(strs.length);

	System.out.println("--------------------------");

	Function<Integer, Apple[]> fun2 = Apple[]::new;
	Apple[] apples = fun2.apply(20);
	System.out.println(apples.length);
}

异常处理

处理Unchecked异常

List<Integer> integers = Arrays.asList(3, 9, 7, 6, 10, 20);
integers.forEach(i -> System.out.println(50 / i));

上述代码在i=0的时候会出现ArithmeticException异常,最简单的方式我们可以通过try—catch处理。

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
    try {
        System.out.println(50 / i);
    } catch (ArithmeticException e) {
        System.err.println(
          "Arithmetic Exception occured : " + e.getMessage());
    }
});

但是此时代码可读性差,不简洁,可以通过编写包装方法consumerWrapper并接收一个lambda表达式,在该方法内会进行异常处理。

static <T, E extends Exception> Consumer<T>
	  consumerWrapper(Consumer<T> consumer, Class<E> clazz) {

	    return i -> {
	        try {
	            consumer.accept(i);
	        } catch (Exception ex) {
	            try {
	                E exCast = clazz.cast(ex);
	                System.err.println(
	                  "Exception occured : " + exCast.getMessage());
	            } catch (ClassCastException ccEx) {
	                throw ex;
	            }
	        }
	    };
	}
integers.forEach(consumerWrapper(x -> System.out.println(50 / x),ArithmeticException.class));

处理Checked异常

本部分内容翻译自:Exceptions in Java 8 Lambda Expressions

这个例子中writeToFile()方法会抛出IOException。IOException是一个Checked异常,所以必须处理。因此要么抛出要么捕获。

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

抛出Checked异常

throws抛出异常仍然会提示错误unhandled IOException。

public static void main(String[] args) throws IOException {
    List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
    integers.forEach(i -> writeToFile(i));
}

尝试实现Consumer,并指定accept抛出Exception,仍然错误。因为accept定义中并未 抛出任何异常。

Consumer<Integer> consumer = new Consumer<Integer>() {

    @Override
    public void accept(Integer integer) throws Exception {
        writeToFile(integer);
    }
};

通过自定义函数式接口并生命方法抛出异常。

@FunctionalInterface
public interface ThrowingConsumer<T, E extends Exception> {
    void accept(T t) throws E;
}
static <T> Consumer<T> throwingConsumerWrapper(
  ThrowingConsumer<T, Exception> throwingConsumer) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    };
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

处理Checked异常

接着对throwingConsumerWrapper进行改造,使其能接收一个异常类型参数。

static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(
  ThrowingConsumer<T, E> throwingConsumer, Class<E> exceptionClass) {

    return i -> {
        try {
            throwingConsumer.accept(i);
        } catch (Exception ex) {
            try {
                E exCast = exceptionClass.cast(ex);
                System.err.println(
                  "Exception occured : " + exCast.getMessage());
            } catch (ClassCastException ccEx) {
                throw new RuntimeException(ex);
            }
        }
    };
}
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(handlingConsumerWrapper(
  i -> writeToFile(i), IOException.class));

类似的,可以根据实际需求编写ThowingFunction, ThrowingBiFunction, ThrowingBiConsumer。如果希望使用现成的工具可以考虑Vavr或ThrowingFunction,这两个工具值得一看。vavr的功能简介可参考https://blog.csdn.net/revivedsun/article/details/80088080

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值