java8学习:lambda表达式(2)

  1. 云栖社区>
  2. 博客>
  3. 正文

java8学习:lambda表达式(2)

期待l 2018-11-07 19:25:15 浏览612
展开阅读全文

内容来自《 java8实战 》,本篇文章内容均为非盈利,旨为方便自己查询、总结备份、开源分享。如有侵权请告知,马上删除。
书籍购买地址:java8实战

  • 紧接上一篇内容,上一篇内容讲了lambda的使用,自定义函数式接口和它自带的函数式接口,这一篇将讲述lambda的类型检查,方法引用和构造器引用等内容
  • 上一篇内容提到过,lambda表达式可以为函数式接口生成一个实例,类似匿名内部类的功能,但是lambda本身并不包含他在实现哪个函数式接口的信息
  • 下面来看一下lambda的类型检查

    • 上一篇文章中说到的,lambda的传入参数可写可不写,如下
    Predicate<Integer> predicate = (i) -> i == 3;  
    • 那么她是怎么知道i值是Integer类型的呢? 为了说明这个情况,我们可以参考下面代码
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Apple {
        private String color;
        private Integer weight;
    }
    public class Java8 {
        @Test
        public void test() throws Exception {
            List<Apple> apples = Arrays.asList();
            filterApple(apples,apple -> apple.getWeight() > 100);
        }
        private void filterApple(List<Apple> apples, Predicate<Apple> predicate){
            for (Apple apple : apples) {
                if (predicate.test(apple)){
                    System.out.println(apple);
                }
            }
        }
    }
    • test方法中的lambda并没有标注apple变量是Apple类型的,那么他为了知道apple变量的类型他会这样做

      • 首先查看filterApple方法签名,从中我们可以看到目标类型是Predicate,这样T泛型就绑定到了Apple
      • 然后查看Predicate内的抽象方法,他接受一个Apple。返回一个boolean
      • 这样函数描述符和lambda的签名是一样的都是传入Apple,apple.getWeight() > 100会返回boolean,所以类型检查是无误的
  • 我们来看下面这个值得注意的地方

    Predicate<Integer> predicate = integer -> list.add(integer);
    • 上面的表达式很正常并没有需要值得注意的地方,list.add方法本来就是返回Predicate抽象方法定义的返回值boolean
    Consumer<Integer> consumer = integer -> list.add(integer);
    • 上面的就会有点疑问了,但是它确实是正确的,Consumer的定义为T->{},所以这就会有一个void兼容规则:如果lambda的主体是一个语句表达式,他就和一个返回void的函数描述符兼容,如上面其实是返回Boolean,而且Consumer明确的说明返回void,他们肯定是不兼容会报错的,但是这个规则就会让他们和平相处
  • 类型推断

    • 上面以及讲到了lambda是怎么推断出参数的类型的,那么我们其实就不用费劲的明确给出其参数的类型,比如
    Comparator<Integer> comparator = (Integer a1,Integer a2) -> a1.compareTo(a2);
    • 完全可以写成
    Comparator<Integer> comparator = (a1,a2) -> a1.compareTo(a2);   //以后还可以简化代码
  • 使用局部变量的限制

    public class Java8 {
        private static int a = 0;
        private int b = 1;
        @Test
        public void test() throws Exception {
            int c = 2;
            IntConsumer consumera = (num) -> System.out.println(num + a);
            IntConsumer consumerb = (num) -> System.out.println(num + b);
            IntConsumer consumerc = (num) -> System.out.println(num + c);
            a = 10;
            b = 11;
            //c = 12;  //只要是重新赋值那么上面引用c变量的地方就会出错
        }
    }
    • 如上我们总结出了,类中的类变量和实例变量,lambda引用他们对他们并不造成影响,但是引用局部变量c的话,c就会被隐式的赋予final修饰
    • 对局部限制的原因

      • 首先是不鼓励使用这种能够改变外部变量的典型命令式编程
      • 并且实例变量和局部变量的实现是不一样的,实例变量是保存在堆中的,但是局部变量是保存在栈中,随着方法弹栈就消失了,考虑以下场景:主线程中定义了c,但是他又开启了一个线程B,在线程B中的lambda访问A中的c,这时候可能会在A把c回收以后B再去访问,因此java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量,如果局部变量仅仅赋值一次那就没什么区别了,因此就有了这个限制
    • 总结来就是被lambda引用的局部变量只能是final
  • 方法引用

    • 只是语法糖而已
    • 分三种方法引用

      • 静态方法的方法引用:Integer.parseInt

        • 凡是可以用类名点出来的,那就属于这种
        Function<String,Integer> function = Integer::parseInt;
        Consumer<String> consumer = Arrays::asList;
        • 为什么可以这么用,拿Integer举例
        • 首先查看一下Function的抽象方法的定义

          R apply(T t);
        • 如上可以看出,需要传入一个传出一个 T -> R
        • 再看parseInt的定义

        1. static int parseInt(String s) throws NumberFormatException {
          return parseInt(s,10);
          }

        • 如上是一个静态方法,那么方法引用就可以引用它,它是传入一个String,返回一个int,那么把上面的Function的泛型固定为String和Int不就好了,所以Function所需要的抽象方法的实现可以由paraseInt方法的实现满足,并且泛型一致,当调用Function中的抽象方法的时候,传入的参数就是传入到parseInt方法中,并返回int,所以可以通过编译
      • 实例的方法引用:"123".length

        List<Integer> list = new ArrayList<>();
        list.sort(Integer::compareTo);
        • 我们看为什么可以如上的用法list.sort(Integer::compareTo);,首先查看sort方法
        default void sort(Comparator<? super E> c) {....}
        • 是需要一个Comparator的函数式接口的抽象方法的实现
        • 再来看Comparator的抽象方法int compare(T o1, T o2);,看到这也就知道了,sort里面需要一个(T,T)->int,那么我们继续看Integer的compareTo方法
        public int compareTo(Integer anotherInteger) {
            return compare(this.value, anotherInteger.value);
        }
        • 上面的compareTo也清楚的表明了是两个参数作为传入并且返回一个int,并且是this.value调用,说明是一个实例对象,所以此方法正好满足Comparator函数式接口中抽象方法的方法签名的定义,所以编译无误
      • 现有对象的实例方法引用:引用局部变量类型中的方法

        public class Java8 {
            @Test
            public void test() throws Exception {
                Java8 java8 = new Java8();
                Function<String,Integer> function1 = str -> java8.parseInt(str);
                Function<String,Integer> function2 = java8::parseInt;
            }
            public Integer parseInt(String str){
                return Integer.parseInt(str);
            }
        }
        • 如上就是引用一个局部变量中的方法
      • 分清第二个方法引用方法和第三个方法引用方法

        public class Java8 {
            @Test
            public void test() throws Exception {
                Java8 java8 = new Java8();
                Function<String,Integer> function2 = str -> str.length();
                Function<String,Integer> function22 = String::length;
                Function<String,Integer> function3 = str -> java8.length(str);
                Function<String,Integer> function33 = java8::length;
            }
            public Integer length(String str){
                return str.length();
            }
        }
        • 如上方法名我做了区别,可以观察出,实例方法引用就是靠传入的参数调用方法,而现有对象实例方法利用是不用引用传入的参数作为调用的对象的
  • 构造方法的引用

    • 这个对于方法引用就简单太多了,举例说明一下
    • 对于无参的构造器
    Supplier<Apple> supplier = Apple::new;
    • 对于一个参数的
    Function<String,Apple> function = Apple::new;
    • 对于两个参数的
    BiFunction<String,Integer,Apple> function = Apple::new;
    • 总结来就是找能与想创建的对象的构造函数参数个数匹配的函数式接口,并且函数式接口的泛型必须跟构造方法参数类型一致
    • 上面虽然没有实例化对象,但是已经引用了此对象,那么就会发生一些有意思的事情
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Fruit {
        private Integer weight;
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Apple extends Fruit {
        private Integer weight;
    }
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class Banana extends Fruit{
        private Integer weight;
    }
    public class Java8 {
        @Test
        public void test() throws Exception {
            Map<String,Function<Integer,Fruit>> map = new HashMap<>();
            map.put("apple",Apple::new);
            map.put("banana",Banana::new);
    
            Fruit apple = get(map, "apple", 123);
            System.out.println(apple);
            Fruit banana = get(map, "banana", 23);
            System.out.println(banana);
        }
        public Fruit get(Map<String,Function<Integer,Fruit>> map,String fruitName,Integer fruitWeight){
            return map.get(fruitName).apply(fruitWeight);
        }
    }
    //输出:Apple(weight=123)
    //输出:Banana(weight=23)
    • 可以根据不同的名称得到不同的重量的各种蔬果,只因为它存在构造器的引用
  • 下面就是lambda表达式的有用的方法的说明

    • 考虑一种情况那就是组合比较,比如说两个对象,比较值a都相同的情况下在比较b值是否相等,然后排序,如上的lambda中我们只是使用Comparator中的方法比较了一次,那么如果我们是连续比较呢?

      @Test
      public void test() throws Exception {
          Apple apple1 = new Apple(2015,22);
          Apple apple2 = new Apple(2014,82);
          Apple apple3 = new Apple(2014,12);
          List<Apple> apples = Arrays.asList(apple1,apple2,apple3);
          apples.sort(Comparator.comparing(Apple::getYear));
          //[Apple(year=2014, weight=82), Apple(year=2014, weight=12), Apple(year=2015, weight=22)]
          System.out.println(apples);
      }
      • 如上使用lambda只能排序一次,那么我们怎么二次排序呢?使用thenComparing就可以啦
      @Test
      public void test() throws Exception {
          Apple apple1 = new Apple(2015,22);
          Apple apple2 = new Apple(2014,82);
          Apple apple3 = new Apple(2014,12);
          List<Apple> apples = Arrays.asList(apple1,apple2,apple3);
          apples.sort(Comparator.comparing(Apple::getYear).thenComparing(Apple::getWeight));
          //[Apple(year=2014, weight=12), Apple(year=2014, weight=82), Apple(year=2015, weight=22)]
          System.out.println(apples);
      }
      • 翻转的排序规则的话使用reversed就可以实现了
    • 在我们使用Predicate判断接口的时候,怎么实现&&,||,^呢?对应的方法也就是and,or,negate

      • 还是利用上面的apples集合,下面我们来做筛选
      • 筛选出2015年的apple
      Predicate<Apple> year2014 = apple -> apple.getYear() == 2014;
      for (Apple apple : apples) {
          if (year2014.test(apple)) {
              //Apple(year=2014, weight=82)
              //Apple(year=2014, weight=12)
              System.out.println(apple);
          }
      }
      Predicate<Apple> year2015 = year2014.negate();
      for (Apple apple : apples) {
          if (year2015.test(apple)) {
              //Apple(year=2015, weight=22)
              System.out.println(apple);
          }
      }
      • negate也就是取反操作,当然取2015年的apple,直接==2015也行,这只是在做演示
    • 取出苹果weight=12或者22的apple
    Predicate<Apple> weight12 = apple -> apple.getWeight() == 12;
    Predicate<Apple> weight22 = weight12.or(apple -> apple.getWeight() == 22);
    for (Apple apple : apples) {
        //Apple(year=2015, weight=22)
        //Apple(year=2014, weight=12)
        if (weight22.test(apple)){
            System.out.println(apple);
        }
    }
    • 只是Predicate的组合而已
    • 取出weight=12,year=2014的aple
Predicate<Apple> weight12 = apple -> apple.getWeight() == 12;
Predicate<Apple> year2014 = weight12.and(apple -> apple.getYear() == 2014);
for (Apple apple : apples) {
    //Apple(year=2014, weight=12)
    if (year2014.test(apple)){
        System.out.println(apple);
    }
}
//Predicate<Apple> and = weight12.and(year2014);这样也是可以的,而且这样标示比较的清楚,只要变量名起的好
  • 书上说:这样可以组合更复杂的表达式,但是一行行去读代码还不如原来的操作符。因为自己也是学习,所以理解不深刻,对于这如果有其他的爽歪歪的用法欢迎评论~
  • 函数的复合

    • 在Function中存在andThen方法,我们来试试看
      public void test() throws Exception {
      Function<Integer,Integer> a = x -> x + 1;
      Function<Integer,Integer> b = x -> x * 2;
      Function<Integer,Integer> c = a.andThen(b);
      System.out.println(c.apply(3));
      //1 4
      //2 6
      //3 8
    • 明显的看出,传入的3是这样计算的(3+1)*2
    • 还提供了另一个方法:compose
Function<Integer,Integer> a = x -> x + 1;
Function<Integer,Integer> b = x -> x * 2;
Function<Integer,Integer> c = a.compose(b);
System.out.println(c.apply(2));
//1 3
//2 5
//3 7
  • 计算过程就是这样的:(2*2)+1
好了本篇就介绍到这了。如果发现不对的,请及时更正哈~

网友评论

登录后评论
0/500
评论
期待l
+ 关注