Java 中文官方教程 2022 版(二十七)(2)

简介: Java 中文官方教程 2022 版(二十七)

Java 中文官方教程 2022 版(二十七)(1)https://developer.aliyun.com/article/1486846

以下示例使用 for-each 循环打印集合roster中包含的所有成员的名称:

for (Person p : roster) {
    System.out.println(p.getName());
}

以下示例使用聚合操作forEach打印出集合roster中包含的所有成员:

roster
    .stream()
    .forEach(e -> System.out.println(e.getName());

尽管在此示例中,使用聚合操作的版本比使用 for-each 循环的版本更长,但您将看到,对于更复杂的任务,使用批量数据操作的版本将更简洁。

下面涵盖了以下主题:

  • 管道和流
  • 聚合操作和迭代器之间的区别

在示例BulkDataOperationsExamples中找到本节描述的代码片段。

管道和流

管道是一系列聚合操作。以下示例使用由聚合操作filterforEach组成的管道打印集合roster中包含的男性成员:

roster
    .stream()
    .filter(e -> e.getGender() == Person.Sex.MALE)
    .forEach(e -> System.out.println(e.getName()));

将此示例与使用 for-each 循环打印集合roster中的男性成员的示例进行比较:

for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}

一个管道包含以下组件:

  • 源:这可以是集合、数组、生成器函数或 I/O 通道。在此示例中,源是集合roster
  • 零个或多个中间操作。中间操作,如filter,会生成一个新流。
    是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源头传递值。此示例通过调用stream方法从集合roster创建流。
    filter操作返回一个包含与其谓词(该操作的参数)匹配的元素的新流。在这个示例中,谓词是 lambda 表达式e -> e.getGender() == Person.Sex.MALE。如果对象egender字段的值为Person.Sex.MALE,则返回布尔值true。因此,在这个示例中,filter操作返回一个包含集合roster中所有男性成员的流。
  • 终端操作。终端操作,比如forEach,产生一个非流结果,比如一个原始值(比如一个双精度值)、一个集合,或者在forEach的情况下,根本没有值。在这个示例中,forEach操作的参数是 lambda 表达式e -> System.out.println(e.getName()),它在对象e上调用getName方法。(Java 运行时和编译器推断出对象e的类型是Person。)

以下示例计算了集合roster中所有男性成员的平均年龄,使用了由filtermapToIntaverage聚合操作组成的流水线:

double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

mapToInt操作返回一个新的IntStream类型的流(这是一个只包含整数值的流)。该操作将其参数中指定的函数应用于特定流中的每个元素。在这个示例中,函数是Person::getAge,这是一个返回成员年龄的方法引用。(或者,你可以使用 lambda 表达式e -> e.getAge()。)因此,在这个示例中,mapToInt操作返回一个包含集合roster中所有男性成员年龄的流。

average操作计算IntStream类型流中包含的元素的平均值。它返回一个OptionalDouble类型的对象。如果流不包含任何元素,则average操作返回一个空的OptionalDouble实例,并调用getAsDouble方法会抛出NoSuchElementException。JDK 包含许多像average这样返回通过组合流内容得到的一个值的终端操作。这些操作被称为归约操作;更多信息请参见归约部分。

聚合操作和迭代器之间的区别

聚合操作,如forEach,看起来像迭代器。然而,它们有几个根本的区别:

  • 它们使用内部迭代:聚合操作不包含像next这样的方法来指示它们处理集合的下一个元素。通过内部委托,您的应用程序确定迭代的集合,但 JDK 确定如何迭代集合。通过外部迭代,您的应用程序确定要迭代的集合以及如何迭代它。然而,外部迭代只能按顺序迭代集合的元素。内部迭代没有这种限制。它可以更容易地利用并行计算,这涉及将问题分解为子问题,同时解决这些问题,然后将子问题的解决方案合并为结果。有关更多信息,请参见并行性部分。
  • 它们从流中处理元素:聚合操作从流中处理元素,而不是直接从集合中处理。因此,它们也被称为流操作
  • 它们支持行为作为参数:您可以为大多数聚合操作指定 lambda 表达式作为参数。这使您可以自定义特定聚合操作的行为。

缩减

原文:docs.oracle.com/javase/tutorial/collections/streams/reduction.html

章节聚合操作描述了以下操作流水线,它计算roster集合中所有男性成员的平均年龄:

double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

JDK 包含许多终端操作(如averagesumminmaxcount),它们通过组合流的内容返回一个值。这些操作称为缩减操作。JDK 还包含返回集合而不是单个值的缩减操作。许多缩减操作执行特定任务,比如找到值的平均值或将元素分组到类别中。然而,JDK 为您提供了通用的缩减操作reducecollect,本节将详细描述这些操作。

本节涵盖以下主题:

  • Stream.reduce 方法
  • Stream.collect 方法

你可以在示例ReductionExamples中找到本节中描述的代码片段。

Stream.reduce 方法

Stream.reduce 方法是一个通用的缩减操作。考虑以下流水线,它计算roster集合中男性成员年龄的总和。它使用Stream.sum 缩减操作:

Integer totalAge = roster
    .stream()
    .mapToInt(Person::getAge)
    .sum();

将此与以下使用Stream.reduce操作计算相同值的流水线进行比较:

Integer totalAgeReduce = roster
   .stream()
   .map(Person::getAge)
   .reduce(
       0,
       (a, b) -> a + b);

在这个例子中,reduce 操作接受两个参数:

  • identity:身份元素既是缩减的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0;这是年龄总和的初始值,也是如果roster集合中不存在成员时的默认值。
  • accumulator: 累加器函数接受两个参数:减少的部分结果(在这个例子中,到目前为止所有处理过的整数的总和)和流的下一个元素(在这个例子中,一个整数)。它返回一个新的部分结果。在这个例子中,累加器函数是一个 lambda 表达式,它将两个Integer值相加并返回一个Integer值:
(a, b) -> a + b

reduce操作总是返回一个新值。然而,累加器函数在处理流的每个元素时也返回一个新值。假设你想将流的元素减少到一个更复杂的对象,比如一个集合。这可能会影响你的应用程序的性能。如果你的reduce操作涉及将元素添加到一个集合中,那么每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。更新现有集合会更有效。你可以使用Stream.collect方法来实现这一点,下一节将介绍。

collect 方法

reduce方法不同,它在处理元素时总是创建一个新值,collect方法修改或改变了现有值。

考虑如何在流中找到值的平均值。你需要两个数据:值的总数和这些值的总和。然而,像reduce方法和所有其他减少方法一样,collect方法只返回一个值。你可以创建一个包含成员变量来跟踪值的总数和这些值总和的新数据类型,比如下面的类Averager

class Averager implements IntConsumer
{
    private int total = 0;
    private int count = 0;
    public double average() {
        return count > 0 ? ((double) total)/count : 0;
    }
    public void accept(int i) { total += i; count++; }
    public void combine(Averager other) {
        total += other.total;
        count += other.count;
    }
}

以下管道使用Averager类和collect方法来计算所有男性成员的平均年龄:

Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);
System.out.println("Average age of male members: " +
    averageCollect.average());

这个例子中的collect操作接受三个参数:

  • supplier: 供应商是一个工厂函数;它构造新实例。对于collect操作,它创建结果容器的实例。在这个例子中,它是Averager类的一个新实例。
  • accumulator: 累加器函数将流元素合并到结果容器中。在这个例子中,它通过将count变量加一并将流元素的值(代表男性成员年龄的整数)加到total成员变量中,修改了Averager结果容器。
  • combiner:合并器函数接受两个结果容器并合并它们的内容。在这个例子中,它通过增加另一个Averager实例的count成员变量的值到Averager结果容器的count变量,并将另一个Averager实例的total成员变量的值加到total成员变量中来修改Averager结果容器。

请注意以下内容:

  • 供应商是一个 lambda 表达式(或方法引用),与reduce操作中的单位元素等值相对。
  • 累加器和合并器函数不返回值。
  • 您可以在并行流中使用collect操作;有关更多信息,请参阅并行性部分。(如果您在并行流中运行collect方法,那么当合并器函数创建一个新对象时,例如在这个例子中创建一个Averager对象时,JDK 会创建一个新线程。因此,您不必担心同步。)

尽管 JDK 为您提供了average操作来计算流中元素的平均值,但如果您需要从流的元素计算多个值,可以使用collect操作和自定义类。

collect操作最适合集合。以下示例使用collect操作将男性成员的姓名放入集合中:

List<String> namesOfMaleMembersCollect = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());

此版本的collect操作接受一个类型为Collector的参数。这个类封装了在collect操作中用作参数的函数,该操作需要三个参数(供应商、累加器和合并器函数)。

Collectors类包含许多有用的归约操作,例如将元素累积到集合中并根据各种标准对元素进行总结。这些归约操作返回Collector类的实例,因此您可以将它们作为collect操作的参数使用。

此示例使用Collectors.toList操作,将流元素累积到一个新的List实例中。与Collectors类中的大多数操作一样,toList操作符返回一个Collector实例,而不是一个集合。

以下示例按性别对集合roster的成员进行分组:

Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));

groupingBy 操作返回一个映射,其键是应用作为其参数指定的 lambda 表达式(称为分类函数)的结果值。在这个例子中,返回的映射包含两个键,Person.Sex.MALEPerson.Sex.FEMALE。键对应的值是包含通过分类函数处理时对应于键值的流元素的 List 实例。例如,对应于键 Person.Sex.MALE 的值是一个包含所有男性成员的 List 实例。

以下示例按性别检索集合 roster 中每个成员的名称并按性别分组:

Map<Person.Sex, List<String>> namesByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.mapping(
                    Person::getName,
                    Collectors.toList())));

在这个例子中,groupingBy 操作需要两个参数,一个分类函数和一个 Collector 实例。Collector 参数称为下游收集器。这是 Java 运行时应用于另一个收集器结果的收集器。因此,这个 groupingBy 操作使您能够对 groupingBy 操作符创建的 List 值应用 collect 方法。这个例子应用了收集器 mapping,它将映射函数 Person::getName 应用于流的每个元素。因此,结果流只包含成员的名称。包含一个或多个下游收集器的管道,像这个例子一样,称为多级减少

以下示例检索每个性别成员的总年龄:

Map<Person.Sex, Integer> totalAgeByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.reducing(
                    0,
                    Person::getAge,
                    Integer::sum)));

reducing 操作需要三个参数:

  • identity:类似于 Stream.reduce 操作,身份元素既是减少的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是 0;这是年龄总和的初始值,如果没有成员存在,则是默认值。
  • mapperreducing 操作将此映射函数应用于所有流元素。在这个例子中,映射器检索每个成员的年龄。
  • operation:操作函数用于减少映射值。在这个例子中,操作函数添加 Integer 值。

以下示例检索每个性别成员的平均年龄:

Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.averagingInt(Person::getAge)));

并行性

原文:docs.oracle.com/javase/tutorial/collections/streams/parallelism.html

并行计算涉及将问题分解为子问题,同时解决这些问题(并行执行,每个子问题在单独的线程中运行),然后将这些子问题的解决方案合并。Java SE 提供了分支/合并框架,它使您能够更轻松地在应用程序中实现并行计算。然而,使用此框架时,您必须指定如何将问题细分(分区)。使用聚合操作,Java 运行时为您执行此分区和解决方案的合并。

在使用集合的应用程序中实现并行性的一个困难在于集合不是线程安全的,这意味着多个线程不能在不引入线程干扰或内存一致性错误的情况下操作集合。集合框架提供了同步包装器,它可以为任意集合添加自动同步,使其线程安全。然而,同步会引入线程争用。你应该避免线程争用,因为它会阻止线程并行运行。聚合操作和并行流使你能够在非线程安全的集合上实现并行性,前提是在操作集合时不修改它。

请注意,并行性并不自动比串行执行操作更快,尽管在有足够数据和处理器核心的情况下可能会更快。虽然聚合操作使您更容易实现并行性,但确定您的应用程序是否适合并行性仍然是您的责任。

本节涵盖以下主题:

  • 并行执行流
  • 并发归约
  • 顺序
  • 副作用
  • 懒惰性
  • 干扰
  • 有状态的 Lambda 表达式

你可以在示例ParallelismExamples中找到本节中描述的代码摘录。

并行执行流

你可以串行或并行执行流。当流并行执行时,Java 运行时将流分成多个子流。聚合操作并行迭代和处理这些子流,然后将结果合并。

当您创建一个流时,除非另有说明,它总是一个串行流。要创建一个并行流,请调用操作Collection.parallelStream。或者,调用操作BaseStream.parallel。例如,以下语句计算所有男性成员的平均年龄:

double average = roster
    .parallelStream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

并发减少

再次考虑以下示例(在减少部分中描述),该示例按性别对成员进行分组。此示例调用collect操作,将集合roster减少为Map

Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));

以下是并行等价的:

ConcurrentMap<Person.Sex, List<Person>> byGender =
    roster
        .parallelStream()
        .collect(
            Collectors.groupingByConcurrent(Person::getGender));

这被称为并发减少。如果包含collect操作的特定管道满足以下所有条件,则 Java 运行时执行并发减少:

注意:此示例返回一个ConcurrentMap的实例,而不是Map,并调用groupingByConcurrent操作,而不是groupingBy。(有关ConcurrentMap的更多信息,请参见并发集合部分。)与操作groupingByConcurrent不同,操作groupingBy在并行流中表现不佳。(这是因为它通过键合并两个映射,这在计算上是昂贵的。)同样,操作Collectors.toConcurrentMap在并行流中的性能优于操作Collectors.toMap

排序

流水线处理流的元素的顺序取决于流是串行执行还是并行执行、流的来源以及中间操作。例如,考虑以下示例,该示例多次使用forEach操作打印ArrayList实例的元素:

Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 };
List<Integer> listOfIntegers =
    new ArrayList<>(Arrays.asList(intArray));
System.out.println("listOfIntegers:");
listOfIntegers
    .stream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("listOfIntegers sorted in reverse order:");
Comparator<Integer> normal = Integer::compare;
Comparator<Integer> reversed = normal.reversed(); 
Collections.sort(listOfIntegers, reversed);  
listOfIntegers
    .stream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Parallel stream");
listOfIntegers
    .parallelStream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("Another parallel stream:");
listOfIntegers
    .parallelStream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");
System.out.println("With forEachOrdered:");
listOfIntegers
    .parallelStream()
    .forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");

该示例包含五个流水线。它打印类似以下的输出:

listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1

该示例执行以下操作:

  • 第一个流水线按照它们添加到列表中的顺序打印listOfIntegers列表的元素。
  • 第二个流水线在使用Collections.sort方法对listOfIntegers进行排序后打印元素。
  • 第三和第四个流水线以一种看似随机的顺序打印列表的元素。请记住,流操作在处理流的元素时使用内部迭代。因此,当您并行执行流时,除非流操作另有规定,否则 Java 编译器和运行时会确定处理流元素的顺序,以最大化并行计算的好处。
  • 第五个流水线使用forEachOrdered方法,该方法按照其来源指定的顺序处理流的元素,无论您是串行还是并行执行流。请注意,如果您在并行流中使用类似forEachOrdered的操作,可能会丧失并行性的好处。

副作用

如果一个方法或表达式除了返回或产生一个值外,还修改了计算机的状态,那么它就具有副作用。例如,可变归约(使用collect操作的操作;有关更多信息,请参见 Reduction 部分)以及调用System.out.println方法进行调试。JDK 在流水线中处理某些副作用很好。特别是,collect方法被设计为以并行安全的方式执行具有副作用的最常见流操作。forEachpeek等操作是为了副作用而设计的;一个返回 void 的 Lambda 表达式,比如调用System.out.println的 Lambda 表达式,除了具有副作用外什么也做不了。即便如此,你应该谨慎使用forEachpeek操作;如果你在并行流中使用其中一个操作,那么 Java 运行时可能会从多个线程同时调用你指定为参数的 Lambda 表达式。此外,永远不要在filtermap等操作中传递具有副作用的 Lambda 表达式作为参数。接下来的章节讨论干扰和有状态的 Lambda 表达式,它们都可能是副作用的来源,并且可能返回不一致或不可预测的结果,特别是在并行流中。然而,首先讨论懒惰的概念,因为它对干扰有直接影响。

懒惰

所有中间操作都是懒惰的。如果一个表达式、方法或算法只有在需要时才会被评估,那么它就是懒惰的(如果算法立即被评估或处理,则是急切的)。中间操作是懒惰的,因为它们直到终端操作开始才开始处理流的内容。懒惰地处理流使得 Java 编译器和运行时能够优化它们处理流的方式。例如,在像filter-mapToInt-average这样的流水线中,average操作可以从mapToInt操作创建的流中获取前几个整数,而这些整数是从filter操作获取的。average操作会重复这个过程,直到它从流中获取了所有需要的元素,然后计算平均值。

Java 中文官方教程 2022 版(二十七)(3)https://developer.aliyun.com/article/1486852


相关文章
|
7月前
|
JavaScript NoSQL Java
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
363 96
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
|
3月前
|
Oracle Java 关系型数据库
java 编程基础入门级超级完整版教程详解
这份文档是针对Java编程入门学习者的超级完整版教程,涵盖了从环境搭建到实际项目应用的全方位内容。首先介绍了Java的基本概念与开发环境配置方法,随后深入讲解了基础语法、控制流程、面向对象编程的核心思想,并配以具体代码示例。接着探讨了常用类库与API的应用,如字符串操作、集合框架及文件处理等。最后通过一个学生成绩管理系统的实例,帮助读者将理论知识应用于实践。此外,还提供了进阶学习建议,引导学员逐步掌握更复杂的Java技术。适合初学者系统性学习Java编程。资源地址:[点击访问](https://pan.quark.cn/s/14fcf913bae6)。
306 2
|
8月前
|
消息中间件 Java 数据库
自研Java框架 Sunrays-Framework使用教程「博客之星」
### Sunrays-Framework:助力高效开发的Java微服务框架 **Sunrays-Framework** 是一款基于 Spring Boot 构建的高效微服务开发框架,深度融合了 Spring Cloud 生态中的核心技术组件。它旨在简化数据访问、缓存管理、消息队列、文件存储等常见开发任务,帮助开发者快速构建高质量的企业级应用。 #### 核心功能 - **MyBatis-Plus**:简化数据访问层开发,提供强大的 CRUD 操作和分页功能。 - **Redis**:实现高性能缓存和分布式锁,提升系统响应速度。 - **RabbitMQ**:可靠的消息队列支持,适用于异步
自研Java框架 Sunrays-Framework使用教程「博客之星」
|
9月前
|
移动开发 前端开发 Java
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
8460 5
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
8月前
|
Java 数据库连接 数据处理
探究Java异常处理【保姆级教程】
Java 异常处理是确保程序稳健运行的关键机制。它通过捕获和处理运行时错误,避免程序崩溃。Java 的异常体系以 `Throwable` 为基础,分为 `Error` 和 `Exception`。前者表示严重错误,后者可细分为受检和非受检异常。常见的异常处理方式包括 `try-catch-finally`、`throws` 和 `throw` 关键字。此外,还可以自定义异常类以满足特定需求。最佳实践包括捕获具体异常、合理使用 `finally` 块和谨慎抛出异常。掌握这些技巧能显著提升程序的健壮性和可靠性。
133 4
|
8月前
|
存储 移动开发 算法
【潜意识Java】Java基础教程:从零开始的学习之旅
本文介绍了 Java 编程语言的基础知识,涵盖从简介、程序结构到面向对象编程的核心概念。首先,Java 是一种高级、跨平台的面向对象语言,支持“一次编写,到处运行”。接着,文章详细讲解了 Java 程序的基本结构,包括包声明、导入语句、类声明和 main 方法。随后,深入探讨了基础语法,如数据类型、变量、控制结构、方法和数组。此外,还介绍了面向对象编程的关键概念,例如类与对象、继承和多态。最后,针对常见的编程错误提供了调试技巧,并总结了学习 Java 的重要性和方法。适合初学者逐步掌握 Java 编程。
145 1
|
9月前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
557 26
|
8月前
|
前端开发 Java 开发工具
Git使用教程-将idea本地Java等文件配置到gitte上【保姆级教程】
本内容详细介绍了使用Git进行版本控制的全过程,涵盖从本地仓库创建到远程仓库配置,以及最终推送代码至远程仓库的步骤。
416 0
|
9月前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
9月前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)

热门文章

最新文章