前言

毫无疑问,Java 8是Java自Java 5(发布于2004年)之后的最重要的版本。这个版本包含语言、编译器、库、工具和JVM等方面的十多个新特性。
本文针对Java开发者经常遇到的几类问题来对Java8新特性进行讲解:

  • 语言
  • 编译器
  • 工具
  • Java虚拟机(JVM)

新特性

1.随着大数据的兴起,函数式编程在处理大数据上的优势开始体现,引入了Lambada函数式编程
2.使用Stream彻底改变了集合使用方式:只关注结果,不关心过程
3.新的客户端图形化工具界面库:JavaFX
4.良好设计的日期/时间API
5.增强的并发/并行API
6.Java与JS交互引擎 -nashorn
7.其他特性

Lambda表达式和函数式接口

什么是Lambda表达式

带有参数变量的表达式,是一段可以传递的代码,可以被一次或多次执行。是一种精简的字面写法,其实就是把匿名内部类中“一定”要做的工作省略掉,然后由JVM通过推导把简化的表达式还原。

Lambda表达式(也称为闭包)是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理。
Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
JDK 也提供了大量的内置函数式接口供我们使用,使得 Lambda 表达式的运用更加方便、高效。

Lamdba表达式很简单,那何时使用呢?

需要显示创建函数式接口对象的地方,都可以使用。实际上函数式接口的转换是Lambda表达式唯一能做的事情。即lambda必须和Functional Interface配套使用。主要用于替换以前广泛使用的内部匿名类,各种回调。比如事件响应器、传入Thread类的Runnable等。

Lambda的优点

1、极大的减少代码冗余,同时可读性也好过冗长的匿名内部类
2、与集合类批处理操作结合,实现内部迭代,并充分利用现代多核CPU进行并行计算。之前集合类的迭代都是外部的,即客户代码。而内部迭代意味着由Java类库来进行迭代,而不是客户代码

和匿名内部类的区别

1、在lambda中,this不是指向lambda表达式产生的那个对象,而是它的外部对象
2、Java 编译器编译 Lambda 表达式并将他们转化为类里面的私有函数,它使用 Java 7 中新加的 invokedynamic 指令动态绑定该方法,但每一个匿名内部类编译器会为其创建一个类文件

Lambda表达式语法

(params) -> expression
or
(params) ->{ statements; }

Lambda表达式特点

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

对接口的要求

虽然使用 Lambda 表达式可以对某些接口进行简单的实现,但并不是所有的接口都可以使用 Lambda 表达式来实现。Lambda 规定接口中只能有一个需要被实现的方法,不是规定接口中只能有一个方法

jdk 8 中有另一个新特性:default, 被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。

1、Lambda 基础语法

准备数据

/**
 * 无参数无返回值
**/
@FunctionalInterface
public interface NoReturnNoParam {
    void method();
}

/**
 * 一个参数无返回值
**/
@FunctionalInterface
public interface NoReturnOneParam {
    void method(int a);
}

/**
 * 多个参数无返回值
**/
@FunctionalInterface
public interface NoReturnMultiParam {
    void method(int a, int b);
}

/**
 * 无参数有返回
**/
@FunctionalInterface
public interface ReturnNoParam {
    int method();
}

/**
 * 一个参数有返回值
**/
@FunctionalInterface
public interface ReturnOneParam {
    int method(int a);
}

/**
 * 多个参数有返回值
**/
@FunctionalInterface
public interface ReturnMultiParam {
    int method(int a, int b);
}

实例1

import com.mc.lambda.*;

public class LambdaTest {
    public static void main(String[] args) {

        // 无参数无返回值
        NoReturnNoParam noReturnNoParam = () -> {
            System.out.println("无参数无返回值");
        };
        noReturnNoParam.method();
        

        
        // 一个参数无返回值
        NoReturnOneParam noReturnOneParam = (int a) -> {
            System.out.println("一个参数无返回值,参数为:" + a);
        };
        noReturnOneParam.method(6);
        

        
        // 多个参数无返回值
        NoReturnMultiParam noReturnMultiParam = (int a, int b) -> {
            System.out.println("多个参数无返回值,参数列表为::" + "{" + a +"," + + b +"}");
        };
        noReturnMultiParam.method(6, 8);
        

        
        // 无参数有返回值
        ReturnNoParam returnNoParam = () -> {
            System.out.print("无参数有返回值");
            return "null";
        };
        String test1 = returnNoParam.method();
        System.out.println("返回值:" + test1);
        

        
        // 一个参数有返回值
        ReturnOneParam returnOneParam = (int a) -> {
            System.out.println("一个参数有返回值,参数为:" + a);
            return a+"";
        };
        String test2 = returnOneParam.method(1);
        System.out.println("返回值:" + test2);
        

       
        // 多个参数有返回值
        ReturnMultiParam returnMultiParam = (int a, int b) -> {
            System.out.println("多个参数有返回值,参数列表为:" + "{" + a + "," + b +"}");
            return a+b+"";
        };
        String test3 = returnMultiParam.method(1, 2);
        System.out.println("返回值:" + test3);
    }
}

实例2 简化表达式

 // 简化参数类型,可以不写参数类型,但是必须所有参数都不写
NoReturnMultiParam test1 = (a, b) -> {
    System.out.println("简化参数类型");
};
test1.method(1, 2);



// 简化参数小括号,若只有一个参数则可以省略参数小括号
NoReturnMultiParam test2 = a -> {
    System.out.println("简化参数小括号");
};
test2.method(1);



// 简化方法体大括号,若方法体只有一条语句,则可以省略方法体大括号
NoReturnMultiParam test3 = () -> System.out.println("简化方法体大括号");
test3.method();



// 简化方法体大括号,若方法体只有一条语句,且是 return 语句,则可以省略方法体大括号
NoReturnMultiParam test4 = a -> a+3;
System.out.println(lambda4.method(5));

2、Lambda 表达式引用方法

普通方法的引用

有时候我们不是必须要自己重写某个匿名内部类的方法,我们可以可以利用 lambda表达式的接口快速指向一个已经被实现的方法。
语法

方法归属者::方法名 静态方法的归属者为类名,普通方法归属者为对象

NoReturnMultiParam lambda1 = a -> staticTest(a);
System.out.println(lambda1.method(1));

//lambda2 引用了已经实现的 staticTest方法    类名 :: 静态方法名
ReturnOneParam lambda2 = Test1::staticTest;
System.out.println(lambda2.method(1));

Test1 test = new Test1();

//lambda4 引用了已经实现的 commonTest方法   对象的引用 :: 普通方法名
ReturnOneParam lambda4 = test::commonTest;
System.out.println(lambda4.method(1));

//lambda4 引用了已经实现的 commonTest方法   类名 :: 普通方法名
// 例:ReturnOneParam接口中有个 test方法  参数为int a,int b 将其指向 String的substring
ReturnOneParam lambda5 = String::substring;
System.out.println(lambda5.test("W","w"));


public class Test1{
  	
  	// 静态实现
    public static int staticTest(int a) {
        return a * 2;
    }
    
    // 普通实现
    public int commonTest(int a) {
        return a + 2;
    }
   
}

构造方法的引用

一般我们需要声明接口,该接口作为对象的生成器,通过 类名::new 的方式来实例化对象,然后调用方法返回对象。

interface Product2 {
    Product getProduct();
}
interface Product1 {
    Product getProduct(int id, String name, BigDecimal price);
}

public class Test2 {
    public static void main(String[] args) {
        Product2 t1 = () -> new Item();
        Product pro = t1.getItem();

        Product2 t2 = Item::new;
        Product pro2 = t2.getItem();

        Product1 t3 = Item::new;
        Product pro3 = t3.getItem(001, "外星人", new BigDecimal("9999.99"));
    }
}

lambda 表达式创建线程

我们以往都是通过创建 Thread 对象,然后通过匿名内部类重写 run() 方法,一提到匿名内部类我们就应该想到可以使用 lambda 表达式来简化线程的创建过程。

Thread t = new Thread(() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println("Thread :" + i);
    }
});
t.start();

遍历集合

我们可以调用集合的 public void forEach(Consumer<? super E> action) 方法,通过 lambda 表达式的方式遍历集合中的元素。以下是 Consumer 接口的方法以及遍历集合的操作。Consumer 接口是 jdk 为我们提供的一个函数式接口。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    //....
}
ArrayList<Integer> list = new ArrayList<>();

Collections.addAll(list, 1,2,3,4,5);

//lambda表达式 方法引用
list.forEach(System.out::println);

list.forEach(element -> {
    if (element % 2 == 0) {
        System.out.println(element);
    }
});

删除集合中的某个元素

我们通过public boolean removeIf(Predicate<? super E> filter)方法来删除集合中的某个元素,Predicate 也是 jdk 为我们提供的一个函数式接口,可以简化程序的编写。

ArrayList<Product> lists = new ArrayList<>();
lists.add(new Product(1, "外星人1", new BigDecimal("9999")));
lists.add(new Product(2, "外星人2", new BigDecimal("8888")));
lists.add(new Product(3, "外星人3", new BigDecimal("7777")));
lists.add(new Product(4, "外星人4", new BigDecimal("6666")));
lists.add(new Product(5, "外星人5", new BigDecimal("5555")));

lists.removeIf(ele -> ele.getId() == 7);

//通过 foreach 遍历,查看是否已经删除
lists.forEach(System.out::println);

集合内元素的排序

在以前我们若要为集合内的元素排序,就必须调用 sort 方法,传入比较器匿名内部类重写 compare 方法,我们现在可以使用 lambda 表达式来简化代码。

ArrayList<Product> lists = new ArrayList<>();
lists.add(new Product(1, "外星人1", new BigDecimal("9999")));
lists.add(new Product(2, "外星人2", new BigDecimal("8888")));
lists.add(new Product(3, "外星人3", new BigDecimal("7777")));
lists.add(new Product(4, "外星人4", new BigDecimal("6666")));
lists.add(new Product(5, "外星人5", new BigDecimal("5555")));

list.sort((o1, o2) -> o1.getId() - o2.getId());

System.out.println(list);

Lambda 表达式中的闭包问题

这个问题我们在匿名内部类中也会存在,如果我们把注释放开会报错,告诉我 num 值是 final 不能被改变。这里我们虽然没有标识 num 类型为 final,但是在编译期间虚拟机会帮我们加上 final 修饰关键字。

public class Test {
    public static void main(String[] args) {

        int num = 1;

        Consumer<String> consumer = ele -> {
            System.out.println(num);
        };

        //num = num + 2;
        consumer.accept("李白");
    }
}

函数式接口

@FunctionalInterface修饰函数式接口的,要求注解的接口要有且仅有一个抽象方法。 这个注解往往会和 lambda 表达式一起出现。具体就是说,注解在Inteface上,且interface里只能有一个抽象方法,可以有default方法。

static方法和default方法

Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法。默认方法使得接口有点类似traits,不过要实现的目标不一样。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写

static方法:

java8中为接口新增了一项功能,定义一个或者多个静态方法。用法和普通的static方法一样,例如:

public interface Interface {
    /**
     * 静态方法
     */
    static void staticMethod() {
        System.out.println("static method");
    }
}

注意:实现接口的类或者子接口不会继承接口中的静态方法。

default方法:

java8在接口中新增default方法,是为了在现有的类库中中新增功能而不影响他们的实现类,试想一下,如果不增加默认实现的话,接口的所有实现类都要实现一遍这个方法,这会出现兼容性问题,如果定义了默认实现的话,那么实现类直接调用就可以了,并不需要实现这个方法

public interface Interface {
    /**
     * default方法
     */
    default void print() {
        System.out.println("hello default");
    }
}

注意:如果接口中的默认方法不能满足某个实现类需要,那么实现类可以覆盖默认方法。不用加default关键字,例如:

public class InterfaceImpl implements Interface {
    @Override
    public  void print() {
        System.out.println("hello default 2");
    }
}

在函数式接口的定义中是只允许有一个抽象方法,但是可以有多个static方法和default方法。

Stream API流式编程

是用函数式编程方式在集合类上进行复杂操作的工具,更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作,高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,Stream 会隐式地在内部进行遍历,做出相应的数据转换,而和迭代器又不同的是,Stream 可以并行化操作,借助于 Lambda 表达式,极大的提高编程效率和程序可读性。

Stream的操作步骤

一、创建Stream
从一个数据源,如集合、数组中获取流。
二、中间操作
一个操作的中间链,对数据源的数据进行操作。
三、终止操作
一个终止操作,执行中间操作链,并产生结果。
要注意的是,对流的操作完成后需要进行关闭操作(或者用JAVA7的try-with-resources)。

java8自带常用的函数式接口
Predicate boolean test(T t) 传入一个参数返回boolean值
Consumer void accept(T t) 传入一个参数,无返回值
Function<T,R> R apply(T t) 传入一个参数,返回另一个类型

准备数据

//A类商品
private static List<Product> AClub = Arrays.asList(
        new Product(001,"A1",new BigDecimal("9999")),
        new Product(002,"A2",new BigDecimal("8888")),
        new Product(003,"A3",new BigDecimal("7777")),
        new Product(004,"A4",new BigDecimal("6666"))
);
//B类商品
private static List<Product> BClub = Arrays.asList(
        new Product(005,"B1",new BigDecimal("9999")),
        new Product(006,"B2",new BigDecimal("8888")),
        new Product(007,"B3",new BigDecimal("7777")),
        new Product(008,"B4",new BigDecimal("6666"))
);
//C类商品
private static List<Product> CClub = Arrays.asList(
        new Product(009,"C1",new BigDecimal("9999")),
        new Product(010,"C2",new BigDecimal("8888")),
        new Product(011,"C3",new BigDecimal("7777")),
        new Product(012,"C4",new BigDecimal("6666"))
);

private static List<List<Product>> allClub = new ArrayList<>();
allClub.add(AClub);
allClub.add(BClub);
allClub.add(CClub);

常用的stream三种创建方式

集合 Collection.stream()
静态方法 Stream.of()
数组 Arrays.stream()

//1.集合
Stream<Student> stream = basketballClub.stream();
//2.静态方法
Stream<String> stream2 = Stream.of("a", "b", "c");
//3.数组
String[] arr = {"a","b","c"};
Stream<String> stream3 = Arrays.stream(arr);

Stream的终止操作

foreach(Consumer c) 遍历操作
collect(Collector) 将流转化为其他形式
max(Comparator) 返回流中最大值
min(Comparator) 返回流中最小值
count 返回流中元素综述

Collectors 具体方法

toList List 把流中元素收集到List
toSet Set 把流中元素收集到Set
toCollection Coolection 把流中元素收集到Collection中
groupingBy Map<K,List> 根据K属性对流进行分组
partitioningBy Map<boolean, List> 根据boolean值进行分组

//此处只是演示 此类需求直接用List构造器即可
List<Student> collect = computerClub.stream().collect(Collectors.toList());
Set<Student> collect1 = pingpongClub.stream().collect(Collectors.toSet());
//注意key必须是唯一的 如果不是唯一的会报错而不是像普通map那样覆盖
Map<String, String> collect2 = pingpongClub.stream()
        .collect(Collectors.toMap(Student::getIdNum, Student::getName));
//分组 类似于数据库中的group by
Map<String, List<Student>> collect3 = pingpongClub.stream()
        .collect(Collectors.groupingBy(Student::getClassNum));
//字符串拼接 第一个参数是分隔符 第二个参数是前缀 第三个参数是后缀
String collect4 = pingpongClub.stream().map(Student::getName).collect(Collectors.joining(",", "【", "】"));
//【小u,小i,小m,小n】
//三个俱乐部符合年龄要求的按照班级分组
Map<String, List<Student>> collect5 = Stream.of(basketballClub, pingpongClub, computerClub)
        .flatMap(e -> e.stream().filter(s -> s.getAge() < 17))
        .collect(Collectors.groupingBy(Student::getClassNum));
//按照是否年龄>16进行分组 key为true和false
ConcurrentMap<Boolean, List<Student>> collect6 = Stream.of(basketballClub, pingpongClub, computerClub)
        .flatMap(Collection::stream)
        .collect(Collectors.groupingByConcurrent(s -> s.getAge() > 16));

Stream的中间操作

filter(Predicate) 筛选流中某些元素

//筛选1501班的学生
computerClub.stream().filter(e -> e.getClassNum().equals("1501")).forEach(System.out::println);
//筛选年龄大于15的学生
List<Student> collect = computerClub.stream().filter(e -> e.getAge() > 15).collect(Collectors.toList());

map(Function f) 接收流中元素,并且将其映射成为新元素

//篮球俱乐部所有成员名 + 暂时住上商标^_^,并且获取所有队员名
List<String> collect1 = basketballClub.stream()
        .map(e -> e.getName() + "^_^")
        .collect(Collectors.toList());
collect1.forEach(System.out::println);
//小c^_^^_^
//小s^_^^_^
//小d^_^^_^
//小y^_^^_^

flatMap(Function f) 将所有流中的元素并到一起连接成一个流

//获取年龄大于15的所有俱乐部成员
List<Student> collect2 = Stream.of(basketballClub, computerClub, pingpongClub)
        .flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
        .collect(Collectors.toList());
collect2.forEach(System.out::println);

//用双层list获取所有年龄大于15的俱乐部成员
List<Student> collect3 = allClubStu.stream()
        .flatMap(e -> e.stream().filter(s -> s.getAge() > 15))
        .collect(Collectors.toList());
collect3.forEach(System.out::println);

peek(Consumer c) 获取流中元素,操作流中元素,与foreach不同的是不会截断流,可继续操作流

//篮球俱乐部所有成员名 + 赞助商商标^_^,并且获取所有队员详细内容
List<Student> collect = basketballClub.stream()
        .peek(e -> e.setName(e.getName() + "^_^"))
        .collect(Collectors.toList());
collect.forEach(System.out::println);
//Student{idNum='2015134012', name='小c^_^', age=13, classNum='1503'}
//Student{idNum='2015134013', name='小s^_^', age=14, classNum='1503'}
//Student{idNum='2015134015', name='小d^_^', age=15, classNum='1504'}
//Student{idNum='2015134018', name='小y^_^', age=16, classNum='1505'}

distinct() 通过流所生成元素的equals和hashCode去重
limit(long val) 截断流,取流中前val个元素
sorted(Comparator) 产生一个新流,按照比较器规则排序
sorted() 产生一个新流,按照自然顺序排序

List<String> list = Arrays.asList("b","b","c","a");
list.forEach(System.out::print); //bbca
List<String> collect = list.stream().distinct().sorted().collect(Collectors.toList());
collect.forEach(System.out::print);//abc
//获取list中排序后的top2 即截断取前两个
List<String> collect1 = list.stream().distinct().sorted().limit(2).collect(Collectors.toList());
collect1.forEach(System.out::print);//ab

skip(long n) 是一个跳过前 n 个元素的中间流操作

当 n < 0 时直接抛出了 IllegalArgumentException 异常;
当 n=0 时,打印出123456;
当 n=3 时,打印了 4, 5 ,6 ;
当n>6时,打印为空;
也就是说 skip(long n) 方法跳过前 n (非负)个元素,返回剩下的流。

并行API

在Java7之前,并行处理数据基本是通过多线程来解决

  • 将数据分成部分
  • 给每个子部分分配一个子线程
  • 等待所有的子线程全部结束
  • 合并子线程

这样的并行数据处理不稳定、易出错,在Java8中Stream接口应用分支/合并框架。将一个数据内容分成多个数据块,并用不同的线程分别处理每个数据块流,Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。

使用方法

通常编写并行代码很难而且容易出错,,但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序 。
调用Stream的parallel()方法
调用Collection的parallelStream()方法
parallel() 与 sequential() 可在并行流与顺序流之间切换

使用并行的建议

尽量使用基本类型的流 IntStream, LongStream, and DoubleStream
有些操作使用并发流的性能会比顺序流的性能更差,比如limit,findFirst,依赖元素顺序的操作在并发流中是极其消耗性能的.findAny的性能就会好很多,因为不依赖顺序
考虑流中计算的性能(Q)和操作的性能(N)的对比, Q表示单个处理所需的时间, N表示需要处理的数量,如果Q的值越大, 使用并发流的性能就会越高
数据量不大时使用并发流,性能得不到提升
考虑数据结构:并发流需要对数据进行分解,不同的数据结构被分解的性能时不一样的
传递给并行流的函数都是线程安全的,无副作用

结语

本文就先介绍这么多,下节再介绍Java8的其他新特性,Date Time API 加强,以及一些工具类。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐