Java8 Stream流使用

Java8 Stream 流式编程

一.Lambda表达式

Lambda表达式也可以称为闭包,它是推动Java8发布的最重要新特性,lambda允许把函数作为一个方法参数传递给方法。

在Java8之前,如果我们新创建一个线程对象,需要使用匿名内部类传递我们要执行的任务,在Java8我们可以使用lambda简化这种操;

public static void main(String[] args) {
    // 匿名内部类
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("使用匿名内部类1");
        }
    }).start();

    // lambda表达式
    new Thread(() -> {
        System.out.println("使用匿名内部类2");
    }).start();
}

什么是函数式接口呢?它必须满足如下条件:

  1. 函数式接口只能包含一个方法
  2. 可以包含多个默认方法default(默认方法相当于已经实现的方法,默认方法不会影响lambda表达式对接口方法的实现)
  3. Object类下的方法不计算在内,例如:toString()、equals()、hashCode()等方法。

限制接口类只有一个抽象方法:@FunctionalInterface注解

例如 Runnble

image

TreeMap 的Comparator

image

二. 方法引用

我们可以将lambda表达式的实现逻辑封装成一个方法,然后直接在lambda表达式函数中调用封装好的方法,称为方法引用,方法引用包括静态方法引用和动态方法引用

无返回值的

public class TestFunction {

    public static void main(String[] args) {
        // 静态方法引用
        print(TestFunction::format1);
        // 普通方法引用
        print(new TestFunction()::format2);
    }

    public static void format1(String name, int age) {
        System.out.printf("name: %s, age: %s%n", name, age);
    }

    public void format2(String name, int age) {
        System.out.printf("name: %s, age: %s%n", name, age);
    }

    public static void print(PrintFunction function) {
        function.print("王大", 23);
    }
}

有返回值的

public class TestResultFunction {

    public static void main(String[] args) {
        // 静态方法引用
        String nameAndAge1 = getNameAndAge("张三", 18, TestResultFunction::format1);

        // 普通方法引用
        String nameAndAge2 = getNameAndAge("张三", 18, new TestResultFunction()::format2);
        // 使用函数调用
        ResultFunction resultFunction = (name, age) -> {
            return name + ":" + age;
        };

        String nameAndAge3 = resultFunction.getNameAndAge("张三", 18);

        System.out.println(nameAndAge1);
        System.out.println(nameAndAge2);
        System.out.println(nameAndAge3);
    }

    public static String format1(String name, int age) {
        return name + ":" + age;
    }

    public String format2(String name, int age) {
        return name + ":" + age;
    }

    public static String getNameAndAge(String name, Integer age, ResultFunction function) {
        return function.getNameAndAge(name, age);
    }
}

三. 四大内置核心函数式接口

因为我们不可能每次需要用到函数式接口就去定义一个接口,这样就是重复工作,所以java给我们按照需求的类型(消费型,供给型,函数型,断言型)提供了四个规范接口,以及他们的拓展变种接口;

1. 消费型接口

无返回值,只处理数据;例如 Stream.peek; forEach; Optional.ifPresent

Consumer<T>
    void accept(T t);

2. 供给型接口

没有参数,只返回数据,例如 Optional.orElseGet; Optional.orElseThrow;

Supplier<T>
    T get();

例如给缓存方法提供为空的值

public class CacheUtil {

    private static HashMap<String, Object> localCache = new ConcurrentHashMap<>();

    public <T> T get(String key, RedisSupplier<T> redisSupplier) {
        Object value = localCache.get(key);
        if (Objects.isNull(value)) {
            T result = redisSupplier.get();
            this.set(key, result, redisSupplier.getExpire(), redisSupplier.getTimeUnit());
            return result;
        }
        return (T) value;
    }
}

3. 函数型接口

提供参数加获取返回值,例如Stream.map; Optional.map; Map.compute; Stream.mapToInt; MybatisPlus.select; MybatisPlus.eq;

Function <T, R>
    R apply(T t);

4. 断言型接口

返回boolean类型值; 例如Stream.filter; Stream.anyMatch; Stream.allMatch;Optional.filter

Predicate<T>
    boolean test(T t);

四.Stream流提供的常用函数

Stream提供的方法分为两种,中间处理数据的方法,和结果集收集方法;

Stream流特性:

  1. 不存储数据
  2. 不改变源数据
  3. 不可重复使用

中间处理方法

函数 解释
map 数据处理,返回新的数据流
flatMap 数据维度降级(合并列表数据)
filter 过滤数据
peek 查看数据
distinct 去重
sorted 排序
limit 数据截取,默认从第一个开始
skip 跳过N个数据

终端收集方法

函数 解释
forEach 数据处理,返回新的数据流
max/min/count 最大值/最小值/计数
reduce 归约函数
anyMatch 至少匹配一个元素
allMatch 匹配所有元素
noneMatch 没有匹配到的所有元素
findFirst 在此流中查找第一个元素
findAny 在此流中查找任意一个元素,存在随机性,一般也是第一个,主要是在并行流中体现
toArray 转成数组
collect 收集器,将流转换为其他形式

collect收集方法

函数 解释
toList 将流中的元素收集到一个List中
toSet 将流中的元素收集到一个Set中
toCollection 将流中的元素收集到一个Collection中
toMap 将流中的元素映射收集到一个Map中
counting 统计流中的元素个数
summingInt 计算流中指定int字段的累加总和。针对不同类型的数字类型,有不同的方法,比如summingDouble等
averagingInt 计算流中指定int字段的平均值。针对不同类型的数字类型,有不同的方法,比如averagingLong等
joining 将流中所有元素(或者元素的指定字段)字符串值进行拼接,可以指定拼接连接符,或者首尾拼接字符
maxBy 根据给定的比较器,选择出值最大的元素
minBy 根据给定的比较器,选择出值最小的元素
groupingBy 根据给定的分组函数的值进行分组,输出一个Map对象
partitioningBy 根据给定的分区函数的值进行分区,输出一个Map对象,且key始终为布尔值类型
collectingAndThen 包裹另一个收集器,对其结果进行二次加工转换
reducing 从给定的初始值开始,将元素进行逐个的处理,最终将所有元素计算为最终的1个值输出

高级: 自定义Collector收集器,实现Collector接口

中间处理方法使用

map的使用

map函数的作用是遍历Collection中的元素,生成一个新的Collection

public class TestStream {

    @Test
    public void testMap() {
        UserInfo userInfo1 = new UserInfo("张三",18,"18273416040");
        UserInfo userInfo2 = new UserInfo("李四",20,"18273416040");
        UserInfo userInfo3 = new UserInfo("王五",17,"18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        System.out.println("*************抽取对象集合中的某个字段 返回数组***************");
        String[] usernameArrays = userInfos.stream().map(UserInfo::getUsername).toArray(String[]::new);
        System.out.println(Arrays.toString(usernameArrays));

        System.out.println("*************抽取对象集合中的某个字段 返回集合***************");
        List<String> usernameList = userInfos.stream().map(UserInfo::getUsername).collect(Collectors.toList());
        System.out.println(usernameList);

        System.out.println("*************对象属性修改***************");
        List<UserInfo> updateList = userInfos.stream().map(item -> {
            item.setAge(100);
            item.setMobile("123");
            return item;
        }).collect(Collectors.toList());
        System.out.println(updateList);

        System.out.println("*************对象集合转map集合***************");
        List<Map<String, Object>> mapList = userInfos.stream().map(BeanUtil::beanToMap).collect(Collectors.toList());
        System.out.println(mapList);


        System.out.println("*************map集合转对象集合***************");
        List<UserInfo> mapToBeamList = mapList.stream().map(item -> {
            UserInfo userInfo = new UserInfo();
            userInfo.setUsername(item.get("username").toString());
            userInfo.setMobile(item.get("mobile").toString());
            userInfo.setAge((Integer) item.get("age"));
            return userInfo;
        }).collect(Collectors.toList());

        System.out.println(mapToBeamList);
    }
}

FlatMap使用

flatmap用于集合的维度降级,也可以理解成把多个Stream流合成一个流;比如多维数组,集合中的元素中包含集合;

@Test
public void testFlatMap() {
    UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
    UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");

    List<String> addressList1 = new ArrayList<>();
    addressList1.add("北京市海淀区");
    addressList1.add("广州市天河区");
    userInfo1.setAddress(addressList1);

    List<String> addressList2 = new ArrayList<>();
    addressList2.add("广州市天河区");
    addressList2.add("广州市海珠区");
    userInfo2.setAddress(addressList2);

    List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2);
    System.out.println(userInfos);

    List<List<String>> collect1 = userInfos.stream().map(UserInfo::getAddress).collect(Collectors.toList());
    // 也可以distinct
    Set<String> collect2 = userInfos.stream().map(UserInfo::getAddress).flatMap(Collection::stream).
                collect(Collectors.toSet());

    System.out.println(collect1);
    System.out.println(collect2);
}

filter peek distinct sorted 使用

public class TestStream {

    @Test
    public void testPage() {
        String[] names = {"宋江", "卢俊义", "吴用", "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进"};
        String[] mobiles = {"10086", "10010"};
        List<UserInfo> userInfos = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            UserInfo userInfo = new UserInfo(names[i], i + 17, mobiles[i % 2]);
            userInfos.add(userInfo);
        }
        userInfos.forEach(System.out::println);
        System.out.println("\n**************************** filter ******************************\n");

        // filter
        List<UserInfo> filters = userInfos.stream().filter(item -> item.getAge() > 24).collect(Collectors.toList());
        System.out.println(filters);

        System.out.println("\n*****************************  peek ***************************\n");

        List<UserInfo> peeks1 = userInfos.stream().peek(System.out::println).collect(Collectors.toList());
//        List<UserInfo> peeks2 = userInfos.stream().peek(item -> item.setAge(0)).collect(Collectors.toList());
//        System.out.println(peeks2);

        System.out.println("\n*****************************  distinct ***************************\n");
        List<String> mobileList1 = userInfos.stream().map(UserInfo::getMobile).collect(Collectors.toList());
        List<String> mobileList2 = userInfos.stream().map(UserInfo::getMobile).distinct().collect(Collectors.toList());
        System.out.println(mobileList1);
        System.out.println(mobileList2);

        System.out.println("\n*****************************  sorted ***************************\n");
        // 自然顺序对流的元素进行排序。元素类必须实现Comparable接口
        List<UserInfo> sorted1 = userInfos.stream().sorted().collect(Collectors.toList());
        sorted1.forEach(System.out::println);

        // reverseOrder降序 naturalOrder升序
        List<UserInfo> sorted2 = userInfos.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
        List<UserInfo> sorted3 = userInfos.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());
        System.out.println("sorted2");
        sorted2.forEach(System.out::println);
        System.out.println("sorted3");
        sorted3.forEach(System.out::println);

        List<UserInfo> sorted4 = userInfos.stream().sorted(Comparator.comparing(UserInfo::getAge).reversed()).collect(Collectors.toList());
        System.out.println("sorted4");
        sorted4.forEach(System.out::println);

        List<UserInfo> sorted5 = userInfos.stream().sorted(Comparator.comparingInt(UserInfo::getAge).reversed()).collect(Collectors.toList());

        List<UserInfo> sorted6 = userInfos.stream().sorted((e1, e2) -> {
            if (Objects.equals(e2.getAge(), e1.getAge())) {
                return e1.getUsername().compareTo(e2.getUsername());
            }
            return Integer.compare(e2.getAge(), e1.getAge());
        }).collect(Collectors.toList());

        System.out.println("\n*****************************  limit ***************************\n");
        List<UserInfo> limit1 = userInfos.stream().limit(1).collect(Collectors.toList());
        List<UserInfo> limit2 = userInfos.stream().sorted(Comparator.comparing(UserInfo::getAge).reversed())
                .limit(1).collect(Collectors.toList());

        System.out.println(limit1);
        System.out.println(limit2);

    }
}

skip limit

limit方法,它是用于限制流中元素的个数,即取前n个元素,返回新的流;
skip()方法用于跳过前面n个元素,然后再返回新的流;

public class TestStream {

    @Test
    public void testSkipAndLimit() {
        String[] names = {"宋江", "卢俊义", "吴用", "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进"};
        String[] mobiles = {"10086", "10010"};
        List<UserInfo> userInfos = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            UserInfo userInfo = new UserInfo(names[i], i + 17, mobiles[i % 2]);
            userInfos.add(userInfo);
        }
        List<UserInfo> skip = userInfos.stream().skip(2).collect(Collectors.toList());
        List<UserInfo> limit = userInfos.stream().limit(2).collect(Collectors.toList());
        System.out.println("skip");
        System.out.println(skip);
        System.out.println("limit");
        System.out.println(limit);

        System.out.println("\n*****************************skip加limit 实现分页**********************\n");
        long pageSize = 3;

        long totalPage = 4;

        for (int pageIndex = 1; pageIndex <= totalPage; pageIndex++) {
            List<UserInfo> infoList = userInfos.stream().skip((pageIndex - 1) * pageSize).limit(pageSize).collect(Collectors.toList());
            System.out.println(infoList);
            System.out.println();
        }
    }
}

终端收集方法 使用

forEach

public class TestStreamCollection {

    @Test
    public void testForEach() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);
        userInfos.stream().forEach(item -> System.out.println(item));
    }
}

count

public class TestStreamCollection {
    @Test
    public void testCount() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        long count = userInfos.stream().count();
//        Long collect = userInfos.stream().collect(Collectors.counting());

        UserInfo maxUser = userInfos.stream().max(Comparator.comparing(UserInfo::getAge)).get();
//        UserInfo maxUser = userInfos.stream().max(Comparator.comparingInt(UserInfo::getAge)).get();
//        UserInfo maxUser = userInfos.stream().max((o1,o2)->o1.getAge()-o2.getAge()).get();

        UserInfo minUser = userInfos.stream().min(Comparator.comparing(UserInfo::getAge)).get();

        System.out.println("count:" + count);
        System.out.println("maxUser:" + maxUser);
        System.out.println("minUser:" + minUser);
    }
}

reduce

public class TestStreamCollection {
    
    @Test
    public void testReduce() {
        List<Integer> ids = Stream.of(1, 2, 3, 4).collect(Collectors.toList());
        // 两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,
        // 这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数
        //第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素
        Optional<Integer> reduce = ids.stream().reduce((acc, item) -> {
            acc += item;
            return acc;
        });

        reduce.ifPresent(System.out::println);


        // 从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素,因为开始值是已经存在的,不存在null的情况,所以返回值是确定的类型
        Integer reduce1 = ids.stream().reduce(2, (acc, item) -> {
            acc += item;
            return acc;
        });
        System.out.println(reduce1);


        //返回与集合中元素不同类型的值,方便我们对复杂对象做计算式和转换
        // 一个参数和两个参数的reduce()只能返回与集合中元素同类型的值。
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);
        Integer sum = userInfos.stream().parallel().reduce(0,
                new BiFunction<Integer, UserInfo, Integer>() {
                    @Override
                    public Integer apply(Integer integer, UserInfo userInfo) {
                        return integer + userInfo.getAge();
                    }
                }, new BinaryOperator<Integer>() {
                    @Override
                    public Integer apply(Integer integer, Integer integer2) {
                        System.out.println("integer1:" + integer + "--integer2:" + integer2);
                        return integer + integer2;
                    }
                });

        System.out.println(sum);
    }
}

match

public class TestStreamCollection {
    @Test
    public void testMatch() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        boolean anyMatch = userInfos.stream().anyMatch(item -> item.getAge() > 18);
        boolean allMatch = userInfos.stream().anyMatch(item -> item.getAge() > 17);
        boolean noneMatch = userInfos.stream().noneMatch(item -> item.getAge() > 18);

        System.out.println(anyMatch);
        System.out.println(allMatch);
        System.out.println(noneMatch);
    }
}

find

public class TestStreamCollection {
    
    @Test
    public void testFind() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        Optional<UserInfo> first = userInfos.stream().filter(item -> item.getAge() > 18).findFirst();

        Optional<UserInfo> any = userInfos.stream().findAny();

        first.ifPresent(System.out::println);
        any.ifPresent(System.out::println);
    }

}

toArray

public class TestStreamCollection {
    
    @Test
    public void testToArray() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        UserInfo[] userInfos1 = userInfos.stream().toArray(UserInfo[]::new);
        System.out.println(Arrays.toString(userInfos1));
    }
    
}

Collect

public class TestStreamCollection {
    
    @Test
    public void testCollect() {
        UserInfo userInfo1 = new UserInfo("张三", 18, "18273416040", "人力资源部");
        UserInfo userInfo2 = new UserInfo("李四", 20, "18273416040", "软件研发部");
        UserInfo userInfo3 = new UserInfo("王五", 17, "18273416040", "软件研发部");

        List<UserInfo> userInfos = Arrays.asList(userInfo1, userInfo2, userInfo3);

        // toList
        List<UserInfo> collect1 = userInfos.stream().collect(Collectors.toList());
        System.out.println(collect1);

        // toSet
        Set<UserInfo> collect2 = userInfos.stream().collect(Collectors.toSet());
        System.out.println(collect2);

        // toMap
        Map<String, Integer> collect3 = userInfos.stream().collect(Collectors.toMap(UserInfo::getUsername, UserInfo::getAge));
        Map<String, UserInfo> collect31 = userInfos.stream().collect(Collectors.toMap(UserInfo::getUsername, (item) -> item));
        System.out.println(collect3);

        // 求和  joining
        Double collect4 = userInfos.stream().collect(Collectors.averagingInt(UserInfo::getAge));
        String collect5 = userInfos.stream().map(UserInfo::getUsername).collect(Collectors.joining(","));
        System.out.println(collect4);
        System.out.println(collect5);

        // 年龄最大的人
        Optional<UserInfo> collect6 = userInfos.stream().collect(Collectors.maxBy(Comparator.comparing(UserInfo::getAge)));
        System.out.println(collect6);

        //分组
        Map<String, List<UserInfo>> collect7 = userInfos.stream().collect(Collectors.groupingBy(UserInfo::getDept));
        System.out.println(JSONUtil.toJsonStr(collect7));

        // 多重分组
        Map<String, Map<Integer, List<UserInfo>>> collectMap = userInfos.stream().collect(Collectors.groupingBy(UserInfo::getDept,
                Collectors.groupingBy(UserInfo::getAge)));
        System.out.println(JSONUtil.toJsonStr(collectMap));

        //分组统计数量
        Map<String, Long> collect8 = userInfos.stream().collect(Collectors.groupingBy(UserInfo::getDept,
                Collectors.counting()));
        Map<String, Integer> collect9 = userInfos.stream().collect(Collectors.groupingBy(UserInfo::getDept,
                Collectors.summingInt(UserInfo::getAge)));
        System.out.println(collect8);
        System.out.println(collect9);


        // collectingAndThen
        UserInfo collect10 = userInfos.stream().collect(Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparing(UserInfo::getAge)
                ), Optional::get));

        System.out.println(collect10);

    }
}

Stream在组合使用才能发货最大的优势,如果仅仅只是单一的操作,其他方法也许更简单高效;

parallel

并行流(Parallel Stream)使用ForkJoinPool实现并行性,利用所有可用CPU内核的优势,并行处理任务。如果任务数超过内核数,则其余任务将等待当前正在运行的任务完成。

可以通过 Runtime.getRuntime().availableProcessors()来获取当前计算机的CPU内核数量。 默认的线程数量就是处理器的数量,也可以通过设置系统属性来改变 System.setProperty("
java.util.concurrent.ForkJoinPool.common.parallelism", "12")

使用场景

Java 使用ForkJoinPool实现并行性,ForkJoinPool派生源流并提交执行;

  • 源数据流应该是可拆分的。例如:ArrayList的数据
  • 在处理问题的时候确实遇到性能问题,否则请不要为了并行而并行。
  • 需要确保线程之间的所有共享资源都是正确同步,否则可能会产生数据不一致问题。

下面的测试方法,cpu是AMD 5600G 情况下 未使用Parallel需要13秒,使用Parallel之后,2秒

public class ParallelStreamTest {

    @Test
    public void test1() {
        List<Integer> data = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            data.add(i);
        }

        Instant start = Instant.now();
        long sum = data.stream()
                .map(i -> (int) Math.sqrt(i))
                .map(ParallelStreamTest::performComputation)
                .reduce(0, Integer::sum);

        Instant end = Instant.now();

        System.out.println(sum);
        System.out.printf("Time taken to complete:%s秒", Duration.between(start, end).getSeconds());

    }

    @Test
    public void test2() {
        List<Integer> data = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            data.add(i);
        }

        Instant start = Instant.now();
        long sum = data.stream().parallel()
                .map(i -> (int) Math.sqrt(i))
                .map(ParallelStreamTest::performComputation)
                .reduce(0, Integer::sum);

        Instant end = Instant.now();

        System.out.println(sum);
        System.out.printf("Time taken to complete:%s秒", Duration.between(start, end).getSeconds());

    }


    public static int performComputation(int n) {
        int sum = 0;
        for (int i = 1; i < 100000; i++) {
            int a = (n / i);
            sum += a;
        }
        return sum;
    }
}

Stream操作debug

对stream操作的代码行打上断点,点击debug中的下图所示图标

image

此时会弹出一个界面,显示stream的所有操作(数据加载可能有延迟,出现没有数据的情况);上面的卡片选项代表stream流操作,下面对应的内容是操作的结果;一目了然,让我们可以清除的知道整个流执行的过程和结果;

image

image

Stream项目中使用场景

  1. 拉取第三方数据时,需要把数据处理完之后存库
  2. 集合数据同步对比(取交集,并集,差集)
  3. 无法使用数据库,或者数据库压力大时,在代码中对数据进行处理
posted @ 2022-07-04 17:50  房东的Tom  阅读(360)  评论(0编辑  收藏  举报