Java 核心知识总结 Java8 特性
Java 8 的特性包括接口的默认方法和静态方法、Lamdba、Stream、Optional。
Lambda 表达式
🔥什么场景下适合使用 Lambda 表达式
Lambda 表达式是一个匿名函数,可以理解其为一段可以传递的代码。Lambda 语法将代码像数据一样传递,可以代替大部分匿名内部类,使用它可以写出更简洁、更灵活的代码。
当需要启动一个线程去完成任务时,通常会通过 java.lang.Runnable
接口来定义任务内容,并使用 java.lang.Thread
类来启动该线程。代码如下:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start(); // 启动线程
Runnable 接口有唯一的抽象方法 run,它的方法名称、方法参数、方法返回值已经确定,我们只需要关注它的方法体。
Lambda 表达式的语法格式
Lambda 表达式:在 Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->
” , 该操作符被称为 Lambda 操作符
或 箭头操作符
。它将 Lambda 分为两个部分:
- 左侧:指定了 Lambda 表达式需要的参数列表。
- 右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能。
-
无参,无返回值
@Test public void test1(){ // 未使用Lambda表达式 Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hello world!"); } }; r1.run(); // 使用Lambda表达式 Runnable r2 = () -> { System.out.println("Hello world!"); }; r2.run(); }
-
有参数,无返回值
@Test public void test2(){ // 未使用Lambda表达式 Consumer<String> con = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s); } }; con.accept("Hello world!"); // 使用Lambda表达式 // 可以省略参数类型,可以有编译器推断得出,称为类型推断 // 参数只有1个时可省略小括号 Consumer<String> con1 = s -> { System.out.println(s); }; con1.accept("Hello world!"); }
-
有参数,有返回值
@Test public void test5(){ // 未使用Lambda表达式 Comparator<Integer> com1 = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { System.out.println(o1); System.out.println(o2); return o1.compareTo(o2); } }; System.out.println(com1.compare(12,21)); // 使用Lambda表达式 Comparator<Integer> com2 = (o1,o2) -> { System.out.println(o1); System.out.println(o2); return o1.compareTo(o2); }; System.out.println(com2.compare(12,6)); }
-
当 Lambda 体只有一条语句时,可以省略 return 或大括号
@Test public void test6(){ Comparator<Integer> com1 = (o1, o2) -> { return o1.compareTo(o2); }; System.out.println(com1.compare(12, 21)); Comparator<Integer> com2 = (o1, o2) -> o1.compareTo(o2); System.out.println(com2.compare(12, 21)); }
函数式接口
什么是函数式接口?
- 只包含一个抽象方法(Single Abstract Method,简称 SAM)的接口,称为函数式接口。该接口可以包含其他非抽象方法。
- 你可以通过 Lambda 表达式来创建该接口的对象。若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明。
- 我们可以在一个接口上使用
@FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。 - 在
java.util.function
包下定义了Java 8 的丰富的函数式接口
为什么要使用函数式接口?
- Java 从诞生日起就是一直倡导“一切皆对象”,在 Java 里面面向对象编程是一切。但是随着 python、scala 等语言的兴起和新技术的挑战,Java 不得不做出调整以便支持更加广泛的技术要求,即 Java 不但可以支持 OOP 还可以支持 OOF(面向函数编程)。
- Java8 引入了 Lambda 表达式之后,Java 也开始支持函数式编程。
- 在函数式编程语言当中,函数被当做一等公民对待。在将函数作为一等公民的编程语言中,Lambda 表达式的类型是函数。但是在 Java8 中,Lambda 表达式是对象,它们必须依附于一类特别的对象类型——函数式接口。
- 简单的说,在 Java8 中,Lambda 表达式就是一个函数式接口的实例。这就是 Lambda 表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用 Lambda 表达式来表示。
Java 中的函数式接口有哪些?
四大核心函数式接口
函数式接口 | 称谓 | 用途 |
---|---|---|
Consumer<T> | 消费型接口 | 对类型为T的对象应用操作,包含方法:void accept(T t) |
Supplier<T> | 供给型接口 | 返回类型为T的对象,包含方法:T get() |
Function<T, R> | 函数型接口 | 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t) |
Predicate<T> | 判断型接口 | 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t) |
-
其它接口
类型 1:消费型接口
消费型接口的抽象方法特点:有形参,但是返回值类型是 void。
接口名 抽象方法 描述 BiConsumer<T,U> void accept(T t, U u) 接收两个对象用于完成功能 DoubleConsumer void accept(double value) 接收一个double值 IntConsumer void accept(int value) 接收一个int值 LongConsumer void accept(long value) 接收一个long值 ObjDoubleConsumer<T> void accept(T t, double value) 接收一个对象和一个double值 ObjIntConsumer<T> void accept(T t, int value) 接收一个对象和一个int值 ObjLongConsumer<T> void accept(T t, long value) 接收一个对象和一个long值 类型 2:供给型接口
这类接口的抽象方法特点:无参,但是有返回值
接口名 抽象方法 描述 BooleanSupplier boolean getAsBoolean() 返回一个boolean值 DoubleSupplier double getAsDouble() 返回一个double值 IntSupplier int getAsInt() 返回一个int值 LongSupplier long getAsLong() 返回一个long值 类型 3:函数型接口
这类接口的抽象方法特点:既有参数又有返回值
接口名 抽象方法 描述 UnaryOperator<T> T apply(T t) 接收一个T类型对象,返回一个T类型对象结果 DoubleFunction<R> R apply(double value) 接收一个double值,返回一个R类型对象 IntFunction<R> R apply(int value) 接收一个int值,返回一个R类型对象 LongFunction<R> R apply(long value) 接收一个long值,返回一个R类型对象 ToDoubleFunction<T> double applyAsDouble(T value) 接收一个T类型对象,返回一个double ToIntFunction<T> int applyAsInt(T value) 接收一个T类型对象,返回一个int ToLongFunction<T> long applyAsLong(T value) 接收一个T类型对象,返回一个long DoubleToIntFunction int applyAsInt(double value) 接收一个double值,返回一个int结果 DoubleToLongFunction long applyAsLong(double value) 接收一个double值,返回一个long结果 IntToDoubleFunction double applyAsDouble(int value) 接收一个int值,返回一个double结果 IntToLongFunction long applyAsLong(int value) 接收一个int值,返回一个long结果 LongToDoubleFunction double applyAsDouble(long value) 接收一个long值,返回一个double结果 LongToIntFunction int applyAsInt(long value) 接收一个long值,返回一个int结果 DoubleUnaryOperator double applyAsDouble(double operand) 接收一个double值,返回一个double IntUnaryOperator int applyAsInt(int operand) 接收一个int值,返回一个int结果 LongUnaryOperator long applyAsLong(long operand) 接收一个long值,返回一个long结果 BiFunction<T,U,R> R apply(T t, U u) 接收一个T类型和一个U类型对象,返回一个R类型对象结果 BinaryOperator<T> T apply(T t, T u) 接收两个T类型对象,返回一个T类型对象结果 ToDoubleBiFunction<T,U> double applyAsDouble(T t, U u) 接收一个T类型和一个U类型对象,返回一个double ToIntBiFunction<T,U> int applyAsInt(T t, U u) 接收一个T类型和一个U类型对象,返回一个int ToLongBiFunction<T,U> long applyAsLong(T t, U u) 接收一个T类型和一个U类型对象,返回一个long DoubleBinaryOperator double applyAsDouble(double left, double right) 接收两个double值,返回一个double结果 IntBinaryOperator int applyAsInt(int left, int right) 接收两个int值,返回一个int结果 LongBinaryOperator long applyAsLong(long left, long right) 接收两个long值,返回一个long结果 类型 4:判断型接口
这类接口的抽象方法特点:有参,但是返回值类型是 boolean 结果。
接口名 抽象方法 描述 BiPredicate<T,U> boolean test(T t, U u) 接收两个对象 DoublePredicate boolean test(double value) 接收一个double值 IntPredicate boolean test(int value) 接收一个int值 LongPredicate boolean test(long value) 接收一个long值 // 举例 1 public class TestConsumer { public static void main(String[] args) { List<String> list = Arrays.asList("java", "c", "python", "c++", "VB", "C#"); // 遍历 Collection 集合,并将传递给 action 参数的操作代码应用在每一个元素上。 list.forEach(s -> System.out.println(s)); } } // 举例 2 public class TestFunction { public static void main(String[] args) { //使用 Lambda 表达式实现 Function<T,R> 接口,可以实现将一个字符串首字母转为大写的功能。 Function<String,String> fun = s -> s.substring(0,1).toUpperCase() + s.substring(1); System.out.println(fun.apply("hello")); } }
方法引用和构造器引用
Lambda 表达式是可以简化函数式接口的变量或形参赋值的语法。而方法引用和构造器引用是为了简化 Lambda 表达式的。
方法引用
当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用。
方法引用可以看做是 Lambda 表达式深层次的表达。换句话说,方法引用就是 Lambda 表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是 Lambda 表达式的一个语法糖。
方法引用格式
- 格式:使用方法引用操作符 “
::
” 将类(或对象)与方法名分隔开来。 - 如下三种主要使用情况:
- 情况 1:
对象 :: 实例方法名
- 情况 2:
类 :: 静态方法名
- 情况 3:
类 :: 实例方法名
- 情况 1:
方法引用使用前提
- Lambda 体只有一句语句,并且是通过调用一个对象/类现有的方法来完成的。例如:System.out 对象,调用 println() 方法来完成 Lambda 体;Math 类,调用 random() 静态方法来完成 Lambda 体。
- 对形参列表、返回值类型的要求:
-
针对情况 1:函数式接口中的抽象方法 a 在被重写时使用了某一个对象的方法 b。如果方法 a 与方法 b 的形参列表、返回值类型相同,我们可以使用方法 b 实现对方法 a 的重写、替换。
@Test public void test1() { Consumer<String> con1 = str -> System.out.println(str); con1.accept("Hello world!"); PrintStream ps = System.out; Consumer<String> con2 = ps::println; con2.accept("Hello world!"); }
-
针对情况 2:函数式接口中的抽象方法 a 在被重写时使用了某一个类的静态方法 b。如果方法 a 与方法 b 的形参列表、返回值类型相同,我们可以使用方法 b 实现对方法 a 的重写、替换。
@Test public void test3() { Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2); System.out.println(com1.compare(12, 21)); Comparator<Integer> com2 = Integer::compare; System.out.println(com2.compare(12, 21)); }
-
针对情况 3:函数式接口中的抽象方法 a 在被重写时使用了某一个对象的方法 b。如果方法 a 与方法 b 的返回值类型相同,同时方法 a 的形参列表中的参数比方法 b 多 1 个,且方法 a 的第 1 个参数作为方法 b 的调用者,剩余参数匹配(类型相同或满足多态场景也可以)。我们可以使用方法 b 实现对方法 a 的重写、替换。
@Test public void test5() { Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2); System.out.println(com1.compare("abc","abd")); Comparator<String> com2 = String::compareTo; System.out.println(com2.compare("abc","abd")); }
-
构造器引用
当 Lambda 表达式是创建一个对象,并且满足 Lambda 表达式形参,正好是给创建这个对象的构造器的实参列表,就可以使用构造器引用。
格式:类名::new
。
public void test1(){
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());
Supplier<Employee> sup2 = Employee :: new;
System.out.println(sup2.get());
}
class Employee {...}
数组构造引用
当 Lambda 表达式是创建一个数组对象,并且满足 Lambda 表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。
格式:数组类型名::new
。
@Test
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));
Function<Integer,String[]> func2 = String[] :: new;
String[] arr2 = func2.apply(5);
System.out.println(Arrays.toString(arr2));
}
Stream API
什么是 Stream API?
- Java 8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。
- Stream API (
java.util.stream
) 把真正的函数式编程风格引入到 Java 中。这是目前为止对 Java 类库最好的补充,因为 Stream API 可以极大提供 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。 - Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 **使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。**Stream API 提供了一种高效且易于使用的处理数据的方式。
为什么要使用 Stream API?
实际开发中,项目中多数数据源都来自于 MySQL、Oracle 等。但现在数据源可以更多了,有 MongDB,Redis 等,而这些 NoSQL 的数据就需要 Java 层面去处理。
什么是 Stream?
Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
Stream 和 Collection 集合的区别:**Collection 是一种静态的内存数据结构,讲的是数据,而 Stream 是有关计算的,讲的是计算。**前者是主要面向内存,后者主要是面向 CPU。
注意:
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。
- Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。
介绍一下 Stream 的操作三个步骤
创建 Stream:通过一个数据源(如:集合、数组)获取一个流。
-
通过集合
Java 8 中的 Collection 接口被扩展,提供了两个获取流的方法:
default Stream<E> stream()
: 返回一个顺序流default Stream<E> parallelStream()
: 返回一个并行流
@Test public void test01(){ List<Integer> list = Arrays.asList(1,2,3,4,5); //JDK1.8中,Collection系列集合增加了方法 Stream<Integer> stream = list.stream(); }
-
通过数组
Java 8 中的 Arrays 的静态方法 stream() 可以获取数组流:
static <T> Stream<T> stream(T[] array)
:返回一个流public static IntStream stream(int[] array)
public static LongStream stream(long[] array)
public static DoubleStream stream(double[] array)
@Test public void test02(){ String[] arr = {"hello","world"}; Stream<String> stream = Arrays.stream(arr); } @Test public void test03(){ int[] arr = {1,2,3,4,5}; IntStream stream = Arrays.stream(arr); }
-
通过 Stream 的
of()
可以调用 Stream 类静态方法
of()
, 通过显示值创建一个流。它可以接收任意数量的参数。public static<T> Stream<T> of(T... values)
: 返回一个流
@Test public void test04(){ Stream<Integer> stream = Stream.of(1,2,3,4,5); stream.forEach(System.out::println); }
-
创建无限流(了解)
可以使用静态方法
Stream.iterate()
和Stream.generate()
, 创建无限流。- 迭代
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
- 生成
public static<T> Stream<T> generate(Supplier<T> s)
@Test public void test05() { // 迭代 // public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) Stream<Integer> stream = Stream.iterate(0, x -> x + 2); stream.limit(10).forEach(System.out::println); // 生成 // public static<T> Stream<T> generate(Supplier<T> s) Stream<Double> stream1 = Stream.generate(Math::random); stream1.limit(10).forEach(System.out::println); }
- 迭代
中间操作
每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然是 Stream 类型的对象。多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为“惰性求值”。
-
筛选与切片
方 法 描 述 filter(Predicatep) 接收 Lambda , 从流中排除某些元素 distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素 limit(long maxSize) 截断流,使其元素不超过给定数量 skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补。 -
映射
方法 描述 map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。 mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。 mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。 flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 -
排序
方法 描述 sorted() 产生一个新流,其中按自然顺序排序 sorted(Comparator com) 产生一个新流,其中按比较器顺序排序
public void test01(){
// 1. 创建Stream
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
// 2. 加工处理
// 过滤:filter(Predicate p)
// Predicate是函数式接口,抽象方法:boolean test(T t)
stream = stream.filter(t -> t%2==0);
// 3. 终结操作:例如:遍历
stream.forEach(System.out::println);
}
终止操作(终端操作) 终止操作的方法返回值类型就不再是 Stream 了,因此一旦执行终止操作,就结束整个 Stream 操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束 Stream。
- 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
- 流进行了终止操作后,不能再次使用。
-
匹配与查找
方法 描述 allMatch(Predicate p) 检查是否匹配所有元素 anyMatch(Predicate p) 检查是否至少匹配一个元素 noneMatch(Predicate p) 检查是否没有匹配所有元素 findFirst() 返回第一个元素 findAny() 返回当前流中的任意元素 count() 返回流中元素总数 max(Comparator c) 返回流中最大值 min(Comparator c) 返回流中最小值 forEach(Consumer c) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了) -
归约
方法 描述 reduce(T identity, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 T reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional<T> map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。
-
收集
方 法 描 述 collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:
方法 返回类型 作用 toList Collector<T, ?, List<T>> 把流中元素收集到List toSet Collector<T, ?, Set<T>> 把流中元素收集到Set toCollection Collector<T, ?, C> 把流中元素收集到创建的集合 counting Collector<T, ?, Long> 计算流中元素的个数 summingInt Collector<T, ?, Integer> 对流中元素的整数属性求和 averagingInt Collector<T, ?, Double> 计算流中元素Integer属性的平均值 summarizingInt Collector<T, ?, IntSummaryStatistics> 收集流中Integer属性的统计值。如:平均值 joining Collector<CharSequence, ?, String> 连接流中每个字符串 maxBy Collector<T, ?, Optional<T>> 根据比较器选择最大值 minBy Collector<T, ?, Optional<T>> 根据比较器选择最小值 reducing Collector<T, ?, Optional<T>> 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中元素逐个结合,从而归约成单个值 collectingAndThen Collector<T,A,RR> 包裹另一个收集器,对其结果转换函数 groupingBy Collector<T, ?, Map<K, List<T>>> 根据某属性值对流分组,属性为K,结果为V partitioningBy Collector<T, ?, Map<Boolean, List<T>>> 根据true或false进行分区
// 示例
List<Employee> emps= list.stream().collect(Collectors.toList());
Map<Emp.Status, List<Emp>> map= list.stream().collect(
Collectors.groupingBy(Employee::getStatus));
Map<Boolean,List<Emp>> vd = list.stream().collect(
Collectors.partitioningBy(Employee::getManage));
介绍一下 Stream 中 Java 9 新增的 API
ofNullable() 的使用
Java 8 中 Stream 不能完全为 null,否则会报空指针异常。而 Java 9 中的 ofNullable 方法允许我们创建一个单元素 Stream,可以包含一个非空元素,也可以创建一个空 Stream。
// 报 NullPointerException
// Stream<Object> stream1 = Stream.of(null);
// System.out.println(stream1.count());
// 不报异常,允许通过
Stream<String> stringStream = Stream.of("AA", "BB", null);
System.out.println(stringStream.count()); //3
// 不报异常,允许通过
List<String> list = new ArrayList<>();
list.add("AA");
list.add(null);
System.out.println(list.stream().count());//2
// ofNullable():允许值为null
Stream<Object> stream1 = Stream.ofNullable(null);
System.out.println(stream1.count());//0
Stream<String> stream = Stream.ofNullable("hello world");
System.out.println(stream.count());//1
iterator() 重载的使用
//原来的控制终止方式:
Stream.iterate(1, i -> i + 1).limit(10).forEach(System.out::println);
//现在的终止方式:
Stream.iterate(1, i -> i < 100, i -> i + 1).forEach(System.out::println);
语法糖
什么是语法糖?
语法糖是指编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
例如,Java 中的 for-each
就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。
Java 中有哪些常见的语法糖?
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
转载自:https://juejin.cn/post/7384808872487534607