Java 8 Stream API:如何高效操作集合

Java 8 Stream API

Stream API 基于Lambda表达式和函数式接口对集合操作进行了增强,极大的提高了编程效率和程序可读性。

Stream API 分为创建流,流的中间操作,终止操作。

创建流

流操作的起点,我们需要将我们所需要的数据先得到流,一般有以下几种

集合/其他获取流方法备注
离散值:如 1,3,5,7Stream.of(…)静态方法
数组Arrays.stream(T[] arr);静态方法
Listlist.stream()成员方法
Setset.stream()成员方法
Mapmap.entrySet.stream()成员方法

1、离散值生成Stream

Stream<Integer> intStream = Stream.of(1, 2, 3, 6); // 生成int流
Stream<String> stringStream = Stream.of("Tom", "Jerry");  // 生成string流
Stream<String> empty = Stream.empty(); // 空流

2、数组生成Stream

用Arrays工具类

String[] strArray = new String[]{"Tom", "Jerry"};
// 将数组转成流
Stream<String> strArrayStream = Arrays.stream(strArray); 

// 枚举类转stream
Stream<HttpMethod> stream = Arrays.stream(HttpMethod.values());

// 字符串分割后转stream
String products = "computer,watch,pad";
Stream<String> productStream = Arrays.stream(products.split(","));

3、List生成stream

list自带stream方法

List<String> list = new ArrayList<>();
// 通过stream获取流
Stream<String> listStream = list.stream();

4、Set生成stream

Set 也同样自带 stream方法

Set<String> set = new HashSet<>();
// 通过stream获取流
Stream<String> setStream = set.stream();

5、Map生成Stream

map并没有像list或set提供 stream方法,而是需要先得到 entrySet,然通过set的stream来获取流

Map<String, String> map = new HashMap<>();
// 通过entrySet再获取到流
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();

一般来说以上5种基本上满足了开发种所需要的流创建情况。

中间操作

流的中间操作是指对Stream对象进行的一系列转换操作,这些操作返回的仍然是一个Stream对象,可以链式调用多个中间操作。中间操作不会立即执行,只有在终端操作调用时才会触发执行。中间操作中大多数的参数都是函数式接口类型参数,所以可以使用lambda表达式来替代调用

filter(Predicate)

根据指定的条件过滤Stream中的元素,只保留满足条件的元素

Stream<Integer> stream = Stream.of(1, 3, 4, 6, 10);
// filter 过滤偶数
stream.filter(num -> num % 2 == 0);
// 输出流(这是终止操作)
stream1.forEach(System.out::println);
// 最终输出 4 6 10 

因为流式中间操作返回的还是一个Stream,所以可以用链式写法(以下均使用链式写法)。等同于如下

Stream.of(1, 3, 4, 6, 10)
    .filter(num -> num % 2 == 0)
    .forEach(System.out::println);

map(Function)

对Stream中的每个元素应用指定的函数,将其转换为另一种类型

Stream.of(1, 3, 4, 6, 10)
    .map(num -> num * 2) // 每个数×2
    .forEach(System.out::println);
// 最终输出2 6 8 12 20

distinct()

去除Stream中重复的元素,根据元素的hashCode()和equals()方法判断是否重复

Stream.of(1, 3, 3, 4)
    .distinct()
    .forEach(System.out::println);
// 结果输出 1 3 4

flatMap(Function)

将多个Stream组合成新的Stream

// 统计两篇文章出现过的单词数(去重)
String article1 = "hello world";
String article2 = "hello stream";
long count = Stream.of(article1, article2)  // 这里构建了字符串的流
    // flatMap 对流中每个字符串用空格分割成数组,然后把数组再生成流,此时可以看成
    // 合并了 Stream.of("hello", "world"), Stream.of("hello", "stream")
    // 合并新的流后 等价于 Stream.of("hello","world","hello","stream")
    .flatMap(art -> Arrays.stream(art.split(" "))) 
    .distinct()// 去重
    .count(); // 终止操作,统计数量

System.out.println(count);
// 输出3

sorted()

按自然顺序排序

Stream.of(3, 4, 9, 5, 1)
    .sorted()
    .forEach(System.out::println);
// 输出 1 3 4 5 9

sorted(Comparator)

按指定的比较器(Comparator)进行排序,

Stream.of(3, 4, 9, 5, 1)
    .sorted((a, b) -> b - a).
    .forEach(System.out::println);
// 输出 9 5 4 3 1
关于Comparator的用法
  • Comparator的抽象方法
int compare(T o1, T o2);

所以,对应lambda表达式形式:(a, b) -> { return (int)}

  • Comparator.comparing

    这个方法可以直接让我们简单指定以某个字段作为比较对象

    // comparing方法源码
    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
                Function<? super T, ? extends U> keyExtractor)
        {
            Objects.requireNonNull(keyExtractor);
            return (Comparator<T> & Serializable)
                (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
        }
    
    

    例如,我们想对学生的成绩分数排名。

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
static class Student{
    private String name;
    private Integer score;
}

public static void main(String[] args) {
    Student s1 = new Student("Tom", 95);
    Student s2 = new Student("Jerry", 90);
    // 常规写法
    Stream.of(s1, s2)
        .sorted((a, b) -> a.getScore() - b.getScore())
        .forEach(System.out::println);

	// 使用 Comparator.comparing 方法
    Stream.of(s1, s2)
        .sorted(Comparator.comparing(Student::getScore))
        .forEach(System.out::println);
    // 若根据姓名排序
    Stream.of(s1, s2)
        .sorted(Comparator.comparing(Student::getName))
        .forEach(System.out::println);
}

  • reversed方法

    当然,我们可能更多时候希望成绩是降序排序的,那么还能使用 Comparator.comparing 方法吗?还是得回归到常规写法呢?此时可以使用reversed方法

// 使用 Comparator.comparing 方法, 并用reversed降序排序(reversed 反转)
Stream.of(s1, s2)
    .sorted(Comparator.comparing(Student::getScore).reversed())
    .forEach(System.out::println);

细心的网友可能发现:Comparator.comparing方法要求的是一个Function接口,为什么 Student的getScore 是一个无参的方法。

那么为什么 Student::getScore 可以作为Function???后面再来解释这个问题

  • thenComparing方法

    有时候我们需要多个字段排序。比如按学生成绩排名后,如果一样分数我们希望按name排序。

Stream.of(s1, s2, s3, s4)
    .sorted(Comparator.comparing(Student::getScore)
            .reversed()
            .thenComparing(Student::getName))
    .forEach(System.out::println);

输出结果

StudentScoreDemo.Student(name=Jerry, score=98)
StudentScoreDemo.Student(name=Lucy, score=98)
StudentScoreDemo.Student(name=Tom, score=98)
StudentScoreDemo.Student(name=Jack, score=95)

扩展:如果按成绩降序后,依然希望按名称也是降序的情况

Stream.of(s1, s2, s3, s4)
    .sorted(Comparator.comparing(Student::getScore)
            .reversed()
            .thenComparing(Student::getName, Comparator.reverseOrder()))
    .forEach(System.out::println);

输出结果

StudentScoreDemo.Student(name=Tom, score=98)
StudentScoreDemo.Student(name=Lucy, score=98)
StudentScoreDemo.Student(name=Jerry, score=98)
StudentScoreDemo.Student(name=Jack, score=95)

这里要注意的是这里使用的是 thenComparing的第二个参数制定了反转顺序,而不是在thenComparing 方法后跟上reversed().因为如果thenComparing执行后再加上reversed 就相当于是上一个例子的结果做了反转。

limit(long)

限制个数,只取前n个

// 输出 1 2
Stream.of(1, 2, 3, 4, 5).limit(2).forEach(System.out::println);

skip(long)

跳过前n个

// 输出 3 4 5
Stream.of(1, 2, 3, 4, 5).skip(2).forEach(System.out::println);

终止操作

终止操作是指最终将Stream中的元素处理完毕,终止操作后不再是一个stream结果。

forEach(Consumer)

针对Stream的每个元素应用指定操作,并将元素消费掉

// 将流中元素依次输出
Stream.of(1, 2, 3, 4, 5).forEach(System.out::println);

// 一般的forEach用法。
Stream.of(1, 2, 3, 4, 5).forEach(num -> {
    // do something
})

collect(Collector)

将Stream中的元素进行收集。

我们使用stream操作,最终目的要么处理流,要么得到一个结果,所以收集方法也是最常用的方法之一。

关于Collector & Collectors

Collector是collect方法的参数,而Collectors则是jdk提供的生成各种Collector的工具类,类似于 Collections之于Collection。 以下主要介绍 Collectors工具类。

Collectors

1、toList()

收集为list

List<Student> list = Stream.of(s1, s2).collect(Collectors.toList());
2、toSet()

收集为set

Set<Student> set = Stream.of(s1, s2).collect(Collectors.toSet());
3、toMap() & toConcurrentMap()

收集为map 或 ConcurrentMap

Stream中元素如何拆分成Map中的key和value?这就需要收集器指定了,所以toMap 需要指定参数,Collectors提供了3个重载方法

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
        = (map, element) -> map.merge(keyMapper.apply(element),
                                      valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}


最终都会执行第三个方法,即除了指定key和value外,还需要指定key重复时的合并方式,以及指定Map容器。

我们现在将学生生成map, key为学生的名称, value为学生对象。

Map<String, Student> studentMap = Stream.of(s1, s2).collect(
    Collectors.toMap(
        Student::getName,   // 指定name为key
        Function.identity(),  // 指定 student为对象
        (v1, v2) -> v1,  // 当key重复时 value取前一个。
        HashMap::new // 指定使用新建hashMap对象
    )
);

一般来说第四个参数比较不需要,当然如果你想要将收集起来的map添加到已有的map中, 可以使用Supplier提供map。

Function.identity() 相当于 表达式:v -> v , 即输入参数原样输出。

// identity() 方法源码
static <T> Function<T, T> identity() {
    return t -> t;
}

建议指定key重复时value的处理方式,因为流收集时在碰到key重复时并不会以覆盖原值的方式进行处理,而是会报错。所以,最常用的toMap 方法应该是3个参数的这个方法。

4、groupingBy 分组

分组,可以看成是按某个维度将集合分割成多个集合。

例如:按班级分组,即按班级这个维度将学生这个集合分割成多个集合(每个班级一个集合),同样还可以继续按性别继续分组等等。

所以分组有2个要点

  1. 以什么为分组依据(Key)
  2. 分割后依然是集合(Collection),依然可以再次进行集合的收集操作。

所以,分组后可以使用Map来存储分组结果。

即 Map<Key, Collection>。

Collectors.groupingBy的3个重载方法

有了上面的理解,我们从源码入手

public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
}

public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
    return groupingBy(classifier, HashMap::new, downstream);
}

public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
	// 分组逻辑,此处省略
}

3个参数:

  • classifier : 指定以什么作为分组 (Key),这里的key将作为Map容器中的key。
  • mapFactory: 分组存放容器,一般为 Map, (默认使用了HashMap::new, 见第二个方法)
  • downstream:分组之后的元素要如何收集,(默认使用 toList(),即收集为List,见第一个方法),所以这里也可以使用Collectors.toList(), Collectors.toSet(), Collectors.toMap(),甚至可以再进行分组Collectors.groupingBy操作。

依然以学生为例

// 按学生年级分组
List<Student> list = new ArrayList<>();  //list存储所有学生。
Map<Integer, List<Student>> gradeStudentMap = list.stream().collect(Collectors.groupingBy(Student::getGrade)); // 默认收集方式 toList()

// 按年级分组后统计每个年级的学生总人数
Map<Integer, Long> countMap = list.stream().collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));
5、mapping

映射:将一个对象映射成另一个对象,mapping大多数情况配合groupingBy使用,类似于map操作,只不过它是收集时使用,没有办法再得到流。

我们把上一个栗子的需求改一下:按学生年级分组后,只需要用到学生的name

// 这是groupingBy的例子
Map<Integer, List<Student>> gradeStudentMap = list.stream().collect(Collectors.groupingBy(Student::getGrade)); 
// 现在只需要学生名字即 结果是 Map<Integer, List<String>>
 Map<Integer, List<String>> result = list.stream()
     .collect(
         Collectors.groupingBy(
             Student::getGrade,
             // 参数1 表示Student只映射 name字段
             // 参数2 表示映射后以list方式收集
             Collectors.mapping(Student::getName, Collectors.toList())
         ));
6、joining

连接操作,将流中的字符串元素以分隔符连接。

3个重载方法

public static Collector<CharSequence, ?, String> joining() {
    return new CollectorImpl<CharSequence, StringBuilder, String>(
   
}

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
    
}

public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
    
}
String str1 = Stream.of("hello", "Stream", "API").collect(Collectors.joining());
String str2 = Stream.of("hello", "Stream", "API").collect(Collectors.joining(","));
String str3 = Stream.of("hello", "Stream", "API").collect(Collectors.joining(",", "[", "]"));
System.out.println(str1); // helloStreamAPI
System.out.println(str2); // hello,Stream,API
System.out.println(str3); // [hello,Stream,API]

reduce(BinaryOperator)

将Stream中的元素逐个进行二元运算,并返回最终结果。reduce是一种递减操作,可以将n个元素最终缩减到只有一个元素,从这点上来说也可以看成是归纳操作。

reduce可以接受一个初始值开始计算。所以,jdk提供了3个reduce重载方法中的其中两个如下(第三个比较特殊)

T reduce(T identity, BinaryOperator<T> accumulator);

Optional<T> reduce(BinaryOperator<T> accumulator);

  • 第一个方法接受一个初始值,并提供二元操作方法
  • 第二个参数没有初始值,仅提供二元操作方法,注意此次返回值是Optional包装(因为考虑到流中可能没有元素或产生空值),需要执行get()方法才能获取原值

注:BinaryOperator 的函数式接口声明如下:

public interface BinaryOperator<T> extends BiFunction<T,T,T>

栗子1:求和

Integer sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).reduce((a, b) -> {
    System.out.println("a:" + a + ",b:" + b);
    return a + b;
}).get();
System.out.println("sum:" + sum);

为了方便我们清楚执行过程,加了输出语句

输出结果如下:

a:1,b:2
a:3,b:3
a:6,b:4
a:10,b:5
a:15,b:6
a:21,b:7
a:28,b:8
a:36,b:9
a:45,b:10
最终结果:55

可以看到,从第二行开始,a的值等于上一行 a+b, 即每次二元操作的结果将作为下一次二元操作的第一个输入参数。

栗子2:字符串拼接

String s = Stream.of("hello", "java", "8", "stream", "api").reduce("这是我构建的语句:", (a, b) -> {
    System.out.println("a:" + a + ",b:" + b);
    return a + " " + b;
});
System.out.println("最终结果:" + s);

控制台输出

a:这是我构建的语句:,b:hello
a:这是我构建的语句: hello,b:java
a:这是我构建的语句: hello java,b:8
a:这是我构建的语句: hello java 8,b:stream
a:这是我构建的语句: hello java 8 stream,b:api
最终结果:这是我构建的语句: hello java 8 stream api

可以看到,当提供了初始值时,二元操作第一步会将初始值和第一个元素先执行一次。同时,结果会直接返回string,而不是Optional。

第三个reduce重载方法

此方法的第三个参数需要在并行流中才会生效。

<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

参数说明:

  • identity: 初始化最终返回类型的实例。
  • accumulator: 声明identity(类型U)上 操作 数据来源的逻辑。并最终返回identity类型。这里与第一个重载方法不同的是,这里对应的是流中的每个元素都是在identity的基础上操作的。
  • combiner: 此参数需要在并行流中才会生效,在并行流中,多线程分别执行accumulator操作,combiner可以对线程操作后的结果进行合并。

栗子:没有实际意义,通过这个栗子看下整个过程

List<Integer> initList = new ArrayList();
for (int i = 1; i < 10; i++) {
    // 数字1-9
    initList.add(i);
}

List<Integer> result = initList.parallelStream()
    .reduce(
        // 参数1: 声明返回类型是一个List,并初始化,这里使用CopyOnWriteArrayList满足并发操作
        new CopyOnWriteArrayList<>(),
        // 在参数1(list)的基础上对stream的元素操作逻辑
        // 这里简单将元素添加到list,最终返回list
        (list, num) -> {
            System.out.println("accumulator 操作:list:" + list + ",num:" + num);
            list.add(num);
            return list;
        },
        // 第二个参数可以看成是一个线程操作的结果,多线程下每2个线程的操作结果做合并
        (list1, list2) -> {
            System.out.println("combiner 操作:list1:" + list1 + ",list2:" + list2);
            // 这里为什么只返回list1?(当然返回list2也是一样)
            // 因为 我们这里第二个参数用list.add,即对list来说元素已经加进去了。这里的list1, list2始终会保持一致
            return list1;
        });

输出结果

accumulator 操作:list:[],num:6
accumulator 操作:list:[],num:2
accumulator 操作:list:[],num:4
accumulator 操作:list:[],num:9
accumulator 操作:list:[],num:7
accumulator 操作:list:[],num:8
accumulator 操作:list:[],num:5
accumulator 操作:list:[],num:3
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5],list2:[6, 2, 4, 9, 7, 8, 5]
combiner 操作:list1:[6, 2, 4, 9, 7, 8],list2:[6, 2, 4, 9, 7, 8]
accumulator 操作:list:[],num:1
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3],list2:[6, 2, 4, 9, 7, 8, 5, 3]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3],list2:[6, 2, 4, 9, 7, 8, 5, 3]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
combiner 操作:list1:[6, 2, 4, 9, 7, 8, 5, 3, 1],list2:[6, 2, 4, 9, 7, 8, 5, 3, 1]
  • accumulator操作时,大部分的list都是空数组,说明并行流在初始操作时,启动了较多的线程。如果我们设定的线程流个数较多(比如从100个数),那么可以看到中间的过程中添加新元素时list已经有元素了。

  • combiner打印出来的list数据,一般list1和list2都一样的元素。

  • 为什么combiner后面打印的list比之前打印的少?这一点没有想通。

count()

计算stream中的元素数量,返回long类型的数值

long count = Stream.of(1, 2, 3, 4, 5).count(); // 5

anyMatch(Predicate)

至少一个满足条件。判断Stream中是否存在满足指定条件的元素,如果存在则返回true,否则返回false

allMatch(Predicate)

都满足条件。判断Stream中所有元素是否都满足指定条件,如果都满足则返回true,否则返回false

noneMatch(Predicate)

都不满足条件。判断Stream中是否存在不满足指定条件的元素,如果存在则返回true,否则返回false。

boolean result1 = Stream.of(1, 2, 3, 4, 5).anyMatch(num -> num > 3);  // true, 存在大于3
boolean result2 = Stream.of(1, 2, 3, 4, 5).allMatch(num -> num > 3);  // false, 都大于3
boolean result3 = Stream.of(1, 2, 3, 4, 5).noneMatch(num -> num > 5); // true, 都不大于5

为什么 Student::getScore 可以作为Function

常规写法:

Function<Student, Integer> function = (student) -> student.getScore()

首先,getScore 方法是实例方法,而Student::getScore是使用类引用实例方法(一般这样的引用方式是静态方法引用),那么我们可以认为当使用 类::实例方法 引用时,是需要额外传入实例对象的,对应的lambda表达式就需要额外的参数(第一个参数)接收。

所以,Student::getScore() 可以看成需要额外 student实例, 再使用student.getScore() 方法。

即(student) -> student.getScore(), 与常规写法等价,所以

Function<Student, Integer> function = Student::getScore;

同理: Student::setScore 等价于Lambda表达式

(student, score) -> student.setScore(score); 

对应的函数式接口 BiConsumer<Student, Integer>

// BiConsumer 抽象方法: void accept(T t, U u);
BiConsumer<Student, Integer> biConsumer = Student::setScore;

有兴趣的同学可以尝试更多参数的情况(参考答案写在最后):

// 这样的函数式接口(lambda)所对应的成员方法引用应该怎么实现。
@FunctionalInterface
public interface MyFunctionInterface<T, A, B, C, D, R> {
    R function(T t, A a, B b, C c, D d);
}

并行流

并行流将流数据分成多块,并使用多个线程分别处理后再做合并。使用的是Fork/Join框架处理。

  • 一般集合创建并行流可以使用 parallelStream()
List<String> list = new ArrayList<>();
list.stream();// 顺序流
list.parallelStream(); // 并行流
  • 顺序流转并行流
Stream.of(1, 2, 3).parallel(); // 将顺序流转为并行流

简单看个栗子:求和 (将reduce的栗子改成并行流)

Integer sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).parallel().reduce((a, b) -> {
    System.out.println("a:" + a + ",b:" + b);
    return a + b;
}).get();
System.out.println("sum:" + sum);

输出结果

a:6,b:7
a:9,b:10
a:1,b:2
a:4,b:5
a:3,b:9
a:3,b:12
a:8,b:19
a:13,b:27
a:15,b:40
sum:55

执行顺序不再是从1开始,而且过程中已经并行流自行做了合并操作。

IDEA如何调试Stream

1、将断点打在流式语句上(行断点)。

2、Debug调试启动,当执行到断点时,点击下图所示图标

3、此时会根据当前流式操作展示流跟踪视图,顶部可以切换流操作查看逻辑

4、点击左下角可以切换视图模式:SPLIT MODE和FLAT MODE. 流调试可以方便我们在流操作过程中定位查找流操作的问题。

前面遗留的问题答案(仅做参考):

static class Test {
    // 这里简化了返回值类型,指定为Integer类型
    private <A, B, C, D> Integer func(A a, B b, C c, D d) {
        return 0;
    }
}

 public static void main(String[] args) {
     // 第一个参数为调用实例对象类型, 最后一个为返回值类型, 中间四个因为func方法声明的是泛型,这里可以任意指定
     MyFunctionInterface<Test, String, Integer, Number, Object, Integer> myFunctionInterface = Test::func;
 }

写在最后, 欢迎关注公众号" Java后端架构技术进阶 "

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值