-
起因在java项目中,我在maven的pom.xml中引用了io.github.officiallysingh:spring-boot-starter-spark:1.3包,然后这个包里又有org.apache.spark:spark-core_2.13:3.5.5包的引用,而在spark-core_2.13包中又引用了org.apache.avro:avro-mapred:1.11.4包,这个包的版本0.10.0修改为0.9.0,我们如何实现呢?推荐方法通过在dependencyManagement中声明三方包的版本,来在自己项目中,将所有指定包的版本进行统一,并且包版本不同产生的冲突在当前项目的pom.xml中添加代码<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-mapred</artifactId> <version>1.11.3</version> </dependency> </dependencyManagement>刷新依赖之后,可以看到三方包里的依赖包avro-mapred版本已经改变了其它方法下面这个表格总结了你可以在项目中使用的三种主要策略。方法操作方式适用场景与说明💡 直接声明依赖在 <dependencies> 中直接声明你想要的 jersey-client 版本。最简洁直接,适用于单模块项目,快速覆盖传递依赖的版本。📦 依赖管理在 <dependencyManagement> 中统一管理 jersey-client 的版本。推荐用于多模块项目,可以保证所有模块使用的版本一致,避免冲突。🗑️ 排除+引入先通过 <exclusions> 排除旧版本,再显式引入新版本。最严格的控制,确保构建时不会引入冲突的旧版本,但配置稍显繁琐。 转载自https://www.cnblogs.com/lori/p/19142696
-
【问题来源】【必填】【可选问题来源:某地12345AICC项目】【问题简要】:由于CTI时间和北京时间差了8分钟(此时坐席登录都是正常的)后将cti更改为北京时间,此时坐席立马就登录失败,报100-003错误,【message:message:"General Error."retcode:"100-003"】 反复更改后发现差了5分钟也是可以登录的 差了4分钟就又登录失败 不知道这个时间为何会影响坐席登录【问题类别】【可选问题分类:坐席签入问题】【AICC解决方案版本】【必填】【AICC可选择版本:AICC 8.15.0】【UAP可选择版本:UAP9600 V100R005C00SPC026】【CTI可选择版本:ICD V300R008C23】【期望解决时间】越快越好测试工号999 测试时间24号00:30左右
-
前言欢迎来到 Redis Set 的终极指南。如果您曾需要管理一组独一无二的元素集合——无论是用户 ID、文章标签还是邮件地址——并希望以闪电般的速度对其执行强大的集合运算,那么您来对地方了。Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 redis-plus-plus 来演示所有示例,并逐行剖析代码。无论您是 C++ 开发者、后端工程师,还是仅仅对 Redis 感到好奇,读完本文,您都将深刻理解是什么让 Set 成为 Redis 中功能最丰富的工具之一。Redis Set 究竟是什么?在我们深入代码之前,先来建立一个清晰的思维模型。想象你有一个魔力袋,你可以往里面扔东西,但这个袋子有两条非常特殊的规则:强制保持唯一:这个袋子会自动拒绝重复的物品。如果你想把一个标有“A”的弹珠放进一个已经有“A”弹珠的袋子里,它会阻止你,确保袋子里每样东西都只有一个。顺序毫不在意:当你从袋子里往外取东西时,它们的顺序是完全随机的。袋子不记得到底是按什么顺序把东西放进去的。这个“魔力袋”正是 Redis Set 的精准比喻:一个无序的、元素唯一的字符串集合。这个简单的定义是其强大功能的基石,使其能够以惊人的速度进行成员资格检查、数量统计以及诸如交集、并集等复杂的服务器端运算。第一章:基础入门 - 创建和查看你的第一个 Set让我们从最基本的操作开始:如何向一个 Set 添加元素,以及如何查看它的全部内容。为此,我们将使用 SADD 和 SMEMBERS 这两个命令。SADD:向集合中添加成员SADD 是您向 Set 中添加一个或多个元素的主要工具。如果某个元素已经存在,Redis 会优雅地忽略它。该命令的返回值是新成功添加的元素的数量。SMEMBERS:获取所有成员SMEMBERS 的功能正如其名:返回指定 Set 中的所有成员。这对于获取整个集合非常有用,但请注意:在拥有数百万元素的超大 Set 上使用此命令可能会暂时阻塞您的 Redis 服务器,因为它需要时间来准备所有数据。我们将在后续章节中讨论更安全的替代方案 SSCAN。C++ 实战:sadd 与 smembers现在,让我们来分析一段代码,它演示了这些基础操作。 // 引入必要的头文件...#include <iostream>#include <set>#include <string>#include <vector>#include <iterator>#include <sw/redis++/redis.h>// 一个辅助函数,用于打印容器内容template<typename T>void PrintContainer(const T& container) { for (const auto& elem : container) { std::cout << elem << " "; } std::cout << std::endl;}void test1(sw::redis::Redis& redis){ std::cout << "sadd 和 smembers" << std::endl; // 清空数据库,确保一个干净的测试环境 redis.flushall(); // 1. 一次添加一个元素 redis.sadd("key", "111"); // 2. 使用初始化列表,一次添加多个元素 redis.sadd("key", {"222", "333", "444"}); // 3. 使用迭代器,从另一个容器中添加多个元素 std::set<std::string> elems = {"555", "666", "777"}; // 返回值是成功插入了多少个元素 redis.sadd("key", elems.begin(), elems.end()); // --- 现在,让我们获取所有元素 --- std::set<std::string> result; // 为我们的 C++ set 构建一个插入迭代器 auto it = std::inserter(result, result.end()); // 从 Redis set 中获取所有成员,并插入到我们的 C++ set 中 redis.smembers("key", it); PrintContainer(result);}一键获取完整项目代码cpp代码剖析:redis.flushall():我们首先清空整个 Redis 数据库,以确保测试环境的纯净。单个元素 sadd:redis.sadd("key", "111"); 将字符串 “111” 添加到名为 key 的 Set 中。由于 Set 原本是空的,此命令返回 1。初始化列表 sadd:redis.sadd("key", {"222", "333", "444"}); 展示了 redis-plus-plus 库的一个便捷特性,允许您一次性添加多个元素。这比发送三个独立的命令效率更高。此调用将返回 3。基于迭代器的 sadd:在这里,我们先填充了一个 C++ 的 std::set,然后使用它的迭代器(elems.begin(), elems.end())将其所有元素添加到 Redis 的 Set 中。这对于将现有 C++ 容器中的数据同步到 Redis 非常有用。使用 smembers 获取数据:我们创建了一个 std::set<string> result; 来存放从 Redis 返回的数据。在客户端使用 std::set 是一个绝佳选择,因为它不仅 mirroring(镜像)了 Redis Set 的唯一性,还能自动对元素进行排序,便于我们进行可预测的展示。auto it = std::inserter(result, result.end()); 是至关重要的一行。我们需要一种方式告诉 redis-plus-plus 应该把接收到的元素放在哪里。inserter 是一种特殊的迭代器,当你给它赋值时,它会调用其关联容器的 insert() 方法。redis.smembers("key", it); 执行命令。redis-plus-plus 获取 key 中的所有成员,并使用我们的迭代器 it 将它们逐一插入到 result 集合中。C++ 关键概念:inserter vs back_inserter在原始笔记中,有一个关键的区别被强调了出来:std::back_inserter 创建一个调用 push_back() 的迭代器。它适用于 std::vector, std::list, std::deque 等容器。std::set 没有 push_back() 方法,因为它需要维护内部的排序。因此,对于 std::set,我们必须使用 std::inserter,它会调用 insert() 方法。预测输出:PrintContainer 函数将打印 result 集合的内容。由于 std::set 会对其元素进行排序,输出将是按字母/数字顺序排列的。sadd 和 smembers111 222 333 444 555 666 777一键获取完整项目代码第二章:深入探索 - 检查与修改你的 Set既然我们知道了如何构建一个 Set,接下来让我们学习如何查询它的属性并执行基本的修改。这些命令是 Set 日常操作的核心,并且它们都快得令人难以置信。SISMEMBER:这个元素存在吗? (时间复杂度 O(1))这是 Set 命令库中最强大的命令之一。SISMEMBER 检查一个特定元素是否是 Set 的成员。如果存在,返回 1 (true);如果不存在,返回 0 (false)。它的性能是 O(1),这意味着其速度是恒定的,不依赖于 Set 的大小。无论是在一个有10个元素的 Set 还是在一个有1000万个元素的 Set 中检查成员资格,花费的时间都是相同的。C++ 实战:sismembervoid test2(sw::redis::Redis& redis){ std::cout << "sismember" << std::endl; redis.flushall(); redis.sadd("key", {"111", "222", "333", "444"}); // 检查 "111" 是否是集合的成员 bool result = redis.sismember("key", "111"); std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们创建一个 Set,然后使用 sismember 检查 “111” 是否存在。redis-plus-plus 库非常方便地将 Redis 返回的 1 或 0 直接映射为了 C++ 的 bool 类型。因为 “111” 确实在 Set 中,result 将为 true。应用场景:标签系统:检查一篇博客文章是否已经被标记为 “DevOps”。权限控制:检查一个 userID 是否在 admin_users 这个 Set 中。唯一性事件:检查用户是否已经执行了某个一次性操作(例如,“voted_on_poll_123”)。预测输出:当 bool true 被输出到 cout 时,通常会显示为 1。sismemberresult:1一键获取完整项目代码SCARD:集合里有多少元素? (时间复杂度 O(1))SCARD 代表 “Set Cardinality”(集合基数),它简单地返回一个 Set 中元素的数量。与 SISMEMBER 一样,这也是一个 O(1) 操作。Redis 内部维护了一个计数器,所以它不需要遍历所有元素就能告诉你总数。C++ 实战:scardvoid test3(sw::redis::Redis& redis){ std::cout << "scard" << std::endl; redis.flushall(); // 向集合中添加4个唯一元素 redis.sadd("key", {"111", "222", "333", "444"}); // 获取集合中的元素个数 long long result = redis.scard("key"); // 返回 4 std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们添加了四个元素,然后调用 scard。命令返回了计数 4。应用场景:在线用户:跟踪已登录的独立用户数量。点赞计数:快速显示一张照片获得的独立点赞数。数据分析:统计今天访问网站的独立 IP 地址数量。预测输出:scardresult:4一键获取完整项目代码SPOP:随机移除并返回一个元素SPOP 是一个既有趣又实用的命令。它会从 Set 中随机选择一个元素,将其移除,然后返回给你。这是一种“破坏性读取”,因为元素在被读取后就从集合中消失了。C++ 实战:spopvoid test4(sw::redis::Redis& redis){ std::cout << "spop" << std::endl; redis.flushall(); redis.sadd("key", {"111", "222", "333", "444"}); // 随机弹出一个元素,spop 的返回值是 Optional<string> auto result = redis.spop("key"); if (result) { // 因为返回值是 Optional,我们通过 .value() 来获取原始的 string 内容 std::cout << "result:" << result.value() << std::endl; } else { std::cout << "result is empty" << std::endl; }}一键获取完整项目代码cpp剖析:auto result = redis.spop("key"); 执行命令。redis-plus-plus 将返回值包装在 sw::redis::Optional<std::string> 中。这是因为如果你对一个空 Set 执行 spop,Redis 会返回 nil(空)。Optional 类型可以优雅地处理这种情况,避免空指针等问题。if (result) 检查 Optional 对象是否真的包含一个值。在我们的例子中,由于 Set 非空,它肯定会弹出一个元素,所以条件为真。result.value() 从 Optional 中提取出实际的 std::string 值。核心特性:随机性:SPOP 最大的特点就是随机。这意味着每次运行这段代码,得到的结果都可能不同。它非常适合需要随机处理任务的场景。应用场景:抽奖系统:从参与用户 Set 中随机抽取一名中奖者。任务队列:从待处理任务池中随机分配一个任务给工作进程。在线匹配:从等待匹配的玩家池中随机抽取一个进行游戏。预测输出:输出是不确定的,可能是以下四种情况之一:// 可能的输出 1spopresult:111// 可能的输出 2spopresult:333一键获取完整项目代码第三章:集合的威力 - 集合运算这才是 Redis Set 真正大放异彩的地方。Redis 能够在服务器端以极高的效率执行集合的交集 (intersection)、并集 (union) 和差集 (difference) 运算,避免了将大量数据传输到客户端再进行计算的开销。交集运算:SINTER & SINTERSTORE交集运算会找出所有给定的 Set 中共同存在的元素。SINTER: 计算交集并直接返回给客户端。SINTERSTORE: 计算交集,但不返回,而是将结果存储在一个新的目标 Set 中。C++ 实战:sinter (求交集并返回)void test5(sw::redis::Redis& redis){ // 这里的 cout 应该是 "sinter",一个小笔误 std::cout << "sinter" << std::endl; redis.flushall(); redis.sadd("key1", {"111", "222", "333", "444"}); redis.sadd("key2", {"111", "222", "444"}); std::set<std::string> result; auto it = std::inserter(result, result.end()); // 求交集涉及多个 key,我们使用初始化列表来描述 // 将 "key1" 和 "key2" 的交集插入到 result 中 redis.sinter({"key1", "key2"}, it); PrintContainer(result);}一键获取完整项目代码cpp剖析:key1 包含 {"111", "222", "333", "444"}。key2 包含 {"111", "222", "444"}。redis.sinter({"key1", "key2"}, it); 命令计算出两个集合的共同成员是 {"111", "222", "444"},并通过迭代器将它们存入 C++ 的 result 集合中。应用场景:共同好友:计算用户A的好友列表和用户B的好友列表的交集。内容推荐:找出同时对 “科幻” 和 “悬疑” 标签感兴趣的用户。预测输出:sinter111 222 444一键获取完整项目代码12C++ 实战:sinterstore (求交集并存储)void test6(sw::redis::Redis& redis){ std::cout << "sinterstore" << std::endl; redis.flushall(); redis.sadd("key1", {"111", "222", "333"}); redis.sadd("key2", {"111", "222", "444"}); // 指定一个 destination ("key3"),将交集结果存储到其中 long long len = redis.sinterstore("key3", {"key1", "key2"}); std::cout << "len:" << len << std::endl; // 检查 "key3" 中的元素以验证结果 std::set<std::string> result; auto it = std::inserter(result, result.end()); redis.smembers("key3", it); PrintContainer(result);}一键获取完整项目代码cpp剖析:redis.sinterstore("key3", {"key1", "key2"}); 计算出交集 {"111", "222"},然后将这个结果存入一个全新的 Set key3 中。如果 key3 已存在,它将被覆盖。该命令返回新生成的 key3 集合的元素数量,即 2。所以 len 的值为 2。后续的 smembers 验证了 key3 的内容确实是正确的交集结果。应用场景:当你需要缓存或复用交集计算结果时,SINTERSTORE 非常有用。例如,为一组用户预先计算出他们共同喜欢的商品列表。预测输出:sinterstorelen:2111 222一键获取完整项目代码第四章:超越基础 - 更多强大的 Set 命令我们已经覆盖了所提供代码中的所有命令,但 Redis Set 的能力远不止于此。为了成为真正的 Set 大师,让我们来了解一下其他一些极其有用的命令。并集运算:SUNION & SUNIONSTORE并集运算返回所有给定集合的全部不重复的元素。命令:SUNION key [key ...] 和 SUNIONSTORE destination key [key ...]应用场景:好友圈:获取用户A的好友、用户B的好友和用户C的好友的完整、不重复的列表。权限合并:一个用户属于 “editor” 角色组和 “publisher” 角色组,通过并集可以得到该用户拥有的所有权限的集合。差集运算:SDIFF & SDIFFSTORE差集运算返回那些只存在于第一个集合中,但不在任何后续集合中的元素。命令:SDIFF key [key ...] 和 SDIFFSTORE destination key [key ...]应用场景:好友推荐:找出我的好友中,有哪些还不是我朋友A的好友,从而可以向我推荐。内容去重:向用户展示新闻时,从“今日热点”中排除掉他“已读新闻”Set 中的内容。安全迭代:SSCAN正如前文提到的,SMEMBERS 对于大集合是危险的。SSCAN 提供了安全的替代方案。它使用一个游标 (cursor) 来分批次地返回集合中的元素,每次只返回一小部分,绝不会阻塞服务器。命令:SSCAN key cursor [MATCH pattern] [COUNT count]工作方式:你用一个初始为 0 的游标开始第一次调用。Redis 返回下一批元素和一个新的游标。你用这个新的游标进行下一次调用,如此往复,直到返回的游标为 0,表示迭代完成。适用场景:任何需要遍历生产环境中大集合的操作,例如数据迁移、离线分析等。总结Redis Set 是一种看似简单却异常强大的数据结构。让我们回顾一下它的核心优势:唯一性:自动处理数据去重,简化了应用逻辑。极速性能:绝大多数核心操作(增、删、查、计数)的时间复杂度都是 O(1),性能与集合大小无关。强大的集合运算:能够在服务器端原子性地、高效地执行交、并、差集运算,极大地减少了网络开销和客户端的计算压力。从简单的在线用户统计,到复杂的社交网络好友关系分析,再到智能推荐系统,Redis Set 都能以其优雅和高效提供坚实的解决方案。希望通过本文的深度解析和 C++ 代码示例,您已经准备好在自己的项目中发挥 Redis Set 的真正威力了。————————————————原文链接:https://blog.csdn.net/2301_80863610/article/details/152178781
-
Java 中的并发(Concurrency) 指多个任务在同一时间段内交替执行(宏观上同时进行,微观上可能是 CPU 快速切换调度),目的是提高程序效率,充分利用系统资源(如 CPU、内存、I/O 等)。一、为什么需要并发?资源利用率最大化当程序执行 I/O 操作(如读写文件、网络请求)时,CPU 通常处于空闲状态。通过并发,可在等待 I/O 时让 CPU 处理其他任务,避免资源浪费。例如:一个下载文件的程序,在等待网络数据时,可同时解析已下载的部分数据。响应速度提升对于交互式程序(如 GUI 应用、服务器),并发能避免单任务阻塞导致的界面卡顿或请求超时。例如:Web 服务器同时处理多个用户的请求,而非逐个排队处理。二、并发的核心概念1. 线程(Thread)与进程(Process)进程:程序的一次执行过程,是系统资源分配的基本单位(有独立的内存空间)。线程:进程内的执行单元,是 CPU 调度的基本单位(共享进程的内存空间)。关系:一个进程可包含多个线程(多线程),线程间切换成本远低于进程切换。2. 并行(Parallelism)与并发(Concurrency)的区别并发:多个任务“交替执行”(CPU 切换速度快,看起来同时进行),适用于单 CPU 或多 CPU。并行:多个任务“同时执行”(需多 CPU 核心,每个核心处理一个任务)。例如:4 核 CPU 同时运行 4 个线程是并行,1 核 CPU 快速切换 4 个线程是并发。三、Java 实现并发的方式Java 提供了多种并发编程工具,核心是通过线程实现:1. 基础方式继承 Thread 类:重写 run() 方法定义任务,调用 start() 启动线程。实现 Runnable 接口:定义任务逻辑,通过 Thread 类包装并启动(推荐,避免单继承限制)。实现 Callable 接口:与 Runnable 类似,但可返回结果并抛出异常,配合 Future 获取结果。// Callable 示例import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 定义任务(有返回值) Callable<Integer> task = () -> { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; }; // 2. 包装任务 FutureTask<Integer> futureTask = new FutureTask<>(task); // 3. 启动线程 new Thread(futureTask).start(); // 4. 获取结果(会阻塞直到任务完成) System.out.println("1-100的和:" + futureTask.get()); // 输出5050 }}一键获取完整项目代码java2. 线程池(ThreadPoolExecutor)频繁创建/销毁线程会消耗资源,线程池通过复用线程提高效率,是生产环境的首选。Java 提供 Executors 工具类快速创建线程池:import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadPoolDemo { public static void main(String[] args) { // 创建固定大小的线程池(3个线程) ExecutorService pool = Executors.newFixedThreadPool(3); // 提交5个任务(线程池会复用3个线程处理) for (int i = 0; i < 5; i++) { int taskId = i; pool.submit(() -> { System.out.println("处理任务" + taskId + ",线程:" + Thread.currentThread().getName()); }); } // 关闭线程池 pool.shutdown(); }}一键获取完整项目代码java四、并发带来的问题及解决方案并发虽提高效率,但多线程共享资源时会引发问题:1. 线程安全问题当多个线程同时操作共享数据(如全局变量、集合),可能导致数据不一致。示例:两个线程同时对变量 count 做 ++ 操作,预期结果为 2,实际可能为 1(因 ++ 是多步操作,可能被打断)。2. 解决方案synchronized 关键字:通过“锁”保证同一时间只有一个线程执行临界区代码(修饰方法或代码块)。public class SynchronizedDemo { private static int count = 0; private static final Object lock = new Object(); // 锁对象 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { // 同步代码块:同一时间只有一个线程进入 count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { count++; } } }); t1.start(); t2.start(); t1.join(); // 等待线程执行完毕 t2.join(); System.out.println("count最终值:" + count); // 正确输出20000 }}一键获取完整项目代码javajava.util.concurrent 工具类:提供线程安全的集合(如 ConcurrentHashMap)、原子类(如 AtomicInteger)、锁机制(如 ReentrantLock)等,比 synchronized 更灵活。五、并发编程的核心挑战可见性:一个线程修改的共享变量,其他线程可能无法立即看到(因 CPU 缓存导致)。解决方案:使用 volatile 关键字(保证变量修改后立即刷新到主内存)。原子性:一个操作不可被中断(如 count++ 实际是“读-改-写”三步,非原子操作)。解决方案:synchronized、原子类(AtomicInteger)。有序性:CPU 可能对指令重排序优化,导致代码执行顺序与预期不一致。解决方案:volatile、synchronized 或显式内存屏障。六、总结并发的本质:通过多线程交替执行,提高资源利用率和程序响应速度。核心问题:线程安全(数据不一致),需通过锁机制或并发工具解决。实践建议:优先使用线程池管理线程,避免手动创建;复杂场景下借助 java.util.concurrent 包的工具类(如 CountDownLatch、Semaphore)简化开发。理解并发是 Java 进阶的关键,尤其在高并发场景(如分布式系统、高流量服务器)中,合理设计并发模型能显著提升系统性能。————————————————原文链接:https://blog.csdn.net/2508_93307008/article/details/153339720
-
什么是动态代理首先,动态代理是代理模式的一种实现方式,代理模式除了动态代理还有 静态代理,只不过静态代理能够在编译时期确定类的执行对象,而动态代理只有在运行时才能够确定执行对象是谁。代理可以看作是对最终调用目标的一个封装,能够通过操作代理对象来调用目标类,这样就可以实现调用者和目标对象的解耦合。动态代理的应用场景有很多,最常见的就是 AOP 的实现、RPC 远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。动态代理的实现有很多,但是 JDK 动态代理是很重要的一种,下面就 JDK 动态代理来深入理解一波。 JDK 动态代理首先先来看一下动态代理的执行过程在 JDK 动态代理中,实现了 InvocationHandler 的类可以看作是 代理类(因为类也是一种对象,所以上面为了描述关系,把代理类形容成了代理对象)。JDK 动态代理就是围绕实现了 InvocationHandler 的代理类进行的,比如下面就是一个 InvocationHandler 的实现类,同时它也是一个代理类。public class UserHandler implements InvocationHandler { private UserDao userDao; public UserHandler(UserDao userDao){ this.userDao = userDao; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { saveUserStart(); Object obj = method.invoke(userDao, args); saveUserDone(); return obj; } public void saveUserStart(){ System.out.println("---- 开始插入 ----"); } public void saveUserDone(){ System.out.println("---- 插入完成 ----"); }}一键获取完整项目代码代理类一个最最最重要的方法就是 invoke 方法,它有三个参数Object proxy: 动态代理对象,关于这个方法后面会说。Method method: 表示最终要执行的方法,method.invoke 用于执行被代理的方法,也就是真正的目标方法Object[] args: 这个参数就是向目标方法传递的参数。这里构造好了代理类,现在就要使用它来实现对目标对象的调用,那么如何操作呢?请看下面代码public static void dynamicProxy(){ UserDao userDao = new UserDaoImpl(); InvocationHandler handler = new UserHandler(userDao); ClassLoader loader = userDao.getClass().getClassLoader(); Class<?>[] interfaces = userDao.getClass().getInterfaces(); UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler); proxy.saveUser();}一键获取完整项目代码如果要用 JDK 动态代理的话,就需要知道目标对象的类加载器、目标对象的接口,当然还要知道目标对象是谁。构造完成后,就可以调用 Proxy.newProxyInstance方法,然后把类加载器、目标对象的接口、目标对象绑定上去就完事儿了。这里需要注意一下 Proxy 类,它就是动态代理实现所用到的代理类。Proxy 位于java.lang.reflect 包下,这同时也旁敲侧击的表明动态代理的本质就是反射。下面就围绕 JDK 动态代理,来深入理解一下它的原理,以及搞懂为什么动态代理的本质就是反射。 动态代理的实现原理在了解动态代理的实现原理之前,先来了解一下 InvocationHandler 接口 InvocationHandler 接口JavaDoc 告诉我们,InvocationHandler 是一个接口,实现这个接口的类就表示该类是一个代理实现类,也就是代理类。InvocationHandler 接口中只有一个 invoke 方法。动态代理的优势在于能够很方便的对代理类中方法进行集中处理,而不用修改每个被代理的方法。因为所有被代理的方法(真正执行的方法)都是通过在 InvocationHandler 中的 invoke 方法调用的。所以只需要对 invoke 方法进行集中处理。invoke 方法只有三个参数public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;一键获取完整项目代码proxy:代理对象method: 代理对象调用的方法args:调用方法中的参数。动态代理的整个代理过程不像静态代理那样一目了然,清晰易懂,因为在动态代理的过程中,没有看到代理类的真正代理过程,也不明白其具体操作,所以要分析动态代理的实现原理,必须借助源码。那么问题来了,首先第一步应该从哪分析?如果不知道如何分析的话,干脆就使用倒推法,从后往前找,直接先从 _Proxy.newProxyInstance_入手,看看是否能略知一二。 Proxy.newInstance 方法分析Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。Proxy.newProxyInstance 源码(java.lang.reflect.Proxy)public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } Class<?> cl = getProxyClass0(loader, intfs); try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); } final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } return cons.newInstance(new Object[]{h}); } catch (Exception e) { ...}一键获取完整项目代码乍一看起来有点麻烦,其实源码都是这样,看起来非常复杂,但是慢慢分析、厘清条理过后就好,最重要的是分析源码不能着急。上面这个 Proxy.newProxyInstsance 其实就做了下面几件事,这里画了一个流程图作为参考。从上图中也可以看出,newProxyInstsance 方法最重要的几个环节就是获得代理类、获得构造器,然后构造新实例。对反射有一些了解的同学,应该会知道获得构造器和构造新实例是怎么回事。所以重点就放在了获得代理类,这是最关键的一步,对应源码中的 _Class<?> cl = getProxyClass0(loader, intfs);_ 进入这个方法一探究竟private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } return proxyClassCache.get(loader, interfaces);}一键获取完整项目代码这个方法比较简单,首先会直接判断接口长度是否大于 65535(刚开始看到这里是有点不明白的,这个判断是要判断什么?interfaces 这不是一个 class 类型吗,从 length 点进去也看不到这个属性,细看一下才明白,这居然是可变参数,Class … 中的 … 就是可变参数,所以这个判断应该是判断接口数量是否大于 65535。)然后会直接从 proxyClassCache 中根据 loader 和 interfaces 获取代理对象实例。如果能够根据 loader 和 interfaces 找到代理对象,将会返回缓存中的对象副本;否则,它将通过 ProxyClassFactory 创建代理类。proxyClassCache.get 就是一系列从缓存中的查询操作,注意这里的 proxyClassCache 其实是一个 WeakCache,WeakCahe 也是位于 java.lang.reflect 包下的一个缓存映射 map,它的主要特点是一个弱引用的 map,但是它内部有一个 SubKey ,这个子键却是强引用的。这里不用去追究这个 proxyClassCache 是如何进行缓存的,只需要知道它的缓存时机就可以了:即在类加载的时候进行缓存。如果无法找到代理对象,就会通过 ProxyClassFactory 创建代理,ProxyClassFactory 继承于 BiFunctionprivate static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> {...}一键获取完整项目代码ProxyClassFactory 里面有两个属性一个方法。proxyClassNamePrefix:这个属性表明使用 ProxyClassFactory 创建出来的代理实例的命名是以 “$Proxy” 为前缀的。nextUniqueNumber:这个属性表明 ProxyClassFactory 的后缀是使用 AtomicLong 生成的数字所以代理实例的命名一般是 Proxy1这种。这个 apply 方法是一个根据接口和类加载器进行代理实例创建的工厂方法,下面是这段代码的核心。@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { ... long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); }}一键获取完整项目代码可以看到,代理实例的命名就是上面所描述的那种命名方式,只不过它这里加上了 proxyPkg 包名的路径。然后下面就是生成代理实例的关键代码。ProxyGenerator.generateProxyClass 跟进去是只能看到 .class 文件的,class 文件是虚拟机编译之后的结果,所以要看一下 .java 文件源码。.java 源码位于 OpenJDK中的 sun.misc 包中的 ProxyGenerator 下。此类的 generateProxyClass() 静态方法的核心内容就是去调用 generateClassFile() 实例方法来生成 Class 文件。方法太长了就不贴了,这里就大致解释以下其作用:第一步:收集所有要生成的代理方法,将其包装成 ProxyMethod 对象并注册到 Map 集合中。第二步:收集所有要为 Class 文件生成的字段信息和方法信息。第三步:完成了上面的工作后,开始组装 Class 文件。而 defineClass0 这个方法点进去是 native ,底层是 C/C++ 实现的,于是去看了一下 C/C++ 源码,路径在点开之后的 C/C++ 源码还是挺让人绝望的。不过再回头看一下这个 defineClass0 方法,它实际上就是根据上面生成的 proxyClassFile 字节数组来生成对应的实例罢了,所以不必再深究 C/C++ 对于代理对象的合成过程了。所以总结一下可以看出,JDK 生成了一个叫 $Proxy0 的代理类,这个类文件放在内存中的,在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。所以最开始的 dynamicProxy 方法反编译后的代码就是这样的public final class $Proxy0 extends java.lang.reflect.Proxy implements com.cxuan.dynamic.UserDao { public $Proxy0(java.lang.reflect.InvocationHandler) throws ; Code: 0: aload_0 1: aload_1 2: invokespecial #8 // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V 5: return一键获取完整项目代码可以看到代理类继承了 Proxy 类,所以也就决定了 Java 动态代理只能对接口进行代理。 于是,上面这个图应该就可以看懂了。 invoke 方法中第一个参数 proxy 的作用细心的小伙伴们可能都发现了,invoke 方法中第一个 proxy 的作用是啥?代码里面好像 proxy 也没用到,这个参数的意义是啥呢?它运行时的类型是啥啊?为什么不使用 this 代替呢?Stackoverflow 给出了一个回答 https://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca什么意思呢?就是说这个 proxy ,它是真正的代理对象,invoke 方法可以返回调用代理对象方法的返回结果,也可以返回对象的真实代理对象,也就是 $Proxy0,这也是它运行时的类型。至于为什么不用 this 来代替 proxy,因为实现了 InvocationHandler 的对象中的 this ,指代的还是 InvocationHandler 接口实现类本身,而不是真实的代理对象————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/152166279
-
HTTP协议HTTP 理论和实践同样重要。如果我们未来写web开发(写网站)。HTTP就是我们工作中最常用到的东西。什么是HTTP?HTTP是应用层的协议,HTTP现在大规模使用的版本是 HTTP/1.1使用HTTP协议的场景:1.浏览器打开网站(基本上)2.手机APP访问对应的服务器(大概率)报文格式HTTP最重要的就是报文格式了HTTP的报文格式和 TCP/IP/UDP 这些不同,HTTP 的报文格式,要分两个部分来看待请求和响应HTTP协议,是一种 一问一答 结构模型的协议,请求和响应的协议格式,是有所差别的这里也不只有一问一答的模型结构:一问一答(访问网站)多问一答(上传文件,把请求拆分成多份传给服务器)一问多答(下载文件,把响应拆成多份进行下载)多问多答(串流 / 远程桌面,远程桌面就是这边操作了,那边就会有响应)如何查看到 HTTP 请求和响应的格式呢?抓包工具:把网卡上经过的数据,获取到,并显示出来(这个是我们必备的技能,分析和调试程序的重要手段)Fiddler抓包工具:wireshark:功能非常强大,可以抓各种各样的协议,使用起来比较复杂,但是用这个抓 HTTP 是不太方便的Fiddler工具:是专门用来抓HTTP的,使用起来简单,企业中也经常用到在官网下载Fiddler Classic 版本的就可以FIddler的请求和响应,还要下载证书,否则就只抓包http了,不能抓https了安装证书:出现这个点cancel就行4. Fiddler 本质上是一个 代理,可能会和其他的代理软件冲突举个栗子:除了 fiddler 之外,有的程序也是代理(1) 加速器(2) vpn…这些代理程序之间可能会产生冲突如果你的 fiddler 不能抓包了,一定要检查关闭之前的开启的代理软件(也可能是一个浏览器插件),如果还是不行,还可以尝试换一个浏览器,有的时候 edge浏览器 可能会不行,比如出现下面这样的情况postman 是构造请求的软件fiddler 是抓取/显示已有的请求的软件ctrl + a:选中所有的请求,delete删除这里所有的请求,方便后续观察想要观察的httpHTTP 协议是文本格式的协议(协议里面都是字符串)TCP,UDP,IP…都是二进制格式的协议**HTTP 响应也是文本的。直接查看,往往能看到二进制的数据(压缩后的),HTTP 响应经常会被压缩。因为压缩之后,体积更小,传输的时候,节省网络带宽。**但是,压缩和解压缩,是需要消耗额外的 cpu 和 时间 的。其实也没关系,一台服务器,最贵的硬件资源就是网络带宽,这样压缩和解压缩就是用便宜的换贵的解压缩之后,可以看到,响应的数据其实是 html,浏览器上显示的网页,就是html,往往都是浏览器先请求对应的服务器,从服务器这边拿到的 网页 数据(html),每次访问网页,都会下载对应的网页数据,会消耗一定的带宽(不像我们下载APP一样,只需要下载一次) 请求:键值对这里是 标准规定 的,所以不能自己胡编乱造响应:URLURL 是计算机中的非常重要的概念,不仅仅是在 HTTP 中涉及到jdbc设置数据源setUrl(“jdbc:mysql://127.0.0.1:3306/java1?characterEncoding=utf8&useSSL=false”);setUsersetPasswordURL,描述了某个资源在网络上的所属位置。数据库也是一种资源URL的具体信息:1.协议方案名(协议名):例如,https:// ,jdbc:mysql://2.登录信息(认证):这个东西现在几乎不会用到了(尤其是针对用户的产品)3.服务器地址:可以 IP地址,也可以是 域名4.服务器端口号:5.带层次的文件路径:6.查询字符串(query string)关于以上 url 的几个部分(query string),举个例子:对于 query string 来说,如果 value 部分要包含一些特殊符号的话,往往需要进行 urlencode 操作比如:搜索C++这个词后面使用 url 的时候,要记得针对 query string 的内容进行好 urlencode 工作。如果不处理好,有些浏览器就可能会解析失败,导致请求无法正常工作中文汉字也需要转义,如果不转义的话,汉字也出现特殊的符号的话,不就bug了7.片段标识符:比如说这个网址就有 [vue技术文档],片段标识符实现页面内部的跳转(https://cn.vuejs.org/guide/introduction.html#the-progressive-framework)请求方法方法既有 GET 还有POST,出现最多的还是GET,POST出现的比较少POST请求POST(1) 登录(2) 上传文件习惯用法(不是硬性规定,也可以不遵守的)这个不遵守的话,客户端和服务器都要一起不遵守的,不然会出现问题GET 请求,通常会把要传给服务器的数据,加到 url 的 query string 中。POST 请求,通常把要传给服务器的数据,加到 body 中登录和上传头像的请求 出现蓝色的字体(在刚开始加载网页的时候出现的,后续Fiddler删除这些包,就不会出现了),是获取到网页(得到 html 的响应)对于浏览器缓存的理解:刚才最开始没有抓到这里的返回的请求,是因为命中了浏览器的缓存存在网络缓存的原因:浏览器显示的网页,其实是从服务去这边下载的 html。html 内容可能会比较多,体积可能比较大,通过网络加载,消耗的时间就可能会比较多。浏览器一般都会自己带有缓存,就会把之前加载过的页面,保存在本地硬盘上。下次访问直接读取本地硬盘的数据即可。缓存可能出现的bug:如果缓存的是之前未修改的网页,缓存之后又修改了网页,就可能会出现bug上传图像的body本身是比较长的方法的种类:这些HTTP请求最初的初心是为了表示不同的 语义(不同的含义),现在在实际的使用过程中,初心,已经被遗忘了。HTTP的各种请求,目前来说已经不一定完全遵守自己的初心了,实际上程序员如何使用,更加随意了比如:还可以用GET来上传文件,用POST获取文件,也是可行的还有 POST和 PUT 目前来说,可以理解成 没有任何的区别!!!(任何使用 POST 的场景,换成 PUT 完全可以,反之亦然)GET 和 POST 的区别经典的面试题:GET 和 POST 的区别GET 和 POST 之间的差别,网络上的有些说法,需要大家来注意!(都是错误的说法)GET 请求能传递的数据量有上限,POST 传递的数据量没有上限GET 请求传递的数据不安全,POST 请求传递的数据更安全安全关键在于是否加密了GET 只能给服务器传输 文本数据。POST 可以给服务器传输文本 和 二进制数据(1) GET 也不是不能使用body(body 中是可以直接放二进制数的)(2) GET 也可以把 二进制的数据进行 base64 转码(转码成字符串的文本文件),放到 url 的query string 中,之后再解码,不也是二进制的数据吗下面说法不够准确,但是也不是完全错的1.GET 请求是幂等的。POST 请求不是幂等的。幂等:数学概念,输入相同的内容,输出是稳定的。举个栗子:吃进去的是草,挤出来的是奶。如果任何时候吃草,挤出来的都是奶,就是幂等的。如果吃草之后,不同时候挤出来的东西不一样,就不是幂等的。GET 和 POST 具体是否是幂等的,取决于代码的实现。GET 是否幂等,也不绝对。只不过 RFC 标准文档上建议 GET 请求实现成幂等的。再举个例子:不同的时间,广告的顺序都可能会不同 Header有哪些常见的Header,理解Header的含义HOSTHOST:www.sogou.comHOST后这个信息在 url 中也是存在的比如,在使用代理的情况下,Host 的内容是可能和 url 中的内容不同的Content-Length:body 中数据的长度Content-Type:body 中数据的格式注意:请求中有 body,才会有这两个属性。通常情况下 GET 请求没有 body,POST 请求有 bodybody 中的格式,可以选择的方式是非常多的body中数据的格式:请求:1.json2.form 表单的格式:相当于是把 GET 的query string 给搬到 body 中。(后面写个代码来构造一个)上传图片:3.form-data 的格式:上传文件的时候,会涉及到(也不一定就是 form-data,也可能是 form 表单)响应:后续给服务器提交请求,不同的 Content-Type(body),服务器处理数据的逻辑是不同的。服务器返回数据给浏览器,也需要设置合适的 Content-Type,浏览器也会根据不同的Content-Type 做出不同的处理User-Agent(简称UA)UA是用来区分不同的设备的,上古时期的UA是用来区分浏览器的兼容问题的,可以让旧的浏览器支持文字,让新的浏览器支持图片,这样也就区分开了现在 UA 使用来区分是PC端还是 移动端的Referer:描述了当前页面是从哪个页面跳转而来的如果是直接在地址栏输入 url(或者是点击收藏夹中的按钮)都是没有 RefererReferer的用途:这个也可以用来算钱的搜索引擎,每点击一次,它都会赚钱通过域名来区分是百度投放的广告,还是搜狗投放的广告 5. CookieCookie可以认为是浏览器在本地存储数据的一种机制对于安全性的考虑,所以引入了Cookie,存储简单的字符串也是够用的Cookie是怎么进行存储的(重点)浏览器中的 CookieCookie的作用:Servlet / Spring 中会结合代码更深入地理解Cookie响应状态码状态码响应 状态码表示了这次请求对应的响应,是啥样的状态(成功,失败,其他的情况,对应的原因是什么)这里的状态码,种类非常多。咱们不需要全都记住成功2xx 都表示成功,200是最常见的重定向3xx 表示重定向,(可以理解为爱情转移)请求中访问的是 A 这样的地址。响应返回了一个重定向报文,告诉你应该要访问B地址举个例子:比如你请教一个老师,老师说这边还没有空,叫你去找另一个老师,如果还不行的话,再来找我301和302301 Moved Permanently是永久重定向,是有缓存的,比如你地址就在新的网站了,以后就不要访问旧的网站了302 Move tmporarily 是临时重定向,是没有缓存的,指不定后面又要更改,比如说你地址在新网站了,但是可能后面又会改回旧的网站重定向的响应报文中,会带有Location字段,描述出当前要跳转到哪个新的地址(后面会在代码中具体演示)请求错误客户端,也就是浏览器(客户端)会构造一个http请求,不符合服务器的一些要求404 Not Found请求中访问的资源,在服务器上不存在比如:HTTP/1.1 404 Not Found404这个状态码表示的是自愿不存在,同时在 body 中也是可以返回一个指定的错误页面的,很多网站会把这个错误页面做的很丰富多彩比如:bilibili403 Forbidden表示访问的资源没有权限服务器错误5xx 表示服务器出错了看到这个说明服务器挂了比较容易出现的是500,后面我们自己写服务器的时候,还是比较容易写出500这样的问题的(一般就是你的代码有bug)一个经典的面试题:说一下,HTTP的状态码有哪些常见的通过不同的数字来表示状态,这种做法其实是非常常见的(在C语言中也有,比如 errno,表示打开文件失败的原因, strerror 把errno翻译成字符串)可能我们面试中也会考察C语言的 如何让客户端构造一个HTTP请求如何让服务器处理一个HTTP请求,这是非常重要的内容(Servlet/Spring中都会涉及到这一部分)浏览器:直接在浏览器地址栏输入url,此时构造了一个GET请求html中,一些特殊的html标签,可能会触发GET请求,比如像 img,a,link,script…通过form表单来触发GET/POST请求(我们需要了解一下),form本质也是一个HTML标签写一个简单的html代码,来编写逻辑(解释form的逻辑)form表达如何编写?使用form标签设置提交按钮:构造GET请求:构造POST请求:4. ajax的方式1.引入jquery库(第三方库,是需要额外下载引入的)前端引入第三方库非常容易的,只要代码中写一个库的地址即可jquery cdn中有一些资源和库2.编写代码回调函数:js中定义变量:ajax的get请求写法:ajax的post请求写法:构造请求还有更简单,更方便的方式,比如使用第三方工具,可以实现这里的效果。form能够构造get和post请求,ajax也能构造get和post请求。上面的知道这些就可以了,等学了前端来看才能够懂!!!一种更简单的构造HTTP请求的方式直接通过第三方工具,图形化的界面来构造。我们这里使用postman先创建一个Workspaces,然后创建标签页请求的设定,最后发送请求postman 也是我们常用的构造请求的方式,一般是用于测试阶段不会写ajax也没事,postman都会给你自动生成代码,就在右上角的code按钮HTTPS引入HTTPS的原因:使用HTTPS进行加密,也是为了防止运营商劫持,还有比如在商场你连商场的wifi,也可能会被黑客截获数据,黑客把wifi伪装成商场的wifi一样的名字————————————————原文链接:https://blog.csdn.net/2301_79722622/article/details/151930571
-
1.什么是闭包:闭包是JavaScript中最强大且独特的特性之一,它是函数与其词法环境的组合。闭包使得函数能够访问其外部作用域的变量,即使外部函数已经执行完毕。这种机制让JavaScript具备了许多其他语言需要复杂语法才能实现的功能。// 最简单的闭包示例function outer() { const message = "Hello from outer!"; // 外部变量 function inner() { console.log(message); // inner函数"记住"了message } return inner; // 返回inner函数}const myClosure = outer(); myClosure(); // "Hello from outer!"一键获取完整项目代码javascript2. 核心概念与前置知识:2.1 执行上下文 (Execution Context):执行上下文是 JavaScript 代码执行时的环境,每当代码执行时都会创建对应的执行上下文。执行上下文栈 (Call Stack)全局执行上下文Global EC函数执行上下文 1Function EC 1函数执行上下文 2Function EC 2函数执行上下文 3Function EC 3执行上下文的组成部分:// 伪代码:执行上下文的内部结构ExecutionContext = { // 1. 词法环境 (用于let/const和函数声明) LexicalEnvironment: { EnvironmentRecord: {}, // 环境记录 outer: null // 外部环境引用 }, // 2. 变量环境 (用于var声明) VariableEnvironment: { EnvironmentRecord: {}, outer: null }, // 3. this绑定 ThisBinding: undefined}一键获取完整项目代码javascript2.2 词法环境 (Lexical Environment):词法环境是存储标识符-变量映射的结构,由环境记录和外部环境引用组成。环境记录类型词法环境结构声明式环境记录Declarative对象环境记录Object全局环境记录Global词法环境Lexical Environment环境记录Environment Record外部环境引用Outer Reference词法环境的创建过程:function createLexicalEnvironment() { // 示例:词法环境的创建 function outer(x) { let a = 10; const b = 20; function inner(y) { let c = 30; console.log(a + b + c + x + y); // 访问多个作用域的变量 } return inner; } return outer;}// 执行过程中的词法环境变化/*1. 全局词法环境:{ EnvironmentRecord: { createLexicalEnvironment: <function>, outer: <function> }, outer: null}2. outer函数的词法环境:{ EnvironmentRecord: { x: 参数值, a: 10, b: 20, inner: <function> }, outer: <全局词法环境的引用>}3. inner函数的词法环境:{ EnvironmentRecord: { y: 参数值, c: 30 }, outer: <outer函数词法环境的引用>}*/一键获取完整项目代码javascript2.3 作用域链 (Scope Chain)作用域链是通过词法环境的外部引用形成的链条,用于标识符解析。当前词法环境父级词法环境全局词法环境null作用域链查找算法// 作用域链查找示例function demonstrateScopeChain() { const globalVar = "全局变量"; function level1() { const level1Var = "第一层变量"; function level2() { const level2Var = "第二层变量"; function level3() { const level3Var = "第三层变量"; // 变量查找顺序演示 console.log(level3Var); // 1. 在当前环境找到 console.log(level2Var); // 2. 向上一层查找 console.log(level1Var); // 3. 继续向上查找 console.log(globalVar); // 4. 查找到全局环境 // console.log(nonExistent); // 5. 找不到则报错 } return level3; } return level2; } return level1;}// 调用过程中的作用域链const fn = demonstrateScopeChain()()();fn(); // 执行时会沿着作用域链查找变量一键获取完整项目代码javascript3. 代码示例与分析:3.1 经典的闭包示例function makeCounter() { let count = 0; // 外部函数的局部变量 return function() { // 返回的内部函数形成闭包 count++; // 访问外部函数的变量 return count; };}const counter = makeCounter(); // 调用外部函数console.log(counter()); // 1 - 调用闭包函数console.log(counter()); // 2 - count 变量被保持console.log(counter()); // 3一键获取完整项目代码javascript3.2 逐行执行过程分析全局执行上下文makeCounter执行上下文闭包函数1. 创建全局执行上下文2. 调用makeCounter(),创建新执行上下文3. 创建count变量,值为04. 创建匿名函数,形成闭包5. 返回函数,makeCounter执行完毕6. makeCounter执行上下文出栈7. 但闭包仍保持对count的引用8. 调用counter()9. 访问并修改count变量10. 返回结果全局执行上下文makeCounter执行上下文闭包函数详细步骤解析:// 步骤 1-2: 全局执行上下文创建,调用makeCounterfunction makeCounter() { // 步骤 3: 在makeCounter的词法环境中创建count变量 let count = 0; // 步骤 4: 创建匿名函数,该函数的[[Environment]]属性 // 指向makeCounter的词法环境,形成闭包 return function() { // 步骤 9: 通过作用域链找到外部的count变量 count++; return count; }; // 步骤 5-6: makeCounter执行完毕,执行上下文出栈 // 但count变量因为被闭包引用而不会被垃圾回收}// 步骤 7: counter变量保存了闭包函数的引用const counter = makeCounter();// 步骤 8-10: 每次调用counter()都会访问保存的count变量console.log(counter()); // 1一键获取完整项目代码javascript3.3 V8 引擎的优化V8 引擎对闭包进行了以下优化:变量提升优化:只保留被闭包实际使用的外部变量内存管理:未被引用的外部变量会被垃圾回收作用域分析:在编译时分析变量使用情况function optimizationExample() { let used = "被闭包使用的变量"; let unused = "未被使用的变量"; // V8会优化掉这个变量 let alsoUnused = "同样未被使用"; return function() { console.log(used); // 只有这个变量会被保留在闭包中 };}一键获取完整项目代码javascript注意:在调试时,由于V8的优化,某些未使用的变量可能在调试器中显示为 “undefined”。4. 经典应用场景与最佳实践:4.1 模块化封装const Calculator = (function() { let result = 0; // 私有变量 return { add: function(x) { result += x; return this; }, multiply: function(x) { result *= x; return this; }, getResult: function() { return result; }, reset: function() { result = 0; return this; } };})();// 使用Calculator.add(5).multiply(2).getResult(); // 10一键获取完整项目代码javascript4.2 事件处理与回调function createButtonHandler(name) { return function(event) { console.log(`按钮 ${name} 被点击了`); // name变量被闭包保存 };}document.getElementById('btn1').onclick = createButtonHandler('按钮1');document.getElementById('btn2').onclick = createButtonHandler('按钮2');一键获取完整项目代码javascript4.3 防抖和节流// 防抖函数function debounce(func, delay) { let timeoutId; // 被闭包保存的变量 return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); };}// 节流函数function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } };}一键获取完整项目代码javascript5. 常见陷阱与解决方案:5.1 循环中的闭包陷阱问题代码:// 经典错误示例for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出三次 3 }, 100);}一键获取完整项目代码javascript解决方案:// 方案1:使用IIFE创建新的作用域for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 输出 0, 1, 2 }, 100); })(i);}// 方案2:使用let块级作用域for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出 0, 1, 2 }, 100);}// 方案3:使用bindfor (var i = 0; i < 3; i++) { setTimeout(function(j) { console.log(j); // 输出 0, 1, 2 }.bind(null, i), 100);}一键获取完整项目代码javascript5.2 内存泄漏风险// 可能导致内存泄漏的代码function createHandler() { const largeData = new Array(1000000).fill('data'); // 大量数据 return function() { // 即使不使用largeData,它也会被闭包保留 console.log('handler called'); };}// 解决方案:显式释放不需要的引用function createHandler() { const largeData = new Array(1000000).fill('data'); const needed = largeData.slice(0, 10); // 只保留需要的部分 return function() { console.log(needed.length); // largeData会被垃圾回收 };}————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/151932979
-
如果你用过 Java 集合(如List、Map),一定见过List<String>、Map<Integer, String>这样的写法 —— 这就是泛型。泛型是 Java 5 引入的核心特性,它解决了 “容器存储元素类型不明确” 的问题,让代码更安全、更简洁。今天我们就从泛型的作用讲起,深入剖析泛型类、泛型方法、泛型接口的定义与使用,配合直观图解,让你彻底搞懂泛型!一、为什么需要泛型?—— 从两个痛点说起 在没有泛型的 Java 早期版本中,集合(如ArrayList)只能存储Object类型的元素。这会导致两个严重问题:类型不安全和频繁强制转型。痛点 1:类型不安全(运行时错误) 假设我们创建一个ArrayList,本意是存字符串,却不小心存入了整数。编译器不会报错,但运行时调用字符串方法会抛ClassCastException:// Java 5之前的代码(无泛型)List list = new ArrayList();list.add("hello");list.add(123); // 存入整数,编译器不报错 // 取出元素时,假设都是字符串String str = (String) list.get(1); // 运行时报错:Integer cannot be cast to String一键获取完整项目代码java痛点 2:频繁强制转型(代码冗余)即使存入的元素类型一致,取出时也必须手动转型,代码繁琐且易出错:List list = new ArrayList();list.add("apple");list.add("banana"); // 每次取元素都要转型String s1 = (String) list.get(0);String s2 = (String) list.get(1);一键获取完整项目代码java泛型如何解决这些问题? 泛型的核心思想是:在定义类 / 接口 / 方法时,不指定具体类型,而是留出 “类型参数”,在使用时再指定具体类型。用泛型改写上面的例子:// 定义时指定类型参数<String>,表示只能存字符串List<String> list = new ArrayList<>();list.add("hello");// list.add(123); // 编译时直接报错,类型不匹配 // 取出时无需转型,编译器已知是String类型String str = list.get(0);一键获取完整项目代码java泛型的两大核心作用:类型安全:编译时检查元素类型,避免存入错误类型的元素(将运行时错误提前到编译时);避免强制转型:编译器自动推断元素类型,取出时无需手动转型,简化代码。泛型作用图解二、泛型类:让类支持 “类型参数化” 泛型类是指在类定义时声明类型参数,使得类中的字段、方法参数或返回值可以使用这些类型参数。当创建类的实例时,指定具体类型,从而实现 “一个类适配多种数据类型”。1. 泛型类的定义语法public class 类名<类型参数1, 类型参数2, ...> { // 类型参数可以作为字段类型 private 类型参数1 变量名; // 可以作为方法参数或返回值类型 public 类型参数1 方法名(类型参数2 参数) { // ... }}一键获取完整项目代码java类型参数命名规范(约定俗成,增强可读性):T:Type(表示任意类型);E:Element(表示集合中的元素类型);K:Key(表示键类型);V:Value(表示值类型);若有多个参数,可用T1、T2或K、V等组合。2. 泛型类示例:自定义容器类假设我们需要一个 “容器” 类,既能存整数,也能存字符串,还能存自定义对象。用泛型类实现:// 定义泛型类,类型参数为T(表示容器中元素的类型)public class Container<T> { private T item; // 用T作为字段类型 // 构造方法,参数类型为T public Container(T item) { this.item = item; } // 方法返回值类型为T public T getItem() { return item; } // 方法参数类型为T public void setItem(T item) { this.item = item; }}一键获取完整项目代码java使用泛型类时,指定具体类型:// 创建存字符串的容器Container<String> strContainer = new Container<>("hello");String str = strContainer.getItem(); // 无需转型 // 创建存整数的容器Container<Integer> intContainer = new Container<>(123);int num = intContainer.getItem(); // 无需转型 // 创建存自定义对象的容器(如User类)Container<User> userContainer = new Container<>(new User("张三"));User user = userContainer.getItem();一键获取完整项目代码java注意:类型参数不能是基本类型(如int、double),必须用包装类(Integer、Double)。3. 多类型参数的泛型类如果需要多个类型参数(如键值对),可以声明多个类型参数:// 键值对泛型类,K表示键类型,V表示值类型public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // getter和setter public K getKey() { return key; } public V getValue() { return value; }}一键获取完整项目代码java使用时分别指定键和值的类型:Pair<String, Integer> pair = new Pair<>("age", 20);String key = pair.getKey(); // "age"Integer value = pair.getValue(); // 20一键获取完整项目代码java泛型类结构图解三、泛型方法:单个方法的 “类型参数化” 泛型方法是指在方法声明时单独声明类型参数的方法,它可以定义在泛型类中,也可以定义在普通类中。泛型方法的核心优势是:方法的类型参数独立于类的类型参数,灵活性更高。1. 泛型方法的定义语法// 修饰符 <类型参数> 返回值类型 方法名(参数列表) { ... }public <T> T 方法名(T 参数) { // ...}一键获取完整项目代码java关键:泛型方法必须在返回值前声明类型参数(如<T>),这是区分泛型方法与普通方法的标志。2. 泛型方法示例:通用打印方法实现一个方法,能打印任意类型的数组元素:public class GenericMethodDemo { // 泛型方法:打印任意类型的数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } public static void main(String[] args) { // 打印字符串数组 String[] strArray = {"a", "b", "c"}; printArray(strArray); // 输出:a b c // 打印整数数组 Integer[] intArray = {1, 2, 3}; printArray(intArray); // 输出:1 2 3 // 打印自定义对象数组(如User) User[] userArray = {new User("张三"), new User("李四")}; printArray(userArray); // 输出:User{name='张三'} User{name='李四'} }}一键获取完整项目代码java 为什么不用泛型类?如果用泛型类,每次打印不同类型的数组都要创建不同的类实例,而泛型方法可以直接通过静态方法调用,更简洁。3. 泛型方法与泛型类的区别对比项 泛型类 泛型方法类型参数声明 在类名后(如class A<T>) 在方法返回值前(如<T> void f())作用范围 整个类 仅当前方法灵活性 依赖类的实例化类型 调用时可独立指定类型泛型方法调用图解四、泛型接口:让接口支持 “类型参数化” 泛型接口与泛型类类似,在接口定义时声明类型参数,实现类可以指定具体类型或继续保留类型参数。1. 泛型接口的定义语法public interface 接口名<类型参数> { // 类型参数可以作为方法参数或返回值 类型参数 方法名(); void 方法名(类型参数 参数);}一键获取完整项目代码java2. 泛型接口示例:自定义比较器接口JDK 中的Comparable接口就是典型的泛型接口,我们模仿它定义一个简单的泛型接口:// 泛型接口:支持比较任意类型的对象public interface MyComparable<T> { // 比较当前对象与另一个对象,返回正数/负数/0表示大于/小于/等于 int compareTo(T other);}一键获取完整项目代码java实现方式 1:实现类指定具体类型// 让User类实现MyComparable,指定T为User(比较两个User对象)public class User implements MyComparable<User> { private String name; private int age; // 实现compareTo方法,按年龄比较 @Override public int compareTo(User other) { return this.age - other.age; } // 构造器和getter省略}一键获取完整项目代码java使用时:User u1 = new User("张三", 20);User u2 = new User("李四", 25);System.out.println(u1.compareTo(u2)); // -5(u1年龄小于u2)一键获取完整项目代码java实现方式 2:实现类继续保留泛型参数// 实现类不指定具体类型,继续使用泛型Tpublic class PairComparator<T> implements MyComparable<Pair<T, T>> { @Override public int compareTo(Pair<T, T> other) { // 假设Pair的比较逻辑(此处简化) return 0; }}一键获取完整项目代码java3. JDK 中的泛型接口:Comparable与ComparatorComparable<T>:类自身实现,定义 “自然排序”(如String按字典序排序);Comparator<T>:外部定义排序规则,更灵活(如按自定义规则排序)。这两个接口广泛用于Collections.sort()等方法中,是泛型接口的经典应用。泛型接口实现图解五、泛型的底层:类型擦除(简单了解) Java 泛型是 “编译时特性”,在运行时会擦除类型参数信息(称为 “类型擦除”)。也就是说,JVM 在运行时看不到List<String>和List<Integer>的区别,它们都会被擦除为List。类型擦除的规则:若类型参数有上限(如<T extends Number>),擦除为上限类型(Number);若没有上限,擦除为Object。例如:// 编译前List<String> list = new ArrayList<>();list.add("a"); // 编译后(类型擦除)List list = new ArrayList();list.add("a"); // 隐含String类型检查String s = (String) list.get(0); // 编译器自动添加转型一键获取完整项目代码java 为什么需要了解类型擦除?避免踩坑:例如不能用instanceof判断泛型类型(因为运行时已擦除):// 错误:编译不通过(无法判断List的泛型类型)if (list instanceof List<String>) { ... }一键获取完整项目代码java六、总结泛型是 Java 中简化代码、提升安全性的核心特性,核心要点:泛型的作用:类型安全:编译时检查元素类型,避免运行时类型转换错误;避免转型:编译器自动推断类型,减少手动转型代码。泛型类:在类定义时声明类型参数(如class Container<T>),实例化时指定具体类型,适用于类整体需要适配多种类型的场景。泛型方法:在方法返回值前声明类型参数(如<T> void print(T t)),可独立于类的泛型使用,灵活性更高。泛型接口:类似泛型类,实现类可指定具体类型或保留泛型(如Comparable<T>)。 掌握泛型是理解 Java 集合框架、实现通用组件的基础。实际开发中,合理使用泛型能让代码更简洁、更安全、更易维护。下一篇我们将深入泛型的高级特性(通配符、上限下限等),敬请期待!————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152209932
-
1,JUC(java.util.concurrent)的常见类1)Callable 接口我们之前学过Runnable接口,它是一个任务,我们可以在创建线程的时候把任务丢给线程使用匿名内部类等方法来完成创建对象,现在我们有了一个新的方法来创建任务,并且执行这个任务,就是我们的Callable接口,Runnable的run方法是没有返回值的,但是Callable提供了返回值,支持泛型,我们就能获取到我们想要的参数,我们来看看是怎么用的; Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { return null; } };一键获取完整项目代码java我们使用匿名内部类的方法创建一个Callable对象,并且重写call方法,就相当与重写Runnable的run方法, 我们是不能把这个对象直接放到线程的构造方法中的,因为Thread没有提供传入Callable的版本,我们要使用另一个类FutureTask来拿到结果,在把创建的futureTask对象放到线程创建时的构造方法中去; FutureTask<Integer> task = new FutureTask<>(callable); Thread t1 = new Thread(task); t1.start(); System.out.println(task.get());一键获取完整项目代码java这里的task.get方法会阻塞main线程结束,直到t1线程正确计算出结果; 2)ReentrantLock这个是上古时期的锁,现在有更智能,更好的替代synchronized,那我们还学它干嘛呢,它还活着就一定是有原因的,1,synchronized是关键字,是由JVM内部通过C++实现的,而ReentrantLock是一个类;2,synchronized是通过进出代码块来实现的,ReentrantLock需要Lock和UnLock方法来辅助; ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); a++; reentrantLock.unlock();一键获取完整项目代码java3,ReentrantLock除了提供Lock和unLock之外还提供了一个不会造成阻塞的tryLock()它会根据是否加锁成功返回true或者false;4,synchronized是非公平锁,而ReentrantLock是默认是非公平锁,但是也提供了公平锁的实现; 5,ReentrantLock的等待通知机制是Condition类,比synchronized的wait和notify功能更强3)线程池博主博主,咱们之前不是讲过线程池了吗,怎么又来一遍呀,确实嗷,上次虽然给大家详细讲过了,但是我们还没有用呀,哈哈哈哈哈,我直接上代码;我们先来简单的版的;public class Demo2 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(1111); } } }; ExecutorService executorService = Executors.newFixedThreadPool(1);//创建固定数目的线程池 //ExecutorService executorService1 = Executors.newSingleThreadExecutor(); 创建单线程池 //ExecutorService executorService2 = Executors.newCachedThreadPool(); 创建线程动态增长的线程池 //ScheduledExecutorService service = Executors.newScheduledThreadPool(1); 创建定时线程池 //executorService.submit(runnable); executorService.shutdown(); }}一键获取完整项目代码java我们还可以通过execute来提交任务executorService.execute(runnable);一键获取完整项目代码java都是官方给提供的现成的,我们这会来自己创建;ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20, 10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());一键获取完整项目代码java这就是我们自己创建的线程池,我们要把所有的参数都填上;1. 任务队列类型队列类型 特点ArrayBlockingQueue 有界队列,需指定容量LinkedBlockingQueue 无界队列(默认使用,可能 OOM)SynchronousQueue 不存储任务,直接提交给线程PriorityBlockingQueue 支持优先级排序2. 拒绝策略策略类 行为AbortPolicy(默认) 抛出 RejectedExecutionExceptionCallerRunsPolicy 由提交任务的线程直接执行任务DiscardPolicy 静默丢弃新任务DiscardOldestPolicy 丢弃队列中最旧的任务,然后重试提交工厂模式那个也是官方给提供的现成的哈哈哈哈,太懒了我; 4)信号量 Semaphore一种计数器,可以表示可用资源的个数;信号量的P操作,申请资源,计数器加一;信号量的V操作,释放资源,计数器减一;如果此时计数器为零,再尝试申请资源就会进入阻塞等待;有一点点像锁;我们使用acquire来申请资源,使用release来释放资源,我们来试试写代码;public class Demo4 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(3);//3个可用资源 Runnable runnable = new Runnable() { @Override public void run() { try { System.out.println("申请资源"); semaphore.acquire(); System.out.println("获取到了资源"); Thread.sleep(10000); semaphore.release(); System.out.println("释放资源"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; Thread t1= new Thread(runnable); Thread t2= new Thread(runnable); Thread t3= new Thread(runnable); t1.start(); t2.start(); t3.start(); Thread t4= new Thread(runnable); t4.start(); t4.join(); }}一键获取完整项目代码java我通过运行这个代码可以看到t1, t2,t3线程获取申请资源之后不释放,t4申请资源就要等着,直到10s之后,t4线程才开始工作;5)CountDownLatch也类似一个计数器,我们传入构造方法的参数就是需要完成的任务个数,完成一个任务就调用countDown()方法,主线程中使用await方法,等待所有任务完成主线程才结束;public class Demo5 { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(10); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(111); countDownLatch.countDown(); } }; for (int i = 0; i < 10; i++) { Thread t = new Thread(runnable); t.start(); } countDownLatch.await(); }}一键获取完整项目代码java2,线程安全的集合类我们之前学习的数据结构大部分是不安全的,我们还想使用之前的数据结构就要做相应的修改;1)多线程环境使用ArrayList1,使用ArrayList的第一种方式就是自己加锁,使用synchronized或者ReentrantLock,来对容易引发线程安全的地方来加以限制;2,就是套壳collections.synchronized(new ArrayList)对于public的方法都加上synchronized;3,使用CopyOnWriteArrayList这个方法是不去加锁的,我们知道,读操作是不影响线程安全的,那么我们在使用ArrayList的时候,我们修改了,我们就再复制一个数组,我们读取的时候只能读到旧的数据或者是已经修改完成的数据,不存在读取修改一半的情况,但是,如果我们的数据很大很大呢,难道我们要一下复制所有的元素吗,是的,就是这么难受,并且多个线程修改数据的时候也可能会发生问题,那我们干嘛要用它,这个是存在特定的使用场景的,服务器如果修改配置了的话是需要重新启动的,我们玩游戏的时候,如果我们要修改设置,比如打开声音,或者设置按键等,难道我们还要关掉游戏吗,我们这时候就是我们给出指令,根据新的设置,服务器就会创建新的哈希数组,来代替旧的数组,完成配置文件的修改,而不是服务器的重启;2)多线程环境下使用队列1. ArrayBlockingQueue 基于数组实现的阻塞队列2. LinkedBlockingQueue 基于链表实现的阻塞队列3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列4. TransferQueue 最多只包含⼀个元素的阻塞队列3)多线程环境下使用哈希表哈希表,查找时间复杂度O(1)啊,这必选得拿到多线程中,我们之前讲过,Hashtable是线程安全的,但它只是对HashMap的所有方法加锁,效率肯定是不高的,我们有一个完美的替代品就是ConcurrentHashMap;ConcurrentHashMap是对桶级别加锁,和HashTable不一样,更高效; 大家还记不记得的哈希表是咋样的了, 我们要解决哈希冲突,我们通常是在每个下标中构建链表或者是红黑树;如果链表太长了,我们还涉及到扩容操作; ConcurrentHashMap是对每个下标都加锁的,锁对象就使用表头,当两个线程在不同的下标是,就不会发生锁竞争,当两个线程修改同一个下标时,就存在线程安全性问题了,因为有表头锁的存在就会发生竞争,成功避免了线程安全问题;另外,记录的元素个数size怎么办呢,两个线程同时增加数据,size也会有线程安全问题,还有加锁吗,忘了我们的AtomicIngter了吗,这个原子类也是很好用的呀,大家不要忘了;还有最后一个哈希扩容问题,如果发生扩容就意味着和CopyOnWriteArrayLIst一样了,我们要把原来的数距全部复制过来,那肯定需要很多的时间,所以我们不会一次就把所有元素复制过去,我们会把每次put一些数据的过程中偷偷复制一些数据到新哈希表,就意味着我们把100%的任务分三开,每次执行别的操作都完成一点点的任务,直到扩容完全完毕;————————————————原文链接:https://blog.csdn.net/2301_79083481/article/details/146140421
-
1.前置知识1.1 Tomcat定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应:tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件)1.2 Servlet1.2.1 定义Servlet是Java语言编写的、运行在服务器端的程序,它遵循一套标准的API规范(Tomcat是这套规范的一个具体实现/容器,并提供了让Servlet与前端交互的运行时环境)1.2.2 API示范创建项目/配置文件:(1)在IEDA中创建Maven项目(2)在pom.xml文件中添加servlet依赖(置于< project >< /project >标签下)<dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <!--servlet依赖版本应与jdk和tomcat的版本相匹配--> <version>6.1.0</version> <scope>provided</scope> </dependency></dependencies>运行本项目xml(3)在main路径下创建webapp/Web-INF/web.xml,在xml文件中添加以下内容<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app> <display-name>Archetype Created Web Application</display-name></web-app>运行本项目xml(4)下载插件:Smart Tomcat(为了方便启动项目)API示例:import jakarta.servlet.annotation.WebServlet;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;//设置访问路径(url)@WebServlet("/method")//继承HttpServlet并重写doGet和doPost方法public class MethodServlet extends HttpServlet { //接收method=post的请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPost"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPost"); } //接收method=put的请求 @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPut"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPut"); } //接收method=delete的请求 @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doDelete"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doDelete"); }}运行本项目java运行1.2.3 生命周期定义:Servlet 生命周期由 Web容器(如Tomcat)管理,包含加载、初始化、处理请求和销毁四个阶段。每个阶段对应特定的方法调用,开发者可通过重写这些方法实现自定义逻辑1.类加载:Web容器通过类加载器加载 Servlet 类(通常首次请求触发或容器启动时预加载,字节码文件被加载到内存但未实例化)。具体类加载流程请阅读:Java虚拟机——JVM(JavaVirtualMachine)解析一1.5 实例化:确认Servlet类成功加载后立刻执行,在整个Web容器中每种Servlet类(如HttpServlet)只会有一个实例化对象2.初始化:Web容器调用刚刚创建好的Servlet实例的init(ServletConfig config)方法,在整个servlet实例的生命周期中仅调用一次,主要作用是读取配置和资源加载。若初始化失败,抛出ServletException,Servlet不会被加入可用队列3.处理请求:Web容器为每个请求创建线程,调用service(ServletRequest req, ServletResponse res)方法。service() 方法根据HTTP请求类型(get/post)调用doGet()或doPost()4.销毁:Web容器调用 destroy()方法,Servlet 实例被标记为垃圾回收2.SpringBootServlet是Java EE规范中处理Web请求的核心组件,但随着应用复杂度提升,Servlet的直接使用显得笨重。Spring框架通过一系列抽象和扩展,简化了企业级应用开发我们可以用一个非常形象的比喻来贯穿始终:建造一座房子第一阶段:Servlet 时代 - 自己烧砖砌墙目标:建造一个能遮风挡雨的房子(一个能处理HTTP请求的Web应用)你的工作状态:材料: 你有最基础的原材料——泥土(Java 语言)和水(JVM)。你需要自己烧制砖块(编写Servlet类)工具: 只有简单的泥瓦刀(Servlet API)过程:1.你为每一面墙、每一扇门都亲手烧制一块特定的砖(编写 LoginServlet, UserServlet, OrderServlet)2.你亲自规划每块砖的位置(在web.xml中配置大量的 < servlet > 和 < servlet-mapping >)3.你亲自搅拌水泥,一块一块地砌墙(在每个 Servlet 的doGet/doPost 方法中手动解析参数、处理业务、组装 HTML)核心特点:高度灵活: 你可以造出任何形状的砖极其繁琐: 大量重复性劳动(每个 Servlet 都有获取参数、关闭连接等样板代码)难以维护: 砖块之间紧密耦合(对象依赖硬编码),想换一扇窗(修改功能)可能会牵动整面墙依赖外部: 房子建在别人的地上(需要将war包部署到外部的Tomcat服务器)总结:Servlet提供了Web开发的基础能力,但开发效率极低,代码冗余且难以维护第二阶段:Spring 时代 - 使用预制件和设计图纸目标:用更高效、更标准化的方式建造一个结构更好、更易扩展的房子你的工作状态:材料: 你不再烧砖,而是使用工厂提供的标准化预制件(Spring Bean,如 @Controller, @Service)核心创新: 你聘请了一位神奇的管家(IoC 容器)你不再亲自“砌砖”(用new实例化对象),只需告诉管家你需要什么(用@Autowired声明依赖)管家会自动把预制件(Bean)按照图纸(配置)组装好,送到你手上(依赖注入DI)过程:1.一个总大门(DispatcherServlet): 房子只有一个入口,所有访客(请求)都先到这里2.管家调度: 总大门处的接待员(DispatcherServlet)根据访客需求,呼叫房子里对应的专业房间(@Controller中的方法)来接待3.开发者只需专注于房间内的专业服务(业务逻辑),而不用关心访客是怎么进来的核心特点:解耦: 预制件之间是松耦合的,易于更换和测试专业化: AOP(面向切面编程)可以像“装修队”一样,非侵入式地为所有房间统一安装中央空调(日志、安全、事务)效率提升: 避免了大量重复劳动,结构清晰配置复杂: 绘制详细的“组装图纸”(配置 Spring)本身成了一项复杂的工作总结:Spring框架通过IoC/DI和AOP等理念,解决了代码耦合和重复劳动问题,但引入了显著的配置复杂度Spring Boot 1.0.0正式发布于2014年4月1日,标志着该框架的首次稳定版本发布。SpringBoot基于SpringFramework 4进行设计,显著减少了开发者的配置工作量,彻底消除了Spring的配置地狱约定大于配置:约定了默认配置Start机制:是一种依赖管理机制,每个Starter包含特定功能所需的依赖库和自动配置类,开发者只需引入对应Starter即可快速启用功能模块嵌入式容器:内置了Tomcat等嵌入式容器,无需部署war文件到外部容器,直接运行即可启动应用3.Spring Web MVC3.1 概述官方描述:Spring Web MVC是基于Servlet API构建的原始Web框架,并从一开始就在 Spring框架中。正式名称“Spring Web MVC”, 来自其源模块的名称(spring-webmvc),但它通常被称为“Spring MVC”MVC的起源与发展:MVC(Model-View-Controller)模式最初由挪威计算机科学家Trygve Reenskaug于1978年在施乐帕克研究中心(Xerox PARC)提出,目的是为Smalltalk编程语言设计用户界面。其核心思想是将应用程序的逻辑分为三个独立组件:Model:处理数据逻辑和业务规则View:负责数据展示和用户界面Controller:接收用户输入并协调Model与View的交互Spring MVC与MVC的关系:Spring MVC是MVC模式在Spring框架中的具体化,同时扩展了传统MVC的功能以适应现代Web开发需求3.2 必需工具Postman:主要用于 API 的开发和测试。它提供了一个用户友好的界面,支持发送HTTP请求、管理请求历史、自动化测试以及团队协作下载地址:Download PostmanFiddler:是一个网络调试代理工具,主要用于监控和分析HTTP/HTTPS流量。它可以捕获设备与服务器之间的所有请求和响应,支持修改请求、重放请求以及性能分析下载地址:Fiddler - Download3.3 RequestMapping作用:是Spring MVC中最核心、最基础的注解之一,用于将HTTP请求映射到具体的方法上注解级别:类+方法作为类注解:可以为整个类提供一个统一的url前缀(可有可无)作为方法注解:指定该方法负责处理哪个url的请求(强制要求)@RequestMapping("/HelloController")//@RestController声明该类是一个Spring MVC控制器@RestControllerpublic class HelloController { //0.不接收参数 //作为 方法注解 时,@RequestMapping(value = "/hello",method = RequestMethod.GET)等同于@GetMapping(value = "/hello") //设置返回的响应是json格式,produces = "application/json" @RequestMapping(value = "/hello",method = RequestMethod.GET,produces = "application/json") public String hello() { return "{\"Hello\" : World}"; } //1.一个参数 @RequestMapping("/receiveAge1") //不传参或者传递的参数名不匹配时默认为null public String receiveAge1(Integer age) { return "接收到参数 age:" + age; } @RequestMapping("/receiveAge2") //不传参或者传递的参数名不匹配时尝试设置为null,但int无法被设置为null,所以抛出IllegalStateException public String receiveAge2(int age) { return "接收到参数 age:" + age; } //2.接收数组 @RequestMapping("/receiveArray") public String receiveArray(String[] array) { return "接收到参数 array:" + Arrays.toString(array); } //3.接收对象,需要保证传递的参数名称和数量与Java对象保持一致 @RequestMapping("/receivePerson") public String receivePerson(Person person) { return "接收到参数 person:" + person; }}运行本项目java运行3.4 RequestBody作用:将HTTP请求体中的json数据绑定到Java对象(方法注解)注解级别:方法 @RequestMapping("/receivePerson") //@RequestBody接收JSON格式的数据 public String receivePerson(@RequestBody Person person) { return "接收到参数 person:" + person; }运行本项目java运行3.5 RequestParam作用:是Spring MVC框架中从HTTP请求中提取参数/查询字符串的注解,主要用于将请求参数绑定到控制器方法的参数上注解级别:方法 @RequestMapping("/receiveRename") //@RequestParam将url中key=name的查询字符串绑定到控制器的userName参数上 //required = false设置该参数为非必传(默认为true,必传) public String receiveRename(@RequestParam(value = "name",required = false) String userName) { return "接收到参数name:" + userName; }运行本项目java运行注意:需要接收多个同名参数时(如param=value1¶m=value2),直接绑定到List类型需通过该注解明确声明 @RequestMapping("/receiveList1") public String receiveList1(ArrayList<String> list) { //返回的list为空 return "接收到参数 list:" + list; }运行本项目java运行(1)在Spring MVC中,参数绑定机制对集合类型和数组类型的处理存在差异(2)使用ArrayList< String >作为方法参数时,必须显式添加@RequestParam注解,原因如下:默认绑定规则:Spring默认将单个请求参数的值绑定到简单类型(如 String、int)或单个对象。对于集合类型,框架无法自动推断是否需要将多个同名参数合并为集合需要明确指示:@RequestParam注解会告知Spring将同名请求参数的值收集到一个集合中(3)数组(如 String[])无需 @RequestParam 注解即可正确接收,原因如下:内置支持:Spring对数组类型有原生支持,能自动将多个同名请求参数值绑定到数组。这是框架的默认行为,无需额外配置 @RequestMapping("/receiveList2") public String receiveList2(@RequestParam(required = false) ArrayList<String> list) { //正确返回 return "接收到参数 list:" + list; }运行本项目java运行 @RequestMapping("/receiveList3") public String receiveList3(List<String> list) { //报错 return "接收到参数 list:" + list; }运行本项目java运行后端报错:java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List。receiveList3方法使用List< String >接口类型而非具体实现类。Spring虽然支持接口类型参数绑定,但需要满足特定条件:必须配合@RequestParam注解使用不能直接使用未注解的接口类型参数报错根本原因:Spring尝试实例化List接口失败(接口不可实例化)3.6 PathVariable作用:用于从URL路径中提取变量值并绑定到方法的参数上注解级别:方法 @RequestMapping("/receivePath/{article}/{blog}") //required = false设置该参数为非必传(默认为true,必传) public String receivePath(@PathVariable(value = "article",required = false)Integer title,@PathVariable(value = "blog",required = false)String content) { return "接收到参数 article:" + title + " blog:" + content; }运行本项目java运行3.7 RequestPart作用:用于处理 HTTP 请求中的 multipart/form-data 类型数据,通常用于文件上传或同时上传文件和其他表单字段的场景注解级别:方法 @RequestMapping("/receiveFile") public String receiveFile(@RequestPart(value = "file",required = false) MultipartFile imgFile,@RequestParam(value = "userName",required = false) String name) { //返回原始的文件名 return "用户:" + name+ ",接收到文件:" +imgFile.getOriginalFilename(); }运行本项目java运行3.8 Controller&ResponseBody&RestControllerController作用:是Spring MVC中的核心注解,用于标记一个类作为Web请求的处理器(声明一个类是一个Spring MVC控制器),负责处理HTTP请求并返回视图注解级别:类ResponseBody作用:指示方法返回值应直接写入HTTP响应体,而非通过视图解析器渲染注解级别:类+方法@RequestMapping("/ControllerResponse")@Controllerpublic class ControllerResponse { //返回视图 @RequestMapping("/HTMLView") public String HTMLView(){ return "/show.html"; } //返回数据 @ResponseBody @RequestMapping("/HTMLData") public String HTMLData(){ return "/show.html"; }}运行本项目java运行RestController作用:是Spring MVC中的一个组合注解,它结合了@Controller和@ResponseBody的功能,标记的类所有方法返回值默认直接作为 HTTP 响应体(JSON/XML 等格式),无需额外视图渲染注解级别:类4.Gitee————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/148194312
-
【前言】在Java面向对象编程中,抽象类和接口是两个非常重要的概念,它们为代码的抽象化、模块化和可扩展性提供了强大的支持。无论是开发大型企业级应用,还是小型程序,掌握抽象类和接口的使用都至关重要。本文将通过详细的理论讲解、丰富的代码示例、直观的图片以及对比表格,帮助你深入理解Java抽象类和接口的本质与应用。文章目录:一、抽象类1.什么是抽象类2.抽象类语法3.抽象类特征3.1 抽象类不能实例化对象3.2 抽象方法不能是private的3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用二、接口1.什么是接口?2.语法规则3.接口的使用4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象4.2 接口中的方法会被默认为public abstract,其他修饰符会报错4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现4.4 重写接口方法时,不能使用默认的访问权限4.5 接口中可以有变量,但他会被默认为public static final 变量4.6 接口中不能有静态代码块和构造方法5.实现多个接口6.接口间的继承7.接口使用实例8.Clonable接口和深拷贝8.1 Clonable接口8.2 浅拷贝8.3 深拷贝9.抽象类和接口的区别一、抽象类1.什么是抽象类在面对对象的概念中,所以对象都是通过类来描述的,但并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类Dog类和Cat类都属于Animal类,但Dog()和Cat()方法没有实际的行为,可以将他们设计成抽象方法,包含抽象方法的类就是抽象类。2.抽象类语法在Java中,被abstract修饰的类称抽象类,抽象类中,被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。public abstract class Animal { abstract public void eat();//抽象方法:被abstract修饰,没有方法体 public double show(){ //自己增加的方法 return show1; } protected double show1;//参数}运行本项目java运行3.抽象类特征3.1 抽象类不能实例化对象public abstract class Animal { Animal animal = new Animal();}运行本项目java运行3.2 抽象方法不能是private的public abstract class Animal { abstract private void eat();//抽象方法:被abstract修饰,没有方法体}运行本项目java运行3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写abstract final void eat();abstract public static void eat();运行本项目java运行3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰public abstract class Cat extends Animal { @Override public void eat(){ }}运行本项目java运行public abstract class Cat extends Animal { @Override public abstract void eat(); }运行本项目java运行3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用抽象类本身不能被实例化,只能创建子类,并且重写抽象方法,这就起到检验的作用二、接口1.什么是接口?字面意思:我们生活中的水龙头,插座,充电线都是有接口的,才能插上。接口就是公共的行为规范标准,在实现时,只要符合规范标准,就能通用2.语法规则接口的定义和类基本相同,将class换成interface关键字public interface IShape { public static final int SIZE = 100;//接口变量默认public static final public abstract void test();//接口方法默认public abstract}运行本项目java运行【注意】接口命名一般以大写字母I开头;接口不能被实例化;3.接口的使用类与接口之间是implements的关系public class 类名 implenments 接口名{//...}运行本项目java运行这里建立一个IUSB接口和Mouse类public interface IUSB { void openDevice(); void closeDevice();}运行本项目java运行public class Mouse implements IUSB{ @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); }}运行本项目java运行4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象public class TestUSB { public static void main(String[] args) { USB usb = new USB(); }}运行本项目java运行4.2 接口中的方法会被默认为public abstract,其他修饰符会报错public interface USB { private void openDevice();//private使用错误 void closeDevice();}运行本项目java运行4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现public interface USB { //void openDevice(); void closeDevice(){ System.out.println("关闭USB设备"); }}运行本项目java运行4.4 重写接口方法时,不能使用默认的访问权限public class Mouse implements USB{ @Override public void openDevice() { System.out.println("打开鼠标"); }}运行本项目java运行4.5 接口中可以有变量,但他会被默认为public static final 变量public interface USB { int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行4.6 接口中不能有静态代码块和构造方法public interface USB { public USB(){ } { } int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行5.实现多个接口一个类可以实现多个接口,这就是继承所做不到的这里创建一个Dog类,可以让他实现多个接口,如IRunning,ISwimming,IFlying。每个接口的抽象方法都有重写,否则必须设置为抽象类AIT + InS快捷键重写public class Dog extends Animal implements IRunning,ISwimming{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行狗:既能跑,又能游;【注意】有了接口之后,我们就不用注意具体类型,只需要关注这个类是否具备某种类型6.接口间的继承在Java中,类和类之间是单继承的,但一个类可以实现多个接口,接口与接口之间可以多继承这段代码就继承了两个接口:游和跑public class Dog extends Animal implements ISwimming,IRunning{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行7.接口使用实例对象之间大小比较:public class Student { public String name; public int score; public Student (String name,int score){ this.name=name; this.score=score; } @Override public String toString() { return super.toString(); }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",10); System.out.println(s1>s2); }}运行本项目java运行这样进行比较会报错,因为没有指定根据分数还是什么来比较,这样不太灵活,我们可以使用接口,如下:使用Comparable接口public class Student implements Comparable<Student>{ public String name; public int age; public Student (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { if(this.age>o.age) return 1; else if(this.age == o.age) return 0; else return -1; } }运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); //System.out.println(s1>s2); if(s1.compareTo(s2)>0){ System.out.println("s1>s2"); } }}运行本项目java运行使用Comparator接口import java.util.Comparator;public class AgeComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.age-o2.age; }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); AgeComparator ageComparator=new AgeComparator(); int ret = ageComparator.compare(s1,s2); if(ret>0){ System.out.println("s1>s2"); }运行本项目java运行如果是根据名字来比较的,就要看对应字母的大小8.Clonable接口和深拷贝8.1 Clonable接口Java 中内置了⼀些很有⽤的接⼝,Clonable就是其中之⼀.Object 类中存在⼀个clone⽅法,调⽤这个⽅法可以创建⼀个对象的"拷⻉".但是要想合法调⽤clone⽅法,必须要先实现Clonable接⼝,否则就会抛出CloneNotSupportedException异常.public class Person implements Cloneable{ public String name; public int age; public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone();//强制类型转换 System.out.println(person2); }}运行本项目java运行受查异常/编译时异常解决方法:8.2 浅拷贝class Money implements Cloneable{ public double money = 9.9;运行本项目java运行 @Override protected Object clone() throws CloneNotSupportedException { return super.clone();运行本项目java运行lic class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行通过clone,我们只是拷⻉了Person对象。但是Person对象中的Money对象,并没有拷⻉。通过person2这个引⽤修改了m的值后,person1这个引⽤访问m的时候,值也发⽣了改变。这⾥就是发⽣了浅拷⻉。8.3 深拷贝class Money implements Cloneable{ public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class Person implements Cloneable{ public String name; public int age; public Money m = new Money(); public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { //return super.clone(); Person tmp = (Person) super.clone(); tmp.m = (Money) this.m.clone(); return tmp; }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行核心代码:9.抽象类和接口的区别抽象类中可以包含普通⽅法和普通字段,这样的普通⽅法和字段可以被⼦类直接使⽤(不必重写),⽽接⼝中不能包含普通⽅法,⼦类必须重写所有的抽象⽅法.No 区别 抽象类(abstract) 接口(interface)1 结构组成 普通类+抽象方法 抽象方法+全局常量2 权限 各种权限 public3 子类使用 使用extends关键字继承抽象类 使用implements关键字实现接口4 关系 一个抽象类可以实现若干接口 接口不能继承抽象类,可extends继承多个父接口5 子类限制 一个子类只能继承一个抽象类 一个子类可以实现多个接口【总结】在 Java 里,抽象类与接口是实现抽象编程的关键工具。抽象类融合普通类与抽象方法,可定义部分实现逻辑,权限灵活;接口由抽象方法和全局常量构成,成员权限默认 public 。子类继承抽象类用 extends ,实现接口用 implements 。关系上,抽象类能实现多个接口,接口可多继承。继承限制方面,子类单继承抽象类,却可多实现接口 。二者各有适用场景,抽象类适合提炼共性、留存部分实现;接口利于规范行为、实现多态解耦。合理运用它们,能让代码架构更清晰、可扩展与可维护,助力构建灵活且健壮的 Java 程序 。————————————————原文链接:https://blog.csdn.net/user340/article/details/148381303
-
引言 在Java编程中,尤其是在使用匿名内部类时,许多开发者都会遇到这样一个限制:从匿名内部类中访问的外部变量必须声明为final或是"等效final"。这个看似简单的语法规则背后,其实蕴含着Java语言设计的深层考量。本文将深入探讨这一限制的原因、实现机制以及在实际开发中的应用。 一、什么是匿名内部类? 在深入讨论之前,我们先简单回顾一下匿名内部类的概念。匿名内部类是没有显式名称的内部类,通常用于创建只使用一次的类实例。 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Button clicked!"); }});一键获取完整项目代码java 二、final限制的历史与现状1、Java 8之前的严格final要求在Java 8之前,语言规范强制要求:任何被匿名内部类访问的外部方法参数或局部变量都必须明确声明为final// Java 7及之前版本public void process(String message) { final String finalMessage = message; // 必须声明为final new Thread(new Runnable() { @Override public void run() { System.out.println(finalMessage); // 访问外部变量 } }).start();}一键获取完整项目代码java 2、Java 8的等效final(effectively final)Java 8引入了一个重要改进:等效final的概念如果一个变量在初始化后没有被重新赋值,即使没有明确声明为final,编译器也会将其视为final,这就是"等效final"// Java 8及之后版本public void process(String message) { // message是等效final的,因为它没有被重新赋值 new Thread(new Runnable() { @Override public void run() { System.out.println(message); // 可以直接访问 } }).start(); // 如果取消下面的注释,会导致编译错误 // message = "modified"; // 这会使message不再是等效final的}一键获取完整项目代码java 三、为什么不能修改外部局部变量?1、变量生命周期不一致核心问题:方法参数和局部变量的生命周期与匿名内部类实例的生命周期不一致局部变量存在于栈帧上,其生命周期随着方法的结束而结束但是匿名内部类或 Lambda 表达式可能在方法返回后仍然存在(比如被传递给其他线程、存储在成员变量中等),如果它们直接使用方法的局部变量,而该变量已经被销毁,就会出问题解决方案:为了保证Lambda/内部类能访问到局部变量,Java并没有直接引用该变量,而是捕获了它的值的一个副本(拷贝)public void example() { int value = 10; // 局部变量,存在于栈帧中 Runnable r = new Runnable() { @Override public void run() { // 这里拿到的是value的副本,不是原始变量(引用地址不一样) System.out.println(value); } }; new Thread(r).start(); // 方法结束后,value的栈帧被销毁,value不复存在}一键获取完整项目代码java 2、数据一致性保证如果允许你修改一个外部局部变量,而Lambda使用的是值的拷贝,那么你修改了变量,但 Lambda 内部看不到这个修改(因为用的是拷贝)或者你误以为你修改了 Lambda 使用的那个值,但实际上你修改的是另一个东西允许修改会导致一种错觉:好像Lambda和外部共享了状态,其实不是// 假设Java允许这样做(实际上不允许)public void problematicExample() { int counter = 0; Runnable r = new Runnable() { @Override public void run() { // 假设允许访问,但 value 是拷贝的 0 System.out.println(counter); } }; counter = 5; // 修改原始变量 r.run(); // 输出0,你以为你改成了5}一键获取完整项目代码java 而且匿名内部类可能在另一个线程中执行,而原始变量可能在原始线程中被修改。final限制避免了线程安全问题3、解决方案如果确实需要“共享可变状态”,可以使用一个单元素数组、或者一个Atomicxxx类(如 AtomicInteger),或者将变量封装到一个对象中public class LambdaWorkaround { public static void main(String[] args) { int[] counter = {0}; // 使用数组来包装 Runnable r = () -> { counter[0]++; // 合法:修改的是数组内容,不是外部变量本身 System.out.println("Count: " + counter[0]); }; r.run(); // Count: 1 r.run(); // Count: 2 }}一键获取完整项目代码java 注意:这里你修改的是数组的内容,而不是变量 holder的引用,所以不违反规则 四、底层实现机制Java编译器通过以下方式实现这一特性: 值拷贝:编译器将final变量的值拷贝到匿名内部类中合成字段:在匿名内部类中创建一个合成字段来存储捕获的值构造函数传递:通过构造函数将捕获的值传递给匿名内部类实例可以通过反编译匿名内部类来观察这一机制: // 源代码public class Outer { public void method(int param) { Runnable r = new Runnable() { @Override public void run() { System.out.println(param); } }; }}一键获取完整项目代码java 反编译后的内部类和内部类大致如下:(参数自动添加final,内部类通过构造方法引入变量) // 反编译原始类 public class Outer { public void method(final int var1) { Runnable var10000 = new Runnable() { public void run() { System.out.println(var1); } }; }} // 反编译后能看到单独生成的匿名内部类class Outer$1 implements Runnable { Outer$1(Outer var1, int var2) { this.this$0 = var1; this.val$param = var2; } public void run() { System.out.println(this.val$param); }}一键获取完整项目代码java 五、常见问题与误区1、为什么实例变量没有这个限制?因为实例变量(成员变量)存储在堆(Heap)中,和对象生命周期一致而局部变量存储在栈(Stack)中,方法结束后就被销毁了Java 为保证 Lambda / 匿名内部类能安全访问变量,对这两者的处理方式完全不同public class Outer { private int instanceVar = 10; // 实例变量 public void method() { new Thread(new Runnable() { @Override public void run() { instanceVar++; // 可以直接修改实例变量 } }).start(); }}一键获取完整项目代码java 2、等效final的实际含义等效final意味着变量虽然没有明确声明为final,但符合final的条件:只赋值一次且不再修改public void effectivelyFinalExample() { int normalVar = 10; // 等效final final int explicitFinal = 20; // 明确声明为final // 两者都可以在匿名内部类中使用 Runnable r = () -> { System.out.println(normalVar + explicitFinal); }; // 如果这里修改变量,同样会编译报错 // normalVar = 5;}———————————————— 原文链接:https://blog.csdn.net/qq_35512802/article/details/151362542
-
前言 自古以来,中秋佳节便与圆月紧密相连,成为人们寄托思念与团圆之情的象征。在民间流传着这样一种说法:“十五的月亮十六圆”,仿佛这已成为一种铁律,深入人心。然而,这种说法是否真的站得住脚呢?在这背后,隐藏着怎样的天文奥秘?又是否可以通过科学的方法来验证这一传统观念呢?在科技飞速发展的今天,我们不妨借助编程的力量,运用Java语言来实证求解,揭开中秋满月的真相。 中秋赏月的传统由来已久,早在《周礼》中就有“中秋夜迎寒”的记载,而到了唐代,中秋赏月、玩月的风俗开始盛行。文人墨客们更是留下了许多描写中秋月夜的佳作,如苏轼的“但愿人长久,千里共婵娟”,将中秋的月与人间的思念紧密相连,赋予了中秋月深厚的文化内涵。在这样的文化背景下,“十五的月亮十六圆”这一说法也逐渐流传开来,成为人们茶余饭后的话题之一。然而,这种说法真的准确无误吗? 本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。在接下来的章节中,我们将详细介绍如何使用Java语言进行天文数据的处理和计算,以及如何通过模拟实验来验证“十五的月亮十六圆”这一说法。我们将逐步展开这一探索之旅,最终揭示中秋满月的真相。让我们一起踏上这段充满趣味和挑战的旅程,用科学的视角重新审视中秋的圆月,探索其中隐藏的奥秘。一、天文上的满月 在天文学中,月亮的圆缺变化是一个非常有趣且复杂的自然现象,这种变化主要源于月球绕地球的公转运动。月球绕地球运行一周的时间大约是29.5天,这个周期被称为一个“朔望月”。在这个周期中,月球相对于太阳的位置不断变化,从而导致我们从地球上看到的月相也随之改变。博主不是专业天文专业,这里仅分享一些简单的满月基础知识,让大家有一个概念。1、形成原理及定义 说到满月就必须提及月相,月相的形成是由于太阳光照射月球的不同部分,而我们从地球上看到的只是月球被太阳照亮的那一部分。随着月球绕地球的公转,被太阳照亮的部分逐渐增加,依次出现“娥眉月”“上弦月”“凸月”“满月”“下弦月”“残月”等不同的月相。其中满月是指月球完全被太阳照亮的那一面朝向地球,此时月球与太阳在地球的两侧,三者几乎在一条直线上。理论上,满月应该出现在农历的十五或十六,但实际的情况并非总是如此。由于月球的公转轨道是椭圆形的,且受到多种因素的影响,如地球的引力、太阳的引力等,月球的实际运行轨迹并非完全规律,因此满月出现的时间也会有所变化。2、出现时间及观测 “十五的月亮十六圆”这一说法广为流传,但实际上满月并不总是出现在农历的十六。根据天文观测数据,满月可能出现在农历的十四到十七之间的任何一天。例如,在某些年份,满月可能出现在农历十四的晚上,而在另一些年份,满月可能出现在农历十七的早晨。这种变化是由于月球的公转速度和轨道形状的不规则性所导致的。满月是观测月球的最佳时机之一,因为此时月球的整个盘面都被照亮,可以清晰地看到月球表面的山脉、陨石坑和月海等特征。在满月期间,月球的亮度会达到最大,这使得它在夜空中格外明亮。3、文化意义 在许多文化中,满月都具有重要的象征意义。在中国文化中,满月象征着团圆和完满,因此中秋节成为了家人团聚的重要节日。在西方文化中,满月也常常与神秘和浪漫联系在一起,许多文学作品和民间传说都以满月为背景。二、Java模拟月满计算 随着计算机技术的发展,我们有了更强大的工具来探索和验证这些天文现象。Java作为一种广泛使用的编程语言,具有强大的功能和灵活性,可以用来编写各种复杂的算法和程序。在本研究中,我们将利用Java语言编写程序,通过计算月球在不同时间的位置,来确定中秋满月的具体时间。我们将收集多年来的天文数据,包括月球的公转周期、轨道参数等,然后利用这些数据进行模拟计算。通过这种方式,我们可以得到一个较为准确的中秋满月时间表,从而验证“十五的月亮十六圆”这一说法的准确性。1、整体实现逻辑 使用Java求解中秋满月整体时间逻辑如下:public class MidAutumnFullMoonCalculator { // 主计算方法 public static Date calculateFullMoonTime(int year, int month, int day) { ... } // 核心天文算法 private static double calculateFullMoonJulianDay(double jd) { ... } // 辅助方法 private static double normalizeAngle(double angle) { ... } private static double calendarToJulianDay(Calendar cal) { ... } private static Calendar julianDayToCalendar(double jd) { ... }}一键获取完整项目代码java2、主计算方法详解 功能:这是程序的入口点,接收农历中秋的公历日期,返回精确的满月时刻,核心方法如下:/** * -计算指定农历中秋日期的月亮最圆时刻 * @param year 年份 * @param month 农历月份(八月) * @param day 农历日期(十五) * @return 月亮最圆时刻的Date对象 */public static Date calculateFullMoonTime(int year, int month, int day) { // 创建农历中秋日期(使用中午12点作为基准时间) Calendar midAutumnDate = Calendar.getInstance(); midAutumnDate.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); midAutumnDate.set(year, month - 1, day, 12, 0, 0); // month-1因为Calendar月份从0开始 // 计算精确的满月时刻 return calculatePreciseFullMoonTime(midAutumnDate);}一键获取完整项目代码java 参数说明:year:公历年份(如2024)month:公历月份(如9)day:公历日期(如17) 处理流程:创建Calendar对象,设置为北京时间将时间设为中午12点作为计算基准调用核心算法计算精确的满月时刻3、核心天文算法详解 核心算法是相关计算中最核心的内容,主要包括儒略日的计算、时间参数的计算、天文参数计算和周期项修正等内容,这里的天文计算采用近似计算,如需精度计算,请使用更精准的天文算法。3.1 儒略日计算基础/** * -计算满月时刻的儒略日 * -基于Jean Meeus的天文算法 */private static double calculateFullMoonJulianDay(double jd) { // 计算从2000年1月6日(基准新月)开始的月相周期数 double k = Math.floor((jd - 2451550.09765) / 29.530588853); // 满月对应的k值(新月+0.5) k = k + 0.5; // 计算T(儒略世纪数) double T = k / 1236.85; //----其它计算}一键获取完整项目代码java 儒略日(Julian Day):天文学中常用的连续时间计数法,从公元前4713年1月1日格林尼治平午开始计算。月相周期数k:2451550.09765:2000年1月6日18:14的儒略日,作为一个基准新月时刻29.530588853:一个朔望月的平均长度(天)k:从基准时间开始经过的月相周期数k + 0.5:从新月到满月是半个周期3.2 时间参数计算// 计算T(儒略世纪数)double T = k / 1236.85; // 计算基础儒略日double JDE = 2451550.09765 + 29.530588853 * k + 0.0001337 * T * T - 0.000000150 * T * T * T + 0.00000000073 * T * T * T * T;一键获取完整项目代码java T(儒略世纪数):以36525天为一世纪的时间单位,用于高阶项的计算。 (儒略历书日):考虑了长期项修正的基础满月时刻。3.3 天文参数计算 // 计算太阳平近点角 double M = normalizeAngle(2.5534 + 29.10535669 * k - 0.0000218 * T * T - 0.00000011 * T * T * T); // 计算月亮平近点角double Mprime = normalizeAngle(201.5643 + 385.81693528 * k + 0.1017438 * T * T + 0.00001239 * T * T * T - 0.000000058 * T * T * T * T); // 计算月亮升交点平黄经double F = normalizeAngle(160.7108 + 390.67050274 * k - 0.0016341 * T * T - 0.00000227 * T * T * T + 0.000000011 * T * T * T * T); // 计算Omega(月亮轨道升交点经度)double Omega = normalizeAngle(124.7746 - 1.56375580 * k + 0.0020691 * T * T + 0.00000215 * T * T * T);一键获取完整项目代码java 天文参数说明:M(太阳平近点角):太阳在轨道上的平均位置角度系数:2.5534° + 29.10535669°/周期反映地球公转轨道的椭圆性影响M'(月亮平近点角):月亮在轨道上的平均位置角度系数:201.5643° + 385.81693528°/周期反映月球公转轨道的椭圆性影响F(月亮升交点平黄经):月球轨道与黄道交点的平均位置系数:160.7108° + 390.67050274°/周期反映月球轨道平面的进动Ω(月亮轨道升交点经度):更精确的轨道交点位置系数:124.7746° - 1.56375580°/周期3.4 周期项修正计算// 转换为弧度double M_rad = Math.toRadians(M);double Mprime_rad = Math.toRadians(Mprime);double F_rad = Math.toRadians(F);double Omega_rad = Math.toRadians(Omega);// 计算周期项修正double correction = 0;// 主要修正项correction += -0.40720 * Math.sin(Mprime_rad);correction += 0.17241 * 0.016708617 * Math.sin(M_rad);correction += 0.01608 * Math.sin(2 * Mprime_rad);correction += 0.01039 * Math.sin(2 * F_rad);correction += 0.00739 * 0.016708617 * Math.sin(Mprime_rad - M_rad);correction += -0.00514 * 0.016708617 * Math.sin(Mprime_rad + M_rad);correction += 0.00208 * 0.016708617 * 0.016708617 * Math.sin(2 * M_rad);correction += -0.00111 * Math.sin(Mprime_rad - 2 * F_rad);correction += -0.00057 * Math.sin(Mprime_rad + 2 * F_rad);correction += 0.00056 * 0.016708617 * Math.sin(2 * Mprime_rad + M_rad);correction += -0.00042 * Math.sin(3 * Mprime_rad);correction += 0.00042 * 0.016708617 * Math.sin(M_rad + 2 * F_rad);correction += 0.00038 * 0.016708617 * Math.sin(M_rad - 2 * F_rad);correction += -0.00024 * 0.016708617 * Math.sin(2 * Mprime_rad - M_rad);correction += -0.00017 * Math.sin(Omega_rad);correction += -0.00007 * Math.sin(Mprime_rad + 2 * M_rad);correction += 0.00004 * Math.sin(2 * Mprime_rad - 2 * F_rad);correction += 0.00004 * Math.sin(3 * M_rad);correction += 0.00003 * Math.sin(Mprime_rad + M_rad - 2 * F_rad);correction += 0.00003 * Math.sin(2 * Mprime_rad + 2 * F_rad);correction += -0.00003 * Math.sin(Mprime_rad + M_rad + 2 * F_rad);correction += 0.00003 * Math.sin(Mprime_rad - M_rad + 2 * F_rad);correction += -0.00002 * Math.sin(Mprime_rad - M_rad - 2 * F_rad);correction += -0.00002 * Math.sin(3 * Mprime_rad + M_rad);correction += 0.00002 * Math.sin(4 * Mprime_rad); // 应用修正double preciseJDE = JDE + correction;一键获取完整项目代码java修正项原理:每个修正项都对应一个特定的天文效应:-0.40720 × sin(M'):月球椭圆轨道的主要修正(中心差)0.17241 × e × sin(M):地球轨道偏心率对月相的影响0.01608 × sin(2M'):月球轨道的二阶椭圆项0.01039 × sin(2F):月球轨道倾角的影响0.00739 × e × sin(M' - M):地球和月球轨道相互影响-0.00514 × e × sin(M' + M):地球和月球轨道的组合效应e = 0.016708617:地球轨道偏心率这些修正项基于布朗月球运动理论,考虑了月球轨道的各种摄动因素。4、辅助方法详解 本小节将对辅助方法进行简单介绍。4.1 角度标准化/** * -将角度标准化到0-360度范围内 */private static double normalizeAngle(double angle) { angle = angle % 360; if (angle < 0) { angle += 360; } return angle;}一键获取完整项目代码java 功能:将角度限制在0-360度范围内,避免数值溢出。4.2 日历与儒略日转换/** * -将Calendar转换为儒略日 */private static double calendarToJulianDay(Calendar cal) { int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); double decimalHour = hour + minute / 60.0 + second / 3600.0; if (month <= 2) { year--; month += 12; } int a = year / 100; int b = 2 - a + a / 4; return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + decimalHour / 24.0 + b - 1524.5;}一键获取完整项目代码java 转换公式:标准的天文儒略日计算公式,考虑了:闰年规则格里高利历改革(1582年)时间的小数部分处理4.3 儒略日转日历/** * -将儒略日转换为Calendar */private static Calendar julianDayToCalendar(double jd) { jd += 0.5; double z = Math.floor(jd); double f = jd - z; double a; if (z < 2299161) { a = z; } else { double alpha = Math.floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.floor(alpha / 4); } double b = a + 1524; double c = Math.floor((b - 122.1) / 365.25); double d = Math.floor(365.25 * c); double e = Math.floor((b - d) / 30.6001); double day = b - d - Math.floor(30.6001 * e) + f; int month = (int) (e < 14 ? e - 1 : e - 13); int year = (int) (month > 2 ? c - 4716 : c - 4715); double time = day - Math.floor(day); int hour = (int) (time * 24); int minute = (int) ((time * 24 - hour) * 60); int second = (int) Math.round((((time * 24 - hour) * 60 - minute) * 60)); // 处理秒数进位 if (second >= 60) { second = 0; minute++; } if (minute >= 60) { minute = 0; hour++; } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); cal.set(year, month - 1, (int) Math.floor(day), hour, minute, second); cal.set(Calendar.MILLISECOND, 0); return cal;}一键获取完整项目代码java 关键点:jd += 0.5:儒略日从中午开始,调整为从午夜开始处理格里高利历改革(1582年10月4日后跳过10天)精确的时间分量计算三、近年中秋满月计算及对比 本节将结合实例对每年的中秋月满时间进行计算,通过本小节就可以获取每年的满月日期和具体的时间,并且与官方提供的时间进行对比,大家通过对比就可以知晓问题的开始,是不是所有的月亮都是十六圆了。 1、近年中秋满月计算/** * -测试方法 - 计算未来几年的中秋节月亮最圆时刻 */public static void main(String[] args) { // 已知的农历中秋日期(公历日期) int[][] midAutumnDates = { {2019, 9, 13}, // 2019年中秋节 {2020, 10, 1}, // 2020年中秋节 {2021, 9, 21}, // 2021年中秋节 {2022, 9, 10}, // 2022年中秋节 {2023, 9, 29}, // 2023年中秋节 {2024, 9, 17}, // 2024年中秋节 {2025, 10, 6}, // 2025年中秋节 {2026, 9, 25}, // 2026年中秋节 }; SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); System.out.println("中秋节月亮最圆时刻计算结果:"); System.out.println("================================="); for (int[] date : midAutumnDates) { int year = date[0]; int month = date[1]; int day = date[2]; Date fullMoonTime = calculateFullMoonTime(year, month, day); System.out.printf("%d年中秋节(公历%d月%d日)月亮最圆时刻: %s%n", year, month, day, sdf.format(fullMoonTime)); }}一键获取完整项目代码java 接下来我们在IDE中运行意以上成就可以得到以下结果: 以上就是实现一个从2019年到2026年,跨度为7年的中秋满月计算过程。2、近年计算与公布时间对比 通过以上7年的计算,再结合官方公布的满月日期及时刻,来对比一下我们的计算方法与官方公布的时间相差是多少?年份 中秋(公历) 满月时间(本地) 是否当天 满月时间(官方公布) 误差2019 2019-9-13 09月14日 08时39分21秒 否(十六) 9月14日12时33分 3时54分2020 2020-10-1 10月02日 01时28分17秒 否(十六) 10月2日 05时5分 3时37分2021 2021-9-21 09月21日 03时58分38秒 是(十五) 9月21日 07时54分 3时56分2022 2022-9-10 09月10日 13时34分12秒 是(十五) 9月10日 17时59分 4时25分2023 2023-9-29 09月29日 13时49分05秒 是(十五) 9月29日 17时58分 4时8分2024 2024-9-17 09月18日 06时15分39秒 否(十六) 9月18日 10时34分 4时19分2025 2024-10-06 10月07日 07时41分10秒 否(十六) 10月7日 11时48分 4时7分 结合近七年的满月日期及时刻来看,并不是所有的中秋月圆都是十六圆,有的是当天就圆了。所以,从这个角度来定义,十五的月亮十六圆可不是准确的哦。通过这种本地近似的计算,虽然在具体的时刻上有一些误差,但是日期是与官方公布的是完全一致的,时刻的误差通过近7年的验证,相差时间在4个小时左右,所以未来可以结合更长序列的时间进行相应的修正。四、总结 以上就是本文的主要内容,本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。通过Java满月近似求解,并结合2019年到2025年的中秋满月日期时刻的计算,得出了重要的一个结论,十五的月亮不一定十六圆,通过严谨的程序计算得到的数据支撑。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/152717752
-
一、AQS 是什么?AQS,全称 AbstractQueuedSynchronizer,即抽象队列同步器。抽象:它是一个抽象类,本身不能直接实例化,需要子类去继承它,并实现其保护方法来管理同步状态。队列:它内部维护了一个先进先出(FIFO)的等待队列,用于存放那些没有抢到锁的线程。同步器:它是构建锁和其他同步组件(如 Semaphore、CountDownLatch 等)的基础框架。核心思想:AQS 使用一个整型的 volatile 变量(state) 来表示同步状态(例如,锁被重入的次数、许可的数量等),并通过一个内置的 FIFO 队列来完成资源获取线程的排队工作。设计模式:AQS 是 模板方法模式 的经典应用。父类(AQS)定义了骨架和核心算法,而将一些关键的操作以 protected 方法的形式留给子类去实现。这样,实现一个自定义同步器只需要关注如何管理 state 状态即可,至于线程的排队、等待、唤醒等复杂操作,AQS 已经帮我们完成了。二、AQS 的核心结构AQS 的核心可以概括为三部分:同步状态(state)、等待队列 和 条件队列。1. 同步状态(State)这是一个 volatile int 类型的变量,是 AQS 的灵魂。private volatile int state;一键获取完整项目代码它的具体含义由子类决定,非常灵活:在 ReentrantLock 中,state 表示锁被同一个线程重复获取的次数。state=0 表示锁空闲,state=1 表示锁被占用,state>1 表示锁被重入。在 Semaphore 中,state 表示当前可用的许可数量。在 CountDownLatch 中,state 表示计数器当前的值。对 state 的操作是原子的,通过 getState(), setState(int newState), compareAndSetState(int expect, int update) 等方法进行。2. 等待队列(CLH 队列的变体)这是一个双向链表,是 AQS 实现阻塞锁的关键。当线程请求共享资源失败时,AQS 会将当前线程以及等待状态等信息构造成一个节点(Node) 并将其加入队列的尾部,同时阻塞该线程。头节点(Head):指向获取到资源的线程所在的节点。头节点不持有线程,是一个“虚节点”。尾节点(Tail):指向队列中最后一个节点。当一个线程释放资源时,它会唤醒后继节点,后继节点成功获取资源后,会将自己设置为新的头节点。主要原理图如下:AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。3. 条件队列(Condition Object)AQS 内部类 ConditionObject 实现了 Condition 接口,用于支持 await/signal 模式的线程间协作。每个 ConditionObject 对象都维护了一个自己的单向链表(条件队列)。当线程调用 Condition.await() 时,会释放锁,并将当前线程构造成节点加入条件队列,然后阻塞。当线程调用 Condition.signal() 时,会将条件队列中的第一个等待节点转移到 AQS 的等待队列中,等待重新获取锁。注意:一个 AQS 实例可以对应多个 Condition 对象(即多个条件队列),但只有一个等待队列。三、AQS 的设计与关键方法AQS 将资源获取的方式分为两种:独占模式(Exclusive):一次只有一个线程能执行,如 ReentrantLock。共享模式(Shared):多个线程可以同时执行,如 Semaphore、CountDownLatch。AQS 提供了顶层的入队和出队逻辑,而将尝试获取资源和尝试释放资源的具体策略留给了子类。需要子类重写的关键方法(Protected)这些方法在 AQS 中是 protected 的,默认抛出 UnsupportedOperationException。独占模式:boolean tryAcquire(int arg):尝试以独占方式获取资源。成功返回 true,失败返回 false。boolean tryRelease(int arg):尝试以独占方式释放资源。成功返回 true,失败返回 false。共享模式:int tryAcquireShared(int arg):尝试以共享方式获取资源。负数表示失败;0 表示成功,但后续共享获取可能失败;正数表示成功,且后续共享获取可能成功。boolean tryReleaseShared(int arg):尝试以共享方式释放资源。其他:boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用。在 Condition 相关操作中会用到。供外部调用的重要方法(Public)这些是模板方法,子类一般不重写,使用者(或子类)直接调用。独占模式:void acquire(int arg):以独占模式获取资源,忽略中断。如果获取失败,会进入等待队列。void acquireInterruptibly(int arg):同上,但响应中断。boolean tryAcquireNanos(int arg, long nanosTimeout):在 acquireInterruptibly 基础上增加了超时限制。boolean release(int arg):以独占模式释放资源。共享模式:void acquireShared(int arg):以共享模式获取资源。void acquireSharedInterruptibly(int arg):响应中断的共享获取。boolean tryAcquireSharedNanos(int arg, long nanosTimeout):带超时的共享获取。boolean releaseShared(int arg):以共享模式释放资源。四、源码级工作流程解析(以 acquire 为例)我们来看一下最核心的 acquire 方法,它展示了 AQS 的完整工作流程:public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 尝试直接获取资源(子类实现) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 获取失败,则加入队列;3. 在队列中自旋/阻塞等待 selfInterrupt(); // 如果在等待过程中被中断,补上中断标记}一键获取完整项目代码tryAcquire(arg):这是子类实现的方法。比如在 ReentrantLock 的非公平锁实现中,它会直接尝试使用 CAS 修改 state,如果成功,就将当前线程设置为独占线程。如果 tryAcquire 成功,整个 acquire 方法就结束了,线程继续执行。如果失败,进入下一步。addWaiter(Node.EXCLUSIVE):创建一个代表当前线程的 Node 节点,模式为独占模式(Node.EXCLUSIVE)。通过 CAS 操作,快速地将这个新节点设置为尾节点。如果失败,则进入 enq(node) 方法,通过自旋 CAS 的方式确保节点被成功添加到队列尾部。acquireQueued(final Node node, int arg):这是核心中的核心。节点入队后,会在这个方法里进行自旋(循环)等待。在循环中,它会检查自己的前驱节点是不是头节点(p == head)。如果是,说明自己是队列中第一个等待的线程,会再次调用 tryAcquire 尝试获取资源(因为此时锁可能刚好被释放了,这是一个避免不必要的线程挂起、提高性能的优化)。如果获取成功,就将自己设为新的头节点,然后返回。如果前驱不是头节点,或者再次尝试获取失败,则会调用 shouldParkAfterFailedAcquire 方法,检查并更新前驱节点的状态(比如将其 waitStatus 设置为 SIGNAL,表示“当你释放锁时,需要唤醒我”)。如果一切就绪,就调用 parkAndCheckInterrupt() 方法,使用 LockSupport.park(this) 阻塞(挂起)当前线程。当线程被唤醒后(通常是由前驱节点释放锁时 unpark 的),会再次检查自己是否是头节点的后继,并重复上述自旋过程,直到成功获取资源。selfInterrupt():如果在等待过程中线程被中断,acquireQueued 方法会返回 true,这里会调用 selfInterrupt 补上中断标志,因为 AQS 在 acquire 过程中是忽略中断的。释放流程(release)相对简单:public final boolean release(int arg) { if (tryRelease(arg)) { // 1. 子类尝试释放资源 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 2. 唤醒后继节点 return true; } return false;}一键获取完整项目代码unparkSuccessor 会找到队列中第一个需要唤醒的线程(通常是头节点的下一个有效节点),然后调用 LockSupport.unpark(s.thread) 将其唤醒。五、AQS 的应用举例AQS 是 JUC 包的基石,几乎所有的同步工具都基于它:ReentrantLock:使用 AQS 的独占模式,state 表示重入次数。ReentrantReadWriteLock:读写锁。AQS 的 state 高16位表示读锁状态,低16位表示写锁状态。Semaphore:使用 AQS 的共享模式,state 表示可用许可数。CountDownLatch:使用 AQS 的共享模式,state 表示计数器值。countDown() 是 releaseShared,await() 是 acquireShared。ThreadPoolExecutor:其内部的工作线程 Worker 类,也继承了 AQS,用于实现独占锁,来判断线程是否空闲。六、总结AQS 的核心贡献在于,它提供了一个强大的框架,将复杂的线程排队、阻塞、唤醒等底层操作封装起来,让同步器的开发者只需要关注一个核心问题:如何管理那个 state 变量。它的优点:极大地降低了构建锁和同步器的复杂度。性能高效:通过自旋、CAS 等无锁编程技术,减少了线程上下文切换的开销。灵活强大:通过两种模式的区分,可以构建出各种复杂的同步工具。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153201201
-
Spring AI 1.0 GA 深度解析:Java生态的AI革命已来作者按:在经历了8个里程碑版本的迭代后,Spring AI 1.0 GA于2025年5月20日正式发布。作为Spring生态的官方AI框架,它标志着Java开发者正式迈入AI原生应用时代。本文基于生产环境实践,深度剖析其核心架构与落地策略。一、为什么Spring AI是Java开发者的AI"入场券"?1.1 从Spring Boot到AI Boot:技术演进的必然想象一下这个场景:你的团队需要在现有Spring微服务中集成AI能力,但面对OpenAI、通义千问、Claude等不同API时,每个都需要单独适配。更痛苦的是,当业务需要从聊天机器人升级到RAG知识库,再到多Agent协作时,代码重构的噩梦就开始了。这正是Spring AI要解决的问题。它就像当年Spring整合JDBC、Hibernate一样,现在统一了AI领域的"混沌"。基于我们的生产实践,使用Spring AI后:代码量减少60%,模型切换成本从2人周降至2小时。 1.2 企业级AI的三座大山传统AI集成面临的核心痛点: 模型碎片化:OpenAI、Claude、通义千问各自为政 技术栈割裂:Python AI与Java业务系统难以融合 生产就绪度低:缺乏监控、限流、熔断等企业级能力 运行本项目Spring AI通过三层抽象完美解决:ChatClient:统一所有大模型调用VectorStore:屏蔽向量数据库差异Advisor:AOP式增强AI能力二、核心架构:比LangChain更懂Java的设计哲学2.1 ChatClient:AI世界的JDBCTemplate@RestControllerpublicclassSmartController{privatefinalChatClient chatClient;publicSmartController(ChatClient.Builder builder){this.chatClient = builder .defaultSystem("你是一个专业的Java架构师").build();}@GetMapping("/ai/code-review")publicCodeReviewreviewCode(@RequestParamString code){return chatClient.prompt().user("请分析这段代码的设计模式:{code}", code).call().entity(CodeReview.class);// 直接返回结构化对象}}运行本项目性能数据:在我们的压测中,ChatClient相比原生HTTP调用:平均延迟降低35%,内存使用减少40%2.2 向量数据库的"USB-C"接口Spring AI支持20种向量数据库的统一抽象,性能对比实测:数据库 百万级向量QPS 延迟P99 最佳场景Milvus 1200 15ms 大规模图像检索Weaviate 800 25ms 知识图谱场景Chroma 200 80ms 原型开发PGVector 500 40ms 已有PostgreSQL配置示例:spring:ai:vectorstore:milvus:host: localhost port:19530index-type: HNSW metric-type: COSINE 运行本项目2.3 Advisor:AI领域的Spring AOP通过拦截器链实现模型增强,核心Advisor对比:Advisor类型 作用 性能开销QuestionAnswerAdvisor RAG检索增强 +15msChatMemoryAdvisor 会话记忆 +5msSafeGuardAdvisor 敏感词过滤 +2ms三、生产级RAG架构实战3.1 亿级文档的RAG流水线基于某金融客户的真实案例,架构演进过程:阶段1:简单RAG(100万文档)@BeanpublicRetrievalAugmentationAdvisorragAdvisor(){returnRetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(milvusVectorStore).similarityThreshold(0.75).topK(5).build()).queryAugmenter(ContextualQueryAugmenter.builder().maxTokens(500).build()).build();}运行本项目阶段2:分布式RAG(1000万文档)引入MultiQueryExpansion:提升召回率30%DocumentReRanker:使用Cross-Encoder重排序混合检索:向量+BM25混合打分阶段3:实时RAG(亿级文档)增量索引:Kafka实时同步文档变更缓存策略:Redis缓存热点查询负载均衡:多向量库分片存储3.2 性能调优秘籍向量维度优化:实测发现:1536维vs768维在准确率上仅差2%,但存储减少50%建议:业务场景优先768维,精度敏感再用1536维分块策略:// 智能分块:按语义完整性切割DocumentSplitter splitter =newTokenTextSplitter(800,// 每块最大token200,// 重叠token5,// 最小块数10000// 最大token);运行本项目四、Function Calling:让AI"动手"的魔法4.1 从天气预报到股票交易工具定义:@ServicepublicclassStockService{@Tool(description ="获取股票实时价格")publicStockPricegetPrice(String symbol){return webClient.get().uri("/stock/{symbol}", symbol).retrieve().bodyToMono(StockPrice.class).block();}@Tool(description ="执行股票交易")publicTradeResultexecuteTrade(@ToolParam(description ="股票代码")String symbol,@ToolParam(description ="交易数量")int quantity,@ToolParam(description ="交易类型")TradeType type){// 实际交易逻辑}}运行本项目实测数据:工具调用成功率:99.2%(基于10000次调用)平均响应时间:180ms(含API往返)错误恢复:自动重试3次,指数退避4.2 复杂业务流程编排Agent工作流模式:模式类型 适用场景 代码复杂度Chain 顺序任务流 Parallel 批量处理 Routing 智能分流 Orchestrator 动态任务分解 实战案例:订单处理AgentpublicclassOrderAgent{publicvoidprocessOrder(String orderRequest){OrchestratorWorkersWorkflow workflow =newOrchestratorWorkersWorkflow(chatClient);// 1. 分析订单 -> 2. 检查库存 -> 3. 计算价格 -> 4. 生成发货单OrderResult result = workflow.process(orderRequest);}}运行本项目五、MCP协议:AI生态的TCP/IP5.1 什么是MCP?模型上下文协议(Model Context Protocol)就像AI世界的HTTP协议,它让任何AI应用都能:发现可用工具标准化调用方式安全权限控制Spring AI MCP架构:┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ AI App │────│ MCP Client │────│ MCP Server │ │ (ChatGPT) │ │ (Spring AI) │ │ (Weather) │ └─────────────┘ └──────────────┘ └─────────────┘ 运行本项目5.2 企业级MCP实践安全控制:@ConfigurationpublicclassMcpSecurityConfig{@BeanpublicSecurityFilterChainmcpSecurity(HttpSecurity http)throwsException{ http .requestMatchers("/mcp/**").authenticated().oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);return http.build();}}运行本项目性能监控:MCP调用监控:通过Micrometer导出QPS、延迟指标工具健康检查:集成Spring Boot Actuator审计日志:记录每次工具调用的参数与结果六、性能基准测试:真实数据说话6.1 测试环境硬件:16核CPU, 64GB内存, SSD模型:gpt-4-turbo, qwen-plus并发:1000虚拟用户6.2 关键指标对比场景 Spring AI 原生HTTP 提升简单问答 120ms 180ms 33%RAG查询 350ms 520ms 33%工具调用 200ms 280ms 29%内存使用 800MB 1.2GB 33%6.3 生产调优参数spring:ai:chat:options:temperature:0.7max-tokens:1000retry:max-attempts:3backoff:multiplier:2max-delay: 5s circuitbreaker:failure-rate-threshold: 50% wait-duration-in-open-state: 30s 运行本项目七、企业落地路线图7.1 三阶段演进策略阶段1:试点验证(2-4周)选择非核心业务场景(如内部知识问答)使用Chroma+OpenAI快速原型建立监控和评估体系阶段2:核心场景(2-3个月)迁移到Milvus企业级向量库集成Spring Cloud微服务体系实现多模型路由策略阶段3:全面AI化(6-12个月)构建企业AI能力中台实现MCP生态集成建立AI治理体系7.2 避坑指南技术陷阱: 直接在生产环境使用Chroma(超过100万文档性能急剧下降) 忽视Token成本控制(实测GPT-4每月账单可达数万美元)缺少限流熔断(大促期间API额度耗尽导致服务雪崩)最佳实践: 使用分层架构:原型用Chroma,生产用Milvus实现Token预算管理:按用户/业务线配额部署本地模型兜底:Ollama+Llama2作为备用八、未来展望:Java AI原生时代Spring AI 1.0 GA的发布不是终点,而是Java AI生态的起点。随着以下特性的roadmap逐步实现:2025 Q3:多模态支持(图像、音频处理)2025 Q4:分布式Agent框架2026 Q1:AI工作流可视化编排2026 Q2:自动模型优化与压缩我们可以预见,未来3年内:80%的Java企业应用将具备AI能力,而Spring AI将成为这个时代的"Spring Boot"。 附录:快速开始指南1. 创建项目spring init --dependencies=web,ai my-ai-app 运行本项目2. 配置模型spring:ai:openai:api-key: ${OPENAI_API_KEY}dashscope:api-key: ${DASHSCOPE_API_KEY}运行本项目3. 第一个AI接口@SpringBootApplicationpublicclassAiApplication{publicstaticvoidmain(String[] args){SpringApplication.run(AiApplication.clas从0到1只需30分钟,这就是Spring AI的魅力。————————————————原文链接:https://blog.csdn.net/mr_yuanshen/article/details/153403301
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签