七、封装(1)
本章概要
- 包的概念
- 代码组织
- 创建独一无二的包名
- 冲突
- 定制工具库
- 使用 import 改变行为
- 使用包的忠告
访问控制(Access control)(或者_隐藏实现(implementation hiding)_)与“最初的实现不恰当”有关。
所有优秀的作者——包括那些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是_重构_(refactoring)的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。
但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,一些用户(客户端程序员(client programmers))希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:“如何区分变动的事物和不变的事物”。
这个问题对于类库(library)而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。
这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。
为了解决这一问题,Java 提供了_访问修饰符_(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:public,protected,包访问权限(package access)(没有关键字)和 private。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 private,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与那些使用其他语言(尤其是 C)编程以及习惯了不受限制地访问任何东西的人们的直觉相违背。
然而,类库组件的概念和对类库组件访问的控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚的类库单元中。Java 中通过 package 关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
包的概念
包内包含一组类,它们被组织在一个单独的_命名空间_(namespace)下。
例如,标准 Java 发布中有一个工具库,它被组织在 java.util 命名空间下。java.util 中含有一个类,叫做 ArrayList。使用 ArrayList 的一种方式是用其全名 java.util.ArrayList。
// hiding/FullQualification.javapublic class FullQualification {public static void main(String[] args) {java.util.ArrayList list = new java.util.ArrayList();}
}
这种方式使得程序冗长乏味,因此你可以换一种方式,使用 import 关键字。如果需要导入某个类,就需要在 import 语句中声明:
// hiding/SingleImport.java
import java.util.ArrayList;public class SingleImport {public static void main(String[] args) {ArrayList list = new ArrayList();}
}
现在你就可以不加限定词,直接使用 ArrayList 了。但是对于 java.util 包下的其他类,你还是不能用。要导入其中所有的类,只需使用 ***** ,就像本书中其他示例那样:
import java.util.*
之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 A 中的方法 f()
不会与类 B 中具有相同签名的方法 f()
冲突。但是如果类名冲突呢?假设你创建了一个 Stack 类,打算安装在一台已经有别人所写的 Stack 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。
到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做“未命名”包或_默认包_(default package)。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。
一个 Java 源代码文件称为一个_编译单元(compilation unit)(有时也称_翻译单元(translation unit))。每个编译单元的文件名后缀必须是 .java。在编译单元中可以有一个 public 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 .java)。每个编译单元中只能有一个 public 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 public 类,此时它们为主 public 类提供“支持”类 。
代码组织
当编译一个 .java 文件时,.java 文件的每个类都会有一个输出文件。每个输出的文件名和 .java 文件中每个类的类名相同,只是后缀名是 .class。因此,在编译少量的 .java 文件后,会得到大量的 .class 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 .class 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 jar 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
类库是一组类文件。每个源文件通常都含有一个 public 类和任意数量的非 public 类,因此每个文件都有一个 public 组件。如果把这些组件集中在一起,就需要使用关键字 package。
如果你使用了 package 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:
package hiding;
意味着这个编译单元是一个名为 hiding 类库的一部分。换句话说,你正在声明的编译单元中的 public 类名称位于名为 hiding 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 import 关键字导入 hiding 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
例如,假设文件名是 MyClass.java ,这意味着文件中只能有一个 public 类,且类名必须是 MyClass(大小写也与文件名相同):
// hiding/mypackage/MyClass.java
package hiding.mypackage;public class MyClass {// ...
}
现在,如果有人想使用 MyClass 或 hiding.mypackage 中的其他 public 类,就必须使用关键字 import 来使 hiding.mypackage 中的名称可用。还有一种选择是使用完整的名称:
// hiding/QualifiedMyClass.javapublic class QualifiedMyClass {public static void main(String[] args) {hiding.mypackage.MyClass m = new hiding.mypackage.MyClass();}
}
关键字 import 使之更简洁:
// hiding/ImportedMyClass.java
import hiding.mypackage.*;public class ImportedMyClass {public static void main(String[] args) {MyClass m = new MyClass();}
}
package 和 import 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。
创建独一无二的包名
你可能注意到,一个包从未真正被打包成单一的文件,它可以由很多 .class 文件构成,因而事情就变得有点复杂了。为了避免这种情况,一种合乎逻辑的做法是将特定包下的所有 .class 文件都放在一个目录下。也就是说,利用操作系统的文件结构的层次性。这是 Java 解决混乱问题的一种方式;稍后你还会在我们介绍 jar 工具时看到另一种方式。
将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 .class 文件所在的路径位置编码成 package 名称来实现的。按照惯例,package 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 package 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。
此技巧的第二部分是把 package 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 .class 文件所在的位置。首先,它找出环境变量 CLASSPATH(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。CLASSPATH 包含一个或多个目录,用作查找 .class 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(取决于你的操作系统,包名 foo.bar.baz 变成 foo\bar\baz 或 foo/bar/baz 或其它)。然后这个路径与 CLASSPATH 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 .class 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。
为了理解这点,比如说域名 MindviewInc.com,将之反转并全部改为小写后就是 com.mindviewinc,这将作为创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 simple 的类库,从而细分名称:
package com.mindviewinc.simple;
这个包名可以用作下面两个文件的命名空间保护伞:
// com/mindviewinc/simple/Vector.java
// Creating a package
package com.mindviewinc.simple;public class Vector {public Vector() {System.out.println("com.mindviewinc.simple.Vector");}
}
如前所述,package 语句必须是文件的第一行非注释代码。第二个文件看上去差不多:
// com/mindviewinc/simple/List.java
// Creating a package
package com.mindviewinc.simple;public class List {System.out.println("com.mindview.simple.List");
}
这两个文件都位于我机器上的子目录中,如下:
C:\DOC\Java\com\mindviewinc\simple
(注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。)
如果你回头看这个路径,会看到包名 com.mindviewinc.simple,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\Java
CLASSPATH 可以包含多个不同的搜索路径。
但是在使用 JAR 文件时,有点不一样。你必须在类路径写清楚 JAR 文件的实际名称,不能仅仅是 JAR 文件所在的目录。因此,对于一个名为 grape.jar 的 JAR 文件,类路径应包括:
CLASSPATH=.;D\JAVA\LIB;C:\flavors\grape.jar
一旦设置好类路径,下面的文件就可以放在任意目录:
// hiding/LibTest.java
// Uses the library
import com.mindviewinc.simple.*;public class LibTest {public static void main(String[] args) {Vector v = new Vector();List l = new List();}
}
输出:
com.mindviewinc.simple.Vector
com.mindviewinc.simple.List
当编译器遇到导入 simple 库的 import 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 com/mindviewinc/simple,然后从已编译的文件中找出名称相符者(对 Vector 而言是 Vector.class,对 List 而言是 List.class)。注意,这两个类和其中要访问的方法都必须是 public 修饰的。
对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时是这么觉得的),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从https://github.com/BruceEckel/OnJava8-examples 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。
冲突
如果通过 * 导入了两个包含相同名字类名的类库,会发生什么?例如,假设程序如下:
import com.mindviewinc.simple.*;
import java.util.*;
因为 java.util.* 也包含了 Vector 类,这就存在潜在的冲突。但是只要你不写导致冲突的代码,就不会有问题——这样很好,否则就得做很多类型检查工作来防止那些根本不会出现的冲突。
现在如果要创建一个 Vector 类,就会出现冲突:
Vector v = new Vector();
这里的 Vector 类指的是谁呢?编译器不知道,读者也不知道。所以编译器报错,强制你明确指明。对于标准的 Java 类 Vector,你可以这么写:
java.util.Vector v = new java.util.Vector();
这种写法完全指明了 Vector 类的位置(配合 CLASSPATH),那么就没有必要写 import java.util.* 语句,除非使用其他来自 java.util 中的类。
或者,可以导入单个类以防冲突——只要不在同一个程序中使用有冲突的名字(若使用了有冲突的名字,必须明确指明全名)。
定制工具库
具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。
一般来说,我会使用反转后的域名来命名要创建的工具包,比如 com.mindviewinc.util ,但为了简化,这里我把工具包命名为 onjava。
比如,下面是“控制流”一章中使用到的 range()
方法,采用了 for-in 语法进行简单的遍历:
// onjava/Range.java
// Array creation methods that can be used without
// qualifiers, using static imports:
package onjava;public class Range {// Produce a sequence [0,n)public static int[] range(int n) {int[] result = new int[n];for (int i = 0; i < n; i++) {result[i] = i;}return result;}// Produce a sequence [start..end)public static int[] range(int start, int end) {int sz = end - start;int[] result = new int[sz];for (int i = 0; i < sz; i++) {result[i] = start + i;}return result;}// Produce sequence [start..end) incrementing by steppublic static int[] range(int start, int end, int step) {int sz = (end - start) / step;int[] result = new int[sz];for (int i = 0; i < sz; i++) {result[i] = start + (i * step);}return result;}
}
这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 onjava 的目录下。编译完之后,就可以在系统的任何地方使用 import onjava 语句来使用这些方法了。
从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 onjava 库。
使用 import 改变行为
Java 没有 C 的_条件编译_(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 package 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。
使用包的忠告
当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 package 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 package 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。
相关文章:
七、封装(1)
本章概要 包的概念 代码组织创建独一无二的包名冲突定制工具库使用 import 改变行为使用包的忠告 访问控制(Access control)(或者_隐藏实现(implementation hiding)_)与“最初的实现不恰当”有关。 所有优…...

问题解决和批判性思维是软件工程的重要核心
软件工程的重心在于问题解决和批判性思维(合理设计和架构降低复杂度),而非仅局限于编程。 许多人误以为软件工程就只是编程,即用编程语言编写指令,让计算机按照这些指令行事。但实际上,软件工程的内涵远超…...

【EI/SCOPUS征稿】2023年通信网络与机器学习国际学术会议(CNML 2023)
2023年通信网络与机器学习国际学术会议(CNML 2023) 2023 International Conference on Communication Networks and Machine Learning 随着数据流量的显著增长,新的通信应用程序不断出现,并产生更多的数据流量,这些数…...
算法-岛屿数量
给你一个由 1(陆地)和 0(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边…...

Crescent QuickPak Crack
Crescent QuickPak Crack Crescent QuickPak是一个32位ActiveX组件的综合集合,用于使用Visual Basic开发应用程序,这将减少开发时间并提高生产力。Crescent QuickPak包含Internet功能,用于打开、读取和解析IIS日志文件,将日志文件…...

六、ESP32数码管显示数字
1. 本节课的成功 2. 数码管 为什么会亮呢? 答:里面就是LED灯...

【Kubernetes】当K8s出现问题时,从哪些方面可以排查
前言 kubernetes,简称K8s,是用8代替名字中间的8个字符“ubernete”而成的缩写。是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kub…...

[ MySQL ] — 库和表的操作
目录 库的操作 创建数据库 语法: 使用: 字符集和校验规则 查看系统默认字符集以及校验规则 查看数据库支持的字符集 查看数据库支持的字符集校验规则 校验规则对数据库的影响 操纵数据库 查看数据库 显示创建语句 修改数据库 删除数据库 备…...
Hive常见面试题
Hive的基本概念 什么是Hive?它的主要作用是什么? Hive是一个基于Hadoop生态系统的数据仓库和数据处理工具。 它提供了类似于SQL的查询语言(HiveQL),使用户能够使用SQL语句来查询和分析 大规模存储在Hadoop集群上的数…...
【单片机】晨启科技,酷黑版,密码锁
密码锁 任务要求: 当输入密码(至少6位密码)时,OLED显示屏显示输入的数字(或者字符),当密码位数输入完毕按下确认键时,对输入的密码与设定的密码进行比较(可使用外设键盘&…...

常见监控网络链路和网络设备的方法
网络监控主要包括网络链路监控和网络设备监控,通常系统运维人员会比较关注。 一、网络链路监控 网络链路监控主要包含三个部分,网络连通性、网络质量、网络流量。 连通性和质量的监控手段非常简单,就是在链路一侧部署探针,去探…...

C#控制台程序+Window增加右键菜单
有时候我们可能会想定制一些自己的右键菜单功能,帮我们减少重复的操作。那么使用控制台程序加自定义右键菜单,就可以很好地满足我们的需求。 1 编写控制台程序 因为我只用到了在文件夹中空白处的右键菜单,所以这里提供了一个对应的模板&…...

【Docker】Docker+Zipkin+Elasticsearch+Kibana部署分布式链路追踪
文章目录 1. 组件介绍2. 服务整合2.1. 前提:安装好Elaticsearch和Kibana2.2. 再整合Zipkin 点击跳转:Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套(质量有保证,内容详情) 本文主要讨论在Ela…...

【小沐学C++】C++ 基于CMake构建工程项目(Windows、Linux)
文章目录 1、简介2、下载cmake3、安装cmake4、测试cmake4.1 单个源文件4.2 同一目录下多个源文件4.3 不同目录下多个源文件4.4 标准组织结构4.5 动态库和静态库的编译4.6 对库进行链接4.7 添加编译选项4.8 添加控制选项 5、构建最小项目5.1 新建代码文件5.2 新建CMakeLists.txt…...

计算机视觉与图形学-神经渲染专题-ConsistentNeRF
摘要 Neural Radiance Fields (NeRF) 已通过密集视图图像展示了卓越的 3D 重建能力。然而,在稀疏视图设置下,其性能显着恶化。我们观察到,在这种情况下,学习不同视图之间像素的 3D 一致性对于提高重建质量至关重要。在本文中&…...
初级算法-其他
文章目录 位1的个数题意:解:代码: 汉明距离题意:解:代码: 颠倒二进制位题意:解:代码: 杨辉三角题意:解:代码: 有效的括号题意…...

Containerd的两种安装方式
1. 轻量级容器管理工具 Containerd 2. Containerd的两种安装方式 3. Containerd容器镜像管理 4. Containerd数据持久化和网络管理 操作系统环境为centos7u6 1. YUM方式安装 1.1 获取YUM源 获取阿里云YUM源 # wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun…...

Android学习之路(1) 文本设置
Android学习之路(1) 文本 一、设置文本内容 设置文本内容的两种方式: 一种是在XML文件中通过属性android:text设置文本代码如下 <TextViewandroid:id"id/tv_hello"android:layout_width"wrap_content"android:layout_height"wrap_c…...

Docker相关命令与入门
1. Docker 命令 # centos 7 systemctl start docker # 启动服务 systemctl stop docker systemctl restart docker # 重启服务 systemctl status docker systemctl enable docker # 开机自启动1.1 镜像相关的命令 # 查看镜像 docker images docker images -q # 查看…...

如何配置一个永久固定的公网TCP地址来SSH远程树莓派?
文章目录 如何配置一个永久固定的公网TCP地址来SSH远程树莓派?前置条件命令行使用举例:修改cpolar配置文件 1. Linux(centos8)安装redis数据库2. 配置redis数据库3. 内网穿透3.1 安装cpolar内网穿透3.2 创建隧道映射本地端口 4. 配置固定TCP端口地址4.1 …...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...

三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...
git: early EOF
macOS报错: Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...
Python网页自动化Selenium中文文档
1. 安装 1.1. 安装 Selenium Python bindings 提供了一个简单的API,让你使用Selenium WebDriver来编写功能/校验测试。 通过Selenium Python的API,你可以非常直观的使用Selenium WebDriver的所有功能。 Selenium Python bindings 使用非常简洁方便的A…...

深入解析光敏传感技术:嵌入式仿真平台如何重塑电子工程教学
一、光敏传感技术的物理本质与系统级实现挑战 光敏电阻作为经典的光电传感器件,其工作原理根植于半导体材料的光电导效应。当入射光子能量超过材料带隙宽度时,价带电子受激发跃迁至导带,形成电子-空穴对,导致材料电导率显著提升。…...
LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考
目录 lua脚本 记录流水 记录流水的作用 流水什么时候删除 我们在做库存扣减的时候,显示基于Lua脚本和Redis实现的预扣减 这样可以在秒杀扣减的时候保证操作的原子性和高效性 lua脚本 // ... 已有代码 ...Overridepublic InventoryResponse decrease(Inventor…...

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手
华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…...

篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...