探索Java中的函数式接口与Streams API的高级用法

引言

在Java中,函数式编程已经不是什么新鲜事物了。从Java 8开始,函数式编程的概念被引入,给我们带来了全新的编程范式。为什么这么多年过去了,咱们还在讨论它?因为,无论是对于老手还是新手程序员来说,掌握函数式接口与Streams API的高级用法,都能在处理复杂数据时,让代码更加简洁、易读,而且效率更高。

在这篇博客里,小黑想跟咱们聊聊,函数式接口和Streams API到底是什么,它们为什么重要,以及它们如何改变了咱们编写Java代码的方式。通过这个讨论,小黑希望能够帮助咱们建立起对函数式编程在Java中应用的全面理解。

函数式接口简介

所谓函数式接口,其实就是只定义一个抽象方法的接口。虽然听起来很简单,但这个定义背后的含义是深远的。Java中的Lambda表达式,就是建立在函数式接口的基础上的。它们使得咱们可以用更简洁的方式来表示方法传递,或者说是行为的传递。这在之前的Java版本中是难以想象的。

来,小黑给咱们举个例子。假设咱们需要一个接口,用于对整数进行某种形式的处理。在Java 8之前,咱们可能会这样写:

interface IntegerProcessor {
    int process(int number);
}

然后,如果咱们想实现一个将数字加倍的处理器,可能会这样做:

class DoubleProcessor implements IntegerProcessor {
    public int process(int number) {
        return number * 2;
    }
}

IntegerProcessor processor = new DoubleProcessor();
System.out.println(processor.process(4)); // 输出 8

这种方式虽然行得通,但是对于这么简单的行为,写一个实现类似乎太繁琐了。有了Lambda表达式后,咱们可以这样做:

IntegerProcessor processor = (int number) -> number * 2;
System.out.println(processor.process(4)); // 同样输出 8

看,是不是简洁多了?Lambda表达式使得咱们可以直接将行为(在这里是将数字加倍)传递给IntegerProcessor接口的实现,而不需要写一个单独的实现类。

这就是函数式接口的魅力所在。它们可以与Lambda表达式搭档,让咱们的代码更加简洁,意图更加明显。接下来的章节中,小黑会继续探讨更多关于函数式接口的高级用法,以及它们是如何与Streams API一起工作,来帮助咱们更加高效地处理数据的。

小黑偷偷告诉你一个买会员便宜的网站: 小黑整的视頻会园优惠站

Lambda表达式与函数式接口

在继续深入之前,咱们先来弄清楚Lambda表达式究竟是什么。简单来说,Lambda表达式是一种匿名函数,它允许咱们以简洁的方式写出实现一个方法的代码。当咱们谈到函数式接口时,Lambda表达式就成了它的完美伴侣,因为一个函数式接口的实例可以通过一个Lambda表达式来创建。

举个例子,如果有一个函数式接口叫作GreetingService,它的作用是打招呼:

@FunctionalInterface
interface GreetingService {
    void sayMessage(String message);
}

使用Lambda表达式,咱们可以轻松地实现这个接口,不需要定义一个实现类:

GreetingService greeting = message -> System.out.println("Hello, " + message);
greeting.sayMessage("小黑"); // 输出:Hello, 小黑

这里,message -> System.out.println("Hello, " + message)就是一个Lambda表达式。它接受一个参数message,然后执行括号里的代码,即打印出招呼信息。

Lambda表达式的语法非常灵活,对于只有一个参数的情况,咱们甚至可以不用写括号。如果表达式体包含多条语句,就需要用大括号{}将这些语句包围起来。这种语法的灵活性,让代码的可读性和简洁性大大提升。

那么,Lambda表达式是怎样与函数式接口配合工作的呢?实际上,每当咱们写一个Lambda表达式时,Java编译器就会将它匹配到一个函数式接口。这意味着Lambda表达式的类型取决于它的上下文环境。在上面的例子中,Lambda表达式被赋值给了GreetingService类型的变量,所以它的类型就是GreetingService

这种机制不仅仅让代码变得更加简洁,而且还增强了代码的表达能力。想象一下,如果咱们有一个方法,需要一个行为作为参数,咱们现在可以直接传入一个Lambda表达式,非常直观和方便:

public void executeGreeting(GreetingService greeting, String message) {
    greeting.sayMessage(message);
}

executeGreeting(message -> System.out.println("Hi, " + message), "小黑"); // 输出:Hi, 小黑

在这里,咱们定义了一个executeGreeting方法,它接受一个GreetingService实例和一个字符串作为参数。调用这个方法时,咱们直接传入了一个Lambda表达式和一个字符串。这种做法让咱们的代码更加灵活,同时也更加易于理解。

通过这个章节,小黑希望咱们能够看到,Lambda表达式不仅仅是一种简洁的语法糖。它们在Java中引入了一个强大的函数式编程能力,让咱们能够以更加声明式的方式来编写代码,这对于处理集合数据、事件监听器等场景特别有用。

常用函数式接口的高级用法

接下来,小黑想带咱们深入探讨几个Java中常用的函数式接口:FunctionPredicateConsumerSupplier。这些接口在日常编程中非常有用,理解它们的高级用法能让咱们的代码更加灵活和强大。

Function 接口

Function<T,R>接口代表接受一个输入参数T,返回一个结果R的函数。这个接口非常适合进行转换操作。比如,小黑想把一个字符串转换成它的长度:

Function<String, Integer> stringLength = (String s) -> s.length();
System.out.println(stringLength.apply("Hello, 小黑")); // 输出 8

更进一步,Function接口有一个compose方法,让咱们可以组合多个函数。比如,先把字符串转换成大写,然后获取其长度:

Function<String, String> toUpperCase = (String s) -> s.toUpperCase();
Function<String, Integer> stringLength = (String s) -> s.length();
Function<String, Integer> upperStringLength = stringLength.compose(toUpperCase);

System.out.println(upperStringLength.apply("hello, 小黑")); // 输出 8
Predicate 接口

Predicate<T>接口表示一个参数的谓词(布尔值)函数。这是用来表示一个测试某条件是否满足的非常好的方式。比如,小黑想测试一个数字是否大于5:

Predicate<Integer> isGreaterThan5 = (Integer number) -> number > 5;
System.out.println(isGreaterThan5.test(9)); // 输出 true

Predicate还有andornegate等默认方法,让咱们可以构建复杂的条件逻辑:

Predicate<Integer> isLessThan10 = (Integer number) -> number < 10;
System.out.println(isGreaterThan5.and(isLessThan10).test(7)); // 输出 true
Consumer 接口

Consumer<T>接口代表接受单个输入参数但不返回结果的操作。这主要用于操作或处理对象。比如,小黑想打印出一个字符串:

Consumer<String> printer = (String s) -> System.out.println(s);
printer.accept("Hello, 小黑"); // 输出 Hello, 小黑
Supplier 接口

最后,Supplier<T>接口代表一个输出。这是当咱们需要提供一个对象实例时,而这个实例是通过无参构造函数创建的,非常有用。比如,小黑想获取一个新的日期对象:

Supplier<LocalDate> dateSupplier = () -> LocalDate.now();
System.out.println(dateSupplier.get()); // 输出当前日期

通过这些例子,小黑希望咱们能看出来,函数式接口在Java中的应用是非常灵活和强大的。它们可以帮助咱们写出更简洁、更易于理解和维护的代码。而且,随着咱们对这些接口的深入了解,咱们会发现,很多编程问题都可以通过这些工具以优雅的方式解决。

Streams API基础

这个API在Java 8中被引入,旨在为集合(如列表、集合)带来一种新的抽象层次,允许以更加声明式的方式处理数据。Streams API通过提供一套丰富的操作和表达式,使得对数据的操作变得更加直观和简洁。

什么是Stream?

首先,Stream和咱们常说的集合(Collections)不一样。集合关注的是数据的存储,而Stream关注的是对数据的计算。Stream就像是一个高级版本的迭代器,除了线性遍历之外,它还允许咱们执行更复杂的操作,比如筛选、转换、汇总等。

创建Stream

创建Stream的方式有很多,最直接的方式是从一个集合的接口开始。比如,从一个列表创建一个Stream:

List<String> strings = Arrays.asList("Hello", "World", "小黑");
Stream<String> stream = strings.stream();

此外,还可以通过Stream.of直接创建:

Stream<String> stream = Stream.of("Hello", "World", "小黑");
常见操作

Stream提供了一系列的操作,这些操作可以分为中间操作和终端操作。中间操作返回的是一个新的Stream,可以链式调用;终端操作则会返回一个结果或者副作用(比如输出到控制台)。

  • 筛选(Filter):对Stream中的元素进行条件筛选。
List<String> filtered = stream.filter(s -> s.contains("小"))
                              .collect(Collectors.toList());
System.out.println(filtered); // 输出包含“小”的字符串
  • 映射(Map):将Stream中的每一个元素映射成另外的形式。
List<Integer> lengths = stream.map(String::length)
                              .collect(Collectors.toList());
System.out.println(lengths); // 输出每个字符串的长度
  • 收集(Collect):是一个终端操作,它可以将Stream转换成其他形式,比如一个List或者一个Set。
List<String> list = stream.collect(Collectors.toList());
使用Stream

使用Stream时,最大的好处是代码的声明性增强了。比如,如果小黑想从一列表中筛选出所有包含"小"的字符串,然后转换成大写,最后收集到一个新的列表中,使用Stream,可以非常直接地表达这个过程:

List<String> result = strings.stream()
                             .filter(s -> s.contains("小"))
                             .map(String::toUpperCase)
                             .collect(Collectors.toList());
System.out.println(result); // 输出处理后的结果

这种方式不仅代码更简洁,而且易于理解。每一步操作都清晰地对应着咱们想要的数据处理流程,这就是Streams API的魅力所在。

通过这个章节,小黑希望咱们能对Streams API有了基本的了解。这个API通过提供一种新的方式来处理集合数据,大大提高了Java编程的表达力和效率。在接下来的章节中,小黑会进一步探讨Streams API的高级特性,帮助咱们充分利用这个强大的工具。

Streams API高级特性

接下来的内容,小黑要带咱们深入了解一下Streams API的一些高级特性。咱们已经看到了如何使用流来执行简单操作,比如筛选、映射和收集。现在,小黑想展示一下,怎样利用Streams API进行更复杂的数据处理,比如利用flatMap进行扁平化处理,以及使用reduce来汇总数据。

扁平化映射(flatMap)

有时候,咱们处理的数据结构可能是多层嵌套的,比如一个列表里面嵌套着其他列表。这时候,如果想要对内层的每个元素进行操作,就需要使用flatMap方法。flatMap可以帮助咱们将一个流中的每个元素转换为另一个流,然后将所有的流连接起来成为一个流。

假设小黑有一个字符串列表的列表,现在想把它们全部转换成大写,然后放到一个列表里:

List<List<String>> listOflists = Arrays.asList(
    Arrays.asList("Hello", "World"),
    Arrays.asList("小黑", "在此")
);
List<String> allUpperCase = listOflists.stream()
    .flatMap(Collection::stream)
    .map(String::toUpperCase)
    .collect(Collectors.toList());
System.out.println(allUpperCase); // 输出所有字符串转换成大写后的结果

通过使用flatMap,咱们可以轻松地将嵌套的流扁平化,然后进行统一处理。

数据汇总(reduce)

reduce操作是一个终端操作,它可以将流中的元素反复结合起来,得到一个值。这对于进行数值汇总或者合并操作非常有用。

比如,如果小黑想计算一个数字列表的总和,可以这样做:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println(sum); // 输出 15

这里,reduce的第一个参数是初始值,第二个参数是一个二元操作,用来定义如何合并两个元素。

并行流

最后,小黑想提一下并行流。Streams API提供了一种简单的方式来利用多核处理器的并行能力,只需调用parallelStream()方法而不是stream()方法。这样,流的操作就可以在多个核心上并行执行,提高处理效率。

但是,并行流并不是万能的,它的使用场景和性能提升需要仔细考量。比如,对于小数据量,或者涉及大量I/O操作的任务,并行化可能不会带来预期的性能提升,甚至可能会因为线程管理的开销而变慢。

int parallelSum = numbers.parallelStream()
    .reduce(0, Integer::sum);
System.out.println(parallelSum); // 输出与上面相同的结果,但是可能通过并行处理更快完成

通过这个章节,小黑希望咱们能对Streams API的一些高级特性有了更深入的了解。正确地利用这些特性,能让咱们处理集合数据时更加得心应手。无论是进行复杂的数据转换、数据汇总,还是充分利用系统资源进行并行计算,Streams API都提供了强大的工具来帮助咱们完成任务。

函数式编程在实际开发中的应用

经过前面的章节,咱们已经看到了函数式接口和Streams API的强大之处。现在,小黑想和咱们聊聊,这些特性在实际开发中是如何应用的,通过具体的案例来看看它们如何解决实际问题,提高开发效率和代码的可读性。

案例一:批量处理数据

想象一下,如果小黑在处理一个电商平台的后台服务,需要从数据库中获取一批商品信息,然后对这些商品的价格进行调整,最后保存回数据库。在函数式编程出现之前,这可能需要写很多循环和临时存储的代码。但是有了Streams API,事情变得简单多了:

List<Product> products = productRepository.findAll(); // 从数据库获取商品列表
products.stream()
    .filter(product -> product.getCategory().equals("Books")) // 只选择书籍类商品
    .map(product -> {
        product.setPrice(product.getPrice() * 0.9); // 对书籍类商品打9折
        return product;
    })
    .forEach(productRepository::save); // 保存修改后的商品信息回数据库

这个例子中,小黑使用了流来筛选、修改和保存商品信息,整个过程没有显式的循环,代码看起来既简洁又易于理解。

案例二:事件处理

在现代的Java应用中,事件驱动模型是很常见的。假设小黑正在开发一个应用,需要在用户注册后发送欢迎邮件。使用函数式接口,咱们可以定义一个事件监听器,当注册事件发生时,自动触发邮件发送:

interface EventListener {
    void handle(Event event);
}

class UserRegistrationService {
    private EventListener listener;

    public void setOnUserRegistered(EventListener listener) {
        this.listener = listener;
    }

    public void registerUser(User user) {
        // 用户注册逻辑...
        if (listener != null) {
            listener.handle(new UserRegisteredEvent(user));
        }
    }
}

UserRegistrationService registrationService = new UserRegistrationService();
registrationService.setOnUserRegistered(event -> emailService.sendWelcomeEmail(event.getUser()));

在这个例子中,setOnUserRegistered方法接受一个EventListener,当用户注册成功时,就会触发这个监听器。利用Lambda表达式,咱们可以非常简单地为这个服务添加一个发送欢迎邮件的功能。

案例三:并行处理任务

考虑到现代服务器通常都是多核的,利用并行流来提高数据处理的速度是一个非常实际的场景。假设小黑需要在后台服务中处理大量的日志文件,分析里面的数据:

List<LogEntry> entries = logRepository.findAll();
Map<String, Long> errorCountByDay = entries.parallelStream()
    .filter(entry -> entry.getType().equals(LogType.ERROR))
    .collect(Collectors.groupingBy(
        entry -> entry.getTimestamp().toLocalDate().toString(),
        Collectors.counting()
    ));

这个例子通过并行流来筛选和统计错误日志,然后按日期分组,最后计算每天的错误数量。使用并行流,可以利用多核处理器并行执行筛选和统计的任务,对于大量数据的处理,这可以显著提高效率。

通过这些案例,小黑希望咱们能看到,函数式编程和Streams API在实际开发中的强大应用。它们不仅能让代码更加简洁易读,而且还能提高代码的执行效率,是现代Java开发中不可或缺的工具。

总结

小黑希望咱们已经对Java中的函数式接口和Streams API有了深入的理解。从基础的概念到高级的应用。它们不仅提高了代码的可读性和简洁性,而且还带来了对多核并行计算的强大支持。

函数式接口与Lambda表达式

通过函数式接口和Lambda表达式,咱们学会了如何用更简洁、更灵活的方式来编写代码。这种方式不仅让代码更易于理解和维护,而且还能帮助咱们更好地利用Java 8引入的新特性,提升开发效率。

Streams API

Streams API的引入,则彻底改变了咱们对集合操作的看法。通过流,咱们可以以声明式的方式来表达复杂的数据处理逻辑,从而避免了繁琐的循环和条件判断。更重要的是,Streams API让并行计算变得触手可及,为处理大量数据提供了强大的工具。

函数式编程在Java中的引入,标志着Java语言的一次重大进化。随着时间的推移,我们可以预见,Java社区会继续探索和扩展函数式编程的边界。未来,可能会有更多的函数式接口和操作加入到标准库中,为Java程序员提供更多的工具和可能性。

同时,随着硬件发展,多核处理器已经变得非常普及。并行流和相关的并行计算技术将会更加重要。咱们可以期待Java平台在未来版本中,会提供更多的特性和优化,以充分利用硬件资源,进一步提升并行计算的性能和效率。

函数式编程和Streams API已经成为现代Java开发中不可或缺的一部分。通过不断学习和实践,咱们可以更好地掌握这些工具,编写出更高效、更优雅的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/432952.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

web前端之uniApp实现选择时间功能

MENU 1、孙子组件1.1、html部分1.2、JavaScript部分1.3、css部分 2、子组件2.1、html部分2.2、JavaScript部分2.3、css部分 3、父组件3.1、html部分3.2、JavaScript部分 4、效果图 1、孙子组件 1.1、html部分 <template><view><checkbox-group change"ch…

如何使用 ArcGIS Pro 统计四川省各市道路长度

在某些时候&#xff0c;我们需要进行分区统计&#xff0c;如果挨个裁剪数据再统计&#xff0c;不仅步骤繁琐、耗时&#xff0c;还会产生一些多余的数据&#xff0c;这里教大家如何在不裁剪数据的情况下统计四川各市的道路长度&#xff0c;希望能对你有所帮助。 数据来源 教程…

【目标检测】1. 目标检测概述

目标检测(Object Detection)实质上上多目标的定位,即在一个图片中定位多个目标物体&#xff0c;包括分类和定位&#xff0c;也就是多个目标分别在哪里?分别属于那个类别? 图像分类常用算法: VGG GoogleNet ResNet 目标检测常用算法&#xff1a; …

It is also possible that a host key has just been changed

问题&#xff1a;ssh失败&#xff0c;提示如上图 分析: ssh的key存在上图里的路径里。 解决&#xff1a;win10删这个文件C:\Users\admin\.ssh\known_hosts , linux删这个文件.ssh\known_hosts ,或者删除这个文件里的制定ip的那一行&#xff0c;例如“106.1.1.22 ecdsa-sha2-…

2.13计算机工作过程

2.三个级别的语言 1)机器语言。又称二进制代码语言&#xff0c;需要编程人员记忆每条指令的二进制编码。机器语言是计算机唯一可以直接识别和执行的语言。 2)汇编语言。汇编语言用英文单词或其缩写代替二进制的指令代码&#xff0c;更容易为人们记忆和理解。使用汇编语言编辑的…

Redis集群(哨兵集群)

一.Sentinel作用和原理: 1.作用 监控:Sentinel会不断监控master和slave是否按预期工作. 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也会以新的master为主。 通知&#xff1a;Sentinel充当redis客户端的服务发现来源,当集群发生故障…

uniapp模仿下拉框实现文字联想功能 - uniapp输入联想(官方样式-附源码)

一、效果 废话不多说&#xff0c;上效果图&#xff1a; 在下方的&#xff1a; 在上方的&#xff1a; 二、源码 一般是个输入框&#xff0c;输入关键词&#xff0c;下拉一个搜索列表。 ElementUI有提供<el-autocomplete>&#xff0c;但uniapp官网没提供这么细&#x…

python基于django的药品进销存管理系统elsb2

本系统是通过面向对象的python语言搭建系统框架&#xff0c;通过关系型数据库MySQL存储数据。使用django框架进行药店药品的信息管理&#xff0c;用户只需要通过浏览器访问系统即可获取药店药品信息&#xff0c;并可以在线管理&#xff0c;实现了信息的科学管理与查询统计。本文…

鸿蒙实战开发:数据交互【RPC连接】

概述 本示例展示了同一设备中前后台的数据交互&#xff0c;用户前台选择相应的商品与数目&#xff0c;后台计算出结果&#xff0c;回传给前台展示。 样例展示 基础信息 RPC连接 介绍 本示例使用[ohos.rpc]相关接口&#xff0c;实现了一个前台选择商品和数目&#xff0c;后台…

推荐一本书籍,澳福读后发现投资真谛

在现在的经济环境下&#xff0c;澳福外汇推荐各位投资读一本书籍就会发现投资者的真谛&#xff0c;那就是经济危机爆发前一年&#xff0c;黎巴嫩裔美国商人纳西姆尼古拉斯塔勒布出版的《黑天鹅:极不可能事件的影响》&#xff0c;在书中一书作者用“黑天鹅事件”这个词来指代影响…

一、项目中Camunda的使用

基本依赖请看另一篇文章 camunda学习使用 介绍 开始事件 结束事件 网关 顺序流 任务 用户任务 活动 上面是项目中使用到的一些图形&#xff0c;简单介绍一下 项目集成 依赖 <spring-boot.version>2.5.6</spring-boot.version> <spring-cloud.version>20…

智能门锁:越便宜,越难卖?

【潮汐商业评论/ 原创】 独居的Gail最近在网上种草了一款带电子猫眼的智能门锁&#xff0c;用她的话来说&#xff1a;“这小东西不仅是个电子锁&#xff0c;还是个智能监控&#xff0c;太适合独居的我了&#xff0c;天知道之前给快递员、外卖员开门都要纠结半天啊。” 但烦恼…

运维知识点-hibernate引擎-HQL

HQL有两个主要含义&#xff0c;分别是&#xff1a; HQL&#xff08;Hibernate Query Language&#xff09;是Hibernate查询语言的缩写&#xff0c;它是一种面向对象的查询语言&#xff0c;类似于SQL&#xff0c;但不是去对表和列进行操作&#xff0c;而是面向对象和它们的属性…

一台云服务器在手,天下我有!2024年3月上云采购季不可错过!

有一台云服务器可以做什么&#xff1f; 搭建微信提醒小助手、搭建个人博客、运行一个365天不休息的程序、存文件、定时发送邮件、数据爬取、加速网络请求、学习使用Linux命令&#xff08;部署&#xff09;、部署自己的小程序的服务端、青龙面包薅羊毛 这些都是常规用法&#xf…

openGauss环境搭建 | 新手指南

一、搭建准备 openGauss开发需要使用linux环境&#xff0c;先下载远程连接工具Xshell/MobaXterm 。 1. 使用工具连接远程linux服务器&#xff0c;使用root账号远程登录&#xff0c;创建个人账号。 useradd -d /home/xxx -m xxx 2. 设置密码。 passwd xxx 3. 切换到个人账…

【归并排序】AcWing. 505 / NOIP2013提高组《火柴排队》(c++)

【题目描述】 涵涵有两盒火柴&#xff0c;每盒装有 n 根火柴&#xff0c;每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列&#xff0c;同一列火柴的高度互不相同&#xff0c;两列火柴之间的距离定义为&#xff1a; 其中 ai 表示第一列火柴中第 i 个火柴的高度&a…

gitlab的安装

1、下载rpm 安装包 (1)直接命令下载 wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-11.6.10-ce.0.el7.x86_64.rpm&#xff08;2&#xff09;直接去服务器上下载包 Index of /gitlab-ce/yum/el7/ | 清华大学开源软件镜像站 | Tsinghua Open Source…

基于springboot+vue的政府管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

Maya笔记 软选择

文章目录 1什么是软选择2注意3如何打开软选择3.1方法一3.2方法二 4调整软选择的范围5衰减模式5.1体积模式5.2表面模式 6衰减曲线 1什么是软选择 也就是渐变选择&#xff0c;从中心点向外影响力度越来越小 软选择针对的是点线面这些模型元素 下图中展示了对被软选择的区域移动…

禅道软件介绍:开源版(免费)和付费版的区别

禅道免费版是有两个版本其中一个是开源的&#xff0c;另一个是云禅道的5人以下免费版。禅道免费版和付费版的区别在于&#xff1a;禅道免费版虽然提供基础项目管理功能&#xff0c;但也只适合有技术能力自行维护和定制的团队。付费版&#xff08;如企业版、旗舰版&#xff09;则…