Stream流常用api及如何执行的lambda表达式

概述

之前说过java8的新特性,lambda表达式

在这里,我们又不得不提,日常开发中的,Stream流

Java8 Stream 使用的是函数式编程模式,被用来对集合进行链状流式的操作

继承关系如下

  • 4种stream接口继承自BaseStream
  • IntStream, LongStream, DoubleStream对应三种基本类型(int, long, double)
  • 为不同数据类型设置不同stream接口作用:1.提高性能,2.增加特定接口函数

特点

  1. 不是数据结构,不会保存数据
  2. 不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。(保留意见:毕竟peek方法可以修改流中元素)
  3. 惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算

使用步骤

使用stream的步骤如下:

  1. 创建stream;
  2. 通过一个或多个中间操作(intermediate operations)将初始stream转换为另一个stream;
  3. 通过 中止/终止/终端操作(terminal operation)获取结果;该操作触发之前的懒操作的执行,中止操作后,该stream关闭,不能再使用了;

Java Stream 的操作类型主要分为两类:中间操作和终止操作

区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作

Stream 流类型

从各种数据源中创建 Stream 流,其中以 Collection 集合最为常见。

  • 如 List 和 Set 均支持 stream() 方法来创建顺序流或者是并行流。

并行流是通过多线程的方式来执行的,它能够充分发挥多核 CPU 的优势来提升性能。

在 Java 8 中, 集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流。
java 复制代码
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

// 我们使用 parallelStream 来输出空字符串的数量:
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();

创建Stream

Stream.of()

从一堆对象中创建 Stream 流

java 复制代码
Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);  // a1

通过上述方法,便不必刻意地创建一个集合,再通过集合来获取 Stream 流

xxxStream

因为Java的范型不支持基本类型,所以我们无法用Stream<int>这样的类型,会发生编译错误。为了保存int,只能使用Stream,但这样会产生频繁的装箱、拆箱操作。

为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream(是int, long, double,不是包装类型)这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:

其中,IntStreams.range()方法还可以被用来取代常规的 for 循环

操作类型(分类)

先了解下述几个概念

  • 无状态:指元素的处理不受之前元素的影响
  • 有状态:指该操作只有拿到所有元素之后才能继续下去。
  • 非短路操作:指必须处理所有元素才能得到最终结果
  • 短路操作:指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果。

分类主要如下

我们又可以按功能区分常用操作:

  • 转换操作:map(),filter(),sorted(),distinct();
  • 合并操作:concat(),flatMap();
  • 并行处理:parallel();
  • 聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();
  • 其他操作:allMatch(), anyMatch(), forEach()。

中间操作(intermediate operations)

中间操作是指返回一个新的 Stream,允许对数据进行进一步的处理

  • 这些操作是惰性求值的,只有在终止操作被调用时才会执行

常见的中间操作包括:

  • filter
  • map
  • sorted
  • distinct
  • limit
  • skip

filter

filter:根据给定的条件过滤元素返回符合条件的元素组成的新 Stream

  • 就是对一个Stream的所有元素一一进行测试,不满足条件的就被"滤掉"了,剩下的满足条件的元素就构成了一个新的Stream。

  • 除了常用于数值外,也可应用于任何Java对象。

  • filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

    java 复制代码
    @FunctionalInterfacepublic interface Predicate<T> {
        // 判断元素t是否符合条件:
        boolean test(T t);
    }

示例

java 复制代码
// 使用 filter 方法过滤出空字符串:
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.stream().filter(string -> string.isEmpty()).count();

map

map:对 Stream 中的每个元素应用一个函数,返回一个包含转换后元素的新 Stream。

  • map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。
  • 利用map(),不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的

示例

java 复制代码
// 使用 map 输出了元素对应的平方数:
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
List<Integer> squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());

List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
    .stream()
    .map(String::trim) // 去空格
    .map(String::toLowerCase) // 变小写
    .forEach(System.out::println); // 打印

flatMap()

相当于把原stream中的所有元素都"摊平"之后组成的Stream,转换前后元素的个数和类型都可能会改变

示例

java 复制代码
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
    .forEach(i -> System.out.println(i));

sorted

sorted:对 Stream 中的元素进行排序,返回一个排序后的新 Stream。

java 复制代码
// 使用 sorted 方法对输出的 10 个随机数进行排序:
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);

// 将输出按照长度升序排序后的字符串
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length()-str2.length())
    .forEach(str -> System.out.println(str));

distinct

distinct:去除 Stream 中的重复元素,返回一个只包含唯一元素的新 Stream。

java 复制代码
Stream<String> stream= Stream.of("I", "love", "you", "too", "too");
stream.distinct()
    .forEach(str -> System.out.println(str));

limit

limit:限制 Stream 中的元素数量,返回一个包含前 N 个元素的新 Stream。

java 复制代码
// 使用 limit 方法打印出 10 条数据
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

skip

skip:跳过 Stream 中的前 N 个元素,返回一个包含剩余元素的新 Stream。

中止/终止/终端操作(terminal operation)

终止操作是指返回一个非 Stream 的结果,执行这些操作会触发中间操作的计算

常见的终止操作包括:

  • collect
  • forEach
  • count
  • reduce
  • anyMatch、allMatch、noneMatch

forEach

forEach:对 Stream 中的每个元素执行指定的操作,通常用于遍历。

java 复制代码
// 使用 forEach 输出了10个随机数:
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

count

count:返回 Stream 中元素的数量。

reduce

reduce:通过指定的二元操作将 Stream 中的元素合并为一个结果。

方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并。

anyMatch、allMatch、noneMatch

anyMatch、allMatch、noneMatch:用于检查 Stream 中是否有元素满足某个条件

java 复制代码
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
 
boolean allMatch = list.stream().allMatch(e -> e > 10); //false
boolean noneMatch = list.stream().noneMatch(e -> e > 10); //true
boolean anyMatch = list.stream().anyMatch(e -> e > 4);  //true

方法引用类别

诸如String::length的语法形式叫做方法引用

高级操作

Collect

collect 是一个非常有用的终端操作,它可以将流中的元素转变成另外一个不同的对象

  • collect:将 Stream 中的元素收集到一个集合中,如将Stream转换成List、Set和Map

    • Collectors 类实现了很多归约操作,Collectors工具类可通过静态方法生成各种常用的Collector,例如将流转换成集合和聚合元素。

    可以查看lambda表达式里面有关于collect的使用

collect()方法定义:

  • 通常情况下我们不需要手动指定collect()的三个参数,调用collect(Collector<? super T,A,R> collector)方法,并且参数中的Collector对象大都是直接通过Collectors工具类获得
  • 想要人为指定容器的实际类型,这个需求可通过`Collectors.toCollection(Supplier collectionFactory)```
java 复制代码
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)`

<R,A> R collect(Collector<? super T,A,R> collector)

生成Collection

java 复制代码
// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)


Stream<String> stream = Stream.of("I", "love", "you", "too");
// 将Stream转换成容器或List
List<String> list = stream.collect(Collectors.toList()); // (1)
// 将Stream转换成容器或Set
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// 将Stream转换成容器或Map
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

生成Map

常在三种情况下collect()的结果会是Map:

  1. 使用Collectors.toMap()生成的收集器,用户需要指定如何生成Map的key和value。
  2. 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操作时用到。
  3. 使用Collectors.groupingBy()生成的收集器,对元素做group操作时用到。
java 复制代码
// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =
     students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key
                                     student -> computeGPA(student)));// 如何生成value

使用partitioningBy()生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分

java 复制代码
// 展示将学生分成成绩及格或不及格的两部分。
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

跟SQL中的group by语句类似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上

  • groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等
  • 这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。
java 复制代码
// 将员工按照部门进行分组
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));
            
// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

Function

  1. Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:
  2. Java 8允许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。
  3. Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

使用collect()做字符串join

java 复制代码
// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

FlatMap

通过map操作, 将流中的对象转换为另一种类型。但是,Map只能将每个对象映射到另一个对象。

flatMap:想要将一个对象转换为多个其他对象或者根本不做转换操作

  • FlatMap 能够将流的每个元素, 转换为其他对象的流。
  • 因此,每个对象可以被转换为零个,一个或多个其他对象,并以流的方式返回。之后,这些流的内容会被放入flatMap返回的流中。
java 复制代码
class Foo {
    String name;
    List<Bar> bars = new ArrayList<>();

    Foo(String name) {
        this.name = name;
    }
}

class Bar {
    String name;

    Bar(String name) {
        this.name = name;
    }
}

List<Foo> foos = new ArrayList<>();

// 创建 foos 集合
IntStream
    .range(1, 4)
    .forEach(i -> foos.add(new Foo("Foo" + i)));

// 创建 bars 集合
foos.forEach(f ->
    IntStream
        .range(1, 4)
        .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name))));
        
        
foos.stream()
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));

// Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3

// 上述代码可简写为
IntStream.range(1, 4)
    .mapToObj(i -> new Foo("Foo" + i))
    .peek(f -> IntStream.range(1, 4)
        .mapToObj(i -> new Bar("Bar" + i + " <- " f.name))
        .forEach(f.bars::add))
    .flatMap(f -> f.bars.stream())
    .forEach(b -> System.out.println(b.name));
flatMap也可用于Java8引入的Optional类。Optional的flatMap操作返回一个Optional或其他类型的对象。所以它可以用于避免繁琐的null检查。
class Outer {
    Nested nested;
}

class Nested {
    Inner inner;
}

class Inner {
    String foo;
}

Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

// 上述代码可以使用下述代码实现
Optional.of(new Outer())
    .flatMap(o -> Optional.ofNullable(o.nested))
    .flatMap(n -> Optional.ofNullable(n.inner))
    .flatMap(i -> Optional.ofNullable(i.foo))
    .ifPresent(System.out::println);

Reduce

规约操作可以将流的所有元素组合成一个结果

  • 实现从一组元素中生成一个值,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数只是因为常用

  • reduce()擅长的是生成一个值
    Java 8 支持三种不同的reduce方法。

  • 第一种将流中的元素规约成流中的一个元素。

    • 来筛选出年龄最大的那个人

      java 复制代码
      persons
          .stream()
          .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
          .ifPresent(System.out::println);    // Pamela
  • 第二种reduce方法接受标识值和BinaryOperator累加器。

  • 第三种reduce方法接受三个参数:标识值,BiFunction累加器和类型的组合器函数BinaryOperator。

  • 注:函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式

    java 复制代码
    Optional<T> reduce(BinaryOperator<T> accumulator)
    T reduce(T identity, BinaryOperator<T> accumulator)
    <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

示例:求出一组单词的长度之和。这是个"求和"操作,操作对象输入类型是String,而结果类型是Integer

  • 两步操作,使用reduce()函数将这两步合二为一
java 复制代码
// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

容器执行Lambda表达式的方式(Stream Pipelines)

理解Stream我们更关心的是另外两个问题:流水线和自动并行

举例如下:

  • 求出以字母A开头的字符串的最大长度,一种直白的方式是为每一次函数调用都执一次迭代,这样做能够实现功能,但效率上肯定是无法接受的
  • 类库的实现者使用流水线(Pipeline)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中尽可能多的执行用户指定的操作
  • 就是调用filter()方法后立即执行,选出所有以A开头的字符串并放到一个列表list1中,之后让list1传递给mapToInt()方法并立即执行,生成的结果放到list2中,最后遍历list2找出最大的数字作为最终结果。
java 复制代码
int longestStringLengthStartingWithA
        = strings.stream()
              .filter(s -> s.startsWith("A"))
              .mapToInt(String::length)
              .max();

Stream流水线解决方案

应该采用某种方式记录用户每一步的操作,当用户调用结束操作时将之前记录的操作叠加到一起在一次迭代中全部执行掉。那么。便分为如下几种操作

  1. 用户的操作如何记录?
  2. 操作如何叠加?
  3. 叠加之后的操作如何执行?
  4. 执行后的结果(如果有)在哪里?

操作如何记录

很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据来源,操作,回调函数>构成的三元组

  • Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线
  • 相关类和接口的继承关系如下:
    • IntPipeline, LongPipeline, DoublePipeline没在图中画出
    • 图中Head用于表示第一个Stage,即调用调用诸如Collection.stream()方法产生的Stage,Stage里不包含任何操作
    • StatelessOp和StatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作 无状态:指元素的处理不受之前元素的影响;、

      有状态:指该操作只有拿到所有元素之后才能继续下去

Stream流水线组织结构示意图如下

  • Collection.stream()方法得到Head也就是stage0
  • 这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作

操作如何叠加

前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系

这种协议由Sink接口完成,Sink接口包含的方法如下表所示:

每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道其内部是如何处理的

  • 对于有状态的操作,Sink的begin()和end()方法也是必须实现的 比如Stream.sorted()是一个有状态的中间操作,
    • 其对应的Sink.begin()方法可能创建一个盛放结果的容器,
    • 而accept()方法负责将元素添加到该容器,
    • 最后end()负责对容器进行排序
  • 对于短路操作,Sink.cancellationRequested()也是必须实现的 比如Stream.findFirst()是短路操作,只要找到一个元素
    • cancellationRequested()就应该返回true,以便调用者尽快结束查找。

实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法

有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以

  • 可能的Sink.accept()方法流程如下:

    java 复制代码
    void accept(U u){
        1. 使用当前Sink包装的回调函数处理u
        2. 将处理结果传递给流水线下游的Sink
    }
  • Sink接口的其他几个方法也是按照这种[处理->转发]的模型实现。

如何将自身的操作包装成Sink

Stream.sorted()方法中的Sink的四个接口方法是协同工作的:

  1. 首先beging()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小
  2. 之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素
  3. 最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink;
  4. 如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理

叠加之后如何操作

Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。

  • 启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
  • 结束操作不会创建新的流水线阶段(Stage),就是流水线的链表不会再往后延伸了
  • 结束操作会创建一个包装了自己操作的Sink,这也是流水线中最后一个Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)
  • 对于Sink的[处理->转发]模型,结束操作的Sink就是调用链的出口
上游的Sink是如何找到下游Sink的

设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法来得到Sink

  • 作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream的Sink对象

  • 为什么要产生一个新对象而不是返回一个Sink字段?这是因为使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。

    只要从流水线的最后一个Stage开始,不断调用上一个Stage的opWrapSink()方法直到最开始(不包括stage0,因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink

  • 这时候,流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下:

    • 首先调用wrappedSink.begin()方法告诉Sink数据即将到来
    • 然后调用spliterator.forEachRemaining()方法对数据进行迭代(Spliterator是容器的一种迭代器,将lambda里面的章节),
    • 最后调用wrappedSink.end()方法通知Sink数据处理结束。逻辑如此清晰。
java 复制代码
// AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
    ...
    if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
        wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
        spliterator.forEachRemaining(wrappedSink);// 迭代
        wrappedSink.end();// 通知遍历结束
    }
    ...
}

执行后的结果在哪里

要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作。

返回类型

相关推荐
数据小爬虫@3 分钟前
利用Python爬虫获取淘宝店铺详情
开发语言·爬虫·python
高 朗14 分钟前
【GO基础学习】基础语法(2)切片slice
开发语言·学习·golang·slice
寒笙LED29 分钟前
C++详细笔记(六)string库
开发语言·c++·笔记
IT书架36 分钟前
golang面试题
开发语言·后端·golang
初遇你时动了情1 小时前
uniapp 城市选择插件
开发语言·javascript·uni-app
编程修仙1 小时前
Collections工具类
linux·windows·python
芝麻团坚果1 小时前
对subprocess启动的子进程使用VSCode python debugger
linux·ide·python·subprocess·vscode debugger
EterNity_TiMe_2 小时前
【论文复现】神经网络的公式推导与代码实现
人工智能·python·深度学习·神经网络·数据分析·特征分析
Stara05112 小时前
Git推送+拉去+uwsgi+Nginx服务器部署项目
git·python·mysql·nginx·gitee·github·uwsgi
zongzi_4942 小时前
二次封装的天气时间日历选择组件
开发语言·javascript·ecmascript