• [技术干货] Java应用内存优化:从GC调优到内存泄漏排查
    1. 背景在开发大型Java应用程序时,内存管理是一个至关重要的环节。Java虚拟机(JVM)中的垃圾回收机制(GC)在一定程度上可以帮助程序员管理内存,但随着程序规模的扩大和复杂度的提升,内存使用和GC的效率成为了性能瓶颈。本篇文章将介绍如何通过调优GC策略、减少对象分配以及排查内存泄漏来优化Java应用的内存使用,提升程序的稳定性和运行效率。2. JVM内存模型JVM将内存分为堆内存(Heap)和栈内存(Stack)。堆内存是用于存储所有Java对象的,而栈内存则存储方法调用和局部变量。2.1 JVM内存分区    •    Eden区:新创建的对象首先存放于此区。    •    Survivor区:经过GC后未被清除的对象会转移到Survivor区。    •    老年代(Old Generation):在Survivor区存活时间较长的对象会进入老年代。    •    永久代/元数据区(Permanent/Metaspace):用于存储类的元数据。3. 垃圾回收机制(GC)调优3.1 GC的种类在JVM中,有几种不同的垃圾回收器,适用于不同场景:    •    Serial GC:单线程GC,适用于小型应用。    •    Parallel GC:多线程GC,适用于吞吐量优先的应用。    •    CMS(Concurrent Mark-Sweep)GC:适用于低延迟场景,GC过程与应用线程并行运行。    •    G1 GC:设计用于处理大内存堆,兼顾吞吐量和低延迟的需求。3.2 如何选择合适的GC策略通过分析应用的性能需求,可以选择合适的GC策略。例如,如果应用要求较低的延迟,可以考虑使用CMS或G1 GC。在服务器上运行的高并发应用,则可能更适合使用Parallel GC。3.3 调优GC参数通过调整JVM的启动参数,可以进一步优化GC的行为:-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200这些参数可以帮助我们设置堆内存的初始大小、最大大小以及垃圾回收器类型等。通过合理的GC调优,可以减少应用程序的停顿时间和内存抖动。4. 内存泄漏问题的排查4.1 常见的内存泄漏原因    •    静态变量持有对象引用:静态变量的生命周期与JVM相同,如果静态变量没有被正确释放,它们引用的对象将不会被GC回收。    •    未关闭的资源(如文件流、数据库连接等):如果使用完毕后未及时关闭,可能会导致内存泄漏。    •    不正确的数据结构使用:例如,使用HashMap等数据结构时,未正确清除过期或不再使用的对象。4.2 使用工具分析内存泄漏可以使用以下工具来排查内存泄漏:    •    VisualVM:JDK自带的内存分析工具,可以帮助监控堆内存使用情况。    •    Eclipse Memory Analyzer (MAT):强大的内存分析工具,可以帮助分析内存快照,找出内存泄漏的根源。通过在应用运行过程中生成堆快照(heap dump),可以使用这些工具分析内存中哪些对象占用了大量空间,以及它们的引用路径,从而帮助我们排查出具体的内存泄漏问题。5. 优化内存使用的最佳实践5.1 避免频繁的对象创建频繁创建短生命周期的小对象会增加GC压力,影响性能。可以考虑使用对象池(Object Pool)或StringBuilder等技术来避免频繁创建不必要的对象。5.2 使用弱引用(WeakReference)对于一些无需强引用的对象,可以使用弱引用(WeakReference)或软引用(SoftReference)来避免对象长时间驻留在内存中,从而减少内存压力。5.3 注意集合类的使用在使用集合类(如ArrayList, HashMap等)时,应该根据实际需要指定合适的初始大小,避免动态扩容带来的额外开销。6. 结论Java内存管理虽然有JVM的垃圾回收机制来辅助,但对于大型复杂应用,合理的GC调优、对象分配优化和内存泄漏排查仍是不可忽视的工作。通过以上方法,可以有效减少内存使用,优化Java应用的性能和稳定性。————————————————原文链接:https://blog.csdn.net/weixin_42063627/article/details/142310062
  • [技术干货] 在 Eclipse 中接入 DeepSeek:开启 Java 开发新效率
    在 Java 开发的广阔天地里,开发效率是每个开发者都关注的重点。DeepSeek 作为人工智能技术在代码辅助领域的佼佼者,凭借强大的代码生成、智能补全以及深度代码理解能力,为开发者提供了极大的便利。而 Eclipse,作为一款经典且被广泛使用的 Java 集成开发环境,拥有丰富的插件生态和完善的开发工具。当我们将 DeepSeek 接入 Eclipse,就如同为 Eclipse 插上了智能的翅膀,能够显著提升 Java 开发的效率与质量。本文将详细介绍在 Eclipse 中接入 DeepSeek 的方法,帮助开发者充分利用这两者的结合,在 Java 开发中如虎添翼。二、DeepSeek 功能概述(一)强大的代码生成能力DeepSeek 能够根据自然语言描述,精准生成高质量的 Java 代码片段。无论是简单的方法编写,比如 “编写一个 Java 方法,用于计算两个整数的乘积”,还是复杂的类和功能模块创建,例如 “创建一个 Java 类,实现文件的读写操作,并包含异常处理机制”,它都能快速给出合理且可运行的代码示例。DeepSeek 生成的代码不仅实现了基本功能,还遵循 Java 的编码规范,添加了必要的注释,让代码更易于理解和维护,大大节省了开发者从头编写代码的时间和精力。(二)深度代码理解与分析对于庞大复杂的 Java 项目代码库,DeepSeek 具备深度解析的能力。在接手一个全新的大型 Java 项目时,开发者往往需要花费大量时间去梳理代码逻辑、理解各个类和方法之间的关系。DeepSeek 可以帮助开发者快速理清项目结构,识别关键代码路径,理解代码的核心功能和业务逻辑,从而显著缩短熟悉项目的周期,加快开发进度。(三)智能代码补全在编写 Java 代码过程中,DeepSeek 的智能补全功能能够实时预测开发者接下来可能输入的代码。它会根据当前代码的上下文,包括类的定义、方法的参数、已导入的包等信息,以及 Java 编程的常见模式,提供准确且有针对性的补全建议。例如,当在一个操作数据库的类中编写代码时,它会优先提供与数据库连接、查询、更新等相关的方法和类的补全选项,让代码编写更加流畅高效,减少因思考代码细节而产生的停顿。三、在 Eclipse 中接入 DeepSeek 的前期准备(一)安装 Eclipse确保你已经安装了合适版本的 Eclipse。如果尚未安装,可以前往 Eclipse 官方网站(Eclipse Downloads | The Eclipse Foundation ),根据你的操作系统和开发需求,选择相应的 Eclipse 安装包进行下载。安装过程相对简单,按照安装向导的提示逐步操作即可完成。安装完成后,启动 Eclipse。(二)获取 DeepSeek API 密钥要在 Eclipse 中使用 DeepSeek,首先需要获取其 API 密钥。访问 DeepSeek 的官方平台,进行注册并登录账号。在个人设置或 API 管理相关页面,找到生成 API 密钥的选项,按照平台提供的详细指引,生成属于你自己的唯一 API 密钥。请务必妥善保管这个密钥,因为它是连接你与 DeepSeek 服务的关键凭证,一旦泄露,可能会导致安全风险和服务滥用。四、具体接入步骤(一)安装 DeepSeek 插件打开 Eclipse,点击菜单栏中的 “Help”(帮助),选择 “Eclipse Marketplace...”(Eclipse 应用市场)。在弹出的 Eclipse 应用市场窗口中,找到搜索框。在搜索框中输入 “DeepSeek”,然后点击搜索按钮。在搜索结果中找到对应的 DeepSeek 插件。点击插件右侧的 “Install”(安装)按钮,Eclipse 会开始下载并安装插件。在安装过程中,可能会出现一些提示询问你是否信任插件的来源等信息,按照提示进行确认操作。安装完成后,点击 “Finish”(完成),然后根据提示重启 Eclipse,使插件生效。(二)配置 API 密钥重启 Eclipse 后,点击菜单栏中的 “Window”(窗口),选择 “Preferences”(首选项)。在弹出的首选项窗口中,找到 “DeepSeek” 选项(通常在列表的较下方,如果找不到,可以在搜索框中输入 “DeepSeek” 进行搜索)。点击 “DeepSeek” 选项,在右侧的设置页面中,将之前获取的 DeepSeek API 密钥粘贴到 “API Key” 对应的输入框中。点击 “Test Connection”(测试连接)按钮,检查 Eclipse 是否能够成功连接到 DeepSeek 服务。如果连接成功,会弹出提示框显示连接正常;若连接失败,请仔细检查 API 密钥是否正确,以及网络连接是否稳定。确认连接成功后,点击 “OK” 保存设置。此时,Eclipse 与 DeepSeek 之间的连接已初步建立。(三)使用 DeepSeek 功能打开一个 Java 项目或新建一个 Java 文件。将光标定位到需要使用 DeepSeek 功能的代码位置,比如在类中需要编写一个新的方法,或者在方法体中需要补充代码逻辑。可以通过快捷键(默认快捷键可在插件设置中查看和修改)或者右键点击鼠标,在弹出的菜单中选择与 DeepSeek 相关的操作选项。常见的操作选项包括 “Generate Code with DeepSeek”(使用 DeepSeek 生成代码)、“Code Completion by DeepSeek”(DeepSeek 智能补全)等。以 “Generate Code with DeepSeek” 为例,选择该选项后,会弹出一个输入框,在其中输入自然语言描述,例如 “编写一个 Java 方法,实现对 List 集合中的元素进行排序”。输入完成后,点击 “OK”,等待 DeepSeek 处理请求。DeepSeek 会根据你的描述生成相应的 Java 代码,并将其插入到光标所在的位置。你可以对生成的代码进行审查、修改和完善,使其符合项目的具体需求和编码规范。五、使用技巧与注意事项(一)使用技巧精准描述需求:在使用 DeepSeek 生成代码时,尽可能提供详细、准确的自然语言描述。描述越清晰,生成的代码就越符合你的预期。例如,不要简单地说 “写个排序方法”,而是详细说明 “用 Java 编写一个使用快速排序算法对整数数组进行排序的方法,并返回排序后的数组”,这样可以让 DeepSeek 生成更具针对性和实用性的代码。利用上下文信息优化补全:在使用智能补全功能时,充分利用当前代码的上下文环境。DeepSeek 会根据上下文信息,如当前类的继承关系、已定义的变量和方法、导入的包等,提供更精准的补全建议。比如在一个继承自 HttpServlet 的类中编写代码时,它会优先提供与 HTTP 请求处理相关的方法和类的补全选项。多轮交互优化代码质量:如果 DeepSeek 生成的代码不完全符合需求,可以通过多轮交互来优化。例如,生成的代码实现了基本功能,但性能不佳,你可以在生成的代码基础上,再次使用 DeepSeek 功能,输入 “优化这段代码的性能,减少时间复杂度”,DeepSeek 会根据已有代码和新的指令,进一步调整代码,提高代码的质量和效率。(二)注意事项保护 API 密钥安全:API 密钥是连接你与 DeepSeek 服务的重要凭证,一旦泄露,可能会带来严重的安全问题。不要将 API 密钥分享给他人,避免在不安全的网络环境或不可信的设备上使用。如果发现 API 密钥有泄露风险,应立即在 DeepSeek 官方平台上重新生成新的密钥,并在 Eclipse 中及时更新配置。遵守服务使用规则:DeepSeek 的服务可能存在一些使用限制,如调用频率限制、使用时长限制、请求内容限制等。在使用过程中,要仔细阅读并严格遵守 DeepSeek 的服务条款和使用规则。避免因频繁调用服务或超出使用配额而导致服务异常或被限制使用。如果遇到调用失败的情况,首先检查是否违反了服务使用规则,并根据提示进行相应调整。严格审查生成的代码:虽然 DeepSeek 生成的代码质量较高,但由于其基于算法和大量数据生成,可能存在一些潜在问题,如与项目特定的编码风格不一致、在某些特殊场景下存在逻辑错误等。在将生成的代码应用到实际项目中之前,务必进行严格的代码审查。可以从代码的正确性、可读性、可维护性以及安全性等多个角度进行审查,确保代码符合项目的要求和标准。六、总结通过在 Eclipse 中接入 DeepSeek,Java 开发者能够充分利用人工智能技术的优势,显著提升开发效率和代码质量。从前期的安装准备,到具体的接入步骤,再到使用过程中的技巧和注意事项,每一个环节都为打造高效智能的开发环境提供了有力支持。希望本文能够帮助你顺利地将 DeepSeek 融入到 Eclipse 的开发流程中,让你在 Java 开发的道路上更加得心应手,创造出更优秀的 Java 应用程序。随着人工智能技术的不断发展,相信会有更多类似的强大工具出现,为 Java 开发领域带来更多的创新和变革,让我们共同期待并积极拥抱这些变化。————————————————原文链接:https://blog.csdn.net/fq1986614/article/details/145885315
  • [技术干货] Java四大常用JSON解析性能对比:Hutool、Fastjson2、Gson与Jackson测试
    1. 引言JSON 是现代软件开发中常用的数据交换格式,尤其在微服务和前后端分离的架构中更是必不可少。本文将对 Java 中四大主流 JSON 解析库——Hutool、Fastjson2、Gson 和 Jackson 进行性能测试和对比分析,通过实测 20 万条数据解析,揭示各库在批量和逐条处理中的表现。测试结果仅供参考!!! 请多次测试再抉择2. 环境与依赖2.1 环境信息操作系统:Window11JDK 版本:jdk1.8.0_281CPU :  AMD Ryzen 9 7945HX内存:32GB2.2 Maven依赖在项目的 pom.xml 文件中引入以下依赖:      <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->        <dependency>            <groupId>com.google.code.gson</groupId>            <artifactId>gson</artifactId>            <version>2.10.1</version>        </dependency>        <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->        <dependency>            <groupId>com.alibaba.fastjson2</groupId>            <artifactId>fastjson2</artifactId>            <version>2.0.52</version>        </dependency>        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->        <dependency>            <groupId>com.fasterxml.jackson.core</groupId>            <artifactId>jackson-databind</artifactId>            <version>2.14.2</version>        </dependency>        <dependency>            <groupId>cn.hutool</groupId>            <artifactId>hutool-all</artifactId>            <version>5.8.35</version>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>        </dependency>3. 测试代码3.1 数据模型定义一个简单的实体对象:import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor; /** * @author 阿水 */@Data@AllArgsConstructor@NoArgsConstructorpublic class Variable {    private int id;    private String name;    private double value;    private String description;    private String type;}3.2 测试数据生成模拟 20 万条数据用于测试:  // 生成测试数据    public static List<Variable> generateData(int size) {        List<Variable> list = new ArrayList<>(size);        for (int i = 0; i < size; i++) {            Variable data = new Variable();            data.setId(i);            data.setName("Name" + i);            data.setValue(Math.random() * 1000);            data.setDescription(IdUtil.fastSimpleUUID());            data.setType(IdUtil.fastSimpleUUID()+i);            list.add(data);        }        return list;    }3.3 四大库序列化与反序列化测试import cn.hutool.core.util.IdUtil;import cn.hutool.json.JSONUtil;import com.alibaba.fastjson2.JSON;import com.fasterxml.jackson.core.type.TypeReference;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.gson.Gson;import com.google.gson.reflect.TypeToken;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;import java.util.List;import java.util.stream.Collectors; /** * @description: JSON 序列化、解析性能测试 * @author 阿水 */@Slf4jpublic class JsonBenchmarkTest {    //数据总条数    public static int dataSize = 200000;    //测试次数    public static  int iterations = 10;     public static void main(String[] args) throws Exception {        // 生成测试数据        List<Variable> testData = generateData(dataSize);        log.info("测试数据总条数为: {} 条", dataSize);        log.info("以下测试结果均为进行 {} 次计算之后,耗费时间取平均值计算得出。", iterations);        // 序列化测试        String jsonString = serializationTest(testData);        log.info("JSON 数据总大小为: {} 字节", jsonString.length());         // 批量解析测试        log.info("===== 使用批量解析 JSON(即解析集合API)=====");        batchParseTest(jsonString);         // 单条解析测试        log.info("===== 循环遍历逐个解析 JSON API =====");        singleParseTest(jsonString);         // 单条解析并插入集合测试        log.info("===== 循环遍历逐个解析 JSON并插入集合 API=====");        singleParseAndAddListTest(jsonString);    }    // 生成测试数据    public static List<Variable> generateData(int size) {        List<Variable> list = new ArrayList<>(size);        for (int i = 0; i < size; i++) {            Variable data = new Variable();            data.setId(i);            data.setName("Name" + i);            data.setValue(Math.random() * 1000);            data.setDescription(IdUtil.fastSimpleUUID());            data.setType(IdUtil.fastSimpleUUID()+i);            list.add(data);        }        return list;    }    /**     * 序列化测试     */    private static String serializationTest(List<Variable> testData) throws Exception {        String jsonResult = null;        // Hutool        long hutoolTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            String hutoolJson = JSONUtil.toJsonStr(testData);            long end = System.currentTimeMillis();            hutoolTotal += (end - start);            if (i == 0) jsonResult = hutoolJson; // 保存结果        }        log.info("HuTool 序列化平均耗时: {} ms", hutoolTotal / iterations);         // Fastjson2        long fastjsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            String fastjsonJson = JSON.toJSONString(testData);            long end = System.currentTimeMillis();            fastjsonTotal += (end - start);        }        log.info("Fastjson2 序列化平均耗时: {} ms", fastjsonTotal / iterations);         // Gson        Gson gson = new Gson();        long gsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            String gsonJson = gson.toJson(testData);            long end = System.currentTimeMillis();            gsonTotal += (end - start);        }        log.info("Gson 序列化平均耗时: {} ms", gsonTotal / iterations);         // Jackson        ObjectMapper objectMapper = new ObjectMapper();        long jacksonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            String jacksonJson = objectMapper.writeValueAsString(testData);            long end = System.currentTimeMillis();            jacksonTotal += (end - start);        }        log.info("Jackson 序列化平均耗时: {} ms", jacksonTotal / iterations);         return jsonResult;    }     /**     * 批量解析测试     */    private static void batchParseTest(String jsonString) throws Exception {        // Hutool        long hutoolTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            List<Variable> result = JSONUtil.toList(JSONUtil.parseArray(jsonString), Variable.class);            long end = System.currentTimeMillis();            hutoolTotal += (end - start);        }        log.info("HuTool 批量解析平均耗时: {} ms", hutoolTotal / iterations);         // Fastjson2        long fastjsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            List<Variable> result = JSON.parseArray(jsonString, Variable.class);            long end = System.currentTimeMillis();            fastjsonTotal += (end - start);        }        log.info("Fastjson2 批量解析平均耗时: {} ms", fastjsonTotal / iterations);         // Gson        Gson gson = new Gson();        long gsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            List<Variable> result = gson.fromJson(jsonString, new TypeToken<List<Variable>>() {}.getType());            long end = System.currentTimeMillis();            gsonTotal += (end - start);        }        log.info("Gson 批量解析平均耗时: {} ms", gsonTotal / iterations);         // Jackson        ObjectMapper objectMapper = new ObjectMapper();        long jacksonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            List<Variable> result = objectMapper.readValue(jsonString, new TypeReference<List<Variable>>() {});            long end = System.currentTimeMillis();            jacksonTotal += (end - start);        }        log.info("Jackson 批量解析平均耗时: {} ms", jacksonTotal / iterations);    }     /**     * 单条解析测试     */    private static void singleParseTest(String jsonString) throws Exception {        List<String> messageList = JSON.parseArray(jsonString, Variable.class).stream()                .map(JSON::toJSONString)                .collect(Collectors.toList());        // Hutool        long hutoolTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = JSONUtil.toBean(msg, Variable.class);            }            long end = System.currentTimeMillis();            hutoolTotal += (end - start);        }        log.info("HuTool 单条解析平均耗时: {} ms", hutoolTotal / iterations);         // Fastjson2        long fastjsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = JSON.parseObject(msg, Variable.class);            }            long end = System.currentTimeMillis();            fastjsonTotal += (end - start);        }        log.info("Fastjson2 单条解析平均耗时: {} ms", fastjsonTotal / iterations);         // Gson        Gson gson = new Gson();        long gsonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = gson.fromJson(msg, Variable.class);            }            long end = System.currentTimeMillis();            gsonTotal += (end - start);        }        log.info("Gson 单条解析平均耗时: {} ms", gsonTotal / iterations);         // Jackson        ObjectMapper objectMapper = new ObjectMapper();        long jacksonTotal = 0;        for (int i = 0; i < iterations; i++) {            long start = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = objectMapper.readValue(msg, Variable.class);            }            long end = System.currentTimeMillis();            jacksonTotal += (end - start);        }        log.info("Jackson 单条解析平均耗时: {} ms", jacksonTotal / iterations);    }    /**     * 循环遍历单条解析并插入集合测试     */    /**     * 循环遍历单条解析并插入集合测试 (平均耗时计算)     */    static void singleParseAndAddListTest(String jsonString) throws Exception {        // 转换为模拟 MQ 消息体        List<String> messageList = JSON.parseArray(jsonString, Variable.class).stream()                .map(JSON::toJSONString) // 将每个对象转为 JSON 字符串模拟单条消息                .collect(Collectors.toList());         // 平均耗时变量定义        double hutoolTotalTime = 0;        double fastjsonTotalTime = 0;        double gsonTotalTime = 0;        double jacksonTotalTime = 0;         // 循环 10 次计算平均耗时        for (int i = 0; i < iterations; i++) {             // 1. Hutool JSONUtil 单条解析            List<Variable> hutoolList = new ArrayList<>();            long hutoolStart = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = JSONUtil.toBean(msg, Variable.class);                hutoolList.add(v); // 将对象存入集合            }            long hutoolEnd = System.currentTimeMillis();            hutoolTotalTime += (hutoolEnd - hutoolStart);             // 2. Fastjson2 单条解析            List<Variable> fastjsonList = new ArrayList<>();            long fastjsonStart = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = JSON.parseObject(msg, Variable.class);                fastjsonList.add(v);            }            long fastjsonEnd = System.currentTimeMillis();            fastjsonTotalTime += (fastjsonEnd - fastjsonStart);             // 3. Gson 单条解析            List<Variable> gsonList = new ArrayList<>();            Gson gson = new Gson();            long gsonStart = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = gson.fromJson(msg, Variable.class);                gsonList.add(v);            }            long gsonEnd = System.currentTimeMillis();            gsonTotalTime += (gsonEnd - gsonStart);             // 4. Jackson 单条解析            List<Variable> jacksonList = new ArrayList<>();            ObjectMapper objectMapper = new ObjectMapper();            long jacksonStart = System.currentTimeMillis();            for (String msg : messageList) {                Variable v = objectMapper.readValue(msg, Variable.class);                jacksonList.add(v);            }            long jacksonEnd = System.currentTimeMillis();            jacksonTotalTime += (jacksonEnd - jacksonStart);        }         // 输出平均耗时结果        log.info("HuTool 单条解析并存入集合平均耗时: {} ms", hutoolTotalTime / iterations);        log.info("Fastjson2 单条解析并存入集合平均耗时: {} ms", fastjsonTotalTime / iterations);        log.info("Gson 单条解析并存入集合平均耗时: {} ms", gsonTotalTime / iterations);        log.info("Jackson 单条解析并存入集合平均耗时: {} ms", jacksonTotalTime / iterations);    } }4. 20W条数据、31098673字节测试结果分析(仅供参考 仅供参考 仅供参考 !!!)库名称    序列化+反序列化总耗时    性能排名Fastjson2    110ms左右    🥇 第一名Jackson    170ms左右    🥈 第二名Gson    210ms左右    🥉 第三名Hutool    1800ms左右    第四名5. 性能分析与总结测试结果分析Fastjson2性能表现: 测试结果中,无论是批量解析、逐条解析还是逐条解析并插入集合,它的速度都是最快的(30ms、39ms、39.8ms)。特性优势: 支持复杂对象结构解析,API 设计简洁高效,并修复了旧版 Fastjson 的反序列化漏洞,安全性更高。适用场景: 高并发、大数据量处理及性能敏感型应用场景。Hutool性能表现: 表现最慢,尤其在批量解析(637ms)和逐条解析(589ms)中远落后于其他库。特性优势: API 优雅轻便,开发效率高,但性能瓶颈明显,不适合超大规模数据解析。适用场景: 适合中小规模项目的快速开发,便捷性优先于性能要求的场合。Jackson性能表现: 表现较优,解析速度仅次于 Fastjson2(78ms、101ms、99.8ms),兼顾性能与功能复杂性。特性优势: 支持复杂 Java 对象和自定义配置,兼容性强,适合复杂数据结构映射需求。适用场景: 企业级应用、大型复杂系统开发及对灵活性要求高的项目。Gson性能表现: 性能优于 Hutool,但低于 Fastjson2 和 Jackson(93ms、119ms、119.5ms)。特性优势: API 简单直观,开发成本低,但在大数据量或高并发处理时性能表现不够理想。适用场景: 小规模数据解析需求,或对性能要求不高的任务。6. 推荐选择需求类型    推荐库    适用场景描述性能优先    Fastjson2    在所有测试场景中速度最快,适合高性能和高并发场景,适合日志分析、大数据处理和消息队列数据解析等应用。轻量便捷    Hutool    更适合中小规模项目的快速开发,虽然性能较低,但 API 优雅轻便,适合便捷性优先的场合。功能复杂需求    Jackson    兼顾性能与灵活性,支持复杂数据结构和自定义配置,适合大型企业级应用或复杂对象映射需求。简单解析需求    Gson    API 简洁易用,适合小规模数据解析任务,学习成本低,但不适合大数据和高并发需求。7. 结论与建议Fastjson2:性能最高,适合高并发与大数据处理需求。安全性较高,是性能敏感应用的首选。Hutool:开发便捷但性能较低,适合中小规模项目。如果对开发效率要求高且数据量适中,可以选择它。Jackson:性能与灵活性兼顾,适合复杂对象和企业级系统。面向需要自定义解析规则的场景表现更出色。Gson:简单易用但性能低于 Fastjson2 和 Jackson。适合小型项目或对性能要求不高的场合。注意事项安全性: Fastjson2 安全性最佳,其他库需关注版本更新,以避免反序列化漏洞。兼容性: Jackson 在跨平台兼容性和复杂结构处理方面表现更佳。性能评估: 项目正式使用前,应基于实际生产环境进行更大规模的性能测试和压力测试。仅当前测试数据明确显示:Fastjson2 性能最佳,适合高性能需求。Hutool 性能最慢,更适合便捷开发而非大规模数据解析。Jackson 在性能和灵活性之间取得平衡,适合复杂应用场景。Gson 表现优于 Hutool,但略逊于 Fastjson2 和 Jackson,适合轻量级需求。————————————————原文链接:https://blog.csdn.net/lps12345666/article/details/144931886
  • [技术干货] 【Java从入门到起飞】面向对象编程
    1. 抽象类1.1 概述1.1.1 抽象类引入​ 父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。 抽象方法 : 没有方法体的方法。抽象类:包含抽象方法的类。1.2 abstract使用格式abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。 1.2.1 抽象方法使用abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。 定义格式: 修饰符 abstract 返回值类型 方法名 (参数列表); 代码举例: public abstract void run(); 1.2.2 抽象类如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。 定义格式: abstract class 类名字 {   } 代码举例: public abstract class Animal {    public abstract void run();} 1.2.3 抽象类的使用要求:继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。 代码举例: // 父类,抽象类abstract class Employee {private String id;private String name;private double salary; public Employee() {} public Employee(String id, String name, double salary) {this.id = id;this.name = name;this.salary = salary;} // 抽象方法// 抽象方法必须要放在抽象类中abstract public void work();} // 定义一个子类继承抽象类class Manager extends Employee {public Manager() {}public Manager(String id, String name, double salary) {super(id, name, salary);}// 2.重写父类的抽象方法@Overridepublic void work() {System.out.println("管理其他人");}} // 定义一个子类继承抽象类class Cook extends Employee {public Cook() {}public Cook(String id, String name, double salary) {super(id, name, salary);}@Overridepublic void work() {System.out.println("厨师炒菜多加点盐...");}} // 测试类public class Demo10 {public static void main(String[] args) {// 创建抽象类,抽象类不能创建对象// 假设抽象类让我们创建对象,里面的抽象方法没有方法体,无法执行.所以不让我们创建对象// Employee e = new Employee();// e.work(); // 3.创建子类Manager m = new Manager();m.work(); Cook c = new Cook("ap002", "库克", 1);c.work();}} 此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法。 1.3 抽象类的特征抽象类的特征总结起来可以说是 有得有失 有得:抽象类得到了拥有抽象方法的能力。 有失:抽象类失去了创建对象的能力。 其他成员(构造方法,实例方法,静态方法等)抽象类都是具备的。 1.4 抽象类的细节不需要背,只要当idea报错之后,知道如何修改即可。 关于抽象类的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。 理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。 理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。 抽象类存在的意义是为了被子类继承。 理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。 1.5 抽象类存在的意义​ 抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。 2. 接口2.1 概述我们已经学完了抽象类,抽象类中可以用抽象方法,也可以有普通方法,构造方法,成员变量等。那么什么是接口呢?接口是更加彻底的抽象,JDK7之前,包括JDK7,接口中全部是抽象方法。接口同样是不能创建对象的。 2.2 定义格式//接口的定义格式:interface 接口名称{    // 抽象方法} // 接口的声明:interface// 接口名称:首字母大写,满足“驼峰模式” 2.3 接口成分的特点在JDK7,包括JDK7之前,接口中的只有包含:抽象方法和常量 2.3.1.抽象方法​ 注意:接口中的抽象方法默认会自动加上public abstract修饰程序员无需自己手写!!​ 按照规范:以后接口中的抽象方法建议不要写上public abstract。因为没有必要啊,默认会加上。 2.3.2 常量在接口中定义的成员变量默认会加上: public static final修饰。也就是说在接口中定义的成员变量实际上是一个常量。这里是使用public static final修饰后,变量值就不可被修改,并且是静态化的变量可以直接用接口名访问,所以也叫常量。常量必须要给初始值。常量命名规范建议字母全部大写,多个单词用下划线连接。 2.3.3 案例演示public interface InterF {    // 抽象方法!    //    public abstract void run();    void run();     //    public abstract String getName();    String getName();     //    public abstract int add(int a , int b);    int add(int a , int b);      // 它的最终写法是:    // public static final int AGE = 12 ;    int AGE  = 12; //常量    String SCHOOL_NAME = "百度"; } 2.4 基本的实现2.4.1 实现接口的概述类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类,也可以称为接口的子类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。 2.4.2 实现接口的格式/**接口的实现:    在Java中接口是被实现的,实现接口的类称为实现类。    实现类的格式:*/class 类名 implements 接口1,接口2,接口3...{ } 从上面格式可以看出,接口是可以被多实现的。大家可以想一想为什么呢? 2.4.3 类实现接口的要求和意义必须重写实现的全部接口中所有抽象方法。如果一个类实现了接口,但是没有重写完全部接口的全部抽象方法,这个类也必须定义成抽象类。意义:接口体现的是一种规范,接口对实现类是一种强制性的约束,要么全部完成接口申明的功能,要么自己也定义成抽象类。这正是一种强制性的规范。2.4.4 类与接口基本实现案例假如我们定义一个运动员的接口(规范),代码如下: /**   接口:接口体现的是规范。 * */public interface SportMan {    void run(); // 抽象方法,跑步。    void law(); // 抽象方法,遵守法律。    String compittion(String project);  // 抽象方法,比赛。} 接下来定义一个乒乓球运动员类,实现接口,实现接口的实现类代码如下: package com.itheima._03接口的实现;/** * 接口的实现: *    在Java中接口是被实现的,实现接口的类称为实现类。 *    实现类的格式: *      class 类名 implements 接口1,接口2,接口3...{ * * *      } * */public class PingPongMan  implements SportMan {    @Override    public void run() {        System.out.println("乒乓球运动员稍微跑一下!!");    }     @Override    public void law() {        System.out.println("乒乓球运动员守法!");    }     @Override    public String compittion(String project) {        return "参加"+project+"得金牌!";    }} 测试代码: public class TestMain {    public static void main(String[] args) {        // 创建实现类对象。        PingPongMan zjk = new PingPongMan();        zjk.run();        zjk.law();        System.out.println(zjk.compittion("全球乒乓球比赛"));     }} 2.4.5 类与接口的多实现案例类与接口之间的关系是多实现的,一个类可以同时实现多个接口。 首先我们先定义两个接口,代码如下: /** 法律规范:接口*/public interface Law {    void rule();} /** 这一个运动员的规范:接口*/public interface SportMan {    void run();} 然后定义一个实现类: /** * Java中接口是可以被多实现的: *    一个类可以实现多个接口: Law, SportMan * * */public class JumpMan implements Law ,SportMan {    @Override    public void rule() {        System.out.println("尊长守法");    }     @Override    public void run() {        System.out.println("训练跑步!");    }} 从上面可以看出类与接口之间是可以多实现的,我们可以理解成实现多个规范,这是合理的。 2.5 接口与接口的多继承Java中,接口与接口之间是可以多继承的:也就是一个接口可以同时继承多个接口。大家一定要注意: 类与接口是实现关系 接口与接口是继承关系 接口继承接口就是把其他接口的抽象方法与本接口进行了合并。 案例演示: public interface Abc {    void go();    void test();} /** 法律规范:接口*/public interface Law {    void rule();    void test();}  * *  总结: *     接口与类之间是多实现的。 *     接口与接口之间是多继承的。 * */public interface SportMan extends Law , Abc {    void run();} 2.6扩展:接口的细节不需要背,只要当idea报错之后,知道如何修改即可。 关于接口的使用,以下为语法上要注意的细节,虽然条目较多,但若理解了抽象的本质,无需死记硬背。 当两个接口中存在相同抽象方法的时候,该怎么办?只要重写一次即可。此时重写的方法,既表示重写1接口的,也表示重写2接口的。 实现类能不能继承A类的时候,同时实现其他接口呢?继承的父类,就好比是亲爸爸一样实现的接口,就好比是干爹一样可以继承一个类的同时,再实现多个接口,只不过,要把接口里面所有的抽象方法,全部实现。 实现类能不能继承一个抽象类的时候,同时实现其他接口呢?实现类可以继承一个抽象类的同时,再实现其他多个接口,只不过要把里面所有的抽象方法全部重写。 实现类Zi,实现了一个接口,还继承了一个Fu类。假设在接口中有一个方法,父类中也有一个相同的方法。子类如何操作呢?处理办法一:如果父类中的方法体,能满足当前业务的需求,在子类中可以不用重写。处理办法二:如果父类中的方法体,不能满足当前业务的需求,需要在子类中重写。 如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?可以在接口跟实现类中间,新建一个中间类(适配器类)让这个适配器类去实现接口,对接口里面的所有的方法做空重写。让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象 3. 内部类3.1 概述3.1.1 什么是内部类将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。可以把内部类理解成寄生,外部类理解成宿主。 3.1.2 什么时候使用内部类一个事物内部还有一个独立的事物,内部的事物脱离外部的事物无法独立使用 人里面有一颗心脏。汽车内部有一个发动机。为了实现更好的封装性。3.2 内部类的分类按定义的位置来分 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)局部内部类,类定义在方法内匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。3.3 成员内部类成员内部类特点: 无static修饰的内部类,属于外部类对象的。宿主:外部类对象。内部类的使用格式:  外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类 获取成员内部类对象的两种方式: 方式一:外部直接创建成员内部类的对象 外部类.内部类 变量 = new 外部类().new 内部类(); 方式二:在外部类中定义一个方法提供内部类的对象 案例演示 方式一:public class Test {    public static void main(String[] args) {        //  宿主:外部类对象。       // Outer out = new Outer();        // 创建内部类对象。        Outer.Inner oi = new Outer().new Inner();        oi.method();    }} class Outer {    // 成员内部类,属于外部类对象的。    // 拓展:成员内部类不能定义静态成员。    public class Inner{        // 这里面的东西与类是完全一样的。        public void method(){            System.out.println("内部类中的方法被调用了");        }    }}  方式二:public class Outer {    String name;    private class Inner{        static int a = 10;    }    public Inner getInstance(){        return new Inner();    }} public class Test {    public static void main(String[] args) {        Outer o = new Outer();        System.out.println(o.getInstance());      }} 3.4 成员内部类的细节编写成员内部类的注意点: 成员内部类可以被一些修饰符所修饰,比如: private,默认,protected,public,static等在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。创建内部类对象时,对象中有一个隐含的Outer.this记录外部类对象的地址值。(请参见3.6节的内存图)详解: ​ 内部类被private修饰,外界无法直接获取内部类的对象,只能通过3.3节中的方式二获取内部类的对象 ​ 被其他权限修饰符修饰的内部类一般用3.3节中的方式一直接获取内部类的对象 ​ 内部类被static修饰是成员内部类中的特殊情况,叫做静态内部类下面单独学习。 ​ 内部类如果想要访问外部类的成员变量,外部类的变量必须用final修饰,JDK8以前必须手动写final,JDK8之后不需要手动写,JDK默认加上。 3.5 成员内部类面试题请在?地方向上相应代码,以达到输出的内容 注意:内部类访问外部类对象的格式是:外部类名.this public class Test {    public static void main(String[] args) {        Outer.inner oi = new Outer().new inner();        oi.method();    }} class Outer { // 外部类    private int a = 30;     // 在成员位置定义一个类    class inner {        private int a = 20;         public void method() {            int a = 10;            System.out.println(???); // 10   答案:a            System.out.println(???); // 20 答案:this.a            System.out.println(???); // 30 答案:Outer.this.a        }    }} 3.6 成员内部类内存图  3.7 静态内部类静态内部类特点: 静态内部类是一种特殊的成员内部类。有static修饰,属于外部类本身的。总结:静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类.内部类。拓展1:静态内部类可以直接访问外部类的静态成员。拓展2:静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。拓展3:静态内部类中没有银行的Outer.this。内部类的使用格式: 外部类.内部类。 静态内部类对象的创建格式: 外部类.内部类  变量 = new  外部类.内部类构造器; 调用方法的格式: 调用非静态方法的格式:先创建对象,用对象调用调用静态方法的格式:外部类名.内部类名.方法名();案例演示: // 外部类:Outer01class Outer01{    private static  String sc_name = "黑马程序";    // 内部类: Inner01    public static class Inner01{        // 这里面的东西与类是完全一样的。        private String name;        public Inner01(String name) {            this.name = name;        }        public void showName(){            System.out.println(this.name);            // 拓展:静态内部类可以直接访问外部类的静态成员。            System.out.println(sc_name);        }    }} public class InnerClassDemo01 {    public static void main(String[] args) {        // 创建静态内部类对象。        // 外部类.内部类  变量 = new  外部类.内部类构造器;        Outer01.Inner01 in  = new Outer01.Inner01("张三");        in.showName();    }} 3.8 局部内部类局部内部类 :定义在方法中的类。定义格式: class 外部类名 {数据类型 变量名; 修饰符 返回值类型 方法名(参数列表) {// …class 内部类 {// 成员变量// 成员方法}}} 3.9 匿名内部类【重点】3.9.1 概述匿名内部类 :是内部类的简化写法。他是一个隐含了名字的内部类。开发中,最常用到的内部类就是匿名内部类了。 3.9.2 格式new 类名或者接口名() {     重写方法;}; 包含了: 继承或者实现关系 方法重写 创建对象 所以从语法上来讲,这个整体其实是匿名内部类对象 3.9.2 什么时候用到匿名内部类实际上,如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用 是为了简化代码。 之前我们使用接口时,似乎得做如下几步操作: 定义子类重写接口中的方法创建子类对象调用重写后的方法interface Swim {    public abstract void swimming();} // 1. 定义接口的实现类class Student implements Swim {    // 2. 重写抽象方法    @Override    public void swimming() {        System.out.println("狗刨式...");    }} public class Test {    public static void main(String[] args) {        // 3. 创建实现类对象        Student s = new Student();        // 4. 调用方法        s.swimming();    }} 我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。 3.9.3 匿名内部类前提和格式匿名内部类必须继承一个父类或者实现一个父接口。 匿名内部类格式 new 父类名或者接口名(){    // 方法重写    @Override     public void method() {        // 执行语句    }}; 3.9.4 使用方式以接口为例,匿名内部类的使用,代码如下: interface Swim {    public abstract void swimming();} public class Demo07 {    public static void main(String[] args) {        // 使用匿名内部类new Swim() {@Overridepublic void swimming() {System.out.println("自由泳...");}}.swimming();         // 接口 变量 = new 实现类(); // 多态,走子类的重写方法        Swim s2 = new Swim() {            @Override            public void swimming() {                System.out.println("蛙泳...");            }        };         s2.swimming();        s2.swimming();    }} 3.9.5 匿名内部类的特点定义一个没有名字的内部类这个类实现了父类,或者父类接口匿名内部类会创建这个没有名字的类的对象3.9.6 匿名内部类的使用场景通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。———————————————— 原文链接:https://blog.csdn.net/fj123789/article/details/145681640
  • [技术干货] 【Java篇】静动交融,内外有别:从静态方法到内部类的深度解析
    文章目录类和对象(下)八、static 关键字8.1 静态变量8.1.1 概念8.1.2 访问方式8.2 静态方法8.2.1 概念8.2.2 访问限制8.3 static成员变量初始化8.4 静态成员的使用场景九、代码块8.1 代码块概念以及分类8.2 普通代码块(局部代码块)8.3 构造代码块(实例代码块)8.3.1 执行时机8.3.2 作用和特点8.4 静态代码块(静态初始化块)8.4.1 执行时机8.4.2 使用场景8.4.3 示例8.4.4 注意事项8.5 同步代码块(了解)十、内部类10.1 内部类概述10.2 成员内部类10.2.1 基本语法10.2.2 访问规则10.3 静态内部类10.3.1 基本语法10.3.2 访问方式10.4 局部内部类10.4.1 特点10.5 匿名内部类(抽象类和接口时详细介绍)10.5.1 基本写法10.5.2 特点10.6 小结十一、对象的打印11.1 Object 的 toString()11.2 重写 toString()11.2.1 基本示例11.2.2 重写要点11.3 打印数组11.4 小结十二、总结与展望类和对象(下)欢迎讨论:如果你对本篇内容有任何疑问或想深入探讨,欢迎在评论区留言交流!点赞、收藏与分享:觉得内容有帮助就请点赞、收藏并分享给更多学习Java的小伙伴! 继续学习之旅:本篇文章将详细讲解 static 关键字、代码块以及内部类的相关概念和使用,让你在面向对象的世界中更进一步,玩转 Java 的高级特性!八、static 关键字static(静态)是 Java 中的一个关键字,主要用于修饰类中的变量或方法。被 static 修饰的成员属于 类本身 而非某个实例对象。它在编译阶段就会被加载到方法区中(Java 8 之后元空间/Metaspace),无需通过创建对象就能访问。其生命周期伴随类的一生(即:随类的加载而创建,随类的卸载而销毁)8.1 静态变量8.1.1 概念非静态变量:也称为实例变量,每创建一个对象就会产生一份新的变量副本。静态变量:也称为类变量,只有一份共享的副本,所有该类对象都可以访问和修改。public class Counter {    public static int count = 0; // 静态变量    public int id;              // 实例变量    public Counter() {        count++;        this.id = count;    }    public static void main(String[] args) {        Counter c1 = new Counter();        Counter c2 = new Counter();        System.out.println(Counter.count); // 2        System.out.println(c1.id);         // 1        System.out.println(c2.id);         // 2    }}count 为静态变量,全类共享;id 为实例变量,每个对象拥有自己的 id 值。8.1.2 访问方式类名.静态变量(推荐)对象名.静态变量(不推荐,容易引起误解)System.out.println(Counter.count); // 推荐System.out.println(c1.count);      // 不推荐8.2 静态方法8.2.1 概念被 static 修饰的方法属于类本身。无需创建对象,就可以通过 类名.方法名() 的方式直接调用。在静态方法中,不能 直接访问非静态成员(因为实例成员依赖于对象的存在,而静态方法执行时可能尚未创建任何对象)。public class MathUtil {    public static int add(int a, int b) {        return a + b;    }        public static void main(String[] args) {        int sum = MathUtil.add(3, 5);        System.out.println(sum); // 8    }}8.2.2 访问限制静态方法中只能访问 静态成员;非静态方法中则可以同时访问静态成员和非静态成员。8.3 static成员变量初始化注意:静态成员变量一般不会在构造方法中进行初始化,因为构造方法与对象实例关联,而静态成员变量则属于类本身。静态成员变量的初始化有两种方式:就地初始化:在定义时直接给出初值静态代码块初始化:在 static { ... } 代码块中完成赋值(下文再讲)就地初始化示例代码:public class Student {    private String name;    private String gender;    private int age;        // 静态成员变量就地初始化    private static String classRoom = "Bit36";    // 构造方法、普通方法、get/set方法等    // ...}8.4 静态成员的使用场景工具类/工具方法:如 Math 类的 sqrt()、abs() 等,方便直接通过类名调用。单例模式:通过静态字段持有唯一实例。计数器:统计对象数量或方法调用次数。常量池:public static final 定义常量,方便全局使用且避免重复创建。九、代码块8.1 代码块概念以及分类定义:在 Java 中使用 {} 包裹的一段或多段代码,即可称为“代码块”。常见分类:普通(局部)代码块:最常见,出现在方法体内部、流程控制语句中等,用来限制局部变量作用域或组织逻辑。构造代码块(实例代码块):定义在类中、方法外,不带 static,在构造方法执行之前运行,每次创建对象都会执行。静态代码块(静态初始化块):使用 static {} 修饰,类加载时(只一次)执行,用来初始化静态成员或做只需执行一次的操作。同步代码块:用 synchronized(锁对象) { ... } 来实现多线程同步,控制临界区内的线程安全,(入门只需了解)。8.2 普通代码块(局部代码块)普通代码块通常指在方法或语句块内部,用花括号 {} 包裹的那部分代码。它的主要功能是局部作用域的划分。示例:public class Main {    public static void main(String[] args) {        // 普通(局部)代码块        {            int x = 10;            System.out.println("x = " + x);        }        // x 的作用域仅限于上面的 {} 之内        // System.out.println(x); // 编译报错        // 再次声明一个同名变量 x,不会冲突        int x = 100;        System.out.println("x2 = " + x);    }}输出:x = 10x2 = 100要点:普通代码块会限制局部变量的作用范围。常用于封装临时逻辑或缩短变量生命周期,防止与其他同名变量冲突或占用资源太久。8.3 构造代码块(实例代码块)构造代码块(又称“实例初始化块”)是指在类中、方法外,但没有 static 修饰的一段 {} 代码。它在创建对象时会先于构造方法执行,用来对实例成员进行初始化或执行公共逻辑。从先于构造函数体执行的思想上看和C++的初始化列表有相似之处。8.3.1 执行时机每次调用 new 构造对象时,都会先执行构造代码块,然后再执行构造方法。如果有多个构造方法,则不管调用哪一个,都会先执行这段构造代码块。示例:public class Student {    private String name;    private int age;    // 构造代码块(实例代码块)    {        this.name = "bit";        this.age = 12;        System.out.println("I am instance init()!");    }    public Student() {        System.out.println("I am Student init()!");    }    public void show() {        System.out.println("name: " + name + "  age: " + age);    }    public static void main(String[] args) {        Student stu = new Student();        stu.show();    }}输出:I am instance init()!I am Student init()!name: bit  age: 12可以看到,构造代码块先于构造方法执行。8.3.2 作用和特点作用:可在对象创建前做一些实例变量的初始化或公共操作。执行次数:与 new 对象的次数相同,每次创建对象都会执行一次。与构造方法的关系:若有多个构造方法,可将公共初始化操作放到构造代码块,避免重复代码。执行顺序:构造代码块 → 构造方法。8.4 静态代码块(静态初始化块)静态代码块使用 static {} 修饰,是属于类级别的初始化逻辑。它会在类加载的时候执行一次,不随着对象创建反复执行。8.4.1 执行时机类第一次加载时,JVM 执行所有静态代码块。只执行一次,与后续创建多少对象无关。8.4.2 使用场景初始化静态成员变量。进行一次性的操作(如注册驱动、加载配置等)。8.4.3 示例public class Student {    private String name;    private int age;    private static String classRoom;    // 静态代码块    static {        classRoom = "bit306";        System.out.println("I am static init()!");    }    // 构造代码块    {        this.name = "bit";        this.age = 12;        System.out.println("I am instance init()!");    }    public Student() {        System.out.println("I am Student init()!");    }    public static void main(String[] args) {        System.out.println("----开始 main 方法----");        Student s1 = new Student();        Student s2 = new Student();    }}输出顺序示例:I am static init()!----开始 main 方法----I am instance init()!I am Student init()!I am instance init()!I am Student init()!8.4.4 注意事项静态代码块只执行一次,无论创建多少对象都不会重复执行。如果在同一个类里定义多个 static {},会按照代码顺序依次执行。(即合并)静态环境下无法访问实例成员;要访问实例变量或实例方法,需要先创建对象。8.5 同步代码块(了解)“同步代码块”并不是为了初始化而存在,而是为了解决多线程并发访问同一资源时的线程安全问题。写法一般是:synchronized(锁对象) {    // 需要线程同步的代码}当一个线程进入该代码块并持有“锁对象”时,其他线程只能等待,直到该线程执行完毕并释放锁。通常“锁对象”可用 this(当前实例)、某个类对象、或专门的锁实例等。十、内部类内部类(Inner Class)是将一个类的定义放在另一个类的内部,从而形成逻辑上的隶属关系。Java 提供了多种内部类形式,包括成员内部类、静态内部类、局部内部类以及匿名内部类。通过内部类,我们可以更好地封装和管理代码结构,也能直接访问外部类的私有成员,增强代码的灵活性和可读性。10.1 内部类概述定义:在一个类的内部再定义一个类(或接口),该类称之为“内部类”或“嵌套类”。分类:成员内部类(非静态内部类)静态内部类局部内部类(定义在方法或代码块内)匿名内部类(没有类名,直接定义并实例化)好处:内部类可以直接访问外部类的成员(包括私有成员),从而简化了访问操作。逻辑上隶属关系更清晰,起到封装与隐藏的作用。通过匿名内部类等方式,可以使代码更简洁,尤其在回调或事件监听等场景中。10.2 成员内部类成员内部类又叫非静态内部类,它是定义在外部类的成员位置(与外部类的成员变量、方法同级)但不带 static 关键字的内部类。10.2.1 基本语法public class Outer {    private String name = "OuterName";    // 成员内部类    public class Inner {        public void show() {            // 直接访问外部类的私有成员            System.out.println("Outer name: " + name);        }    }    public void test() {        Inner inner = new Inner();        inner.show();    }    public static void main(String[] args) {        Outer outer = new Outer();        outer.test();    }}执行流程:创建 Outer 对象:Outer outer = new Outer();在 outer.test() 方法中,实例化 Inner:Inner inner = new Inner();调用 inner.show(),可以直接访问 Outer 类中的 name。10.2.2 访问规则内部类可以直接访问外部类的所有成员(包括 private)。若内部类成员与外部类成员同名,可用 外部类名.this.成员 的方式区分,例如 Outer.this.name。在外部类的非静态方法中,可以直接创建内部类实例;在外部类的静态方法或其他类中,则需通过 外部类对象.new 内部类构造() 来创建。10.3 静态内部类静态内部类,也称静态嵌套类,使用 static 修饰。它与成员内部类的主要区别在于:静态内部类只能访问外部类的静态成员,无法直接访问外部类的非静态成员。创建静态内部类的对象时,不需要外部类对象的实例。10.3.1 基本语法public class Outer {    private String name = "OuterName";    private static String staticName = "StaticOuterName";    // 静态内部类    public static class Inner {        public void show() {            // 只能直接访问外部类的静态成员            System.out.println("Outer staticName: " + staticName);            // System.out.println(name); // 非静态成员,无法直接访问        }    }    public static void main(String[] args) {        // 不需要外部类对象,直接创建静态内部类对象        Outer.Inner inner = new Outer.Inner();        inner.show();    }}10.3.2 访问方式静态内部类对象的创建方式:外部类名.内部类名 对象名 = new 外部类名.内部类名();静态内部类中的实例方法,依然需要创建内部类实例来调用;但如果有静态方法或静态变量,可以通过 Outer.Inner.静态方法 或 Outer.Inner.静态变量 直接访问。10.4 局部内部类局部内部类是定义在方法体或代码块内部的类,只在该方法或代码块中可见和使用。局部内部类可以看作“更局部化”的内部类,常用于一些只在某个方法中使用的场景。public class Outer {    public void method() {        class Inner {            public void show() {                System.out.println("I am local inner class!");            }        }        // 在方法内部创建并使用        Inner inner = new Inner();        inner.show();    }    public static void main(String[] args) {        new Outer().method();    }}10.4.1 特点作用域限制:只能在定义它的方法或代码块中创建并使用。访问外部变量:可以访问外部类的成员,也可以访问该方法中被 final 或“事实上的最终”变量所修饰的局部变量(Java 8+ 开始,只要不修改该变量即可,不必显式 final)。10.5 匿名内部类(抽象类和接口时详细介绍)匿名内部类(Anonymous Inner Class)没有类名,通常用于简化创建某些接口或抽象类子类对象的过程,尤其在回调、事件处理等场景中使用广泛。10.5.1 基本写法interface ITest {    void func();}public class Demo {    public static void main(String[] args) {        // 匿名内部类:直接 new 接口(或抽象类),然后立刻重写其中的方法        ITest test = new ITest() {            @Override            public void func() {                System.out.println("Anonymous Inner Class func");            }        };        test.func();    }}new 接口/抽象类() { ... }:创建一个实现该接口或继承该抽象类的匿名子类对象。匿名:没有类名,直接在此处定义并实例化。10.5.2 特点只能使用一次:若需要多次创建同样功能的对象,通常还是单独定义一个类或使用 Lambda(对于函数式接口)更好。简化代码:不用显式定义一个实现类/子类。10.6 小结成员内部类:需要先创建外部类对象,再通过 外部类对象.new 内部类() 来实例化。可以访问外部类的所有成员。静态内部类:使用 static 修饰,只能直接访问外部类的静态成员。无需外部类实例即可创建,使用 外部类名.内部类名 方式。局部内部类:定义在方法或代码块内部,仅在该方法/代码块中可见。可以访问外部类的成员,也可以访问方法内“事实上的最终”变量。匿名内部类:没有类名,常用于简化接口或抽象类的实现。使用场景多在回调、事件监听等。掌握内部类的使用场景与写法,可以使代码的封装性更好,也能让某些实现方式更灵活、更简洁。在实际开发中,根据需求选择合适的内部类形式:若需要频繁调用且功能独立,建议成员内部类或静态内部类;若仅在某个方法中临时使用,且逻辑不复杂,可选择局部内部类或匿名内部类来简化代码。十一、对象的打印在 Java 中,当我们使用 System.out.println(obj); 或字符串拼接(如 "" + obj)来打印一个对象时,实质上是自动调用该对象的 toString() 方法。如果类中没有重写 toString(),则默认会调用 Object 类的 toString() 方法,打印出类似 类名@哈希值 的信息(如 com.bit.demo.Student@3f99bd52),往往并不是我们想要的“可读输出”。11.1 Object 的 toString()默认实现:Object 的 toString() 返回的字符串格式一般是:getClass().getName() + "@" + Integer.toHexString(hashCode())也就是“类的完整名称@哈希码”形式,便于识别对象在内存中的“标识”,但并不展示对象的具体属性信息。为什么常被打印?在使用 System.out.println(对象引用); 或字符串拼接时,会自动调用 toString()。如果没有重写该方法,就会使用 Object 的默认实现。11.2 重写 toString()为了打印出更有意义的信息,我们通常会在自定义的类中重写(Override) toString() 方法。这样当打印对象时,就能输出该对象的关键属性值或其他说明性内容。11.2.1 基本示例public class Student {    private String name;    private int age;    public Student(String name, int age) {        this.name = name;        this.age = age;    }    // 重写 toString() 方法    @Override    public String toString() {        return "Student{name='" + name + "', age=" + age + "}";    }    public static void main(String[] args) {        Student stu = new Student("Alice", 20);        System.out.println(stu);         // 自动调用 stu.toString() => 输出: Student{name='Alice', age=20}    }}11.2.2 重写要点方法签名:必须是 public String toString(),且带有 @Override 注解(可选但建议)。返回值:返回一个可读性强、能够体现对象核心信息的字符串。风格多样:可以根据需要自定义输出格式,或使用 JSON、XML 等形式。IDE 快捷生成:大多数 IDE(如 Eclipse、IntelliJ IDEA)可以自动生成 toString() 代码,方便使用。11.3 打印数组打印数组对象时,如果使用 System.out.println(arr); 也会得到类似 [Ljava.lang.String;@1540e19d 这样的结果(同样是 Object 的默认 toString())。如果想查看数组元素,可以使用以下方式:Arrays.toString(数组):适用于一维数组。String[] arr = {"Hello", "World"};System.out.println(Arrays.toString(arr)); // [Hello, World]Arrays.deepToString(数组):适用于多维数组。String[][] arr2D = {{"A", "B"}, {"C", "D"}};System.out.println(Arrays.deepToString(arr2D)); // [[A, B], [C, D]]11.4 小结默认打印对象:调用 Object.toString(),返回“类名@哈希值”,可读性差。重写 toString():通过在自定义类中重写 toString(),让打印对象时输出更有意义的属性信息。打印数组:使用 Arrays.toString() 或 Arrays.deepToString() 更好地展示数组内容。在实际开发中,重写 toString() 不仅方便调试与日志记录,也能让我们更直观地了解对象的核心数据。合理使用 toString() 让输出信息更友好,对日常开发帮助很大。十二、总结与展望本篇内容主要围绕以下三个方面展开:static关键字静态变量和静态方法在类级别上提供共享资源和操作,无需创建对象即可使用。静态代码块为类提供一次性初始化的机会。代码块包括静态代码块、实例代码块等,用于在不同阶段进行初始化操作。掌握代码块的执行顺序有助于理解对象的生命周期。内部类提供了更灵活的访问方式和封装机制。主要分为成员内部类、静态内部类、匿名内部类和局部内部类。未来的学习方向Java 继承与多态:继续探索面向对象的另外两个特性,理解类与类之间的继承关系以及动态绑定机制。接口与抽象类:深入理解接口与抽象类的应用场景,以及如何运用多态思想进行程序扩展。常用设计模式:结合内部类与静态特性,在单例、工厂、观察者等常见模式中,静态和内部类都有独特的用武之地。———————————————— 原文链接:https://blog.csdn.net/2301_79849925/article/details/146019987
  • [技术干货] Java对接微信支付全过程详解
    Java对接微信支付全过程详解引言在数字化商业蓬勃发展的今天,移动支付已成为连接用户与服务的核心纽带。微信支付凭借其庞大的用户基数和成熟的生态体系,成为企业拓展线上业务不可或缺的支付工具。然而,对于Java开发者而言,如何高效、安全地对接微信支付接口,构建稳定可靠的支付系统,仍面临诸多技术挑战——从复杂的签名验签机制到异步通知的幂等性处理,从证书管理到高并发场景下的性能优化,每一步都需要精准的技术设计与严谨的代码实现。本文将以系统性、实战性为导向,深入剖析Java对接微信支付的核心流程与关键技术。无论是Native支付、JSAPI支付还是小程序支付,其底层逻辑均围绕预支付订单生成、支付结果异步通知、订单状态主动查询三大核心环节展开。文章不仅提供清晰的代码示例(基于Spring Boot框架与微信支付V3 API),更聚焦于实际开发中的高频痛点:如何通过RSA签名保障通信安全?如何设计幂等回调接口避免重复扣款?如何利用微信平台证书防止伪造请求?这些问题将在文中逐一击破。此外,本文还将探讨企业级支付系统中的最佳实践,例如使用WechatPay Apache HttpClient简化证书管理、通过分布式锁实现订单状态同步、结合日志监控提升系统可观测性等。无论您是初探支付领域的开发者,还是需优化现有支付架构的技术负责人,均可从中获得从基础配置到高阶优化的完整知识链路,助力构建高可用、高安全的支付服务体系,为业务增长筑牢技术基石。一、准备工作注册微信商户平台获取商户号(mchid)、API密钥(API_KEY)。下载API证书(apiclient_cert.pem 和 apiclient_key.pem)。配置支付参数# application.propertieswxpay.mch-id=你的商户号wxpay.api-key=你的API密钥wxpay.notify-url=https://你的域名/api/wxpay/notify二、核心接口实现1. 生成预支付订单(Native支付)public class WxPayService {    private String mchId;    private String apiKey;    private String notifyUrl;    // 初始化参数(通过@Value注入或配置文件读取)    /**     * 生成Native支付二维码链接     */    public String createNativeOrder(String orderId, int amount) throws Exception {        String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";        Map<String, Object> params = new HashMap<>();        params.put("mchid", mchId);        params.put("appid", "你的AppID");  // 如果是公众号/小程序支付        params.put("description", "订单描述");        params.put("out_trade_no", orderId);        params.put("notify_url", notifyUrl);        params.put("amount", Map.of("total", amount, "currency", "CNY"));        // 生成签名并发送请求        String body = JSON.toJSONString(params);        String authorization = generateSignature("POST", url, body);                // 使用OkHttp或RestTemplate发送请求        String response = sendPostRequest(url, body, authorization);        return JSON.parseObject(response).getString("code_url");    }    /**     * 生成V3接口的Authorization头     */    private String generateSignature(String method, String url, String body) {        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);        String nonceStr = UUID.randomUUID().toString().replace("-", "");        String message = method + "\n" + url + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n";        String signature = signWithSHA256RSA(message, apiKey);  // 使用私钥签名        return "WECHATPAY2-SHA256-RSA2048 "                 + "mchid=\"" + mchId + "\","                + "nonce_str=\"" + nonceStr + "\","                + "timestamp=\"" + timestamp + "\","                + "serial_no=\"你的证书序列号\","                + "signature=\"" + signature + "\"";    }}2. 处理微信支付回调(关键!)@RestController@RequestMapping("/api/wxpay")public class WxPayCallbackController {    @PostMapping("/notify")    public String handleNotify(@RequestBody String requestBody,                               @RequestHeader("Wechatpay-Signature") String signature,                               @RequestHeader("Wechatpay-Timestamp") String timestamp,                               @RequestHeader("Wechatpay-Nonce") String nonce) {        // 1. 验证签名(防止伪造请求)        String message = timestamp + "\n" + nonce + "\n" + requestBody + "\n";        boolean isValid = verifySignature(message, signature, publicKey); // 使用微信平台公钥验证        if (!isValid) {            return "<xml><return_code>FAIL</return_code></xml>";        }        // 2. 解析回调数据        Map<String, String> result = parseXml(requestBody);        String orderId = result.get("out_trade_no");        String transactionId = result.get("transaction_id");                // 3. 幂等性处理:检查订单是否已处理        if (orderService.isOrderPaid(orderId)) {            return "<xml><return_code>SUCCESS</return_code></xml>";        }        // 4. 更新订单状态        orderService.updateOrderToPaid(orderId, transactionId);                // 5. 返回成功响应(必须!否则微信会重试)        return "<xml><return_code>SUCCESS</return_code></xml>";    }}3. 查询订单状态public class WxPayService {    public Map<String, String> queryOrder(String orderId) throws Exception {        String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"                     + orderId + "?mchid=" + mchId;                String authorization = generateSignature("GET", url, "");        String response = sendGetRequest(url, authorization);        return JSON.parseObject(response, Map.class);    }}三、关键注意事项签名验证所有回调必须验证签名,防止伪造请求。使用微信提供的平台证书验证。幂等性设计通过数据库唯一索引或Redis锁防止重复处理订单。证书管理推荐使用WechatPay Apache HttpClient简化证书处理:<dependency>    <groupId>com.github.wechatpay-apiv3</groupId>    <artifactId>wechatpay-apache-httpclient</artifactId>    <version>0.4.7</version></dependency>日志记录记录所有微信请求和回调,方便排查问题。四、完整调用示例(Spring Boot)@RestControllerpublic class PaymentController {    @Autowired    private WxPayService wxPayService;    @GetMapping("/createOrder")    public String createOrder(@RequestParam String orderId, @RequestParam int amount) {        try {            String codeUrl = wxPayService.createNativeOrder(orderId, amount);            return "<img src=\"https://example.com/qr?data=" + codeUrl + "\">";        } catch (Exception e) {            return "支付创建失败: " + e.getMessage();        }    }}五、常见问题解决证书加载失败:检查证书路径和格式(必须为PEM格式)。签名错误:使用微信官方验签工具调试。回调未触发:检查notify_url是否外网可访问,且返回SUCCESS。通过以上步骤,可完成微信支付的核心对接,确保支付流程的可靠性和安全性。————————————————原文链接:https://blog.csdn.net/lilinhai548/article/details/146105883
  • [技术干货] 庖丁解牛,洞悉 Java 面向对象的精妙架构
    文章目录#面向对象的介绍:一、设计对象并使用1.类和对象2.类的几个补充注意事项3.开发中类的设计二、封装1.封装的介绍2.封装的好处3.private关键字三、this关键字1.成员变量和局部变量2.举例代码详细解释:1. 类的成员变量定义2. 构造方法中的 `this` 关键字使用3. `set` 方法中的 `this` 关键字使用4. `get` 方法中的 `this` 关键字使用5. `displayInfo` 方法中的 `this` 关键字使用四、构造方法1.构造方法的概述2.构造方法的格式3.构造方法的作用4.构造方法的分类5.构造方法的注意事项五、标准JavaBean1.标准的JavaBean类六、对象内存图1.一个对象的内存图内存执行顺序解析(基于Java内存模型)**1. 类加载阶段(方法区)****2. 栈内存操作(main方法启动)****3. 堆内存分配(对象实例化)****4. 对象初始化流程****5. 变量关联与操作****6. 方法调用(方法区与栈协作)****内存操作完整流程总结****关键现象解释**2.多个对象的内存图**2.1、执行顺序与内存操作分步解析****1. 加载class文件(方法区)****2. 声明局部变量(栈内存)****3. 堆内存分配(对象空间开辟)****4. 默认初始化(堆内存)****5. 显示初始化(堆内存)****6. 构造方法初始化(堆内存)****7. 地址赋值(栈内存)****2.2、内存模型与对象独立性的关键验证****1. 对象独立性的体现****2. 内存操作流程图解****2.3、执行流程总结(分阶段)****2.4、常见问题解答****1. 为什么`System.out.println(s)` 输出地址?****2. 显示初始化和构造方法初始化有何区别?****3. 如何优化内存使用?**3.两个变量指向同一个对象内存图3.1、类加载阶段(方法区)3.2、栈内存操作(main方法栈帧)3.3、堆内存操作(对象关联)3.4、最终内存结构3.5、输出结果分析4.this的内存原理4.1、类加载阶段(方法区核心操作)4.2、对象实例化流程(堆栈协同)4.3、方法调用时的内存隔离(栈帧作用域)4.4、关键差异对比表4.5、技术扩展:`this`的底层实现5.基本数据类型和引用数据类型的区别5.1基本数据类型5.2引用数据类型七、补充知识:成员变量、局部变量区别#面向对象的介绍:面向:拿、找对象:能干活的东西面向对象编程:拿东西过来做对应的事情面向对象编程的例子:import java.util.Random;import java.util.Scanner;public class mian {    public static void main(String[] args) {        //面向对象,导入一个随机数        Random r = new Random();        int data = r.nextInt(10)+1;        //面向对象,输入一个随机数        System.out.println(data);        Scanner sc = new Scanner(System.in);        // 面向对象,输出一个数        System.out.println("请输入一个数:");        int a = sc.nextInt();        System.out.println(a);    }}为什么java要采取这种方法来编程呢?我们在程序之中要干某种事,需要某种工具来完成,这样更符合人类的思维习惯,编程更简单,更好理解。面向对象的重点学习对象是什么?学习获取已有对象并使用,学习如何自己设计对象并使用。——面向对象的语法一、设计对象并使用1.类和对象类(设计图):是对象共同特征的描述如何定义类:public class 类名{    1.成员变量(代表属性,一般是名词)    2.成员方法(代表行为,一般是动词)    3.构造器(后面学习)    4.代码块(后面学习)    5.内部类(后面学习)}public class Phone{    //属性(成员变量)    String brand;    double price;     public void call(){    }    public void playGame(){    }}如何得到对象?如何得到类的对象:类名 对象名= new 类名();Phone p = new Phone();对象:是真实存在的具体东西拿到对象后能做什么?对象.成员变量;对象.成员方法(...)在JAVA中,必须先设计类,才获得对象public class phone {    //属性    String name;    double price;    public void call(){        System.out.println("打电话");    }    public void send(){        System.out.println("发短信");    }}//测试public class phoneTest {    public static void main(String[] args) {        //创建手机对象        phone p = new phone();        //给手机对象赋值        p.name = "小米";        p.price = 1999;        //获取手机对象的属性值        System.out.println(p.name);        System.out.println(p.price);        //调用手机对象的方法        p.call();        p.send();    }}2.类的几个补充注意事项用来描述一类事物的类,专业叫做:Javabean类。在javabean类中,是不写main方法的。在以前,编写main方法的类,叫做测试类。我们可以在测试中创建javabean类的对象并进行赋值调用。public class 类名 {    1.成员变量(代表属性)    2.成员方法(代表行为)}public class Student {    //属性(成员变量)    String name;    int age;    //行为方法    public void study(){        System.out.println("好好学习,天天向上");    }    public void doHomework(){        System.out.println("键盘敲烂,月薪过万");    }}类名首字母建议大写,需要见名知意,驼峰模式。一个java文件中可以定义多个class类,且只能一个类是public修饰的类名必须成为代码文件名。实际开发中建议还是一个文件定义一个class类。成员变量的完整定义格式是:修饰符 数据类型 变量名称=初始化值;一般无需指定初始化值,存在默认值。int age;//这里不写初始化值是因为,这里学生的年龄是一个群体的值,没有一个固定的初始化值。//如果给age赋值,比如是18岁,那就代表者所有的学生年龄都是18岁。//类的赋值不是在类里面赋值,而是在创建了对象之后再赋值,这时赋值的时这个特定的对象。Student stu = new Student();Stu.name="张三";Stu.height=187;对象的成员变量的默认值规则数据类型    明细    默认值基本类型    byte,short,int,long    0基本类型    float,double    0.0基本类型    boolean    false引用类型    类、接口、数组、String    null//编写女朋友类,创建女朋友类的对象,给女朋友的属性赋值并调用女朋友类中的方法。自己思考女朋友有哪些属性,有哪些行为?public class girlFriend {    public static void main(String[] args) {        //创建女朋友对象        girl g = new girl();        //给女朋友对象赋值        g.name = "小红";        g.age = 20;        g.hobby = "唱歌";        //获取女朋友对象的属性值        System.out.println(g.name);        System.out.println(g.age);        System.out.println(g.hobby);        //调用女朋友对象的方法        g.eat();        g.sleep();    }}//这是一个类public class girl {    //成员变量(代表属性)    String name;    int age;    String hobby;    //成员方法(代表行为)    public void eat(){        System.out.println("吃饭");    }    public void sleep(){        System.out.println("睡觉");    }}3.开发中类的设计先把需求拿过来,先要看这个需求当中有几类事物。每个事物,每类事务都要定义为单独的类,这类事物的名词都可以定义为属性,这类事物的功能,一般是动词,可以定义为行为。二、封装1.封装的介绍封装是面向对象的三大特征:封装、继承、多态封装的作用:告诉我们,如何正确设计对象的属性和方法。/**需求:定义一个类描述人属性:姓名、年龄行为:吃饭、睡觉*/public class Person{    String name;    int age;    public void eat(){        System.out.println("吃饭");    }    public void sleep(){        System.out.println("睡觉");    }}原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为。public class Circle {    double radius;    public void draw(){        System.out.println("根据半径"+radius+"画圆");    }}//人画圆,我们通常人为行为主体是人,其实是圆//例如:人关门,这个门一定是门自己关的,人只是给了作用力,是门自己关上的。2.封装的好处对象代表什么,就得封装对应的数据,并提供数据对应的行为降低我们的学习成本,可以少学,少记,或者说压根不用学,不用记对象有哪些方法,有需要时去找就行3.private关键字是一个权限修饰符可以修饰成员(成员变量和成员方法)被private修饰的成员只能在本类中才能访问public class GirlFriend{    private String name;    private int age;    private String gender;}public class leiMing {    private int age;    //set(赋值)    public void setAge(int a){        if(a<0||a>120){            System.out.println("你给的年龄有误");            return;        }        age = a;    }    //get(取值)    public int getAge(){        return age;    }}针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作提供“setXxx(参数)”方法,用于给成员变量复制,方法用public修饰提供“getXxx()”方法,用于获取成员变量的值,方法用public修饰为什么要调用set和get呢?封装是面向对象编程的四大特性之一,它将数据(成员变量)和操作数据的方法绑定在一起,并隐藏对象的内部实现细节。通过将成员变量声明为 private,外部类无法直接访问和修改这些变量,只能通过类提供的 set 和 get 方法来间接操作。这样可以防止外部代码对数据进行非法或不恰当的修改,保证数据的安全性和完整性。三、this关键字1.成员变量和局部变量public class GirlFriend{    private int age;//成员变量:方法的外面,类的里面    public void method(){        int age = 10;//局部变量:方法的里面        System.out.println(age);    }}成员变量和局部变量一致时,采用就近原则谁离我近,我就用谁public class GirlFriend{    private int age;//成员变量:方法的外面,类的里面    public void method(){        int age = 10;//局部变量:方法的里面        System.out.println(age);    }}//在这里中,最后1个age距离 age=10最近,所以最后一个age用的是10的值//假如我想用第一个int ,我们可以在System.out.println(this.age)age前加入:this. 这里就可以打破就近原则,选择另一个变量在 Java 中,当局部变量(比如方法的参数)和类的成员变量重名时,就会产生命名冲突。在这种情况下,如果直接使用变量名,Java 默认会使用局部变量。而 this 关键字的一个重要作用就是用来引用当前对象的成员变量,从而区分局部变量和成员变量。2.举例下面通过一个简单的示例来详细讲解从引用成员变量方向 this 关键字的用法:class Employee {    // 定义成员变量    private String name;    private int age;    // 构造方法,用于初始化员工信息    public Employee(String name, int age) {        // 这里参数名和成员变量名相同,使用 this 引用成员变量        this.name = name;        this.age = age;    }    // 设置员工姓名的方法    public void setName(String name) {        // 使用 this 引用成员变量        this.name = name;    }    // 获取员工姓名的方法    public String getName() {        return this.name;    }    // 设置员工年龄的方法    public void setAge(int age) {        // 使用 this 引用成员变量        this.age = age;    }    // 获取员工年龄的方法    public int getAge() {        return this.age;    }    // 显示员工信息的方法    public void displayInfo() {        System.out.println("姓名: " + this.name + ", 年龄: " + this.age);    }}public class ThisKeywordVariableExample {    public static void main(String[] args) {        // 创建一个 Employee 对象        Employee employee = new Employee("李四", 25);        // 调用 displayInfo 方法显示员工信息        employee.displayInfo();        // 调用 setName 和 setAge 方法修改员工信息        employee.setName("王五");        employee.setAge(30);        // 再次调用 displayInfo 方法显示修改后的员工信息        employee.displayInfo();    }}代码详细解释:1. 类的成员变量定义private String name;private int age;12这里定义了两个私有成员变量 name 和 age,用于存储员工的姓名和年龄。2. 构造方法中的 this 关键字使用public Employee(String name, int age) {    this.name = name;    this.age = age;}在构造方法中,参数名 name 和 age 与类的成员变量名相同。此时,this.name 表示当前对象的成员变量 name,而直接使用的 name 则是构造方法的参数(局部变量)。通过 this.name = name; 语句,将局部变量 name 的值赋给了当前对象的成员变量 name。同理,this.age = age; 也是将局部变量 age 的值赋给了成员变量 age。3. set 方法中的 this 关键字使用public void setName(String name) {    this.name = name;}public void setAge(int age) {    this.age = age;}在 setName 和 setAge 方法中,同样存在参数名和成员变量名相同的情况。使用 this 关键字来明确指定要操作的是当前对象的成员变量,避免了与局部变量的混淆。4. get 方法中的 this 关键字使用public String getName() {    return this.name;}public int getAge() {    return this.age;}在 get 方法中,使用 this.name 和 this.age 来返回当前对象的成员变量的值。虽然在这种情况下,不使用 this 关键字也可以正常返回成员变量的值,因为这里没有局部变量与成员变量重名的问题,但使用 this 可以使代码的意图更加清晰,表明是在访问当前对象的成员变量。5. displayInfo 方法中的 this 关键字使用public void displayInfo() {    System.out.println("姓名: " + this.name + ", 年龄: " + this.age);}在 displayInfo 方法中,使用 this.name 和 this.age 来获取当前对象的成员变量的值,并将其输出。四、构造方法1.构造方法的概述构造方法也叫做构造器、构造函数2.构造方法的格式public class Student{    修饰符 类名(参数){        方法体;    }}public class Student {    private String name;    private int age;    //如果我们自己没有写构造方法    // 那么编译器会自动生成一个无参构造方法    public Student() {        System.out.println("无参构造方法");    }    public Student(String name, int age) {        this.name = name;        this.age = age; //有参构造方法    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }}public class StudentTest {    public static void main(String[] args) {        //创建类的对象        //调用的空参构造        //Student s1 = new Student();        Student s = new Student(name:"张三", age:20);        System.out.println(s.getName());        System.out.println(s.getAge());    }}特点:方法名与类名相同,大小写也要一致没有返回值类型,连void都没有没有具体的返回值(不能由return带回结果数据)执行时机:创建对象的时候由虚拟机调用,不能手动调用构造方法每创建一次对象,就会调用过一次构造方法3.构造方法的作用在创建对象的时候,由虚拟机自动调用构造方法,作用是给成员变量进行初始化的4.构造方法的分类public class Student{    private String name;    private int age;        public Student(){     ...//空参构造方法    }        public Student (String name, int age){    ....//带全部参数构造方法    }}无参构造方法:初始化的对象时,成员变量的数据均采用默认值有参构造方法:在初始化对象的时候,同时可以为对象进行5.构造方法的注意事项构造方法的定义如果没有定义构造方法,系统将给出一个默认的无参数构造方法如果定义了构造方法,系统将不再提供默认的构造方法构造方法的重载带参构造方法,和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载推荐的使用方式无论是否使用,都动手书写无参数构造方法,和带全部参数的构造方法五、标准JavaBean1.标准的JavaBean类类名需要见名知意成员变量使用private修饰提供至少两个构造方法无参构造方法带全部参数的构造方法成员方法提供每一个成员变量对应的setXxx()/getXxx()如果还有其他行为,也需要写上举例子:根据一个登录界面写一个JavaBean类public class User {    //属性    private String username;    private String password;    private String email;    private String gender;    private int age;    //构造方法    //无参构造    public User() {    }    //有参构造    public User(String username, String password, String email, String gender, int age) {        this.username = username;        this.password = password;        this.email = email;        this.gender = gender;        this.age = age;    }    //方法    //set和get方法    public String getUsername() {        return username;    }    public void setUsername(String username) {        this.username = username;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public String getEmail() {        return email;    }    public void setEmail(String email) {        this.email = email;    }    public String getGender() {        return gender;    }    public void setGender(String gender) {        this.gender = gender;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    }我们再写一个javabean中会遇到一个问题:这样写纯体力活啊!没事的没事的!我们有快捷键:    方法一:    alt+insert 或 alt+insert+Fnalt+insert 第一个是构造函数,点击无选择生成的是空参 ,全选ok生成的是有参数的构造函数alt+insert 点击setter和geteer,全选生成的是set和get    方法二:    下载插件pdg,下载完成后点击空白处就会出现。然后点击Ptg To JavaBean六、对象内存图1.一个对象的内存图Student s = new Student();1加载class文件申明局部变量在堆中开辟一个空间默认初始化显示初始化构造方法初始化将堆中空间的地址值赋值给左边的局部变量举例:public class Student{    String name;    int age;        public void study(){        System.out.println("好好学习")    }}public class TestStudent{    public static void main(String [] args){        Student s= new Student();        System.out.println(s);        System.out.println(s.name+"...."+s.age);        s.name = "阿强";        s.age = 23;        System.out.println(s.name+"..."+s.age);        s.study();    }}解析:内存执行顺序解析(基于Java内存模型)1. 类加载阶段(方法区)加载class文件JVM将Student.class和TestStudent.class加载到方法区,存储类结构信息(字段、方法签名、常量池等)。Student类包含字段name(String)、age(int)和方法study()。TestStudent类包含main()方法入口。2. 栈内存操作(main方法启动)声明局部变量执行main()时,在栈内存中创建main方法的栈帧,声明局部变量s(此时s未指向任何对象,值为null)。3. 堆内存分配(对象实例化)在堆中开辟空间执行new Student()时,在堆内存中为Student对象分配空间,内存大小由字段类型决定(String引用 + int值)。4. 对象初始化流程默认初始化对象字段赋默认值:name → null(引用类型默认值)age → 0(基本类型默认值)。显示初始化(本例中无)如果类中字段有显式赋值(如String name = "默认";),此时会执行。但示例代码未定义,此步骤跳过。构造方法初始化(本例中无)如果存在构造方法(如public Student() { age = 18; }),会通过构造器赋值。示例代码未定义构造方法,此步骤跳过。5. 变量关联与操作地址赋值给局部变量堆中对象地址赋值给栈帧中的s1变量,完成引用关联。执行Student s = new Student();后,s指向堆内存中的对象。对象字段修改后续代码通过s.name = "阿强";和s.age = 23;直接修改堆中对象的字段值,无需重新初始化。6. 方法调用(方法区与栈协作)执行s.study()从方法区加载study()的字节码指令。在栈中创建study()方法的栈帧,执行System.out.println(" 好好学习")(注:用户代码此处缺少分号,实际会编译报错)。内存操作完整流程总结步骤    操作内容    内存区域    示例代码体现1    加载类信息    方法区    Student和TestStudent类加载2    声明局部变量s    栈内存    Student s;3    堆中分配对象空间    堆内存    new Student()4    字段默认初始化(null/0)    堆内存    s.name 和s.age 初始值5    显式/构造初始化(无)    -    代码未定义相关逻辑6    对象地址赋值给s    栈内存    s = new Student();7    修改字段值    堆内存    s.name = "阿强";等操作关键现象解释System.out.println(s) 输出哈希值因打印对象时默认调用toString(),而Student未重写该方法,输出格式为类名@哈希值。字段值修改的可见性直接通过引用s修改堆中对象字段,所有指向该对象的引用均会看到更新后的值。编译隐患study()方法中System.out.println(" 好好学习")缺少分号,实际运行前会因语法错误中断。2.多个对象的内存图举例:public class Student{    String name;    int age;        public void study(){        System.out.println("好好学习");    }}public class TestStudent{    public static void main(String [] args){        Student s= new Student();        System.out.println(s);        s.name = "阿强";        s.age = 23;        System.out.println(s.name+"..."+s.age);        s.study();                Student s2= new Student();        System.out.println(s2);        s2.name = "阿珍";        s2.age = 24;        System.out.println(s2.name+"..."+s2.age);        s2.study();    }}第二次在创建对象时。class文件不需要再加载一次解析:2.1、执行顺序与内存操作分步解析1. 加载class文件(方法区)触发条件:首次使用Student类时。操作内容:将Student.class 加载到方法区,存储类结构(字段name、age和方法study()的定义)。将TestStudent.class 加载到方法区,存储main()方法入口。2. 声明局部变量(栈内存)操作内容:执行main()方法时,在栈内存创建main方法的栈帧。声明局部变量s和s2(初始值均为null)。3. 堆内存分配(对象空间开辟)操作内容:new Student()触发堆内存分配,根据类结构计算对象大小(String引用 + int值)。示例:s = new Student() → 堆地址0x001。s2 = new Student() → 堆地址0x002(独立空间)。4. 默认初始化(堆内存)操作内容:对象字段赋默认值:name → null(引用类型默认值)。age → 0(基本类型默认值)。示例:s的初始状态:name=null, age=0。s2的初始状态:name=null, age=0。5. 显示初始化(堆内存)触发条件:类中显式赋值的字段(如String name = "默认")。当前代码:Student类未定义显式赋值字段,跳过此步骤。6. 构造方法初始化(堆内存)触发条件:存在自定义构造方法(如public Student() { ... })。当前代码:Student类未定义构造方法,使用默认无参构造器,跳过此步骤。7. 地址赋值(栈内存)操作内容:将堆内存地址赋值给栈中的局部变量。示例:s = 0x001(指向第一个对象)。s2 = 0x002(指向第二个对象)。2.2、内存模型与对象独立性的关键验证1. 对象独立性的体现对象    堆地址    字段修改后的值s    0x001    name="阿强", age=23s2    0x002    name="阿珍", age=24验证逻辑:s和s2指向不同堆地址,修改其中一个对象的字段不会影响另一个对象。System.out.println(s == s2) → 输出false。2. 内存操作流程图解2.3、执行流程总结(分阶段)阶段    操作内容    内存区域类加载    加载Student和TestStudent类信息    方法区栈帧创建    声明s和s2(初始null)    栈内存堆内存分配    为s和s2分配独立空间    堆内存对象初始化    默认初始化 → 显式赋值(用户代码修改)    堆内存方法调用    study()从方法区加载逻辑到栈执行    栈内存2.4、常见问题解答1. 为什么System.out.println(s) 输出地址?原因:未重写toString()方法,默认调用Object.toString() ,格式为类名@哈希值。2. 显示初始化和构造方法初始化有何区别?显示初始化:直接在类中赋值字段(如String name = "张三"),编译时自动插入到构造方法中。构造方法初始化:通过自定义构造器赋值(优先级高于显示初始化)。3. 如何优化内存使用?复用对象:避免频繁new对象(尤其循环中)。垃圾回收:main()结束后,s和s2成为垃圾对象,由GC自动回收。附:修正后的代码输出示例Student@1b6d3586  阿强...23  好好学习  Student@4554617c  阿珍...24  好好学习  3.两个变量指向同一个对象内存图举例:public class Student{    String name;    int age;        public void study(){        System.out.println("好好学习");    }}public class TestStudent{    public static void main(String [] args){        Student s= new Student();        s.name = "阿强";            Student s2= s;        s2.name = "阿珍";        System.out.println(s.name+"..."+s2.name);    }}3.1、类加载阶段(方法区)加载TestStudent.class当JVM启动时,首先将TestStudent.class 加载到方法区,存储类结构信息(成员方法、字段描述等)加载Student.class执行new Student()时触发类加载机制,将Student.class 加载到方法区,包含name、age字段和study()方法元数据3.2、栈内存操作(main方法栈帧)声明局部变量在main方法栈帧中创建引用变量s(地址未初始化)和s2(此时两者均为null)对象创建指令new Student()操作码触发堆内存分配,此时:在堆中生成对象内存空间(包含对象头 + String name + int age)默认初始化:name=null,age=0(基本类型和引用类型的零值初始化)显式初始化:由于Student类没有直接赋值的字段(如String name = "默认名"),此阶段跳过构造方法执行若存在构造方法(本案例未定义),会通过invokespecial指令调用<init>方法完成初始化3.3、堆内存操作(对象关联)地址赋值将堆中Student对象地址赋值给栈帧中的s变量(完成s = new Student())引用传递s2 = s操作使s2指向堆中同一个对象(此时两个引用共享对象数据)字段修改通过s2.name = "阿珍"修改堆内存对象数据,此时s.name 同步变化(引用指向同一实体)3.4、最终内存结构内存区域    存储内容方法区    TestStudent类字节码、Student类元数据(包含study()方法代码)堆内存    Student对象实例(name=“阿珍”, age=0)栈内存    main方法栈帧:s=0x100(指向堆对象), s2=0x100(与s同地址)3.5、输出结果分析System.out.println(s.name+"..."+s2.name)→ 输出阿珍...阿珍(s与s2引用同一对象,堆内数据修改对所有引用可见)关键理解点:引用类型变量的赋值操作传递的是对象地址值,而非创建新对象。这种特性是Java对象共享机制的核心体现。4.this的内存原理public class Student{    private int age;    public void method(){        int age=10;        System.out.println(age);//10        System.out.println(this.age);//成员变量的值 0    }}this的作用:区分局部变量和成员变量this的本质:所在方法调用者的地址值public class Student{    private int age;    public void method(){        int age=10;        System.out.println(age);//10        System.out.println(this.age);//成员变量的值 0    }}public class StudentTest{    public static void main (String[] args){        Student s = new Student();        s.method();    }}4.1、类加载阶段(方法区核心操作)加载StudentTest.classJVM启动时优先加载含main()的类到方法区存储类元数据:静态变量、方法表(含main()入口地址)触发Student.class 加载当执行new Student()时触发类加载方法区新增:字段描述表(private int age的访问权限和偏移量)method()的字节码指令集合隐式默认构造器<init>方法(因无自定义构造方法)4.2、对象实例化流程(堆栈协同)步骤    内存区域    具体行为    代码对应3    栈内存    在main方法栈帧声明局部变量s(初始值null)    Student s;4    堆内存    分配对象空间:对象头(12字节)+ int age(4字节)= 16字节    new Student()5    堆内存    默认初始化:age=0(基本类型零值填充)    隐式执行6    堆内存    构造方法初始化:执行空参数的<init>方法(无实际操作)    隐式调用7    栈内存    将堆地址(如0x7a3f)赋值给s变量    s = new...4.3、方法调用时的内存隔离(栈帧作用域)执行s.method() 时发生:新建栈帧:在栈顶创建method()1的独立空间,包含:隐式参数this(指向堆地址0x7a3f)局部变量age=10(存储于栈帧变量表)变量访问规则:输出语句    内存访问路径    结果System.out.println(age)    访问栈帧局部变量表    10System.out.println(this.age)    通过this指针访问堆内存字段    04.4、关键差异对比表特征    成员变量this.age    局部变量age存储位置    堆内存对象内部    栈帧局部变量表生命周期    与对象共存亡    随方法栈帧销毁而消失初始化值    默认零值(int=0)    必须显式赋值访问方式    需通过对象引用    直接访问4.5、技术扩展:this的底层实现当调用method()时:字节码层面:java复制aload_0         // 将this引用压入操作数栈(对应堆地址0x7a3f)getfield #2    // 根据字段偏移量读取堆中age值(#2为字段符号引用)内存隔离机制:局部变量age会遮蔽同名的成员变量,必须通过this.显式穿透访问堆数据5.基本数据类型和引用数据类型的区别5.1基本数据类型public class Test{    public static void main (String [] args){        int a = 10;    }}基本数据类型:在变量当中存储的是真实的数据值从内存角度:数据值是存储再自己的空间中特点:赋值给其他变量,也是赋值的真实的值。5.2引用数据类型public class TestStudent{    public static void main(String[] args){        Student s=new Student;    }}引用数据类型:堆中存储的数据类型,也就是new出来的,变量中存储的是地址值。引用:就是使用其他空间中数据的意思。从内存的角度:数据值是存储在其他空间中,自己空间中存储的是地址值特点:赋值给其他变量,赋的地址值。七、补充知识:成员变量、局部变量区别成员变量:类中方法外的变量局部变量:方法中的变量 区别:区别    成员变量    局部变量类中位置不同    类中,方法外    方法内、方法申明上初始化值不同    有默认初始化值    没有,使用前需要完成赋值内存位置不同    堆内存    栈内存生命周期不同    随着对象的创建而存在,随着对象的消失而消失    随着方法的调用而存在,随着方法的运行结束而消失作用域    整个类中有效    当前方法中有效————————————————原文链接:https://blog.csdn.net/2401_87533975/article/details/145992557
  • [技术干货] JDK 1.8 64位安装详解与教程
    简介:本文详细介绍了Java Development Kit (JDK) 1.8的安装过程,包括下载、安装、配置环境变量、卸载以及JDK 1.8的新特性。JDK 1.8是Oracle公司发布的重要Java开发工具包版本,适用于64位操作系统,并具备更好的资源利用率和性能。安装步骤包括接受许可协议、选择安装类型和位置、配置环境变量以及在必要时进行手动设置。文章还强调了正确安装和配置JDK对于Java编程的重要性,并对JDK 1.8新增的特性进行了总结,以帮助开发者充分掌握这一版本的JDK。 1. JDK 1.8概述与重要性简介Java Development Kit 8(JDK 1.8)是Java开发者必须熟悉的重要工具集。它在2014年发布,带来了许多新的语言特性和API,同时对JVM性能也进行了优化,是目前广泛使用的稳定版本之一。重要性JDK 1.8的重要性在于其引入的新特性极大地方便了Java开发者的编程工作,比如Lambda表达式简化了代码编写,Stream API增强了集合操作,新的日期时间API提供了更强大的日期时间处理能力。同时,对于企业级应用来说,稳定的JDK版本是降低风险和维护成本的首选。使用范围由于其广泛的支持和丰富的功能集,JDK 1.8适用于各种规模的项目,从简单的单机应用到大型分布式服务。同时,JDK 1.8为Java 9及更高版本的模块化特性做了铺垫,使开发者能更容易适应未来Java平台的变化。2. 64位JDK安装程序下载与介绍随着技术的发展,64位系统已成为主流,因此安装64位的JDK是大多数开发者的选择。64位JDK的安装不仅需要下载适合您系统的安装程序,还要确保它与您的操作系统兼容,同时理解安装文件夹中的各种组件。2.1 选择合适的JDK版本在开始安装之前,选择正确的JDK版本至关重要。由于JDK版本众多,选择一个适合自己操作系统和项目需求的版本是安装的第一步。2.1.1 确定系统类型和位数确定您的操作系统类型和位数是选择JDK的第一步。JDK有32位和64位之分,而操作系统也有Windows、macOS和Linux等不同版本。请务必确保您下载的JDK版本与您的操作系统兼容。2.1.2 访问Oracle官网下载页面选择版本后,进入Oracle官网,找到JDK下载页面。页面上会列出不同版本的JDK,您可以根据自己的需求选择相应版本进行下载。通常,页面会自动推荐最适合您系统类型的版本。2.2 安装程序的结构和组件安装程序安装包中包含了许多组件,其中一些是安装过程中的必需品,而另一些则提供了额外的功能。了解这些组件可以帮助您更好地利用JDK。2.2.1 JDK安装文件夹内容概览安装JDK后,会得到一个包含多个文件和子文件夹的目录。最重要的文件夹是 bin ,其中包含了JDK的主要可执行文件,如 java 和 javac 。 lib 文件夹包含了运行JDK所需的库文件。除了这些,还有 include 文件夹,它包含了一些平台特定的头文件。2.2.2 JDK与JRE的区别及其组件在安装JDK的同时,您会发现JRE(Java Runtime Environment)也会被安装。JRE是运行Java应用程序所必需的环境,而JDK则包含了JRE以及开发Java应用所需的编译器、调试器和其他工具。JDK的安装确保了开发者既有编译代码的工具也有运行代码的环境。2.3 安装前的系统要求与检查在安装JDK之前,检查系统是否满足安装JDK的要求是必要的。这不仅包括操作系统兼容性,还包括硬件的配置要求。2.3.1 操作系统兼容性要求JDK要求的操作系统类型和版本多种多样。例如,对于Windows系统,JDK 1.8支持从Windows XP到最新版本的Windows 10;对于Linux系统,则可能需要满足特定的包依赖关系。请确保您的系统符合要求。2.3.2 硬件配置和环境需求除了操作系统的兼容性外,您的系统也需要满足一定的硬件配置。JDK的安装和运行至少需要以下硬件资源:256MB的内存,40MB的硬盘空间。不过,为了更好的性能和开发体验,推荐的内存是1GB以上。为了确保系统满足这些要求,可以使用系统信息工具来检查您的硬件配置,并在必要时进行升级。当硬件和操作系统都符合要求后,您就可以开始安装JDK了。3. 安装步骤详解3.1 安装向导的启动与选项3.1.1 接受许可协议在开始安装JDK之前,需要首先接受Oracle的最终用户许可协议(EULA)。这一步骤是必须的,因为它规定了用户可以如何使用JDK。一般而言,安装向导会首先展示协议内容,并要求用户选择是否接受。用户必须选择“Accept”才能继续进行安装过程。3.1.2 选择安装路径和组件选择安装路径对于系统的环境变量配置和后续使用至关重要。在选择安装路径时,用户需要确保有相应的权限,并且安装路径不应包含空格或特殊字符,以避免潜在的路径解析错误。此外,用户需要明确安装JDK的各个组件,例如JRE(Java Runtime Environment)、JavaDoc工具和源代码等。通常情况下,建议安装所有组件以保证JDK功能的完整性。3.2 完成安装的确认步骤3.2.1 安装完成界面的解读在安装过程完成后,安装向导会显示一个完成界面。这一步骤提供了关于安装成功的确认,并通常会给出一些关于后续步骤的提示,比如配置环境变量或打开一个命令行窗口测试安装的JDK。确认安装成功后,即可关闭安装向导。3.2.2 安装过程中可能遇到的问题及解决方案安装JDK的过程中可能会遇到各种问题,比如权限不足、安装路径不正确、系统兼容性问题等。解决这些问题通常需要一些基本的故障排除步骤,比如重新选择安装路径、以管理员权限运行安装程序、确保系统满足最低要求等。在遇到安装问题时,建议首先查阅Oracle的官方文档或社区论坛获得帮助,因为可能已经有其他用户遇到并解决了相同的问题。另外,查看安装向导提供的错误日志或系统事件日志,也可能会提供问题的线索。下面是关于安装过程中遇到权限问题的代码示例及其解释:# Windows系统下运行安装程序时遇到权限不足的错误处理# 假设在以管理员权限运行安装程序时遇到如下错误信息:# "The setup cannot continue as the user does not have sufficient privileges. Please contact your system administrator." # 1. 首先,需要右键点击安装程序,选择“Run as administrator”。#    在Windows Vista及更新版本中,可能会收到UAC(用户帐户控制)提示,此时需确认。 # 2. 如果通过右键菜单直接运行安装程序仍然出现权限不足的问题,#    可以考虑手动以管理员身份打开命令提示符或PowerShell窗口,然后运行安装程序:start /b "" java -jar jdk-8uXXX-windows-x64.exe上述代码展示的是在Windows环境下处理权限问题的方式。首先解释了如何以管理员身份运行安装程序,然后解释了如果直接运行还是出现问题,该如何操作。表格展示:| 安装步骤 | 操作描述 | 注意事项 | |-----------------|--------------------------------------|--------------------------------------------| | 启动安装向导 | 双击JDK安装程序或运行命令行安装指令。 | 确保安装程序文件是完整的,以及未被病毒或恶意软件影响。 | | 接受许可协议 | 阅读协议条款,选择接受。 | 不接受协议将无法继续安装。 | | 选择安装路径 | 选择合适的目录,避免使用系统文件夹。 | 确保有访问权限,避免使用包含空格的路径。 | | 定制安装组件 | 根据需要选择要安装的组件。 | 通常建议安装所有组件以保证功能完整性。 | | 完成安装确认 | 完成安装后,确保检查安装成功提示。 | 如果有提示错误,根据信息进行相应的故障排除。 | | 解决安装问题 | 如遇问题,查看错误日志,参考官方文档或社区。 | 确保满足系统要求,以管理员权限运行安装程序。 |在完成以上安装步骤后,JDK就安装在指定的系统中了。接下来,要确保JDK可以被系统正确识别和使用,配置环境变量是下一步的关键工作。4. 环境变量配置方法4.1 环境变量的定义和作用4.1.1 PATH变量的作用PATH环境变量是一个重要的系统变量,它定义了操作系统搜索可执行文件的目录列表。当用户在命令行输入一个命令时,系统会在PATH变量指定的目录中查找对应的可执行文件。正确设置PATH变量能够让系统识别命令行中输入的命令,如 java 或 javac ,而不需要在每次调用时指定完整的路径。在JDK安装后配置PATH变量,是为了让系统能够找到JDK的 bin 目录,其中存放了Java的运行时环境(JRE)和编译器(JAVAC)等工具。这对于开发人员来说,确保命令行工具可以被系统识别,从而有效地运行Java程序。4.1.2 JAVA_HOME变量的重要性JAVA_HOME环境变量是一个高级设置,它为JDK安装位置提供了一个引用。将JDK的安装路径设置为JAVA_HOME变量,可以提高环境配置的灵活性。当需要在多个Java版本之间切换或者更新JDK时,只需修改JAVA_HOME变量的值,而不需要逐个修改系统环境变量或项目中引用JDK路径的设置。同时,许多Java应用服务器和开发工具依赖JAVA_HOME变量来确定JDK的安装位置。如果没有正确设置JAVA_HOME变量,可能会导致这些应用服务器或工具无法启动,从而影响开发和部署工作。4.2 配置环境变量的步骤4.2.1 Windows系统下的配置方法在Windows操作系统下,配置环境变量可以分为几个步骤:右击“我的电脑”或“此电脑”,选择“属性”。在弹出的系统窗口中,点击“高级系统设置”。在系统属性对话框中,点击“环境变量”按钮。在“系统变量”区域,点击“新建”按钮来设置JAVA_HOME变量。变量名填写 JAVA_HOME ,变量值填写JDK安装的完整路径,例如 C:\Program Files\Java\jdk1.8.0_291 。在“系统变量”中找到PATH变量,选择“编辑”,然后在变量值的末尾添加 ;C:\Program Files\Java\jdk1.8.0_291\bin (注意,前面的分号 ; 是分隔符,用于分隔不同的路径,如果PATH变量原本为空则不需要分号)。点击“确定”保存设置。配置完毕后,可以在命令行中输入 java -version 来检查JDK版本信息,确认配置是否成功。4.2.2 Linux系统下的配置方法在Linux系统下,环境变量的配置通常通过修改 ~/.bashrc 或 ~/.profile 文件来完成。以下是配置JAVA_HOME和PATH的具体步骤:打开终端。使用文本编辑器打开 ~/.bashrc 文件,例如使用命令 nano ~/.bashrc 。在文件的末尾添加以下内容:export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64export PATH=$JAVA_HOME/bin:$PATH请确保将 JAVA_HOME 的值替换为实际的JDK安装路径。保存并关闭文件。使配置生效,可以使用命令 source ~/.bashrc 或者重新启动终端。为了验证配置是否成功,同样可以在终端输入 java -version ,检查Java版本信息。4.3 验证环境变量配置是否成功4.3.1 使用命令行检查JDK版本在命令行界面,输入命令 java -version ,如果配置成功,将显示JDK的版本信息。例如:java version "1.8.0_291"Java(TM) SE Runtime Environment (build 1.8.0_291-b11)Java HotSpot(TM) 64-Bit Server VM (build 25.291-b11, mixed mode)此外,使用 javac -version 检查Java编译器的版本也是个好主意,确保编译环境同样配置正确。4.3.2 检查其他开发工具的配置情况除了确认Java环境变量之外,其他与Java相关的开发工具(如Maven、Gradle等)的配置也需要检查。可以通过在命令行输入这些工具的命令,如 mvn -v 或 gradle -v 来验证。这些工具通常会读取系统环境变量,并显示其版本信息及Java版本信息。例如,使用Maven的命令可能显示如下信息:Apache Maven 3.6.3Maven home: /usr/local/apache-maven-3.6.3Java version: 1.8.0_291, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-1.8.0-openjdk-amd64/jreDefault locale: en_US, platform encoding: UTF-8OS name: "linux", version: "5.4.0-65-generic", arch: "amd64", family: "unix"以上步骤能够确保JDK环境变量在系统中配置正确,并且可以为后续的开发工作打下坚实的基础。5. JDK 1.8的卸载过程5.1 卸载前的准备工作在开始卸载JDK之前,确保已经做好了适当的准备工作,以便在卸载后能够迅速恢复系统状态,并最小化对系统稳定性和开发工作的影响。5.1.1 关闭所有Java相关进程在卸载JDK前,需要确保系统中没有运行任何与Java相关的进程。可以通过任务管理器(Windows)或使用命令行工具(如 ps 命令在Linux中)来检查并终止这些进程。通常,这些进程包括Java虚拟机(JVM)实例、IDE或任何Java应用程序。5.1.2 备份必要的文件和设置在执行卸载操作之前,推荐备份任何重要的Java配置文件,例如 server.xml 、 context.xml 等,以及任何Java项目文件。虽然这些文件在卸载后通常不会被删除,但在进行系统操作时备份总是一种良好的习惯,以防万一需要恢复数据。5.2 系统卸载程序的使用接下来,我们将介绍如何在Windows和Linux系统上使用标准卸载程序来移除JDK。5.2.1 Windows控制面板中的卸载步骤打开“控制面板”。点击“程序和功能”或“卸载程序”。在列表中找到与Java相关的条目,通常是“Java”或“Java Platform (JDK)”。点击“卸载”按钮,然后按照屏幕上的指示完成卸载过程。5.2.2 Linux中的删除JDK包和文件在Linux系统中,卸载JDK通常需要使用包管理工具。对于基于Debian的系统(如Ubuntu),可以使用以下命令:sudo apt-get remove --purge oracle-java8-installer对于基于Red Hat的系统(如CentOS),可以使用:sudo yum erase java-1.8.0-openjdk请确保替换上述命令中的包名以匹配您系统上的JDK包名。5.3 手动清理残留文件即使使用了系统卸载程序,有时候一些文件仍会遗留在系统中。因此,在卸载程序完成后,手动检查并清理剩余的Java相关文件是一个好习惯。5.3.1 检查并删除Java相关目录在系统中可能遗留下一些Java相关的目录,如 /usr/lib/jvm 或其他类似路径。可以通过文件管理器或命令行进行检查:# Linux命令行检查ls /usr/lib/jvm如果发现有JDK相关的目录,请使用 rm -rf 命令将其删除:# Linux命令行删除sudo rm -rf /usr/lib/jvm/java-1.8.0请谨慎使用上述命令,因为 rm -rf 可以删除指定目录及其所有内容。5.3.2 清除环境变量残留配置如果在环境变量中设置了与Java相关的路径,那么在卸载JDK后,需要从环境变量中清除这些设置。在Windows中,这可以在“系统属性”->“高级”->“环境变量”中修改。在Linux中,需要编辑 ~/.bashrc 或 ~/.profile 文件,移除或注释掉Java相关的行。# Linux中移除PATH变量中的Java路径export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin完成这些步骤后,重新加载环境变量(在终端中输入 source ~/.bashrc 或重新登录)以确保更改生效。通过遵循上述步骤,可以确保JDK被彻底且安全地从您的系统中卸载。6. JDK 1.8新特性介绍Java Development Kit (JDK) 1.8引入了一系列新的特性和优化,旨在提升开发效率,增强语言表达能力,以及改善JVM性能。本章节我们将深入探讨这些新特性,并展示它们在实际应用中的优势。6.1 语言和API级别的改进6.1.1 Lambda表达式的深入应用Lambda表达式为Java带来了函数式编程的特性,允许开发者以更简洁的方式编写代码。Lambda表达式本质上是函数式接口的一个实例。// 示例代码:使用Lambda表达式排序字符串列表List<String> list = Arrays.asList("peter", "john", "maria", "bob");Collections.sort(list, (a, b) -> ***pareTo(b));在上述代码中, (a, b) -> ***pareTo(b) 是一个Lambda表达式,它等效于一个匿名内部类,实现了 Comparator 接口的 compare 方法。Lambda表达式的引入显著简化了代码,尤其是当需要传递行为作为参数时。6.1.2 Stream API的新增功能Stream API提供了一种高效且易于读写的处理数据的方式,适用于集合类的数据操作。Stream API通过其丰富的中间操作和终端操作,简化了集合操作的复杂度。// 示例代码:使用Stream API筛选和排序学生对象列表List<Student> students = // ... 获取学生列表List<Student> sortedStudents = students.stream()    .filter(student -> student.getGrade() > 70)    .sorted(***paring(Student::getGrade).reversed())    .collect(Collectors.toList());在上述代码中,我们通过Stream API对学生成绩进行筛选和排序,操作链式进行,逻辑清晰且易于理解。6.2 JVM的性能优化6.2.1 Graal编译器的引入Graal编译器是一个高性能的JIT(Just-In-Time)编译器,它可以将Java字节码编译成机器码。自JDK 1.8以来,Graal可以作为实验性特性使用,它旨在提高JVM的性能。6.2.2 垃圾收集器的改进JDK 1.8对垃圾收集器进行了一系列的改进,例如引入了并行的Full GC,以及改善了GC的停顿时间。这使得在高性能应用中,如实时系统,垃圾收集的影响大大降低。6.3 开发工具的增强6.3.1 JShell的介绍和使用JShell是JDK 1.9引入的一个交互式命令行工具,它允许开发者快速测试和执行Java代码片段,无需编写完整的类定义。# 示例JShell命令jshell> System.out.println("Hello, JShell!");|  Hello, JShell!通过JShell,开发者可以立即看到代码执行结果,极大地方便了学习和测试新API。6.3.2 Java Mission Control的更新Java Mission Control是一个集成环境,用于监控和管理Java应用程序。在JDK 1.8中,Java Mission Control进行了更新,增加了对Java Flight Recorder的集成,提供了实时监控和分析运行中的Java应用程序的能力。通过以上讨论,我们看到JDK 1.8的每个新特性都旨在解决特定的开发痛点,并极大地提升开发效率。这些特性不仅改善了开发体验,也提高了应用性能和可靠性。对于那些有意采用JDK 1.8的开发者来说,了解和掌握这些新特性是至关重要的。7. JDK 1.8在实际开发中的应用随着企业软件系统的不断扩展和应用程序的日益复杂,选择合适的开发工具和版本对于软件的稳定性和开发效率有着至关重要的影响。JDK 1.8作为Java领域内的一个重要版本,其引入的新特性对现代应用程序开发产生了深远的影响。本章节将深入探讨JDK 1.8在实际开发环境中的应用、特性使用,以及如何应对兼容性和升级问题。7.1 开发环境的搭建与配置在进行任何实际的代码编写之前,构建一个高效且稳定的开发环境是至关重要的。JDK 1.8的引入,使得开发者可以利用其改进的性能和新特性,提升开发效率和代码质量。7.1.1 集成开发环境(IDE)的设置大多数现代IDE都对JDK 1.8有着良好的支持。以IntelliJ IDEA为例,配置JDK的过程非常简单:打开IntelliJ IDEA,选择 "File" > "Project Structure"。在弹出的窗口中选择 "SDKs"。点击 "+" 选择 "JDK",然后浏览至JDK 1.8的安装目录并选择。点击 "Apply" 和 "OK",以确认并保存设置。完成这些步骤后,JDK 1.8将被设置为项目的默认JDK版本,你可以开始使用该版本的所有新特性。7.1.2 项目构建工具Maven和Gradle的集成Maven和Gradle作为两个主流的Java项目构建工具,同样能够很好地支持JDK 1.8。以Maven为例,配置如下:<properties>    <***piler.source>1.8</***piler.source>    <***piler.target>1.8</***piler.target></properties>通过在项目的 pom.xml 文件中添加上述配置,即可指定使用JDK 1.8作为编译器的源代码和目标代码的版本。对于Gradle用户,可以通过以下配置:sourceCompatibility = 1.8targetCompatibility = 1.8在项目的 build.gradle 文件中添加这些配置,同样可以确保Gradle使用JDK 1.8进行构建。7.2 高效使用JDK 1.8特性JDK 1.8引入的新特性极大地提升了Java开发的效率和代码的可读性。本节将介绍如何利用Lambda表达式和Stream API来简化代码编写和数据处理。7.2.1 利用Lambda简化代码编写Lambda表达式提供了函数式编程的能力,允许我们以更简洁的方式编写代码。例如,创建一个简单的比较器可以不再需要匿名内部类:Comparator<String> comparator = (s1, s2) -> ***pareTo(s2);而使用Lambda表达式后,代码变得更加简洁:Comparator<String> comparator = String::compareTo;7.2.2 使用Stream进行高效数据处理Stream API的引入使得集合类的数据操作更加优雅和高效。在处理集合数据时,我们可以通过流的方式进行链式调用,例如对列表中的元素进行过滤和排序:List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");names.stream()     .filter(name -> name.length() > 4)     .sorted()     .forEach(System.out::println);上面的代码首先创建了一个字符串列表的流,然后通过 filter 方法过滤长度大于4的元素,接着通过 sorted 方法排序,最后通过 forEach 方法输出。7.3 解决兼容性和升级问题随着技术的演进,应用程序需要不断地升级和改进。在从旧版本JDK迁移到JDK 1.8时,可能会遇到一些兼容性问题。7.3.1 兼容性考虑及解决方案JDK 1.8在引入新特性的同时,也注重了向后兼容性。但某些新特性可能需要对旧代码进行修改。当遇到特定的兼容性问题时,可以考虑以下解决方案:使用 @FunctionalInterface 注解 :在使用Lambda表达式之前,确保接口符合函数式接口的要求。检查第三方库的兼容性 :确保所有第三方库都支持JDK 1.8的特性。7.3.2 从旧版本JDK升级到JDK 1.8的步骤和注意事项当决定升级到JDK 1.8时,需要遵循以下步骤,并注意相应的问题:评估项目依赖 :确认项目依赖的库和框架都与JDK 1.8兼容。更新环境变量 :更新 PATH 和 JAVA_HOME 环境变量以指向新的JDK安装路径。修改项目配置 :如上所述,更新IDE和构建工具的配置。运行代码兼容性检查 :运行项目并测试所有功能以确保一切正常。升级过程中,可能会遇到需要代码重构的情况,比如对方法引用和Lambda表达式的支持。使用IDE提供的重构工具能够帮助简化这一过程。总结来说,JDK 1.8为Java开发带来了诸多新特性,能够有效地提升开发效率和代码质量。然而,升级到新版本的JDK同样伴随着对现有代码和环境的调整。通过本章的介绍,开发者能够更好地理解和应用JDK 1.8,同时能够更平滑地进行版本升级。本文还有配套的精品资源,点击获取 简介:本文详细介绍了Java Development Kit (JDK) 1.8的安装过程,包括下载、安装、配置环境变量、卸载以及JDK 1.8的新特性。JDK 1.8是Oracle公司发布的重要Java开发工具包版本,适用于64位操作系统,并具备更好的资源利用率和性能。安装步骤包括接受许可协议、选择安装类型和位置、配置环境变量以及在必要时进行手动设置。文章还强调了正确安装和配置JDK对于Java编程的重要性,并对JDK 1.8新增的特性进行了总结,以帮助开发者充分掌握这一版本的JDK。————————————————原文链接:https://blog.csdn.net/weixin_36001279/article/details/143217770
  • [技术干货] Java优先级队列揭秘:堆的力量让数据处理飞起来
    引言在开发中,尤其是需要处理大量数据或者进行任务调度的场景下,如何高效地管理数据的顺序和优先级是一个至关重要的问题。Java 提供了优先级队列(PriorityQueue),它基于堆(Heap)实现,能够以高效的方式管理数据的优先级。在本文中,我们将深入探讨优先级队列的工作原理,特别是堆的作用,并通过示例代码帮助你更好地理解其应用。一、什么是优先级队列?优先级队列(Priority Queue)是一种队列数据结构,其中每个元素都包含一个优先级,队列总是按元素的优先级顺序进行排序。与普通队列(先进先出 FIFO)不同,优先级队列确保每次从队列中移除的元素是具有最高优先级的元素。有些场景下,使⽤队列显然不合适,⽐如:在⼿机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在 Java 中,PriorityQueue 是基于堆的实现。堆是一种特殊的二叉树结构,满足特定的顺序性质:最大堆保证每个父节点的值大于等于其子节点的值,而最小堆则相反。二、堆的基本原理JDK1.8中的PriorityQueue底层使⽤了堆这种数据结构,⽽堆实际就是在完全⼆叉树的基础上进⾏了⼀些调整。具有以下特点:对于最大堆,父节点的值始终大于或等于子节点的值;对于最小堆,父节点的值始终小于或等于子节点的值。2.1 堆的概念如果有⼀个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全⼆叉树的顺序存储⽅式存储在⼀个⼀维数组中,并满⾜:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为⼩堆(或⼤堆)。Java 中的 PriorityQueue 默认是最小堆,也就是说队列中最小的元素将具有最高的优先级。堆的性质:• 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;• 堆总是⼀棵完全⼆叉树。 2.2 堆的存储⽅式从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储,注意:对于⾮完全⼆叉树,则不适合使⽤顺序⽅式进⾏存储,因为为了能够还原⼆叉树,空间中必须 要存储空节点,就会导致空间利⽤率⽐较低。将元素存储到数组中后,可以根据⼆叉树章节的性质5对树进⾏还原。假设i为节点在数组中的下标,则有:• 如果i为0,则i表⽰的节点为根节点,否则i节点的双亲节点为 (i - 1)/2• 如果2 * i + 1 ⼩于节点个数,则节点i的左孩⼦下标为2 * i + 1,否则没有左孩⼦• 如果2 * i + 2 ⼩于节点个数,则节点i的右孩⼦下标为2 * i + 2,否则没有右孩⼦三、堆操作时间复杂度操作类型    描述    时间复杂度插入元素    使用 add() 或 offer() 方法插入元素    O(log n)删除最小元素    使用 poll() 方法移除并返回最小元素    O(log n)查看最小元素    使用 peek() 方法返回堆顶元素而不移除    O(1)获取堆大小    使用 size() 方法返回当前堆的元素数量    O(1)3.1 建堆的时间复杂度因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个节点不影响最终结果): 因此:建堆的时间复杂度为O(N)。四、PriorityQueue 的基本操作1. PriorityQueue中放置的元素必须要能够⽐较⼤⼩,不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出 ClassCastException异常2. 不能插⼊null对象,否则会抛出NullPointerException3. 没有容量限制,可以插⼊任意多个元素,其内部可以⾃动扩容4. 插⼊和删除元素的时间复杂度为5. PriorityQueue底层使⽤了堆数据结构6. PriorityQueue默认情况下是⼩堆—即每次获取到的元素都是最⼩的元素4.1 插⼊/删除/获取优先级最⾼的元素注意:优先级队列的扩容说明:• 如果容量⼩于64时,是按照oldCapacity的2倍⽅式扩容的• 如果容量⼤于等于64,是按照oldCapacity的1.5倍⽅式扩容的•如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进⾏扩容五、 构造一个最小堆的优先级队列import java.util.PriorityQueue;public class PriorityQueueExample {    public static void main(String[] args) {        // 创建一个最小堆        PriorityQueue<Integer> pq = new PriorityQueue<>();        // 添加元素        pq.add(10);        pq.add(5);        pq.add(15);        pq.add(7);        // 打印并移除元素        while (!pq.isEmpty()) {            System.out.println(pq.poll());  // 依次输出 5, 7, 10, 15        }    }}输出:5710151234在这个示例中,PriorityQueue 自动按照最小堆的规则对元素进行排序。每次调用 poll() 方法时,队列中优先级最高的元素(即最小的元素)会被移除。六、 自定义优先级假设我们有一个包含多个任务的列表,每个任务有一个优先级,我们希望按优先级顺序处理这些任务。我们可以通过实现 Comparator 接口来自定义优先级。import java.util.PriorityQueue;import java.util.Comparator;class Task {    String name;    int priority;    public Task(String name, int priority) {        this.name = name;        this.priority = priority;    }    @Override    public String toString() {        return name + " (Priority: " + priority + ")";    }}public class CustomPriorityQueueExample {    public static void main(String[] args) {        // 自定义Comparator,按优先级降序排列        PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() {            @Override            public int compare(Task t1, Task t2) {                return Integer.compare(t2.priority, t1.priority);  // 优先级高的排前面            }        });        // 添加任务        pq.add(new Task("Task 1", 3));        pq.add(new Task("Task 2", 5));        pq.add(new Task("Task 3", 1));        pq.add(new Task("Task 4", 4));        // 打印并移除任务        while (!pq.isEmpty()) {            System.out.println(pq.poll());        }    }}输出:Task 2 (Priority: 5)Task 4 (Priority: 4)Task 1 (Priority: 3)Task 3 (Priority: 1)在这个例子中,PriorityQueue 被用来管理多个任务,并按照任务的优先级(从高到低)排序。. 自定义优先级示例代码解释步骤    代码示例    说明创建优先级队列    PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() {...});    创建一个带有自定义排序规则的优先级队列,按优先级降序排序添加任务    pq.add(new Task("Task 1", 3));    向队列中添加一个新任务打印任务    System.out.println(pq.poll());    输出并移除队列中的优先级最高(优先级最大)的任务七、常见堆的应用场景应用场景    说明    示例任务调度    根据任务的优先级执行任务,堆帮助管理和调度任务顺序    操作系统的调度程序,网络请求调度器合并多个有序数据流    使用堆合并多个已排序的数据流,维持整体有序性    合并 k 个有序链表、流式数据处理实时数据处理    动态地从数据流中获取最小/最大值    获取最近的数据流中的最大值/最小值,实时计算排名前N的元素最短路径算法    在图算法(如 Dijkstra 算法)中,用堆优化路径的计算    Dijkstra 算法,最短路径计算中的优先级队列K 个最大元素问题    找出数组中最大的 K 个元素    求数组中前 K 大的元素,堆排序方法拓展:TOP-K问题:即求数据集合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:1. ⽤数据集合中前K个元素来建堆◦ 前k个最⼤的元素,则建⼩堆◦ 前k个最⼩的元素,则建⼤堆2. ⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素八、总结通过本文的介绍,我们了解了 Java 中优先级队列(PriorityQueue)的基本概念和实现原理。利用堆结构,优先级队列能够高效地管理数据并根据优先级进行处理。无论是任务调度、数据流合并,还是实时数据处理,堆都能发挥其强大的性能优势。8.1 堆的优点优点    说明高效的优先级管理    通过堆结构,可以快速处理数据的优先级。插入和删除操作的时间复杂度为 O(log n),适合动态数据处理。无序输入,高效排序    堆无需输入数据有序,只需通过堆的结构来维护顺序。适用于合并已排序数据流。内存占用少    堆是完全二叉树结构,相比于其他数据结构(如 AVL 树、红黑树)占用的内存较少。8.2 优先级队列的优势与局限性优势/局限性    说明优势    - 对于频繁插入和删除操作非常高效。- 适合任务调度、流式数据处理、最短路径问题等场景。局限性    - 不支持按优先级范围查询或批量删除。- 不是完全通用的排序工具,通常只适用于频繁访问最大或最小元素的场景。8.3 堆与其他数据结构对比数据结构    操作    时间复杂度    优势    局限性堆    插入、删除、查看顶元素    O(log n)    高效管理优先级,适合动态数据处理。    不支持按特定条件的排序,无法直接获取中间元素。数组    排序、查找    O(n log n)    方便查找和排序,简单易用。    插入、删除操作较慢,尤其是在无序数据中。链表    插入、删除    O(1)    插入和删除效率高,尤其适合频繁变动的场景。    查找元素需要 O(n) 的时间,无法高效管理优先级。红黑树    插入、删除、查找    O(log n)    支持高效的查找、插入和删除操作。    相较于堆,内存占用更大,且需要更多的平衡操作。哈希表    查找、插入、删除    O(1)    查找操作极快,适合无序数据的快速检索。    不支持排序,不适合优先级管理。前景:随着大数据和实时计算的不断发展,堆结构和优先级队列将在更多的算法优化和数据流处理中扮演重要角色,尤其是在机器学习、数据挖掘、搜索引擎优化等领域。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145959834
  • [技术干货] MySQL新增字段后Java实体未更新的潜在问题与解决方案【转】
    1. 问题背景:数据库与 Java 实体不同步1.1 常见场景数据库新增字段(如 ALTER TABLE ADD COLUMN),但 Java 实体类未更新。程序继续运行,调用 saveBatch()、insert()、查询等方法。是否会报错? 取决于字段约束和 ORM 框架行为。1.2 示例代码假设有一个 StatisticsData 实体类(使用 MyBatis-Plus):1234567@Data@TableName("statistics_data")public class StatisticsData extends BaseModel {    private String agentId;    private Long click;    // 其他字段...}然后数据库新增一个字段:1ALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL;此时,如果 Java 代码未更新,会有什么影响?2. 不同操作的影响分析2.1 查询操作(SELECT)默认情况下,MyBatis-Plus 会忽略数据库中存在但实体类没有的字段,查询不会报错。但如果使用 SELECT * 或手动映射全部字段,可能会触发警告(取决于日志级别)。2.2 插入操作(INSERT)如果新增字段允许 NULL 或有默认值:1ALTER TABLE statistics_data ADD COLUMN new_column INT DEFAULT 0;✅ save() 或 saveBatch() 不会报错,插入时该字段会用 NULL 或默认值填充。如果新增字段是 NOT NULL 且无默认值:1ALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL;❌ saveBatch() 会报错:1ERROR 1364 (HY000): Field 'new_column' doesn't have a default value因为 MyBatis-Plus 生成的 SQL 不包含未定义的字段,导致 MySQL 拒绝插入。2.3 批量插入(saveBatch)saveBatch() 的底层逻辑:1234567// MyBatis-Plus 默认实现(简化版)public boolean saveBatch(Collection<T> entityList) {    for (T entity : entityList) {        baseMapper.insert(entity); // 生成 INSERT SQL,仅包含实体类定义的字段    }    return true;}如果 new_column 是 NOT NULL,由于 SQL 不包含该字段,MySQL 会报错。如果允许 NULL 或设置默认值,则正常执行。3. 解决方案3.1 临时修复(不推荐长期使用)(1)修改数据库字段约束12345-- 允许 NULLALTER TABLE statistics_data MODIFY new_column INT NULL; -- 或设置默认值ALTER TABLE statistics_data MODIFY new_column INT DEFAULT 0; (2)避免自动映射,手动指定 SQL1234567// 使用 @TableField(exist = false) 忽略未知字段@TableField(exist = false)private String ignoredField; // 或自定义 SQL(明确指定插入字段)@Insert("INSERT INTO statistics_data (agent_id, click) VALUES (#{agentId}, #{click})")void customInsert(StatisticsData data); 3.2 长期最佳实践(推荐)(1)同步更新 Java 实体类1234567@Data@TableName("statistics_data")public class StatisticsData extends BaseModel {    private String agentId;    private Long click;    private Integer newColumn; // 新增字段} (2)使用数据库迁移工具(如 Flyway/Liquibase)12345-- V1__init.sqlCREATE TABLE statistics_data (...); -- V2__add_new_column.sqlALTER TABLE statistics_data ADD COLUMN new_column INT DEFAULT 0; (3)自动化检查(可选)通过单元测试或 Schema 校验工具,确保数据库与实体类一致:123// 示例:使用 Hibernate Validator 检查(如果适用)@Column(nullable = false)private Integer newColumn; 4. 完整代码示例4.1 更新后的 Java 实体1234567891011@Data@TableName("statistics_data")public class StatisticsData extends BaseModel {    private String agentId;    private Long click;    private Integer newColumn; // 新增字段     @TableField("`date`")    private String date;    // 其他字段...} 4.2 安全的批量插入方法12345678// 检查数据完整性后再插入public void safeBatchInsert(List<StatisticsData> dataList) {    if (dataList == null || dataList.isEmpty()) {        return;    }    // 可在此处做字段校验    statisticsDataService.saveBatch(dataList);} 4.3 数据库变更脚本(Flyway 示例)123-- V2__add_new_column.sqlALTER TABLE statistics_data ADD COLUMN new_column INT NOT NULL DEFAULT 0 COMMENT '新增字段'; 5. 总结场景是否报错解决方案新增字段允许 NULL 或 DEFAULT❌ 不报错可暂时不更新实体类新增字段 NOT NULL 且无默认值✅ 报错更新实体类 或 修改表结构使用 saveBatch()取决于约束同步实体类或调整 SQL
  • [区域初赛赛题问题] 请问后面还可以加队友吗
    现在我们队是两个人,请问进复赛有没有机会再加新的队友?
  • [技术干货] Java RMI技术详解与案例分析
    Java RMI(Remote Method Invocation)是一种允许Java虚拟机之间进行通信和交互的技术。它使得远程Java对象能够像本地对象一样被访问和操作,从而简化了分布式应用程序的开发。一些应用依然会使用 RMI 来实现通信和交互,今天的内容我们来聊聊 RMI 的那些事儿。一、先来了解一下概念RMI原理RMI的基本思想是远程方法调用。客户端调用远程方法时,实际上是发送一个调用请求到服务器,由服务器执行该方法,并将结果返回给客户端。RMI通过存根(Stub)和骨架(Skeleton)类来实现远程调用,存根位于客户端,而骨架位于服务器端。RMI组件远程接口:必须继承自java.rmi.Remote接口,并声明抛出RemoteException。远程对象:实现了远程接口的类。RMI服务器:提供远程对象,并处理客户端的调用请求。RMI客户端:发起远程方法调用请求。注册服务(Registry):提供服务注册与获取,类似于目录服务。数据传递RMI使用Java序列化机制来传递数据。客户端将方法参数序列化后通过网络发送给服务器,服务器反序列化参数并执行远程方法,然后将结果序列化回传给客户端。RMI案例以下是一个简单的RMI案例,包括服务器和客户端的实现思路,下文V 将再用代码来解释:服务器端实现一个远程接口,例如PersonController,包含一个远程方法queryName。创建该接口的具体实现类PersonControllerImpl,并在其中实现远程方法。在服务器的main方法中,实例化远程对象,创建RMI注册表,并使用Naming.rebind将远程对象绑定到指定名称。客户端通过Naming.lookup方法,使用RMI注册表提供的名称获取远程对象的存根。调用存根上的方法,就像调用本地方法一样,实际上是在调用服务器上的远程方法。RMI的局限性语言限制:RMI是Java特有的技术,不能直接用于非Java应用程序。安全性问题:RMI的序列化机制可能带来安全风险,不建议将1099端口暴露在公网上。性能和扩展性:RMI的性能受网络延迟和带宽影响,且在高并发情况下可能面临扩展性限制。RMI的应用场景RMI适用于需要Java程序之间进行远程通信的场景,如分布式银行系统、游戏服务器、股票交易系统和网上商城等。接下来一起看一个简单的案例使用吧。二、案例使用先来搞一个简单的Java RMI服务器端和客户端的实现案例。这个案例中,服务器端将提供一个名为HelloWorld的远程服务,客户端将调用这个服务并打印返回的问候语。服务器端实现定义远程接口:服务器和客户端都需要这个接口。它必须继承自java.rmi.Remote接口,并且所有远程方法都要声明抛出RemoteException。import java.rmi.Remote; import java.rmi.RemoteException; public interface HelloWorld extends Remote { String sayHello() throws RemoteException; }实现远程接口:创建一个实现了上述接口的类,并实现远程方法。import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; public class HelloWorldImpl extends UnicastRemoteObject implements HelloWorld { protected HelloWorldImpl() throws RemoteException { super(); } @Override public String sayHello() throws RemoteException { return "Hello, World!"; } }设置RMI服务器:创建一个主类来设置RMI服务器,绑定远程对象到RMI注册表。import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; public class HelloWorldServer { public static void main(String[] args) { try { // 创建远程对象 HelloWorld helloWorld = new HelloWorldImpl(); // 获取RMI注册表的引用,并在指定端口上创建或获取注册表实例 LocateRegistry.createRegistry(1099); // 将远程对象绑定到RMI注册表中,客户端可以通过这个名字访问远程对象 Naming.bind("rmi://localhost/HelloWorld", helloWorld); System.out.println("HelloWorld RMI object bound"); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }客户端实现调用远程服务:客户端使用RMI注册表的名字来查找远程对象,并调用其方法。import java.rmi.Naming; import java.rmi.RemoteException; public class HelloWorldClient { public static void main(String[] args) { try { // 使用RMI注册表的名字查找远程对象 HelloWorld helloWorld = (HelloWorld) Naming.lookup("rmi://localhost/HelloWorld"); // 调用远程方法 String response = helloWorld.sayHello(); System.out.println("Response: " + response); } catch (Exception e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }来详细解释吧远程接口 (HelloWorld): 这是服务器和客户端之间通信的协议。它定义了可以被远程调用的方法。远程对象实现 (HelloWorldImpl): 这是远程接口的一个实现。RMI调用实际上会调用这个实现中的方法。服务器 (HelloWorldServer): 负责创建远程对象的实例,并将这个实例绑定到RMI注册表中。这样客户端就可以通过注册表的名字来访问这个对象。客户端 (HelloWorldClient): 使用RMI注册表的名字来查找服务器上的远程对象,并调用其方法。接下来就可以编译所有类文件,运行服务器端程序,确保RMI注册表已经启动(在某些Java版本中会自动启动),再运行客户端程序,搞定。注意一下哈,由于RMI使用Java序列化机制,因此客户端和服务器的类路径必须一致或兼容。三、RMI 在分布式银行系统中的应用接下来V哥要介绍业务场景下的应用了,拿在分布式银行系统中来说,我们可以使用RMI来实现不同银行分行之间的通信,例如,实现账户信息的查询、转账等操作。以下是一个简化的示例,其中包括两个基本操作:查询账户余额和执行转账,按步骤一步一步来吧。步骤1: 定义远程接口首先,定义一个远程接口BankService,它将被各个分行实现以提供银行服务。import java.rmi.Remote; import java.rmi.RemoteException; public interface BankService extends Remote { double getAccountBalance(String accountNumber) throws RemoteException; boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException; }步骤2: 实现远程接口接下来,实现这个接口来创建远程对象,这个对象将提供实际的银行服务。import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; import java.util.HashMap; import java.util.Map; public class BankServiceImpl extends UnicastRemoteObject implements BankService { private Map<String, Double> accounts = new HashMap<>(); protected BankServiceImpl() throws RemoteException { super(); // 初始化一些账户信息 accounts.put("123456789", 5000.00); accounts.put("987654321", 1000.00); } @Override public double getAccountBalance(String accountNumber) throws RemoteException { return accounts.getOrDefault(accountNumber, 0.00); } @Override public boolean transferFunds(String fromAccount, String toAccount, double amount) throws RemoteException { if (accounts.containsKey(fromAccount) && accounts.get(fromAccount) >= amount) { accounts.put(fromAccount, accounts.get(fromAccount) - amount); accounts.merge(toAccount, amount, Double::sum); return true; } return false; } }步骤3: 设置RMI服务器服务器端将创建BankService的远程对象实例,并将其绑定到RMI注册表中。import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; public class BankServer { public static void main(String[] args) { try { LocateRegistry.createRegistry(1099); // 创建RMI注册表 BankService bankService = new BankServiceImpl(); Naming.rebind("//localhost/BankService", bankService); // 绑定远程对象 System.out.println("BankService is ready for use."); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }步骤4: 实现RMI客户端客户端将使用RMI注册表的名字来查找远程对象,并调用其方法。import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.RemoteException; public class BankClient { public static void main(String[] args) { try { BankService bankService = (BankService) Naming.lookup("//localhost/BankService"); System.out.println("Account balance: " + bankService.getAccountBalance("123456789")); // 执行转账操作 boolean isTransferSuccess = bankService.transferFunds("123456789", "987654321", 200.00); if (isTransferSuccess) { System.out.println("Transfer successful."); } else { System.out.println("Transfer failed."); } // 再次查询余额 System.out.println("New account balance: " + bankService.getAccountBalance("123456789")); } catch (RemoteException | NotBoundException e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }来详细解释一下远程接口 (BankService): 定义了两个方法:getAccountBalance用于查询账户余额,transferFunds用于执行转账操作。远程对象实现 (BankServiceImpl): 实现了BankService接口。它使用一个HashMap来模拟账户和余额信息。服务器 (BankServer): 设置了RMI服务器,将BankService的实现绑定到RMI注册表中,供客户端访问。客户端 (BankClient): 查找RMI注册表中的BankService服务,并调用其方法来查询余额和执行转账。撸完代码后,编译所有类文件,运行服务器端程序BankServer,再运行客户端程序BankClient,测试效果吧。转载自https://www.cnblogs.com/wgjava/p/18343209
  • [技术干货] Volatile不保证原子性及解决方案
    原子性的意义原子性特别是在并发编程领域,是一个极其重要的概念,原子性指的是一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况。这意味着原子性操作是不可分割的,它们在执行过程中不会被其他操作中断或干扰。原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多进程的环境中,当多个操作同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操作的原子性,可以避免这种情况,从而维护数据的完整性和程序的正确执行。了解了上面的原子性的重要概念后,接下来一起聊一聊 volatile 关键字。volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操作的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的问题,尽管它们不会看到部分更新的值。Volatile 的限制不保证原子性:volatile 变量的单个读写操作是原子的,但复合操作(如自增或同步块)不是原子的。不保证顺序性:volatile 变量的读写操作不会与其他操作(如非 volatile 变量的读写)发生重排序。一个例子用一个示例来解释会更清楚点,假如我们有一段代码是这样的:class Counter { private volatile int count = 0; void increment() { count++; } int getCount() { return count; } } 尽管 count 是 volatile 变量,但 increment 方法中的复合操作 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。volatile 不保证原子性的代码验证以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操作原子性的问题:public class VolatileTest { private static volatile int counter = 0; public static void main(String[] args) throws InterruptedException { int numberOfThreads = 10000; Thread[] threads = new Thread[numberOfThreads]; for (int i = 0; i < numberOfThreads; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 100; j++) { counter++; } }); threads[i].start(); } for (int i = 0; i < numberOfThreads; i++) { threads[i].join(); } System.out.println("Expected count: " + (numberOfThreads * 100)); System.out.println("Actual count: " + counter); } }在这个例子中:counter 是一个 volatile 变量。每个线程都会对 counter 执行 100 次自增操作。理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。然而,由于 counter++ 包含三个操作:读取 counter 的值、增加 1、写回 counter 的值,这些操作不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操作的原子性。解决方案1. 使用 synchronized 方法或块:将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。class Counter { private volatile int count = 0; synchronized void increment() { count++; } synchronized int getCount() { return count; } }2. 使用 AtomicInteger 类:java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 变量。import java.util.concurrent.atomic.AtomicInteger; class Counter { private AtomicInteger count = new AtomicInteger(0); void increment() { count.incrementAndGet(); } int getCount() { return count.get(); } }3. 使用锁(如 ReentrantLock):使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Counter { private volatile int count = 0; private final Lock lock = new ReentrantLock(); void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }使用volatile变量的正确使用场景如果操作是简单的读写,并且你只需要保证可见性,可以使用 volatile。但对于复合操作,可以使用上述其他方法来实现,通过这些方法,可以确保在多线程环境中对共享资源的正确同步和可见性。转载自https://www.cnblogs.com/wgjava/p/18311697
  • [技术干货] Java Executors类的9种创建线程池的方法及应用场景分析
    在Java中,Executors 类提供了多种静态工厂方法来创建不同类型的线程池。在学习线程池的过程中,一定避不开Executors类,掌握这个类的使用、原理、使用场景,对于实际项目开发时,运用自如,以下是一些常用的方法,V哥来一一细说:newCachedThreadPool(): 创建一个可缓存的线程池,如果线程池中的线程超过60秒没有被使用,它们将被终止并从缓存中移除。newFixedThreadPool(int nThreads): 创建一个固定大小的线程池,其中 nThreads 指定了线程池中线程的数量。newSingleThreadExecutor(): 创建一个单线程的执行器,它创建单个工作线程来执行任务。newScheduledThreadPool(int corePoolSize): 创建一个固定大小的线程池,它可以根据需要创建新线程,但会按照固定延迟执行具有给定初始延迟的任务。newWorkStealingPool(int parallelism): 创建一个工作窃取线程池,它使用多个队列,每个线程都从自己的队列中窃取任务。newSingleThreadScheduledExecutor(): 创建一个单线程的调度执行器,它可以根据需要创建新线程来执行任务。privilegedThreadFactory(): 创建一个线程工厂,用于创建具有特权访问的线程。defaultThreadFactory(): 创建一个默认的线程工厂,用于创建具有非特权访问的线程。unconfigurableExecutorService(ExecutorService executor): 将给定的 ExecutorService 转换为不可配置的版本,这样调用者就不能修改它的配置。这些方法提供了灵活的方式来创建和管理线程池,以满足不同的并发需求,下面 V 哥来一一介绍一下9个方法的实现以及使用场景。1. newCachedThreadPool()newCachedThreadPool 方法是 Java java.util.concurrent 包中的 Executors 类的一个静态工厂方法。这个方法用于创建一个可缓存的线程池,它能够根据需要创建新线程,并且当线程空闲超过一定时间后,线程会被终止并从线程池中移除。下面是 newCachedThreadPool 方法的大致实现原理和源代码分析:实现原理线程创建: 当提交任务到线程池时,如果线程池中的线程数少于核心线程数,会创建新的线程来执行任务。线程复用: 如果线程池中的线程数已经达到核心线程数,新提交的任务会被放入任务队列中等待执行。线程回收: 如果线程池中的线程在一定时间内(默认是60秒)没有任务执行,它们会被终止,从而减少资源消耗。源代码分析在 Java 的 java.util.concurrent 包中,Executors 类并没有直接提供 newCachedThreadPool 的实现,而是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }参数解释:corePoolSize: 核心线程数,这里设置为0,表示线程池不会保留任何核心线程。maximumPoolSize: 最大线程数,这里设置为 Integer.MAX_VALUE,表示理论上可以创建无限多的线程。keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为60秒。unit: keepAliveTime 参数的时间单位,这里是秒。workQueue: 一个任务队列,这里使用的是 SynchronousQueue,它是一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的移除操作。实现过程初始化: 当调用 newCachedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会检查是否有空闲线程可以立即执行任务。线程创建: 如果没有空闲线程,并且当前线程数小于 maximumPoolSize,则创建新线程执行任务。任务队列: 如果当前线程数已经达到 maximumPoolSize,则将任务放入 SynchronousQueue 中等待。线程复用: 当一个线程执行完任务后,它不会立即终止,而是尝试从 SynchronousQueue 中获取新任务。线程回收: 如果线程在 keepAliveTime 时间内没有获取到新任务,它将被终止。这种设计使得 newCachedThreadPool 非常适合处理大量短生命周期的任务,因为它可以动态地调整线程数量以适应任务负载的变化。然而,由于它可以创建无限多的线程,如果没有适当的任务队列来控制任务的数量,可能会导致资源耗尽。因此,在使用 newCachedThreadPool 时,需要谨慎考虑任务的特性和系统的资源限制。使用场景:适用于执行大量短期异步任务,尤其是任务执行时间不确定的情况。例如,Web服务器处理大量并发请求,或者异步日志记录。2. newFixedThreadPool(int nThreads)newFixedThreadPool(int nThreads) 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个固定大小的线程池,它能够确保线程池中始终有固定数量的线程在工作。以下是 newFixedThreadPool 方法的实现原理、源代码分析以及实现过程:实现原理固定线程数: 线程池中的线程数量始终保持为 nThreads。任务队列: 提交的任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个阻塞队列中等待执行。线程复用: 线程池中的线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。源代码分析newFixedThreadPool 方法是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor( nThreads, // 核心线程数 nThreads, // 最大线程数 0L, // 线程空闲时间,这里设置为0,表示线程不会空闲 TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务 ); }参数解释:corePoolSize: 核心线程数,这里设置为 nThreads,表示线程池中始终有 nThreads 个线程。maximumPoolSize: 最大线程数,这里也设置为 nThreads,表示线程池的线程数量不会超过 nThreads。keepAliveTime: 当线程数大于核心线程数时,多余的空闲线程能等待新任务的最长时间,这里设置为0,表示如果线程池中的线程数超过核心线程数,这些线程将立即终止。unit: keepAliveTime 参数的时间单位,这里是毫秒。workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个基于链表的阻塞队列,可以存储任意数量的任务。实现过程初始化: 当调用 newFixedThreadPool 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会检查是否有空闲的核心线程可以立即执行任务。任务队列: 如果所有核心线程都在忙碌状态,新提交的任务将被放入 LinkedBlockingQueue 中等待。线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。线程数量控制: 由于 keepAliveTime 设置为0,当线程池中的线程数超过核心线程数时,这些线程会立即终止,从而保证线程池中的线程数量不会超过 nThreads。这种设计使得 newFixedThreadPool 非常适合处理大量且持续的任务,因为它可以保证任务以固定的线程数量并行执行,同时避免了线程数量的无限制增长。然而,由于线程池的大小是固定的,如果任务提交的速率超过了线程池的处理能力,可能会导致任务在队列中等待较长时间。因此,在使用 newFixedThreadPool 时,需要根据任务的特性和预期的负载来合理设置 nThreads 的值。使用场景:适用于执行大量长期运行的任务,其中线程数量需要固定。例如,同时运行多个数据加载或数据处理任务,且希望限制并发数以避免资源过载。3. newSingleThreadExecutor()newSingleThreadExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个单线程的执行器。这个执行器确保所有任务都按照任务提交的顺序,在一个线程中顺序执行。以下是 newSingleThreadExecutor 方法的实现原理、源代码分析以及实现过程:实现原理单线程执行: 线程池中只有一个线程,所有任务都由这个线程顺序执行。任务队列: 如果这个线程在执行任务时有新任务提交,新任务会被放入一个阻塞队列中等待执行。线程复用: 这个线程会重复利用,执行完一个任务后,会立即尝试从队列中获取下一个任务执行。源代码分析newSingleThreadExecutor 方法同样是通过调用 ThreadPoolExecutor 类的构造函数来实现的。以下是 ThreadPoolExecutor 构造函数的调用示例:public static ExecutorService newSingleThreadExecutor() { return new ThreadPoolExecutor( 1, // 核心线程数 1, // 最大线程数 0L, TimeUnit.MILLISECONDS, // 线程空闲时间,这里设置为0,表示线程不会空闲 new LinkedBlockingQueue<Runnable>() // 使用阻塞队列来存储任务 ); }参数解释:corePoolSize: 核心线程数,这里设置为1,表示线程池中始终有一个核心线程。maximumPoolSize: 最大线程数,这里也设置为1,表示线程池的线程数量不会超过1。keepAliveTime: 线程空闲时间,这里设置为0,表示如果线程空闲,它将立即终止。unit: keepAliveTime 参数的时间单位,这里是毫秒。workQueue: 一个任务队列,这里使用的是 LinkedBlockingQueue,它是一个无界队列,可以存储任意数量的任务。实现过程初始化: 当调用 newSingleThreadExecutor 时,会创建一个 ThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,如果核心线程空闲,则立即执行任务;如果核心线程忙碌,则将任务放入 LinkedBlockingQueue 中等待。顺序执行: 由于只有一个线程,所有任务都将按照提交的顺序被执行。任务队列: 如果核心线程在执行任务,新提交的任务将被放入 LinkedBlockingQueue 中排队等待。线程复用: 核心线程执行完一个任务后,会尝试从 LinkedBlockingQueue 中获取新任务继续执行。线程数量控制: 由于 keepAliveTime 设置为0,核心线程在没有任务执行时会立即终止。但由于 corePoolSize 和 maximumPoolSize 都为1,线程池会立即重新创建一个线程。这种设计使得 newSingleThreadExecutor 非常适合处理需要保证任务顺序的场景,例如,当任务之间有依赖关系或者需要按照特定顺序执行时。同时,由于只有一个线程,这也避免了多线程环境下的并发问题。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。使用场景:适用于需要保证任务顺序执行的场景,例如,顺序处理队列中的消息或事件。也适用于需要单个后台线程持续处理周期性任务的情况。4. newScheduledThreadPool(int corePoolSize)newScheduledThreadPool 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个固定大小的线程池,这个线程池支持定时以及周期性的任务执行。以下是 newScheduledThreadPool 方法的实现原理、源代码分析以及实现过程:实现原理定时任务: 线程池能够按照指定的延迟执行任务,或者以固定间隔周期性地执行任务。固定线程数: 线程池中的线程数量被限制为 corePoolSize 指定的大小。任务队列: 任务首先由核心线程执行,如果核心线程都在忙碌状态,新任务将被放入一个延迟任务队列中等待执行。源代码分析newScheduledThreadPool 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }这里的 ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的一个子类,专门为执行定时任务设计。ScheduledThreadPoolExecutor 构造函数的参数 corePoolSize 定义了线程池中核心线程的数量。ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。实现过程初始化: 当调用 newScheduledThreadPool 时,会创建一个 ScheduledThreadPoolExecutor 实例。任务提交: 当任务提交给线程池时,线程池会根据任务的预定执行时间,将任务放入 DelayedWorkQueue 中。任务调度: 线程池中的线程会从 DelayedWorkQueue 中获取任务,如果任务的执行时间已经到达,线程将执行该任务。线程复用: 执行完一个任务的线程会再次尝试从 DelayedWorkQueue 中获取下一个任务。线程数量控制: 如果任务队列中的任务数量超过了核心线程能够处理的范围,ScheduledThreadPoolExecutor 会创建新的线程来帮助处理任务,直到达到 corePoolSize 指定的最大线程数。特点ScheduledThreadPoolExecutor 允许设置一个线程工厂,用于创建具有特定属性的线程。它还允许设置一个 RejectedExecutionHandler,当任务无法被接受时(例如,线程池关闭或任务队列已满),这个处理器会被调用。与 ThreadPoolExecutor 不同,ScheduledThreadPoolExecutor 的 shutdown 和 shutdownNow 方法不会等待延迟任务执行完成。使用 newScheduledThreadPool 创建的线程池非常适合需要执行定时任务的场景,例如,定期执行的后台任务、定时检查等。然而,由于它是基于固定大小的线程池,所以在高负载情况下,任务可能会排队等待执行,这需要在设计时考虑适当的 corePoolSize 以满足性能要求。使用场景:适用于需要定期执行任务或在将来某个时间点执行任务的场景。例如,定时备份数据、定时发送提醒等。5. newWorkStealingPool(int parallelism)newWorkStealingPool 是 Java 8 中新增的 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个工作窃取(Work-Stealing)线程池,它能够提高并行任务的执行效率,特别是在多处理器系统上。实现原理工作窃取: 在工作窃取线程池中,每个线程都有自己的任务队列。当一个线程完成自己的任务后,它会尝试从其他线程的任务队列中“窃取”任务来执行。并行级别: 线程池的大小由 parallelism 参数决定,这个参数通常等于主机上的处理器核心数。动态调整: 工作窃取线程池可以动态地添加或移除线程,以适应任务的负载和线程的利用率。源代码分析newWorkStealingPool 方法是通过调用 ForkJoinPool 类的静态工厂方法 commonPoolFor 来实现的。以下是 ForkJoinPool 构造函数的调用示例:public static ExecutorService newWorkStealingPool(int parallelism) { return new ForkJoinPool( parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, // 没有未处理的异常处理器 false // 不是一个异步任务 ); }参数解释:parallelism: 线程池的并行级别,即线程池中的线程数量。ForkJoinPool.defaultForkJoinWorkerThreadFactory: 默认的线程工厂,用于创建线程。null: 未处理的异常处理器,这里没有指定,因此如果任务抛出未捕获的异常,它将被传播到 ForkJoinTask 的调用者。false: 表示这不是一个异步任务。ForkJoinPool 内部使用了 ForkJoinWorkerThread 来执行任务,并且每个线程都有一个 ForkJoinQueue 来存储任务。实现过程初始化: 当调用 newWorkStealingPool 时,会创建一个 ForkJoinPool 实例。任务提交: 当任务提交给线程池时,它们会被放入调用线程的本地队列中。任务执行: 每个线程首先尝试执行其本地队列中的任务。工作窃取: 如果本地队列为空,线程会尝试从其他线程的队列中窃取任务来执行。动态调整: 线程池可以根据需要动态地添加或移除线程。特点工作窃取线程池特别适合于工作量不均匀分布的任务,因为它可以减少空闲时间并提高资源利用率。它也适用于可分解为多个子任务的并行计算任务,因为可以将任务分解后,再将子任务提交给线程池。由于每个线程都有自己的队列,因此减少了锁的争用,提高了并发性能。使用 newWorkStealingPool 创建的线程池非常适合于需要高并发和高吞吐量的场景,尤其是在多处理器系统上。然而,由于工作窃取机制,它可能不适用于任务执行时间非常短或者任务数量非常少的场景,因为窃取任务本身可能会引入额外的开销。使用场景:适用于工作量不均匀或可分解为多个小任务的并行计算任务。例如,图像处理、数据分析等,可以在多核处理器上有效利用所有核心。6. newSingleThreadScheduledExecutor()newSingleThreadScheduledExecutor 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个单线程的调度执行器,它可以安排命令在给定的延迟后运行,或者定期地执行。以下是 newSingleThreadScheduledExecutor 方法的实现原理、源代码分析以及实现过程:实现原理单线程执行: 执行器确保所有任务都在单个线程中顺序执行,这保证了任务的执行顺序。定时任务: 支持延迟执行和周期性执行任务。任务队列: 所有任务首先被放入一个任务队列中,然后由单线程按顺序执行。源代码分析newSingleThreadScheduledExecutor 方法是通过调用 ScheduledThreadPoolExecutor 类的构造函数来实现的。以下是 ScheduledThreadPoolExecutor 构造函数的调用示例:public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new ScheduledThreadPoolExecutor(1); }这里,ScheduledThreadPoolExecutor 是 ExecutorService 的一个实现,专门为执行定时任务设计。构造函数只有一个参数,即核心线程数,这里设置为1,表示这是一个单线程的执行器。ScheduledThreadPoolExecutor 内部使用了一个 DelayedWorkQueue 作为任务队列,这个队列能够按照任务的预定执行时间对任务进行排序。实现过程初始化: 当调用 newSingleThreadScheduledExecutor 时,会创建一个 ScheduledThreadPoolExecutor 实例,其核心线程数为1。任务提交: 当任务提交给执行器时,任务会被封装成 ScheduledFutureTask 或者 RunnableScheduledFuture,然后放入 DelayedWorkQueue 中。任务调度: 单线程会不断地从 DelayedWorkQueue 中获取任务,并按照预定的时间执行。如果任务的执行时间已经到达,任务将被执行;如果还没有到达,线程会等待直到执行时间到来。顺序执行: 由于只有一个线程,所有任务都将按照它们被提交的顺序被执行。周期性任务: 对于需要周期性执行的任务,执行器会在每次任务执行完毕后,重新计算下一次执行的时间,并再次将任务放入队列。特点newSingleThreadScheduledExecutor 创建的执行器非常适合需要保证任务顺序的场景,例如,需要按照特定顺序执行的任务或者具有依赖关系的任务。它也适合执行定时任务,如定期执行的维护任务或者后台任务。由于只有一个线程,这也避免了多线程环境下的并发问题,简化了任务同步和状态管理。使用 newSingleThreadScheduledExecutor 创建的执行器可以提供强大的定时任务功能,同时保持任务执行的顺序性。然而,由于只有一个线程执行任务,这也限制了并行处理的能力,如果任务执行时间较长,可能会导致后续任务等待较长时间。因此,在使用 newSingleThreadScheduledExecutor 时,需要根据任务的特性和对顺序的要求来决定是否适用。使用场景:适用于需要单个后台线程按计划执行任务的场景。例如,定时检查系统状态、定时执行维护任务等。7. privilegedThreadFactory()privilegedThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个线程工厂,该工厂能够产生具有特权访问的线程。这意味着这些线程可以加载系统属性和库,并且可以访问文件系统。以下是 privilegedThreadFactory 方法的实现原理、源代码分析以及实现过程:实现原理特权访问: 创建的线程将具有访问系统资源的权限,例如,加载系统属性和库。线程创建: 线程工厂将创建新的线程实例,这些线程实例将继承创建它们的线程的上下文。源代码分析在 Java 的标准库中,privilegedThreadFactory 方法的实现细节并未公开,因为它是一个私有方法。然而,我们可以分析其大致工作原理。privilegedThreadFactory 方法的调用示例如下:public static ThreadFactory privilegedThreadFactory() { return new PrivilegedThreadFactory(); }这里,PrivilegedThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。实现过程初始化: 当调用 privilegedThreadFactory 方法时,会返回一个新的 PrivilegedThreadFactory 实例。线程创建: 当使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。特权访问: 在 newThread(Runnable r) 方法的实现中,会使用 AccessController.doPrivileged 方法来确保新创建的线程具有特权访问。上下文复制: 通常,新线程会复制创建它的线程的上下文,包括类加载器等。示例代码虽然我们不能查看 privilegedThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有特权访问的线程:public class PrivilegedThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { return AccessController.doPrivileged(new PrivilegedAction<>() { @Override public Thread run() { return new Thread(r); } }); } }在这个示例中,PrivilegedAction 是一个实现了 PrivilegedAction<T> 接口的匿名类,其 run 方法创建了一个新的线程。AccessController.doPrivileged 方法用于执行一个特权操作,这里是为了确保线程创建过程中具有必要的权限。特点使用 privilegedThreadFactory 创建的线程可以在需要访问敏感系统资源的情况下使用。这种线程工厂通常用于需要执行特权操作的应用程序,例如,访问系统属性或者执行文件 I/O 操作。使用 privilegedThreadFactory 可以确保线程在执行任务时具有适当的安全权限,从而避免安全异常。然而,需要注意的是,过度使用特权访问可能会带来安全风险,因此在设计应用程序时应谨慎使用。使用场景:适用于需要线程具有更高权限来访问系统资源的场景。例如,需要访问系统属性或执行文件I/O操作的应用程序。8. defaultThreadFactory()defaultThreadFactory 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法,用于创建一个默认的线程工厂。这个线程工厂生成的线程没有特殊的权限,它们是普通的线程,具有标准的访问权限。以下是 defaultThreadFactory 方法的实现原理、源代码分析以及实现过程:实现原理标准线程创建: 创建的线程工厂将生成具有默认属性的线程。线程名称: 生成的线程具有默认的线程名称前缀,通常是 "pool-x-thread-y",其中 x 和 y 是数字。线程优先级: 线程的优先级设置为 Thread.NORM_PRIORITY,这是 Java 线程的默认优先级。非守护线程: 创建的线程不是守护线程(daemon threads),它们的存在不会阻止 JVM 退出。源代码分析Java 的 defaultThreadFactory 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ThreadFactory 接口和一些公开的源代码片段来分析其大致实现。以下是 defaultThreadFactory 方法的调用示例:public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); }这里,DefaultThreadFactory 是 Executors 类的一个私有静态内部类,它实现了 ThreadFactory 接口。ThreadFactory 接口定义了一个 newThread(Runnable r) 方法,用于创建新的线程。实现过程初始化: 当调用 defaultThreadFactory 方法时,会返回一个新的 DefaultThreadFactory 实例。线程创建: 使用这个工厂创建线程时,它会调用 newThread(Runnable r) 方法。设置线程名称: 在 newThread(Runnable r) 方法的实现中,会创建一个新的 Thread 对象,并设置一个默认的线程名称。设置线程组: 新线程会被分配到一个默认的线程组中。线程优先级和守护状态: 线程的优先级设置为默认值,且线程不是守护线程。示例代码虽然我们不能查看 defaultThreadFactory 的具体实现,但是我们可以提供一个示例实现,以展示如何创建具有默认属性的线程:public class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }在这个示例中,DefaultThreadFactory 使用 AtomicInteger 来确保线程池和线程编号的唯一性。创建的线程名称具有前缀 "pool-x-thread-y",其中 x 和 y 是自增的数字。线程不是守护线程,且优先级设置为 Thread.NORM_PRIORITY。特点使用 defaultThreadFactory 创建的线程工厂生成的线程具有标准的 Java 线程属性。这种线程工厂通常用于不需要特殊权限的应用程序。由于线程不是守护线程,它们的存在可以维持 JVM 的运行,直到所有非守护线程执行完毕。使用 defaultThreadFactory 可以确保线程在执行任务时具有标准的安全和执行属性,适合大多数常规用途。然而,如果应用程序需要特殊的线程属性,如守护线程或不同的优先级,可能需要自定义线程工厂。使用场景:适用于大多数标准应用程序,需要创建具有默认属性的线程。这是大多数 ExecutorService 实现的默认选择。9. unconfigurableExecutorService(ExecutorService executor)unconfigurableExecutorService 是 Java 中 java.util.concurrent 包的 Executors 类的一个静态工厂方法。这个方法用于创建一个不可配置的 ExecutorService 包装器,这意味着一旦包装后的 ExecutorService 被创建,就不能更改其配置,比如不能修改其线程池大小或任务队列等。以下是 unconfigurableExecutorService 方法的实现原理、源代码分析以及实现过程:实现原理封装: 将现有的 ExecutorService 封装在一个不可配置的代理中。不可修改: 所有修改配置的方法调用,如 shutdown, shutdownNow, setCorePoolSize 等,都将抛出 UnsupportedOperationException。转发: 除了配置修改的方法外,其他方法调用将被转发到原始的 ExecutorService。源代码分析unconfigurableExecutorService 方法的具体实现细节并未完全公开,因为它是 Executors 类的一个私有静态方法。但是,我们可以根据 Java 的 ExecutorService 接口和代理机制来分析其大致实现。以下是 unconfigurableExecutorService 方法的调用示例:public static ExecutorService unconfigurableExecutorService(ExecutorService executor) { return new FinalizableDelegatedExecutorService(executor); }这里,FinalizableDelegatedExecutorService 是 Executors 类的一个私有静态内部类,它实现了 ExecutorService 接口,并代理了对另一个 ExecutorService 的调用。实现过程初始化: 当调用 unconfigurableExecutorService 方法时,会返回一个新的 FinalizableDelegatedExecutorService 实例,它将原始的 ExecutorService 作为参数。方法调用拦截: 对 FinalizableDelegatedExecutorService 的方法调用将首先被拦截。配置修改拦截: 如果调用的方法是用于修改配置的,比如 shutdown 或 shutdownNow,将抛出 UnsupportedOperationException。转发其他调用: 对于其他不涉及配置修改的方法调用,比如 submit, execute, 将被转发到原始的 ExecutorService。示例代码下面V哥来模拟一个示例实现,以展示如何创建一个不可配置的 ExecutorService 代理:public class UnconfigurableExecutorService implements ExecutorService { private final ExecutorService executor; public UnconfigurableExecutorService(ExecutorService executor) { this.executor = executor; } @Override public void shutdown() { throw new UnsupportedOperationException("Shutdown not allowed"); } @Override public List<Runnable> shutdownNow() { throw new UnsupportedOperationException("Shutdown not allowed"); } @Override public boolean isShutdown() { return executor.isShutdown(); } @Override public boolean isTerminated() { return executor.isTerminated(); } @Override public void execute(Runnable command) { executor.execute(command); } // 其他 ExecutorService 方法的实现,遵循相同的模式 }在这个示例中,UnconfigurableExecutorService 拦截了 shutdown 和 shutdownNow 方法,并抛出了异常。其他方法则直接转发到原始的 ExecutorService。特点使用 unconfigurableExecutorService 创建的 ExecutorService 代理确保了线程池的配置不能被外部修改。这可以用于防止意外地更改线程池的状态,提高线程池使用的安全性。除了配置修改的方法外,其他所有方法都保持了原有 ExecutorService 的行为。使用 unconfigurableExecutorService 可以为现有的 ExecutorService 提供一个安全层,确保它们的状态不会被意外地更改。这对于在多线程环境中共享 ExecutorService 时特别有用。使用场景:适用于需要确保线程池配置在创建后不被更改的场景。例如,当多个组件共享同一个线程池时,可以防止一个组件意外修改配置转载自https://www.cnblogs.com/wgjava/p/18292258
  • [技术干货] 使用httpclient调用第三方接口返回javax.net.ssl.SSLHandshakeException异常
    1. 踩坑经历最近做了个需求,需要调用第三方接口获取数据,在联调时一直失败,代码抛出javax.net.ssl.SSLHandshakeException异常,具体错误信息如下所示:javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target2.原因分析因为调用第三方接口的代码是复用项目中原有的工具类(基于httpclient封装),所以在确认完传参没问题后,第一时间排除了编码问题。然后开始怀疑第三方提供的接口地址(因为竟然是IP+端口访问),在和第三方确认没有域名访问后,在浏览器里输入第三方的接口地址,发现证书有问题:又使用Postman调用第三方接口,也是失败,提示自签名证书:通过以上分析,可以发现出现该问题的根本原因是Java客户端不信任目标服务器的SSL证书,比如这个第三方使用的自签名证书。3.解决方案解决方案一般有2种,第1种方案是将服务器证书导入Java信任库,第2种方案是绕过SSL验证,这里采用第2种方案。首先,新建HttpClient工具类:import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; public class HttpClientUtils { public static CloseableHttpClient createIgnoreCertClient() throws NoSuchAlgorithmException, KeyManagementException { SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, new TrustManager[]{new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}, new java.security.SecureRandom()); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE); return HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build(); } }然后将原来声明httpClient的代码改为如下所示:CloseableHttpClient httpClient = HttpClientUtils.createIgnoreCertClient();注意事项:确保项目中引入了httpclient依赖:<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>转载自https://www.cnblogs.com/zwwhnly/p/18795523
总条数:737 到第
上滑加载中