JDK8 流与λ表达式

λ表达式

什么是λ表达式

λ表达式有三部分组成:参数列表,箭头(->),以及一个表达式或者语句块。

public int add(int x, int y) {
    return x + y;
}

转换为λ表达式

(int x, int y) -> x + y;

去除参数类型

(x, y) -> x + y;

无参 以及 只有一个参数

() -> { System.out.println("Hello Lambda!"); 
c -> { return c.size(); }

λ表达式的类型

λ表达式可以被当做是一个Object(注意措辞)。λ表达式的类型,叫做“目标类型(target type)”。λ表达式的目标类型是“函数接口(functional interface)”,这是Java8新引入的概念。它的定义是:一个接口,如果只有一个显式声明的抽象方法,那么它就是一个函数接口。一般用@FunctionalInterface标注出来(也可以不标)。举例如下:

@FunctionalInterface
public interface Runnable{
    void run(); 
}
    
public interface Callable<V>{
    V call() throws Exception; 
}
       
public interface Comparator<T>{
    int compare(T o1, T o2); boolean equals(Object obj); 
}

所以 可以定义

Runnable r1 = () -> {System.out.println("Hello Lambda!");};

思考:可否定义 Object o = () -> {System.out.println("Hello Lambda!");};

JDK常用的预定义接口函数

//入参为T,返回R
@FunctionalInterface
public interface Function<T, R> {  
    R apply(T t);
}

//入参为T,无返回值
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

//无入参,返回T(通常配合构造方法)
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

//入参为T,返回值为boolean
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

//入参为T,U,返回值为R 类似还有BiConsumer等
@FunctionalInterface
public interface BiFunction<T, U> {
    R accept(T t, U u);
}

方法引用

方法引用让开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。

public static class Car {
    public static Car create( final Supplier<Car> supplier ) {
        return supplier.get();
    }              
        
    public static void collide( final Car car ) {
        System.out.println( "Collided " + car.toString() );
    }
        
    public void follow( final Car another ) {
        System.out.println( "Following the " + another.toString() );
    }
        
    public void repair() {   
        System.out.println( "Repaired " + this.toString() );
    }
}

第一种方法引用的类型是构造器引用,语法是Class::new,或者更一般的形式:Class<T>::new。注意:这个构造器没有参数。

Car car = Car.create(Car::new);
List<Car> cars = Arrays.asList(car);

第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。

cars.forEach(Car::collide);

第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:

cars.forEach(Car::repair);

第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:

final Car police = Car.create(Car::new);
cars.forEach(police::follow);

什么是流

Stream 不是数据结构,不保存数据,它是有关算法和计算的,就如同一个高级版本的迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。同时又与迭代器不同,迭代器只能串行操作,Stream可以并行化操作。

流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道
,如下图所示。

image

常用的构建流的方式:

  • 集合Collection
 Collection.stream()
 Collection.parallelStream()
  • 数组
Stream.of(T[] tArray)
  • 多个相同类型对象
Stream.of("chaimm","peter","john"); 

流的基本使用

常见操作

中间操作 无状态 map (mapToInt, flatMap 等)、filter、peek
有状态 distinct、sorted、limit、skip
终结操作 非短路 forEach、forEachOrdered、toArray、reduce、collect、min、 max、 count
短路操作 anyMatch、allMatch、noneMatch、findFirst、findAny

Stream中的操作可以分为两大类:中间操作与终结操作

中间操作(Intermediate):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。中间操作又可以分为无状态(Stateless)操作与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响;后者是指该操作只有拿到所有元素之后才能继续下去。

终结操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。终结操作又可以分为短路与非短路操作,短路是指遇到某些符合条件的元素就可以得到最终结果,比如找到第一个满足条件的元素。而非短路是指必须处理所有元素才能得到最终结果。

map/flatMap

对流中的每个元素执行一个函数,使得元素转换成另一种类型输出。

map 一对一 (入参 Function<T, R>)

List<Person> persons = new ArrayLisy<>();
List<Long> result = persons.stream().map(x -> x.getId())
        .collect(Collectors.toList());

List<Student> result = persons.stream().map(x -> {
    Student s = new Student();
    s.setName(x.getName());
    return s;
}).collect(Collectors.toList());

flatMap 一对多 (入参 Function<T, ? extends Stream>)

List<List<Person>> listAll = new ArrayList<>();

List<Person> result = listAll.stream().flatMap(x -> x.stream())
        .collect(Collectors.toList());

List<Student> result = listAll.stream().flatMap(x -> x.stream())
        .map(x -> {
            Student s = new Student();
            s.setName(x.getName());
            return s;
        }).collect(Collectors.toList());

filter

filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。 (入参 Predicate)

Integer[] nums = new Integer[]{1, 2, 3, 4, 5, 6};
Integer[] evens = Stream.of(nums).filter(n -> n%2 == 0)
        .toArray(Integer[]::new);

List<Student> result = persons.stream().filter(x -> x.getAge() < 20)
        .map(x -> {
            Student s = new Student();
            s.setName(x.getName());
            return s;
        }).collect(Collectors.toList());

peek

peek 方法我们可以拿到元素,然后做一些其他事情。(入参 Consumer)

List<Long> result = persons.stream().map(x -> x.getId())
        .peek(x -> System.out.println(x))
        .collect(Collectors.toList());

Map<Long, Person> map = new HashMap<>();
List<Student> result = persons.stream()
        .filter(x -> x.getAge() < 20 )
        .peek(x -> map.put(x.getId(), x) )
        .map(x -> {
            Student s = new Student();
            s.setName(x.getName());
            return s;
        }).collect(Collectors.toList());

limit/skip

limit 返回 Stream 的前面 n 个元素,skip 则是扔掉前 n 个元素

List<Long> result = persons.stream().map(x -> x.getId())
    .limit(10).skip(3).collect(Collectors.toList());

sorted

对元素进行排序 (入参 Comparator)

List<Person> personList2 = persons.stream()
    .sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
    .collect(Collectors.toList());

forEach

对元素进行遍历消费 (入参 Consumer)

persons.stream().sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
    .forEach(peron -> {
        System.out.println(persion.getName());
    });

collect

对元素进行收集

List<Person> personList2 = persons.stream()
    .sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
    .collect(Collectors.toList());

List<Person> personList2 = persons.stream()
    .sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
    .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
Collectors.toMap

转换为MAP

Map<Long, String> personIdNameMap = persons.stream()
    .collect(Collectors.toMap(Person::getId, Person::getName);

Map<Long, String> personIdNameMap = persons.stream()
    .collect(Collectors.toMap(Person::getId, Person::getName, (v1,v2)->v1);
Collectors.groupingBy

分组

Map<Integer, List<Person>> personAgeMap = persons.stream()
    .collect(Collectors.groupingBy(Person::getAge));

Map<Integer, Map<Long, Person>> personAgeMap =  persons.stream()
    .collect(Collectors.groupingBy(Person::getAge,
        Collectors.toMap(Person::getId, Function.identity())));
Collectors.collectingAndThen

收集然后处理

Map<Integer, Integer> personAgeMap =  persons.stream()
    .collect(Collectors.groupingBy(Person::getAge,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list->list.size()
        )));

接口方法

接口default方法

默认方法使得开发者可以在不破坏兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写

private interface HelloService {
    default String sayHello() { 
        return "hello"; 
    }        
}
        
private static class HelloImpl implements HelloService {
}
    
private static class HelloWorldImpl implements HelloService {
    @Override
    public String sayHello() {
        return "hello world";
    }
}

思考:为啥要加入default方法?

接口static方法

private interface HelloService {
    static boolean testHello(String s) { 
        return Objects.equals(s,"hello");
    }        
}

Stream流水线解决方案

Stage(Pipeline)

java8用Stage来记录Stream的中间操作,很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据来源,操作,回调函数>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的Pipeline来代表Stage,然后将具有先后顺序的各个Stage连到一起,就构成了整个流水线。


image

Sink

有了操作,我们需要将所有操作叠加起来,让流水线起到应有的作用,java用Sink来协调相邻Stage之间的调用关系。每个Stage必须实现opWrapSink方法。Sink接口的主要方法如下

方法名 作用
void begin(long size) 开始遍历元素之前调用该方法,通知Sink做好准备
void end() 所有元素遍历完成之后调用,通知Sink没有更多的元素了
boolean cancellationRequested() 是否可以结束操作,可以让短路操作尽早结束
void accept(T t) 遍历元素时调用,接受一个待处理元素,并对元素进行处理

每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个乘放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。对于短路操作,Sink.cancellationRequested()也是必须实现的,比如Stream.findFirst()是短路操作,只要找到一个元素,cancellationRequested()就应该返回true,以便调用者尽快结束查找。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法

map方法的主要实现

public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
    Objects.requireNonNull(mapper);
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
            return new Sink.ChainedReference<P_OUT, R>(sink) {
                @Override
                public void accept(P_OUT u) {
                    downstream.accept(mapper.apply(u));
                }
            };
        }
    };
}
static abstract class ChainedReference<T, E_OUT> implements Sink<T> {
    protected final Sink<? super E_OUT> downstream;

    public ChainedReference(Sink<? super E_OUT> downstream) {
        this.downstream = Objects.requireNonNull(downstream);
    }

    @Override
    public void begin(long size) {
        downstream.begin(size);
    }

    @Override
    public void end() {
        downstream.end();
    }

    @Override
    public boolean cancellationRequested() {
        return downstream.cancellationRequested();
    }
}
// Stream.sort()方法用到的Sink实现
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
    private ArrayList<T> list;// 存放用于排序的元素
    RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
        super(downstream, comparator);
    }
    @Override
    public void begin(long size) {
        ...
        // 创建一个存放排序元素的列表
        list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
    }
    @Override
    public void end() {
        list.sort(comparator);// 只有元素全部接收之后才能开始排序
        downstream.begin(list.size());
        if (!cancellationWasRequested) {// 下游Sink不包含短路操作
            list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink
        }
        else {// 下游Sink包含短路操作
            for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
                if (downstream.cancellationRequested()) break;
                downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink
            }
        }
        downstream.end();
        list = null;
    }
    @Override
    public void accept(T t) {
        list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
    }
}

多个Sink叠加

多个Stage组成的链路如图所示,那么什么时候出发执行结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。

final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator){
    ...
    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
        wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
        spliterator.forEachRemaining(wrappedSink);// 迭代
        wrappedSink.end();// 通知遍历结束
    }
    ...
}

元空间

永久代的消除

从JDK1.7开始,贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap。符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。但永久代仍然存在于JDK7,并没有完全的移除。在JDK1.8版本中永久带被彻底移除。永久代的参数-XX:PermSize和-XX:MaxPermSize也被移除。该参数在JDK1.8使用会有警告

元空间

JDK1.8将类信息存储在元空间中,元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的(取决于内存)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Int Double Long 设置特定的stream类型, 提高性能,增加特定的函数 无存储。stream不是一...
    patrick002阅读 1,232评论 0 0
  • 为了支持函数式编程,Java 8引入了Lambda表达式.在Java 8中采用的是内部类来实现Lambda表达式....
    禅与计算机程序设计艺术阅读 937评论 2 10
  • 简介 λ表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或...
    东方灵龙阅读 633评论 0 2
  • 自从我踏上三尺讲台的那天起,我一直站在教育工作的前沿。在工作中兢兢业业,尽职尽责,始终用爱心去感动学生,让...
    bqxlhy阅读 171评论 2 2