Java 8 引入的Stream API提供了一种新的数据处理方式,它以声明式、函数式的编程模型,极大地简化了对集合、数组或其他支持数据源的操作。Stream可以被看作是一系列元素的流水线。允许你高效地对大量数据执行复杂的过滤、映射、排序、聚合等操作,而无需显式地使用循环或者临时变量。Stream API的设计理念主要包括两个方面:链式调用惰性求值。链式调用允许我们将多个操作连接在一起,形成一个流水线,而惰性求值意味着只有在真正需要结果的时候才执行计算,从而避免了不必要的计算开销。

接下来我们就来盘点一下日常开发中常用的一些Stream API。

创建Stream

  • 集合创建
List<String> list = new ArrayList<>();
// 串行流
Stream<String> stream = list.stream();
// 并行流
Stream<String> parallelStream = list.parallelStream();
  • 数组创建
String[] strs = new String[3];
Stream<String> stream = Arrays.stream(strs);
  • 使用Stream.of(T...values)创建
Stream<String> stream = Stream.of("Apple", "Orange", "Banana");
  • 使用Stream.generate()创建流
// 生成一个无限流,通过limit()限制元素个数
Stream<Double> randomStream = Stream.generate(Math::random).limit(5);
  • 使用Stream.iterate()创建流
// 生成一个等差数列,通过limit()限制元素个数
Stream<Integer> integerStream = Stream.iterate(0, n -> n + 2).limit(5);
  • 使用IntStream、LongStream、DoubleStream创建原始类型流
// 使用IntStream创建
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4] // 使用LongStream创建
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

IntStream我们使用的地方还是比较多的,比如我们按照下标遍历一个集合时,同常的做法是:for(int i = 0; i < list.size(); i++){},我们可以使用IntStream去改造一下,IntStream.rangeClosed(0, list.size()).forEach();

中间操作

中间操作是构建流水线的一部分,用于对流进行转换和处理,但它们并不会触发实际的计算。

  • 过滤操作(filter)

    过滤操作用于筛选流中的元素,保留满足指定条件的元素。Stream<T> filter(Predicate<? super T> predicate)filter接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。
private static void filterTest(){
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
// 过滤长度大于5的水果
List<String> filteredFruits = fruits.stream().filter(fruit -> fruit.length() > 5).collect(Collectors.toList());
System.out.println("长度大于5的水果: "+ filteredFruits);
} private static void filterTest(List<Student> students){
List<Student> filterStudents = students.stream()
.filter(student -> Objects.equals("武汉大学", student.getSchool()))
.collect(Collectors.toList()); filterStudents.forEach(System.out::println);
}

打印结果:

  • 映射操作(map/flatMap)

    映射操作用于对流中的每个元素进行转换。他有map以及flatMap两种操作。map就是基本的映射操作,对每个元素进行提取转换。
// 将实体层映射成学生姓名字符串
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toList()); // 将字符串转大写。
List<String> upperList = Lists.newArrayList("hello", "world", "stream", "api").stream().map(String::toUpperCase).collect(Collectors.toList());

日常开发中map操作我们用的非常多,比如数据库中查询出来的DO实体,我们需要转换为VO返回给前端页面展示,这时候我们可以使用map进行转换操作:

List<StudentDO> studentDOList = studentMapper.listStudents();

List<StudentVO> studentVOList = studentDOList.stream().map(studentDO -> {
StudentVO studentVO = StudentVO.builder().studentNo(studentDO.getId())
.studentName(studentDO.getName()).build();
return studentVO;
}).collect(Collectors.toList());

而flatMap的作用略微特殊,它用于将一个元素映射为一个流,然后将所有流连接成一个流。这在处理嵌套结构或集合中的元素是另一个集合的情况下非常有用。

List<List<String>> nestedWords = Arrays.asList(
Arrays.asList("Java", "Kotlin"),
Arrays.asList("Python", "Ruby"),
Arrays.asList("JavaScript", "TypeScript")
); // 使用 flatMap 将嵌套的 List<String> 转换为一个扁平的 List<String>, 结果将是包含所有单词的扁平流
List<String> wordList = nestedWords.stream()
.flatMap(List::stream).collect(Collectors.toList()); System.out.println(wordList); // 打印结果: [Java, Kotlin, Python, Ruby, JavaScript, TypeScript]

flatMap在使用时,通常会涉及到处理复杂的数据结构,比如处理嵌套的对象集合或者进行数据的扁平化。

@Data
@Builder
class Student {
private String name;
private List<Integer> grades;
} @Data
@Builder
class ClassRoom {
private List<Student> studentList;
} @Data
@Builder
class School {
private List<ClassRoom> classRoomList;
} School school = School.builder()
.classRoomList(Lists.newArrayList(
ClassRoom.builder().studentList(Lists.newArrayList(
Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),
Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()
)).build(),
ClassRoom.builder().studentList(Lists.newArrayList(
Student.builder().name("Charlie").gradeList(Lists.newArrayList(95, 89, 91)).build(),
Student.builder().name("David").gradeList(Lists.newArrayList(82, 87, 79)).build()
)).build()
))
.build(); // 使用 flatMap 扁平化处理获取所有学生的所有课程成绩
List<Integer> allGrades = school.getClassRoomList().stream()
.flatMap(classroom -> classroom.getStudentList().stream())
.flatMap(student -> student.getGradeList().stream())
.collect(Collectors.toList()); System.out.println(allGrades);
// 打印结果:[90, 85, 88, 78, 92, 80, 95, 89, 91, 82, 87, 79]
  • mapToInt操作

    mapToInt 是 Stream API 中的一种映射操作,专门用于将元素映射为 IntStream。通过 mapToInt,你可以将流中的元素映射为 int 类型,从而进行更专门化的操作,例如数值计算。
int totalAge2 = students.stream().mapToInt(Student::getAge).sum();

类似的还有mapToLongmapToDouble 操作,这两个操作类似于 mapToInt,分别用于将流中的元素映射为 LongStreamDoubleStream

  • 排序操作(sorted)

    排序操作用于对流中的元素进行排序。
List<String> cities = Lists.newArrayList("New York", "Tokyo", "London", "Paris");

// 对城市按字母顺序排序
List<String> sortedStream = cities.stream().sorted().collect(Collectors.toList());

对于集合中对象的排序,sorted要求待比较的元素必须实现Comparable接口。

@Data
@Builder
static class Student implements Comparable<Student>{
private String name;
private Integer age; @Override
public int compareTo(Student other) {
return other.getAge()-this.getAge();
}
} List<String> sortedList = students.stream()
.sorted()
.map(Student::getName())
.collect(Collectors.toList());

如果没有实现,就需要将比较器作为参数传递给sorted(Comparator<? super T> comparator)

@Data
@Builder
static class Student {
private String name;
private Integer age;
} List<String> sortedList = students.stream()
.sorted((student1,student2) -> student2.getAge() - student1.getAge())
.map(Student::getName())
.collect(Collectors.toList());
  • 去重操作(distinct)

    去重操作用于去除流中的重复元素。distinct基于Object.equals(Object)实现。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);
// 去除重复的数字
List<Integer> distinctList = numbers.stream().distinct().collect(Collectors.toList()); // 或者去除学生中姓名相同的
List<String> studentNameList = students.stream()
.map(Student::getName())
.distinct()
.collect(Collectors.toList());
  • 截断操作(limit)

    截断操作用于限制流中元素的数量。limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);
// 只取前三个数字
List<Integer> limitedList = numbers.stream().limit(3).collect(Collectors.toList()); // 取土工工程专业的年龄最小的前两名学生
List<Student> limitStu = students.stream()
.filter(student -> Objects.equals("土木工程", student.getMajor()))
.sorted((student1,student2) -> student2.getAge() - student1.getAge())
.limit(2)
.collect(Collectors.toList());
  • 跳过操作(skip)

    跳过操作用于跳过流中的前几个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。作用上跟limit相反。
List<Integer> numbers = Lists.newArrayList(1, 2, 3, 2, 4, 5, 3, 6);
// 跳过前三个数字,返回后面的数字
List<Integer> limitedList = numbers.stream().skip(3).collect(Collectors.toList()); // 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
List<Student> limitStu = students.stream()
.filter(student -> Objects.equals("土木工程", student.getMajor()))
.sorted((student1,student2) -> student2.getAge() - student1.getAge())
.skip(2)
.collect(Collectors.toList());
  • peek操作

    peek 方法对每个元素执行操作并返回一个新的 Stream。peek 的主要目的是用于调试和观察流中的元素,通常用于打印调试信息、记录日志或其他类似的目的,而不会改变流中元素的结构。
List<String> words = Arrays.asList("apple", "banana", "orange", "grape");  

List<String> modifiedWords = words.stream()
.filter(word -> word.length() > 5)
.peek(word -> System.out.println("Filtered Word: " + word))
.map(String::toUpperCase)
.peek(word -> System.out.println("Uppercase Word: " + word))
.collect(Collectors.toList());

Stream的终端操作

终端操作是对流进行最终计算的操作,执行终端操作后,流将被消耗,不能再被使用。

  • 迭代forEach操作

    forEach 迭代操作,用于对流中的每个元素执行指定的操作。
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");

// 使用 forEach 输出每个水果
fruits.stream().forEach(fruit -> System.out.println(fruit));
// 执行forEach时可省略 stream(),即
fruits.forEach(fruit -> System.out.println(fruit));
// 或
fruits.stream().forEach(System.out::println);
  • 收集操作(collect)

    通过collect()方法结合java.util.stream.Collectors工具类将Stream转换为另一种形式,例如列表、集合(toList, toSet, toMap)、映射或归约结果。如上述示例中的:
  1. 收集到List

    使用Collectors.toList()
// 跳过土工工程专业的年龄最小的前两名学生,取后面的学生
List<Student> limitStu = students.stream()
.filter(student -> Objects.equals("土木工程", student.getMajor()))
.sorted((student1,student2) -> student2.getAge() - student1.getAge())
.skip(2)
.collect(Collectors.toList());
  1. 收集到Set

    使用Collectors.toSet()
// 将学生姓名收集到Set
Set<String> studentNameSet = students.stream().map(Student::getName)
.collect(Collectors.toSet());
  1. List转Map

    使用Collectors.toMap。日常开发中使用很多。
// 转换为年龄对应的学生信息
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(
Student::getAge,
Function.identity(),
(e1,e2) -> e1));

这段代码代表,我们使用年龄作为Map的key,对应学生信息作为value。Function.identity():这是一个提取元素自身的映射函数。(e1, e2) -> e1:这是一个合并冲突的操作。如果在流中存在相同的年龄(相同的键),这个函数定义了当出现重复键时应该如何处理。在这里,我们选择保留第一个出现的元素,即保留先出现的 Student 对象。当然我们还可以这样(e1, e2) -> {...}自定义合并冲突策略,例如:

// 转换为年龄对应的学生信息,如果年龄相同,则取名字较长的
Map<Integer, Student> studentMap = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), (e1,e2) -> {
return e1.getName().length() > e2.getName().length() ? e1 : e2;
}));

如果value的值是一些number,我们也可以做一些加减乘除之类的合并。

日常开发中,这个用法很频繁。

  1. 字符串拼接:

    使用Collectors.joining(拼接符)
List<Student> students  = Lists.newArrayList(
Student.builder().name("Alice").gradeList(Lists.newArrayList(90, 85, 88)).build(),
Student.builder().name("Bob").gradeList(Lists.newArrayList(78, 92, 80)).build()
); String studentName = students.stream().map(Student::getName).collect(Collectors.joining(",")); // 打印出来:Alice,Bob
  1. 分组

    即按照集合中的元素的某个属性进行分组,转换为Map<Object, List<Object>>:
List<String> fruits = Arrays.asList("apple", "banana", "orange", "grape");
Map<Integer, List<String>> lengthToNamesMap = fruits.stream()
.collect(Collectors.groupingBy(String::length)); // 按照年龄分组
Map<Integer, List<Student>> studentMap = students.stream().collect(Collectors.groupingBy(Student::getAge)); // 连续进行分组
Map<String,Map<String,List<Student>>> groupsStudent = students.stream()
// 先按照学校分组
.collect(Collectors.groupingBy(Student::getSchool
// 再按照专业分组
,Collectors.groupingBy(Student::getMajor)));
  1. counting()

    counting() 收集器用于计算流中元素的数量。等同于Stream的count()操作。
long studentCount = students.stream().collect(Collectors.counting());
// 效果同等于
long studentCount = students.stream().count();
  1. maxBy()

    maxBy()基于指定的比较器,用于找到流中的最大的元素。等同于Stream的max操作
// 年龄最大的学生
Student olderStudent = students.stream()
.collect(Collectors.maxBy((s1,s2) -> s1.getAge()- s2.getAge())).orElse(null); Student olderStudent2 = students.stream()
.collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null); // 等价于stram的max
Student olderStudent = students.stream()
.max(Comparator.comparing(Student::getAge)).orElse(null);
  1. minBy()

    minBy()基于指定的比较器,用于找到流中的最小的元素。等同于Stream的min操作。
// 年龄最小的学生
Student youngStudent = students.stream()
.collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null); Student youngStudent = students.stream()
.min(Comparator.comparing(Student::getAge)).orElse(null);
  1. averagingInt

    averagingInt() 收集器用于计算流中元素的平均值。
// 求学生平均年龄
double avgAge = students.stream()
.collect(Collectors.averagingInt(Student::getAge));
  1. summarizingInt()

    summarizingInt() 收集器用于计算流中元素的汇总统计信息,包括总数、平均值、最大值和最小值。
// 一次性得到元素个数、总和、均值、最大值、最小值
IntSummaryStatistics summaryStatistics = students.stream().collect(Collectors.summarizingInt(Student::getAge)); System.out.println("总数:" + summaryStatistics.getCount());
System.out.println("平均值:" + summaryStatistics.getAverage());
System.out.println("最大值:" + summaryStatistics.getMax());
System.out.println("最小值:" + summaryStatistics.getMin());
  • partitioningBy()

    将流中的元素按照指定的条件分成两个部分。在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。
// 分为武汉大学学生,非武汉大学学生
Map<Boolean,List<Student>> partStudent = students.stream()
.collect(Collectors.partitioningBy(student -> Objects.equals("武汉大学",student.getSchool())));
  • count操作

    count 用于计算流中的元素个数。效果等同于Collectors.counting()
long studentCount = students.stream().count();
// 效果同等于
long studentCount = students.stream().collect(Collectors.counting());
  • max操作

    基于指定比较器,max用于找到流中最大的元素。效果等同于Collectors.maxBy()
Student olderStudent = students.stream()
.max(Comparator.comparing(Student::getAge)).orElse(null); Student olderStudent2 = students.stream()
.collect(Collectors.maxBy(Comparator.comparing(Student::getAge))).orElse(null);
  • min操作

    基于指定比较器,min用于找到流中最小的元素。效果等同于Collectors.minBy()
Student youngStudent = students.stream()
.min(Comparator.comparing(Student::getAge)).orElse(null); // 年龄最小的学生
Student youngStudent = students.stream()
.collect(Collectors.minBy(Comparator.comparing(Student::getAge))).orElse(null);
  • reduce操作

    reduce 用于对流中的元素进行归约操作,得到一个最终的结果。
// 计算学生的总年龄
int totalAge1 = students.stream()
.map(Student::getAge)
.reduce(0, (a,b) -> a+b); // 也可以使用Integer.sum
int totalAge2 = students.stream()
.map(Student::getAge)
.reduce(0, Integer::sum); // 也可以不设置初始值0,直接Integer.sum,但是返回的是Optional
int totalAge3 = students.stream()
.map(Student::getAge)
.reduce(Integer::sum).orElse(0);
  • findFirst操作

    findFirst 用于查找流中的第一个元素。也即list.get(0)
Student firstStu = students.stream()
.filter(student -> Objects.equals("土木工程", student.getMajor()))
.findFirst().orElse(null);

曾经有个小兄弟问我,他有一段代码类似 Student firstStu = students.get(0)。他们组长让他优化优化,然后就用了这种方式优化的。

  • findAny操作

    findAny 用于查找流中的任意一个元素。在并行流中,findAny 可以更快地获取结果,而在串行流中与 findFirst 的效果基本一致。
Student anyStu = students.stream()
.filter(student ->Objects.equals("土木工程", student.getMajor()))
.findAny().orElse(null);
  • anyMatch操作

    anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true。
boolean hasQh = students.stream()
.anyMatch(student -> Objects.equals("清华大学", student.getSchool()));
  • noneMatch

    noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true.
boolean hasBd = students.stream()
.noneMatch(student -> Objects.equals("北京大学", student.getSchool()));
  • allMatch

    allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true。
boolean isAdult = students.stream()
.allMatch(student -> student.getAge() > 18);

并行流

在Java 8及以上版本,你可以使用并行流(Parallel Stream)来充分利用多核处理器的能力。并行流在处理大量数据时可以提高性能,但并不是在所有情况下都比顺序流更快。当在并行流上进行操作时,需要注意并发问题。确保你的操作是无状态的、无副作用的,或者使用合适的并发工具。一定一定要注意线程安全。并行流本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。

创建并行流,只需要将stream()替换成parallelStream()即可。

List<Student> list = studentMapper.listStudents();
Stream<Student> parallelStream = students.parallelStream();

与顺序流相似,你可以在并行流上执行各种中间和终端操作。

日常中,对于大批量的数据处理转换,我们可以使用并行流去处理。我们可以先把数据切分成100或者其他数值一组的List<List<Student>> 然后使用并行流去处理这些数据。

List<StudentVO> studentVOList = Collections.synchronizedList(Lists.newArrayList());
Lists.partition(students, 100).parallelStream().forEach(pList -> {
// 处理转换数据
List<StudentVO> voList = convertList(pList);
studentVOList.addAll(voList);
});

再比如一些大批量的数据分批次查询,都可以使用并行流去做,但是一定要注意线程安全。

注意事项

使用Stream API可使Java集合处理更简洁、清晰,充分发挥现代、函数式编程的优势。然而,需注意Stream的惰性求值,只在终端操作触发时执行中间操作,确保操作的必要性,避免不必要计算。Stream鼓励无状态、无副作用的操作,避免在中间操作修改共享状态,以确保流的预测性和可维护性。Stream不可重用,一旦被消费,无法再次使用,需谨慎设计流程。并行流虽提高性能,但需谨慎使用,不适用于所有情况,可能导致额外性能开销。

总结

Java 8中引入的Stream API为开发者带来了全新的编程范式。其链式调用和惰性求值的设计理念,使得数据处理变得更为简单和高效。通过深入理解Stream API,我们能够更好地利用这一强大工具,在实际开发中写出更为优雅和易读的代码。

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

提高Java开发生产力,我选Stream API,真香啊的更多相关文章

  1. Atitit 实现java的linq 以及与stream api的比较

    Atitit 实现java的linq 以及与stream api的比较 1.1. Linq 和stream api的关系,以及主要优缺点1 1.2. Linq 与stream api的适用场景1 1. ...

  2. 提高Java开发效率,Idea必装的几款插件

    1.codeMaker 重复代码生成工具 domian和dto两个类基本差不多,这个工具就可以通过一个类快速生成另一个类. 2.gsonFormat json转换成Java类 很多时候根据别人接口给的 ...

  3. 吴裕雄--天生自然 JAVA开发学习:流(Stream)、文件(File)和IO

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //使用 BufferedReader 在控制台读取 ...

  4. java 8 学习三(Stream API)

    集合讲的是数据,流讲的是计算. 流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter. map. reduce. find. match. sort等. 流操作可以顺 ...

  5. 还在用迭代器处理集合吗?试试Stream,真香

    前言 上一篇博客一文带你深入了解 Lambda 表达式和方法引用我给大家介绍了 Java8 函数式特性中的 Lambda,这篇文章我将继续讨论 stream 流的用法 声明:本文首发于博客园,作者:后 ...

  6. Java 8新特性(二):Stream API

    本篇文章继续介绍Java 8的另一个新特性--Stream API.新增的Stream API与InputStream和OutputStream是完全不同的概念,Stream API是对Java中集合 ...

  7. java 8 新特性之Stream的排序/分类

    Stream简介 Stream是Java8提供的一个新的API,它位于java.util.stream包下.Stream API提供了一种新的方式来对Java集合进行操作,这种操作方式极大的提高了Ja ...

  8. Java8中的Stream API

    本篇文章继续介绍Java 8的另一个新特性——Stream API.新增的Stream API与InputStream和OutputStream是完全不同的概念,Stream API是对Java中集合 ...

  9. Java 8中Stream API学习笔记

    1)函数式编程的优势和劣势分别是什么?优势:①不可变性 ②并行操作 ③执行顺序更灵活 ④代码更加简洁纯粹的函数式编程,变量具有不可变性,同一个参数不会在不同场景下得出不同的结果,因此大大增强了系统的稳 ...

  10. java EE技术体系——CLF平台API开发注意事项(4)——API生命周期治理简单说明

    文档说明 截止日期:20170905,作者:何红霞,联系方式:QQ1028335395.邮箱:hehongxia626@163.com 综述 有幸加入到javaEE技术体系的研究与开发,也得益于大家的 ...

随机推荐

  1. ACP 知识点总结

    记录下学习ACP过程不断遇到的且需要记录的知识点: 在阿里云专有网络VPC创建之后,路由器也是随着VPC一起自动创建,所以不需要手动创建,这个时候需要继续创建交换机才能在交换机种创建其他云产品. 7层 ...

  2. 同步FIFO设计

    FIFO有一个读口和一个写口,读写时钟一致是同步FIFO,时钟不一致就是异步FIFO IP设计中通常使用的是同步FIFO 异步FIFO通常使用在跨时钟域设计中 RAM(Random Access Me ...

  3. Go-错误-error

  4. [转帖]Alertmanager 部署配置

    https://www.cnblogs.com/winstom/p/11940570.html 目录 前言 源码安装 配置 启动 配置prometheus监控Alertmanager 修改promet ...

  5. [转帖]查看x86 cpu睿频命令

    查看cpu是否开启睿频,offline掉一些cpu核心后,查看cpu睿频是否升高? turbostat统计X86 处理器的频率.空闲状态.电源状态.温度等状态等 [root@rootbird~]# t ...

  6. [转帖]总结:Servlet

    一.背景 开发了很久的web服务,使用了很多web框架,都忘记web技术最原始的模样了,今天来回顾下. 二.Servlet是什么? Servlet是sun公司提供的一门用于开发动态web资源的技术.我 ...

  7. C#开源免费的开发效率提升利器:DevToys开发人员的瑞士军刀!

    前言 今天分享一款基于C#开源(MIT License开源协议).免费.离线.功能齐全的Windows开发者工具箱,号称开发人员的瑞士军刀,可以帮助开发者完成日常工作开发中常用功能:DevToys. ...

  8. pytest-数据驱动

    今天介绍两种实现数据驱动的方法,json和excel,我们以获取企业微信token接口为例,共 有两个参数corpid&corpsecret 一.json 方法一:@pytest.mark.p ...

  9. vite按需加载element-plus,减少项目体积,你必须学会

    1.在项目中安装 $ npm install element-plus --save $ yarn add element-plus $ pnpm install element-plus 2.安装对 ...

  10. OpenIM集群(非k8s)部署文档

    自行部署etcd/zookeeper/mysql/kafka/mongo/redis集群,可以根据此性能评估服务器需求. 以下是针对一台华为云主机s3的压测数据:8核16G内存,普通磁盘(非SSD)( ...