Function
Function 接口接受一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen):
package java.util.function; import java.util.Objects; @FunctionalInterface public interface Function<T, R> { //将Function对象应用到输入的参数上,然后返回计算结果。 R apply(T t); //将两个Function整合,并返回一个能够执行两个Function对象功能的Function对象。 default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } // default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } static <T> Function<T, T> identity() { return t -> t; } }
Supplier
Supplier 接口产生给定泛型类型的结果。 与 Function 接口不同,Supplier 接口不接受参数。
Supplier<Person> personSupplier = Person::new; personSupplier.get(); // new Person
Consumer
Consumer 接口表示要对单个输入参数执行的操作。
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker")); // Hello luke
Comparator
Java中的经典老接口。
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
4、方法和构造函数引用(Method and Constructor References)
4.1 介绍
Java 8中引入了方法和构造函数的引用,它们是一种新的语法,可以使得代码更加简洁、易于理解。方法和构造函数的引用允许我们直接引用一个已经存在的方法或构造函数,而不需要重新实现它们。
4.2 方法引用
方法引用是一种通过方法的名称来引用已经存在的方法的语法。在Lambda表达式中,我们可以通过::符号来引用一个已经存在的方法。
4.2.1 方法引用格式
方法的持有者::方法名称
比如上面的类型转换案例中,就能将其改成静态方法的引用
// 例子一 Converter<String, Integer> converter = Integer::valueOf; // 静态方法引用 Integer converted = converter.convert("123"); System.out.println(converted.getClass());
// 例子二 public class MethodsReferences { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); names.forEach(System.out::print); // 不换行 names.forEach(System.out::println); // 换行 } }
我们使用方法引用的语法来引用了System.out的println()方法。在Lambda表达式中,我们将System.out::println
作为参数传递给了names集合的forEach()方法。这个语法与Lambda表达式的语法非常相似,但是使用了::
符号来引用方法。
Java 8允许您通过::
关键字传递方法或构造函数的引用。 上面的示例显示了如何引用静态方法。 但我们也可以引用对象方法:
/** * @author Shier 2023/2/19 22:46 */ public class ConstructReferences { public static void main(String[] args) { Something something = new Something(); Converter<String, String> converter = something::startsWith; // 对象方法的引用 String converted = converter.convert("Java"); System.out.println(converted); // "J" } } class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); // } }
4.3 构造函数的引用
构造函数引用是一种通过构造函数的名称来引用已经存在的构造函数的语法。在Lambda表达式中,我们可以通过类名::new的方式来引用一个已经存在的构造函数。
4.3.1 语法格式:
类名::new
4.3.2 实现案例
接下来看看构造函数是如何使用::
关键字来引用的,首先我们定义一个包含多个构造函数的简单类:
class Person { String name; String age; Person() {} Person(String name, int age) { this.name = name; this.age = age; } }
接下来我们指定一个用来创建Person对象的对象工厂接口:
interface PersonFactory<P extends Person> { P create(String name, int age); }
这里我们使用构造函数引用来将他们关联起来,而不是手动实现一个完整的工厂:
public class ConstructReferences { public static void main(String[] args) { PersonFactory<Person> personFactory = Person::new; personFactory.add("shier", 19); } }
我们只需要使用 Person::new 来获取Person类构造函数的引用,Java编译器会自动根据PersonFactory.create方法的参数类型来选择合适的构造函数。
需要注意的是,方法和构造函数的引用只能用于符合特定签名的方法和构造函数。也就是说,如果我们想要使用方法或构造函数的引用,必须确保它们的参数列表和返回值类型与Lambda表达式的参数列表和返回值类型一致。
5、Optional
Optional不是函数式接口,而是用于防止/避免 NullPointerException 的漂亮工具。
Optional是一个容器对象,可以包含一个非空对象或者一个空对象。如果包含了非空对象,则isPresent()方法会返回true,调用get()方法会返回该对象。如果包含了空对象,则isPresent()方法会返回false,调用get()方法会抛出NoSuchElementException异常。
Optional 中提供一系列处理空指针异常的方法。
of(T value):返回一个包含指定非空值的Optional对象,如果value为空,则抛出NullPointerException异常。
ofNullable(T value):返回一个包含指定值的Optional对象,如果value为空,则返回一个空Optional对象。
empty():返回一个空的Optional对象。
isPresent():如果包含非空对象,则返回true,否则返回false。
get():如果包含非空对象,则返回该对象,否则抛出NoSuchElementException异常。
orElse(T other):如果包含非空对象,则返回该对象,否则返回指定的默认值other。
orElseGet(Supplier<? extends T> other):如果包含非空对象,则返回该对象,否则返回由Supplier提供的默认值。
map(Function<? super T, ? extends U> mapper):如果包含非空对象,则对其进行转换并返回包含转换结果的Optional对象,否则返回空Optional对象。
flatMap(Function<? super T, Optional<U>> mapper):如果包含非空对象,则对其进行转换并返回转换结果,否则返回空Optional对象。示例说明
/** * @author Shier */ public class OptionalExample { public static void main(String[] args) { // 返回一个非空的Optional对象,为空则会出现NPE异常 Optional<String> optional1 = Optional.of("hello"); // 返回一个包含指定对象的Optional对象,为空则返回Optional对象 Optional<String> optional2 = Optional.ofNullable(null); // 判断Optional实例对象是否为空 System.out.println(optional1.isPresent()); // true System.out.println(optional2.isPresent()); // false // 包含非空对象则返回非空对象,否则就是NPE System.out.println(optional1.get()); // hell System.out.println(optional2.orElse("world")); // world // 非空则进行转换,返回的是一个转换后的Optional对象 Optional<String> optional3 = optional1.map(s -> s.toUpperCase()); System.out.println(optional3.get()); // HELLO // 如果包含非空对象,则对其进行转换并返回转换结果 Optional<Integer> optional4 = optional1.flatMap(s -> Optional.of(s.length())); System.out.println(optional4.get()); // 5 } }
6、Stream介绍
6.1. Stream流的作用是什么?
Stream API 用于处理集合数据。Stream可以用于对集合进行筛选、排序、聚合等操作,它的使用方式和SQL语句类似。Stream不会修改源数据,而是通过生成一个新的Stream来执行操作,因此它是一个惰性求值的操作。只有在需要返回结果时才会执行实际操作。
又或者可以说是Stream 操作将分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间的操作则返回的是Stream本身,这样就可以将多个Stream操作串起来。
6.2. Stream流的思想和使用步骤
- 先得到集合或者数组的Stream流(就是一根传送带)
- 把元素放上去
- 然后就用这个Stream流简化的API来方便的操作元素
6.3. Stream流常用API方法
filter(Predicate<T> predicate):用于对Stream进行筛选,保留符合条件的元素。(过滤掉,只要符合的内容)
map(Function<T, R> mapper):用于对Stream进行映射,将元素进行转换。
sorted(Comparator<T> comparator):用于对Stream进行排序。
distinct():用于对Stream进行去重,去掉重复的元素。
limit(long maxSize):用于对Stream进行截取,只保留前maxSize个元素。
skip(long n):用于对Stream进行跳过,跳过前n个元素。
forEach(Consumer<T> action):用于对Stream进行遍历,对每个元素执行指定的操作。
collect(Collector<T, A, R> collector):用于对Stream进行聚合,将元素收集到一个容器中并返回。Stream流操作后的结果数据转回到集合或者数组中去。
reduce(T identity, BinaryOperator<T> accumulator):用于对Stream进行聚合,将元素逐个应用给定的累加器函数,并返回累加结果。
Collectors工具类提供具体的收集方式
方法 | 说明 |
public static Collector taList() | 把元素收集到List集合中 |
public static Collector toet() | 把元素收集到Set集合中 |
public static collector oap(Function kexMlapper , Function valueMappe.) | 把元素收集到Map集合中 |
6.5 示例并对API解释
首先创建一个动态数组,也就是ArrayList集合,并对里面添加一些数据
List<String> stringList = new ArrayList<>(); stringList.add("ddd2"); stringList.add("aaa2"); stringList.add("bbb1"); stringList.add("aaa1"); stringList.add("bbb3"); stringList.add("ccc"); stringList.add("bbb2"); stringList.add("ddd1");
6.5.1 Filter(过滤)
过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。
// 测试 Filter(过滤) stringList .stream() .filter((s) -> s.startsWith("a"))// 以a开头的 .forEach(System.out::println);//aaa2 aaa1 使用了函数式接口中方法的引用 stringList.stream().filter((str) -> str.endsWith("1")).forEach(System.out::println);// bbb1 aaa1 ddd1
forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。
6.5.2 Sorted(排序)
排序也是一个 中间操作,返回的是排序好后的 Stream。如果你不指定一个自定义的 Comparator 则会使用默认排序(升序)。
// 排序 stringList.stream().sorted() .filter((s) -> s.startsWith("a"))// 以a开头的 .forEach(System.out::println); //aaa1 aaa2 使用了函数式接口中方法的引用 System.out.println("Sorted倒序排序:"); stringList.stream().sorted((a, b) -> b.compareTo(a)) .filter((s) -> s.startsWith("a")) .forEach(System.out::println); //aaa2 aaa1
需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringList是不会被修改的
6.5.3 Map (映射)
中间操作 map 会将元素根据指定的 Function 接口来依次将元素转成另外的对象。
示例
// 测试映射 stringList.stream().map(s -> s.toUpperCase()).sorted().forEach(System.out::println); stringList.stream().map(String::toUpperCase).sorted().forEach(System.out::println);
6.5.4 Match (匹配)
Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是 最终操作 ,并返回一个 boolean 类型的值。
anyMatch 方法:如果集合中至少有一个元素满足Predicate条件,返回true,否则返回false。
List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> numbers2 = Arrays.asList(1, 3, 5, 7, 9); boolean hasEven1 = numbers1.stream().anyMatch(n -> n % 2 == 0); boolean hasEven2 = numbers2.stream().anyMatch(n -> n % 2 == 0); System.out.println("number1: " + hasEven1); // true System.out.println("number2: " + hasEven2); // false
allMatch方法:如果集合中所有元素都满足Predicate条件,返回true,否则返回false。
List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); boolean hasEven1 = numbers1.stream().allMatch(n -> n % 2 == 0); System.out.println("number1: " + hasEven1); // true
noneMatch方法:如果集合中没有元素满足Predicate条件,返回true,否则返回false。
List<Integer> numbers1 = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> numbers2 = Arrays.asList(1, 3, 5, 7, 9); boolean hasEven1 = numbers1.stream().noneMatch(n -> n % 2 == 0); boolean hasEven2 = numbers2.stream().noneMatch(n -> n % 2 == 0); System.out.println("number1: " + hasEven1); // false System.out.println("number2: " + hasEven2); // true
6.5.5 Count (计数)
计数是一个 最终操作,返回Stream中元素的个数,返回值类型是 long。
// 测试计数 Count 操作 long count = stringList.stream().filter(s -> s.startsWith("a")).count(); System.out.println(count); // 2
6.5.6 Reduce (规约)
这是一个 最终操作 ,允许通过指定的函数来将stream中的多个元素规约为一个元素,规约后的结果是通过Optional 接口表示的
// 规约 Optional<String> reduce = stringList.stream().sorted().reduce((a, b) -> a + "-" + b); System.out.println(reduce);
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于Integer sum = integers.reduce(0, (a, b) -> a+b);也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
// 字符串连接 String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); System.out.println(concat); //ABCD // 求最小值 double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); System.out.println(minValue); // -3.0 // 求和 int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); System.out.println(sumValue);// sumValue = 10, 有起始值 // 求和 sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get(); System.out.println(sumValue);// sumValue = 10, 无起始值 // 过滤 concat = Stream.of("a", "B", "c", "D", "e", "F") .filter(x -> x.compareTo("Z") > 0) .reduce("", String::concat); System.out.println(concat);//字符串连接,concat = "ace"
上面代码例如第一个示例的 reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。
7、Parallel Streams (并行流)
上一章提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。
下面开始介绍并行流如何提高性能:
首先创建一个没有重复的大表:
int max = 1000000; List<String> values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
分别使用串行和并行进行排序,看看执行的时间;
/** * @author Shier 2023/2/20 14:19 */ public class ParallelStreamTest { public static void main(String[] args) { int max = 1000000; List<String> values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); } // 串行排序 long startTime = System.currentTimeMillis(); long count = values.stream().sorted().count(); long endTime = System.currentTimeMillis(); System.out.println("串行执行时间:" + (endTime - startTime) + "ms"); // 665ms // 并行排序 long startParallelTime = System.currentTimeMillis(); long count2 = values.parallelStream().sorted().count(); long endParallelTime = System.currentTimeMillis(); System.out.println("并行执行时间:" + (endParallelTime - startParallelTime) + "ms"); // 332ms } }
上面的串并行排序的代码几乎是一样的,但是并行排序的效率块了50%左右。可见性能提高了不少。与普通的串行流相比,并行流可以充分利用多核CPU的优势,加速集合的处理速度。
需要注意的是,并行流在某些情况下并不一定比串行流更快,甚至有可能比串行流更慢。这是因为并行流需要将数据分成多个部分,分别在不同的线程上进行处理,而这个过程本身也需要一定的时间和资源。因此,在使用并行流时,需要根据具体的情况进行测试和优化。
8、 可重复注解
在 Java 8 中,可以使用@Repeatable注解来定义可重复注解,这使得我们可以将同一种注解多次应用于同一元素上。在使用可重复注解时,需要先定义一个包含@Repeatable注解的容器注解,再在容器注解中定义注解的值,如果想要给同一个类型的注解使用多次,只需要给该注解标注一下@Repeatable即可。
例如,假设我们要定义一个@Author注解,用于标记一篇文章的作者,我们可以按照以下步骤进行操作:定义容器注解Authors:
/** * 定义一个容器注解类Authors */ public @interface Authors { Author[] value(); }
定义一个包含@Repeatable注解的容器注解Authors:
/** * @author Shier 2023/2/20 14:39 * 使用容器注解类的注解@Repeatable说明是可重复的 */ @Repeatable(Authors.class) public @interface Author { String name(); }
第一步和第二步都是在创建一个包装类,将Authors作为注解
- 使用@Author注解时,可以将多个作者名字传递给容器注解Authors:
/** * 第一种写完整的 * 在使用容器注解来声明值,最外层的为复数形式的注解类,也就是第一个步骤创建的类,里面的Author是第二步创建的可重复的注解类 */ @Authors({ @Author(name = "shier"), @Author(name = "小十二"), }) public class Article {} /** * 第二种简单写法 */ @Author(name = "shier") @Author(name = "小十二") public class Article {}
在上面的例子中,@Author注解表示一篇文章的作者,使用@Repeatable注解来定义可重复注解。然后定义一个容器注解Authors,包含一个Author类型的数组。最后,在定义一篇文章时,可以将多个作者名字传递给@Authors注解。
通过使用可重复注解,我们可以更加灵活地定义和使用注解,使代码更加简洁和易读。