Java8新特性【函数式编程API、新时间日期处理API、Optional容器类】总结

  1. Lambda 表达式

  2. 函数式接口

  3. 方法引用与构造器引用

  4. Stream API

  5. 接口中的默认方法与静态方法

  6. 新时间日期API

  7. 其他新特性

速度更快

代码更少(增加了新的语法 Lambda 表达式

强大的 Stream API

便于并行

最大化减少空指针异常 Optional

其中最为核心的为 Lambda 表达式与Stream API

1、Lambda表达式

1.1什么是Lambda表达式

Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

面向对象思想需要关注用什么对象完成什么事情。而函数式编程思想就类似于我们数学中的函数,它主要关注的是对数据进行了什么操作。

1.2从匿名类到 Lambda 的转换

我们在创建线程并启动时可以使用匿名内部类的写法:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("lambda111");
    }
}).start();

可以使用Lambda的格式对其进行修改。修改后如下:

new Thread(()->{
    System.out.println("lambda222");
}).start();
1.3Lambda表达式语法

Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。

它将 Lambda 分为两个部分:

**左侧:**指定了 Lambda 表达式需要的所有参数

**右侧:**指定了 Lambda 体,即 Lambda 表达式要执行的功能

(参数列表)->{代码}

image-20211130222932677

类型推断

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,

这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。

省略规则

  • 参数类型可以省略
  • 方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
  • 方法只有一个参数时小括号可以省略

练习

1.现有方法定义如下,其中IntBinaryOperator是一个接口。先使用匿名内部类的写法调用该方法。

    public static int calculateNum(IntBinaryOperator operator){
        int a = 10;
        int b = 20;
        return operator.applyAsInt(a, b);
    }

    public static void main(String[] args) {
        int i = calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        });
        System.out.println(i);
    }

Lambda写法:

    public static void main(String[] args) {
        int i = calculateNum((int left, int right)->{
            return left + right;
        });
        System.out.println(i);
    }

2.现有方法定义如下,其中IntPredicate是一个接口。先使用匿名内部类的写法调用该方法。

    public static void printNum(IntPredicate predicate){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            if(predicate.test(i)){
                System.out.println(i);
            }
        }
    }
    public static void main(String[] args) {
        printNum(new IntPredicate() {
            @Override
            public boolean test(int value) {
                return value%2==0;
            }
        });
    }

Lambda写法:

    public static void main(String[] args) {
        printNum((int value)-> {
            return value%2==0;
        });
    }

3.现有方法定义如下,其中Function是一个接口。先使用匿名内部类的写法调用该方法。

    public static <R> R typeConver(Function<String,R> function){
        String str = "1235";
        R result = function.apply(str);
        return result;
    }
    public static void main(String[] args) {
        Integer result = typeConver(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return Integer.valueOf(s);
            }
        });
        System.out.println(result);
    }

Lambda写法:

        Integer result = typeConver((String s)->{
            return Integer.valueOf(s);
        });
        System.out.println(result);

4.现有方法定义如下,其中IntConsumer是一个接口。先使用匿名内部类的写法调用该方法。

    public static void foreachArr(IntConsumer consumer){
        int[] arr = {1,2,3,4,5,6,7,8,9,10};
        for (int i : arr) {
            consumer.accept(i);
        }
    }
    public static void main(String[] args) {
        foreachArr(new IntConsumer() {
            @Override
            public void accept(int value) {
                System.out.println(value);
            }
        });
    }

Lambda写法:

    public static void main(String[] args) {
        foreachArr((int value)->{
            System.out.println(value);
        });
    }

2、函数式接口

2.1什么是函数式接口

只包含一个抽象方法的接口,称为函数式接口。 你可以通过 Lambda 表达式来创建该接口的对象。

(若 Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。

我们可以在任意函数式接口上使用 @FunctionalInterface 注解,

这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。

2.2自定义函数式接口
image-20211130223818095
2.3内置核心函数式接口
image-20211130224434284
2.4接口中常用的默认方法
  • and

    我们在使用Predicate接口时候可能需要进行判断条件的拼接。而and方法相当于是使用&&来拼接两个判断条件

    例如:

    打印作家中年龄大于17并且姓名的长度大于1的作家。

            List<Author> authors = getAuthors();
            Stream<Author> authorStream = authors.stream();
            authorStream.filter(new Predicate<Author>() {
                @Override
                public boolean test(Author author) {
                    return author.getAge()>17;
                }
            }.and(new Predicate<Author>() {
                @Override
                public boolean test(Author author) {
                    return author.getName().length()>1;
                }
            })).forEach(author -> System.out.println(author));
    
  • or

    我们在使用Predicate接口时候可能需要进行判断条件的拼接。而or方法相当于是使用||来拼接两个判断条件。

    例如:

    打印作家中年龄大于17或者姓名的长度小于2的作家。

    //        打印作家中年龄大于17或者姓名的长度小于2的作家。
            List<Author> authors = getAuthors();
            authors.stream()
                    .filter(new Predicate<Author>() {
                        @Override
                        public boolean test(Author author) {
                            return author.getAge()>17;
                        }
                    }.or(new Predicate<Author>() {
                        @Override
                        public boolean test(Author author) {
                            return author.getName().length()<2;
                        }
                    })).forEach(author -> System.out.println(author.getName()));
    
  • negate

    Predicate接口中的方法。negate方法相当于是在判断添加前面加了个! 表示取反

    例如:

    打印作家中年龄不大于17的作家。

    //        打印作家中年龄不大于17的作家。
            List<Author> authors = getAuthors();
            authors.stream()
                    .filter(new Predicate<Author>() {
                        @Override
                        public boolean test(Author author) {
                            return author.getAge()>17;
                        }
                    }.negate()).forEach(author -> System.out.println(author.getAge()));
    

3、方法引用与构造器引用

3.1 推荐用法

​ 我们在使用lambda时不需要考虑什么时候用方法引用,用哪种方法引用,方法引用的格式是什么。我们只需要在写完lambda方法发现方法体只有一行代码,并且是方法的调用时使用快捷键尝试是否能够转换成方法引用即可。

​ 当我们方法引用使用的多了慢慢的也可以直接写出方法引用。

3.2 基本格式

​ 类名或者对象名::方法名

3.3 语法详解(了解)
3.3.1引用类的静态方法

​ 其实就是引用类的静态方法

格式

类名::方法名

使用前提

​ 如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的静态方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个静态方法中,这个时候我们就可以引用类的静态方法。

例如:

如下代码就可以用方法引用进行简化

        List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        
        authorStream.map(author -> author.getAge())
                .map(age->String.valueOf(age));

注意,如果我们所重写的方法是没有参数的,调用的方法也是没有参数的也相当于符合以上规则。

优化后如下:

        List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();

        authorStream.map(author -> author.getAge())
                .map(String::valueOf);
3.3.2 引用对象的实例方法

格式

对象名::方法名

使用前提

​ 如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个对象的成员方法,并且我们把要重写的抽象方法中所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用对象的实例方法

例如:

        List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream.map(author -> author.getName())
                .forEach(name->sb.append(name));

优化后:

        List<Author> authors = getAuthors();

        Stream<Author> authorStream = authors.stream();
        StringBuilder sb = new StringBuilder();
        authorStream.map(author -> author.getName())
                .forEach(sb::append);
3.3.4 引用类的实例方法

格式

类名::方法名

使用前提

​ 如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了第一个参数的成员方法,并且我们把要重写的抽象方法中剩余的所有的参数都按照顺序传入了这个成员方法中,这个时候我们就可以引用类的实例方法。

例如:

    interface UseString{
        String use(String str,int start,int length);
    }

    public static String subAuthorName(String str, UseString useString){
        int start = 0;
        int length = 1;
        return useString.use(str,start,length);
    }
    public static void main(String[] args) {

        subAuthorName("666", new UseString() {
            @Override
            public String use(String str, int start, int length) {
                return str.substring(start,length);
            }
        });

	}

优化后如下:

    public static void main(String[] args) {

        subAuthorName("666", String::substring);

    }
3.3.5 构造器引用

​ 如果方法体中的一行代码是构造器的话就可以使用构造器引用。

格式

类名::new

使用前提

​ 如果我们在重写方法的时候,方法体中只有一行代码,并且这行代码是调用了某个类的构造方法,并且我们把要重写的抽象方法中的所有的参数都按照顺序传入了这个构造方法中,这个时候我们就可以引用构造器。

例如:

        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(name->new StringBuilder(name))
                .map(sb->sb.append("-666").toString())
                .forEach(str-> System.out.println(str));

优化后:

        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getName())
                .map(StringBuilder::new)
                .map(sb->sb.append("-666").toString())
                .forEach(str-> System.out.println(str));

4、Stream API

Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API(java.util.stream.*)

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。

4.1什么是Stream

**流(Stream) 到底是什么呢?**是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

“集合讲的是数据,流讲的是计算!”

注意:

①Stream 自己不会存储元素。

②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。

③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

4.2Stream操作的三个步骤
image-20211130225158388

数据中准备

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode//用于后期的去重使用
public class Author {
    //id
    private Long id;
    //姓名
    private String name;
    //年龄
    private Integer age;
    //简介
    private String intro;
    //作品
    private List<Book> books;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode//用于后期的去重使用
public class Book {
    //id
    private Long id;
    //书名
    private String name;

    //分类
    private String category;

    //评分
    private Integer score;

    //简介
    private String intro;

}
    private static List<Author> getAuthors() {
        //数据初始化
        Author author = new Author(1L,"蒙多",33,"一个从菜刀中明悟哲理的祖安人",null);
        Author author2 = new Author(2L,"亚拉索",15,"狂风也追逐不上他的思考速度",null);
        Author author3 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);
        Author author4 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);

        //书籍列表
        List<Book> books1 = new ArrayList<>();
        List<Book> books2 = new ArrayList<>();
        List<Book> books3 = new ArrayList<>();

        books1.add(new Book(1L,"刀的两侧是光明与黑暗","哲学,爱情",88,"用一把刀划分了爱恨"));
        books1.add(new Book(2L,"一个人不能死在同一把刀下","个人成长,爱情",99,"讲述如何从失败中明悟真理"));

        books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
        books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
        books2.add(new Book(4L,"吹或不吹","爱情,个人传记",56,"一个哲学家的恋爱观注定很难把他所在的时代理解"));

        books3.add(new Book(5L,"你的剑就是我的剑","爱情",56,"无法想象一个武者能对他的伴侣这么的宽容"));
        books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
        books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));

        author.setBooks(books1);
        author2.setBooks(books2);
        author3.setBooks(books3);
        author4.setBooks(books3);

        List<Author> authorList = new ArrayList<>(Arrays.asList(author,author2,author3,author4));
        return authorList;
    }
4.2.1创建Stream

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

default Stream stream() : 返回一个顺序流

default Stream parallelStream() : 返回一个并行流

由值创建流:Stream.of

可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。

Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);

单列集合: 集合对象.stream()

        List<Author> authors = getAuthors();
		Stream<Author> stream = authors.stream();

数组:Arrays.stream(数组)或者使用Stream.of来创建

        Integer[] arr = {1,2,3,4,5};
        Stream<Integer> stream = Arrays.stream(arr);
        Stream<Integer> stream2 = Stream.of(arr);

双列集合:转换成单列集合后再创建

        Map<String,Integer> map = new HashMap<>();
        map.put("蜡笔小新",19);
        map.put("黑子",17);
        map.put("日向翔阳",16);

        Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

由函数创建流:创建无限流

可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。

迭代 :public static Stream iterate(final T seed, final UnaryOperator f)

生成:public static Stream generate(Supplier s)

创建无限流
//迭代
Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2).limit(10);
stream3.forEach(System.out::println);

//生成
Stream<Double> stream4 = Stream.generate(Math::random).limit(5);
stream4.forEach(System.out::println);
4.2.2Stream的中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理

而在终止操作时一次性全部处理,称为“惰性求值”

1)筛选与切片

filter(Predicate p)

接收 Lambda ,可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

例如:

​ 打印所有姓名长度大于1的作家的姓名

        List<Author> authors = getAuthors();
        authors.stream()
                .filter(author -> author.getName().length()>1)
                .forEach(author -> System.out.println(author.getName()));

distinct()

筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素

例如:

​ 打印所有作家的姓名,并且要求其中不能有重复元素。

        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .forEach(author -> System.out.println(author.getName()));

注意:distinct方法是依赖Object的equals方法来判断是否是相同对象的。所以需要注意重写equals方法。

limit(long maxSize)

可以设置流的最大长度,超出的部分将被抛弃。

例如:

​ 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名。

        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted()
                .limit(2)
                .forEach(author -> System.out.println(author.getName()));

skip(long n)

​ 跳过流中的前n个元素,返回剩下的元素,若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补

例如:

​ 打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。

//        打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。
        List<Author> authors = getAuthors();
        authors.stream()
                .distinct()
                .sorted()
                .skip(1)
                .forEach(author -> System.out.println(author.getName()));
2)映射

map(Function f)

接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

例如:

​ 打印所有作家的姓名

        List<Author> authors = getAuthors();

        authors
                .stream()
                .map(author -> author.getName())
                .forEach(name->System.out.println(name));
//        打印所有作家的姓名
        List<Author> authors = getAuthors();

//        authors.stream()
//                .map(author -> author.getName())
//                .forEach(s -> System.out.println(s));

        authors.stream()
                .map(author -> author.getAge())
                .map(age->age+10)
                .forEach(age-> System.out.println(age));

flatMap(Function f)

​ map只能把一个对象转换成另一个对象来作为流中的元素。而flatMap可以把一个对象转换成多个对象作为流中的元素。

例一:

​ 打印所有书籍的名字。要求对重复的元素进行去重。

//        打印所有书籍的名字。要求对重复的元素进行去重。
        List<Author> authors = getAuthors();

        authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .forEach(book -> System.out.println(book.getName()));

例二:

​ 打印现有数据的所有分类。要求对分类进行去重。不能出现这种格式:哲学,爱情

//        打印现有数据的所有分类。要求对分类进行去重。不能出现这种格式:哲学,爱情     爱情
        List<Author> authors = getAuthors();
        authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
                .distinct()
                .forEach(category-> System.out.println(category));

mapToDouble(ToDoubleFunction f)

接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。

mapToInt(ToIntFunction f)

接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。

mapToLong(ToLongFunction f)

接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。

3)排序

sorted()

产生一个新流,其中按自然顺序排序

sorted(Comparator comp)

产生一个新流,其中按比较器顺序排序

例如:

​ 对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。

        List<Author> authors = getAuthors();
//        对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
        authors.stream()
                .distinct()
                .sorted()
                .forEach(author -> System.out.println(author.getAge()));
        List<Author> authors = getAuthors();
//        对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
        authors.stream()
                .distinct()
                .sorted((o1, o2) -> o2.getAge()-o1.getAge())
                .forEach(author -> System.out.println(author.getAge()));

注意:如果调用空参的sorted()方法,需要流中的元素是实现了Comparable。

4.2.3Stream的终止操作

终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。

1)查找与匹配

allMatch(Predicate p)

检查是否匹配所有元素,都符合结果为true,否则结果为false。

例子:

​ 判断是否所有的作家都是成年人

//        判断是否所有的作家都是成年人
        List<Author> authors = getAuthors();
        boolean flag = authors.stream()
                .allMatch(author -> author.getAge() >= 18);
        System.out.println(flag);

anyMatchPredicate p)

检查是否至少匹配一个元素

例子:

​ 判断是否有年龄在29以上的作家

//        判断是否有年龄在29以上的作家
        List<Author> authors = getAuthors();
        boolean flag = authors.stream()
                .anyMatch(author -> author.getAge() > 29);
        System.out.println(flag);

noneMatch(Predicate p)

检查是否没有匹配所有元素 ,判断流中的元素是否都不符合匹配条件。如果都不符合结果为true,否则结果为false

例子:

​ 判断作家是否都没有超过100岁的。

//        判断作家是否都没有超过100岁的。
        List<Author> authors = getAuthors();

        boolean b = authors.stream()
                .noneMatch(author -> author.getAge() > 100);

        System.out.println(b);

findFirst()

返回第一个元素

例子:

​ 获取一个年龄最小的作家,并输出他的姓名。

//        获取一个年龄最小的作家,并输出他的姓名。
        List<Author> authors = getAuthors();
        Optional<Author> first = authors.stream()
                .sorted((o1, o2) -> o1.getAge() - o2.getAge())
                .findFirst();

        first.ifPresent(author -> System.out.println(author.getName()));

findAny()

返回当前流中的任意一个元素,该方法没有办法保证获取的一定是流中的第一个元素。

例子:

​ 获取任意一个年龄大于18的作家,如果存在就输出他的名字

//        获取任意一个年龄大于18的作家,如果存在就输出他的名字
        List<Author> authors = getAuthors();
        Optional<Author> optionalAuthor = authors.stream()
                .filter(author -> author.getAge()>18)
                .findAny();

        optionalAuthor.ifPresent(author -> System.out.println(author.getName()));

count()

返回流中元素总数

例子:

​ 打印这些作家的所出书籍的数目,注意删除重复元素。

//        打印这些作家的所出书籍的数目,注意删除重复元素。
        List<Author> authors = getAuthors();

        long count = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .distinct()
                .count();
        System.out.println(count);

max(Comparator c)

返回流中最大值

min(Comparator c)

返回流中最小值

例子:

​ 分别获取这些作家的所出书籍的最高分和最低分并打印。

//        分别获取这些作家的所出书籍的最高分和最低分并打印。
        //Stream<Author>  -> Stream<Book> ->Stream<Integer>  ->求值

        List<Author> authors = getAuthors();
        Optional<Integer> max = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .max((score1, score2) -> score1 - score2);

        Optional<Integer> min = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .map(book -> book.getScore())
                .min((score1, score2) -> score1 - score2);
        System.out.println(max.get());
        System.out.println(min.get());

forEach(Consumer c)

内部迭代(使用 Collection 接口需要用户去做代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了)

例子:

​ 输出所有作家的名字

//        输出所有作家的名字
        List<Author> authors = getAuthors();

        authors.stream()
                .map(author -> author.getName())
                .distinct()
                .forEach(name-> System.out.println(name));

2)归约

reduce(T iden, BinaryOperator b)

可以将流中元素反复结合起来,得到一个值。 返回 T

reduce(BinaryOperator b)

可以将流中元素反复结合起来,得到一个值。返回 Optional

备注:map 和 reduce 的连接通常称为map-reduce 模式,因 Google 用它来进行网络搜索而出名。

例子:

​ 使用reduce求所有作者年龄的和

//        使用reduce求所有作者年龄的和
        List<Author> authors = getAuthors();
        Integer sum = authors.stream()
                .distinct()
                .map(author -> author.getAge())
                .reduce(0, (result, element) -> result + element);
        System.out.println(sum);

​ 使用reduce求所有作者中年龄的最大值

//        使用reduce求所有作者中年龄的最大值
        List<Author> authors = getAuthors();
        Integer max = authors.stream()
                .map(author -> author.getAge())
                .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result);

        System.out.println(max);

​ 使用reduce求所有作者中年龄的最小值

//        使用reduce求所有作者中年龄的最小值
        List<Author> authors = getAuthors();
        Integer min = authors.stream()
                .map(author -> author.getAge())
                .reduce(Integer.MAX_VALUE, (result, element) -> result > element ? element : result);
        System.out.println(min);

​ 如果用一个参数的重载方法去求最小值代码如下:

        //        使用reduce求所有作者中年龄的最小值
        List<Author> authors = getAuthors();
        Optional<Integer> minOptional = authors.stream()
                .map(author -> author.getAge())
                .reduce((result, element) -> result > element ? element : result);
        minOptional.ifPresent(age-> System.out.println(age));
3)收集

collect(Collector c)

将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法

例子:

​ 获取一个存放所有作者名字的List集合。

//        获取一个存放所有作者名字的List集合。
        List<Author> authors = getAuthors();
        List<String> nameList = authors.stream()
                .map(author -> author.getName())
                .collect(Collectors.toList());
        System.out.println(nameList);

​ 获取一个所有书名的Set集合。

//        获取一个所有书名的Set集合。
        List<Author> authors = getAuthors();
        Set<Book> books = authors.stream()
                .flatMap(author -> author.getBooks().stream())
                .collect(Collectors.toSet());

        System.out.println(books);

​ 获取一个Map集合,map的key为作者名,value为List

//        获取一个Map集合,map的key为作者名,value为List<Book>
        List<Author> authors = getAuthors();

        Map<String, List<Book>> map = authors.stream()
                .distinct()
                .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));

        System.out.println(map);

Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下:

image-20211130230746001
4.3高级用法

基本数据类型优化

​ 我们之前用到的很多Stream的方法由于都使用了泛型。所以涉及到的参数和返回值都是引用数据类型。即使我们操作的是整数小数,但是实际用的都是他们的包装类。**JDK5中引入的自动装箱和自动拆箱让我们在使用对应的包装类时就好像使用基本数据类型一样方便。但是你一定要知道装箱和拆箱肯定是要消耗时间的。**虽然这个时间消耗很下。但是在大量的数据不断的重复装箱拆箱的时候,你就不能无视这个时间损耗了。所以为了让我们能够对这部分的时间消耗进行优化。Stream还提供了很多专门针对基本数据类型的方法。

例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。

    private static void test27() {
        List<Author> authors = getAuthors();
        authors.stream()
                .map(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age->age>18)
                .map(age->age+2)
                .forEach(System.out::println);

        authors.stream()
                .mapToInt(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age->age>18)
                .map(age->age+2)
                .forEach(System.out::println);
    }
4.4并行流与串行流

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

**Fork/Join 框架:**就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。

image-20211130231108158

Fork/Join 框架与传统线程池的区别

采用 “工作窃取”模式(work-stealing):

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。

public class ForkJoinCalculate extends RecursiveTask<Long> {

    /**
     *
     */
    private static final long serialVersionUID = 13475679780L;

    private long start;
    private long end;

    private static final long THRESHOLD = 10000L; //临界值

    public ForkJoinCalculate(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long length = end - start;

        if (length <= THRESHOLD) {
            long sum = 0;

            for (long i = start; i <= end; i++) {
                sum += i;
            }

            return sum;
        } else {
            long middle = (start + end) / 2;

            ForkJoinCalculate left = new ForkJoinCalculate(start, middle);
            left.fork(); //拆分,并将该子任务压入线程队列

            ForkJoinCalculate right = new ForkJoinCalculate(middle + 1, end);
            right.fork();

            return left.join() + right.join();
        }

    }

}
--------------------------------------------------------------------------
public class TestForkJoin {
	
	@Test
	public void test1(){
		long start = System.currentTimeMillis();
		
		ForkJoinPool pool = new ForkJoinPool();
		ForkJoinTask<Long> task = new ForkJoinCalculate(0L, 100000L);
		
		long sum = pool.invoke(task);
		System.out.println(sum);
		
		long end = System.currentTimeMillis();
		
		System.out.println("耗费的时间为: " + (end - start)); //112-1953-1988-2654-2647-20663-113808
	}
	
	@Test
	public void test2(){
		long start = System.currentTimeMillis();
		
		long sum = 0L;
		
		for (long i = 0L; i <= 10000000000L; i++) {
			sum += i;
		}
		
		System.out.println(sum);
		
		long end = System.currentTimeMillis();
		
		System.out.println("耗费的时间为: " + (end - start)); //34-3174-3132-4227-4223-31583
	}
	
	@Test
	public void test3(){
		long start = System.currentTimeMillis();
		
		Long sum = LongStream.rangeClosed(0L, 10000000000L)
							 .parallel()
							 .sum();
		
		System.out.println(sum);
		
		long end = System.currentTimeMillis();
		
		System.out.println("耗费的时间为: " + (end - start)); //2061-2053-2086-18926
	}

}

Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。

Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换。

当流中有大量元素时,我们可以使用并行流去提高操作的效率。其实并行流就是把任务分配给多个线程去完全。如果我们自己去用代码实现的话其实会非常的复杂,并且要求你对并发编程有足够的理解和认识。而如果我们使用Stream的话,我们只需要修改一个方法的调用就可以使用并行流来帮我们实现,从而提高效率。

​ parallel方法可以把串行流转换成并行流。

    private static void test28() {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Integer sum = stream.parallel()
                .peek(new Consumer<Integer>() {
                    @Override
                    public void accept(Integer num) {
                        System.out.println(num+Thread.currentThread().getName());
                    }
                })
                .filter(num -> num > 5)
                .reduce((result, ele) -> result + ele)
                .get();
        System.out.println(sum);
    }

​ 也可以通过parallelStream直接获取并行流对象。

        List<Author> authors = getAuthors();
        authors.parallelStream()
                .map(author -> author.getAge())
                .map(age -> age + 10)
                .filter(age->age>18)
                .map(age->age+2)
                .forEach(System.out::println);

5、新时间日期API

问题引入

SimpleDateFormat存在多线程安全问题

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

		Callable<Date> task = new Callable<Date>() {

			@Override
			public Date call() throws Exception {
				return sdf.parse("20161121");
			}

		};

		ExecutorService pool = Executors.newFixedThreadPool(10);

		List<Future<Date>> results = new ArrayList<>();

		for (int i = 0; i < 10; i++) {
			results.add(pool.submit(task));
		}

		for (Future<Date> future : results) {
			System.out.println(future.get());
		}

		pool.shutdown();

ThreadLocal解决多线程安全问题

//解决多线程安全问题
		Callable<Date> task = new Callable<Date>() {

			@Override
			public Date call() throws Exception {
				return DateFormatThreadLocal.convert("20161121");
			}

		};

		ExecutorService pool = Executors.newFixedThreadPool(10);

		List<Future<Date>> results = new ArrayList<>();

		for (int i = 0; i < 10; i++) {
			results.add(pool.submit(task));
		}

		for (Future<Date> future : results) {
			System.out.println(future.get());
		}

		pool.shutdown();
	private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
		
		protected DateFormat initialValue(){
			return new SimpleDateFormat("yyyyMMdd");
		}
		
	};
	
	public static final Date convert(String source) throws ParseException{
		return df.get().parse(source);
	}

java8中日期处理的转换DateTimeFormatter解决多线程安全问题

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");
		
		Callable<LocalDate> task = new Callable<LocalDate>() {

			@Override
			public LocalDate call() throws Exception {
				LocalDate ld = LocalDate.parse("20161121", dtf);
				return ld;
			}
			
		};

		ExecutorService pool = Executors.newFixedThreadPool(10);
		
		List<Future<LocalDate>> results = new ArrayList<>();
		
		for (int i = 0; i < 10; i++) {
			results.add(pool.submit(task));
		}
		
		for (Future<LocalDate> future : results) {
			System.out.println(future.get());
		}
		
		pool.shutdown();
5.1LocalDate、LocalTime、LocalDateTime

类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。

它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息

注:ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法

image-20211130231439542
public void test1(){
    LocalDateTime ldt = LocalDateTime.now();
    System.out.println(ldt);

    LocalDateTime ld2 = LocalDateTime.of(2016, 11, 21, 10, 10, 10);
    System.out.println(ld2);

    LocalDateTime ldt3 = ld2.plusYears(20);
    System.out.println(ldt3);

    LocalDateTime ldt4 = ld2.minusMonths(2);
    System.out.println(ldt4);

    System.out.println(ldt.getYear());
    System.out.println(ldt.getMonthValue());
    System.out.println(ldt.getDayOfMonth());
    System.out.println(ldt.getHour());
    System.out.println(ldt.getMinute());
    System.out.println(ldt.getSecond());
}
5.2Instant 时间戳

用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC[世界标准时间]时区1970年1月1日午夜时分)开始所经历的描述进行运算

public void test2(){
		Instant ins = Instant.now();  //默认使用 UTC 时区
		System.out.println(ins);
		
		OffsetDateTime odt = ins.atOffset(ZoneOffset.ofHours(8));
		System.out.println(odt);
		
		System.out.println(ins.getNano());
		
		Instant ins2 = Instant.ofEpochSecond(5);
		System.out.println(ins2);
	}
5.3Duration 、Period

Duration:用于计算两个“时间”间隔

Period:用于计算两个“日期”间隔

public void test3(){
		Instant ins1 = Instant.now();
		
		System.out.println("--------------------");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
		}
		
		Instant ins2 = Instant.now();
		
		System.out.println("所耗费时间为:" + Duration.between(ins1, ins2));
		
		System.out.println("----------------------------------");
		
		LocalDate ld1 = LocalDate.now();
		LocalDate ld2 = LocalDate.of(2011, 1, 1);
		
		Period pe = Period.between(ld2, ld1);
		System.out.println(pe.getYears());
		System.out.println(pe.getMonths());
		System.out.println(pe.getDays());
	}
5.4TemporalAdjuster 时间校正器

有时我们可能需要获取例如:将日期调整到“下个周日”等操作。

TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。

public void test4(){
	LocalDateTime ldt = LocalDateTime.now();
		System.out.println(ldt);
		
		LocalDateTime ldt2 = ldt.withDayOfMonth(10);
		System.out.println(ldt2);
		
		LocalDateTime ldt3 = ldt.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
		System.out.println(ldt3);
		
		//自定义:下一个工作日
		LocalDateTime ldt5 = ldt.with((l) -> {
			LocalDateTime ldt4 = (LocalDateTime) l;
			
			DayOfWeek dow = ldt4.getDayOfWeek();
			
			if(dow.equals(DayOfWeek.FRIDAY)){
				return ldt4.plusDays(3);
			}else if(dow.equals(DayOfWeek.SATURDAY)){
				return ldt4.plusDays(2);
			}else{
				return ldt4.plusDays(1);
			}
		});
		
		System.out.println(ldt5);
5.5DateTimeFormatter解析与格式化

java.time.format.DateTimeFormatter类:该类提供了三种格式化方法:

预定义的标准格式

语言环境相关的格式

自定义的格式

public void test5(){
//		DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE;
		
		DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E");
		
		LocalDateTime ldt = LocalDateTime.now();
		String strDate = ldt.format(dtf);
		
		System.out.println(strDate);
		
		LocalDateTime newLdt = ldt.parse(strDate, dtf);
		System.out.println(newLdt);

时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDate、ZonedTime、ZonedDateTime。

其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式例如 :Asia/Shanghai 等。

ZoneId:该类中包含了所有的时区信息 ,

getAvailableZoneIds() : 可以获取所有时区时区信息 ,
of(id) : 用指定的时区信息获取ZoneId 对象。

public void test6(){
    Set<String> set = ZoneId.getAvailableZoneIds();
    set.forEach(System.out::println);
}

public void test7(){
    LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
    System.out.println(ldt);

    ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("US/Pacific"));
    System.out.println(zdt);
}
5.6新API与传统时间日期转换
image-20211130232007927

6、接口中可用默认方法与静态方法

1)接口默认方法的”类优先”原则

若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时

  • 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。

  • 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突

2)Java8 中,接口中可添加静态方法

public class TestDefaultInterface {
	
	public static void main(String[] args) {
		SubClass sc = new SubClass();
		System.out.println(sc.getName());
		
		MyInterface.show();
	}

}

public class SubClass /*extends MyClass*/ implements MyFun, MyInterface {

	@Override
	public String getName() {
		return MyInterface.super.getName();
	}

}

public interface MyFun {
	
	default String getName(){
		return "哈哈哈";
	}

}

public interface MyInterface {
	
	default String getName(){
		return "呵呵呵";
	}
	
	public static void show(){
		System.out.println("接口中的静态方法");
	}

}

7、Optional容器类

7.1什么是Optional类

Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。

我们在编写代码的时候出现最多的就是空指针异常。所以在很多情况下我们需要做各种非空的判断。

        Author author = getAuthor();
        if(author!=null){
            System.out.println(author.getName());
        }

尤其是对象中的属性还是一个对象的情况下。这种判断会更多。 而过多的判断语句会让我们的代码显得臃肿不堪。

所以在JDK8中引入了Optional,养成使用Optional的习惯后你可以写出更优雅的代码来避免空指针异常。

并且在很多函数式编程相关的API中也都用到了Optional,如果不会使用Optional也会对函数式编程的学习造成影响。

常用方法

7.2常用方法
1) 创建对象

Optional就好像是包装类,可以把我们的具体数据封装Optional对象内部。然后我们去使用Optional中封装好的方法操作封装进去的数据就可以非常优雅的避免空指针异常。

我们一般使用Optional静态方法ofNullable来把数据封装成一个Optional对象。无论传入的参数是否为null都不会出现问题。

        Author author = getAuthor();
        Optional<Author> authorOptional = Optional.ofNullable(author);

​ 你可能会觉得还要加一行代码来封装数据比较麻烦。但是如果改造下getAuthor方法,让其的返回值就是封装好的Optional的话,我们在使用时就会方便很多。

​ 而且在实际开发中我们的数据很多是从数据库获取的。Mybatis从3.5版本可以也已经支持Optional了。我们可以直接把dao方法的返回值类型定义成Optional类型,MyBastis会自己把数据封装成Optional对象返回。封装的过程也不需要我们自己操作。

​ 如果你确定一个对象不是空的则可以使用Optional静态方法of来把数据封装成Optional对象。

        Author author = new Author();
        Optional<Author> authorOptional = Optional.of(author);

​ 但是一定要注意,如果使用of的时候传入的参数必须不为null。(尝试下传入null会出现什么结果)

​ 如果一个方法的返回值类型是Optional类型。而如果我们经判断发现某次计算得到的返回值为null,这个时候就需要把null封装成Optional对象返回。这时则可以使用Optional静态方法empty来进行封装。

		Optional.empty()

​ 所以最后你觉得哪种方式会更方便呢?ofNullable

2)安全消费值

​ 我们获取到一个Optional对象后肯定需要对其中的数据进行使用。这时候我们可以使用其ifPresent方法对来消费其中的值。这个方法会判断其内封装的数据是否为空,不为空时才会执行具体的消费代码。这样使用起来就更加安全了。

例如,以下写法就优雅的避免了空指针异常。

        Optional<Author> authorOptional = Optional.ofNullable(getAuthor());

        authorOptional.ifPresent(author -> System.out.println(author.getName()));
3)获取值

​ 如果我们想获取值自己进行处理可以使用get方法获取,但是不推荐。因为当Optional内部的数据为空的时候会出现异常。

4)安全获取值

​ 如果我们期望安全的获取值。我们不推荐使用get方法,而是使用Optional提供的以下方法。

  • orElseGet

    获取数据并且设置数据为空时的默认值。如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建对象作为默认值返回。

            Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
            Author author1 = authorOptional.orElseGet(() -> new Author());
    
  • orElseThrow

    获取数据,如果数据不为空就能获取到该数据。如果为空则根据你传入的参数来创建异常抛出。

            Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
            try {
                Author author = authorOptional.orElseThrow((Supplier<Throwable>) () -> new RuntimeException("author为空"));
                System.out.println(author.getName());
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
    
5)过滤

​ 我们可以使用filter方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的Optional对象。

        Optional<Author> authorOptional = Optional.ofNullable(getAuthor());
        authorOptional.filter(author -> author.getAge()>100).ifPresent(author -> System.out.println(author.getName()));

6)判断

​ 我们可以使用isPresent方法进行是否存在数据的判断。如果为空返回值为false,如果不为空,返回值为true。但是这种方式并不能体现Optional的好处,更推荐使用ifPresent方法

        Optional<Author> authorOptional = Optional.ofNullable(getAuthor());

        if (authorOptional.isPresent()) {
            System.out.println(authorOptional.get().getName());
        }
7)数据转换

​ Optional还提供了map可以让我们的对数据进行转换,并且转换得到的数据也还是被Optional包装好的,保证了我们的使用安全。

例如我们想获取作家的书籍集合。

    private static void testMap() {
        Optional<Author> authorOptional = getAuthorOptional();
        Optional<List<Book>> optionalBooks = authorOptional.map(author -> author.getBooks());
        optionalBooks.ifPresent(books -> System.out.println(books));
    }

8、重复注解与类型注解

Java 8对注解处理提供了两点改进:可重复的注解及可用于类型的注解。
请添加图片描述

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值