Java函数式编程

一、初识函数对象化

如果一个接口中只有一个抽象方法,且抽象方法的参数和返回类型与lambda表达式的参数和返回结果一致,那么就可以将接口类型作为lambda表达式的函数对象类型

java 复制代码
interface Lambda {
    int calculate(int a, int b);
}

Lambda add = (a, b) -> a + b; // 它已经变成了一个 lambda 对象
add.calculate(2,3);  //调用

函数对象化的好处

  • 行为参数化:函数对象化就可以将方法规则作为参数传递给另一个函数
java 复制代码
interface Lambda {
    boolean test(Student student);
}
static List<Student> filter(List<Student> students, Lambda lambda) {
    List<Student> result = new ArrayList<>();
    for (Student student : students) {
        if (lambda.test(student)) {
            result.add(student);
        }
    }
    return result;
}

挑选出性别为男的人

filter(students, student -> student.sex.equals("男"));

挑选出性别为男且年龄小于18岁的人

filter(students, student -> student.sex.equals("男") && student.age < 18);
  • 延迟执行

二、函数式编程语法

表现形式

lambda 对象有两种形式:lambda 表达式与方法引用,lambda表达式和方法引用都代表一个函数对象,lambda表达式功能更全面,方法引用写法更为简洁

lambda 对象的类型是由它的行为决定的,如果有一些 lambda 对象,它们的入参类型、返回值类型都一致,那么它们可以看作是同一类的 lambda 对象,它们的类型,用函数式接口来表示

lambda简化规则

1、明确指出参数类型

java 复制代码
(int a,int b) -> a + b

2、代码多于一行不能省略 {} 以及 return

java 复制代码
(int a,int b) -> { int c = a + b; return c;}

3、可以根据上下文推断出参数类型时,可以省略参数类型

java 复制代码
interface Lambda1 {                   interface Lambda2 {
   int op(int a, int b);                 double op(double a, double b);
}                                     }

Lambda1 lambda = (a, b) -> a + b;

4、只有一个参数时可以省略参数的()

java 复制代码
a -> a

函数式接口

lambda表达式的参数个数类型和返回类型相同,就可以吧函数对象归为一类用函数式接口表示,该接口只能有一个抽象方法,用@FunctionalInterface来检查

jdk中常见的函数式接口

  1. Runnable

    ()-> void

  2. Callable

    ()-> T

  3. Comparator

    (T,T) -> int

  4. Consumer,BiConsumer,LongConsumer,DoubleConsumer

    (T) -> void Bi指两参,Int指参数为int

  5. Function,BiFunction,Int Long Double ...

    (T) -> R 有参数和返回值

  6. Predicate,BiPredicate,Int Long Double ...

    (T) -> boolean 返回值为布尔类型

  7. Supplier,Int Long Double ...

    () -> T 无参有返回值

  8. UnaryOperator,BinaryOperator,Int Long Double ...

    (T) -> T 参数和返回值结果一致

方法引用

1)类名::静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此静态方法
  • 因此这个静态方法需要什么参数,函数对象也提供相应的参数即可
java 复制代码
public class Type2Test {
    public static void main(String[] args) {
        /*
            需求:挑选出所有男性学生
         */
        Stream.of(
                        new Student("张无忌", "男"),
                        new Student("周芷若", "女"),
                        new Student("宋青书", "男")
                )
                .filter(Type2Test::isMale)
                .forEach(student -> System.out.println(student));
    }

    static boolean isMale(Student student) {
        return student.sex.equals("男");
    }

    record Student(String name, String sex) {
    }
}
  • filter 这个高阶函数接收的函数类型(Predicate)是:一个 T 类型的入参,一个 boolean 的返回值
    • 因此我们只需要给它提供一个相符合的 lambda 对象即可
  • isMale 这个静态方法有入参 Student 对应 T,有返回值 boolean 也能对应上,所以可以直接使用

输出

Student[name=张无忌, sex=男]
Student[name=宋青书, sex=男]

2)类名::非静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此非静态方法
  • 因此这个函数对象需要提供一个额外的对象参数,以便能够调用此非静态方法
  • 非静态方法的剩余参数,与函数对象的剩余参数一一对应

例1:

java 复制代码
public class Type3Test {
    public static void main(String[] args) {
        highOrder(Student::hello);
    }

    static void highOrder(Type3 lambda) {
        System.out.println(lambda.transfer(new Student("张三"), "你好"));
    }

    interface Type3 {
        String transfer(Student stu, String message);
    }

    static class Student {
        String name;

        public Student(String name) {
            this.name = name;
        }

        public String hello(String message) {
            return this.name + " say: " + message;
        }
    }
}

上例中函数类型的

  • 参数1 对应着 hello 方法所属类型 Student
  • 参数2 对应着 hello 方法自己的参数 String
  • 返回值对应着 hello 方法自己的返回值 String

输出

张三 say: 你好

例2:改写之前根据性别过滤的需求

java 复制代码
public class Type2Test {
    public static void main(String[] args) {
        /*
            需求:挑选出所有男性学生
         */
        Stream.of(
                        new Student("张无忌", "男"),
                        new Student("周芷若", "女"),
                        new Student("宋青书", "男")
                )
                .filter(Student::isMale)
                .forEach(student -> System.out.println(student));
    }

    record Student(String name, String sex) {
        boolean isMale() {
            return this.sex.equals("男");
        }
    }
}
  • filter 这个高阶函数接收的函数类型(Predicate)是:一个 T 类型的入参,一个 boolean 的返回值
    • 因此我们只需要给它提供一个相符合的 lambda 对象即可
  • 它的入参1 T 对应着 isMale 非静态方法的所属类型 Student
  • 它没有其它参数,isMale 方法也没有参数
  • 返回值都是 boolean

输出

Student[name=张无忌, sex=男]
Student[name=宋青书, sex=男]

例3:将学生对象仅保留学生的姓名

java 复制代码
public class Type2Test {
    public static void main(String[] args) {
        Stream.of(
                        new Student("张无忌", "男"),
                        new Student("周芷若", "女"),
                        new Student("宋青书", "男")
                )
                .map(Student::name)
                .forEach(student -> System.out.println(student));
    }

    record Student(String name, String sex) {
        boolean isMale() {
            return this.sex.equals("男");
        }
    }
}
  • map 这个高阶函数接收的函数类型是(Function)是:一个 T 类型的参数,一个 R 类型的返回值
  • 它的入参1 T 对应着 name 非静态方法的所属类型 Student
  • 它没有剩余参数,name 方法也没有参数
  • 它的返回值 R 对应着 name 方法的返回值 String

输出

张无忌
周芷若
宋青书

3)对象::非静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此非静态方法
  • 因为对象已提供,所以不必作为函数对象参数的一部分
  • 非静态方法的剩余参数,与函数对象的剩余参数一一对应
java 复制代码
public class Type4Test {
    public static void main(String[] args) {
        Util util = new Util(); // 对象
        Stream.of(
                        new Student("张无忌", "男"),
                        new Student("周芷若", "女"),
                        new Student("宋青书", "男")
                )
                .filter(util::isMale)
                .map(util::getName)
                .forEach(student -> System.out.println(student));
    }

    record Student(String name, String sex) {
        boolean isMale() {
            return this.sex.equals("男");
        }
    }

    static class Util {
        boolean isMale(Student student) {
            return student.sex.equals("男");
        }
        String getName(Student student) {
            return student.name();
        }
    }
}

其实较为典型的一个应用就是 System.out 对象中的非静态方法,最后的输出可以修改为

java 复制代码
.forEach(System.out::println);

这是因为

  • forEach 这个高阶函数接收的函数类型(Consumer)是一个 T 类型参数,void 无返回值
  • 而 System.out 对象中有非静态方法 void println(Object x) 与之一致,因此可以将此方法化为 lambda 对象给 forEach 使用

4)类名::new

对于构造方法,也有专门的语法把它们转换为 lambda 对象

函数类型应满足

  • 参数部分与构造方法参数一致
  • 返回值类型与构造方法所在类一致

例如:

java 复制代码
public class Type5Test {
    static class Student {
        private final String name;
        private final int age;

        public Student() {
            this.name = "某人";
            this.age = 18;
        }

        public Student(String name) {
            this.name = name;
            this.age = 18;
        }

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    interface Type51 {
        Student create();
    }

    interface Type52 {
        Student create(String name);
    }

    interface Type53 {
        Student create(String name, int age);
    }

    public static void main(String[] args) {
        hiOrder((Type51) Student::new);
        hiOrder((Type52) Student::new);
        hiOrder((Type53) Student::new);
    }

    static void hiOrder(Type51 creator) {
        System.out.println(creator.create());
    }

    static void hiOrder(Type52 creator) {
        System.out.println(creator.create("张三"));
    }

    static void hiOrder(Type53 creator) {
        System.out.println(creator.create("李四", 20));
    }
}

5)this::非静态方法名

算是形式2的特例,只能用在类内部

java 复制代码
public class Type6Test {
    public static void main(String[] args) {
        Util util = new UtilExt();
        util.hiOrder(Stream.of(
                new Student("张无忌", "男"),
                new Student("周芷若", "女"),
                new Student("宋青书", "男")
        ));
    }

    record Student(String name, String sex) {

    }

    static class Util {
        boolean isMale(Student student) {
            return student.sex.equals("男");
        }

        boolean isFemale(Student student) {
            return student.sex.equals("女");
        }

        void hiOrder(Stream<Student> stream) {
            stream
                    .filter(this::isMale)
                    .forEach(System.out::println);
        }
    }
}

6)super::非静态方法名

算是形式2的特例,只能用在类内部(用在要用 super 区分重载方法时)

java 复制代码
public class Type6Test {
	
    //...
    
    static class UtilExt extends Util {
        void hiOrder(Stream<Student> stream) {
            stream
                    .filter(super::isFemale)
                    .forEach(System.out::println);
        }
    }
}

7)特例

函数接口和方法引用之间,可以差一个返回值,例如

java 复制代码
public class ExceptionTest {
    public static void main(String[] args) {
        Runnable task1 = ExceptionTest::print1;
        Runnable task2 = ExceptionTest::print2;
    }
    
    static void print1() {
        System.out.println("task1 running...");
    }

    static int print2() {
        System.out.println("task2 running...");
        return 1;
    }
}
  • 可以看到 Runnable 接口不需要返回值,而实际的函数对象多出的返回值也不影响使用

三、Stream API

特性

  1. 一次使用:流只能使用一次(终结方法只能调用一次)
  2. 两类操作:
    1. 中间操作,lazy 懒惰的
    2. 终结操作,eager 迫切的

生成方式

构建

  1. 根据已有的数组构建流:Arrays.stream(array)
  2. 根据已有的 Collection 构建流:List.of("a","b","c").stream()(List,Set 等)
  3. 根据已有对象:Stream.of("x", "y")

生成

  1. 生成从 0 ~ 9 的数字
java 复制代码
IntStream.range(0, 10)

或者

java 复制代码
IntStream.rangeClosed(0, 9)
  1. 如果想订制,可以用 iterate 方法,例如下面生成奇数序列
java 复制代码
IntStream.iterate(1, x -> x + 2)
  • 参数1 是初始值
  • 参数2 是一个特殊 Function,即参数类型与返回值相同,它会根据上一个元素 x 的值计算出当前元素
  • 需要用 limit 限制元素个数
  1. 也可以用 iterate 的重载方法
java 复制代码
IntStream.iterate(1, x -> x < 10, x -> x + 2)
  • 参数1 是初始值
  • 参数2 用来限制元素个数,一旦不满足此条件,流就结束
  • 参数3 相当于上个方法的参数2

iterate 的特点是根据上一个元素计算当前元素,如果不需要依赖上一个元素,可以改用 generate 方法

例如下面是生成 5 个随机 int

java 复制代码
Stream.generate(()-> ThreadLocalRandom.current().nextInt()).limit(5)

不过如果只是生成随机数的话,有更简单的办法

java 复制代码
ThreadLocalRandom.current().ints(5)

如果要指定上下限,例如下面是生成从 0~9 的100个随机数

java 复制代码
ThreadLocalRandom.current().ints(100, 0, 10)

中间操作方法

过滤-filter

参数类型:Predicate

java 复制代码
record Fruit(String cname, String name, String category, String color) { }

Stream.of(
    new Fruit("草莓", "Strawberry", "浆果", "红色"),
    new Fruit("桑葚", "Mulberry", "浆果", "紫色"),
    new Fruit("杨梅", "Waxberry", "浆果", "红色"),
    new Fruit("核桃", "Walnut", "坚果", "棕色"),
    new Fruit("草莓", "Peanut", "坚果", "棕色"),
    new Fruit("蓝莓", "Blueberry", "浆果", "蓝色")
)

找到所有蓝色的浆果

方法1:

.filter(f -> f.category().equals("浆果") && f.color().equals("蓝色"))

方法2:让每个 lambda 只做一件事,两次 filter 相对于并且关系

.filter(f -> f.category.equals("浆果"))
.filter(f -> f.color().equals("蓝色"))

方法3:让每个 lambda 只做一件事,不过比方法2强的地方可以 or,and,nagate 运算

.filter(((Predicate<Fruit>) f -> f.category.equals("浆果")).and(f -> f.color().equals("蓝色")))

映射-map

参数类型:Function

java 复制代码
.map(f -> f.cname() + "酱")

扁平化(降维)-flapMap

参数类型:返回值为流的Function

例1:

java 复制代码
Stream.of(
        List.of(
                new Fruit("草莓", "Strawberry", "浆果", "红色"),
                new Fruit("桑葚", "Mulberry", "浆果", "紫色"),
                new Fruit("杨梅", "Waxberry", "浆果", "红色"),
                new Fruit("蓝莓", "Blueberry", "浆果", "蓝色")
        ),
        List.of(
                new Fruit("核桃", "Walnut", "坚果", "棕色"),
                new Fruit("草莓", "Peanut", "坚果", "棕色")
        )
)
    
.flatMap(Collection::stream)    

例2:

java 复制代码
Stream.of(
        new Order(1, List.of(
                new Item(6499, 1, "HUAWEI MateBook 14s"),
                new Item(6999, 1, "HUAWEI Mate 60 Pro"),
                new Item(1488, 1, "HUAWEI WATCH GT 4")
        )),
        new Order(1, List.of(
                new Item(8999, 1, "Apple MacBook Air 13"),
                new Item(7999, 1, "Apple iPhone 15 Pro"),
                new Item(2999, 1, "Apple Watch Series 9")
        ))
)

想逐一处理每个订单的商品

.flatMap(order -> order.items().stream())

拼接

Stream.concat(Stream.of("a","b","c"), Stream.of("d"))

截取

Stream.concat(Stream.of("a", "b", "c"), Stream.of("d"))
    .skip(1)
    .limit(2)
  • skip 是跳过几个元素

  • limit 是限制处理的元素个数

  • dropWhile 是 drop 流中元素,直到条件不成立,留下剩余元素

  • takeWhile 是 take 流中元素,直到条件不成立,舍弃剩余元素

查找与判断

下面的代码找到流中任意(Any)一个偶数

java 复制代码
int[] array = {1, 3, 5, 4, 7, 6, 9};

Arrays.stream(array)
    .filter(x -> (x & 1) == 0)
    .findAny()
    .ifPresent(System.out::println);
  • 注意 findAny 返回的是 OptionalInt 对象,因为可能流中不存在偶数
  • 对于 OptionalInt 对象,一般需要用 ifPresent 或 orElse(提供默认值)来处理

与 findAny 比较类似的是 firstFirst,它俩的区别

  • findAny 是找在流中任意位置的元素,不需要考虑顺序,对于上例返回 6 也是可以的
  • findFirst 是找第一个出现在元素,需要考虑顺序,对于上例只能返回 4
  • findAny 在顺序流中与 findFirst 表现相同,区别在于并行流下会更快

判断流中是否存在任意一个偶数

java 复制代码
Arrays.stream(array).anyMatch(x -> (x & 1) == 0)
  • 它返回的是 boolean 值,可以直接用来判断

判断流是否全部是偶数

java 复制代码
Arrays.stream(array).allMatch(x -> (x & 1) == 0)
  • 同样,它返回的是 boolean 值,可以直接用来判断

判断流是否全部不是偶数

java 复制代码
Arrays.stream(array).noneMatch(x -> (x & 1) == 0)
  • noneMatch 与 allMatch 含义恰好相反

去重与排序

已知有数据

java 复制代码
record Hero(String name, int strength) { }

Stream.of(
    new Hero("独孤求败", 100),
    new Hero("令狐冲", 90),
    new Hero("风清扬", 98),
    new Hero("东方不败", 98),
    new Hero("方证", 92),
    new Hero("任我行", 92),
    new Hero("冲虚", 90),
    new Hero("向问天", 88),
    new Hero("不戒", 88)
)

要求,首先按 strength 武力排序(逆序),武力相同的,按姓名长度排序(正序)

仅用 lambda 来解

java 复制代码
.sorted((a,b)-> {
    int res = Integer.compare(b.strength(), a.strength());
    return (res == 0) ? Integer.compare(a.nameLength(), b.nameLength()) : res; 
})

方法引用改写

java 复制代码
.sorted(
    Comparator.comparingInt(Hero::strength)
      .reversed()
      .thenComparingInt(Hero::nameLength)
)

其中:

  • comparingInt 接收一个 key 提取器(说明按对象中哪部分来比较),返回一个比较器
  • reversed 返回一个顺序相反的比较器
  • thenComparingInt 接收一个 key 提取器,返回一个新比较器,新比较器在原有比较器结果相等时执行新的比较逻辑

增加一个辅助方法

java 复制代码
record Hero(String name, int strength) {
    int nameLength() {
        return this.name.length();
    }
}

原理:

java 复制代码
.sorted((e, f) -> {
    int res =
        ((Comparator<Hero>) (c, d) ->
            ((Comparator<Hero>) (a, b) -> Integer.compare(a.strength(), b.strength()))
                .compare(d, c))
            .compare(e, f);
    return (res == 0) ? Integer.compare(e.nameLength(), f.nameLength()) : res;
})

如果不好看,改成下面的代码

java 复制代码
.sorted(step3(step2(step1())))

static Comparator<Hero> step1() {
    return (a, b) -> Integer.compare(a.strength(), b.strength());
}

static Comparator<Hero> step2(Comparator<Hero> step1) {
    return (c, d) -> step1.compare(d, c);
}

static Comparator<Hero> step3(Comparator<Hero> step2) {
    return (e, f) -> {
        int res = step2.compare(e, f);
        return (res == 0) ? Integer.compare(e.nameLength(), f.nameLength()) : res;
    };
}

化简

reduce(init, (p,x) -> r)

  • init 代表初始值
  • (p,x) -> r 是一个 BinaryOperator,作用是根据上次化简结果 p 和当前元素 x,得到本次化简结果 r

这样两两化简,可以将流中的所有元素合并成一个结果

流终结操作方法

Stream流的收集操作

收集

collect( supplier, accumulator, combiner)

  • supplier 创建什么类型的收集容器 c :()-> c
  • accumulator 向容器 c 添加元素 x的规则:(c, x) -> void
  • combiner 是描述如何合并两个容器:(c1, c2) -> void
    • 串行流下不需要合并容器
    • 并行流如果用的是并发容器,也不需要合并
java 复制代码
Stream<String> stream = Stream.of("令狐冲", "风清扬", "独孤求败", "方证",
                "东方不败", "冲虚", "向问天", "任我行", "不戒", "不戒", "不戒", "不戒");
    
	1) 收集到 List
    List<String> result = stream.collect(() -> new ArrayList<>(), (list, x) -> list.add(x), (a, b) -> { });
    ArrayList::new   ()->new ArrayList()
    ArrayList::add   (list,x)->list.add(x)
    List<String> result = stream.collect(ArrayList::new, ArrayList::add, (a, b) -> { });

    2) 收集到 Set
    Set<String> result = stream.collect(LinkedHashSet::new, Set::add, (a, b) -> { });

    3)收集到 Map
    Map<String, Integer> result = stream.collect(HashMap::new, (map,x)->map.put(x, 1), (a, b) -> { });

    4)收集到 StringBuilder
 StringBuilder sb = stream.collect(StringBuilder::new, StringBuilder::append, (a,b)->{});

    5)收集到 StringJoiner
    StringJoiner sb = stream.collect(()->new StringJoiner(","), StringJoiner::add, (a,b)->{});

收集器

collect收集操作的三个参数比较固定,所以jdk的Collectors 类中提供了很多现成的收集器

java 复制代码
    1) 收集到 List
    List<String> result = stream.collect(Collectors.toList());

    2) 收集到 Set
    Set<String> result = stream.collect(Collectors.toSet());

    3)收集到 StringBuilder
    String result = stream.collect(Collectors.joining());

    4)收集到 StringJoiner
    String result = stream.collect(Collectors.joining(","));

    5)收集到 Map
    Map<String, Integer> map = stream.collect(Collectors.toMap(x -> x, x -> 1));
	//toMap一般被groupingBy所代替
    steam.collect(Collectors.groupingBy(x->x.length(),Collectors.toList()))

下游收集器

groupingBy 分组收集时,组内可能需要进一步的数据收集,如上例的Collectors.toList()称为下游收集器

java 复制代码
    1. mapping(x->y, dc)  需求:根据名字长度分组,分组后组内只保留他们的武力值
                 new Hero("令狐冲", 90)->90
                 dc 下游收集器 down collector

                 stream.collect(groupingBy(h -> h.name().length(), mapping(h -> h.strength(), toList())));

     2. filtering(x->boolean, dc)  需求:根据名字长度分组,分组后组内过滤掉武力小于 90 的

     在分组收集的过程中,执行过滤
     stream.collect(groupingBy(h -> h.name().length(), filtering(h -> h.strength() >= 90, toList())));
     先过滤,再来分组收集
     stream.filter(h -> h.strength() >= 90).collect(groupingBy(h -> h.name().length(), toList()));

     3. flatMapping(x->substream, dc)     需求:根据名字长度分组,分组后组内保留人名,并且人名切分成单个字符

     "令狐冲".chars().mapToObj(Character::toString).forEach(System.out::println);

     stream.collect(groupingBy(h -> h.name().length(),
            flatMapping(h -> h.name().chars().mapToObj(Character::toString), toList())));

     4. counting() 需求:根据名字长度分组,分组后求每组个数

     stream.collect(groupingBy(h -> h.name().length(), counting()));

     5. minBy((a,b)->int) 需求:根据名字长度分组,分组后求每组武功最低的人
     6. maxBy((a,b)->int) 需求:根据名字长度分组,分组后求每组武功最高的人

      stream.collect(groupingBy(h -> h.name().length(), maxBy(Comparator.comparingInt(Hero::strength))));

     7. summingInt(x->int)            需求:根据名字长度分组,分组后求每组武力和
     8. averagingDouble(x->double)    需求:根据名字长度分组,分组后求每组武力平均值

      stream.collect(groupingBy(h -> h.name().length(), averagingDouble(h -> h.strength())));

     9. reducing(init,(p,x)->r)

     求和
      stream.collect(groupingBy(h -> h.name().length(), mapping(h -> h.strength(), reducing(0, (p, x) -> p + x))));
     求个数
      stream.collect(groupingBy(h -> h.name().length(), mapping(h -> 1, reducing(0, (p, x) -> p + x))));

    // 求平均,缺少 finisher
    Map<Integer, double[]> collect = stream.collect(groupingBy(h -> h.name().length(),
            mapping(h -> new double[]{h.strength(), 1},
                    reducing(new double[]{0, 0}, (p, x) -> new double[]{p[0] + x[0], p[1] + x[1]}))));

细节:使用import static静态导入引入Collectors类相当于说明啦静态方法从属于哪个类,就可以省略Collectors.

相关推荐
宅小海7 分钟前
scala String
大数据·开发语言·scala
qq_3273427310 分钟前
Java实现离线身份证号码OCR识别
java·开发语言
锅包肉的九珍11 分钟前
Scala的Array数组
开发语言·后端·scala
心仪悦悦14 分钟前
Scala的Array(2)
开发语言·后端·scala
yqcoder36 分钟前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
baivfhpwxf20231 小时前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
许嵩661 小时前
IC脚本之perl
开发语言·perl
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
直裾1 小时前
Scala全文单词统计
开发语言·c#·scala
心仪悦悦1 小时前
Scala中的集合复习(1)
开发语言·后端·scala