• [技术干货] 「JavaScript深入」Canvas 全面指南
    1. Canvas 基础概念什么是 Canvas?HTML5 提供了 canvas元素,这是一个空白的矩形区域,可以使用 JavaScript 在上面绘制图形、图像和文本<canvas id="myCanvas" width="500" height="300"></canvas>获取 Canvas 绘图上下文要在 Canvas 上绘图,首先需要获取绘图上下文(context)。Canvas 支持不同的绘图上下文类型:2D 上下文 (CanvasRenderingContext2D):用于绘制 2D 图形,支持路径绘制、填充、描边、文本绘制、图像处理等。WebGL (WebGLRenderingContext):用于 3D 图形渲染,基于 OpenGL ES。示例:获取 2D 上下文<canvas id="myCanvas" width="500" height="300"></canvas><script>  const canvas = document.getElementById("myCanvas");  const ctx = canvas.getContext("2d"); // 获取 2D 绘图上下文</script>2. 基本绘图操作绘制矩形ctx.fillStyle = "red"; // 设置填充颜色ctx.fillRect(50, 50, 100, 100); // 绘制填充矩形ctx.strokeStyle = "blue"; // 设置描边颜色ctx.strokeRect(200, 50, 100, 100); // 绘制描边矩形效果绘制路径(线段)ctx.beginPath(); // 开始路径ctx.moveTo(50, 200); // 起点ctx.lineTo(150, 250); // 第一条线ctx.lineTo(250, 200); // 第二条线ctx.closePath(); // 关闭路径ctx.stroke();  // 绘制路径 效果绘制圆形ctx.beginPath();ctx.arc(150, 150, 50, 0, Math.PI * 2);  // 绘制一个圆弧路径ctx.fill(); // 填充路径ctx.arc(150, 150, 50, 0, Math.PI * 2)绘制一个圆弧路径。150, 150:圆心的坐标(x, y)。50:圆的半径。0:起始角度(弧度制),0表示从3点钟方向开始。Math.PI * 2:结束角度,Math.PI * 2表示完整的360度,即一个完整的圆。效果3. 图像与文本绘制绘制图片Canvas 可以绘制图片,通过 drawImage 方法将图片绘制到 Canvas 上const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');const img = new Image();img.src = 'http://gips1.baidu.com/it/u=1658389554,617110073&fm=3028&app=3028&f=JPEG&fmt=auto'; // 替换为实际图像 URLimg.onload = () => {  // 绘制原始大小  ctx.drawImage(img, 0, 0);    // 绘制缩放后的图片  ctx.drawImage(img, 50, 50, 128, 96);  // (image, x, y, width, height)}; 效果绘制文本通过 fillText 和 strokeText 方法,可以在 Canvas 上绘制文本。const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');// 设置字体ctx.font = "30px Arial";ctx.fillStyle = '#FF0000';ctx.fillText("Hello, Canvas!", 50, 50);ctx.strokeText("Outlined Text", 50, 100);效果4. Canvas 动画基础动画实现Canvas 动画通常通过 requestAnimationFrame 进行帧更新。在 requestAnimationFrame 周期内,通过清除先前的 Canvas 内容并绘制新内容,可以实现流畅的动画效果const canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');let posX = 0;const posY = 150;function animate() {  // 清除画布  ctx.clearRect(0, 0, canvas.width, canvas.height);  // 绘制移动的方块  ctx.fillStyle = '#FF5733';  ctx.fillRect(posX, posY, 50, 50);  // 更新位置  posX  += 2;  // 重置位置  if (posX > canvas.width) posX = -50;   // 循环调用  requestAnimationFrame(animate);}animate();  // 启动动画5. Canvas 性能优化在使用 Canvas 进行绘图和动画时,性能优化尤为重要,尤其是在处理复杂图形和高频率动画时1. 减少重绘次数尽量只更新变化部分,避免整屏重绘。使用 requestAnimationFrame 来协调动画更新,避免不必要的渲染。function draw() {  // 仅在需要时重绘  if (needsRedraw) {    ctx.clearRect(0, 0, canvas.width, canvas.height);    // 执行绘图操作    needsRedraw = false;  }  requestAnimationFrame(draw);}2. 使用离屏 Canvas利用离屏 Canvas 进行预绘,然后将结果绘制到主 Canvas 上,减少主线程的计算压力,提升渲染效率// 创建离屏 Canvasconst offscreenCanvas = document.createElement("canvas");const offCtx = offscreenCanvas.getContext("2d");// 在离屏 Canvas 上绘制offCtx.fillStyle = 'red';offCtx.fillRect(0, 0, 100, 100);// 将离屏 Canvas 的内容绘制到主 Canvasconst canvas = document.getElementById('myCanvas');const ctx = canvas.getContext('2d');ctx.drawImage(offscreenCanvas, 50, 50);3. 使用多个层 Canvas将复杂的静态背景和动态元素分层绘制,可以减少需要频繁更新的绘图区域,提升整体渲染效率<div style="position: relative;">  <!-- 静态背景 Canvas -->  <canvas id="bgCanvas" width="600" height="400" style="position: absolute; z-index: 0;"></canvas>  <!-- 动态元素 Canvas -->  <canvas id="fgCanvas" width="600" height="400" style="position: absolute; z-index: 1;"></canvas></div><script>  const bgCanvas = document.getElementById('bgCanvas');  const bgCtx = bgCanvas.getContext('2d');  // 绘制静态背景  bgCtx.fillStyle = '#EEEEEE';  bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);  const fgCanvas = document.getElementById('fgCanvas');  const fgCtx = fgCanvas.getContext('2d');  let posX = 0;  const posY = 200;  function animate() {    fgCtx.clearRect(0, 0, fgCanvas.width, fgCanvas.height);    fgCtx.fillStyle = '#FF0000';    fgCtx.fillRect(posX, posY, 50, 50);    posX += 2;    if (posX > fgCanvas.width) posX = -50;    requestAnimationFrame(animate);  }  animate();</script>6. Canvas VS SVGSVG(Scalable Vector Graphics)是一种用来描述二维矢量图形的XML格式。它是一种基于文本的图像格式,支持交互和动画,广泛用于网页设计和开发中。在选择 Canvas 或 SVG 技术进行项目开发时,需要考虑到它们各自的特性、适用场景和项目需求Canvas 是基于像素的技术,适用于生成即时图形,在处理大量对象(如游戏中的图形)时,Canvas 通常表现得更好,因为它直接操作位图。但每次绘制都是对整个 Canvas 的再绘制,无法直接对单个元素进行操作SVG 是矢量图形技术,缩放时不会失去清晰度,适合高保真度的图像展示,每个元素都是独立可操作的 DOM 元素,可以添加事件、样式和脚本,但对于复杂图形,DOM 节点数可能较大特性    Canvas    SVG绘制方式    基于像素的位图绘制    基于矢量的图形绘制适用场景    高性能实时渲染、大量动态图形、游戏等    需要可缩放、交互性高的静态图形、复杂的布局与样式DOM 结构    单一 <canvas>,绘图内容不在 DOM 中    每个元素都是 DOM 节点,易于操作和样式化性能    适合大量图形和高频率更新,性能较高    动态元素较多时性能可能下降,特别是复杂的 SVG 图形可访问性    需要额外处理,默认不可访问    元素可被屏幕阅读器等辅助技术识别,具备更好的可访问性可缩放性    失真    适用于缩放动画与交互    需要手动实现动画和交互逻辑    支持 CSS 动画、SVG 动画和事件处理如何选择?动态动画(游戏、数据可视化)➡ Canvas可缩放的矢量图(图标、交互 UI)➡ SVG本文提供了 Canvas 的核心知识点、基础绘图、动画处理及优化技巧。希望对你有所帮助!————————————————原文链接:https://blog.csdn.net/XH_jing/article/details/146223643
  • [技术干货] JavaScript 中的 var、let 和 const 详解
    一、var 声明(一)定义与基本用法var 是 JavaScript 中较早期用于声明变量的关键字。使用 var 声明变量非常简单,只需要在变量名前加上 var 关键字即可。例如:var age; age = 25; // 或者可以在声明时直接赋值 var name = "John";(二)作用域var 声明的变量具有函数作用域或全局作用域。这意味着在函数内部使用 var 声明的变量,在整个函数内部都是可访问的,但在函数外部无法访问。例如:function exampleFunction() { var localVar = "I'm a local variable"; console.log(localVar); // 输出: I'm a local variable } console.log(localVar); // 这里会报错,因为 localVar 在函数外部不可访问如果在全局作用域(即不在任何函数内部)使用 var 声明变量,该变量将成为全局对象(在浏览器环境中是 window,在 Node.js 环境中是 global)的属性。例如:var globalVar = "I'm a global variable"; console.log(window.globalVar); // 输出: I'm a global variable(三)声明提升var 声明存在一个重要特性 —— 声明提升。这意味着在函数或全局作用域内,无论 var 声明的变量出现在何处,其声明都会被提升到作用域的顶部,但是赋值操作不会被提升。例如:console.log(num); // 输出: undefined var num = 10; //上述代码等价于:  var num; console.log(num); // 输出: undefined num = 10;这种声明提升可能会导致一些意想不到的结果,特别是在代码结构复杂时。例如:function hoistingExample() { console.log(x); // 输出: undefined if (false) { var x = 10; } console.log(x); // 输出: undefined } hoistingExample();在这个例子中,虽然 if 块中的代码不会执行,但由于 var 的声明提升,x 的声明仍然被提升到函数顶部,所以第一次 console.log(x) 输出 undefined。而第二次输出 undefined 是因为 if 块内的赋值操作没有执行。(四)重复声明使用 var 可以对同一个变量进行多次声明,后面的声明会被忽略(但如果有赋值操作,会覆盖之前的值)。例如:var message = "Hello"; var message; console.log(message); // 输出: Hello var count = 5; var count = 10; console.log(count); // 输出: 10二、let 声明(一)定义与基本用法let 是 ES6 引入的用于声明变量的关键字。它的基本用法与 var 类似,在变量名前加上 let 即可声明变量。例如:let age; age = 30; // 或者声明时直接赋值 let name = "Jane";(二)作用域let 声明的变量具有块级作用域。块级作用域由一对花括号 {} 定义,包括 if 语句块、for 循环块、while 循环块等。在块级作用域内使用 let 声明的变量,仅在该块级作用域内有效。例如:if (true) { let localVar = "I'm a block - level local variable"; console.log(localVar); // 输出: I'm a block - level local variable } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问与 var 的函数作用域相比,块级作用域更加精细,能更好地控制变量的生命周期和作用范围,减少变量污染全局作用域的风险。(三)不存在声明提升与暂时性死区let 声明不存在像 var 那样的声明提升。在使用 let 声明变量之前访问该变量会导致 ReferenceError 错误,这被称为 “暂时性死区”(TDZ)。在代码执行到 let 声明语句之前,该变量就已经存在于其作用域中了,但处于一种 “不可用” 的状态。只有当执行流到达声明语句时,变量才会被初始化,从而可以正常使用。例如:console.log(age); // 报错: ReferenceError: age is not defined let age = 28;在 let age = 28; 这行代码之前,age 处于暂时性死区,任何对它的访问都会触发错误。暂时性死区的存在,实际上是 JavaScript 引擎在解析代码时的一种机制。当遇到 let 声明时,引擎会在作用域中为该变量创建一个绑定,但此时变量处于未初始化状态。只有执行到声明语句本身时,变量才会被初始化并可以正常使用。这一特性使得开发者在编写代码时,对于变量的声明和使用顺序更加清晰,避免了因变量提升而导致的一些难以调试的问题。(四)不能重复声明在同一作用域内,使用 let 重复声明同一个变量会导致 SyntaxError 错误。例如:let count = 5; let count = 10; // 报错: SyntaxError: Identifier 'count' has already been declared这种限制有助于避免变量声明冲突,使代码更加清晰和可维护。三、const 声明(一)定义与基本用法const 同样是 ES6 引入的关键字,用于声明常量。常量一旦声明,其值就不能再被修改。声明常量的方式与 var 和 let 类似,在常量名前加上 const,并且必须在声明时进行初始化赋值。例如:const PI = 3.14159; const MAX_COUNT = 100;(二)作用域const 声明的常量具有块级作用域,与 let 相同。在块级作用域内声明的常量,仅在该块级作用域内有效。例如:if (true) { const localVar = "I'm a constant in a block"; console.log(localVar); // 输出: I'm a constant in a block } console.log(localVar); // 这里会报错,因为 localVar 在块外部不可访问(三)值的不可变性const 声明的常量值不能被重新赋值。尝试对常量重新赋值会导致 TypeError 错误。例如:const PI = 3.14159; PI = 3.14; // 报错: TypeError: Assignment to constant variable.需要注意的是,对于对象和数组类型的常量,虽然不能重新赋值整个对象或数组,但可以修改其内部属性或元素。例如:const person = { name: "Alice", age: 32 }; person.name = "Bob"; // 合法,对象属性可以修改 console.log(person.name); // 输出: Bob const numbers = [1, 2, 3]; numbers.push(4); // 合法,数组元素可以修改 console.log(numbers); // 输出: [1, 2, 3, 4]如果想要确保对象或数组的内容也不可变,可以使用 Object.freeze() 方法。例如:const frozenPerson = Object.freeze({ name: "Charlie", age: 25 }); frozenPerson.name = "David"; // 虽然不会报错,但实际上属性值并未改变 console.log(frozenPerson.name); // 输出: Charlie(四)不存在声明提升与暂时性死区与 let 一样,const 声明也不存在声明提升。在使用 const 声明常量之前访问该常量会导致 ReferenceError 错误,同样存在暂时性死区。在常量声明语句之前,该常量虽然在作用域中已经有了绑定,但处于未初始化状态,无法被访问和使用。例如:console.log(MAX_COUNT); // 报错: ReferenceError: MAX_COUNT is not defined const MAX_COUNT = 200;当代码执行到 const MAX_COUNT = 200; 时,常量 MAX_COUNT 才被初始化并可以正常使用。这与 let 声明的暂时性死区原理一致,都是为了让代码在变量(常量)的声明和使用上更加规范和可预测。四、var、let 和 const 的区别(一)作用域var:具有函数作用域或全局作用域。在函数内部声明的 var 变量在整个函数内有效,在全局作用域声明的 var 变量成为全局对象的属性。let:具有块级作用域。在块级作用域(如 if 块、for 循环块等)内声明的 let 变量仅在该块内有效,能更好地控制变量的作用范围,减少变量污染。const:同样具有块级作用域,与 let 类似,在声明它的块级作用域内有效。(二)声明提升var:存在声明提升,变量声明会被提升到作用域顶部,但赋值操作不会提升。这可能导致在变量声明之前访问它时得到 undefined 值,从而引发一些不易察觉的错误。let:不存在声明提升,在声明变量之前访问会导致 ReferenceError 错误,存在暂时性死区,使得代码在变量声明之前无法访问该变量,提高了代码的可预测性。const:也不存在声明提升,同样存在暂时性死区,在声明常量之前访问会导致 ReferenceError 错误。(三)可变性var:声明的变量可以被重新赋值,也可以在同一作用域内被重复声明(后面的声明会被忽略,有赋值时会覆盖之前的值)。let:声明的变量可以被重新赋值,但在同一作用域内不能重复声明,避免了变量声明冲突。const:声明的常量不能被重新赋值(对于对象和数组类型,虽然不能重新赋值整个对象或数组,但内部属性和元素可以修改,若要完全禁止修改,可使用 Object.freeze() 方法),并且在声明时必须初始化赋值。(四)使用场景建议var:由于其存在声明提升和函数作用域的特性,可能会导致一些代码理解和维护上的困难。在现代 JavaScript 开发中,var 的使用场景逐渐减少,一般仅在需要兼容非常旧的 JavaScript 环境(不支持 ES6 及以上特性)时才考虑使用。let:适用于需要在块级作用域内声明变量,并且变量值可能会发生变化的场景。例如在 for 循环中声明循环变量,或者在 if 块内声明临时变量等。const:用于声明那些值在整个程序运行过程中不会改变的常量,如数学常量(PI)、配置项(MAX_COUNT)等。对于对象和数组类型的常量,如果希望其内部内容也不可变,可结合 Object.freeze() 使用。        综上所述,var、let 和 const 在 JavaScript 中各自具有独特的特性和适用场景。作为新手开发者,深入理解它们之间的区别,并在实际编程中正确使用,将有助于编写更加规范、健壮和易于维护的 JavaScript 代码。随着对 JavaScript 语言的不断学习和实践,能够更加熟练地运用这三种声明方式来满足不同的编程需求。————————————————原文链接:https://blog.csdn.net/2403_87566238/article/details/146290516
  • [技术干货] Java基于工厂+策略模式的物联网设备下载任务队列
    前言        在物联网开发中,通常一个服务端会和很多设备进行交互,设备普遍也会有文件升级的需求。如果下发了一个升级任务到很多个设备,这些设备在收到下载指令后同一个时间段执行升级任务,从服务器拉取升级文件,则会有将服务器带宽占满导致网络阻塞。所以我认为这里需要一个队列来分批次的执行这些任务。下面是我对下载任务实现代码的一些见解。1.工厂模式        关于工厂模式,我推荐大家可以看一下张老师的讲解,链接如下:Java设计模式之创建型:工厂模式详解(简单工厂+工厂方法+抽象工厂)_简单工厂模式,工厂方法模式,抽象工厂模式-CSDN博客2.数据库的设计        我是这样理解的,因为任务下发后不会直接发送给设备。所以需要将下载任务的数据进行存储。所以需要用到数据库将下载的任务和数据存起来,由队列不断地去执行该表中的任务。我这里用到的是MySQL数据库,表设计如下:CREATE TABLE `kwd_download_queue`  (  `id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,  `device_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备编号',  `type` int NULL DEFAULT NULL COMMENT '升级类型 具体的类型根据业务而定',  `status` int NULL DEFAULT NULL COMMENT '任务状态  0 等待 1 执行 2 下载失败 3 下载成功',  `create_time` datetime NULL DEFAULT NULL COMMENT '任务创建时间',  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',  `data_info` longblob NULL COMMENT '升级的数据信息',  `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间',  PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;3.代码实现 3.1 接口设计/** * 升级处理器接口,所有类型的升级处理器都需要实现该接口 */public interface UpgradeHandler {    /**     * 执行升级操作     * @param deviceSn 设备编号     * @param upgradeInfo 升级信息     * @return 操作结果,true表示成功,false表示失败     */    boolean executeUpgrade(String deviceSn, UpgradeInfo upgradeInfo);     /**     * 处理升级回调     * @param taskId 任务ID     * @param success 是否成功     * @param message 回调消息     * @return 处理结果     */    boolean handleCallback(String taskId, boolean success, String message);     /**     * 获取处理器支持的升级类型     * @return 升级类型     */    int getUpgradeType();}         /**     * 升级信息内部类,用于存储在dataInfo字段中的JSON数据     */    @Data    public static class UpgradeInfo {        // 保留发送内容的原始JSON结构        private JsonNode sendContent;         private String taskId;         // 使用Map来存储动态的标识信息字段        @JsonIgnore        private Map<String, Object> additionalProperties = new HashMap<>();         // 处理未知字段        @JsonAnySetter        public void setAdditionalProperty(String name, Object value) {            if (!"sendContent".equals(name)) {                this.additionalProperties.put(name, value);            }        }         @JsonAnyGetter        public Map<String, Object> getAdditionalProperties() {            return this.additionalProperties;        }         // 获取特定的标识信息        public Object getProperty(String key) {            return additionalProperties.get(key);        }         // 获取sendContent作为字符串        public String getSendContentAsString() {            try {                return sendContent != null ? sendContent.toString() : null;            } catch (Exception e) {                return null;            }        }    }        由于业务需求,在download_queue表中的dataInfo字段中,我添加了一些判断性的标识,是区分一些升级的类型的。sendContent中的内容是无需修改直接发送给设备的具体数据;additionalProperties是动态的附加属性,由于不同升级任务可能需要一些标识所以我偷懒使用了这个方法。        但是,这里设计的不太合理了,在此我建议大家扩充数据库字段进行标识,不要写在一个字段中,这样极其不易维护!!!我是纯因为懒才写到字段中用记忆的方式进行标识的哈哈哈。        好了,言归正传,因为不同的升级类型有不同的业务逻辑,需要不同的处理,所以需要先规范接口。让具体的升级类型的类去实现该接口。然后创建抽象类整合一些共有代码。3.2 抽象类设计import com.edison.device.entity.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.impl.KwdDownloadQueueServiceImpl;import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired; /** * 升级处理器的抽象基类 * 实现通用逻辑,各具体处理器继承此类并实现特定逻辑 */@Slf4jpublic abstract class AbstractUpgradeHandler implements UpgradeHandler {     @Autowired    protected KwdDownloadQueueMapper downloadQueueMapper;     @Override    public boolean handleCallback(String taskId, boolean success, String message) {        log.info("处理升级回调: taskId={}, success={}, message={}", taskId, success, message);         // 查询任务        KwdDownloadQueue task = downloadQueueMapper.selectById(taskId);        if (task == null) {            log.error("任务不存在: {}", taskId);            return false;        }         // 检查任务类型是否匹配        if (task.getType() != getUpgradeType()) {            log.error("任务类型不匹配: expected={}, actual={}", getUpgradeType(), task.getType());            return false;        }         // 更新任务状态        int newStatus = success ?                KwdDownloadQueue.DownloadStatus.COMPLETED.getCode() :                KwdDownloadQueue.DownloadStatus.FAILED.getCode();         int updated = downloadQueueMapper.updateTaskStatus(taskId, newStatus);         // 任务完成后的额外处理        if (updated > 0 && success) {            onUpgradeSuccess(task);        } else if (updated > 0 && !success) {            onUpgradeFailed(task, message);        }         return updated > 0;    }     /**     * 当升级成功时的后续处理     * 子类可以覆盖此方法实现特定逻辑     *     * @param task 任务实体     */    protected void onUpgradeSuccess(KwdDownloadQueue task) {        log.info("升级成功: taskId={}, deviceSn={}, type={}",                task.getId(), task.getDeviceSn(), task.getType());    }     /**     * 当升级失败时的后续处理     * 子类可以覆盖此方法实现特定逻辑     *     * @param task 任务实体     * @param errorMessage 错误信息     */    protected void onUpgradeFailed(KwdDownloadQueue task, String errorMessage) {        log.warn("升级失败: taskId={}, deviceSn={}, type={}, error={}",                task.getId(), task.getDeviceSn(), task.getType(), errorMessage);    }      /**     * 记录升级日志     * 用于跟踪升级过程     * @param deviceSn 设备编号     * @param message 日志消息     */    protected void logUpgradeAction(String deviceSn, String message) {        log.info("设备 {} 升级操作: {}", deviceSn, message);         // 可以在此实现将日志写入数据库或其他存储    }}        之后使具体的不同升级类型的类去继承该抽象类实现代码,根据自己的业务去下发升级任务。3.3 具体任务类型的实现类@Component@Slf4jpublic class FileUpgradeHandler extends AbstractUpgradeHandler{       @Override    public boolean executeUpgrade(String deviceSn, KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo) {        log.info("执行文件升级操作,设备:{},升级信息:{}", deviceSn, upgradeInfo);          }     @Override    public int getUpgradeType() {        return UpgradeTypes.FILE;  // 具体的升级类型的标识,后续需要由工厂获取    } }         这是接口、抽象类和具体的实现类的基础代码,那么怎么使用呢?下面就要根据这些代码去创建一个工厂类。3.4 工厂类import com.edison.device.upgrade.handler.UpgradeHandler;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component; import javax.annotation.PostConstruct;import java.util.HashMap;import java.util.List;import java.util.Map; /** * 升级处理器工厂,负责根据升级类型获取对应的处理器 */@Slf4j@Componentpublic class UpgradeHandlerFactory {    @Autowired    private List<UpgradeHandler> upgradeHandlers;     private final Map<Integer, UpgradeHandler> handlerMap = new HashMap<>();      /**     * 初始化处理器映射     */    @PostConstruct    public void init() {        for (UpgradeHandler handler : upgradeHandlers) {            handlerMap.put(handler.getUpgradeType(), handler);            log.info("注册升级处理器:type={}, handler={}",                    handler.getUpgradeType(), handler.getClass().getSimpleName());        }    }     /**     * 根据升级类型获取处理器     * @param upgradeType 升级类型     * @return 对应的处理器,如果不存在则返回null     */    public UpgradeHandler getHandler(int upgradeType) {        UpgradeHandler handler = handlerMap.get(upgradeType);        if (handler == null) {            log.error("未找到类型为{}的升级处理器", upgradeType);        }        return handler;    }}        在项目启动时,所有继承AbstractUpgradeHandler的Bean对象都会被自动注入到upgradeHandlers属性中,然后通过init方法初始化处理器映射对象handlerMap,这样后续就可以通过方法getHandler根据不同的升级类型去调用不同的处理类进行任务的下发。3.5 队列工具类(队列实体服务层代码)import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.edison.common.core.utils.bean.BeanUtils;import com.edison.device.domain.vo.DownloadQueueStats;import com.edison.device.domain.vo.DownloadQueueVO;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.mapper.KwdDownloadQueueMapper;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import com.edison.device.utils.UpgradeUtils;import com.fasterxml.jackson.annotation.JsonAnyGetter;import com.fasterxml.jackson.annotation.JsonAnySetter;import com.fasterxml.jackson.annotation.JsonIgnore;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional; import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.Executor;import java.util.concurrent.Executors; /** * (KwdDownloadQueue)表服务实现类 * */@Service@Slf4jpublic class KwdDownloadQueueServiceImpl extends ServiceImpl<KwdDownloadQueueMapper, KwdDownloadQueue> implements KwdDownloadQueueService {     @Autowired    private KwdDownloadQueueMapper downloadQueueMapper;      @Autowired    private UpgradeHandlerFactory upgradeHandlerFactory;     @Autowired    private ObjectMapper objectMapper;     @Value("${download.concurrent.limit:20}")    private int concurrentLimit;     // 用于跟踪正在处理的任务,确保同一任务不被重复处理    private final ConcurrentHashMap<String, Boolean> processingTasks = new ConcurrentHashMap<>();     // 线程池用于异步执行下载任务    private final Executor downloadExecutor = Executors.newFixedThreadPool(10);     /**     * 创建新的下载任务     * 将VO转换为实体,设置初始状态为等待,并保存到数据库     *     * @param downloadQueueVO 下载队列视图对象,包含设备编号、升级类型、创建人等信息     * @return 创建的任务ID     */    @Override    public String createDownloadTask(DownloadQueueVO downloadQueueVO) {        KwdDownloadQueue downloadQueue = new KwdDownloadQueue();        BeanUtils.copyProperties(downloadQueueVO, downloadQueue);         // 设置初始状态        downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode());        downloadQueue.setCreateTime(new Date());         // 序列化升级信息        try {            if (downloadQueueVO.getUpgradeInfo() != null) {                downloadQueue.setDataInfo(objectMapper.writeValueAsString(downloadQueueVO.getUpgradeInfo()));            }        } catch (JsonProcessingException e) {            log.error("序列化升级信息时出错", e);            throw new RuntimeException("创建下载任务时出错", e);        }         save(downloadQueue);        log.info("已创建ID为的下载任务: {}", downloadQueue.getId());        return downloadQueue.getId();    }            /**     * 批量创建下载任务     * 适用于需要同时给多个设备下发升级指令的场景     *     * @param downloadQueueVOList 下载队列视图对象列表     * @return 成功创建的任务数量     */    @Override    @Transactional(rollbackFor = Exception.class)    public int batchCreateDownloadTasks(List<DownloadQueueVO> downloadQueueVOList) {        List<KwdDownloadQueue> downloadQueueList = new ArrayList<>();         for (DownloadQueueVO vo : downloadQueueVOList) {            KwdDownloadQueue downloadQueue = new KwdDownloadQueue();            BeanUtils.copyProperties(vo, downloadQueue);             // 设置初始状态和ID            downloadQueue.setStatus(KwdDownloadQueue.DownloadStatus.WAITING.getCode());            downloadQueue.setCreateTime(new Date());             // 序列化升级信息            try {                if (vo.getUpgradeInfo() != null) {                    downloadQueue.setDataInfo(objectMapper.writeValueAsString(vo.getUpgradeInfo()));                }            } catch (JsonProcessingException e) {                log.error("序列化设备的升级信息时出错: {}", vo.getDeviceSn(), e);                // 继续处理其他任务                continue;            }             downloadQueueList.add(downloadQueue);        }         // 批量保存        if (!downloadQueueList.isEmpty()) {            saveBatch(downloadQueueList);            log.info("批量创建 [{}] 个下载任务", downloadQueueList.size());            return downloadQueueList.size();        }         return 0;    }      /**     * 开始处理等待中的任务     * 根据配置的并发数,从等待队列中选取任务开始执行     * 确保同时执行的任务数不超过并发限制     *     * @param batchSize 每次处理的批次大小,控制一次最多处理多少个任务     * @return 本次开始处理的任务数量     */    @Override//    @Transactional(rollbackFor = Exception.class)  //根据业务需求添加    public int processWaitingTasks(int batchSize) {        // 获取当前正在执行的任务数        LambdaQueryWrapper<KwdDownloadQueue> queryWrapper = new LambdaQueryWrapper<>();        queryWrapper.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode());        long executingCount = count(queryWrapper);         // 计算可以新启动的任务数        long availableSlots = Math.max(0, concurrentLimit - executingCount);        if (availableSlots <= 0) {            log.debug("没有可用于新下载任务的插槽。当前执行: {}", executingCount);            return 0;        }         // 获取等待中的任务        long tasksToProcess = Math.min(availableSlots, batchSize);        List<KwdDownloadQueue> waitingTasks = downloadQueueMapper.findWaitingTasks(tasksToProcess);         int processedCount = 0;        for (KwdDownloadQueue task : waitingTasks) {            // 检查同一设备是否已有正在执行的任务            int deviceExecutingCount = downloadQueueMapper.countExecutingTasksByDevice(task.getDeviceSn());            if (deviceExecutingCount > 0) {                log.debug("设备 {} 已具有正在执行的任务,跳过", task.getDeviceSn());                continue;            }             // 更新任务状态为执行中            boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.EXECUTING.getCode());            if (!updated) {                log.warn("未能更新任务的任务状态: {}", task.getId());                continue;            }             // 异步处理任务            final String taskId = task.getId();             // 获取升级类型示例,这里并未使用            String type = null;            if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) {                try {                    KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class);                    type = UpgradeUtils.getUpgradeType(upgradeInfo);                } catch (JsonProcessingException e) {                    log.error("解析升级信息时出错: {}", task.getId(), e);                }            }            String finalType = type;             downloadExecutor.execute(() -> {                try {                    processingTasks.put(taskId, true);                    boolean success = processTask(taskId);                    // 如果发送指令失败,才标记为失败                    if (!success) {                        updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode());                    }                } catch (Exception e) {                    log.error("处理任务时出错: {}", taskId, e);                    updateTaskStatus(taskId, KwdDownloadQueue.DownloadStatus.FAILED.getCode());                } finally {                    processingTasks.remove(taskId);                }            });             processedCount++;        }         log.info("已开始处理 {} 个等待任务", processedCount);        return processedCount;    }        /**     * 处理单个任务,执行实际的下载操作     * 根据任务类型调用不同的升级处理器执行具体升级逻辑     *     * @param taskId 任务ID     * @return 处理结果,true表示成功,false表示失败     */    @Override    public boolean processTask(String taskId) {        // 1. 获取任务详情        KwdDownloadQueue task = getById(taskId);        if (task == null) {            log.warn("未找到任务: {}", taskId);            return false;        }         // 2. 验证任务状态 - 只处理"执行中"状态的任务        if (task.getStatus() != KwdDownloadQueue.DownloadStatus.EXECUTING.getCode()) {            log.warn("任务 {} 未处于执行状态", taskId);            return false;        }         log.info("正在处理下载任务 {},类型 {},设备 {}",                taskId, task.getType(), task.getDeviceSn());         try {            // 3. 解析升级信息 (JSON数据转对象)            KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = null;            if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) {                upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class);            }             if (upgradeInfo == null) {                log.error("任务的升级信息无效: {}", taskId);                return false;            }             // 4. 获取对应类型的升级处理器 (策略模式)            UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType());            if (handler == null) {                log.error("找不到任务升级类型 {} 的处理程序: {}", task.getType(), taskId);                return false;            }             // 5. 执行具体的升级操作            upgradeInfo.setAdditionalProperty("taskId", taskId);            // 设置任务ID            upgradeInfo.setTaskId(taskId);            boolean result = handler.executeUpgrade(task.getDeviceSn(), upgradeInfo);             log.info("任务 {} 已处理,并返回结果: {}", taskId, result);            return result;        } catch (Exception e) {            log.error("处理任务时出错: {}", taskId, e);            return false;        }    }         /**     * 更新任务状态     * 用于手动更新任务状态或任务执行完成后更新状态     *     * @param taskId 任务ID     * @param status 新状态值,对应DownloadQueue.DownloadStatus枚举     * @return 更新是否成功     */    @Override    public boolean updateTaskStatus(String taskId, int status) {        int updated = downloadQueueMapper.updateTaskStatus(taskId, status);        return updated > 0;    }      /**     * 获取设备的任务历史     * 查询指定设备的所有下载任务记录,按时间倒序排列     *     * @param deviceSn 设备编号     * @param limit 限制返回的记录数量     * @return 任务历史列表     */    @Override    public List<KwdDownloadQueue> getDeviceTaskHistory(String deviceSn, int limit) {        return downloadQueueMapper.findDeviceTaskHistory(deviceSn, limit);    }         /**     * 检查并处理超时任务     * 将长时间处于执行状态但未完成的任务标记为失败     *     * @param timeoutMinutes 超时时间(分钟),超过这个时间仍未完成的任务会被标记为失败     * @return 处理的超时任务数量     */    @Override    @Transactional(rollbackFor = Exception.class)    public int checkAndHandleTimeoutTasks(int timeoutMinutes) {        List<KwdDownloadQueue> timeoutTasks = downloadQueueMapper.findTimeoutTasks(timeoutMinutes);         int handled = 0;        for (KwdDownloadQueue task : timeoutTasks) {            // 避免处理正在被处理的任务            if (processingTasks.containsKey(task.getId())) {                continue;            }             log.warn("找到超时任务:{},标记为失败", task.getId());             boolean updated = updateTaskStatus(task.getId(), KwdDownloadQueue.DownloadStatus.FAILED.getCode());            if (updated) {                if (task.getType() == 0){                    // 获取升级类型                    String type = null;                    if (task.getDataInfo() != null && !task.getDataInfo().isEmpty()) {                        try {                            KwdDownloadQueueServiceImpl.UpgradeInfo upgradeInfo = objectMapper.readValue(task.getDataInfo(), KwdDownloadQueueServiceImpl.UpgradeInfo.class);                            type = UpgradeUtils.getUpgradeType(upgradeInfo);                        } catch (JsonProcessingException e) {                            log.error("解析升级信息时出错: {}", task.getId(), e);                        }                    }                }                handled++;            }        }         log.info("已处理[{}]个超时任务", handled);        return handled;    }         /**     * 获取下载队列统计信息     * 统计不同状态的任务数量,用于监控和展示     *     * @return 队列统计信息对象     */    @Override    public DownloadQueueStats getQueueStats() {        DownloadQueueStats stats = new DownloadQueueStats();         LambdaQueryWrapper<KwdDownloadQueue> waitingQuery = new LambdaQueryWrapper<>();        waitingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.WAITING.getCode());        stats.setWaitingTasks(count(waitingQuery));         LambdaQueryWrapper<KwdDownloadQueue> executingQuery = new LambdaQueryWrapper<>();        executingQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.EXECUTING.getCode());        stats.setExecutingTasks(count(executingQuery));         LambdaQueryWrapper<KwdDownloadQueue> failedQuery = new LambdaQueryWrapper<>();        failedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.FAILED.getCode());        stats.setFailedTasks(count(failedQuery));         LambdaQueryWrapper<KwdDownloadQueue> completedQuery = new LambdaQueryWrapper<>();        completedQuery.eq(KwdDownloadQueue::getStatus, KwdDownloadQueue.DownloadStatus.COMPLETED.getCode());        stats.setCompletedTasks(count(completedQuery));         return stats;    }      /**     * 重试失败的任务     * 将指定的失败任务重新设置为等待状态,等待重新执行     *     * @param taskId 任务ID     * @return 操作是否成功     */    @Override    public boolean retryFailedTask(String taskId) {        return false;    }      /**     * 取消等待中的任务     * 将等待中的任务删除或标记为已取消     *     * @param taskId 任务ID     * @return 操作是否成功     */    @Override    public boolean cancelWaitingTask(String taskId) {        return false;    }      /**     * 获取设备当前正在执行的任务数     * 用于判断设备是否有正在进行的升级任务     *     * @param deviceSn 设备编号     * @return 执行中的任务数     */    @Override    public int getDeviceExecutingTaskCount(String deviceSn) {        return 0;    }      /**     * 清理历史任务     * 删除或归档指定时间之前的已完成/失败任务     *     * @param daysBefore 天数,删除多少天之前的历史任务     * @return 清理的记录数     */    @Override    public int cleanHistoryTasks(int daysBefore) {        return 0;    }      /**     * 升级信息类,用于存储在dataInfo字段中的JSON数据     */    @Data    public static class UpgradeInfo {        // 保留发送内容的原始JSON结构        private JsonNode sendContent;         private String taskId;         // 使用Map来存储动态的标识信息字段        @JsonIgnore        private Map<String, Object> additionalProperties = new HashMap<>();         // 处理未知字段        @JsonAnySetter        public void setAdditionalProperty(String name, Object value) {            if (!"sendContent".equals(name)) {                this.additionalProperties.put(name, value);            }        }         @JsonAnyGetter        public Map<String, Object> getAdditionalProperties() {            return this.additionalProperties;        }         // 获取特定的标识信息        public Object getProperty(String key) {            return additionalProperties.get(key);        }         // 获取sendContent作为字符串        public String getSendContentAsString() {            try {                return sendContent != null ? sendContent.toString() : null;            } catch (Exception e) {                return null;            }        }    }}        这一块代码会比较杂,因为不便展示,我还删除了一些相关的业务代码,每个方法上都有注释,解释了方法的作用,因为有些接口我并没有使用的需求,所以我没有实现该接口与的完整方法,可以根据自己的业务来修改该方法。3.6 相关实体类import lombok.Data; /** * 下载队列统计数据 * 用于统计不同状态任务的数量,提供给前端展示或监控系统使用 */@Datapublic class DownloadQueueStats {     /**     * 等待中的任务数     */    private long waitingTasks;     /**     * 执行中的任务数     */    private long executingTasks;     /**     * 失败的任务数     */    private long failedTasks;     /**     * 已完成的任务数     */    private long completedTasks;     /**     * 获取总任务数     * @return 所有状态任务的总和     */    public long getTotalTasks() {        return waitingTasks + executingTasks + failedTasks + completedTasks;    }     /**     * 获取活跃任务数(等待中+执行中)     * @return 活跃任务数量     */    public long getActiveTasks() {        return waitingTasks + executingTasks;    }     /**     * 获取完成率     * @return 完成率百分比     */    public double getCompletionRate() {        long total = getTotalTasks();        return total > 0 ? (double) completedTasks / total * 100 : 0;    }     /**     * 获取失败率     * @return 失败率百分比     */    public double getFailureRate() {        long total = getTotalTasks();        return total > 0 ? (double) failedTasks / total * 100 : 0;    }}    import lombok.Data;import javax.validation.constraints.NotBlank;import javax.validation.constraints.NotNull;import java.util.Map; /*** 任务创建**/@Datapublic class DownloadQueueVO {     /**     * 设备编号     */    @NotBlank(message = "设备编号不能为空")    private String deviceSn;     /**     * 类型:0系统升级 1文件升级 2背景图升级 3启动词升级     */    @NotNull(message = "升级类型不能为空")    private Integer type;     /**     * 创建人     */    private String createBy;      /**     * 升级信息,包含url、版本号、MD5等     */    @NotNull(message = "升级信息不能为空")    private Map<String, Object> upgradeInfo;}    import java.util.Date;import java.io.Serializable; import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import com.fasterxml.jackson.annotation.JsonFormat;import lombok.Getter;import org.springframework.format.annotation.DateTimeFormat; /** * @author * @since */@Data@ApiModel("下载队列实体类")public class KwdDownloadQueue implements Serializable {     @ApiModelProperty(value = "${column.comment}")    @TableId(type = IdType.ASSIGN_ID)    private String id;    /**     * 设备编号     */    @ApiModelProperty(value = "设备编号")    private String deviceSn;    /**     * 类型 0系统升级 1文件升级....     */    @ApiModelProperty(value = "类型 0系统升级 1文件升级....")    private Integer type;    /**     * 任务状态  0 等待 1 执行 2 升级失败 3 升级成功     */    @ApiModelProperty(value = "任务状态  0 等待 1执行 2 失败 3历史")    private Integer status;    /**     * 任务创建时间     */    @ApiModelProperty(value = "任务创建时间")    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")    private Date createTime;    /**     * 创建人     */    @ApiModelProperty(value = "创建人")    private String createBy;    /**     * 升级信息     */    @ApiModelProperty(value = "升级信息")    private String dataInfo;    /**     * 执行时间     */    @ApiModelProperty(value = "执行时间")    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")    private Date executeTime;       /**     * 任务类型枚举     */    public enum DownloadType {        SYSTEM_UPGRADE(0, "系统升级"),        FILE_UPGRADE(1, "文件升级"),             private final int code;        private final String desc;         DownloadType(int code, String desc) {            this.code = code;            this.desc = desc;        }         public int getCode() {            return code;        }         public String getDesc() {            return desc;        }    }     /**     * 任务状态枚举     */    @Getter    public enum DownloadStatus {        WAITING(0, "等待中"),        EXECUTING(1, "执行中"),        FAILED(2, "失败"),        COMPLETED(3, "完成");         private final int code;        private final String desc;         DownloadStatus(int code, String desc) {            this.code = code;            this.desc = desc;        }     }}3.7 定时任务执行器import com.edison.device.service.KwdDownloadQueueService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicBoolean; /** * 下载队列调度器,定期处理等待中的任务和检查超时任务 */@Slf4j@Componentpublic class DownloadQueueScheduler {     @Autowired    private KwdDownloadQueueService downloadQueueService;     @Value("${download.batch.size:15}")    private int batchSize;     @Value("${download.timeout.minutes:30}")    private int timeoutMinutes;     // 防止任务重叠执行的标志    private final AtomicBoolean processingFlag = new AtomicBoolean(false);    private final AtomicBoolean timeoutCheckFlag = new AtomicBoolean(false);     /**     * 定期处理等待中的任务     * 每30秒执行一次     */    @Scheduled(fixedDelayString = "${download.process.interval:30000}")    public void processWaitingTasks() {        // 如果已经有一个处理任务在执行,则跳过本次执行        if (!processingFlag.compareAndSet(false, true)) {            log.debug("另一个处理任务正在执行,跳过本次调度");            return;        }         try {            log.info("开始处理等待中的下载任务,批次大小:{}", batchSize);            int processed = downloadQueueService.processWaitingTasks(batchSize);            log.info("本次处理了{}个下载任务", processed);        } catch (Exception e) {            log.error("处理下载任务时发生异常", e);        } finally {            // 重置标志,允许下次执行            processingFlag.set(false);        }    }     /**     * 定期检查超时任务     * 每5分钟执行一次     */    @Scheduled(fixedDelayString = "${download.timeout.check.interval:300000}")    public void checkTimeoutTasks() {        // 如果已经有一个超时检查在执行,则跳过本次执行        if (!timeoutCheckFlag.compareAndSet(false, true)) {            log.debug("另一个超时检查正在执行,跳过本次调度");            return;        }         try {            log.info("开始检查超时任务,超时时间:{}分钟", timeoutMinutes);            int handled = downloadQueueService.checkAndHandleTimeoutTasks(timeoutMinutes);            log.info("处理了{}个超时任务", handled);        } catch (Exception e) {            log.error("检查超时任务时发生异常", e);        } finally {            // 重置标志,允许下次执行            timeoutCheckFlag.set(false);        }    }}        这里使用了定时任务,扫表获取待执行的任务再通过不同的任务执行器去执行任务;由于任务可能长时间没有反馈,需要有一个超时扫表任务修改任务状态来防止一些任务没有及时反馈造成队列阻塞。3.8 设备升级回调接口import com.edison.common.core.domain.Result;import com.edison.device.entity.po.KwdDownloadQueue;import com.edison.device.service.KwdDownloadQueueService;import com.edison.device.upgrade.factory.UpgradeHandlerFactory;import com.edison.device.upgrade.handler.UpgradeHandler;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController; @Slf4j@RestController@RequestMapping("/upgrade/callback")@Api(tags = "设备升级回调接口")public class UpgradeCallbackController {     @Autowired    private KwdDownloadQueueService downloadQueueService;     @Autowired    private UpgradeHandlerFactory upgradeHandlerFactory;     /**     * 接收设备升级结果回调     * @param callback 回调信息     * @return 处理结果     */    @PostMapping    @ApiOperation("设备升级结果回调")    public Result<Boolean> handleCallback(@RequestBody UpgradeCallback callback) {        log.info("收到设备升级回调:taskId={}, deviceSn={}, success={}, message={}",                callback.getTaskId(), callback.getDeviceSn(), callback.isSuccess(), callback.getMessage());         try {            // 查询任务            KwdDownloadQueue task = downloadQueueService.getById(callback.getTaskId());            if (task == null) {                log.error("任务不存在:{}", callback.getTaskId());                return Result.error("任务不存在");            }             // 验证设备编号            if (!task.getDeviceSn().equals(callback.getDeviceSn())) {                log.error("设备编号不匹配:expected={}, actual={}",                        task.getDeviceSn(), callback.getDeviceSn());                return Result.error("设备编号不匹配");            }             // 获取对应的升级处理器            UpgradeHandler handler = upgradeHandlerFactory.getHandler(task.getType());            if (handler == null) {                log.error("未找到对应的升级处理器:type={}", task.getType());                return Result.error("未找到对应的升级处理器");            }             // 处理回调            boolean result = handler.handleCallback(                    callback.getTaskId(),                    callback.isSuccess(),                    callback.getMessage());             return result ?                    Result.OK("回调处理成功", true) :                    Result.error("回调处理失败");        } catch (Exception e) {            log.error("处理升级回调时发生异常", e);            return Result.error("处理回调异常:" + e.getMessage());        }    }     /**     * 升级回调信息     */    @Data    public static class UpgradeCallback {        /**         * 任务ID         */        private String taskId;         /**         * 设备编号         */        private String deviceSn;         /**         * 是否成功         */        private boolean success;         /**         * 回调消息         */        private String message;    }}AI写代码        这里就需要所对接的设备(安卓或者硬件)去向服务端汇报升级结果,然后去修改队列中的升级状态。如果有需要修改设备表的升级状态的需求,可以在此类中添加自己的业务逻辑。但是在此功能开发完毕进入生产环境后,我发现了一个问题,如果升级信息的数据量特别大的情况下,即使表中的记录数不多,下载队列的那张表会检索的特别特别慢!!!然后我寻思着修改数据库中的字段类型为longblob(原来是longtext类型),依然不起作用。然后我暂时没有做处理,目前我所能想到的解决方法只有分表进行处理,将data_info这个字段牵出去用两张表来维护此功能。如果各位老师有更好的办法欢迎留言!————————————————原文链接:https://blog.csdn.net/YyyGxxx/article/details/148327760
  • [技术干货] Java:Map和Set练习
    查找字母出现的次数这道题的思路在后面的题目过程中能用到,所以先把这题给写出来题目要求:给出一个字符串数组,要求输出结果为其中每个字符串及其出现次数。思路:我们可以把数组里的字符串按顺序放进map中,对于没被放进去过的字符串,放进去次数为1,之前被放进过去的字符串,那就在其上重新放入,并把次数重新加1.举个例子,输出的内容是:"this", "dog", "cat", "cat", "this", "dog"现在是把每个元素放进去,在没遇到一样数据之前的过程,如是上面所示,如果遇到了一样的数据, 这个操作看起来可能是把第二个cat放进去了,但是实际上是把cat重新输入了,然后把Key值输入为2了。因为map其中节点的样子如上图所示。代码部分如下import java.util.HashMap;import java.util.Map;import java.util.Set; public class Test {    public static Map<String, Integer> countWords(String[] words){        Map<String, Integer> map = new HashMap<>();        for(String word : words){            if(map.get(word) == null){                map.put(word, 1);            }else {                int val = map.get(word);                map.put(word, val+1);            }        }        return map;    }     public static void main(String[] args) {        String[] words = {"this", "dog", "cat", "cat", "this", "dog"};        Map<String, Integer> map = countWords(words);        Set<Map.Entry<String, Integer>> entryset = map.entrySet();        for (Map.Entry<String, Integer> entry : entryset){            System.out.println("Key: " + entry + "  Val: " + entry.getKey());        }    }}只出现一次的数字题目链接:只出现一次的数字 - 力扣(LeetCode)题目描述:给一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。思路:这里的思路和上面的 查找字母出现的次数 有些像。依次把元素放到set中,如果set中没有该元素,就把该元素放进去,如果有,就把这个元素从set中删去。最后输出set中的元素以 {1,2,3,4,1,2,3} 为例,当第一次往里放,没有遇到重复的元素时,如下图按照数组的顺序,接着向下放,就会遇到重复的元素,这时候就要把set中的元素给删除了 后面的2,3也要依次从set中删除。public static int singleNumber(int[] nums){        HashSet<Integer> set = new HashSet<>();        for (int i = 0; i < nums.length; i++) {            if(set.contains(nums[i])){                set.remove(nums[i]);            }else{                set.add(nums[i]);            }        }         for (int i = 0; i < nums.length; i++) {            if(set.contains(nums[i])){                return nums[i];            }        }        return -1;    }     public static void main(String[] args) {        int[] array = {1,2,3,4,1,2,3};        System.out.println(singleNumber(array));    }运行结果如下坏键盘打字题目链接:题目描述:旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。输入在两行中分别给出应该输入的文字、以及实际输入的文字按照发现顺序,在一行中输出坏掉的键。其中英语字母只输出大写,每个坏键只输入一次。示例输入7_This_is_a_test_hs_s_a_es输出7TI题目思路:该题的思路在于如何找出坏键,这里提供一种思路,先把实际输入的数据放到set中,然后再把应该输入的文字遍历一遍,如果其中有set中没有的数据,那些没有的数据便是坏掉的键。public static void func(String str1, String str2){        //将字符串大写        str1 = str1.toUpperCase();        str2 = str2.toUpperCase();         HashSet<Character> setAct = new HashSet<>();        for (int i = 0; i < str2.length(); i++) {            char ch = str2.charAt(i);            setAct.add(ch);        }         for (int i = 0; i < str1.length(); i++) {            char ch = str1.charAt(i);            if(!setAct.contains(ch)){                 System.out.print(ch);            }        }    }     public static void main(String[] args) {        func("7_This_is_a_test", "_hs_s_a_es");    }这样的代码还是存在问题,没办法把其中重复出现的元素给消去,输出的结果是现在问题变成了如何去重,这部分不难能想到,我们可以创建一个setBroken来存放已经查找到的坏键,如果set和setBroken中都没有这个元素才打印.public class Test {    public static void func(String str1, String str2){        str1 = str1.toUpperCase(Locale.ROOT);        str2 = str2.toUpperCase(Locale.ROOT);         HashSet<Character> setAct = new HashSet<>();        for (int i = 0; i < str2.length(); i++) {            char ch = str2.charAt(i);            setAct.add(ch);        }         //第一步是把不同的数给挑出来,然后对于重复输出的数据给去重        HashSet<Character> setBroken = new HashSet<>();        for (int i = 0; i < str1.length(); i++) {            char ch = str1.charAt(i);            if(!setAct.contains(ch) && !setBroken.contains(ch)){                setBroken.add(ch);                System.out.print(ch);            }        }    }     public static void main(String[] args) {        func("7_This_is_a_test", "_hs_s_a_es");    }}AI写代码输出结果为这次的内容就到这里,我们下篇文章再见————————————————原文链接:https://blog.csdn.net/xiaochuan_bsj/article/details/143368533
  • [技术干货] Java的栈与队列以及代码实现
    栈的概念(Stack)栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等。例如这把枪,第一发子弹是最后发射的,第一发子弹在栈底,而最新安装上去的子弹在栈的顶部,只有将上面的子弹打完(栈顶的数据走完),最后一发子弹才会射出栈的实现栈的实现是基于简单的数组形成的,我们可以将它想象成连续的数组,而栈的顺序是由后放到前放模拟实现栈的方法:push(放入一个元素到栈中)pop(提取栈顶的一个元素,并将在其栈中消除)peek(查看栈顶的元素)size(查看栈中的大小)empty(栈中是否为空)full(栈是否满了)代码import java.util.Arrays;public class MyStack implements IStack {    private int[] elem;    private int top;//数组的栈顶,以及数组栈中存放元素的数量    private static final int DEFAULT_CAPACITY = 10;//这里是初始容量    public MyStack() {        elem = new int[DEFAULT_CAPACITY];        top = -1;//数组下标从0开始    }    @Override    public void push(int item) {        if (full()) {            //如果满了就扩容            elem = Arrays.copyOf(elem, 2 * elem.length);        }        elem[++top] = item;    }    @Override    public int pop() throws RuntimeException {        try {            if (empty()) {                throw new RuntimeException("栈为空");            }        } catch (RuntimeException e) {            e.printStackTrace();        }        return elem[top--];//return返回后删除栈顶的元素    }    @Override    public int peek() {        if (empty()) {            throw new RuntimeException("栈为空");        }        return elem[top];//返回栈顶元素    }    @Override    public int size() {        return top+1;//去除数组0    }    @Override    public boolean empty() {        return top == -1;    }    @Override    public boolean full() {        return top == elem.length;//count==当前的elem的总长度为true    }}队列(Queue)队列是由先进先出的线性数据结构,采用的是先进先出,后进后出,如果要插入元素的话就是从入队尾巴方向插入,而删除作为出队要在头尾删除。队列的方法模拟实现队列(双链表实现)public class MyQueue implements IQueue{    static class Queue{        public int elem;        public Queue next;        public Queue prev;        public Queue(int elem) {            this.elem = elem;        }    }    public Queue head;    public Queue last;    public int size=0;    @Override    public void offer(int val) {        Queue queue = new Queue(val);        if (this.head == null) {            this.head = queue;            this.last = queue;            ++size;            return;        }        this.last.next=queue;        this.last.prev=this.head;        this.last=last.next;        ++size;    }    @Override    public int poll() {        if(this.head==null){            throw new RuntimeException("没有要丢掉的队列");        }        Queue cur =this.head;        if(this.head.next==null){            return -1;        }            this.head=this.head.next;            this.head.prev=null;            size--;        return cur.elem;    }    @Override    public int peek() {        if(this.head!=null){            return this.head.elem;        }        return 0;    }    @Override    public int size() {        return size;    }}循环队列(循环数组实现)数组实现队列的循环需要引入一个公式(目前的下标值+1)%当前数组的长度(index+1)%array.length,下标值从0开始少一个数,当index+1就是当前的总长度时,公式后的值一定为下标0。    private int[] array;    private int front;    private int rear;    public MyCircularQueue(int k) {        array=new int[k+1];        front=0;//初始位置        rear=0;    }        public boolean enQueue(int value) {        //入列        if(isFull()){        //这里如果容量已经满了,需要先删除后在进行插入            return false;        }        array[rear]=value;//rear下标获取元素        rear=(rear+1)%array.length;//rear最终循环为0下标        return true;    }        public boolean deQueue() {        //出列        if(isEmpty()){        //为空返回false            return false;        }        front=(front+1)%array.length;//front只需要往后走        return true;    }        public int Front() {        if(isEmpty()){            return -1;        }        return array[front];    }        public int Rear() {        if(isEmpty()){            return -1;        }        //这里三木操作符判断是否为0如果为0,将rear回退到最后一个位置,不为0则-1        int temp =  (rear==0)?array.length-1:rear-1;        return array[temp];    }        public boolean isEmpty() {        return front==rear;    }        public boolean isFull() {        return (rear+1)%array.length==front;    }}用队列实现栈因为队列是先进先出的,而我们的栈是先进后出的,两种线性结构的关系是颠倒的,一个队列是不能完成的,我们需要两个队列互相工作来完成辅助队列先获取数值,保证辅助队列是最后一个拿到值的,然后将主队列的值给到辅助队列,在交换两个队列的数值,因为队列关系先进先出,每一次最后一个值就是队列先出的数值主队列不为空,将主队列的元素都poll出放到辅助栈中,使用一个tmp来将主队列(这里主队列已经遍历完)和辅助队列交换    Queue<Integer> q1;//主队列    Queue<Integer> q2;//辅助队列    public MyStack() {        q1=new LinkedList<>();//构造方法        q2=new LinkedList<>();    }    public void push(int x) {            q2.offer(x);        while(!q1.isEmpty()){//主队列不为空,则将主队列出列给到辅助队列            q2.offer(q1.poll());        }        //走到这里主队列是为空        Queue tmp=q1;        q1=q2;        q2=tmp;        //将两个队列交换    }        public int pop() {        return q1.poll();    }        public int top() {        return q1.peek();    }        public boolean empty() {        return q1.isEmpty();    }}用栈来实现队列栈来实现队列,栈是先进后出的顺序,而队列是先进先出的顺序将push都放到a栈中当我们peek或者是要删除的时候,我们都将a栈的元素pop给b栈,这样b栈已经有了我们的元素但是我们还需要考虑的是丢掉元素后如果在一起添加元素到a栈呢,这里我们给一个条件,如果b的栈不为空时,我们仍然用b栈的队列如果a为空,这两个栈都是空的说明没有元素直接返回-1,如果a不为空的话且b没有新的元素b继续捕获新的a栈中所有的元素class MyQueue {    Stack<Integer> A;    Stack<Integer> B;    public MyQueue() {        A=new Stack<>();        B=new Stack<>();    }        public void push(int x) {        A.push(x);    }        public int pop() {        int check=peek();        B.pop();        return check;    }        public int peek() {    //先判断b是否是空的,如果不是空的直接返回,是空才可以往下走               if(!B.isEmpty())return B.peek();               //因为b还不是空的,所以不需要将a栈放到b中        if(A.isEmpty())return -1;        while(!A.isEmpty()){            B.push(A.pop());//将所有的a放到b中        }        return B.peek();    }        public boolean empty() {        return A.isEmpty()&&B.isEmpty();        //a和b都为空才为空    }}总结栈分为栈顶和栈底,最先进的为栈底,最后进的为栈顶。队列分为队头和队尾,最先进的为队头,最后进的为队尾。————————————————原文链接:https://blog.csdn.net/weixin_60489641/article/details/143723419
  • [技术干货] 全球首款Java专用AI开发助手实测:一句话生成完整工程代码——飞算 JavaAI
    写在前面几个Java哥们儿瞪着满屏的报错,脸都快贴屏幕上了——项目deadline催命呢,这场景,熟吧?憋屈吧?可你扭头看看隔壁组,人家正端着咖啡杯,有说有笑地做测试呢!为啥?人家刚用了个叫飞算JavaAI的东西,把整个电商平台的后端代码,“唰”一下给整出来了!乖乖,这世道,真变了?说飞算JavaAI,你可别想岔了。它不是你写代码时蹦出来的那种“小补丁”,顶多算个“单词提示”。这玩意儿是动真格的——全球头一个专门伺候Java的,能直接给你“吐”出一整套、能跑、能用的项目代码! 背后是正经搞技术的飞算公司,牛人不少,钱也厚实。它牛在哪?简单说,就是把咱原来那套写代码的苦逼流程,给“掀桌子”了。你跟它叨咕一句“弄个订单管理系统”,它吭哧吭哧就给你整出接口、数据库、业务逻辑全套家伙事儿,直接能跑!科幻片?不,现在真有兄弟在用了。为啥说这玩意儿能救命?专治各种“工伤”!跟产品经理“鸡同鸭讲”?拜拜了您嘞! 产品老哥嘴里的“用户画像”,你以为是打标签?结果他要的是猜用户下一步买啥!来回掰扯,跟传话游戏似的,心累得慌!JavaAI咋整?你直接跟它唠嗑(说话都行),它就能整明白你要啥,连你没想到的(比如商品视频咋存咋管)都能给你拎出来。沟通成本?直接砍半!烦死人的CURD“搬砖”?丢给它! 建表?写增删改查接口?配那些乱七八糟的依赖包?这些破事儿占了大把时间,干完还没啥成就感,纯纯的“工具人”!JavaAI就猛了,点一下,Maven/Gradle项目骨架、标准代码、配置文件,全套齐活! 省下的功夫,琢磨点有意思的技术难点,不香吗?早点下班陪女朋友(如果有的话)不香吗?看见老代码就想跑?它能当“老中医”! 那些用老掉牙的Hibernate写的“祖传屎山”,看着就头大,重构?跟考古挖坟没区别,生怕动一下就塌了!JavaAI自带本地“老中医”功能,能帮你把这堆老古董“号号脉”,再看看现在有啥好用的新玩意儿,给你出个靠谱的升级方案,至少心里有底了。这玩意儿到底有啥能耐?Lethehong给你盘盘道兄弟们都说它是“六边形战士”,真不是瞎吹:嘴皮子一动,设计图就来了:你就说“搞个会员积分系统”,它立马给你列出要哪些接口、数据库表长啥样,连字段啥类型、主键咋设都给你整得门儿清。它肚子里专门琢磨过Java的“脾气”,设计出来的东西,扩展性好,不容易“牵一发动全身”。复杂业务不怕翻车?它有“防呆”招儿! 搞多张表一起操作、或者一堆人同时抢资源(高并发)?心里打鼓怕出幺蛾子吧?它能把复杂的业务逻辑掰开了、揉碎了,变成一步步能走的,还提前帮你瞅瞅哪儿可能打架。更神的是,你改了点小地方,它还能偷偷把相关的逻辑也调顺溜,有效防止“改一行代码,整个系统嗝屁”的惨案(这痛,扎心不?)。代码风格看不上眼?按你的规矩来! 嫌弃生成的代码太死板、没个性?简单!你直接跟它说你们组有啥“家规”(比如“DTO必须验数据”、“不准在代码里写死数字”),它生成的代码,立马就规规矩矩按你的“家规”来,跟你们组自己人码的一模一样。老系统不敢大动?它“小刀慢割”! 面对一堆陈年老代码,2.0版本多了个“一块一块生成”的功能,贼实用。你可以挑着某个接口或者功能,单独让它生成新代码,还能马上看到效果。往老系统里塞的时候,也不用提心吊胆怕把整个系统搞崩了。谁在用?反正不是摆设!刚入行的小白:被Spring Boot那些注解绕得七荤八素?用它生成个标准项目直接跑起来,边改边看边学,比干啃教程快多了,上手贼快!被deadline追着跑的苦命团队:真有兄弟(做医疗平台的)用了,仨小时,订单模块搞定! 搁以前,吭哧吭哧手写至少三天!省下的时间,人家转头就去搞更核心的算法优化了,效率杠杠的。总被“需求误解”气哭的产品经理:这回牛了,能直接甩给开发一个“能跑”的技术方案!再也不用背锅说“我明明说的是A,你们咋做出个C?”了,腰杆都直了!想少掉点头发的技术老大(CTO):用上它的规则引擎统一代码风格,Code Review的破事儿直接少了一大半! 团队代码看起来清清爽爽,老大也省心,少熬点夜,头发能多留几根。别小看它,可能真要“变天”别人还在吵吵AI写的代码片段靠不靠谱,飞算JavaAI已经玩得更深了:它把咱们这帮写Java的,从流水线上拧螺丝的“码农”,变成了指挥AI“施工队”干活的“包工头”(架构师)。有个用了的CTO老哥说的大实话:“以前兄弟们80%的劲儿都耗在写基础代码和擦屁股(修Bug)上了,现在?能腾出手来琢磨点真正有技术含量的、创新的东西了!”飞算这家公司,野心不小。之前搞的SoFlu软件机器人就吹过“一个人就能扛一个项目,十个人能当百人用”,在银行、医院这些地方都用上了。现在这个JavaAI是他们的“大招”,配上低代码、数据工具,搞了个给企业用的“数字化套餐” ——这架势,明摆着是要抢传统软件外包公司那万亿级别的大蛋糕啊!如何安装飞算 JavaAI步骤一:从 IDEA 插件市场安装点击导航-插件,打开应用市场,搜索相关关键字"CalEx JavaAl"或"飞算"都能搜索到,找到CalEx-JavaAl 后点击安装步骤二:注册飞算 JavaAl重启 Inteli IDEA 工具,请先注册账号并登录,登录成功后即可开始体验“智能分析”、"JavaChat”"智能问答“等功能。新项目开发”一键生成完工程文档,”“老项目二次开发”本地化智能分析深度理解+自定义开发规范。 步骤三:在日常开发中使用飞算JavaAlJava AI真的有那么神? 测试项目的优缺点 这里我正好有一个项目在线,于是我就让他给我一个文件中的优缺点 优点清晰的项目结构:文件结构符合标准的Maven项目结构,易于管理和扩展。明确的依赖管理:使用Maven管理依赖,版本明确,便于维护和更新。常用的库版本:使用的MySQL驱动和MyBatis版本都是比较稳定的版本,适合大2多数应用场景。缺点JUnit作用域设置不正确:JUnit的依赖作用域被设置为compile,而通常情况下应该设置为test,以避免将测试依赖打包到生产环境中。缺少日志库依赖:项目中没有包含日志库(如SLF4J或Log4j),这在实际应用中通常是必要的,以便于调试和监控。缺少其他实用依赖:例如数据库连接池(如HikariCP)、Spring框架(如果需要事务管理和依赖注入)等,这些在实际项目中可能会提高开发效率和应用性能。 Java AI制作Java算命这里生成代码的速度已经超越了绝大部分的AI编程工具我想利用Java写一个算命的程序,请你给我给出完整的代码 package com.test; import java.util.Scanner; public class FortuneTeller {     public static void main(String[] args) {        Scanner scanner = new Scanner(System.in);        System.out.println("请输入您的出生年份:");        int birthYear = scanner.nextInt();        scanner.close();         String fortune = tellFortune(birthYear);        System.out.println("您的命运预测如下:");        System.out.println(fortune);    }     private static String tellFortune(int birthYear) {        switch (birthYear % 12) {            case 0: return "您属鼠,今年可能会遇到很多新的开始和机会。";            case 1: return "您属牛,今年可能会有很多稳定和收获。";            case 2: return "您属虎,今年可能会有挑战和冒险,但也充满力量。";            case 3: return "您属兔,今年可能会有快乐和轻松,但也需要注意健康。";            case 4: return "您属龙,今年可能会有领导力和创造力的展现。";            case 5: return "您属蛇,今年可能会有智慧和耐心的增长。";            case 6: return "您属马,今年可能会有活力和速度的提升。";            case 7: return "您属羊,今年可能会有和谐和温暖的感受。";            case 8: return "您属猴,今年可能会有灵活性和适应性的增强。";            case 9: return "您属鸡,今年可能会有勤奋和节俭的习惯。";            case 10: return "您属狗,今年可能会有忠诚和友谊的支持。";            case 11: return "您属猪,今年可能会有幸福和满足的感觉。";            default: return "无法预测的命运。";        }    }}飞算JavaAI vs 国内其他AI编程工具跟通义灵码比较最大的区别就是专注度通义灵码支持Java、Python、C++等多种语言,但飞算JavaAI就专门死磕Java这一门语言。这就像一个是全科医生,一个是心脏科专家的区别。我之前同时用过这两个,感受挺明显的:通义灵码生成代码时,经常给你一些通用的模板,但涉及到Java特有的一些优化和最佳实践,就显得有点浅飞算JavaAI生成的代码遵循Java代码编写的最佳实践,代码风格统一、规范,可读性强代码生成的完整度差别很大这个差别真的很明显。通义灵码在生成复杂业务逻辑代码时,生成的代码结构有时不够清晰,需要开发者花费更多时间去梳理和优化。我拿同一个需求测试过,通义灵码给我生成了几个代码片段,我还得自己组装。飞算JavaAI直接给我一套完整的工程代码,连数据库建表语句都有了。跟文心快码比较百度这个确实厉害,但思路不一样文心快码支持超过100种主流编程语言,覆盖了从系统编程到Web开发、移动应用开发等多个领域。功能很全面,但问题还是老毛病——太泛了。我试过用文心快码做个电商系统,它能理解我的需求,也能给代码,但给的都是一些标准的CRUD操作。想要一些高级功能,比如分布式锁、缓存策略这些,就比较吃力。飞算JavaAI专注于Java单一语言开发,对Java语言特性和编程规范有深入理解,能生成高质量、符合行业最佳实践的Java代码。在处理复杂业务逻辑时,它真的能生成结构清晰、逻辑严谨的代码。跟豆包MarsCode比较字节的这个工具我用得不多主要原因是实测下来,感觉和GitHub Copilot和通义灵码都有差距,说实话是有点失望的。可能是因为发布时间比较晚,还在持续优化中。不过豆包MarsCode有个优势是它除了编程助手,还提供了云端开发环境。但纯粹从代码生成质量来说,跟飞算JavaAI比还是有明显差距的。实际使用建议如果你是:Java专业开发者:强烈推荐飞算JavaAI,真的能大幅提升效率多语言开发者:可以考虑通义灵码或文心快码个人学习者:通义灵码免费,可以先试试企业级项目:飞算JavaAI在代码质量和完整性上更有保障说实话,用过飞算JavaAI之后,再用其他工具总感觉缺点什么。就像习惯了自动挡汽车,再开手动挡总觉得麻烦。当然,这也可能是因为我主要做Java开发的原因。不过有一点要说明,飞算JavaAI目前主要专注后端,如果你要做前端开发,可能还是得配合其他工具使用。写在最后凌晨的办公室,咖啡机还在那儿“咕噜咕噜”响。但原来那密集的键盘“交响乐”少了,多了点飞算JavaAI干活时那种低沉的“嗡嗡”声。一个开发兄弟指着屏幕,乐了:“搞定!订单退款逻辑跑通了,嘿,连测试多人同时退款的代码都给我备好了!” 他那组人已经开始收拾包,张罗着去吃宵夜了。为啥这么潇洒?因为明天产品要的新需求讨论,他们今晚就能把演示版(Demo)整出来。当AI把那些重复的、费脑子的“搬砖”活儿扛了,咱们这帮写Java的脑子,总算能腾出来,干点更带劲、更有创造性的活儿了——比如,想想宵夜点啥烤串? (或者,早点回家睡觉?)————————————————原文链接:https://blog.csdn.net/2301_76341691/article/details/148697903
  • [技术干货] Java基础(一):初识Java——发展历程、技术体系与JDK环境搭建
    一、Java发展史  Java最初由Sun公司的“Green”项目组开发,用于智能家电设备,最初名为Oak。因商标问题,1995年更名为“Java”(灵感源于印尼爪哇岛的咖啡)。发行版本    发行时间    发行的各版本及其特征Java    1995年    Java语言诞生Java 1.0    1996年    首个正式版本,包含基础类库和Applet支持Java 1.1    1997年    引入内部类(Inner Class)、Java Beans、JDBC(数据库连接)和反射APIJava 1.2    1998年    JDK 1.2发布,更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)Java 1.3    2000年    引入HotSpot JVM、JNDI(Java命名与目录接口)Java 1.4    2002年    新增正则表达式、断言(Assert)、NIO(非阻塞I/O)和日志APIJava 5.0    2004年    引入泛型、注解、枚举等革命性特性,为强调版本重要性,Sun将内部版本号1.5公开命名为5.0,此后版本号逐渐简化Java 6.0    2006年    Sun将产品线更名为Java SE/EE/ME,终结“J2”前缀,并宣布开源(OpenJDK)2009年    Oracle以74亿美元收购财务困境的Sun公司,Java正式归属OracleJava 7.0    2011年    Oracle首个大版本,支持菱形语法、多异常捕获,但因收购过渡期特性较少Java 8.0    2014年    继JDK 5后最大更新,引入Lambda表达式、Stream API、新日期时间库。LTS(长期支持)版本Java 9.0    2017年    发布周期改为每半年发布一次版本,每三年推出LTS(长期支持)版本Java 10.0    2018年    废弃“1.x”格式,直接使用主版本号(如JDK 10而非JDK 1.10)Java EE移交Eclipse基金会,重命名为Jakarta EE(如包名从javax.*改为jakarta.*)Java 11.0    2018年    新增HTTP客户端API、局部变量类型推断(var)并移除部分过时功能。LTS(长期支持)版本…    …    Java21.0    2023年    被视为继Java 8后的新一代主流版本,生态支持(如框架适配率)快速提升。LTS(长期支持)版本二、Java技术体系平台1、JavaSEJavaSE 的全称是 Java Platform Standard Edition(Java 平台标准版)面向桌面级应用(如Windows下的应用程序),提供完整的Java核心API,是其他平台(JavaEE、JavaME)的基础JavaSE和JDK的关系JavaSE(规范):定义接口、抽象类、具体类以及JVM的行为和约束(定义语言和API应该是什么样)例:JavaSE规范要求必须有一个ArrayList类,它实现List接口,支持动态扩容JDK(实现):提供这些接口和类的具体代码实现(按照规则实现并提供开发工具和运行环境)例1:OracleJDK的ArrayList源码中,具体实现了扩容机制(如默认扩容1.5倍)例2:OpenJDK的ArrayList可能实现相同的逻辑,但代码细节可能有细微差异(如注释、内部优化)历史名称:早期称为J2SE(JDK 6之前)2、JavaEEJavaEE 的全称是 Java Platform Enterprise Edition(Java 平台企业版)在Java SE基础上扩展了大量企业级API(如Servlet、JSP、EJB),提供分布式计算、事务管理、安全性等企业级功能JavaEE接口由官方规范定义,具体实现由应用服务器(Tomcat、WildFly)或第三方库(Hibernate、ActiveMQ)提供自JDK 10起由Oracle移交Eclipse基金会管理,更名为Jakarta EE历史名称:曾用名J2EE(JDK 6之前)3、JavaMEJavaME 的全称是 Java Platform Micro Edition(Java 平台微型版)针对移动终端(手机、PDA等)的轻量级平台,精简了Java SE的API并加入移动设备支持随着 Android 和 iOS 的普及,JavaME 的使用逐渐减少历史名称:曾用名J2ME4、三者关系JavaSE 是基础:JavaEE 和 JavaME 均基于 JavaSE 的核心功能构建JavaEE 是扩展:在 JavaSE 基础上增加企业级服务规范(如 Servlet、JPA、EJB)JavaME 是精简:仅保留 JavaSE 部分功能,并添加针对微型设备的特性三、Java程序运行机制及运行过程1、Java的跨平台性2、Java虚拟机(核心机制)JVM 是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理数据、内存、寄存器,包含在JDK 中对于不同的平台,有不同的虚拟机Java 虚拟机机制屏蔽了底层运行平台的差别,实现了“一次编译,到处运行”四、Java语言环境搭建1、JDK(Java开发工具包)定义:JDK是用于开发Java应用程序的完整工具包,包含编译、调试、文档生成等开发工具以及运行环境组成部分:JRE:JDK中内置了JRE(包含核心类库),确保开发时可以直接运行程序开发工具:如编译器javac(将Java源代码编译为字节码)、调试器jdb、文档工具javadoc等JDK特有的工具类库:如:tools.jar,支持编译器(javac)、调试器(jdb)等工具的运行(位于JDK的lib目录下)用途:开发者必须安装JDK,才能编写、编译和调试Java程序2、JRE(Java运行时环境)定义:JRE是运行已编译Java程序所需的最小环境,无需开发功能组成部分:JVM(Java虚拟机) :负责执行字节码,实现跨平台特性JRE中的核心类库:以java.*包的形式存在,例如rt.jar、resource.jar下java.lang、java.util等(位于JRE的lib目录下,并由BootstrapClassLoader自动加载)JRE中的扩展类库:以javax.*包的形式组织,例如javax.sql等(JRE的lib/ext目录下,由ExtensionClassLoader加载)用途:普通用户只需安装JRE即可运行Java程序(如.jar或.class文件),无需开发工具3、环境变量及作用3.1、JAVA_HOME该环境变量的值是Java的安装路径,一些Java版本的软件和工具需要用到该变量例如,当Windows平台上JDK的安装目录为“C:\java\jdk8”时,设置如下所示JAVA_HOME=C:\java\jdk813.2、CLASSPATH该环境变量用于指明Java字节码文件(.class文件)的位置默认情况下,如果未设置CLASSPATH,Java启动JVM后,会在当前目录下寻找字节码文件,一旦设置了CLASSPATH,JVM会在指定目录下查找字节码文件环境变量CLASSPATH的值一般为一个以分号“;”作为分隔符的路径列表,设置如下CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;1“.”表示当前目录,因为设置CLASSPATH会覆盖JVM的默认操作(查找当前目录),所以这里需要加上“.”dt.jar是 Java 开发工具包(JDK)中用于为 IDE 提供 Swing/AWT 组件的设计时元数据(如属性、事件描述),支持通过拖拽和图形化界面进行可视化开发的核心类库文件tools.jar的作用:包含编译工具(如javac)所需的类库Java5之前,若用户未显式配置CLASSPATH环境变量JVM不会在当前目录查询.class文件,所以需要配置CLASSPATH但从Java 5(2004年发布)开始,默认情况,无需显式配置CLASSPATH,JVM会自动搜索当前目录和核心类库3.3、PATH该环境变量指定一个路径列表,用于搜索可执行文件执行一个可执行文件时,如果该文件不能在当前路径下找到,则依次寻找PATH中的每一个路径,直至找到。例如:PATH=.;%JAVA_HOME%\bin;1这样可以在命令行中直接使用java和javac命令,而不需要指定完整路径,否则就会出现以下错误:不建议在PATH环境变量中添加当前目录"."的主要原因:如果当前目录"."被加入PATH,当用户进入公共可写目录/tmp时,攻击者可能在该目录下放置与系统命令同名的恶意程序例如:黑客在/tmp目录下创建名为ls的木马文件,当用户(尤其是root用户)执行ls命令时,会优先执行当前目录下的恶意程序而非系统标准的/bin/ls,导致权限泄露或数据被破坏————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148105022
  • [技术干货] JavaScript 数据结构详解
    最近在复习JavaScript的基础知识,和第一次学确实有了很不一样的感受,第一次学的比较浅,但是回头再进行学习的时候,发现有很多遗漏的东西,所以今天想分享一下新学到的知识,后面会一点一点补充更新JavaScript的数据结构有8个,分别是number string boolean object undefined null 还有es6新增的symbol和bigint,今天主要分享一下null undefined number,其他的等复习完会及时更新的null:null是一个独立的数据类型,表示一个空值或者是一个对象没有值null有几个特殊的用法,操作如下:1.当使用Number方法来识别null的时候,输出为0 console.log(Number(null))  //02.对null实现一些运算符操作(可以将null当做0来进行计算) console.log(2 + null) //2 console.log(2 * null) //0undefined:undefined比较特殊,表示未定义,比如你在生命一个变量,但是没给他赋值的时候,然后检测该变量的类型,输出就是undefined let a console.log(a) //undefined 1.当使用Number方法来识别undefined的时候,输出为NaNconsole.log(Number(undefined)) //NaN2.对undefined实现一些运算符操作(可以将undefined当做没有值来进行计算) console.log(undefined + 2) //NaN console.log(undefined * 2) //NaN 3.当使用undefined和null来进行比较的时候,非严格模式下,两者是相等的console.log(null == undefined) //trueconsole.log(null === undefined) //falsenumber:number是用来表示整数和浮点数已经NaN的数据类型,JavaScript的底层没有整数1.所有的数字都是使用64位浮点数来进行存储console.log(1 === 1.0) //true2.当小数在进行相加的时候,具有误差console.log((0.3 - 0.2) === 0.1) //false3.当一个计算的数大于2的53次方,计算就不准确了console.log(Math.pow(2, 53) === Math.pow(2, 53) + 1) //true4.当一个数大于2的1024次方,就会溢出,如果小于2的-1075次方,会溢出为0console.log(Math.pow(2, 1024)) //Infinityconsole.log(Math.pow(2, -1075) ) //05.+0和-0在很多情况下+0和-0是一样的,但是只有当他们表示分母的时候,会有不一样的结果console.log(+0 === -0) //trueconsole.log(1 / +0 === 1 / -0) //false6.NaN表示number类型,当对NaN进行幂运算的时候,输出为1,其他情况下都为NaNconsole.log(NaN ** 0) //17.进制十进制表示没有前导0的数值,二进制前缀(0b/0B),八进制前缀(0o/0O),十六进制前缀(0x/0X),特殊情况: 有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制。console.log(099) //99console.log(088) //88console.log(077) //63 8.infinity运算1. 范围:Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。console.log(Infinity > -100) //trueconsole.log(-Infinity < -100) //falseconsole.log(Infinity > NaN) //falseconsole.log(-Infinity < NaN) //false2. Infinity与undefined计算,返回的都是NaN。console.log(Infinity + undefined) //NaNconsole.log(Infinity - undefined) //NaNconsole.log(Infinity * undefined) //NaNconsole.log(Infinity / undefined) //NaN3. Infinity减去或除以Infinity,得到NaN。console.log(Infinity - Infinity) //NaNconsole.log(Infinity / Infinity) //NaN4.0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity。console.log(0 * Infinity) //NaNconsole.log(0 / Infinity) //0console.log(Infinity / 0) //Infinity5.Infinity与null计算时,null会转成0,等同于与0的计算。只用相乘的时候,返回NaNconsole.log(Infinity + null) //Infinityconsole.log(Infinity - null) //Infinityconsole.log(Infinity * null) //NaNconsole.log(Infinity / null) ————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/148673464
  • [技术干货] 【Java 开发日记】你会不会使用 SpringBoot 整合 Flowable 快速实现工作流呢?
    1、流程引擎介绍Flowable 是一个使用 Java 编写的轻量级业务流程引擎。Flowable 流程引擎可用于部署 BPMN2.0 流程定义(用于定义流程的行业 XML 标准),创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。Java 领域另一个流程引擎是 Activiti,不这两个东西只要会使用其中一个,另一个就不在话下。咱就不废话了,上代码吧。 2、创建项目首先创建一个 Spring Boot 项目,引入 Web、和 MySQL 驱动两个依赖,如下图:项目创建成功之后,引入 flowable 依赖,如下:<dependency>    <groupId>org.flowable</groupId>    <artifactId>flowable-spring-boot-starter</artifactId>    <version>6.7.2</version></dependency>这个会做一些自动化配置,默认情况下,所以位于 resources/processes 的流程都会被自动部署。接下来在 application.yaml 中配置一下数据库连接信息,当项目启动的时候会自动初始化数据库,将来流程引擎运行时候的数据会被自动持久化到数据库中。spring:  datasource:    username: root    password: 123    url: jdbc:mysql:///flowable?serverTimezone=Asia/Shanghai&useSSL=false配置完成后,就可以启动项目了。项目启动成功之后,flowable 数据库中就会自动创建如下这些表,将来流程引擎相关的数据都会自动保存到这些表中。默认的表比较多,截图只是其中一部分。 3、画流程图画流程图算是比较有挑战的一个步骤了,也是流程引擎使用的关键。官方提供了一些流程引擎绘制工具,感兴趣的小伙伴可以自行去体验;IDEA 也自带了一个流程可视化的工具,但是特别难用。这里说一下常用的 IDEA 插件 Flowable BPMN visualizer,如下图:装好插件之后,在 resources 目录下新建 processes 目录,这个目录下的流程文件将来会被自动部署。接下来在 processes 目录下,新建一个 BPMN 文件(插件装好了就有这个选项了),如下:来画个请假的流程,就叫做 ask_for_leave.bpmn20.xml,注意最后面的 .bpmn20.xml 是固定后缀。文件创建出来之后,右键单击,选择 View BPMN(Flowable) Diagram,就打开了可视化页面了,就可以来绘制自己的流程图了。请假流程画出来是这样:员工发起一个请假流程,首先是组长审核,组长审核通过了,就进入到经理审核,经理审核通过了,这个流程就结束了,如果组长审核未通过或者经理审核未通过,则流程给员工发送一个请假失败的通知,流程结束。来看下这个流程对应的 XML 文件,一些流程细节会在 XML 文件中体现出来,如下:<process id="ask_for_leave" name="ask_for_leave" isExecutable="true">    <userTask id="leaveTask" name="请假" flowable:assignee="#{leaveTask}"/>    <userTask id="zuzhangTask" name="组长审核" flowable:assignee="#{zuzhangTask}"/>    <userTask id="managerTask" name="经理审核" flowable:assignee="#{managerTask}"/>    <exclusiveGateway id="managerJudgeTask"/>    <exclusiveGateway id="zuzhangJudeTask"/>    <endEvent id="endLeave" name="结束"/>    <startEvent id="startLeave" name="开始"/>    <sequenceFlow id="flowStart" sourceRef="startLeave" targetRef="leaveTask"/>    <sequenceFlow id="modeFlow" sourceRef="leaveTask" targetRef="zuzhangTask"/>    <sequenceFlow id="zuzhang_go" sourceRef="zuzhangJudeTask" targetRef="managerTask" name="通过">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="zuzhang_reject" sourceRef="zuzhangJudeTask" targetRef="sendMail" name="拒绝">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="jugdeFlow" sourceRef="managerTask" targetRef="managerJudgeTask"/>    <sequenceFlow id="flowEnd" name="通过" sourceRef="managerJudgeTask" targetRef="endLeave">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='通过'}]]></conditionExpression>    </sequenceFlow>    <sequenceFlow id="rejectFlow" name="拒绝" sourceRef="managerJudgeTask" targetRef="sendMail">        <conditionExpression xsi:type="tFormalExpression"><![CDATA[${checkResult=='拒绝'}]]></conditionExpression>    </sequenceFlow>    <serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/>    <sequenceFlow id="endFlow" sourceRef="sendMail" targetRef="askForLeaveFail"/>    <endEvent id="askForLeaveFail" name="请假失败"/>    <sequenceFlow id="zuzhangTask_zuzhangJudeTask" sourceRef="zuzhangTask" targetRef="zuzhangJudeTask"/></process>结合 XML 文件来解释一下这里涉及到的 Flowable 中的组件:<process> :表示一个完整的工作流程。<startEvent> :工作流中起点位置,也就是图中的绿色按钮。<endEvent> :工作流中结束位置,也就是图中的红色按钮。<userTask> :代表一个任务审核节点(组长、经理等角色),这个节点上有一个 flowable:assignee 属性,这表示这个节点该由谁来处理,将来在 Java 代码中调用的时候,需要指定对应的处理人的 ID 或者其他唯一标记。<serviceTask>:这是服务任务,在具体的实现中,这个任务可以做任何事情。<exclusiveGateway> :逻辑判断节点,相当于流程图中的菱形框。<sequenceFlow> :链接各个节点的线条,sourceRef 属性表示线的起始节点,targetRef 属性表示线指向的节点,图中的线条都属于这种。流程图这块松哥和大家稍微说一下,咋一看这个图挺复杂很难画,但是实际上只要认认真真去捋一捋这里边的各个属性,基本上很快就明白到底是怎么一回事。 4、开发接口接下来写几个接口,来体验一把流程引擎。在正式体验之前,先来熟悉几个类,这几个类一会写代码会用到。 4.1 Java 类梳理ProcessDefinition这个最好理解,就是流程的定义,也就相当于规范,每个 ProcessDefinition 都会有一个 id。 ProcessInstance这个就是流程的一个实例。简单来说,ProcessDefinition 相当于是类,而 ProcessInstance 则相当于是根据类 new 出来的对象。 ActivityActivity 是流程标准规范 BPMN2.0 里面的规范,流程中的每一个步骤都是一个 Activity。 ExecutionExecution 的含义是流程的执行线路,通过 Execution 可以获得当前 ProcessInstance 当前执行到哪个 Activity了。 TaskTask 就是当前要做的工作。实际上这里涉及到的东西比较多,不过这里先整一个简单的例子,所以上面这些知识点暂时够用了。 4.2 查看流程图在正式开始之前,先准备一个接口,用来查看流程图的实时执行情况,这样方便查看流程到底执行到哪一步了。具体的代码如下:@RestControllerpublic class HelloController {    @Autowired    RuntimeService runtimeService;    @Autowired    TaskService taskService;    @Autowired    RepositoryService repositoryService;    @Autowired    ProcessEngine processEngine;    @GetMapping("/pic")    public void showPic(HttpServletResponse resp, String processId) throws Exception {        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processId).singleResult();        if (pi == null) {            return;        }        List<Execution> executions = runtimeService                .createExecutionQuery()                .processInstanceId(processId)                .list();        List<String> activityIds = new ArrayList<>();        List<String> flows = new ArrayList<>();        for (Execution exe : executions) {            List<String> ids = runtimeService.getActiveActivityIds(exe.getId());            activityIds.addAll(ids);        }        /**         * 生成流程图         */        BpmnModel bpmnModel = repositoryService.getBpmnModel(pi.getProcessDefinitionId());        ProcessEngineConfiguration engconf = processEngine.getProcessEngineConfiguration();        ProcessDiagramGenerator diagramGenerator = engconf.getProcessDiagramGenerator();        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "png", activityIds, flows, engconf.getActivityFontName(), engconf.getLabelFontName(), engconf.getAnnotationFontName(), engconf.getClassLoader(), 1.0, false);        OutputStream out = null;        byte[] buf = new byte[1024];        int legth = 0;        try {            out = resp.getOutputStream();            while ((legth = in.read(buf)) != -1) {                out.write(buf, 0, legth);            }        } finally {            if (in != null) {                in.close();            }            if (out != null) {                out.close();            }        }    }}这就一个工具,没啥好说的,一会大家看完后面的代码,再回过头来看这个接口,很多地方就都懂了。 4.3 开启一个流程为了方便,接下来的代码都在单元测试中完成。首先来开启一个流程,代码如下:String staffId = "1000";/** * 开启一个流程 */@Testvoid askForLeave() {    HashMap<String, Object> map = new HashMap<>();    map.put("leaveTask", staffId);    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("ask_for_leave", map);    runtimeService.setVariable(processInstance.getId(), "name", "javaboy");    runtimeService.setVariable(processInstance.getId(), "reason", "休息一下");    runtimeService.setVariable(processInstance.getId(), "days", 10);    logger.info("创建请假流程 processId:{}", processInstance.getId());}首先由员工发起一个请假流程,map 中存放的 leaveTask 是在 XML 流程文件中提前定义好的,提前定义好当前这个任务创建之后,该由谁来处理,这里是假设由工号为 1000 的员工来发起这样一个请假流程。同时,还设置了一些额外信息。ask_for_leave 是在 XML 文件中定义的一个 process 的名称。好啦,现在执行这个单元测试方法,执行完成后,控制台会打印出当前这个流程的 id,拿着这个 id 去访问 4.2 小节的接口,结果如下:可以看到,请假用红色的框框起来了,说明当前流程走到了这一步。 4.4 将请求提交给组长接下来,就需要将这个请假流程向后推进一步,将请假事务提交给组长,代码如下:String zuzhangId = "90";/** * 提交给组长审批 */@Testvoid submitToZuzhang() {    //员工查找到自己的任务,然后提交给组长审批    List<Task> list = taskService.createTaskQuery().taskAssignee(staffId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("任务 ID:{};任务处理人:{};任务是否挂起:{}", task.getId(), task.getAssignee(), task.isSuspended());        Map<String, Object> map = new HashMap<>();        //提交给组长的时候,需要指定组长的 id        map.put("zuzhangTask", zuzhangId);        taskService.complete(task.getId(), map);    }}首先利用 staffId 查找到当前员工的 id,进而找到当前员工需要执行的任务,遍历这个任务,调用 taskService.complete 方法将任务提交给组长,注意在 map 中指定组长的 id。提交完成后,再去看流程图片,如下:可以看到,流程图走到组长审批了。 4.5 组长审批组长现在有两种选择,同意或者拒绝,同意的代码如下:/** * 组长审批-批准 */@Testvoid zuZhangApprove() {    List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        //组长审批的时候,如果是同意,需要指定经理的 id        map.put("managerTask", managerId);        map.put("checkResult", "通过");        taskService.complete(task.getId(), map);    }}通过组长的 id 查询组长的任务,同意的话,需要指定经理,也就是这个流程下一步该由谁来处理。拒绝的代码如下:/** * 组长审批-拒绝 */@Testvoid zuZhangReject() {    List<Task> list = taskService.createTaskQuery().taskAssignee(zuzhangId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("组长 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        //组长审批的时候,如果是拒绝,就不需要指定经理的 id        map.put("checkResult", "拒绝");        taskService.complete(task.getId(), map);    }}拒绝的话,就没那么多事了,直接设置 checkResult 为拒绝即可。假设这里执行了同意,那么流程图如下: 4.6 经理审批经理审批和组长审批差不多,只不过经理这里是最后一步了,不需要再指定下一位处理人了,同意的代码如下:/** * 经理审批自己的任务-批准 */@Testvoid managerApprove() {    List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        map.put("checkResult", "通过");        taskService.complete(task.getId(), map);    }}拒绝代码如下:/** * 经理审批自己的任务-拒绝 */@Testvoid managerReject() {    List<Task> list = taskService.createTaskQuery().taskAssignee(managerId).orderByTaskId().desc().list();    for (Task task : list) {        logger.info("经理 {} 在审批 {} 任务", task.getAssignee(), task.getId());        Map<String, Object> map = new HashMap<>();        map.put("checkResult", "拒绝");        taskService.complete(task.getId(), map);    }}4.7 拒绝流程如果组长拒绝了或者经理拒绝了,也有相应的处理方案,首先在 XML 流程文件定义时,如下:<serviceTask id="sendMail" flowable:exclusive="true" name="发送失败提示" isForCompensation="true" flowable:class="org.javaboy.flowable.AskForLeaveFail"/>如果请假被拒绝,会进入到这个 serviceTask,serviceTask 对应的处理类是 org.javaboy.flowable.AskForLeaveFail,该类的代码如下:public class AskForLeaveFail implements JavaDelegate {    @Override    public void execute(DelegateExecution execution) {        System.out.println("请假失败。。。");    }}也就是请假失败会进入到这个方法中,现在就可以在这个方法中该干嘛干嘛了。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/148666102
  • [技术干货] JAVA SE 文件IO
    1. File类的使用这里主要介绍对文件增删改查的操作 , 不是对文件中的内容进行增删改查1.1 构造方法方法    说明File(File parent, String child)    根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例File(String pathname)    根据⽂件路径创建⼀个新的 File 实例,路径可以是绝对路径或者相对路径File(String parent, String child)    根据⽗⽬录 + 孩⼦⽂件路径,创建⼀个新的 File 实例,⽗⽬录⽤路径表⽰1.2 常用方法方法    说明getParent()    返回 File 对象的⽗⽬录⽂件路径getName()    返回 FIle 对象的纯⽂件名称getPath()    返回 File 对象的⽂件路径getAbsolutePath()    返回 File 对象的绝对路径getCanonicalPath()    返回 File 对象的修饰过的绝对路径exists()    判断 File 对象描述的⽂件是否真实存在isDirectory()    判断 File 对象代表的⽂件是否是⼀个⽬录isFile()    判断 File 对象代表的⽂件是否是⼀个普通⽂件createNewFile()    根据 File 对象,⾃动创建⼀个空⽂件。成功创建后返回 truedelete()    根据 File 对象,删除该⽂件。成功删除后返回 truedeleteOnExit()    根据 File 对象,标注⽂件将被删除,删除动作会到 JVM 运⾏结束时才会进⾏list()    返回 File 对象代表的⽬录下的所有⽂件名listFiles()    返回 File 对象代表的⽬录下的所有⽂件,以 File 对象表⽰mkdir()    创建 File 对象代表的⽬录mkdirs()    创建 File 对象代表的⽬录,如果必要,会创建中间⽬录renameTo(File dest)    进⾏⽂件改名,也可以视为我们平时的剪切、粘贴操作canRead()    判断⽤⼾是否对⽂件有可读权限canWrite()    判断⽤⼾是否对⽂件有可写权限2. I/O流I/O是Input/Output的缩写。I/O技术是非常实用的技术,用于处理数据传输。如读/写文件,网络通讯等。Java程序中,对于数据的输入/输出操作以"流(stream)" 的方式进行。java.io包下提供了各种"流"类和接口,用以获取不同种类的数据,并通过方法输入或输出数据2.1 I/O流的分类按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)按数据流的流向不同分为:输入流,输出流按流的角色的不同分为:节点流,处理流抽象基类    字节流    字符流输入流    InputStream    Reader输出流    OutputStream    Writer2.2 I/O体系Java的io流共涉及40多个类,实际上非常规则,都是以上述4个抽象基类派生的3. 字节流3.1 InputStream类InputStream 是抽象类 , 我们现在只关心从文件中读取,所以使用 FileInputStream类实例化对象FileInputStream类的构造方法方法    说明FileInputStream(File file)    通过指定的File对象来创建输入流。FileInputStream(String pathname)    通过指定文件的路径字符串来创建输入流。FileInputStream类的常见方法方法    说明int read()    从输入流中读取一个字节的数据。int read(byte[] b)    从输入流中读取一定数量的字节到字节数组中,返回长度。int read(byte[] b, int off, int len)    最多读取 len - off 字节的数据到 b中,放在从 off 开始,返回实际读到的数量;-1 代表以及读完了void close()    关闭字节流3.2 OutputStream类OutputStream 同样只是一个抽象类,要使用还需要具体的实现类。我们现在还是只关心写入文件中,所以使用 FileOutputStream类实例化对象FileOutputStream的构造方法方法    说明FileOutputStream(File file)    通过指定的File对象来创建输出流FilenOutputStream(String name)    通过指定文件的路径字符串来创建输出流FileOutputStream的常用方法方法    说明write(int b)    写入一个字节到文件。write(byte[] b)    将一个字节数组写入文件write(byte[] b, int off, int len)    从字节数组的指定位置开始,写入指定长度的字节到文件close()    关闭输出流,释放相关资源。4. 字符流4.1 Reader类FileReader类主要用于从文件中读取字符数据。它是一个字符输入流,继承自InputStreamReader(转换流),抽象基类为Reader。可以通过构造方法传入文件路径来创建FileReader的构造方法方法    说明FileReader(File file)    创建一个与指定文件对象相关联的FileReader。FileReader(String fileName)    创建一个与指定文件路径名相关联的FileReaderFileReader的常用方法方法    说明int read()    从输入流中读取一个字符,返回该字符的整数表示(到达文件末尾返回 -1)int read(char[] cbuf)    将字符读入数组。返回读取的长度void close()    关闭该流并释放与之关联的所有资源。4.2 Writer类FileWriter类用于将字符数据写入文件。FileWriter的常用方法方法    说明write(int c)    写入单个字符到文件中。write(char[] cbuf)    将字符数组写入文件中。write(String str)    写入字符串到文件中。write(String str, int off, int len)    写入字符串的一部分到文件中。flush()    刷新缓冲区,将数据写入文件。close()    关闭文件并释放相关资源。————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/148137889
  • [技术干货] 修改trae全局默认的JDK版本
    找到setting.json文件通过Trae中的搜索功能,可以找到Trae所使用的配置文件。修改JDK版本前提:已经按照对应版本的JDK在setting.json文件中找到“java.import.gradle.java.home”和“metals.javaHome”两个配置项,改成你想要的JDK版本的路径即可。小技巧-快捷查找JDK路径可以在setting.json文件中搜索“terminal.integrated.profiles.osx”这个配置项,这个配置项中就是trae扫描到的本机中安装的JDK版本及路径,复制想要的JDK版本的路径,直接使用即可。————————————————原文链接:https://blog.csdn.net/u011924665/article/details/146208263
  • [技术干货] 【Java篇】一气化三清:类的实例化与封装的智慧之道
    五、对象的构造及初始化5.1 如何初始化对象通过前面知识点的学习我们知道,在Java方法内部定义一个局部变量时,必须要初始化,否则会编译失败。public static void main(String[] args) {int a;System.out.println(a);}// Error:(26, 28) java: 可能尚未初始化变量a要让上述代码通过编译,非常简单,只需在正式使用变量之前给它设置初始值即可。public static void main(String[] args) {    Date d = new Date();    d.printDate();    d.setDate(2021,6,9);    d.printDate();}// 代码可以正常通过编译如果是对象,就需要调用之前写的 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    }}构造方法的特点是:名字必须与类名相同,且没有返回值类型(连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    }}示例代码2:无参构造方法public class Date {    public int year;    public int month;    public int day;        // 无参构造方法:给成员变量设置默认初始值    public Date() {        this.year = 1900;        this.month = 1;        this.day = 1;    }}上述两个构造方法名字相同但参数列表不同,构成了方法的重载。如果用户没有显式定义构造方法,编译器会生成一个默认的无参构造方法。注意:一旦用户显式定义了构造方法,编译器就不会再生成默认构造方法了示例代码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();}示例代码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();    }}示例代码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;    }}构造方法中可以通过 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;    }}注意:构造方法中的 this(…) 调用不能形成循环,否则会导致编译错误。public Date(){this(1900,1,1);}public Date(int year, int month, int day) {this();}/*无参构造器调用三个参数的构造器,而三个参数构造器有调用无参的构造器,形成构造器的递归调用编译报错:Error:(19, 12) java: 递归构造器调用*/在大多数情况下,我们使用 public 来修饰构造方法,但在特殊场景下(如实现单例模式)可能会使用 private 修饰构造方法。5.3 默认初始化在上文中提到的第二个问题:为什么局部变量在使用前必须初始化,而成员变量可以不初始化?要搞清楚这个过程,就需要知道 new 关键字背后所发生的一些事情:Date d = new Date(2021,6,9);在程序员看来只是一句简单的语句,但 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    }}在这个例子中,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("关机");    }}public class TestComputer {    public static void main(String[] args) {        Computer c = new Computer("华为", "i9", "16G", "4K");        c.boot();        c.shutDown();    }}注意:一般情况下,成员变量通常设置为 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);    }}这样就可以正常创建 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);    }}通过在创建对象时加上包名,就可以区分来自 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()方法    }}如果不使用静态导入,则需要写成:System.out.println(Math.PI);System.out.println(Math.random());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("上网~~~");    }}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,不允许被其他包中的类访问    }}注意:如果去掉前面的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
  • [技术干货] 计算机基础(三):深入解析Java中的原码、反码、补码
    引言  在Java的世界里,我们每天都在与整数打交道:int age = 30;、long balance = 1000000L;。但你是否思考过,这些数字在计算机内部的真实形态?理解原码、反码、补码不仅是计算机科学的基础,更是深入Java底层、避免隐蔽bug的关键。本文将带你彻底掌握这些二进制表示法的奥秘及其在Java中的实际应用。一、 基础概念:三种编码的诞生背景计算机只能处理二进制(0和1)。如何表示有符号整数(正数、负数)?工程师们设计了三种方案:1、原码定义:最高位表示符号位(0=正数,1=负数),其余位表示数值的绝对值示例 (8位byte为例):+5原码:0000 0101 (符号位0,绝对值5)-5原码:1000 0101 (符号位1,绝对值5)优点:人类直观,理解容易致命缺陷:存在两种零:+0的原码为00000000 ,-0的原码为10000000( 以一个字节长表示 ),浪费表示范围,逻辑上冗余加减运算复杂:正数和负数相加,数值部分实际上是相减,符号取决于绝对值大者的符号,硬件不能直接相加符号位和数值位示例1:(+5) + (-5) = 0,程序直接运算❌原码的加法不支持符号修正(不能自动让结果的符号与数值正确对应)    0000 0101  (+5)  + 1000 0101  (-5)  -----------------    1000 1010  结果为 -10示例2:(-5) + (+3) = -2,程序直接运算❌    1000 0101  (-5)  + 0000 0011  (+3)  -----------------    1000 1000  结果为 -8示例3:(-5) + (-5) = -10,程序直接运算❌原码的加法不支持进位消除(多出的进位被丢弃,不影响结果)    1000 0101  (-5)  + 1000 0101  (-5)  -----------------  1 0000 1010  结果为 10(注意:最高位溢出了1位舍弃)2、反码定义:正数:反码 = 原码负数:反码 = 负数原码符号位不变,数值位按位取反,或者更简单点正数原码全部按位取反示例 (8位byte为例):+5反码:0000 0101 (同原码)--5反码:1111 1010 (-5的原码1000 0101按位取反,符号位不变或者+5原码全部按位取反)遗留问题:两种零依然存在:+0反码为0000 0000,-0反码1111 1111(+0的原码按位取反)循环进位:计算结果最高位有进位时,需要把进位“回加”到最低位(称为“末位加1”或“循环进位”),硬件实现仍不理想示例1:(+5) + (-5) = 0,程序直接运算✅反码的加法在数值上可以看作正确,但从表达角度来看,有点不完美    0000 0101  (+5)  + 1111 1010  (-5)  -----------------    1111 1111  结果是反码,符号位不变其他按位取反,原码就是1000 0000也就是-0(负零)示例2:(-5) + (+3) = -2,程序直接运行✅    1111 1010  (-5)  + 0000 0011  (+3)  -----------------    1111 1101  结果是反码,原码为1000 0010也就是-2示例3:(-5) + (-5) = -10,程序直接运行❌反码的加法需要循环进位✅    1111 1010  (-5)  + 1111 1010  (-5)  -----------------  1 1111 0100  结果是反码(注意:最高位溢出了1位舍弃),最高位进位:1(需循环加回最低位)     1111 0100  +         1  -----------------    1111 0101 这里还是反码,原码为1000 1010也就是-103、补码 🏆 - Java的选择定义:正数:补码 = 原码负数:补码 = 反码 + 1示例 (8位byte为例):+5补码:0000 0101-5补码:+5的原码按位取反获得反码1111 1010,再加1获得补码1111 1011核心优势 (完美解决前两者问题):唯一的零:+0补码为0000 0000,-0补码是+0的原码全部按位取反再加1得到还是0000 0000,溢出一位舍去减法变加法:A - B = A + (-B) 直接成立,无需额外判断符号位或处理循环进位。硬件只需一套加法电路示例1:(+5) + (-5) = 0,程序直接运算✅补码加法支持进位消除(多出的进位被丢弃,不影响结果)    0000 0101  (+5)  + 1111 1011  (-5)  -----------------  1 0000 0001  结果是补码,先减1获得反码0000 0000,也就是0示例2:(-5) + (+3) = -2,程序直接运算✅    1111 1011  (-5)  + 0000 0011  (+3)  -----------------    1111 1110  结果是补码,先减1获得反码1111 1101,符号位不变其他按位取反获取原码1000 0010,也就是-2示例3:(-5) + (-5) = -10,程序直接运算✅    1111 1011  (-5)  + 1111 1011  (-5)  -----------------  1 1111 0110  结果为是补码,先减1获得反码1111 0101,符号位不变其他按位取反获得原码1000 1010,也就是-10二、 Java的坚定选择:补码一统天下1、为什么Java整数表示使用补码?补码解决了原码和反码的固有问题(双零、复杂运算),简化了CPU硬件设计,提高了运算效率最大优势正数和负数的加法、减法可以统一处理(解决痛点:原码需要判断正负)补码中只有一个 0(解决痛点:原码和反码有+0和-0,也需要单独判断)补码支持进位消除(解决痛点:符号参加运行,多出的进位丢弃,不影响结果,反码需要循环进位)Java的所有整数类型(byte, short, int, long)均使用补码表示!这是现代计算机体系结构的标准2、为什么byte的范围是-128到127,而不是-127到127?8 位二进制的表示能力Byte 类型占用 8 位(1 字节)存储空间,共有2^8 = 256种可能的二进制组合在补码体系中,最高位为符号位(0 正 1 负),剩余 7 位表示数值补码表示法的规则正数和零:补码与原码相同,范围是 0000 0000(0)到 0111 1111(127),共 128 个值负数:补码 = 原码取反 + 1,范围是 1000 0001(-127)到 1111 1111(-1),占 127 个值关键点:1000 0000 被定义为 -128二进制(补码)    十进制值1000 0000    -1281000 0001    -1271000 0010    -126...    …1111 1110    -21111 1111    -10000 0000    00000 0001    10000 0010    2...    …0111 1110    1260111 1111    127为何不是 -127 到 127?若范围设为 -127 到 127(含 0),仅能表示 127 + 128 = 255 127 + 128 = 255127+128=255 个值,无法覆盖全部 256 种组合补码的连续性要求:将 1000 0000 分配给 -128 后:数值序列形成闭环:127(0111 1111)+1 溢出为 -128(1000 0000),实现连续循环若不这样设计,会浪费一个二进制组合(1000 0000),且破坏数值连续性,-127(1000 0001)-1正好是-128(1000 0000)编程语言中的实际表现Byte.MAX_VALUE = 127,Byte.MIN_VALUE = -128赋值超出范围(如 byte b = 128;)会触发编译错误,如127 + 1 = -128(因 0111 1111 + 1 = 1000 0000)三、 眼见为实:Java代码验证补码Integer.toBinaryString(int i) 方法会返回一个整数补码表示的字符串(省略前导零,负数显示完整的32位,int占4个字节)public class ComplementDemo {    public static void main(String[] args) {        int positive = 5;        int negative = -5;        // 打印正数5的二进制(补码,省略前导零)        System.out.println(Integer.toBinaryString(positive)); // 输出: 101        // 打印负数-5的二进制(32位完整补码)        System.out.println(Integer.toBinaryString(negative)); // 输出: 1111 1111 1111 1111 1111 1111 1111 1011    }}解读负数输出:11111111111111111111111111111011 就是 -5 的 32 位补码它是由 +5 (00000000_00000000_00000000_00000101) 按位取反 (11111111_11111111_11111111_11111010)再加 1 得到的 (11111111_11111111_11111111_11111011)总结原码:直观但有双零,运算复杂(历史概念)反码:试图改进运算,仍有双零和循环进位问题(历史概念)补码 (Java的选择):统一零表示,完美支持 A - B = A + (-B),硬件实现高效简单,是现代计算机整数表示的标准Java实践:byte, short, int, long 均用补码。Integer.toBinaryString() 可查看补码形式————————————————原文链接:https://blog.csdn.net/qq_35512802/article/details/148684625
  • [技术干货] JVM内存泄漏问题怎么排查
    在排查 JVM 内存泄漏问题时,需要综合利用监控工具、日志分析、堆转储(Heap Dump)分析和代码审查,逐步缩小问题范围,定位泄漏源。下面介绍一种系统化的排查流程及常用方法:1. 监控与日志分析监控内存使用趋势利用 Prometheus、Grafana、JConsole、VisualVM 等工具,监控堆内存、非堆内存、GC 次数和停顿时间。如果发现堆内存使用率持续上升,或者 GC 次数频繁增加(尤其是 Full GC 频率增加),可能存在内存泄漏。开启 GC 日志配置 JVM 参数(例如:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log)记录垃圾回收日志。分析 GC 日志,观察垃圾回收前后堆内存的变化,是否存在“GC overhead limit exceeded”异常等。2. Heap Dump 分析采集 Heap Dump当发现内存使用异常时,可以使用 jmap -dump 或者通过监控工具(如 JVisualVM、Java Mission Control)生成堆转储文件(.hprof 文件)。可以在 OOM(OutOfMemoryError)发生时自动生成 Heap Dump(通过 -XX:+HeapDumpOnOutOfMemoryError)。使用内存分析工具利用 Eclipse Memory Analyzer Tool(MAT)、VisualVM 的插件或 JProfiler 分析 Heap Dump。主要分析点:找出占用内存最多的对象及其数量(疑似泄漏点)。查看对象的引用链(Reference Chain),定位哪些引用没有及时释放,比如长生命周期的静态变量、集合(List、Map)等未清理的容器。分析类加载器问题,排查是否存在因为类未卸载导致的内存泄漏(尤其在动态部署的场景)。3. 代码审查与排查工具代码审查检查是否存在未关闭的资源(如数据库连接、IO 流)、缓存未清理、静态集合持续累积数据等常见内存泄漏原因。注意循环引用、事件监听器未注销、定时任务中使用匿名内部类等情况。使用专业工具使用 JProfiler、YourKit 等商业分析工具,进行实时内存监控和泄漏检测,观察对象分配和垃圾回收情况。断点调试与日志记录对可疑模块加入内存监控日志、统计特定对象的创建与释放,辅助排查内存泄漏的细节。4. 调优与验证调整 GC 参数根据监控结果和 Heap Dump 分析结果,调优垃圾回收参数(如堆大小、年轻代比例、GC 算法),改善内存回收效果,并观察是否能缓解内存增长问题。持续集成和测试编写压力测试和长时间运行测试,验证问题修复情况。采用内存泄漏检测工具(如 Java 的 LeakCanary 或 Apache JMeter 配合内存监控)持续监控内存使用。总结排查 JVM 内存泄漏问题的基本步骤是:监控与日志分析:观察内存使用趋势和 GC 日志,确认异常现象。Heap Dump 分析:采集堆转储,利用 MAT 或其他工具查找内存占用最多的对象和引用链。代码审查与调试:检查资源释放、缓存管理和事件监听等代码,结合专业工具进行实时监控。调优与验证:调整 JVM 参数,优化代码后通过测试验证内存泄漏是否得到解决。通过这种系统化的排查方法,可以较快定位内存泄漏的根本原因,并在此基础上进行相应的优化和修复。————————————————原文链接:https://blog.csdn.net/longgelaile/article/details/145513376
  • [技术干货] 面向对象进阶 | 深入探究 Java 静态成员与继承体系
    一、static(静态)static表示静态,是Java中的一个修饰符,可以修饰成员方法,成员变量。1.static 静态变量被static修饰的成员变量,叫做静态变量。特点:被该类的所有对象共享不属于对象,属于类随着类的加载而加载,优于对象存在调用方式:类名调用(推荐)对象名调用代码展示需求:写一个JavaBean类来描述这个班级的学生属性:姓名、年龄、性别行为:学习JAVA Bean 类package staticdemo;public class Student {    //属性:姓名,年龄,性别    //新增:老师的姓名    private String name;    private int age;    private String gender;    public  static  String teacherName;    public Student() {    }    public Student(String name, int age, String gender) {        this.name = name;        this.age = age;        this.gender = gender;    }    /**     * 获取     * @return name     */    public String getName() {        return name;    }    /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }    /**     * 获取     * @return age     */    public int getAge() {        return age;    }    /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }    /**     * 获取     * @return gender     */    public String getGender() {        return gender;    }    /**     * 设置     * @param gender     */    public void setGender(String gender) {        this.gender = gender;    }    //行为    public void study(){        System.out.println(name+"正在学习");    }    public void show(){        System.out.println(name+","+age+","+gender+","+teacherName);    }}测试类package staticdemo;public class StudentTest {    public static void main(String[] args) {        //1.创建第一个学生对象        Student s1 = new Student();        s1.setName("张三");        s1.setAge(20);        s1.setGender("男");        //公共类,但是s2没有创建对象,所以无法访问teacherName,为null        //public String teacherName;        //于是我们想了个方法,用static修饰teacherName,但是这样就变成了静态属性,所有对象共享        //public  static  String teacherName;        s1.teacherName = "王老师";        //还可以用类名.属性名来访问        //Student.teacherName = "王老师";        s1.study();        s1.show();        //2.创建第二个学生对象        Student s2 = new Student();        s2.setName("李四");        s2.setAge(21);        s2.setGender("女");        s2.study();        s2.show();    }}内存图栈内存方法调用时会在栈内存中创建栈帧。这里main方法首先入栈 ,在main方法执行过程中:执行 Student.teacherName = "阿玮老师"; ,这一步只是对静态变量赋值,在栈内存中记录这个操作。执行 Student s1 = new Student(); 时,在栈内存为引用变量 s1 分配空间,存放指向堆内存中 Student 对象的地址(假设为 0x0011 ) 。执行 s1.name = "张三"; 和 s1.age = 23; ,是通过 s1 引用操作堆内存中对象的实例变量。执行 s1.show(); 时,show 方法的栈帧入栈,在栈帧中记录方法内的局部变量(这里无额外局部变量)以及要操作的对象属性(通过 s1 找到堆内存对象属性)。执行 Student s2 = new Student(); ,在栈内存为引用变量 s2 分配空间,存放指向堆内存中另一个 Student 对象的地址(假设为 0x0022 ) 。执行 s2.show(); 时,show 方法栈帧再次入栈,通过 s2 引用操作其对应的堆内存对象属性。堆内存当执行new Student()时,在堆内存创建Student对象实例。第一个 Student 对象(对应 s1 ),在堆内存中分配空间存储实例变量 name 值为 “张三” ,age 值为 23 。第二个 Student 对象(对应 s2 ),在堆内存中分配空间存储实例变量 name 初始值 null (字符串默认初始值) ,age 初始值 0 (整数默认初始值) 。静态变量 teacherName 存储在堆内存的静态存储位置(静态区),值为 “阿玮老师” ,所有 Student 类的对象共享这个静态变量。注意:静态变量随类的出现而出现,优于变量。2.static 静态方法被static修饰的成员方法,叫做静态方法。特点:多用在测试类和工具类中Javabean类中很少会用调用方式:类名调用(推荐)对象名调用工具类:帮助我们做一些事情的,但是不描述任何事物的类Javabean类:用来描述一类事物的类。比如:Student、Teather、Dog等测试类:用来检查其他类是否书写正确,带有main方法的类,是程序的入口遵守的规范:类名见名知意私有化构造方法方法定义为静态的练习:第一题:需求:在实际开发中,经常会遇到一些数组使用的工具类请按照如下要求编写一个数组的工具类:ArrayUtil工具类:package sta02;public class ArrayUtil {    //私有构造方法,防止外部实例化    private ArrayUtil() {}    public static String printArray(int[] arr) {        StringBuilder sb = new StringBuilder();        sb.append( "[");        for (int i = 0; i < arr.length; i++) {            sb.append(arr[i]);            if (i < arr.length - 1) {                sb.append(", ");            }else {                sb.append("]");            }        }        return sb.toString();    }    public static double getArray(double[] arr) {        double sum = 0;        for (int i = 0; i < arr.length; i++) {            sum += arr[i];  //累加数组元素        }        return sum/arr.length;    }}测试类package sta02;public class Testdemo {    public static void main(String[] args) {        //测试printArray方法        int[] arr = {1, 2, 3, 4, 5};        String result = ArrayUtil.printArray(arr);        System.out.println(result);        //测试getArray方法        double[] arr2 = {1.0, 2.0, 3.0, 4.0, 5.0};        double average = ArrayUtil.getArray(arr2);        System.out.println(average);    }}第二题:需求:定义一个集合,用于存储3个学生对象学生类的属性:name、age、gender定义一个工具类,用于获取集合中最大学生的年龄JavaBean类:package sat03;public class Student {    private String name;    private int age;    private String gender;    public Student() {    }    public Student(String name, int age, String gender) {        this.name = name;        this.age = age;        this.gender = gender;    }    /**     * 获取     * @return name     */    public String getName() {        return name;    }    /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }    /**     * 获取     * @return age     */    public int getAge() {        return age;    }    /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }    /**     * 获取     * @return gender     */    public String getGender() {        return gender;    }    /**     * 设置     * @param gender     */    public void setGender(String gender) {        this.gender = gender;    }}方法类:package sat03;import java.util.ArrayList;public class StudentUtil {    //私有构造方法,防止外部实例化    private StudentUtil() {}    //静态方法    public static int getMaxScore(ArrayList<Student> list ) {        //1.定义一个参照物        int maxAge = list.get(0).getAge();        //2.遍历集合        for (int i = 1; i < list.size(); i++) {            if (list.get(i).getAge() > maxAge) {                maxAge = list.get(i).getAge();            }        }        return maxAge;    }}测试类:package sat03;import java.util.ArrayList;public class testmax {    public static void main(String[] args) {        //1.创建一个集合来存储        ArrayList<Student> list = new ArrayList<Student>();        //2.创建3个学生对象        Student s1 = new Student("Zhangsan", 20, "男");        Student s2 = new Student("lisi", 23, "男");        Student s3 = new Student("wangwu", 25, "女");        //3.将学生对象添加到集合中        list.add(s1);        list.add(s2);        list.add(s3);        //4.调用方法        int max=StudentUtil.getMaxScore(list);        System.out.println(max);    }}3.static注意事项静态方法只能访问静态变量和静态方法非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法静态方法中是没有this关键字的总结:静态方法中,只能访问静态非静态方法可以访问所有静态方法中没有this关键字4.重新认识main方法public class HelloWorld{    public static void main (String[] args){        System.out.println("HelloWorld");    }}public : 被JVM调用,访问权限足够大static :被JVM调用,不用创建对象,直接类名访问​ 因为main方法是静态的,所以测试类中其他方法也是需要是静态的void : 被JVM调用,不需要给JVM返回值main : 一个通用的名称,虽然不是关键字,但是被JVM识别String[] args :以前用于接受键盘录入数据的,现在没用二、继承面向对象三大特征:封装、继承、多态封装:对象代表什么,就得封装对应的数据,并提供数据对应的行为。我们发现在Student类与Teacher类中有重复的元素,于是为了使程序更加便捷便出现了”继承“1.继承概述java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系public class Student extends Person{}1Student称为子类(派生类),Person称为父类(基类或超类)优点:可以把多个子类中重复的代码抽取到父类中,提高代码的复用性子类可以在父类的基础上,增加其他的功能,使子类更强大什么时候用继承?当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码2.继承的特点java中只支持单继承,不支持多继承,但支持多层继承单继承:一个子类只能继承一个直接父类不支持多继承:子类不能同时继承多个父类多层继承:子类A继承父类B,父类B可以继承父类CC是A的间接父类每一个类都直接或者间接的继承于Object练习:核心点:共性内容抽取,子类是父类中的一种写代码是从父类开始写,最后写子类JAVABean类package st5;public class Animal {    public void eat() {        System.out.println("我会吃饭");    }    public void water (){        System.out.println("我会喝水");    }}package st5;public class Cat extends Animal {    public void mice() {        System.out.println("我会抓老鼠");    }}package st5;public class Dog extends Animal {    public void lookhome() {        System.out.println("我会看家");    }}package st5;public class Ragdoll extends Cat{}package st5;public class Lihua extends Cat{}package st5;public class Husky extends Dog{    public void breakhome() {        System.out.println("我会拆家");    }}package st5;public class Teddy extends Dog{    public void Ceng(){        System.out.println("我喜欢蹭一蹭");    }}测试类:package st5;public class Test {    public static void main(String[] args) {        //创建对象并调用方法        //创建布偶猫的对象        Ragdoll rd = new Ragdoll();        System.out.println("我是布偶猫");        rd.mice();        rd.water();        rd.eat();        System.out.println("-------------------");        //创建狸花猫的对象        Lihua lh = new Lihua();        System.out.println("我是狸花猫");        lh.mice();        lh.water();        lh.eat();        System.out.println("-------------------");        //创建泰迪的对象        Teddy td = new Teddy();        System.out.println("我是泰迪");        td.lookhome();        td.water();        td.eat();        td.Ceng();        System.out.println("-------------------");        //创建哈士奇的对象        Husky hs = new Husky();        System.out.println("我是哈士奇");        hs.lookhome();        hs.water();        hs.eat();        hs.breakhome();    }}试运行:注意:子类只能访问父类中非私有的成员3.子类到底能继承父类中的哪些内容构造方法    非私有 不能    private 不能成员变量    非私有 能    private 能 但不能直接用成员方法    虚方法表 能    否则 不能虚方法表:就是经常要用的方法,什么叫虚方法表呢?非private 非static非final4.继承中访问特点继承中:成员变量的访问特点public class Fu{    String name = "Fu";}public class Zi extends Fu{    String name = "Zi";    public void ziShow(){        String name = "ziShow";        System.out.println(name);    }}//就近原则:谁离我近,我就用谁//完整版就近原则:先在局部位置找,本类成员位置找,父类成员位置找,逐级往上//run:ziShow如果出现了重名的成员变量怎么找:System.out.println(name);//从局部位置开始往上找System.out.println(this.name);//从本类成员位置开始往上找System.out.println(super.name);//从父类成员位置开始往上找public class Test{    public static void main (String [] args){        Zi z = new Zi();        z.ziShow();    }}public class Fu{    String name = "Fu";}public class Zi extends Fu{    String name = "Zi";    public void ziShow(){        String name = "ziShow";        System.out.println(name);//ziShow        System.out.println(this.name);//Zi        System.out.println(super.name);//Fu    }}继承中:成员方法的访问特点直接调用满足就近原则:谁离我近,我就用谁super调用,直接访问父类package jicehng;public class test {    public static void main(String[] args) {        //创建一个对象        Student s = new Student();        s.lunch();        /*        吃面条        咖啡        吃米饭        喝水         */    }}class Person {    public void eat(){        System.out.println("吃米饭");    }    public void water(){        System.out.println("喝水");    }}class Student extends Person {    public void lunch(){        this.eat();//就近读取子类吃面条        this.water();//就近读取子类咖啡        super.eat();//调用父类吃米饭        super.water();//调用父类喝水    }    public void eat(){        System.out.println("吃面条");    }    public void water(){        System.out.println("咖啡");    }}方法的重写:当父类的方法不能满足子类现在的需求时,需要进行方法重写书写格式:在继承体系中 ,子类出现了和父类中一模一样的方法声明,我们就称子类这个方法是重写的方法@Override重写体系@Override是放在重写后的方法上,校验子类重写时语法是否正确加上注解后如果有红色波浪线,表示语法错误建议重写方法都加@Override注解,代码安全,优雅!方法重写注意事项和要求:重写方法的名称、形参列表必须于父类中的一致子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写<protected<public)子类重写父类方法时,返回值类型必须小于等于父类建议:重写的方法尽量和父类保持一致只有被添加到虚方法表中的方法才能被重写JAVABean类:package jice;public class Dog {    public void eat() {        System.out.println("Dog is eating.");    }    public void drink() {        System.out.println("Dog is drinking.");    }    public void lookhome() {        System.out.println("Dog is lookhome.");    }}package jice;public class hashiqi extends Dog {    public void breakhome() {        System.out.println("hashiqi is breakhome.");    }}package jice;public class shapi extends Dog{    @Override    public void eat() {        super.eat();// 调用父类的eat方法        System.out.println("shapi is eating gouliang.");    }}package jice;public class chinesedog extends Dog{    @Override    public void eat() {        super.eat();// 调用父类的eat方法        System.out.println("Chinesedog is eating chinesefood.");    }}测试类package jice;public class test {    public static void main(String[] args) {        hashiqi hashiqi = new hashiqi();        hashiqi.eat();        hashiqi.drink();        hashiqi.lookhome();        shapi shapi = new shapi();        shapi.eat();        shapi.drink();        shapi.lookhome();        chinesedog chinesedog = new chinesedog();        chinesedog.eat();        chinesedog.drink();    }}继承中:构造方法的访问特点父类中的构造方法不会被子类继承子类中所有的构造方法默认先访问父类中的无参构造,再执行自己为什么?子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化怎么调用父类构造方法的?子类构造方法的第一行语句默认都是:super(),不写也存在,且必须在第一行如果想调用父类有参构造,必须手动写super进行调用this、super使用总结this:理解为一个变量,表示当前发给发调用者的地址值;super:代表父类存储空间关键字    访问成员变量    访问成员方法    访问构造方法this    this.成员变量 访问本类成员变量    this.成员方法(…) 访问本类成员方法    this(…) 访问本类构造方法super    super.成员变量 访问父类成员变量    super.成员方法(…) 访问父类成员方法    super(…) 访问父类构造方法 chinesedog chinesedog = new chinesedog();    chinesedog.eat();    chinesedog.drink();}}### 继承中:构造方法的访问特点- 父类中的构造方法不会被子类继承- 子类中所有的构造方法默认先访问父类中的无参构造,再执行自己  为什么?  - 子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。  - 子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化  怎么调用父类构造方法的?  - 子类构造方法的第一行语句默认都是:`super()`,不写也存在,且必须在第一行  - 如果想调用父类有参构造,必须手动写`super`进行调用### this、super使用总结this:理解为一个变量,表示当前发给发调用者的地址值;super:代表父类存储空间| 关键字 | 访问成员变量                     | 访问成员方法                           | 访问构造方法                 || ------ | -------------------------------- | -------------------------------------- | ---------------------------- || this   | this.成员变量  访问本类成员变量  | this.成员方法(...) 访问本类成员方法  | this(...)  访问本类构造方法  || super  | super.成员变量  访问父类成员变量 | super.成员方法(...) 访问父类成员方法 | super(...)  访问父类构造方法 |————————————————原文链接:https://blog.csdn.net/2401_87533975/article/details/148388640
总条数:737 到第
上滑加载中