EffectiveJava(6)之Lambda和Stream

注:本文是《Effective Java》学习的笔记。

Lambda 和 Stream是 java8加进来的。真的特别好用。写出来的代码很漂亮。还增加了函数式接口和方法引用等。

42.Lambda优先于匿名类

用comparator排序器来举一个例子。下面是匿名类形式

package zy.service.effective.six;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * @Author zhangyong
 * @Date 2019/7/12 12:32
 */
public class Lambda {
    public static void compare(List list){
        Collections.sort(list, new Comparator() {
            @Override
            public int compare(Number o1, Number o2) {
                if (o1 instanceof Double){
                    return Double.compare(o1.doubleValue(),o2.doubleValue());
                }else if (o1 instanceof Integer){
                    return Integer.compare(o1.intValue(),o2.intValue());
                }
                return 0;    //省略
            }
        });
    }

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(3.0);
        list.add(2.0);
        list.add(3.1);
        compare(list);
        list.forEach(System.out::println);   //2.0  3.0  3.1
    }
}

要是用lambda可以换成这个

Collections.sort(list, (o1, o2) -> {
    if (o1 instanceof Double) {
        return Double.compare(o1.doubleValue(), o2.doubleValue());
    } else if (o1 instanceof Integer) {
        return Integer.compare(o1.intValue(), o2.intValue());
    }
    return 0;
});

注意lambda的类型,参数类型以及返回值类型都没有出现在代码中,编译器利用一个称作类型推导的过程,进行上下文判断从而推断出这些类型。

建议:删除所有的lambda参数的类型吧,除非它们的存在能够使程序变得更加清晰。    泛型对lambda推导有很大的作用,所以不要写原生类型而不使用泛型。

lambda可以用在枚举中。下面写一个使用函数式声明的枚举demo

import java.util.function.IntBinaryOperator;

public enum Week {
    MONDAY(1,(x,y)-> 1),FRIDAY(5, Integer::sum);
    private int number;
    private IntBinaryOperator op;            //函数式声明接口,表示两个入参一个int返回类型。
    Week(int number,IntBinaryOperator op){
        this.number = number;
        this.op = op;
    }
    public int getNumber(){
        return number;
    }
    public int apply(int left,int right){    //实例调用这个方法也就是调用声明实例时的第二个入参函数。
        return op.applyAsInt(left,right);
    }
    public static void main(String[] args) {
        System.out.println(Week.MONDAY.apply(2, 4));        //与下行代码执行结果一致。
        System.out.println(Week.MONDAY.op.applyAsInt(2, 4));
        System.out.println(Week.FRIDAY.apply(2, 4));
    }
}

Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中。

Lambda越短越好。且不是完全替代匿名类,只是大部分场景替换匿名类。   Lambda无法获取自身引用,在Lambda中this是指向外围实例,而匿名类中的this指向的是匿名类实例。

尽可能不要序列化一个Lambda或者匿名类。

千万不要给函数对象使用匿名类,除非必须创建非函数接口的类型的实例。

43.方法引用优先于lambda

实际上方法引用是Lambda的一个语法糖。  瞅着比lambda更简单了。

方法引用共分为四类:
1.类名::静态方法名
2.对象::实例方法名
3.类名::实例方法名 
4.类名::new

下面给举个Demo来演示一下方法声明的使用。

package zy.service.effective.six;
import lombok.*;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
/**
 * @Author zhangyong
 * @Date 2019/7/13 11:21
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Glasses {
    private int price;

    /**
     * 类名调用的方式。静态的,中规中举,可以写在别的类中。
     */
    public static int compare(Glasses glasses1,Glasses glasses2){
        return glasses1.getPrice() - glasses2.getPrice();
    }

    /**
     * 由于使用到this作为lambda的调用方,而glasses对象才是比较。
     */
    public int compareTwo(Glasses glasses){
        return this.getPrice() - glasses.getPrice();
    }
    static class GlassesCompare{
        /**
         * 写在那个类中都行的
         */
        public int compare(Glasses g1,Glasses g2){
            return g1.getPrice() - g2.getPrice();
        }
    }

    public static void main(String[] args) {
        final Glasses g1 = new Glasses(100);
        final Glasses g2 = new Glasses(200);
        final Glasses g3 = new Glasses(50);
        List list = Arrays.asList(g1, g2, g3);
        list.sort(Glasses::compare);   // 类名::静态方法名称
        GlassesCompare g = new GlassesCompare();
        list.sort(g::compare);          // 实例::实例方法名称
        list.sort(Glasses::compareTwo); // 类名::实例方法名称
        Supplier supplier = Glasses::new;  // 类名::new
        Glasses glasses = supplier.get();
    }
}

关于lambda表达式就是下面这种的。   左边参数 → 返回值

()->returnType

对于简单的表达,lambda比函数接口简单的就用lambda就行了。

44.坚持使用标准的函数接口

函数式接口,有且仅有一个抽象方法,Object超类的public方法除外。并标注@FunctionalInterface 注解

Java.util.function包提供了大量标准的函数接口。只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。

其中有基础接口比较重要,其余接口可以被推导出来

Function 代表参数与返回值类型不一样的函数。

@FunctionalInterface
public interface Function {
    R apply(T t);

Predicate接口代表带有一个参数并返回一个boolean的函数。

@FunctionalInterface
public interface Predicate {
    boolean test(T t);

Consumer代表的是带有一个函数但是不返回任何值的函数,相当于消费掉了其参数。

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

Supplier接口代表没有参数且返回一个值的函数。

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

然后其他的函数接口以他们为基准,各种变化。前文叙述枚举时提到过  IntBinaryOperator  相应的还有long和double 这种作用于基本类型的函数接口。但是注意目前对于包装类是没有函数接口的。

45.谨慎使用Stream

Java8中增加了StreamAPI,简化了串行或并行的大批量操作。

这个API提供了两个关键抽象:Stream流代表数据元素有限或无限的顺序,Stream pipeline则代表这些元素的一个多级计算。

一个Stream pipeline包括一个源Stream ,接着是0个或者多个中间操作和一个终止操作。   所有的中间操作就是将一个流转换成为另外一个流。

Stream pipeline通常是lazy的,直到调用终止操作才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算。正是这种lazy的方式使得 无线Stream成为可能,因此千万不能忘记终止操作。

默认情况下Stream pipeline是顺序执行的。要使pipeline 并发执行,只需在该pipeline 的任何Stream 上调用parallel 并行流方法就行。但是通常不建议这么这做。

对于Stream 流的具体操作就不在叙述了。就是过滤筛选计算最后在聚集起来的一个过程。

建议:在没有显示类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要、

Stream 中的数据元素可以是对象引用,或者是基础类型值。 它支持 三种基础数据类型。  int  long  double  所以想在stream中处理char是不合理的。    下面是char在stream中使用时出现的问题,可以看到都变成了int值。当然强制类型转换可以解决这个错误,但是我们还是不要这么玩。

EffectiveJava(6)之Lambda和Stream_第1张图片

注:如果实在不确定用Stream还是用迭代比较好,那么就两种都试试,看看哪一种更好用吧。

46.优先选择Stream中无副作用的函数

1.foreach是Stream终止操作中比较无力的一个。  forEach操作应该只用于报告Stream计算的结果,而不是执行计算。

List list = Arrays.asList("c","b","c"); 
list.stream().sorted().forEach(System.out::println);   // b  c  c

可以看到上面用forEach终止的。我使用Stream一般都是以 collect来收集元素结尾。 内部有几十种方法供你玩。

2.最简单的映射收集器是toMap(keyMapper,valueMapper)   第一个参数是映射成key,第二个参数是映射成value

需要注意的是toMap映射的时候,key有相同的会抛异常。IllegalStateException   重载的就不说了。

List list = Arrays.asList("c","b","e");
Map collect = list.stream().collect(Collectors.toMap(s -> s + "a", s -> s + "z"));
System.out.println(collect);

3.还有我用的比较多的分组 groupingBy 

这个是以你的lambda或者啥条件作为分组条件,符合条件的分到一组。最终生成一个Map,key是你分组时返回值的类型,值是分过去的集合。   重载不说了。

List list = Arrays.asList("c","b","r","er","asd","rq");
Map> listMap = list.stream().collect(Collectors.groupingBy(s -> s.length()));
System.out.println(listMap);     // {1=[c, b, r], 2=[er, rq], 3=[asd]}

4.压根没有理由使用 collect(Collectors.counting());  因为  Stream.count();就有一样的功能。

还包括  summing, averaging, summarizing开头。reducing , filtering, mapping , flatMapping 和 collectingAndThen 方法。

上述方法可以直接 Stream. xx()使用,而不用 collect再使用 Collectors收集。

5.joining方法,这个方法是以分隔符连接起来,我们可以用Guava 的 splitter将字符串以一个字符分割。也可以通过joining来将多个字符串聚集起来。joining还有增加前缀和后缀的重载方法。

List list = Arrays.asList("c","b","r","er","asd","rq");
String collect = list.stream().collect(Collectors.joining("--","[","]"));
System.out.println(collect);    //[c--b--r--er--asd--rq]

6.常见的聚集起来的方式还有 toList

List list = Arrays.asList("c","b","r","er","asd","rq");
List stringList = list.stream().filter(s -> s.length() > 1).collect(Collectors.toList());
System.out.println(stringList);   //[er, asd, rq]

如上最重要的收集工厂是 toList 、toSet、 toMap 、 groupingBy、 joining 

47.Stream要优先用Collection作为返回类型。

对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。

数组可以通过  Stream.of("a","b").collect(Collectors.toList()); 来变成Stream

一般返回的就是List 或者 Map 

48.谨慎使用Stream并行

如果Stream的源头是来自Stream.iterate,或者使用了中间操作的limit,那么并行pipeline也不可能提升性能。而且会导致错误。

在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet、和concurrentHashMap实例,数组,int long等

并行Stream不仅可能降低性能,包括活性失败(CPU使用率不变了一动不动),还可能导致结果出错,以及难以预计的行为,注意并行是没有顺序可言的,所以对于最终结果很容易出现问题。

并行流最佳操作是做减法。 Stream的reduce   min  max  count  sum 等等。骤死性操作如 anyMatch, allMatch noneMatch可以用并行流。

在使用并行流时也要测试一下,虽然多线程跑,线程切换也是需要时间的,小心得不偿失。

 

 

 

 

 

 

你可能感兴趣的:(effectiveJava,effectivejava,lambda,stream)