Lambda粗解

Lambda粗解

案例总结:点此传送门

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

Lambda表达式必须基于一个接口类,该类中只有一个抽象方法。使用分为三步:

  • 定义一个接口类,包含一个抽象方法。
  • 创建该接口类的实例对象,并实现抽象方法(使用Lambda表达式实现)。
  • 直接调用抽象方法,或将实例对象传给某个方法。
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> c) {
    for (T i : list) {
        c.accept(i);
    }
}

public static void main(String[] args) {
//    forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
    Consumer<Integer> c = (Integer i) -> System.out.println(i);
    c.accept(6); // 直接调用抽象方法
    forEach(Arrays.asList(1,2,3,4,5), c); // 行为传递
	
	//直接将lambda表达式作为参数传递给forEach方法,实际传递的是Consumer接口类的实例对象。
	//lambda表达式即accept方法的具体实现,标签为Integer -> void,即T由Integer特化
    //forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i))
}

结果如图:需要注意的是创建接口对象的同时实现抽象方法,传递行为实际上传递的是接口对象(被传递的方法如forEach方法在内部有调用接口的抽象方法accept,而accept方法的实现是在创建对象时实现的,就是lambda表达式),也可直接将lambda表达式作为参数(代表一种行为)传递给forEach方法(实际上传递的是接口实例对象)。
结果图片

类型检查、类型推断以及限制

Lambda表达式本身不不包含它在实现哪个函数式接口的信息。需要通过类型检查、类型推断来判断。使用Lambda表达式时一定要对函数式接口及其抽象方法有所了解。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。有以下两种情况,这里的参数和局部变量实际上都是函数式接口创建的对象。

  • 接受它传递的方法的参数
  • 接受它的值的局部变量。根据局部变量的不同,Lambda表达式可用于多个不同的函数式接口(作为参数传入方法时同理,只是由于被传入的方法已经确定了参数)
/**
 * 此处为第一种情况
 * Lambda表达式为  (Apple a) -> a.getWeight() > 150
 * 签名为 Apple -> boolean
 * 这里是通过filter方法的参数列表来确定Lambda表达式具体实现的是哪个函数式接口
 * filter(List<T>, Predicate<T>)
 * 可以确定Lambda实现的是Predicate函数式接口
 * 对应实现的抽象方法是Predicate函数式接口中的test方法
 * 签名为 T -> booleab,T绑定Apple,能正确匹配
 */
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
/**
 * 此处为第二种情况
 * Lambda表达式均为 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
 * 签名为 (Apple, Apple) -> int
 * 这里是通过局部变量来确定Lambda表达式具体实现的函数式接口
 * 分别为Comparator(compare), ToIntBiFunction(applyAsInt), BiFunction(apply)
 * 确定了函数式接口也就确定了实现的抽象方法
 * compare方法的标签(T, T) -> int
 * applyAsInt方法的标签(T, U) -> int
 * apply方法的标签(T, U) -> int
 * T和U均绑定到Apple,即符合Lambda表达式的标签,所以此处的Lambda可以同时适配不同的函数式接口
 */
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

类型推断

为了进一步简化代码,Java编译器会从上下文中推断出用什么函数式接口来配合Lambda,也意味着它能推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。

如下面代码,可以在Lambda语法中省去标注参数类型。

// 通过目标类型T绑定Apple可以不显式的指明参数类型。
List<Apple> heavierThan150g = filter(inventory, a -> a.getWeight() > 150);
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
// 通过目标类型T绑定Apple可以不显式的指明参数类型。
Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

使用局部变量的限制

在Lambda表达式中使用外层作用域中定义的局部变量时,必须将该局部变量显式声明为final,不推荐在Lambda表达式中使用外部局部变量,会使情况变得复杂。

方法引用

方法引用就是根据已有的方法实现来创建Lambda表达式。显式的指定方法名称,会使代码可读性更好。

// 方法引用,没有实际调用方法
Apple::getWeight;
// Lambda表达式,标签为 Apple -> int
(Apple a) -> a.getWeight();

如何构建方法引用

方法引用主要有三类:

  • 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
  • 指向任意类型实例方法的方法引用(例如String的length方法,写作String:length)。
  • 指向现有对象的实例方法的方法引用(假设有一个局部变量expensiveTranscation用于存放Transcation类型的对象,它支持实例方法getValue,那么就可以写expensiveTranscation::getValue)。
/**
 * 第二种方法的思想:
 * 引用一个对象,而这个对象本身是Lambda的一个参数。
 * 此处引用的对象方法是String类型对象s的toUpperCase方法,而s是Lambda的一个参数。
 */
 String::toUpperCase;
 (String s) -> s.toUpperCase();

/**
 * 第三种方法的思想:
 * 在Lambda中调用一个已经存在的外部对象中的方法。
 * 此处的expensiveTranscation是在外部定义的实例对象,即现有对象,并调用该对象的getValue方法。
 */ 
 expensiveTranscation::getValue
 () -> expensiveTranscation.getValue();

方法引用实例

//Comparator接口类
@FunctionalInterface
public interface Comparator<T> {
	...
	int compare(T o1, T o2);
	...
}

//List接口类
public interface List<E> extends Collection<E> {
	...
	default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
	...
}

//String类
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    ...
    public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
    ...
    public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }
    ...
}

public static void main(String[] args) {
    /**
     * List类的sort方法接收一个Comparator作为对象(Lambda表达式作为参数传入方法,实际是对象)
     * Comparator中抽象方法compare方法签名为(T, T) -> int
     * String类中定义的compareToIgnoreCase方法返回值为int类型
     * 从源码中看compareToIgnoreCase方法实际上使用的还是Comparator类型对象的compare方法
     */
    List<String> str = Arrays.asList("a", "b", "A", "B");
    tr.sort((String s1, String s2) -> s1.compareToIgnoreCase(s2));
    System.out.println(str);
    
	//Lambda调用的是参数对象s1的方法,符合第二种方法,可用方法引用方式简化代码
    List<String> str2 = Arrays.asList("c", "d", "C", "D");
    str2.sort(String::compareToIgnoreCase);
    System.out.println(str2);
}

结果如图:Lambda表达式与方法引用等效
在这里插入图片描述

构造函数引用

构造函数引用与指向静态方法的引用类似,可以用类名和关键字new来创建一个引用:ClassName::new。构造函数引用的思想就是将创建对象的行为传入到其他方法。

/**
 * Supplier接口中抽象方法的签名为 () -> T
 * 适用于无参构造函数
 */
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
// 等价于
Supplier<Apple> c2 = () -> new Apple();
Apple a2 = c2.get();

/**
 * Function接口中抽象方法apply的签名为 T -> R,T绑定Integer,返回R绑定Apple。
 * 适用于参数为Integer的构造函数
 */
Function<Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply(110);
 // 等价于
Function<Integer, Apple> c4 = (weight) -> new Apple(weight);
Apple a4 = c4.apply(110)

上面是直接调用抽象方法的例子(无参和单参,多参的类似,只需使用标签能匹配多参的接口),下面看一个将接口对象传入其他方法的例子。

// 创建一个List对象weights,保存待创建Apple对象的weight属性
List<Integer> weights = Arrays.asList(7,3,4,10);

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;
}

/**
 * 多个参数的Lambda表达式可以写成:
 * (Integer weight, String color) -> new Apple(weight, color)
 * 只需使用标签能匹配的接口
 * 经过下面几步的简化,最后传入map的参数只需要为Apple::new即可
 * 当创建Apple对象需要的参数不同时,需要改的只有map方法第二个参数的接口类型。
 * 
 * 双参可使用BiFunction接口,签名为(T, U) -> R。
 * T绑定String,U绑定Integer,R绑定Apple即可代表构造参数为color和weight
 * 
 * 对于三个及以上的参数,Java没有预定义的接口,可自己创建,如:
 * public interface TriFunction<T, U, V, R> {
 * 	   // TUV代表三个参数,R代表被创建的对象类型 
 * 	   R apply(T t, U u, V v); 
 * }
 */
// 对于weights中的每一个重量都生成一个Apple对象,保存到apples中
Function<Integer, Apple> f = (Integer weight) -> new Apple(weight);
List<Apple> apples = map(weights, f);
// 简化后,直接将Lambda表达式作为参数传入map方法,省去了显式声明实现的具体接口
List<Apple> apples = map(weights, (Integer weight) -> new Apple(weight))
// 省去显式声明Lambda参数的类型
List<Apple> apples = map(weights, weight -> new Apple(weight))
// 使用构造函数引用简化后,最后只需要在map方法中将第二个参数写为Apple::new即可。
List<Apple> apples = map(weights, Apple::new);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值