-
Java 大数据量分批处理与多线程优化实践1. 背景在实际开发中,我们经常需要处理大批量数据的插入或更新操作。如果一次性处理所有数据,可能会导致以下问题:数据库压力过大:单次事务处理大量数据,可能导致锁表、连接池耗尽。内存溢出(OOM):大列表一次性加载到内存,可能触发 OutOfMemoryError。性能瓶颈:单线程处理大数据量时,执行时间过长。本文将介绍如何通过分批处理 + 多线程优化来解决这些问题,并提供完整的代码示例。2. 基础分批处理方案2.1 单线程分批处理public static final int perSize = 200; // 每批次处理200条数据public void batchProcess(List<Data> dataList) { if (CollectionUtil.isNotEmpty(dataList)) { int batchCount = dataList.size() / perSize + 1; // 计算批次数量 for (int i = 0; i < batchCount; i++) { // 获取当前批次数据 List<Data> batchData; if (i == batchCount - 1) { batchData = dataList.subList(i * perSize, dataList.size()); // 最后一批 } else { batchData = dataList.subList(i * perSize, (i + 1) * perSize); // 常规批次 } // 执行批量操作(如插入或更新) batchUpdate(batchData); } }}AI生成项目java运行优点:减少单次数据库操作的数据量,避免锁表时间过长。降低内存占用,防止 OOM。缺点:单线程执行,速度较慢,无法充分利用 CPU 资源。3. 多线程分批优化3.1 使用线程池并发处理// 定义线程池private static final ThreadPoolExecutor updateThreadPool = new ThreadPoolExecutor( 10, // 核心线程数 20, // 最大线程数 60, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue<>(50), // 任务队列容量 Executors.defaultThreadFactory(), // 线程工厂 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:队列满时抛出异常);public void concurrentBatchProcess(List<Data> dataList) throws InterruptedException { if (CollectionUtil.isNotEmpty(dataList)) { int batchCount = dataList.size() / perSize + 1; CountDownLatch latch = new CountDownLatch(batchCount); // 计数器,等待所有任务完成 for (int i = 0; i < batchCount; i++) { final int batchIndex = i; updateThreadPool.submit(() -> { try { List<Data> batchData; if (batchIndex == batchCount - 1) { batchData = dataList.subList(batchIndex * perSize, dataList.size()); } else { batchData = dataList.subList(batchIndex * perSize, (batchIndex + 1) * perSize); } batchUpdate(batchData); // 执行批量操作 } catch (Exception e) { log.error("批次 {} 处理失败: {}", batchIndex, e.getMessage(), e); } finally { latch.countDown(); // 任务完成,计数器减1 } }); } latch.await(); // 等待所有任务完成 }}AI生成项目java运行优化点:多线程并发处理,提高执行速度。CountDownLatch 控制任务完成,确保所有批次执行完毕后再继续后续逻辑。异常捕获,防止单批次失败影响整体任务。3.2 线程池优化建议参数 推荐值 说明核心线程数 Runtime.getRuntime().availableProcessors() 根据 CPU 核心数动态调整最大线程数 CPU核心数 * 2 避免过多线程竞争空闲线程存活时间 30秒 较短时间回收空闲线程任务队列容量 100~1000 避免任务堆积导致 OOM拒绝策略 CallerRunsPolicy 队列满时由提交线程执行,避免丢失任务优化后的线程池配置:ThreadPoolExecutor optimizedPool = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 2, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());AI生成项目java运行4. 事务一致性考虑4.1 是否需要事务?强一致性要求(如金融交易):使用单线程 + 全局事务(@Transactional)。或采用 分布式事务(Seata、TCC)。最终一致性可接受(如日志记录):多线程分批处理 + 失败重试机制。4.2 失败重试方案// 记录失败批次List<Integer> failedBatches = new ArrayList<>();for (int i = 0; i < batchCount; i++) { final int batchIndex = i; updateThreadPool.submit(() -> { try { // ... 执行批次任务 } catch (Exception e) { failedBatches.add(batchIndex); // 记录失败批次 } finally { latch.countDown(); } });}latch.await();// 失败重试if (!failedBatches.isEmpty()) { log.warn("以下批次处理失败,尝试重试: {}", failedBatches); for (int batchIndex : failedBatches) { // 重新执行失败批次 }}AI生成项目java运行5. 完整代码示例import java.util.*;import java.util.concurrent.*;import org.springframework.util.CollectionUtils;public class BatchProcessor { private static final int perSize = 200; // 每批次大小 private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() * 2, 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy() ); public void processBatch(List<Data> dataList) throws InterruptedException { if (CollectionUtils.isEmpty(dataList)) return; int batchCount = dataList.size() / perSize + 1; CountDownLatch latch = new CountDownLatch(batchCount); List<Integer> failedBatches = new ArrayList<>(); for (int i = 0; i < batchCount; i++) { final int batchIndex = i; threadPool.submit(() -> { try { List<Data> batchData = getBatchData(dataList, batchIndex, batchCount); batchUpdate(batchData); } catch (Exception e) { failedBatches.add(batchIndex); log.error("Batch {} failed: {}", batchIndex, e.getMessage(), e); } finally { latch.countDown(); } }); } latch.await(); // 等待所有批次完成 // 失败重试 if (!failedBatches.isEmpty()) { retryFailedBatches(dataList, failedBatches); } } private List<Data> getBatchData(List<Data> dataList, int batchIndex, int batchCount) { int fromIndex = batchIndex * perSize; int toIndex = (batchIndex == batchCount - 1) ? dataList.size() : (batchIndex + 1) * perSize; return dataList.subList(fromIndex, toIndex); } private void batchUpdate(List<Data> batchData) { // 执行数据库批量操作 } private void retryFailedBatches(List<Data> dataList, List<Integer> failedBatches) { // 实现重试逻辑 }}AI生成项目java运行6. 总结方案 适用场景 优点 缺点单线程分批 小数据量、强一致性 简单易实现 速度较慢多线程分批 大数据量、允许最终一致性 速度快,资源利用率高 需处理线程安全、失败重试分布式事务 严格一致性要求 数据强一致 性能较低,实现复杂推荐选择:如果数据量较小(< 1万条),使用 单线程分批。如果数据量大且允许短暂不一致,使用 多线程分批 + 失败重试。如果涉及跨服务事务,使用 Seata/TCC 分布式事务————————————————原文链接:https://blog.csdn.net/qq_58035755/article/details/147554700
-
定义与原理JavaScript中的 while循环 是一种基本的循环控制结构,它允许开发者在特定条件下反复执行一段代码。其核心原理基于一个布尔条件表达式,只要该条件为真(true),循环就会持续执行。这种机制使while循环成为处理不确定次数迭代的理想选择,特别适用于需要动态确定循环终止条件的场景。值得注意的是,为了避免无限循环,开发者必须谨慎设计条件表达式和循环体内的更新逻辑,以确保循环能够在适当的时候终止。例如,在遍历数组元素或等待特定事件发生等情况下,while循环能够提供灵活而强大的解决方案。使用场景在JavaScript编程中,while循环作为一种灵活的控制结构,广泛应用于多种场景。以下是while循环的三个典型使用案例:用户输入验证 :通过不断提示用户输入直至满足特定条件,有效实现输入控制。数组遍历 :尤其适用于处理大小未知或可能变化的数组,提供高效的数据处理方案。游戏开发 :在游戏循环中扮演关键角色,持续更新游戏状态并响应玩家输入,确保游戏流畅运行。这些应用场景充分展示了while循环在处理不确定次数迭代和复杂条件判断方面的优势,使其成为JavaScript开发者的重要工具。while循环语法基本语法结构在JavaScript中,while循环是一种强大而灵活的控制结构,用于重复执行一段代码,直到指定的条件不再满足。其基本语法结构简洁明了,易于理解和使用:while (条件表达式) { // 循环体}AI生成项目这个结构的核心组成部分包括:条件表达式 :位于while关键字后面的小括号内。它是一个返回布尔值的表达式,通常涉及循环控制变量的比较运算。只有当这个表达式的结果为true时,循环才会执行。循环体 :包含在大括号{}之间的代码块。这部分代码在每次循环迭代时都会被执行。循环体可以包含任意数量的语句,甚至可以为空。为了更好地理解while循环的工作方式,让我们看一个典型的例子:let i = 0;while (i < 5) { console.log("这是第" + (i+1) + "次迭代"); i++;}在这个例子中:i = 0 是循环的初始化部分,通常放在循环外部i < 5 是条件表达式,决定了循环的终止条件console.log("这是第" + (i+1) + "次迭代") 是循环体内的主要操作i++ 是更新表达式,负责改变循环条件值得注意的是,while循环的一个重要特性是 先判断后执行 。这意味着在执行循环体之前,会首先评估条件表达式。如果初始条件就为false,那么循环体一次也不会执行。这一点与do-while循环有所不同,后者保证至少执行一次循环体,然后再判断条件。因此,在使用while循环时,需要特别注意以下几点:确保循环终将终止,防止无限循环在循环体内更新循环控制变量根据实际需求合理设置条件表达式通过掌握这些基本语法结构和注意事项,开发者可以在各种复杂的编程场景中有效地运用while循环,提高代码的效率和灵活性。条件表达式在JavaScript的while循环中,条件表达式是控制循环行为的核心要素。它决定了循环是否继续执行,直接影响着程序的流程和效率。为了确保循环的有效性和安全性,正确设置条件表达式至关重要。条件表达式应遵循以下原则:返回布尔值:表达式的结果必须是布尔类型(true或false)清晰明确:避免复杂的逻辑组合,提高可读性防止无限循环:设计合理的终止条件,避免逻辑错误在实践中,常见的条件表达式类型包括:数值比较 :如 i < 10布尔值直接使用 :如 condition === true复合条件 :结合多个条件,如 i < 10 && array[i] !== undefined对于初学者来说,理解条件表达式在不同情况下的执行逻辑尤为重要:条件表达式执行逻辑true进入循环体,执行循环false直接跳出循环值得注意的是,while循环的一个显著特点是“ 先判断后执行 ”。这意味着在进入循环体之前,会首先评估条件表达式。如果初始条件就为false,那么循环体一次也不会执行。这种特性使得while循环在处理不确定次数的迭代时特别有用,但同时也增加了潜在的陷阱,如意外的空循环或无限循环。为了更好地理解条件表达式的重要性,让我们来看一个实际的例子:let number = parseInt(prompt("请输入一个正整数:"));while (number > 0) { console.log(number); number--;}在这个例子中,条件表达式number > 0确保了循环会在用户输入的正整数降序打印完成后自动停止。这里的关键在于,随着循环的进行,number的值逐渐减小,最终会导致条件表达式变为false,从而自然地终止循环。通过合理设置条件表达式,我们可以精确控制循环的行为,使其既能满足预期的功能,又能避免潜在的问题。在实际开发中,根据具体需求选择适当的条件表达式,是优化算法效率和提升代码质量的重要手段。执行流程循环初始化在JavaScript的while循环中,循环初始化是一个至关重要的步骤,它为后续的循环执行奠定了基础。正确的初始化不仅能确保循环顺利启动,还能预防潜在的错误,如无限循环或空循环。循环初始化通常涉及以下几个方面:变量声明与赋值 :在循环开始前,需要为循环控制变量分配内存空间并赋予初始值。例如:let i = 0;这段代码为变量i分配了内存,并将其初始值设为0。这种做法确保了循环有一个明确的起点,为后续的迭代做好准备。初始值的选择 :初始值的选择应与循环的目的和终止条件相匹配。假如我们需要计算1到100的累加和,可以这样初始化:let sum = 0;let currentNumber = 1;这里,我们将累加器sum初始化为0,同时将循环控制变量currentNumber设为1,为后续的累加操作做好准备。防止无限循环 :在初始化阶段,还应考虑如何确保循环能够终止。这通常涉及到初始值与终止条件的关系。例如:let counter = 0;while (counter < 10) { // 循环体 counter++;}AI生成项目在这个例子中,counter的初始值为0,与终止条件counter < 10配合得当,确保了循环将在适当时候终止。适应不同场景 :循环初始化应根据具体需求进行调整。对于倒序循环,初始化可能会有所不同:let number = 10;while (number >= 1) { console.log(number); number--;}这里,我们将number初始化为10,以适应从10递减到1的需求。通过精心设计的循环初始化,我们可以为while循环的成功执行奠定坚实的基础,确保循环既有效又安全地完成预期的任务。循环体执行在while循环的执行过程中,循环体的执行是核心环节。一旦条件表达式被评估为true,循环体内的代码就会开始执行。这个过程会一直持续,直到条件表达式变为false为止。循环体的执行过程主要包括以下几个关键步骤:代码执行 :循环体会按顺序执行其中的所有语句。这些语句可以是简单的操作,如变量赋值或函数调用,也可以是复杂的逻辑控制结构。变量更新 :在循环体内,通常需要更新循环控制变量的值。这是至关重要的,因为它直接影响循环的终止条件。例如:let i = 0;while (i < 5) { console.log(i); i++; // 更新循环控制变量}AI生成项目在这个例子中,i++操作确保了每次迭代后i的值都会增加,最终导致循环条件变为false。多次迭代 :循环体会根据条件表达式的评估结果执行多次。每次迭代结束后,都会重新评估条件表达式。只有当条件首次变为false时,循环才会终止。内部逻辑控制 :循环体内还可以包含额外的控制语句,如if语句或其他循环结构,以实现更复杂的逻辑。例如:let sum = 0;let num = 1;while (num <= 10) { if (num % 2 === 0) { sum += num; } num++;}console.log(sum); // 输出偶数之和这个例子展示了如何在循环体内使用条件语句来过滤特定的迭代,并执行相应的操作。通过巧妙设计循环体内的逻辑,开发者可以实现各种复杂的算法和数据处理任务。然而,需要注意的是,循环体的设计应当确保循环终将终止,以避免无限循环的发生。此外,合理安排循环体内的操作顺序也是优化循环性能的关键因素之一。条件评估在JavaScript的while循环中,条件评估是控制循环执行的核心机制。每次迭代后,JavaScript引擎都会重新评估循环的条件表达式,以决定是否继续执行下一次迭代。这一过程确保了循环能够根据动态变化的条件灵活地终止或继续。条件评估的具体流程如下:执行循环体 :在每次迭代中,循环体内的代码会被执行。重新评估条件 :循环体执行完毕后,JavaScript会立即回到循环的起始处,重新评估条件表达式。布尔值判断 :如果条件表达式的结果为true,循环将继续执行下一次迭代。如果结果为false,则循环终止,控制权转移到循环之后的代码。值得注意的是,条件评估的时机发生在 每次迭代的末尾 ,而非开头。这意味着即使条件在循环体内部发生了变化,也必须等到当前迭代结束才能进行下一次评估。这种机制保证了循环体至少有一次完整的机会执行,无论条件如何变化。为了更好地理解这一过程,让我们看一个具体的例子:let number = 10;while (number > 0) { console.log(number); number--;}在这个例子中,条件number > 0会在每次迭代后被重新评估。初始时,number为10,条件为真,因此执行循环体。每次迭代后,number的值减少1,直到number变为0时,条件才变为false,循环终止。通过这种方式,while循环提供了灵活的方式来控制循环的执行次数,使得它特别适合处理那些不确定迭代次数的场景。这种机制使得while循环成为解决许多编程问题的强大工具,特别是在需要根据动态条件反复执行某段代码的情况下。高级用法无限循环在探讨高级用法时,我们不得不提及无限循环这一特殊技巧。虽然看似违背了循环设计的初衷,但在某些场景下却能发挥独特作用。无限循环通过设置始终为真的条件表达式(如while (true))来创建。这种方法常用于模拟持续运行的应用程序主循环,如游戏引擎或实时数据分析系统。然而,使用时需格外谨慎,因为不当设计可能导致程序陷入无法终止的状态。为避免这种情况,通常在循环体内使用break语句或特殊的退出条件来控制循环的终止。这种方法能在处理不确定持续时间的任务时提供更大的灵活性,但也要求开发者更加小心地管理循环的退出逻辑,以确保程序的安全性和可靠性。嵌套循环在JavaScript中,嵌套循环是一种强大的技术,允许在一个循环结构内部包含另一个循环。这种结构特别适用于处理多维数据或执行复杂的迭代任务。例如:let outerCounter = 0;while (outerCounter < 3) { let innerCounter = 0; while (innerCounter < 2) { console.log(`外层循环: ${outerCounter}, 内层循环: ${innerCounter}`); innerCounter++; } outerCounter++;}这个例子展示了如何在外层while循环中嵌套另一个while循环。内层循环会完整执行其所有迭代,然后外层循环再进行下一次迭代。这种结构在处理矩阵或执行多层次的数据处理时非常有用。值得注意的是,嵌套循环的深度不应超过必要限度,以避免复杂的控制流和潜在的性能问题。在实际应用中,应根据具体需求谨慎使用嵌套循环,以确保代码的可读性和效率。循环控制break语句在JavaScript中,break语句是一种强大的循环控制工具,允许开发者在特定条件下立即终止循环执行。它不仅适用于while循环,还可用于for循环和switch语句。当遇到break语句时,循环会立即终止,跳过剩余的迭代,并继续执行循环之后的代码。break语句的基本语法简单直观:while (条件) { // 循环体 if (终止条件) { break; } // 其他代码}这种结构使得开发者能够灵活地控制循环的执行流程,特别适用于需要根据动态条件提前退出循环的情况。例如,在搜索算法中,一旦找到目标元素,就可以使用break语句立即终止循环,提高效率。continue语句在JavaScript中,continue语句是一种精巧的循环控制工具,专门用于 跳过当前迭代 ,快速过渡到下一次循环。与break语句不同,continue并不终止整个循环,而是巧妙地绕过当前迭代的剩余部分,直接进入下一轮循环。这种机制在处理复杂数据结构或执行条件筛选时尤为有效,能够显著提高代码的效率和可读性。例如:for (let i = 0; i < 10; i++) { if (i % 2 === 0) { continue; } console.log(i);}AI生成项目在这个例子中,continue语句优雅地跳过了所有偶数的输出,仅保留了奇数的打印,展现了其在条件控制上的独特优势———————————————— 原文链接:https://blog.csdn.net/2401_86544677/article/details/144050460
-
一、MQTT协议MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅式消息传递协议,专为物联网(IoT)和嵌入式设备设计,它简化了设备之间的通信,并优化带宽使用。 在MQTT中,消息的发送者称为“发布者”(Publisher)消息的接收者称为“订阅者”(Subscriber),而消息的中转站是“代理”(Broker)。发布者将消息发布到特定的“主题”(Topic),代理负责将消息转发给所有订阅了该主题的订阅者。这种模式解耦了消息的发送者和接收者,使得系统更加灵活和可扩展。二、MQTT优点低功耗、高效、可靠。轻量级:协议设计简洁,消息头部开销小,适用于低带宽和低功耗设备。支持发布/订阅模式:设备可以发布消息到主题,其他设备可以订阅对应的主题接收消息。这一模式解耦了消息生产者和消费者,简化了系统架构,提高了灵活性和可扩展性。可拓展性和兼容性:MQTT允许使用不同的传输协议,包括TCP、WebSocket等。它的简单性使得它易于与其他协议和服务集成。持久化会话:MQTT支持消息持久化,允许设备在断线后重新连接时恢复之前的会话状态,包括未完成的订阅和未收到的消息队列,这对于网络不稳定或经常断开的物联网环境尤为重要。三、三种服务质量等级QoS = 0(最多一次):消息最多被传递一次,可能丢失,但不会重复。此级别提供的可靠性最低,一旦消息被客户端发送出去,它不会等待任何确认,即“Fire and Forget”模式。这意味着发布者不会确认消息是否到达Broker,也不会尝试重传失败的消息)QoS = 1(至少一次):消息至少被传递一次,可能会重复,但不会丢失。此级别保证消息至少被送达一次,但有可能被重复发送。在QoS 1下,Broker(消息队列服务器)会发送PUBACK确认消息给客户端,如果客户端没有收到确认,则会重发消息,直到收到确认为止。因此,虽然可以确保消息不会丢失,但也可能导致相同消息被多次接收QoS = 2(恰好一次):消息保证被传递一次且仅一次,不会丢失也不会重复。这是MQTT提供的最高级别服务质量,确保每条消息只会被接收一次,提供最严格的可靠性保证。该机制通过一个复杂的四次握手过程实现,包括消息标识符的确认和释放,确保消息既不丢失也不重复四、客户端、代理、主题MQTT协议中,三个核心概念分别是客户端(Client)、代理(Broker)和主题(Topic),它们共同构成了MQTT通信的基础框架,实现了消息的发布与订阅机制。1. 客户端(Client):作用:客户端可以是消息的发布者(Publisher)或订阅者(Subscriber),也可以同时具备这两种角色。发布者负责向MQTT系统中的某个主题发布消息;订阅者则订阅感兴趣的主题,以接收来自该主题的消息。客户端可以是传感器、手机应用、服务器程序等各种设备或应用。相互关系:客户端不直接相互通信,而是通过Broker中转消息。发布者客户端向Broker发送消息,而订阅者客户端从Broker接收消息。 2. 代理(Broker):作用:Broker是MQTT通信的中心节点,它接收来自发布者客户端的消息,并根据消息中的主题分发给相应的订阅者客户端。Broker负责维护客户端的连接状态、存储消息(如果需要持久化)、管理主题的订阅关系等。相互关系:Broker是客户端之间的中介,它管理着所有的消息流动。每个客户端都与Broker建立连接,无论发布还是订阅操作,都必须通过Broker来完成。3. 主题(Topic):作用:主题是MQTT中消息的分类标签,类似于一个消息通道或者频道。每个消息都会关联一个主题,发布者通过指定主题来决定消息的去向,而订阅者通过订阅特定主题来接收相关消息。相互关系:主题是连接发布者与订阅者的桥梁。发布者向特定主题发布消息,而订阅者则通过订阅这些主题来接收消息。Broker根据主题匹配规则,确保消息被正确地路由到已订阅该主题的所有客户端。主题可以是静态的字符串,也可以包含通配符(如"+“和”#”)来实现灵活的匹配规则。五、实战应用1. 安装部署(linux) -- 拉取镜像docker pull emqx/emqx:5.0.26-- 安装容器docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx:5.0.262. 访问控制台访问:ip:18083默认的用户名密码:admin/public3. 客户端认证4. 创建用户5. SpringBoot中整合5.1 导入jar包<dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId></dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-stream</artifactId></dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional></dependency>AI写代码XML5.2 yml配置mqtt: #MQTT-服务器连接地址,如果有多个,用逗号隔开 host: tcp://192.168.17.101:1883 #MQTT-连接服务器默认客户端ID,可以随便写 clientId: mqtt_test #MQTT-用户名 username: zhangsan #MQTT-密码 password: 123456 #MQTT-指定消息的推送和订阅主题 topic: test #连接超时 timeout: 100 #设置会话心跳时间 keepalive: 10AI写代码java运行5.3 MqttConfig.java@Slf4j@Configuration@ConfigurationProperties("mqtt")@Datapublic class MqttConfig { String host; String clientId; String topic; String username; String password; Integer timeout; Integer keepalive; // MQTT客户端的配置类,可以设置mqtt服务器的账号和密码 @Bean public MqttConnectOptions mqttConnectOptions() { MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(username); options.setPassword(password.toCharArray()); // 设置是否自动重连 options.setAutomaticReconnect(true); // false 保持会话不被清理自动重连后才能收到订阅的主题消息(包括离线时发布的消息) options.setCleanSession(true); options.setConnectionTimeout(timeout); options.setKeepAliveInterval(keepalive); return options; } // MqttClient 类,MQTT的客户端类,可以去连接MQTT服务器 @Bean public MqttClient mqttClient(MqttConnectOptions mqttConnectOptions) { try { MqttClient client = new MqttClient(host, clientId); // 回调对象,监听消息的获取,采用的接口回调,可以获取对应订阅到的消息 client.setCallback(new MessageCallback(client, this.topic, mqttConnectOptions)); // 连接 client.connect(mqttConnectOptions()); return client; } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("mqtt 连接异常"); } }}AI写代码java运行5.4 MessageCallback.java/** * consumer 消费者,对收到的消息进行处理 *///@Component@Slf4jpublic class MessageCallback implements MqttCallbackExtended { private MqttClient client; private String topic; private MqttConnectOptions mqttConnectOptions; public MessageCallback() { } public MessageCallback(MqttClient mqttClient, String topic, MqttConnectOptions mqttConnectOptions) { this.client = mqttClient; this.topic = topic; this.mqttConnectOptions = mqttConnectOptions; } // 在客户端连接断开时触发 @Override public void connectionLost(Throwable throwable) { if (client != null && !client.isConnected()) { log.info("{}, 连接断开,正在reconnect....", client.getClientId()); try { client.reconnect(); // client.connect(this.mqttConnectOptions); } catch (MqttException e) { e.printStackTrace(); } } else { log.info("未知异常,连接断开"); } } // 在客户端与服务器连接成功时触发 @Override public void connectComplete(boolean b, String url) { log.info("{} 上线了{} {}", client.getClientId(), b, url); try { client.subscribe(this.topic, 0); } catch (MqttException e) { e.printStackTrace(); } } // 在客户端收到订阅的消息时触发 @Override public void messageArrived(String topic, MqttMessage message) throws Exception { log.info("接收消息主题 : " + topic); log.info("接收消息内容 : " + new String(message.getPayload())); String msg = new String(message.getPayload()); try { JSONObject jsonObject = JSON.parseObject(msg); String clientId = String.valueOf(jsonObject.get("clientid")); if (topic.endsWith("disconnected")) { log.info("设备{}已掉线", clientId); } else if (topic.endsWith("connected")) { log.info("设备{}已上线", clientId); } else { log.info("其他主题的消息"); } } catch (JSONException e) { log.error("JSON Format Parsing Exception : {}", msg); } } // 在客户端发送消息至服务器成功时触发 @Override public void deliveryComplete(IMqttDeliveryToken token) { log.info("deliveryComplete---------" + token.isComplete()); }}AI写代码java运行5.5 MqttUtil.java@Component@Slf4jpublic class MqttUtil { @Autowired(required = false) private MqttClient client; /** * 订阅主题 * * @param topic * @param qos */ public void subscribe(String topic, int qos) { try { client.subscribe(topic, qos); } catch (MqttException e) { e.printStackTrace(); } } /** * 订阅主题 * * @param topic */ public void subscribe(String topic) { try { client.subscribe(topic); } catch (MqttException e) { e.printStackTrace(); } } /** * 发布消息 * * @param qos 连接方式 0,1,2 默认0 * @param retained 是否保留最新的消息 * @param topic 订阅主题 * @param pushMessage 消息体 */ public void publish(int qos, boolean retained, String topic, String pushMessage) { MqttMessage message = new MqttMessage(); message.setQos(qos); message.setRetained(retained); message.setPayload(pushMessage.getBytes()); MqttTopic mqttTopic = client.getTopic(topic); if (null == mqttTopic) { log.error("topic not exist"); } MqttDeliveryToken token; try { // 发送消息 token = mqttTopic.publish(message); token.waitForCompletion(); } catch (MqttPersistenceException e) { e.printStackTrace(); } catch (MqttException e) { e.printStackTrace(); } } /** * 发布消息 * * @param topic 主题 * @param pushMessage 消息内容 */ public void publish(String topic, String pushMessage) { publish(0, true, topic, pushMessage); }}AI写代码java运行5.6 MqttController.java@RestController@Slf4jpublic class MqttController { @Autowired MqttClient client; @Autowired MqttUtil mqttUtil; @GetMapping("/send") public String send() { try { for (int i = 0; i < 3; i++) { mqttUtil.publish("test", "消息hello" + i); log.info("发送成功:{}", i); Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } return "SUCCESS"; }}AI写代码java运行六、MQTTX官网地址MQTT客户端工具MQTTX下载地址————————————————原文链接:https://blog.csdn.net/qq_63288465/article/details/146251672
-
一、super 关键字概述在 Java 的面向对象编程中,super是一个非常重要的关键字。它主要用于引用父类的成员,包括属性、方法和构造函数。通过super,子类可以访问和调用父类中被隐藏或重写的成员,从而实现对父类功能的扩展和复用。1.1 super 的基本作用访问父类的属性:当子类中定义了与父类同名的属性时,可以使用super关键字访问父类的属性。调用父类的方法:当子类重写了父类的方法时,可以使用super关键字调用父类的原始方法。调用父类的构造函数:在子类的构造函数中,可以使用super关键字调用父类的构造函数,必须在子类构造函数的第一行使用。二、super 关键字的具体用法2.1 访问父类的属性当子类中定义了与父类同名的属性时,直接使用属性名访问的是子类的属性。如果需要访问父类的属性,则需要使用super关键字。class Parent { int value = 10;} class Child extends Parent { int value = 20; public void printValues() { System.out.println("子类的value: " + value); // 输出20 System.out.println("父类的value: " + super.value); // 输出10 }} public class Main { public static void main(String[] args) { Child child = new Child(); child.printValues(); }}AI生成项目2.2 调用父类的方法当子类重写了父类的方法时,可以使用super关键字调用父类的原始方法。这在需要在子类中扩展父类功能时非常有用。class Animal { public void move() { System.out.println("动物可以移动"); }} class Dog extends Animal { @Override public void move() { super.move(); // 调用父类的move方法 System.out.println("狗可以跑和走"); }} public class Main { public static void main(String[] args) { Dog dog = new Dog(); dog.move(); }}AI生成项目2.3 调用父类的构造函数在子类的构造函数中,可以使用super关键字调用父类的构造函数。这在父类有参数的构造函数时尤为重要,因为子类必须显式调用父类的构造函数来初始化从父类继承的属性。class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; }} class Student extends Person { private int studentId; public Student(String name, int studentId) { super(name); // 调用父类的构造函数 this.studentId = studentId; } public void displayInfo() { System.out.println("姓名: " + super.getName()); // 调用父类的方法 System.out.println("学号: " + studentId); }} public class Main { public static void main(String[] args) { Student student = new Student("张三", 1001); student.displayInfo(); }}AI生成项目三、super 与 this 的区别super和this都是 Java 中的关键字,但它们的用途不同:this:引用当前对象,用于访问当前对象的属性和方法,也可以调用当前对象的其他构造函数。super:引用父类的对象,用于访问父类的属性、方法和构造函数。四、注意事项super 调用构造函数:super()必须是子类构造函数中的第一行代码,否则会导致编译错误。隐式调用父类构造函数:如果子类的构造函数中没有显式调用父类的构造函数,Java 会自动调用父类的无参构造函数。如果父类没有无参构造函数,则会导致编译错误。super 不能在静态方法中使用:因为super引用的是实例对象,而静态方法属于类,不依赖于任何实例。五、总结super关键字在 Java 的继承机制中扮演着重要的角色,它允许子类访问和调用父类的成员,从而实现代码的复用和扩展。通过合理使用super,可以编写出更加清晰、健壮的面向对象程序。希望本文能够帮助你理解 Java 中super关键字的基本概念和用法。如果你有任何疑问或建议,欢迎在评论区留言讨论!文章涵盖了super关键字的基本概念、三种主要用法(访问父类属性、调用父类方法、调用父类构造函数)、与this的区别以及使用时的注意事项,适合 Java 初学者入门学习————————————————原文链接:https://blog.csdn.net/2402_84764726/article/details/147868905
-
一、String类的理解1、类的声明public final class String implements java.io.Serializable, Comparable<String>, CharSequence {AI生成项目java运行final:String是不可以被继承的;Serializable:可序列化的接口,凡是实现此接口的类的对象就可以通过网络或者本地流进行数据的传输。Comparable:凡是实现此接口的类,其对象都可以比较大小。2、内部声明的属性private final char value[];AI生成项目java运行存储字符串数据的容器final:指明此value数组一旦初始化,其地址就不可变3、字符串常量的存储位置字符串常量都存储在字符串常量池(StringTable)中字符串常量池不允许存放两个相同的字符串常量字符串常量池在不同的jdk版本中,存放的位置不同4、字符串的不可变性的理解1、当对字符串变量重新赋值时,需要重新指定一个字符串常量的位置进行赋值,不能在原来的位置修改2、对现有的字符串进行拼接操作时,需要重新开辟空间保存新的字符串。3、当调用字符串的replace方法替换现有的某个字符时,需要重新开辟空间保存修改以后的字符串,不能原地修改public class StringDemo { public static void main(String[] args) { StringDemo s = new StringDemo(); s.test2(); s.test3(); } // todo String的不可变性 // 当对字符串变量重新赋值时,需要重新指定一个字符串常量的位置进行赋值,不能在原来的位置修改 // 对现有的字符串进行拼接操作时,需要重新开辟空间保存新的字符串。 // 当调用字符串的replace方法替换现有的某个字符时,需要重新开辟空间保存修改以后的字符串,不能原地修改 public void test2() { String s1 = "hello"; String s2 = "hello"; s2 = "hi"; s2+="world"; System.out.println(s1); // todo hello System.out.println(s1); // todo hello } public void test3() { String s1 = "hello"; String s2 = "hello"; String s3=s2.replace('l','o'); System.out.println(s1); // hello System.out.println(s2); // hello System.out.println(s3); // heooo }} AI生成项目java运行5、String实例化的两种方式String s1 = “hello”;String s2 = new String(“hello”);public class StringDemo1 { public static void main(String[] args) { StringDemo1 s = new StringDemo1(); s.test1(); } public void test1(){ String s1 = "hello"; String s2 = "hello"; String s3 = new String("hello"); String s4 = new String("hello"); System.out.println(s1==s2); //true System.out.println(s1==s3); //false System.out.println(s1==s4); //false System.out.println(s3==s4); //false System.out.println(s1.equals(s2)); //true System.out.println(s1.equals(s3)); //true System.out.println(s1.equals(s4)); //true System.out.println(s3.equals(s2)); //true }}AI生成项目java运行6、字符串的拼接1、常量+常量:结果仍然存储在字符串常量池;此时的常量可能是字面量,也可能是final修饰的变量。2、常量+变量 或者 变量+常量:都会通过new的方式创建一个新的字符串,返回堆空间中此字符串对象的地址3、调用字符串的intern():返回字面量的地址 public void test2() { String s1 = "hello"; String s2 = "world"; String s3 = "helloworld"; String s4 = "hello" + "world"; String s5 = s1 + "world"; //todo 通过查看字节码文件发现调用了StringBuilder()——》new String() String s6 = "hello" + s2; String s7 = s1 + s2; System.out.println("------------------------------"); System.out.println(s3 == s4); //true System.out.println(s3 == s5); //false System.out.println(s3 == s6); //false System.out.println(s3 == s7); //false System.out.println(s5 == s6); //false System.out.println(s5 == s7); //false }AI生成项目java运行 public void test3() { final String s1 = "hello"; final String s2 = "world"; String s3 = "helloworld"; String s4 = "hello" + "world"; String s5 = s1 + "world"; //todo 通过查看字节码文件发现调用了StringBuilder()——》new String() String s6 = "hello" + s2; String s7 = s1 + s2; System.out.println("------------------------------"); System.out.println(s3 == s5); //true System.out.println(s3 == s6); //true }AI生成项目java运行二、String的构造器1、构造器public String() :初始化新创建的 String对象,以使其表示空字符序列。public String(String original):初始化一个新创建的“String”对象,使其表示一个与参教相同的字符序列public String(char[] value):通过当前参数中的字符数组来构造新的String。public String(char[] valve,int offset,int count):通过字符数组的一部分来构造新的String。public String(byte[] bytes):通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。public String(byte[] bytes,String charsetName)':通过使用指定的字符集解码当前参数中的字节数组来构造新的String。2、String和char之间相互转换String——》char[]:调用String的toCharArray()方法char——》String:调用String的构造器public class StringMethodTest { public static void main(String[] args) { StringMethodTest s = new StringMethodTest(); s.test1(); s.test2(); s.test3(); } public void test1() { String s1 = new String(); String s2 = new String(""); String s3 = new String(new char[]{'a', 'b', 'c'}); System.out.println(s3); } public void test2() { String str = "hello"; //todo String——》char[]:调用String的toCharArray()方法 char[] arr = str.toCharArray(); for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } //todo char——》String:调用String的构造器 String str1 = new String(arr); System.out.println(str1); //hello }}AI生成项目java运行3、String和byte之间相互转换String——》byte[]:调用String的getBytes()方法getBytes(String charsetName):使用指定的字符集在utf-8字符集中,一个汉字占用3个字节,一个字母占用1个字节。在gbk字符集中,一个汉字占用2个字节,一个字母占用1个字节。public class StringMethodTest { public static void main(String[] args) { StringMethodTest s = new StringMethodTest(); s.test1(); s.test2(); s.test3(); } // String与byte[]之间的转换 // 在utf-8字符集中,一个汉字占用3个字节,一个字母占用1个字节。 // 在gbk字符集中,一个汉字占用2个字节,一个字母占用1个字节。 public void test3() { String str = "中国"; //todo String——》byte[]:调用String的toCharArray()方法 byte[] arr = str.getBytes(); //使用默认的字符集 for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); // -28\-72\-83\-27\-101\-67 } System.out.println();// String str1 = new String("abc中国");// // todo getBytes(String charsetName):使用指定的字符集// byte[] arr1 = str1.getBytes("gbk"); //使用默认的字符集// for(int i = 0;i<arr.length;i++){// System.out.println(arr[i]); // 101\101\108\108\111// } // byte[]——》String String str2 = new String(arr); System.out.println(str2); //中国 }}AI生成项目java运行三、String中常用方法1、boolean isEmpty():字符串是否为空;2、int length():返回字符串的长度;3、String concat(xx):字符串拼接;4、boolean equals(Object obj):比较字符串是否相等,区分大小写;5、boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写;6、int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小;7、int compareTolgnoreCase(String other):比较字符串大小,不区分大小写;8、String toLowerCase():将字符串中大写字母转为小写;9、String toUpperCase():将字符串中小写字母转为大写;10、String trim():去掉字符串前后空白符;11、public String intern():结果在常量池中共享;12、boolean contains(xx):是否包含xx13、int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1;14、int indexOf(String str,int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引开始向后找;15、int lastIndexOf(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回-116、int lastIndexOf(String str,int fromIndex):返回指定子字符串在此字符串中最后一次出现处的并且向前找;17、String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取;18、String substring(int beginIndex,int endIndex):返回一个新字符串,它是此字符串从beginIndex开始截取,到endIndex结束;19、char charAt(index):返回[index]位置的字符20、char[] toCharArray(): 将此字符串转换为一个新的字符数组返回21、static String valueOf(char[] data):返回char数组参数的字符串表示形式22、static String valueOf(char[] data,int offset,int count): 返回char数组参数的特定子数组的字符串表示形式23、static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的字符串。24、static String copyValue0f(char[] data,int offset,int count):返回指定数组中指定片段的字符串。start:开始下标 count:长度25、boolean startsWith(xx):方法用于检测字符串是否以指定的子字符串开始。26、boolean startsWith(string prefix,int toffset):如果字符串以指定的前缀开始,则返回 true;否则返回 false。27、boolean endsWith(xx):测试此字符串是否以指定的后结束28、String replace(char oldchar,char newchar):返回一个新的字符串,它是通过用 newchar 替换oldchar;29、String replace(CharSequence target,charSequence replacement):用replacement替换所有的target,两个参数都是字符串。30、String replaceAll(String regex,String replacement):用replacement替换所有的regex匹配项,regex很明显是个正则表达式,replacement是字符串。31、String replaceFirst(String regex,String replacement):基本和replaceAll相同,区别是只替换第一个匹配项————————————————原文链接:https://blog.csdn.net/YZL40514131/article/details/144090278
-
什么是HashMap可以拆开理解,即Hash+Map,HashMap是一个存储键值对的数据结构的一种实现,通过对键作hash索引对键值对进行存取操作。由每一个键值作hash索引后,在HashMap中的存储位置是不尽相同的(会有两个键值不同,但hash值相同的情况),所以在HashMap中存储的数据是无序的,且对于键的值而言是不重复的什么是HashHash散列将一个任意长度的值通过hash函数算法,转换成一个固定的值Java中的hash函数算法用移位去实现,如下是jdk1.7中HashMap的hash函数/** * Retrieve object hash code and applies a supplemental hash function to the * result hash, which defends against poor quality hash functions. This is * critical because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);}AI生成项目java运行什么是Map存储键值对的数据结构,kv对:k是用来查找键值对中数据的索引或者标识,所以在Map里,key值一定是唯一的,v是与其绑定的要存储的数据源码分析问题:两个key的hash值重复,value是否会覆盖HashMap什么时候作扩容?作put()方法时会扩容重点方法:put()和get()以jdk1.7源码为例,看看put()、get()和一些需要讲解的变量常亮先看里面几个比较重要的属性/** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16AI生成项目java运行默认的初始化容量,如果我们调用无参的构造参数,那么我们得到的HashMap,其初始容量即为16/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */static final int MAXIMUM_CAPACITY = 1 << 30;AI生成项目java运行HashMap容量的最大值(2^30),即HashMap的容量不得超过这个值/** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;AI生成项目java运行默认的负载因子为0.75/** * An empty table instance to share when the table is not inflated. */static final Entry<?,?>[] EMPTY_TABLE = {}; /** * The table, resized as necessary. Length MUST Always be a power of two. */transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;AI生成项目java运行一个键值对类型的空表常亮和一个指向这个常亮的键值对类型的哈希索引表(或者数组,下标为哈希索引)/** * The number of key-value mappings contained in this map. */transient int size; /** * The next size value at which to resize (capacity * load factor). * @serial */// If table == EMPTY_TABLE then this is the initial capacity at which the// table will be created when inflated.int threshold; /** * The load factor for the hash table. * * @serial */final float loadFactor; /** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */transient int modCount;AI生成项目java运行表示存储键值对的总量的变量,表示HashMap扩容界限的变量以及表示复杂因子的变量和表示HashMap结构变化次数的常亮再看一下HashMap的构造函数/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init();} AI生成项目java运行HashMap中有4个构造函数,1个无参的,3个有参的,不管调用的是哪个构造函数,最后都会调用到如上的构造函数从该构造函数可以了解到,传入了一个初始化容量和一个loadFactor,即负载系数,然后再进行参数验证后将参数赋给了成员变量loadFactor和threshold只是赋值成员变量吗,会不会还有其它的过程没有找到,先别急,我们再来看下put方法/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}AI生成项目java运行这下我们看到了,当调用put时,传入键值对参数,此时先检查table是否是一个空表,并对空table进行扩充和初始化,所以初始化流程在inflateTable()中/** * Inflates the table. */private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity);}AI生成项目java运行在上面的方法中,判断了给定的容量大小是否会超出容量最大值,作限制处理后将其赋给变量threshold,空table初始化成一个容量为capacity大小的键值对类型的数组,然后执行了方法initHashSeedAsNeeded()/** * Initialize the hashing mask value. We defer initialization until we * really need it. */final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching;}AI生成项目java运行上面的方法主要用于初始化hash码值,并赋值到hashSeed中,依据出入的容量,并从jdk中获取jdk.map.althashing.threshold对应的值来最终判断是否需要初始化hashSeed,这个方法在HashMap扩容时会用到,主要是在扩容时调用此方法,判断扩容后是否需要重做hash索引,后面也会提到再回到put()方法,我们来看下键值对在HashMap中是怎么存储的public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}AI生成项目java运行首先判断key是否为null,若key是null,则存放key为null的value的值,且认为null的hash索引为0private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null;}AI生成项目java运行若key不为null,则会调用hash()方法和indexFor()方法获取该key对应的hash索引,然后遍历该hash索引对应的键值对链表,若存在一个重复键值对(key重复),替换该键值对的value值,否则会在该链表的头部新增一个键值对,如下方法所示void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex);} void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++;}AI生成项目java运行到这里我们大致明白了,HashMap中存储一个键值对时,对于key为null的情况,认为hash索引为0,对key不为null的情况,会算出该key对应hash索引,然后新建链表或者在已有链表上检查是否存在key键重复的情况,将链表进行更新所以对于HashMap而言,数据是key值不重复的,且存储顺序非存入顺序,即无序且key不重复那么键值对又是怎样获取的呢public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();} final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null;}AI生成项目java运行可以看到HashMap是根据传入的key的值去查找对应的键值对中的value值的,若key为null,则去hash索引为0的链表中找key为null的键值对,取其value值;若key非null,则还是去算key对应的hash索引,找出对应的链表,遍历链表中是否存在key相同的键值对,取其null值,若出现找不到的情况,则会返回null到这里,分析完put()方法和get()方法,上文提到的第一个问题就有答案,针对两个key的hash值相同的情况,若两个key重复,则会使原有键值对的value的值替换,若两个key不重复,则会在对应的链表头部新增一个键值对,这是不会有键值对的value替换发生那么HashMap是怎么扩容的?何时扩容的?扩容了多少呢?void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex);} void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);} void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } }}AI生成项目java运行HashMap在调用put()方法,即添加键值对实例时会有扩容操作,条件是数据长度到达或超过临界值,且hash索引对应的链表为空。扩容是将容量扩充至2倍,然后将所有键值对重建hash索引并存储,所以,HashMap的一次扩容操作,会遍历所有存储的键值对,并重建hash索引和对应链表,这个过程是相对耗时的,会降低HashMap的使用效率优缺点分析优点:利用数组+链表的方式存储键值对,利用了数组查找快和链表插入删除操作快的优点通过hash函数和hash索引方式来查找及存储键值对,在不发生hash碰撞和不扩容情况下,put和get的时间复杂度为O(1),发生hash碰撞时,put和get的时间复杂度为O(k),k为对应hash索引的链表的遍历次数缺点:hash碰撞会影响HashMap的存取效率,原因在于hash碰撞时,会对相应链表进行遍历,因此选用hash算法的好坏对于HashMap来说至关重要扩容会影响HashMap的使用效率,每当HashMap扩容的时候,对所有Entry对象重新hash,然后放入新数组里,所以频繁扩容会影响效率怎样去用HashMap基于上述HashMap的优缺点,使用HashMap时应尽量避免HashMap中的hash碰撞情况和扩容操作,如果我们用基本数据类型的包装类以及String作key值,jdk有现成的hash算法,如果我们用对象类型作key,一定要注意实现好对象类型的hashCode()函数;另外如果能预估要存储的键值对的大致数量,可以的话,new一个HashMap时,尽量传入一个自定义的capacity来避免HashMap的扩容操作,提高其使用效率————————————————原文链接:https://blog.csdn.net/qq_17589253/article/details/102743831
-
一、Random 类的基本用法(一)导入类在 Java 中,Random 类位于 java.util 包中,因此在使用之前需要导入该包。import java.util.Random;AI生成项目java运行1(二)创建 Random 对象可以通过以下两种方式创建 Random 对象:Random random = new Random();// 或者指定种子Random random = new Random(long seed);AI生成项目java运行种子用于初始化随机数生成器的起始状态。相同的种子会生成相同的随机数序列。(三)生成随机整数使用 nextInt() 方法可以生成一个随机整数。如果不指定参数,它将返回一个介于 Integer.MIN_VALUE 和 Integer.MAX_VALUE 之间的随机整数。如果指定一个参数 n,它将返回一个介于 0(包含)和 n(不包含)之间的随机整数。Random random = new Random();int randomInt = random.nextInt(); // 生成任意随机整数int randomIntBetween0And10 = random.nextInt(10); // 生成0到9之间的随机整数AI生成项目java运行(四)生成随机 double 值使用 nextDouble() 方法可以生成一个介于 0.0(包含)和 1.0(不包含)之间的随机双精度浮点数。Random random = new Random();double randomDouble = random.nextDouble();AI生成项目java运行(五)生成随机 boolean 值使用 nextBoolean() 方法可以生成一个随机的布尔值,true 和 false 的概率各为 50%。Random random = new Random();boolean randomBoolean = random.nextBoolean();AI生成项目java运行(六)生成随机 float 值使用 nextFloat() 方法可以生成一个介于 0.0(包含)和 1.0(不包含)之间的随机单精度浮点数。Random random = new Random();float randomFloat = random.nextFloat();AI生成项目java运行(七)生成随机 long 值使用 nextLong() 方法可以生成一个介于 Long.MIN_VALUE 和 Long.MAX_VALUE 之间的随机长整数。Random random = new Random();long randomLong = random.nextLong();AI生成项目java运行二、Random 类的高级用法(一)生成指定范围内的随机数在实际开发中,我们常常需要生成指定范围内的随机数。以下是生成指定范围随机数的方法:Random random = new Random();// 生成 [min, max) 范围内的随机整数int min = 10;int max = 20;int randomInRange = random.nextInt(max - min) + min;// 生成 [min, max) 范围内的随机 double 值double minDouble = 1.0;double maxDouble = 10.0;double randomDoubleInRange = random.nextDouble() * (maxDouble - minDouble) + minDouble;AI生成项目java运行(二)生成随机字符串有时我们需要生成随机字符串,例如用于生成验证码或随机密码。以下是一个生成随机字符串的示例:import java.util.Random;public class RandomStringGenerator { private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; public static String generateRandomString(int length) { Random random = new Random(); StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { int index = random.nextInt(CHARACTERS.length()); sb.append(CHARACTERS.charAt(index)); } return sb.toString(); } public static void main(String[] args) { String randomString = generateRandomString(10); System.out.println("随机字符串: " + randomString); }}AI生成项目java运行(三)洗牌算法Random 类可以用于实现洗牌算法,随机打乱数组中的元素顺序。以下是一个使用 Random 的洗牌示例:import java.util.Random;public class ShuffleExample { public static void shuffleArray(int[] array) { Random random = new Random(); for (int i = array.length - 1; i > 0; i--) { int j = random.nextInt(i + 1); // 交换元素 int temp = array[i]; array[i] = array[j]; array[j] = temp; } } public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; System.out.println("原始数组:"); for (int num : array) { System.out.print(num + " "); } shuffleArray(array); System.out.println("\n洗牌后的数组:"); for (int num : array) { System.out.print(num + " "); } }}AI生成项目java运行三、Math.random() 的用法除了 Random 类之外,Java 还提供了一个更简单的随机数生成方法:Math.random()。它返回一个介于 0.0(包含)和 1.0(不包含)之间的随机双精度浮点数。double randomValue = Math.random();AI生成项目java运行如果需要生成指定范围内的随机数,可以结合 Math.random() 和一些简单的数学运算来实现。// 生成 [min, max) 范围内的随机整数int min = 10;int max = 20;int randomInt = (int) (Math.random() * (max - min)) + min;// 生成 [min, max) 范围内的随机 double 值double minDouble = 1.0;double maxDouble = 10.0;double randomDouble = Math.random() * (maxDouble - minDouble) + minDouble;AI生成项目java运行四、总结Random 类和 Math.random() 是 Java 中生成随机数的两种主要方式。Random 类提供了更丰富的功能,适用于需要生成多种类型随机数的场景;而 Math.random() 更加简洁,适用于简单的随机数生成需求。掌握这些随机数生成方法,可以在实际开发中灵活应用,为程序增添更多随机性和趣味性。希望本文的示例和讲解对您有所帮助,如果您在使用随机数时有任何疑问,欢迎随时交流探讨————————————————原文链接:https://blog.csdn.net/C_V_Better/article/details/146469452
-
一、什么是事务回滚?事务回滚指的是:当执行过程中发生异常时,之前对数据库所做的更改全部撤销,数据库状态恢复到事务开始前的状态。这是数据库“原子性”原则的体现。二、Spring 中的 @Transactional 默认行为在 Spring 中,使用注解方式开启事务非常简单:@Transactionalpublic void doSomething() { // 执行数据库操作}AI生成项目java运行此时的默认行为是:事务会在方法成功执行后提交;遇到 RuntimeException 或 Error,会自动回滚;遇到 Checked Exception(即编译时异常),不会自动回滚。例如:@Transactionalpublic void test1() { throw new RuntimeException(); // 会回滚}@Transactionalpublic void test2() throws Exception { throw new Exception(); // 不会回滚}AI生成项目java运行三、使用 rollbackFor 让事务回滚受检异常如果你希望事务在任何异常发生时都回滚,包括受检异常,比如 IOException、SQLException,就需要显式指定:@Transactional(rollbackFor = Exception.class)public void test3() throws Exception { throw new Exception(); // 会回滚}AI生成项目java运行rollbackFor 的值可以是一个或多个异常类;你可以根据需要选择只对某些异常类型回滚,其他的则不回滚。四、rollbackFor 和 rollbackOn 的区别特性 rollbackFor rollbackOn适用范围 Spring Java EE / JTA包名 org.springframework.transaction.annotation.Transactional javax.transaction.Transactional默认行为 回滚RuntimeException 不回滚任何异常明确配置后 可回滚任何指定异常 可回滚任何指定异常示例比较:Spring 中的写法:import org.springframework.transaction.annotation.Transactional;@Transactional(rollbackFor = Exception.class)public void springTransaction() throws Exception { throw new Exception("测试受检异常");}AI生成项目java运行JTA(Java EE)中的写法:import javax.transaction.Transactional;@Transactional(rollbackOn = Exception.class)public void jtaTransaction() throws Exception { throw new Exception("测试受检异常");}AI生成项目java运行注意:使用的是不同的注解类,不能混用!五、常见误区 误区1:以为所有异常都会触发事务回滚Spring 默认只回滚 RuntimeException,不会回滚 Exception(受检异常)。这是导致事务未回滚的最常见原因。 误区2:以为 @Transactional 可以应用于任何方法只有被 Spring 容器管理(即被 Spring 扫描并代理)的类中的 public 方法,@Transactional 才有效。如果你在 private 方法上加了注解,是不会生效的。 误区3:使用错误的注解类Spring 和 JTA 的 @Transactional 注解来自不同的包,使用时务必导入正确:Spring: org.springframework.transaction.annotation.TransactionalJTA: javax.transaction.Transactional六、小结常见问题与解决方式问题 默认行为 解决方式事务不回滚受检异常 不回滚 添加rollbackFor = Exception.class(Spring)或 rollbackOn = Exception.class(JTA)事务注解不生效 方法不是public,类未被 Spring 管理 保证类被 Spring 扫描,方法为public导入错误注解 使用了错误的@Transactional 注解 使用正确包名下的注解(见下表)Spring 与 JTA 的 @Transactional 对比特性 Spring JTA(Java EE)注解类全名 org.springframework.transaction.annotation.Transactional javax.transaction.Transactional默认回滚行为 回滚RuntimeException,不回滚 Exception 不回滚任何异常控制参数 rollbackFor, noRollbackFor 等 rollbackOn, dontRollbackOn常见场景 Spring Boot, Spring MVC 项目 Java EE, Jakarta EE 应用服务器项目建议用法 用 Spring 的事务注解为主 仅在 Java EE 项目中使用七、结语事务控制是保障系统数据一致性的重要手段,理解事务的回滚机制尤为重要。在实际开发中,推荐明确指定异常回滚策略,避免因受检异常不回滚而造成数据异常。希望这篇文章能帮你在开发中更精准地使用 @Transactional,写出更健壮、可控的代码。如果你有更多问题,欢迎留言讨论!————————————————原文链接:https://blog.csdn.net/huangge1199/article/details/148570326
-
使用Docker容器内安装的JDK进行Java 17开发与部署的完整指南引言在当今的软件开发领域,Docker已经成为容器化技术的标准,广泛应用于各种应用和服务的部署与管理。对于Java开发者来说,使用Docker来安装JDK(Java Development Kit)不仅提供了便捷和高效的环境配置方式,还能确保开发、测试和生产环境的一致性。本文将详细介绍如何在Docker容器内安装JDK,并进行Java项目的开发与部署。一、Docker与JDK的基本概念1. Docker简介Docker是一个开源的容器化平台,允许开发者将应用程序及其依赖项打包成一个可移植的容器。这使得应用程序可以在任何支持Docker的环境中无缝运行。2. JDK简介JDK(Java Development Kit)是Java开发的核心工具包,包含了Java运行时环境(JRE)、编译器、调试器以及其他工具。二、准备工作1. 安装Docker确保你的系统中已经安装了Docker。你可以从Docker官网下载并安装适合你操作系统的Docker版本。2. 选择JDK版本根据你的项目需求选择合适的JDK版本。本文将以JDK 17为例进行说明。三、拉取JDK镜像首先,我们需要从Docker Hub拉取JDK镜像。打开终端或命令提示符,执行以下命令:docker pull openjdk:17-jdk AI生成项目这条命令会从Docker Hub下载JDK 17的镜像文件。四、创建数据挂载目录为了方便管理和持久化数据,我们需要在宿主机上创建数据挂载目录。执行以下命令:mkdir -p /opt/project mkdir -p /opt/project/log AI生成项目这些目录将用于存放项目文件和日志。五、启动JDK容器并挂载项目接下来,我们将启动一个JDK容器,并将项目文件挂载到容器中。假设你的项目JAR文件名为server-1.0.0.jar,执行以下命令:docker run -d -p 8091:8091 -v /opt/project/server-1.0.0.jar:/myapp/server-1.0.0.jar --name myjavaapp openjdk:17-jdk AI生成项目这条命令的解释如下:-d:在后台运行容器。-p 8091:8091:将容器的8091端口映射到主机的8091端口。-v /opt/project/server-1.0.0.jar:/myapp/server-1.0.0.jar:将宿主机的/opt/project/server-1.0.0.jar文件挂载到容器的/myapp目录下。--name myjavaapp:为容器命名为myjavaapp。openjdk:17-jdk:使用我们之前拉取的JDK 17镜像。六、在容器中运行Java项目进入容器并运行你的Java项目。首先,获取容器的ID或名称:docker ps AI生成项目然后,使用以下命令进入容器:docker exec -it myjavaapp /bin/bash AI生成项目在容器内,导航到挂载的目录并运行你的JAR文件:cd /myapp java -jar server-1.0.0.jar AI生成项目七、Docker命令实用指南1. 镜像管理拉取镜像:docker pull openjdk:11构建镜像:docker build -t myapp:latest .列出镜像:docker images删除镜像:docker rmi myapp:latest2. 容器管理运行容器:docker run -d -p 8080:8080 --name myappcontainer myapp:latest列出容器:docker ps列出所有容器:docker ps -a启动容器:docker start myappcontainer停止容器:docker stop myappcontainer删除容器:docker rm myappcontainer八、使用Docker Compose进行多服务部署对于复杂的应用,可能需要多个服务协同工作。Docker Compose可以帮助你定义和运行多容器Docker应用程序。创建一个docker-compose.yml文件:version: '3' services: java-app: image: openjdk:17-jdk ports: - "8091:8091" volumes: - /opt/project:/myapp command: java -jar /myapp/server-1.0.0.jar mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: rootpassword AI生成项目然后,使用以下命令启动所有服务:docker-compose up -d AI生成项目九、监控与日志管理1. 查看容器日志使用以下命令查看容器的日志:docker logs myjavaapp AI生成项目2. 监控资源使用使用以下命令监控容器的资源使用情况:docker stats myjavaapp AI生成项目十、总结通过本文的介绍,你已经学会了如何在Docker容器内安装JDK,并进行Java项目的开发与部署。使用Docker不仅可以简化环境配置,还能提高应用的便携性和一致性。希望这些内容对你有所帮助,如果你有任何问题或经验分享,欢迎在评论区留言。————————————————原文链接:https://blog.csdn.net/m0_52647839/article/details/145807537
-
进程和线程的区别:1..进程是资源调度的基本单位。而线程是处理器实行调度的基本单位2.每个进程都有独立的内存资源,而线程之间的内存资源是共享的3.进程的调度开销要比线程的开销大很多,所以线程又被称作轻量级进程,调度开销要比进程低很多。4.进程的并发执行时当一个进程崩溃时5.线程上下文的的切换要比进程上下文的切换快得多 。总结根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位(也可以理解为进程当中的一条执行流程)资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行调度和切换:线程上下文切换比进程上下文切换要快得多。————————————————原文链接:https://blog.csdn.net/qq_53283658/article/details/127145029
-
1.进程的状态public class Test { public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()->{ }); System.out.println(t.getState()); //线程已经创建,但是没有开始执行,这个时候的状态就是new状态 t.start(); t.join(); //TERMINATED就是线程结束之后的这个线程的状态:terminated System.out.println(t.getState()); } }AI生成项目java运行 下面的这个就是在我们的t线程里面设计一个死循环,这个时候我们就可以使用getstate获取到这个时候的状态就是我们的runnable状态的; 下面的这个是对于timed-waiting状态的演示: 2.线程的安全引入线程的安全问题:主要就是这个线程调度的随机性; 下面的这个里面,我们是对于这个全局的静态变量是count=0,我们在这个主方法里面对于这个count分别加上50000次,这个时候我们正常情况下结果应该是100000,但是这个打印结果不是100000,主要就是因为这个调度器的执行问题导致的这个结果不是100000; 实际上,这个count进行++的时候,需要经过三个步骤,分别是 load:把内存里面的数据进行读取到CPU寄存器里面去; add:把这个寄存器里面的数据count++; save:把这个寄存器里面的计算之后的数据放到这个内存里面去; 为什么计算之后的这个结果不是100000呢,下面我们画一下这个计算的过程: 如果是下面的这个情况,我们的两个线程的三步骤都是连续的,这个时候我们的count就会被加上去,这个时候就是2; 但是如果下面的这个情况,就是两个线程之间的这个三步操作就会失效,只有一个会发挥作用,因为其中的一个过程被中断了: 3.线程安全的问题产生原因1.操作系统里面对于线程的调度是随机的(抢占式执行); 我们想要解决问题肯定不可以从这个入手,因为这个是取决于我们的操作系统,我们很难对于这个抢占 式执行的现状进行控制; 2.两个线程,针对于一个变量进行修改; 上面的这个就是属于这个情况,两个线程都是针对于这个count进行操作,因此这个时候因为这个调度执 行的步骤可能会被切断,因此这个时候就会出现问题; 3.修改操作不是原子的; 什么是原子:上面的这个count++就不是原子的,原子的简单的就可以理解为这个操作的步骤是一步到位 还是需要分为多次进行执行,上面的这个load,add,save需要分为三个步骤进行执行,因此这个就不是原 子的; 假设我们的这个步骤一步就可以完成,这个时候我们就把这个操作叫做原子的; 4.内存可见性问题; 5指令重排序问题;(4,5)我们暂时没有遇到,因此这个地方不进行过多的介绍; 4.synchronized关键字的引入4.1修饰代码块我们的这个synchronized实际上就是对于这个操作进行加锁的操作,只有我们的一个线程的三个步骤全部执行完毕之后,我们的另外一个线程才会被执行,相当于我们的t1对于这个全局的变量++的时候,这个就是出于上锁的状态,其他的线程无法进行操作,只有当我们的这个线程执行完毕之后,这个锁被释放掉,也就是开锁,这个时候我们的其他的线程才可以继续执行; 这样的操作保证了这个不同的线程之间的这个操作的独立性,就不会出现上面介绍的一个线程的三个步骤被另外一个线程打断,出现两个线程的操作交叉执行的问题;就是这个load,add,save就是各走各的,而且三个过程是连续的,不会被中断; 除了上面的这个synchronized修饰代码块之外,我们的这个synchronized还是可以修饰我们的静态方法和我们的实例方法的: 4.2修饰实例方法所谓的这个实例方法,其实就是为了和我们的静态方法进行区分,就是一个类里面的普通的成员方法,下面的这个就是我们的synchronized修饰我们的实例方法,下面的两个本质是等效的,因为这个synchronized修饰我们的实例方法本质上就是对于这个this锁对象进行操作,这个时候的锁对象就是我们的this; 因此这样来讲,上面的操作和我们的下面的这个修饰方法就是一样的,只不过我们的这个下面的写法里面,把这个锁对象隐藏了起来; 4.3修饰静态方法下面的两个写法就是一样的,就是我们的synchronized关键字修饰我们的静态成员方法,相当于这个代码块里面的这个参数就是我们的类对象; 我们的代码里面定义了一个类,那么这个里面就一定会有一个类对象,而且一个类只会有一个类对象,不会有多个的; public class Test { public static void main(String[] args) { synchronized public static void increase(){ } public static increase2(){ synchronized (counter.class){ } } }}AI生成项目java运行 4.4对象头介绍synchrinozed修饰的这个锁是存在于我们的这个对象头里面的,那么什么是对象头: 对象头就是我们进行这个对象的创建的时候,一个对象会有自己的内存空间,在这个内存空间里面,除了我们自己对于这个对象定义的属性,这个对象还会有些默认的属性,这个默认的属性就是在我们的对象头里面的; 在对象头里面,就有属性是存放说明我们的这个对象是不是加上了锁的; 4.5死锁-可重入的特性什么是可重入 ,就是对于一个对象,我们连续加锁,这个时候不会出现死锁的情况; 我们使用下面的这个案例对于死锁进行说明: 死锁就是像下面的这个情况一样,我们连续对哦与一个锁对象多次加锁,这个时候就会出现死锁,具体的讲就是线程被卡死了; synchronized(locker){synchronized(locker){.......}}AI生成项目java运行为什么会出现下面的这个死锁的情况,就是我们的第一次加锁的时候,我们的第二次操作正常情况下是进不去的,需要第一次的这个吧这个锁打开之后我们才可以第二次进入,但是下面的这个情况下我们的这个锁想要打开,只有等到这个操作执行完,就是这个代码块执行完,也就是执行到我们下面的这个示例代码的第二个}位置才可以,但是想要执行到这个第二个}位置,必须要执行这个第二次的加锁的操作,这个就是矛盾的地方; 因此这个时候想要加锁,但是这个执行又无法结束,因此这个时候就会出现线程卡死的情况,也就是我们说的死锁现象,为了处理这个问题,synchronized关键字引入了这个可重入的特性,就是对于这个相同的锁对象,我们可以重入,就是反复的入,也就是反复地加锁,这个是可以被允许的; 当然,这个死锁的现象是针对于这个相同的锁对象多次加锁,这个时候才可能会出现死锁的情况,如果每一次加锁针对的锁对象不是一样的,这个时候是不会出现我们的死锁现象的; synchronized(locker){ //下面的这个是针对于一个新的锁对象进行加锁,这个时候肯定不会出现死锁的情况,无论是不是可重入的synchronized(locker2){.......}}AI生成项目java运行那么,在这个可重入的特性下,我们的这个锁什么时候打开呢,正确答案是,直到所有的这个锁全部加上之后,直到我们的这个最外层大括号的时候,这个锁才会被打开; 具体到下面的这个情况,就是执行到最后一个}的时候,这个锁才会被打开,这个过程里面,我们会不断的进行计数,就是这个锁一共加上了几层,即n++,打开的时候,也会不断的对于这个n–直到这个走到最后一个}的时候,这个时候的n=0,也就是我们释放锁的时候; synchronized(locker){synchronized(locker){synchronized(locker){synchronized(locker){synchronized(locker){..........}}}}}AI生成项目 5.关于死锁的分析总结5.1死锁的分析1.一个对象,被连续两次上锁,这个时候如果是不可重入锁,就会发生死锁的现象; 2.两个对象,两把锁,这个时候无论是不是可重入的,都会发生这个死锁现象; 这个经典案例就是我们的钥匙落在了车里,车钥匙落在了家里,这个时候就会出现思索的现象; 这个时候家和车就是两个对象,我们的车钥匙和家钥匙就是锁,这个时候出现的情况就是我们的死锁的现象; 3.N个对象,M把锁,这个时候就是上面的两个对象两把锁的扩展,这个时候更加容易出现死锁的现象; 最经典的N歌对象,M把锁的问题就是我们的哲学家就餐问题: 这个时候我们是使用5个滑稽作为案例的,没有画出来很多,我们的滑稽在就餐的时候,每一个人都是拿走的自己的最近的一个筷子,这个时候,每一个人只有一个,谁都无法就餐,所有的人就是阻塞的状态,这个时候就会出现死锁的现象;(下面我们会介绍这个解决的方案); 5.2死锁成因的必要条件死锁的成因,需要满足下面的这四个条件,并且是同时满足的: 1.互斥使用(死锁的基本特性):当一个线程有一把锁之后,另外一个线程想要使用这个锁,需要进行阻塞等待; 2.不可抢占(死锁的基本特性):当一个锁被线程1拿到之后,线程2只能等待这个线程1主动地进行释放,否则只能处于等待的状态; 3.请求保持(代码结构):一个线程尝试获取多把锁,先拿到第一把锁之后,尝试获取第二把锁,获取这个锁的时候,第一把锁不会被释放; 4.循环等待/环路等待:等待之间的依赖关系,形成了换; 例如一个例子:钥匙锁在了车里,车钥匙锁在了家里;这个就是一个循环的环路等待,这个结果就是一个死循环,也是死锁的一个成因; 5.3死锁的解决方案解决死锁问题的核心就是要破坏上面的必要条件,但是这个里面的第一和第二个必要条件就是我们的锁的特性,因此这个不需要进行考虑,我们主要针对于3,4两个必要条件进行解决; 针对于3这个现象,我们需要进行这个代码结构的调整,不要把两个加锁的代码放到一个代码块里面去; 针对于4这个现象,我们需要进行编号操作,可以有效的解决这个问题: 还是使用这个哲学家的就餐问题,我们进行编号之后,让每一个滑稽取出来这个最小的编号的筷子(自己面前的两个筷子里面的最小的),这样的话,我们的问题就解决了; 我们可以分析一下这个就餐的过程,我们的拿筷子的情况如图所示,每一个人拿的都是自己的这个面前的两个里面的最小的编号,我们的最后一个滑稽取筷子的时候,1和5相比,肯定是1小,这个时候他就不可以取走这个5编号的筷子,这个时候的1已经是被和他相邻的这个滑稽取走了,因此这个时候,只能等待人家用完; 因此这个时候我们的左上角的这个滑稽就可以拿到这个5开始就餐,放下筷子之后,我们的左下角的这个滑稽拿到这个4号筷子吃饭,以此类推,直到我们的右上角的这个滑稽放下筷子,这个时候我们的最上面的这个滑稽就可以吃饭了,这个线程的死锁问题就被解决了; 定是1小,这个时候他就不可以取走这个5编号的筷子,这个时候的1已经是被和他相邻的这个滑稽取走了,因此这个时候,只能等待人家用完; 因此这个时候我们的左上角的这个滑稽就可以拿到这个5开始就餐,放下筷子之后,我们的左下角的这个滑稽拿到这个4号筷子吃饭,以此类推,直到我们的右上角的这个滑稽放下筷子,这个时候我们的最上面的这个滑稽就可以吃饭了,这个线程的死锁问题就被解决了———————————————— 原文链接:https://blog.csdn.net/binhyun/article/details/143165733
-
进程和线程进程进程:是正在执行的程序,是资源分配的基本单位,具有独立的地址空间操作系统会为其分配CPU和内存线程线程:引入线程是为了解决进程开销大,浪费资源的情景,并且多进程并发效率比较低线程是调度执行的基本单位线程之间会相互影响,一个线程挂了,会影响到整个进程都异常结束,线程也自然会结束进程和线程的区别进程包含线程,一个进程里面有多个线程或者是一个线程进程和线程都是用来实现并发编程场景的,但是线程比进程更轻量和高效同一个进程的线程之间共用同一份资源(内存和硬盘),省去了申请资源的开销进程和进程之间都是独立存在的,不会相互影响,同一个进程中,线程和线程之间会相互影响(线程安全问题 + 线程出现异常)进程是分配资源的基本单位,线程是调度执行的基本单位创建线程的五种写法继承Thread,重写runpackage Thread;class MyThread extends Thread{ public void run(){ // 这个是线程的入口方法 while(true) { System.out.println("hello Thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }}// 创建线程public class Demo1 { public static void main(String[] args) throws InterruptedException { Thread thread = new MyThread(); // 使用start方法可以间接调用run方法 // start和 run都时Thread的成员 // run 只是线程的入口(描述了线程要做什么事情) // start才是真正调用了系统的API,在系统中创建出了线程,让线程调用 run thread.start(); // 从这句开始程序就并发执行,一边执行hello main,一边执行hello Thread // 兵分两路进行执行 // 并发 == 并行 + 并发 while(true){ System.out.println("hello main!"); Thread.sleep(1000); } // 先执行main,再执行的是Thread,先执行主线程 }}AI生成项目java运行实现Runnable(接口),重写runpackage Thread;class MyRunable implements Runnable{ public void run(){ while(true){ System.out.println("hello thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }}public class Demo2 { public static void main(String[] args) throws InterruptedException { // 这个接口就是用来实现多态的 Runnable myRunable = new MyRunable(); Thread thread = new Thread(myRunable); thread.start(); while(true){ System.out.println("hello main!"); Thread.sleep(1000); } }}AI生成项目java运行继承Thread,重写run,但是使用匿名内部类使用匿名内部类的方式创建出线程package Thread;public class Demo3 { public static void main(String[] args) { Thread thread = new Thread(){ public void run(){ while(true){ System.out.println("hello Thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; thread.start(); while(true){ System.out.println("hello main!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }}AI生成项目java运行实现Runnable(接口),重写run,但是使用匿名内部类package Thread;public class Demo4 { public static void main(String[] args) { // 法一:创建实例 Runnable runnable = new Runnable(){ public void run(){ System.out.println("hello Thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; // 法二:创建匿名对象 Thread thread = new Thread(new Runnable(){ public void run(){ System.out.println("hello Thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); // Thread thread = new Thread(runnable); thread.start(); while(true){ System.out.println("hello main!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }}AI生成项目java运行使用lambda表达式lambda表达式相当于是匿名内部类的替换写法package Thread;public class Demo5 { public static void main(String[] args) { Thread thread = new Thread(()->{ while(true){ System.out.println("hello Thread!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); thread.start(); while(true){ System.out.println("hello main!"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }}AI生成项目java运行请说明Thread类中run和start的区别从方法的区别,及运行结果的区别分别说明1. start方法可以用来启动一个新的线程,run方法只是一个普通的方法,在主线程中执行2.start方法只能调用一次,run方法可以调用多次3.调用start方法会执行新的线程,新线程和主线程并发执行,run方法只是线程的入口,start方法调用了系统API,start方法创建出了线程,让线程再调用run方法4.run方法和主线程同步执行,start方法启动的线程和主线程异步执行5.run方法按顺序执行,start方法调用的线程中的代码执行顺序由线程调度器决定,顺序不确定————————————————原文链接:https://blog.csdn.net/2301_79722622/article/details/149054854
-
布隆过滤器(Bloom Filter)因其空间效率高、查询速度快的特点,被广泛应用于需要快速判断元素是否存在的场景,尤其适合允许一定误判率但不能漏判的情况。以下是其典型应用场景及具体案例:1. 缓存穿透防护问题恶意请求或高频查询不存在的数据(如 id=-1、未爬取的 URL),导致缓存和数据库均未命中,直接穿透到后端存储,引发性能雪崩。解决方案在缓存前加一层布隆过滤器,存储所有可能存在的键(如数据库主键、URL)。查询流程:先查布隆过滤器 → 若不存在,直接返回空结果(避免穿透)。若可能存在,再查缓存或数据库。案例电商系统:防止用户频繁查询不存在的商品 ID。爬虫系统:避免重复请求不存在的网页。2. URL 去重问题爬虫需要抓取海量网页,但需避免重复抓取同一 URL,否则浪费资源且可能陷入循环。解决方案用布隆过滤器存储已抓取的 URL,每次抓取前先检查是否存在。优势:相比哈希表或数据库,布隆过滤器节省大量内存(例如存储 1 亿 URL 仅需约 120MB 内存,误判率 1%)。案例Google 爬虫:早期使用布隆过滤器去重,后升级为更高效的变种(如 Counting Bloom Filter)。新闻聚合应用:避免重复抓取相同新闻源的链接。3. 垃圾邮件/恶意内容过滤问题需要快速判断邮件发送者、IP 或内容是否在黑名单中,但黑名单可能包含数百万条记录。解决方案用布隆过滤器存储黑名单(如垃圾邮件发送者的邮箱、恶意 IP),实时过滤请求。误判处理:若布隆过滤器返回“可能存在”,再通过精确查询(如数据库)确认。案例SpamAssassin:开源垃圾邮件过滤工具使用布隆过滤器加速黑名单检查。CDN 防护:拦截已知恶意请求的 IP。4. 区块链与加密货币问题区块链节点需要快速验证交易或地址是否有效,但全量存储所有地址不现实(如比特币地址约 2^160 个)。解决方案用布隆过滤器存储已使用的地址或交易哈希,节点间同步时过滤无效数据。比特币轻客户端:通过布隆过滤器向全节点请求与自己钱包相关的交易,减少数据传输量。案例Ethereum:使用布隆过滤器优化日志查询。Zcash:隐私交易中过滤无关数据。5. 数据库与大数据查询优化问题在大数据集(如用户行为日志、点击流)中查询某元素是否存在时,直接扫描全表效率极低。解决方案用布隆过滤器预过滤不可能存在的数据,减少磁盘 I/O 或网络传输。Hive/Spark:支持将布隆过滤器下推到存储层(如 HDFS),加速查询。案例Facebook:用布隆过滤器优化 HBase 的 Scan 操作,避免全表扫描。Elasticsearch:通过布隆过滤器加速 exists 查询。6. 推荐系统与用户行为分析问题需要快速判断用户是否已对某商品点赞、收藏或浏览过,但用户行为数据量巨大。解决方案用布隆过滤器存储用户行为记录(如 user_id + item_id 的哈希),实时去重或推荐。冷启动优化:对新用户快速生成近似行为画像。案例Netflix:用布隆过滤器记录用户观看历史,优化推荐算法。TikTok:实时过滤用户已刷过的视频。7. 分布式系统与一致性哈希问题分布式缓存(如 Redis Cluster)中,节点需要快速判断键是否属于本机分片,避免跨节点查询。解决方案用布隆过滤器存储本节点负责的键范围,减少哈希计算或网络开销。变种应用:结合一致性哈希环,优化数据迁移时的键分配。案例Twitter:用布隆过滤器加速 Timeline 服务的缓存查询。Akamai:CDN 节点用布隆过滤器路由请求到正确的边缘服务器。8. 网络安全与入侵检测问题需要实时检测网络流量中是否包含恶意签名(如病毒特征、攻击模式),但签名库可能包含数百万条规则。解决方案用布隆过滤器存储恶意签名,快速过滤正常流量,减少深度检测(DPI)的负载。误判处理:对布隆过滤器标记的可疑流量进行进一步分析。案例Snort:开源入侵检测系统使用布隆过滤器加速规则匹配。Cloudflare:用布隆过滤器拦截已知的 DDoS 攻击流量。9. 生物信息学与基因测序问题基因测序数据量极大(如人类基因组约 30 亿碱基对),需要快速查找特定序列是否存在。解决方案用布隆过滤器存储已知基因片段,加速测序数据的比对和分析。应用场景:疾病关联分析、进化树构建。案例BWA/Bowtie:主流基因比对工具使用布隆过滤器优化索引查询。COVID-19 测序:快速筛选病毒变异位点。10. 游戏开发与反作弊问题需要实时检测玩家行为是否异常(如外挂、刷分),但行为模式库可能包含大量规则。解决方案用布隆过滤器存储作弊行为特征(如特定按键序列、网络包模式),实时拦截可疑操作。动态更新:通过后台服务定期更新布隆过滤器的作弊规则。案例PUBG Mobile:用布隆过滤器检测外挂脚本。Steam:反作弊系统(VAC)使用布隆过滤器优化行为分析。总结:布隆过滤器的适用性场景是否适用原因需要快速判断存在性✔️查询时间复杂度 O(k),与数据量无关。允许一定误判率✔️误判率可通过调整参数控制(如 1% 或更低)。数据量极大(亿级以上)✔️空间效率远高于哈希表或数据库,内存占用低。不支持删除或动态更新❌(需变种)传统布隆过滤器不支持删除,可用 Counting Bloom Filter 或 Cuckoo Filter。需要零误判❌误判率不可为 0,需结合精确查询(如数据库)使用。推荐实践:单机场景:优先使用 Guava 的 BloomFilter 或 RedisBloom。分布式场景:结合 Redis、HBase 或自定义分片实现。高并发场景:注意布隆过滤器的线程安全性(Guava 实现是线程安全的)。通过合理应用布隆过滤器,可以显著提升系统性能,尤其在处理海量数据的场景下。
-
Java 中的布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于快速判断一个元素是否可能存在于集合中(可能存在误判,但绝不会漏判)。它特别适合处理大规模数据且允许一定误判率的场景,如缓存穿透防护、URL去重、垃圾邮件过滤等。一、布隆过滤器核心原理1. 数据结构位数组(Bit Array):初始时所有位为 0,长度通常为 m。哈希函数集合:k 个独立的哈希函数,每个函数将元素映射到位数组的某个位置。2. 操作流程添加元素:用 k 个哈希函数计算元素的 k 个位置。将这些位置的位全部设为 1。查询元素:用同样的 k 个哈希函数计算 k 个位置。如果所有位置均为 1,则元素可能存在;否则一定不存在。3. 误判率误判(False Positive):元素不在集合中,但布隆过滤器返回“可能存在”。误判率公式:[p \approx \left(1 - e^{-\frac{kn}{m}}\right)^k]n:已添加的元素数量。m:位数组长度。k:哈希函数数量。优化目标:通过调整 m 和 k,使误判率 p 最小化(通常 k ≈ m/n * ln2 时最优)。二、Java 实现方式1. Guava 的 BloomFilter(推荐)Google Guava 库提供了开箱即用的布隆过滤器实现,支持自动计算最优参数。依赖引入<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> <!-- 使用最新版本 --> </dependency> 代码示例import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.nio.charset.StandardCharsets; public class BloomFilterDemo { public static void main(String[] args) { // 1. 创建布隆过滤器:预期插入100万个元素,误判率控制在1%以内 BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 1_000_000, // 预期元素数量 0.01 // 误判率 ); // 2. 添加元素 bloomFilter.put("apple"); bloomFilter.put("banana"); // 3. 查询元素 System.out.println(bloomFilter.mightContain("apple")); // true System.out.println(bloomFilter.mightContain("orange")); // false(可能误判为true) // 4. 序列化(可选) // byte[] bytes = bloomFilter.toByteArray(); // BloomFilter<String> restored = BloomFilter.create( // Funnels.stringFunnel(StandardCharsets.UTF_8), bytes); } } 关键参数说明Funnels.stringFunnel():指定元素类型(如 String、Long 等)。expectedInsertions:预期插入的元素数量(需预估准确,否则影响误判率)。fpp(False Positive Probability):误判率(值越小,位数组越大)。2. Redis 的布隆过滤器(分布式场景)如果需要在分布式系统中使用布隆过滤器,可以通过 Redis 模块 RedisBloom 实现。依赖引入<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.3</version> </dependency> <dependency> <groupId>io.rebloom</groupId> <artifactId>rebloom</artifactId> <version>2.2.0</version> </dependency> 代码示例import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; public class RedisBloomFilterDemo { public static void main(String[] args) { try (Jedis jedis = new Jedis("localhost", 6379)) { // 1. 创建布隆过滤器(key="my_bloom", 预期元素数=1000, 误判率=0.01) jedis.bfReserve("my_bloom", 0.01, 1000); // 2. 添加元素 jedis.bfAdd("my_bloom", "apple"); jedis.bfAdd("my_bloom", "banana"); // 3. 查询元素 System.out.println(jedis.bfExists("my_bloom", "apple")); // true System.out.println(jedis.bfExists("my_bloom", "orange")); // false } } } 三、布隆过滤器的应用场景缓存穿透防护问题:恶意请求查询不存在的数据(如 id=-1),导致缓存和数据库均未命中。解决方案:在缓存前加一层布隆过滤器,若元素不存在则直接返回空结果。URL去重爬虫系统中快速判断 URL 是否已抓取。垃圾邮件过滤判断邮件发送者是否在黑名单中(允许少量误判)。区块链地址校验快速检查地址是否有效(避免全量存储)。四、布隆过滤器的局限性不支持删除操作传统布隆过滤器无法删除元素(需使用变种如 Counting Bloom Filter)。误判率不可为 0需根据业务容忍度调整参数。空间效率与误判率的权衡误判率越低,位数组越大(例如 1% 误判率约需 9.6 位/元素)。五、替代方案对比方案查询时间空间效率支持删除误判率布隆过滤器O(k)高❌可控布谷鸟过滤器O(k)更高✔️更低哈希表O(1)低✔️0Redis 集合O(1)中✔️0选择建议:需要极致空间效率且允许误判 → 布隆过滤器。需要支持删除 → 布谷鸟过滤器或 Redis 集合。需要零误判 → 哈希表或 数据库。总结单机场景:优先使用 Guava 的 BloomFilter,简单高效。分布式场景:选择 RedisBloom 或自研基于 Redis 的布隆过滤器。关键参数:根据预期元素数量和误判率调整 m 和 k(Guava 已自动优化)。通过合理使用布隆过滤器,可以显著提升系统性能,尤其在处理海量数据的场景下。
-
对象的构造及初始化5.1 如何初始化对象通过前面知识点的学习我们知道,在Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。 public static void main(String[] args) {int a;System.out.println(a);}// Error:(26, 28) java: 可能尚未初始化变量aAI生成项目java运行 要让上述代码通过编译,非常简单,只需在正式使用变量之前给它设置初始值即可。 public static void main(String[] args) { Date d = new Date(); d.printDate(); d.setDate(2021,6,9); d.printDate();}// 代码可以正常通过编译AI生成项目java运行 如果是对象,就需要调用之前写的 setDate 方法将具体的日期设置到对象中。 通过上述例子我们发现两个问题: 问题1:每次对象创建好后调用 setDate 方法设置具体日期显得比较麻烦,那么对象该如何初始化?问题2:局部变量必须初始化才能使用,而字段声明之后没有给值依然可以使用,这是因为字段具有默认初始值。为了解决问题1,Java引入了 构造方法,使得对象在创建时就能完成初始化操作。 5.2 构造方法构造方法(也称为构造器)是一种特殊的成员方法,其主要作用是初始化对象。 5.2.1 构造方法的概念public class Date { public int year; public int month; public int day; // 构造方法: // 名字与类名相同,没有返回值类型,设置为void也不行 // 一般情况下使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day){ this.year = year; this.month = month; this.day = day; System.out.println("Date(int,int,int)方法被调用了"); } public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021,6,9); // 输出Date(int,int,int)方法被调用了 d.printDate(); // 2021-6-9 }}AI生成项目java运行 构造方法的特点是: 名字必须与类名相同,且没有返回值类型(连void都不行)。在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次。注意:构造方法的作用是对对象中的成员进行初始化,并不负责给对象开辟内存空间。5.2.2 构造方法的特性构造方法具有如下特性: 名字必须与类名完全相同没有返回值类型,即使设置为void也不行创建对象时由编译器自动调用,且在对象生命周期内只调用一次(就像人的出生,每个人只能出生一次)支持重载:同一个类中可以定义多个构造方法,只要参数列表不同即可示例代码1:带参构造方法 public class Date { public int year; public int month; public int day; // 构造方法:名字与类名相同,没有返回值类型,使用public修饰 // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; System.out.println("Date(int, int, int)方法被调用了"); } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { // 此处创建了一个Date类型的对象,并没有显式调用构造方法 Date d = new Date(2021, 6, 9); // 输出:Date(int, int, int)方法被调用了 d.printDate(); // 输出:2021-6-9 }}AI生成项目java运行 示例代码2:无参构造方法 public class Date { public int year; public int month; public int day; // 无参构造方法:给成员变量设置默认初始值 public Date() { this.year = 1900; this.month = 1; this.day = 1; }}AI生成项目java运行 上述两个构造方法名字相同但参数列表不同,构成了方法的重载。 如果用户没有显式定义构造方法,编译器会生成一个默认的无参构造方法。注意:一旦用户显式定义了构造方法,编译器就不会再生成默认构造方法了 示例代码3:仅定义带参构造方法时默认构造方法不会生成 // 带有三个参数的构造方法public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day;} public void printDate(){ System.out.println(year + "-" + month + "-" + day);} public static void main(String[] args) { Date d = new Date(); // 编译期报错,因为没有无参构造方法 d.printDate();}AI生成项目java运行 示例代码4:只有无参构造方法的情况 public class Date { public int year; public int month; public int day; public void printDate(){ System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); }}AI生成项目java运行 示例代码5:只有带参构造方法的情况 public class Date { public int year; public int month; public int day; public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}AI生成项目java运行 构造方法中可以通过 this(…) 调用其他构造方法来简化代码。注意:this(…) 必须是构造方法中的第一条语句,否则编译器会报错。示例代码6:正确使用this(…)实现构造器链 public class Date { public int year; public int month; public int day; // 无参构造方法 -- 内部调用带参构造方法实现初始化 // 注意:this(1900, 1, 1);必须是构造方法中的第一条语句 public Date(){ // System.out.println(year); // 若取消注释则编译会失败 this(1900, 1, 1); // 以下赋值代码被省略,因为已在带参构造方法中完成初始化 // this.year = 1900; // this.month = 1; // this.day = 1; } // 带有三个参数的构造方法 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; }}AI生成项目java运行 注意:构造方法中的 this(…) 调用不能形成循环,否则会导致编译错误。 public Date(){this(1900,1,1);} public Date(int year, int month, int day) {this();}/*无参构造器调用三个参数的构造器,而三个参数构造器有调用无参的构造器,形成构造器的递归调用编译报错:Error:(19, 12) java: 递归构造器调用*/AI生成项目java运行 在大多数情况下,我们使用 public 来修饰构造方法,但在特殊场景下(如实现单例模式)可能会使用 private 修饰构造方法。5.3 默认初始化在上文中提到的第二个问题:为什么局部变量在使用前必须初始化,而成员变量可以不初始化? 要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情: Date d = new Date(2021,6,9);AI生成项目java运行1在程序员看来只是一句简单的语句,但 JVM 层面需要做好多事情。下面简单介绍下: 检测对象对应的类是否被加载,如果没有则加载为对象分配内存空间并先默认初始化处理并执行类中的 init 方法初始化分配好的空间 (说明:多个线程同时申请资源,JVM 要保证分配给对象的空间内干净。)即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:Java中的 成员变量 会由 JVM 自动赋予默认值(如int类型为0,boolean类型为false等),而局部变量则需要显式初始化才能使用。 Java为不同的数据类型提供了默认值,具体如下所示: 数据类型 默认值byte 0char ‘\u0000’short 0int 0long 0Lboolean falsefloat 0.0fdouble 0.0dreference null这些默认值确保了即使对象的成员变量没有显式初始化,也不会发生错误,成员变量会有一个初始的稳定状态。 设置对象头信息(关于对象内存模型后面会介绍) 调用构造方法,给对象中各个成员赋值 5.4 就地初始化就地初始化是指在成员变量声明时直接为它们赋初值。这种方法在代码的简洁性上具有优势,可以避免每次创建对象时重复设置成员变量的值。 代码示例: public class Date { public int year = 1900; // 就地初始化 public int month = 1; // 就地初始化 public int day = 1; // 就地初始化 public Date() { // 构造方法在此处不需要再次初始化year、month、day,它们已经有默认值 } public void printDate() { System.out.println(year + "-" + month + "-" + day); } public static void main(String[] args) { Date d = new Date(); d.printDate(); // 输出:1900-1-1 }}AI生成项目java运行 在这个例子中,year、month 和 day 的值在声明时就被初始化为1900、1和1,确保了每次创建对象时这些值已经存在。 六、封装6.1 封装的概念封装是面向对象编程的三大特性之一,其核心思想是将数据和操作数据的方法结合在一起,并对外隐藏实现细节。例如,电脑作为一个复杂的设备,用户只需通过开关、键盘和鼠标等接口进行交互,而不必关心内部CPU、显卡等工作原理。 简单来说就是套壳屏蔽细节。 6.2 访问限定符访问修饰符说明 public:可以理解为一个人的外部接口,能被外部访问。protected:主要是给继承使用,子类继承后就能访问到。default(不写修饰符时即为默认访问修饰符):对于同一包内的类可见,不同包则不可见。private:只能在当前类中访问。(这部分要介绍完继承后才能完全理解) 【说明】protected 主要是给继承使用;default 只能给同一个包内使用;按照自己的理解去记忆。 示例代码: public class Computer { private String cpu; private String brand; private String memory; private String screen; public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void boot() { System.out.println("开机"); } public void shutDown() { System.out.println("关机"); }}AI生成项目java运行 public class TestComputer { public static void main(String[] args) { Computer c = new Computer("华为", "i9", "16G", "4K"); c.boot(); c.shutDown(); }}AI生成项目java运行 注意:一般情况下,成员变量通常设置为 private,成员方法设置为 public。 6.3 封装扩展之包6.3.1 包的概念包(Package)用于对类进行分组管理,有助于解决类名冲突并提高代码的组织性。例如,将相似功能的类归为同一包。 为了更好的管理类,把多个类收集在一起成为一组,称为软件包 6.3.3导入包在Java中,如果我们需要使用不在默认 java.lang 包中的类或接口,就需要使用 import 关键字来导入。例如,我们想使用 java.util.Date 这个类,就可以这样做: import java.util.Date; public class TestImport { public static void main(String[] args) { Date d = new Date(); System.out.println(d); }}AI生成项目java运行 这样就可以正常创建 Date 对象并使用。 6.3.3全类名当不同包中存在同名的类时(如 java.util.Date 与 java.sql.Date 都叫 Date),可能会引发冲突。这时,可以使用 全类名(Fully Qualified Name)来指定使用哪个类: public class TestFullName { public static void main(String[] args) { // 使用全类名来区分两个Date类 java.util.Date d1 = new java.util.Date(); java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis()); System.out.println(d1); System.out.println(d2); }}AI生成项目java运行 通过在创建对象时加上包名,就可以区分来自 java.util 和 java.sql 的 Date 类。 6.3.4 静态导入从 Java 5 开始,支持使用 静态导入(import static)的方式将某个类中的 静态成员(常量或方法) 导入到当前类中,从而在调用时可以省略类名。 示例代码: import static java.lang.Math.PI;import static java.lang.Math.random; public class TestStaticImport { public static void main(String[] args) { System.out.println(PI); // 直接使用PI常量 System.out.println(random()); // 直接使用random()方法 }}AI生成项目java运行 如果不使用静态导入,则需要写成: System.out.println(Math.PI);System.out.println(Math.random());AI生成项目java运行 6.3.5 IDE工具中的包结构当我们使用 IntelliJ IDEA 或 Eclipse 等 IDE 工具时,会在项目结构中直观地看到包名与文件夹一一对应。 包名一般使用 小写 的域名反写形式(如 com.example.project),在 IDE 中会对应层级文件夹结构。在同一个包下,可以放置多个类文件,便于组织与管理。在实际开发中,合理划分包结构能让项目更易于维护和理解。 6.3.6 包的访问权限控制举例Computer类位于com.bit.demo1包中,TestComputer位于com.bit.demo2包中: package com.bit.demo1; public class Computer { private String cpu; // cpu private String memory; // 内存 public String screen; // 屏幕 String brand; // 品牌 public Computer(String brand, String cpu, String memory, String screen) { this.brand = brand; this.cpu = cpu; this.memory = memory; this.screen = screen; } public void PowerOff(){ System.out.println("关机~~~"); } public void SurfInternet(){ System.out.println("上网~~~"); }}AI生成项目java运行 ////////////////////////////////////package com.bit.demo2; import com.bit.demo1.Computer; public class TestComputer { public static void main(String[] args) { Computer p = new Computer("HW", "i7", "8G", "13*14"); System.out.println(p.screen); // 公有属性,可以被其他包访问 // System.out.println(p.cpu); // 私有属性,不能被其他包访问 // System.out.println(p.brand); // brand是default,不允许被其他包中的类访问 }}AI生成项目java运行 注意:如果去掉前面的Computer类中的public修饰符,代码也会编译失败。 6.3.7 常见的包java.lang:系统常用基础类(String,Object),此包从JDK1.1后自动导入。java.lang.reflect:Java反射机制包;java.net:进行网络编程开发包;java.sql:进行数据库开发的包;javax.util:Java提供的工具程序包(集合类等)非常重要;javalio.io:编程程序包。注意事项: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.import 只是为了写代码的时候更方便. 区别如下: Java 的 import:仅在编译时为你提供类或接口的简写路径,使你可以直接使用类名而不用写出完整的包名。实际上,编译器会在编译过程中通过类路径(classpath)去查找相应的类文件,而不会把代码插入到当前文件中。 C/C++ 的 #include:预处理器会在编译之前将头文件的内容直接拷贝进源代码文件中,这种方式实际上是将文件的内容“粘贴”到包含它的文件里。 因此,Java 的 import 只是一种简化引用的机制,并不涉及代码的复制。 七、总结与展望在本篇文章中,我们围绕对象的构造与初始化、封装、包的管理以及访问权限等内容展开了详细讲解,帮助大家深入理解Java面向对象编程的核心概念。 7.1 总结对象的构造与初始化 构造方法:通过构造方法,我们可以在创建对象时立即为其成员赋值,保证对象在使用前处于有效状态。构造器重载与this调用:支持多个构造方法以及构造器链,让对象初始化更加灵活和简洁。默认初始化与就地初始化:成员变量会自动获得默认值,同时也可以在声明时直接赋值,避免重复代码。封装 核心思想:将数据与操作数据的方法绑定在一起,通过访问限定符(public、private、protected、default)隐藏内部实现细节。访问控制:合理使用访问修饰符保护数据安全,提供公开接口供外部使用,增强了程序的健壮性和安全性。包的管理与导入 包的概念:通过将相关类归为一组,实现代码的模块化管理和命名空间隔离,避免类名冲突。import语句:在编译时提供类名简写路径,与C/C++的#include机制不同,import不进行代码复制,仅起到标识和简化引用的作用。7.2 总结深入static成员在后续的内容中,我们将更详细地探讨static关键字的使用,包括静态变量、静态方法及其在类中的意义,帮助大家更好地理解类级别的共享特性。 代码块与初始化块将介绍类中的初始化块和静态代码块,讨论它们在对象创建过程中的执行顺序和作用,为进一步掌握对象生命周期奠定基础。 内部类与匿名类内部类是一种特殊的类定义方式,它能够更紧密地绑定外部类的成员,将在未来篇章中深入剖析其用法和设计思想。 面向对象的设计原则随着对类和对象理解的深入,我们也将探讨更多面向对象设计的原则和模式,帮助大家构建更健壮、可维护的Java应用程序———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146015106
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签