3276 2023-06-08 2023-06-14
前言:Java 8就引入了 Lambda 和 Stream API(截止到2023年6月,JDK 20亦然发布,参考网址:https://injdk.cn/),这两种特性平时在工作中也是经常使用,但一直没有机会进行全面地系统性总结,这次抽空来好好过一下。
补充:本文不做源码层面的解读,抱着够用就行的心态,随便稍稍深入了解一丢丢的思想来进行的。源码实现可以参考这篇:https://www.throwx.cn/2021/10/06/stream-of-jdk/
一、Lambda
Lambda 表达式是JDK 8的一个新特性,支持 Java 能进行简单的“函数式编程”,可以取代大部分的匿名内部类(接口里面只有一个抽象方法),写出更优雅的Java代码。尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
1、函数式接口
如果定义的一个接口有且只有一个抽象方法
,这样的接口就成为函数式接口(Functional Interface)。函数式接口可以有任意个 default 或者 static 方法
。
任何函数式接口都可以使用 Lambda 表达式替换。Lambda表达式的本质是实现函数式接口的一种方式,编译时仍然会替换为一个实现类,只不过语法上做了简化,是Java提供的又一个语法糖。
如下是一个简单的示例:
/**
* 说明:@FunctionalInterface 注解会显式提醒编译器这是一个函数式接口,但加不加,没啥实际影响
*/
@FunctionalInterface
public interface IPerson {
void saySomething();
}
public interface IPerson1 {
String concatStr(String str1, String str2);
}
public class Test {
public static void main(String[] args) {
// 1.无入参,空返回实现
IPerson person = () -> System.out.println("我是中国人");
person.saySomething();
// 2.多个入参,且有方法体实现
IPerson1 person1 = (s1, s2) -> {
// 这行如果没有的话,可以直接 (s1, s2) -> s1 + "_" + s2
System.out.println("输入为:" + s1 + "," + s2);
return s1 + "_" + s2;
};
String str = person1.concatStr("haha", "haha");
System.out.println(str);
// 3.双冒号 :: 为引⽤运算符,一般与包 java.util.function 提供的函数式接口相配合,返回一个方法应用
// 一般要求调用方法无入参或只有一个入参
Arrays.asList("1", "2", "3").forEach(System.out::println);
Supplier<Double> supplier = Math::random;
System.out.println(supplier.get());
}
}
2、常用函数式接口
java.util.function包默认提供了大量函数式接口,这些接口一般可与Stream API完美配合使用,如下是一些常见Stream接口API说明:
// 1.条件筛选
filter(Predicate<? super T> predicate)
// 2.对单个item对象转换操作
map(Function<? super T, ? extends R> mapper)
// 3.对item对象操作,并返回一个新的Stream流
flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
// 4.循环遍历
forEach(Consumer<? super T> action)
// 5.条件筛选,整个集合item有一个为true
anyMatch(Predicate<? super T> predicate)
// 6.条件筛选,全部item为true
allMatch(Predicate<? super T> predicate)
// 7.累计计算,值累加等
reduce(BinaryOperator<T> accumulator)
// 8.集合操作,元素汇总转换计算
collect(Collector<? super T, A, R> collector)
1、Supplier
Supplier<T>
供给型接口,无参有返回值。Supplier<T>接口之所以被称之为生产型接口,是因为如果我们指定了接口的泛型是什么类型,那么接口中的get方法就会生产什么类型的数据供我们使用。
T get()
方法:用于获得结果;不需要参数,它会按照某种实现逻辑(由Lambd表达式实现)返回一个数据。
public class SupplierTest {
public static void main(String[] args) {
System.out.println(concatStr(() -> "haha"));
}
private static String concatStr(Supplier<String> supplier) {
return supplier.get() + "_concat";
}
}
2、Consumer
Consumer<T>
消费型接口,有参数无返回值。Consumer<T>接口也被称之为消费型接口,它消费的数据的类型由泛型指定,包含两个方法。
void accept(T t)
:对给定的参数执行此操作default Consumer<T> andThen(Consumer after)
:返回一个组合的Consumer,依次执行此操作,然后执行after操作。
public class ConsumerTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
// 简单实用,foreach的入参就是一个 Consumer<? super T> action
list.forEach(System.out::print);
// 自己编写 accept 方法体的 Consumer
consumer(list, s -> {
System.out.println("测试消费:" + s);
});
// andThen的测试,每一个数会依次经过 consumer->consumer1->consumer2
Consumer<Integer> consumer = x -> {
System.out.println("全部消费:" + x);
};
Consumer<Integer> consumer1 = x -> {
if (x % 2 == 0) {
System.out.println("偶数消费:" + x);
}
};
Consumer<Integer> consumer2 = x -> {
if (x % 2 != 0) {
System.out.println("基数消费:" + x);
}
};
// 这种链式反应很奇妙
list.forEach(consumer.andThen(consumer1).andThen(consumer2));
}
private static void consumer(List<Integer> list, Consumer<Integer> consumer) {
// consumer::accept 也可已直接替换为 consumer
list.forEach(consumer::accept);
}
}
3、Predicate
Predicate<T>
断言型接口,有参有返回值,返回值是boolean类型。Predicate<T>
方法通常用于判断参数是否满足指定的条件,常用的四个方法:
- boolean test(T t):对给定的参数进行判断(判断逻辑由Lambda表达式实现),返回一个布尔值。
- default Predicate
negate():返回一个逻辑的否定,对应逻辑非。 - default Predicate
and(Predicate other):返回一个组合判断,对应短路与。 - default Predicate
or(Predicate other):返回一个组合判断,对应短路或。
public class PredicateTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 过滤出偶数
list = list.stream().filter(s -> s % 2 == 0).collect(Collectors.toList());
System.out.println(list);
Predicate<String> predicate = s -> s.contains("sb");
System.out.println("包含侮辱词汇: " + predicate.test("abcdesb"));
System.out.println("包含侮辱词汇: " + predicate.test("abcdefgbbs"));
Predicate<String> isMan = s -> s.contains(" man");
Predicate<String> isWoman = s -> s.contains(" woman");
Predicate<String> isStudent = s -> s.contains(" student");
Predicate<String> isTeacher = s -> s.contains(" teacher");
String human1 = "he is a man who work as a as student in home";
String human2 = "she is a women who work as a teacher in home";
// and or negate 方法使用演
System.out.println("这个人是一个男性:" + isStudent.and(isMan).test(human1));
System.out.println("这是一个老师或者学生:" + isStudent.or(isMan).test(human1));
System.out.println("这是一个既是又是老师,又是一名男性:" + isStudent.and(isMan).test(human2));
System.out.println("这是一个既是又是老师,又是一名女性:" + isStudent.and(isWoman).test(human2));
System.out.println("他不是一名老师:" + isTeacher.negate().test(human1));
}
}
4、Function
Function<T,R>
函数式接口,有参有返回值。接口通常用于对参数进行处理和转换,然后返回一个新的值。
R apply(T t)
:将此函数应用于给定的参数。default <V> Function andThen(Function after)
:返回一个组合函数,首先将该函数应用于输入,然后将after函数应用于结果。
public class FunctionTest {
public static void main(String[] args) {
Function<Integer, String> delInt = i -> {
System.out.println("开始处理int数字:" + i);
boolean r = i % 2 == 0;
return r ? (i + "是偶数") : (i + "是奇数");
};
Function<String, String> delStr = s -> {
System.out.println("开始处理:" + s);
boolean r = s.contains("偶数");
return r ? (s + "√") : (s + "×");
};
// 1.map是把集合每个元素重新映射
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
List<String> result1 = list1.stream().map(delInt.andThen(delStr)).collect(Collectors.toList());
System.out.println(result1);
// 2.flatMap从字面上来说是压平这个映射,实际作用就是将每个元素进行一个一对多的拆分,
// 细分成更小的单元,返回一个新的Stream流,新的流元素个数增加
Function<String, Stream<String>> split = s -> Stream.of(s.trim().split(" "));
List<String> list2 = Arrays.asList("a b c", "e f g", "h");
List<String> result2 = list2.stream().flatMap(split).collect(Collectors.toList());
System.out.println(result2);
// 3.生成 1-100 数字,并计算结果
// rangeClosed/range两个方法的区别在于一个是闭区间,一个是半开半闭区间
// sum聚合方法,底层实现还是reduce方法
long sum = IntStream.rangeClosed(1, 100).asLongStream().sum();
System.out.println("sum方法 从1加到100=" + sum);
// 使用 reduce 方法
long sum1 = IntStream.rangeClosed(1, 100).reduce((x1, x2) -> x1 + x2).getAsInt();
System.out.println("reduce方法 从1加到100=" + sum1);
long sum2 = IntStream.rangeClosed(1, 100).reduce(4950, Integer::sum);
System.out.println("reduce方法 4950 + " + sum1 + " = " + sum2);
}
}
二、Stream
Stream 中文称为 “流”,通过将集合转换为这么一种叫做 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。有如下特性
-
Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作
-
这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作
-
Stream不保存数据,故每个Stream流只能使用一次
在上一节中,已经可以看到 Lambda + Stream 配合使用的强大。对于一些较为简单的API方法,这里不多作说明,着重列举下一些需要注意的相关重点API。
1、Optional
-
Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
-
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
-
Optional 类的引入很好的解决空指针异常。
public class OptionalTest {
public static void main(String[] args) {
// 1.基本演示
// of 方法不允许入参为null,会报NPE
// ofNullable允许入参为null,内部构造一个empty对象
Optional<String> optional = Optional.ofNullable(null);
System.out.println("初始optional中是否有值:" + optional.isPresent());
// 2.filter map 等方法默认过滤null值
String s1 = optional
.filter(s -> s.contains("0"))
.map(s -> s + "1")
// 直接get会报NPE,因此最好使用 orElse orElseThrow
.orElse("这是个默认值");
System.out.println(s1);
// 3.手动处理异常
/// 这里会让程序直接抛异常
/// s1 = optional.orElseThrow(() -> new RuntimeException("参数不能为null"));
/// System.out.println(s1);
// 4.Stream聚合操作
optional.ifPresent(s -> {
// 不会输出,有不为null的值才会输出
});
Stream.of(null, "1", "2", "3", null, "4")
/// .map(Objects::nonNull) 也行
.filter(s -> Optional.ofNullable(s).isPresent() && Integer.parseInt(s) > 1)
.map(s -> Optional.of(s))
.forEach(s -> {
System.out.println("输出值:" + s.get());
});
}
}
2、聚合(max/min/count)
不多讲,上代码
public class CountTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("11.221", "33.22", "22.113", "44.55", "4.0");
// 1.找出转 Double 后的最大值
Optional<String> optional = list.stream().max(Comparator.comparingDouble(Double::valueOf));
System.out.println("最大值:" + optional.get());
// min同
System.out.println("最小值:" + list.stream().min(Comparator.comparingDouble(Double::valueOf)).get());
// 2.找出字符串最长的那个
String maxLengthOne = list.stream().max(Comparator.comparing(String::length)).get();
System.out.println("最长的字符串:" + maxLengthOne);
// min同
System.out.println("最短的字符串:" + list.stream().min(Comparator.comparing(String::length)).get());
// 3.自然排序,字典顺序,结果为 [11.221, 22.113, 33.22, 4.0, 44.55]
System.out.println(list.stream().sorted().collect(Collectors.toList()));
// 转成 Double 再排序,结果为 [4.0, 11.221, 22.113, 33.22, 44.55]
System.out.println(list.stream().mapToDouble(Double::valueOf).boxed()
.sorted().collect(Collectors.toList()));
// 倒序排序,反字典顺序,结果为 [44.55, 4.0, 33.22, 22.113, 11.221]
System.out.println(list.stream()
.sorted(Comparator.reverseOrder()).collect(Collectors.toList()));
// 自定义排序规则,结果为 [4.0, 11.221, 22.113, 33.22, 44.55]
System.out.println(list.stream()
.sorted(Comparator.comparing(Double::valueOf)).collect(Collectors.toList()));
// 自定义复杂规则,结果为 [4.0, 33.22, 44.55, 22.113, 11.221]
System.out.println(list.stream()
.sorted((s1, s2) -> {
// 自定义比较规则,整数位 + 小数位,从小到大
int i1 = Arrays.stream(s1.split("\\.")).mapToInt(Integer::valueOf).sum();
int i2 = Arrays.stream(s2.split("\\.")).mapToInt(Integer::valueOf).sum();
return i1 > i2 ? 1 : -1;
}).collect(Collectors.toList()));
// 4.去重后,统计数量
list = Arrays.asList("123456543210".split(""));
// 输出为:[1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]
System.out.println("原始值:" + list);
// 输出位:[1, 2, 3, 4, 5, 6, 0]
System.out.println("去重后:" + list.stream().distinct().collect(Collectors.toList()));
// 统计个数,sum,average
System.out.println("去重后个数:" + list.stream().distinct().count());
System.out.println("去重后总和:" + list.stream().distinct().mapToInt(Integer::valueOf).sum());
System.out.println("去重后平均值:" + list.stream().distinct()
.mapToInt(Integer::valueOf).average().orElse(0L));
}
}
3、Reduce
reduce思想还是有必要再单独提一下的,虽然之前也有过例子,如下
public class ReduceTest {
public static void main(String[] args) {
// 1.生成 1-10 数字,并计算结果
// rangeClosed/range两个方法的区别在于一个是闭区间,一个是半开半闭区间
// 也可以直接 .asLongStream().sum() 得到sum结果
Stream<String> stream = IntStream.rangeClosed(1, 10).asLongStream().boxed().map(String::valueOf);
// 字符串拼接,结果为 12345678910
String concatStr = stream.reduce((s1, s2) -> s1 + s2).orElse("");
System.out.println(concatStr);
// 2.计算sum、max、乘积
Stream<Integer> integerStream = IntStream.rangeClosed(1, 10).boxed();
long sum = integerStream.reduce(0, (x1, x2) -> (x1 + x2));
System.out.println("sum = " + sum);
// 同一个流不能操作两次
integerStream = IntStream.rangeClosed(1, 10).boxed();
int max = integerStream.reduce((x1, x2) -> (x1 > x2 ? x1 : x2)).orElse(0);
System.out.println("max = " + max);
integerStream = IntStream.rangeClosed(1, 10).boxed();
int result = integerStream.reduce(1, (x1, x2) -> (x1 * x2));
System.out.println("乘积 = " + result);
}
}
4、分组(groupingBy/toMap)
public class GroupTest {
public static void main(String[] args) {
List<Person> list = Arrays.asList(
new Person("张三", 22, "男", "北京"),
new Person("李四", 25, "女", "深圳"),
new Person("王五", 32, "男", "北京"),
new Person("赵六", 35, "女", "上海")
);
// 1.根据性别分组
Map<String, List<Person>> sexMap = list
.stream().collect(Collectors.groupingBy(Person::getSex));
System.out.println("按照sex分组:" + sexMap);
// 2.按照年龄区间分区,partitioningBy 接收一个 Predicate 对象
Map<Boolean, List<Person>> ageMap = list.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() < 30 ));
System.out.println("按照age分组:" + ageMap);
// 3.多个分组,先按性别,再按年龄
Map<String, Map<String, List<Person>>> map = list.stream()
.collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getAddress)));
System.out.println("复合条件分组:" + map);
// 4.补充一个遇到的bug,2022-04-21 stream toMap value不能为空
// 如果key重复,会报重复 IllegalStateException Duplicate key 张三
// 如果value位null,会报空指针
// 这里list不能add,会报 UnsupportedOperationException
list = new ArrayList<>(list);
Map<String, String> map1 = list.stream()
// key应该具有唯一标识,且value不能为null
.collect(Collectors.toMap(Person::getName, Person::getSex));
System.out.println(map1);
// 5.单个Map转换,比如,我只获得一个 Map<String, Person> 对象,那应该怎么做呢
// 这里再加一个女张三
list.add(new Person("张三", 18, "女", "杭州"));
Map<String, Person> collectMap = list.stream()
// 三个参数分为 key value key冲突时保留前者还是后者,这里是保留前者,因此女张三会被丢弃
.collect(Collectors.toMap(Person::getName, p -> p, (p1, p2) -> p1));
System.out.println(collectMap);
}
@AllArgsConstructor
@Data
private static class Person {
private String name;
private int age;
private String sex;
private String address;
}
}
三、总结
熟练使用 Lambda + Stream 能够有效提升编码效率,提升代码的扩展性和可读性,既装逼又实惠,是时候好好学习一波了!
总访问次数: 53次, 一般般帅 创建于 2023-06-08, 最后更新于 2023-06-14
欢迎关注微信公众号,第一时间掌握最新动态!