-
前言 在当今数字化时代,数据的存储、传输与处理愈发依赖于灵活且高效的格式,JSON(JavaScript Object Notation)以其简洁、易读易写的特性脱颖而出,成为跨平台数据交换的首选格式之一。而在地理信息系统(GIS)领域,GeoJSON作为一种基于JSON的地理空间数据格式,为地理信息的表达与共享提供了强大支持。它能够以一种标准化的方式描述地理空间数据,包括点、线、面等几何对象以及与之相关的属性信息,广泛应用于地图绘制、空间分析、地理数据可视化等诸多场景。 Java作为一种功能强大、应用广泛的编程语言,在企业级应用开发、大数据处理、云计算等诸多领域占据着重要地位。随着地理空间数据应用的不断拓展,越来越多的Java开发者需要在项目中处理GeoJSON数据,例如从数据库动态生成GeoJSON数据以供前端地图应用展示,或者根据用户输入动态构建GeoJSON对象进行空间查询等。然而,对于许多Java开发者而言,动态创建JSON,尤其是结构相对复杂的GeoJSON,往往存在诸多困惑与挑战。如何在Java中高效、灵活地生成符合GeoJSON规范的数据,成为开发者亟待解决的问题。 本文将深入浅出地为读者呈现一份Java动态创建GeoJSON的完整实现指南。无论你是初涉GeoJSON的Java新手,还是希望在项目中优化GeoJSON处理流程的资深开发者,本文都将为你提供实用的思路与方法。我们将从Java处理JSON的基础讲起,介绍常用的JSON处理库,如Jackson、Gson等,并详细阐述它们在GeoJSON创建中的适用场景与优势。接着,深入剖析GeoJSON的结构组成,包括几何对象(点、线、多边形等)和属性部分,通过具体代码示例,逐步展示如何在Java中动态构建这些元素,实现从简单到复杂的GeoJSON对象生成。同时,结合实际应用场景,如地理数据的动态查询与转换为GeoJSON,探讨如何优化代码以提高性能和可维护性。 一、动态属性应用场景 本节将重点介绍动态属性的应用场景,以及需要考虑的一些问题。 1、场景介绍在面向GIS的业务场景中,我们通常可以将业务表中的列属性直接包装成Properties,然后通过后台返回给前端时,可以直接对这些数据进行展示。大家可以思考以下问题:假如一些属性信息在进行表连接查询时,并没有相关的业务表查询,而是要通过计算后才能给到前端的。这种情况下,我们还能只依靠纯SQL来解决这些问题吗?答案肯定是不行的,比如我们有一个场景,使用SQL的动态属性生成时,已经包含以下属性: String originalJson = "{\"type\" : \"Feature\", \"geometry\" : {\"type\":\"Point\",\"coordinates\":[113.902426,22.729881]}, \"properties\" : {\"id\" : 1369981, \"location\" : \"光明区玉塘街道文明路13号\", \"durationHours\" : 2}}";一键获取完整项目代码bash 然后我们需要在这个字符串中添加新的属性,这就是我们的使用场景。 2、需要考虑的问题 在实现这个需求的时候,需要考虑以下的问题,比如最简单的是如何实现简单的key-value的键值新增,更复杂一点的是如何实现嵌套对象的新增,还有更复杂的是如何实现嵌入的对象的新增。以上这些问题,都是需要我们考虑的,因此在本文后续的内容中我们都会进行实现和说明。 二、Java动态属性实现 本节将以Java语言为例,将从设计原则,Java核心类、编辑器的设计和从设计模式支持这几个角度进行介绍。让大家对这个动态属性生成实现有一个基本的认识。 1、设计原则这里我们使用面向对象的设计方法,因此设计的原则也是基本的OOP思想,即: /** * JSON属性操作工具类的面向对象设计 * 主要设计思想: * 1. 单一职责原则:每个类专注于一个特定功能 * 2. 开闭原则:扩展开放,修改关闭 * 3. 依赖倒置原则:依赖于抽象,而非具体实现 * 4. 组合优于继承:使用组合构建复杂功能 */一键获取完整项目代码bash2、核心类解析2.1主核心类 JsonPropertyManager/** * JsonPropertyManager - 外观模式(Facade Pattern) * 提供统一的静态接口,隐藏内部复杂性 * 设计原则:简化客户端调用,统一入口 */public class JsonPropertyManager { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // 私有构造器:防止实例化,确保工具类的正确使用方式 private JsonPropertyManager() { throw new IllegalStateException("工具类,无需实例化"); } /** * 静态工厂方法:创建JsonEditor实例 * 设计模式:工厂方法模式 * 好处:封装对象创建逻辑,便于后续扩展 */ public static JsonEditor createEditor(String jsonStr) throws JsonProcessingException { return new JsonEditor(jsonStr); }}一键获取完整项目代码java 2.2编辑器类:JsonEditor - 核心业务对象/** * JsonEditor - 建造者模式(Builder Pattern)+ 状态模式(State Pattern) * * 职责: * 1. 封装JSON文档的编辑状态 * 2. 提供链式调用的API * 3. 管理当前操作的目标节点 * * 面向对象特性: * - 封装:将JSON节点状态和操作封装在一起 * - 多态:支持多种数据类型操作 * - 聚合:组合了ArrayEditor等子组件 */public static class JsonEditor { // 状态变量:封装对象状态 private final ObjectNode rootNode; // 根节点 - 不变状态 private ObjectNode currentTargetNode; // 当前目标节点 - 可变状态 /** * 构造函数:初始化状态 * 面向对象原则:确保对象创建时处于有效状态 */ public JsonEditor(String jsonStr) throws JsonProcessingException { this.rootNode = (ObjectNode) OBJECT_MAPPER.readTree(jsonStr); this.currentTargetNode = rootNode; // 默认操作根节点 } /** * 目标节点设置方法 - 状态模式实现 * 允许动态切换操作上下文 */ public JsonEditor target(String nodePath) { // 实现路径解析和节点定位逻辑 return this; // 返回this支持链式调用 - 流畅接口模式 }}一键获取完整项目代码java 2.3数组编辑器类:ArrayEditor - 组合模式应用/** * ArrayEditor - 组合模式(Composite Pattern) * * 职责: * 1. 专门处理JSON数组操作 * 2. 提供类型安全的数组构建方法 * 3. 支持递归构建嵌套结构 * * 设计理念:将数组操作从JsonEditor中分离,实现单一职责 */public static class ArrayEditor { private final ArrayNode arrayNode; // 封装ArrayNode,提供更友好的API /** * 添加元素方法 - 支持多种数据类型,展示多态性 */ public ArrayEditor add(Object value) { // 运行时类型检查和处理 - 运行时多态 if (value instanceof String) { arrayNode.add((String) value); } else if (value instanceof Map) { // 处理Map类型 - 递归处理 arrayNode.add(OBJECT_MAPPER.valueToTree(value)); } return this; // 链式调用支持 } /** * 添加对象到数组 - 命令模式(Command Pattern)元素 * 通过Consumer回调,实现灵活的配置 */ public ArrayEditor addObject(Consumer<JsonEditor> consumer) { // 创建新对象节点 ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); // 使用临时JsonEditor配置对象 JsonEditor editor = new JsonEditor("{}") { @Override public ObjectNode getRootNode() { return objectNode; } }; // 应用配置 consumer.accept(editor); arrayNode.add(objectNode); return this; }}一键获取完整项目代码java 3、设计模式支持 这里将简单介绍在Json动态属性管理器设计中使用的一些设计模型。设计模式是个好方法,通过设计模式可以让代码设计更合理,扩展更方便。这里涉及的设计模式包含以下: 3.1建造者模式(Builder Pattern)/** * 建造者模式在工具类中的应用: * * 特点: * 1. 分离复杂对象的构建和表示 * 2. 允许逐步构建复杂对象 * 3. 提供流畅的API接口 * * 在JsonEditor中的体现: */public class JsonEditor { // 链式调用示例 public JsonEditor add(String key, String value) { currentTargetNode.put(key, value); return this; // 返回this实现链式调用 } public JsonEditor addMap(String key, Map<String, ?> map) { currentTargetNode.set(key, OBJECT_MAPPER.valueToTree(map)); return this; } // 使用示例:流畅的API调用 JsonEditor editor = JsonPropertyManager.createEditor(jsonStr) .target("properties") .add("status", "处理中") .addMap("contact", contactMap) .addNestedObject("analysis", this::configureAnalysis);}一键获取完整项目代码java 3.2策略模式(Strategy Pattern)/** * 策略模式:通过函数式接口实现不同的数据处理策略 */public class JsonEditor { /** * 接受Consumer策略,对属性值执行自定义操作 */ public JsonEditor with(String key, Consumer<JsonNode> action) { JsonNode node = currentTargetNode.get(key); if (node != null) { action.accept(node); // 执行策略 } return this; } /** * 接受Function策略,转换属性值 */ public JsonEditor transform(String key, Function<JsonNode, JsonNode> transformer) { JsonNode node = currentTargetNode.get(key); if (node != null) { JsonNode transformed = transformer.apply(node); // 应用转换策略 currentTargetNode.set(key, transformed); } return this; } // 使用示例:应用不同的策略 editor.with("data", node -> { // 自定义处理逻辑 System.out.println("Processing node: " + node); }); editor.transform("array", node -> { // 自定义转换逻辑 return node.isArray() ? node : OBJECT_MAPPER.createArrayNode(); });}一键获取完整项目代码java 3.3模板方法模式(Template Method Pattern)/** * 模板方法模式:定义算法骨架,具体步骤由子类或回调实现 * * 在addNestedObject方法中的体现: */public class JsonEditor { /** * 模板方法:定义创建和配置嵌套对象的步骤 * 1. 创建嵌套对象节点 * 2. 保存当前状态 * 3. 应用配置(由consumer实现) * 4. 恢复状态 * 5. 添加嵌套对象 */ public JsonEditor addNestedObject(String key, Consumer<JsonEditor> consumer) { // 步骤1:创建嵌套对象 ObjectNode nestedNode = OBJECT_MAPPER.createObjectNode(); ObjectNode originalTarget = currentTargetNode; // 步骤2:保存状态 // 步骤3:应用配置(具体实现由consumer提供) currentTargetNode = nestedNode; consumer.accept(this); // 步骤4:恢复状态 currentTargetNode = originalTarget; // 步骤5:添加嵌套对象 currentTargetNode.set(key, nestedNode); return this; }}一键获取完整项目代码java 通过这些设计模式的使用,可以有效的提升我们的应用程序的实现。在需要扩展时非常方便。 三、调用实践 本节将基于动态属性管理独享来实现简单属性、嵌套属性、负责类型嵌入这几个方面来进行实例调用实践,为大家提供调用演示。 1、添加简单属性 首先来介绍如何添加简单属性,这是最简单的属性添加,可以理解成主要就是进行key_value的值映射。调用代码如下: // 原始JSON字符串String originalJson = "{\"type\" : \"Feature\", \"geometry\" : {\"type\":\"Point\",\"coordinates\":[113.902426,22.729881]}, \"properties\" : {\"id\" : 1369981, \"location\" : \"光明区玉塘街道文明路13号\", \"reason\" : \"故障\", \"startTime\" : \"10:13\", \"estimatedRestore\" : \"10:15\", \"durationHours\" : 2}}"; System.out.println("=== 原始JSON ===");System.out.println(originalJson); System.out.println("\n=== 示例1: 添加List<Map<?>> - 不限定key ===");JsonEditor editor1 = JsonPropertyManager.createEditor(originalJson); // 创建不同类型的List<Map>List<Map<String, Object>> poiList = new ArrayList<>(); // 第一个POI - 简单类型Map<String, Object> poi1 = new HashMap<>();poi1.put("name", "南山外国语学校");poi1.put("type", "学校");poi1.put("distance", 500);poi1.put("isPublic", true);poiList.add(poi1); HashMap<String,Object> cotactMap = new HashMap<String, Object>();// 第二个POI - 包含嵌套对象Map<String, Object> poi2 = new HashMap<>();poi2.put("name", "某大型数据中心");poi2.put("type", "商业");poi2.put("capacity", "1000台服务器");cotactMap.put("person", "李主任");cotactMap.put("phone","13800138001");poi2.put("contact", cotactMap);poiList.add(poi2);// 添加POI列表到propertieseditor1.target("properties").addListMap("majorPOIs", poiList);一键获取完整项目代码java 2、添加嵌套类型 如果有嵌套类型,属性添加进来则会有一些问题。可以使用以下方法来进行动态添加,代码如下: JsonEditor editor2 = JsonPropertyManager.createEditor(originalJson);editor2.target("properties") .addNestedObject("analysis", nested -> { nested.add("riskLevel", "中") .add("impactRadius", 1000) .addNestedArray("affectedServices", services -> { services.add("电力供应") .add("网络通信") .add("安防系统"); }) .addNestedObject("timeline", timeline -> { timeline.add("detectionTime", "10:10") .add("dispatchTime", "10:20") .add("estimatedCompletion", "12:00"); }); }) .addNestedArray("repairTeams", teams -> { try {teams.addObject(team -> { team.add("name", "光明供电局抢修一队") .add("members", 5) .add("equipment", Arrays.asList("绝缘杆", "万用表", "工具箱")); }) .addObject(team -> { team.add("name", "技术支持小组") .add("members", 3) .add("specialties", Arrays.asList("变压器维修", "线路检测")); });} catch (JsonProcessingException e) {// TODO Auto-generated catch blocke.printStackTrace();} }); System.out.println(editor2.toPrettyJson());一键获取完整项目代码java 通过以上方法基本就可以实现Json的动态属性管理,如果需要更复杂的属性添加可以根据方法来进行添加。篇幅有限,这里不进行赘述。大家如果对如何进行Json的动态属性扩展感兴趣,在自己的项目中可能会遇到这类问题,可以下载源码:Java实现JSON的动态属性添加源码。里面代码大家可以自行进行优化。 程序调用完成后,可以在控制台看到以下输出: 四、总结 以上就是本文的主要内容,本文将深入浅出地为读者呈现一份Java动态创建GeoJSON的完整实现指南。无论你是初涉GeoJSON的Java新手,还是希望在项目中优化GeoJSON处理流程的资深开发者,本文都将为你提供实用的思路与方法。通过阅读本文,你将不仅掌握Java动态创建GeoJSON的技术细节,更将理解其背后的原理与最佳实践,从而在实际项目中能够灵活运用,轻松应对各种与GeoJSON相关的开发任务,让Java动态创建GeoJSON变得不再困难。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/156097415
-
最近在深入学习 Java 后端和 Redis 中间件时,遇到了一个非常经典且重要的问题:在分布式场景下,如何生成一个全局唯一的 ID?在单体架构时代,我们习惯使用数据库的自增 ID(Auto Increment),但在分库分表、微服务的高并发场景下,这种方式由于性能瓶颈和单点问题,显然已经力不从心。今天这篇博客就来总结一下目前业界最主流的 4 种全局唯一 ID 生成策略,分析它们的原理、优缺点以及适用场景。什么样的 ID 才是好 ID?在设计 ID 生成器之前,我们需要明确“好 ID”的标准。通常有以下几个核心要求:全局唯一性:这是最基本的要求,不能出现重复。高可用 & 高性能:生成 ID 的动作非常频繁,不能成为系统的瓶颈,且服务要足够稳定。递增性(趋势有序):这一点常被忽略。对于使用 MySQL(InnoDB 引擎)的系统,主键建议保持递增,因为 InnoDB 使用 B+ 树索引,有序的主键写入能避免频繁的“页分裂”,极大提升写入性能。安全性:某些业务场景下(如订单号),ID 不应过于明显地暴露业务量(比如不能让人轻易猜出你一天有多少单)。方案一:UUID (Universally Unique Identifier)UUID 是最简单、最暴力的方案。JDK 原生支持,一行代码搞定。代码实现public static void main(String[] args) { // 生成一个 UUID,并去掉中间的横线 String id = UUID.randomUUID().toString().replace("-", ""); System.out.println("UUID: " + id);}一键获取完整项目代码java优缺点分析优点:性能极高:完全在本地生成,没有网络消耗。使用简单:不依赖任何外部组件(DB、Redis 等)。缺点:无序性(致命伤):UUID 是无序的字符串。如果作为 MySQL 主键,会导致大量的数据页分裂和移动,严重拖慢插入速度。存储成本高:32 个字符(或 16 字节),相比 Long 类型特别占空间,也会导致索引变大。信息不安全:完全随机,无法携带时间或业务含义。结论:适合生成 Token、Session ID 或非数据库主键的场景。坚决不建议用作 MySQL 的主键。方案二:数据库自增 (Database Auto-Increment)利用 MySQL 的 auto_increment 特性,或者 Oracle 的 Sequence。原理应用服务向数据库插入数据,数据库自动累计 ID。优缺点分析优点:简单:利用现有数据库功能,成本低。单调递增:对索引非常友好,查询效率高。缺点:并发瓶颈:在高并发下,数据库往往是最大的瓶颈。分库分表麻烦:如果未来需要分库,不同库的自增 ID 会重复。虽然可以通过设置不同的“步长”(Step)来解决(如 DB1 生成 1,3,5... DB2 生成 2,4,6...),但这增加了扩容和维护的难度。单点故障:数据库挂了,整个 ID 生成服务就不可用了。结论:适合并发量不高的中小项目,或者不需要分库分表的数据表。方案三:Redis 自增策略Redis 是单线程处理命令的,其 INCR 命令是原子的,天生适合做计数器。这是我最近在学 Redis 时觉得非常有意思的一个应用点。代码思路 (Java + RedisTemplate)为了避免 ID 被推测出业务量,通常会结合“时间戳”使用。格式示例:yyyyMMdd + Redis自增值。// 伪代码示例public long generateId(String keyPrefix) { // 1. 生成时间戳部分 String dateStr = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now()); // 2. 利用 Redis 原子递增 // key 举例: icr:order:20251216 Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + dateStr); // 3. 拼接 ID (实际生产中通常需要通过位运算或字符串填充补齐位数) return Long.parseLong(dateStr + String.format("%06d", increment));}一键获取完整项目代码java优缺点分析优点:高性能:基于内存操作,吞吐量远高于数据库。有序递增:对数据库索引友好。灵活:可以方便地把日期、业务类型编排进 ID 中。缺点:强依赖组件:如果 Redis 挂了,ID 生成服务就断了(需要配置 Sentinel 或 Cluster 高可用)。运维成本:引入了额外的中间件维护成本。 结论:非常适合高并发的业务场景(如秒杀、订单生成),且生成的 ID 具有业务含义。方案四:雪花算法 (Snowflake)这是目前分布式系统中最流行、最成熟的方案,由 Twitter 开源。它的核心思想是将一个 64 位的 long 型数字切割成不同的部分。结构图解 (64 bit)1 bit:符号位(固定为0)。41 bits:时间戳(毫秒级,可以使用 69 年)。10 bits:机器 ID(支持 1024 个节点)。12 bits:序列号(同一毫秒内支持生成 4096 个 ID)。代码实现通常不需要自己手写位运算,推荐使用成熟的工具包,例如 Hutool。// 引入 Hutool 依赖后public class IdTest { public static void main(String[] args) { // 参数1: 终端ID, 参数2: 数据中心ID Snowflake snowflake = IdUtil.getSnowflake(1, 1); long id = snowflake.nextId(); System.out.println("Snowflake ID: " + id); }}一键获取完整项目代码java优缺点分析优点:极高并发:每秒可生成几百万个 ID。不依赖网络:本地生成(除了启动时校验机器 ID),无单点故障。趋势递增:整体按时间递增,索引性能好。缺点:时钟回拨问题:严重依赖服务器时间。如果服务器时间被回调(比如校准时间),算法可能会生成重复 ID。 结论:几乎所有互联网大厂的主流选择,适合超大规模的分布式系统。总结对比最后,用一张表来总结这几种策略:策略 唯一性 有序性 性能 依赖组件 核心痛点UUID 高 无 极高 无 索引性能差,ID太长DB自增 高 严格有序 低 数据库 并发瓶颈,扩展麻烦Redis 高 严格有序 高 Redis 依赖 Redis 高可用Snowflake 高 趋势有序 极高 无 时钟回拨问题个人建议:如果你是初学者或者项目规模较小,Redis 自增是一个非常好的练手方案,既能满足性能要求,又能加深对 Redis 的理解。而如果是企业级的大型项目,Snowflake(配合 Hutool 等工具库)则是目前的最优解。希望这篇总结对大家有所帮助!如果你有更好的方案,欢迎在评论区交流。————————————————原文链接:https://blog.csdn.net/m0_58782205/article/details/155989061
-
1. Java概述Java 是一种跨平台、面向对象的高级编程语言,广泛应用于企业级开发、大数据、移动端等领域。1.1 什么是 Java语言:人和人交流用的工具,比如中文、英文。计算机语言:人与计算机之间信息交流沟通的一种特殊语言,用来告诉计算机应该做什么。Java:一种非常流行的计算机语言,我们可以通过 Java 代码一步步指挥计算机完成各种任务。2. 环境准备这里需要配置 JDK。2.1 JDK的配置2.1.1 JDK概述JDK(Java Development Kit)称为 Java 开发工具,包含了 JRE 和开发工具。在这里,我将带大家快速配置 JDK。注意:针对不同的操作系统,需要下载对应版本的 JDK。如果电脑是 Windows 32 位的,建议重装成 64 位操作系统。因为 Java 从 9 版本开始,不再提供 32 位的安装包。2.1.2 快速下载打开 Oracle 官网下载地址:Oracle快速传送门选择 JDK21 版本,这里有 Linux、macOS、Windows 三种操作系统,根据电脑版本选择下载。一般只要是 LTS 版本(如 17 或 21) 都可以正常学习使用。Windows 其他版本(JDK8、17、21)下载:百度网盘快速传送门这里以 Windows 为例,点击 Installer,下载即可。 傻瓜式安装,下一步即可。默认的安装路径是在 C:\Program Files 下。建议自己创建一个文件夹,将开发相关的东西都塞进去,便于管理与查找。该文件夹上的所有路径,不能有中文,不能有特殊符号,不能有空格。2.1.3 环境配置配置环境变量的作用:如果我想在电脑的任意目录下,都可以启动一个软件,那么就可以把这个软件的路径配置到环境变量中。在启动软件的时候,操作系统会先在当前路径下找,如果在当前路径没有找到,再到环境变量的路径去找。如果都找不到就提示无法启动。具体配置方法:快捷指令:打开 Win+R输入 sysdm.cpl 回车选择“高级”,再点击下面的“环境变量”手动查找:右键“此电脑”,选择“属性”点击左侧的“高级系统设置”选择“高级”,再点击下面的“环境变量”点击“新建”,将 JDK 安装目录写进去,变量名起 JAVA_HOME点击 Path,点击“编辑”点击“新建”,填写 %JAVA_HOME%\bin将 %JAVA_HOME%\bin 移到最上面移动的好处:在 CMD 中打开软件时,会先找当前路径,再找环境变量,在环境变量中从上往下依次查找,如果路径放在最上面,查找最快。2.1.4 检查是否安装成功打开 Win+R输入 cmd输入 java -version注意:java -version 中间有一个空格如果有以下效果,则成功,21.0.6 是我的 JDK 版本。java version "21.0.6" 2025-01-21 LTSJava(TM) SE Runtime Environment (build 21.0.6+8-LTS-188)Java HotSpot(TM) 64-Bit Server VM (build 21.0.6+8-LTS-188, mixed mode, sharing)一键获取完整项目代码shell拓展:Win10 可能会有个 BUG,当电脑重启后,环境变量可能会失效。如果你遇到这种情况,可以在 PATH 中直接添加完整的 JDK bin 目录来解决。2.2 IDEA的配置如果你是在学校,可能会让你使用 Eclipse,但我强烈建议你使用 IDEA。2.2.1 IDEA 概述IDEA 全称 IntelliJ IDEA,是用于 Java 语言开发的集成环境,它是业界公认的目前用于 Java 程序开发最好的工具。集成环境:把代码编写、编译、执行、调试等多种功能综合到一起的开发工具。2.2.2 快速下载和配置IDEA 下载链接:IDEA官网下载快速传送门下载完,进行安装,将目录改为自己的文件夹。其余只需要勾选创建快捷方式,其他的一直 next 即可。 注:IDEA 是需要付费的,个人使用每年约 1400 元,支持正版人人有责。因此,请大家务必不要刻意到网上寻找对应版本 IDEA 激活码进行激活。3. IDEA 介绍3.1 IDEA 概述IntelliJ IDEA 是一款强大的 Java 集成开发环境(IDE),被广泛认为是开发 Java 程序的最佳工具之一。在开始本文档之前,请确保你已正确安装 JDK 和 IDEA。3.2 项目层级结构在 IDEA 中,Java 项目按以下层级进行组织:project(项目、工程):比如 QQ、微信等应用。module(模块):项目的功能子模块,如聊天、通讯录。package(包):模块内的业务分类,如聊天模块中的朋友圈、视频号等。class(类):具体的代码单元,实际开发主要在类中完成。3.3 快速入门创建一个新的项目需要遵循以下层级:创建项目(Project)新建包(Package)新建类(Class)创建一个新的项目在弹出的窗口中:选择项目类型:Java填写项目名称和路径Name:项目名称Location:项目存储路径add sample code:取消添加简单代码选项右键 src 文件夹选择 New → Java Class输入类名,如 HelloWorld回车确认输入以下代码:public class HelloWorld { public static void main(String[] args) { System.out.println("Hello,World"); }}一键获取完整项目代码java右键类文件,点击“运行”即可。 这就完成了第一个程序的运行。代码解析:// class:定义一个类,后面跟随类名// HelloWorld:类的名字,通常与文件名一致// {}:类的范围,代码需要在范围中书写public class HelloWorld { // 主程序入口(main 方法) // 程序运行会从这里开始,从上往下依次执行 // 在 IDEA 中,可以通过输入 psvm 快速生成 public static void main(String[] args) { // 输入语句 // 作用:将括号内的内容打印到控制台 // 快速生成:sout System.out.println("Hello,World"); }}一键获取完整项目代码java结语在本文中,我们掌握了以下知识点:安装了 Java 开发环境:JDK 和 IDEA。编写并运行了第一个 Java 程序:HelloWorld。初步认识了 class、main 等基础关键字的作用。如果本文对你有帮助:欢迎点赞、收藏,让更多正在学 Java 的同学看到。遇到问题或有不同理解:可以在评论区留言,一起讨论、互相学习。想系统看更多内容:可以关注专栏《Java成长录》,一起把基础打牢。————————————————原文链接:https://blog.csdn.net/Chase_______/article/details/155501655
-
阻塞队列有哪些?在Java的java.util.concurrent包里面,阻塞队列的实现挺多的,我们可以根据它的功能和结构来记,主要分这么几类:1. 按容量划分:有界队列: 就是队列有固定的容量。ArrayBlockingQueue: 最经典的一个,底层是数组,创建时必须指定大小。它的生产和消费用同一把锁,性能相对稳定。LinkedBlockingQueue: 底层是链表,它既可以是有界的(构造时指定容量),也可以默认是无界的(默认是Integer.MAX_VALUE,几乎相当于无界)。它的生产和消费用了两把锁,在高并发场景下吞吐量通常比ArrayBlockingQueue更高。无界队列: 理论上是无限的,只要内存够就能一直放。PriorityBlockingQueue: 一个支持优先级排序的无界队列。元素必须实现Comparable接口,或者构造时传入Comparator。它出队的顺序是按优先级来的,不是先进先出DelayQueue: 一个很特殊的队列,里面放的是实现了Delayed接口的元素。每个元素都有个到期时间,只有到期了的元素才能被取出来。典型应用就是做缓存过期、定时任务调度。2. 特殊功能的队列:SynchronousQueue: 这个队列非常特别,它不存储元素。每一个put操作必须等待一个take操作,就像“手递手”交接一样。它直接传递任务,效率很高,常用于线程池之间直接传递工作。CachedThreadPool用的就是它。LinkedTransferQueue: 是LinkedBlockingQueue和SynchronousQueue的结合体。它多了个transfer方法,如果当前有消费者在等待,就直接把元素给消费者;如果没有,就入队,并且会阻塞直到该元素被消费掉。性能很好。所以,我比较常记的是ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue和SynchronousQueue这几个,它们各有各的应用场景。拒绝策略有哪些?拒绝策略是线程池的一个组成部分。当线程池的工作队列满了,并且所有线程也都达到最大线程数了,这时候再来新任务,就会触发拒绝策略。Java在ThreadPoolExecutor类里提供了4种内置的策略,都实现了RejectedExecutionHandler接口:1. AbortPolicy(中止策略 - 默认策略):做法: 直接抛出一个RejectedExecutionException异常。感受: “比较粗暴,但好处是能让我们及时感知到系统出了饱和问题。”2. CallerRunsPolicy(调用者运行策略):做法: 不抛弃任务,也不抛异常,而是将任务回退给调用者(提交任务的线程),让调用者自己去执行这个任务。感受: “这是一种‘负反馈’机制。提交任务的线程突然要自己去干活,它就忙起来了,自然就慢下来提交新任务了,给了线程池一个喘息的机会。这个策略在生产环境挺实用的,能平滑地降低流量。”3. DiscardPolicy(丢弃策略):做法: 默默地把新提交的任务丢弃掉,不执行,也不给任何通知。感受: “风险比较大,因为任务丢了我们都不知道。除非是一些无关紧要的场景,否则一般不推荐。”4. DiscardOldestPolicy(丢弃最老策略):做法: 把工作队列里排队时间最长的那个任务(队头的任务)丢掉,然后尝试把新任务再放进队列。感受: 这个策略有点‘喜新厌旧’。它可能丢弃掉一个非常重要的老任务,风险也挺高的,用的时候得想清楚业务上能不能接受。当然,如果这四种都不满足需求,我们还可以自己实现RejectedExecutionHandler接口,来自定义拒绝策略,比如把拒绝的任务持久化到磁盘,或者记录日志后发个告警等等。面试回答Java中的阻塞队列主要有像ArrayBlockingQueue这种基于数组的有界队列,也有LinkedBlockingQueue这种基于链表可以无界的队列。还有一些特殊用途的,比如按优先级出队的PriorityBlockingQueue,做延时任务的DelayQueue,以及不存元素专门做传递的SynchronousQueue。 关于拒绝策略,是线程池满载后的处理方式。默认的AbortPolicy会直接抛异常;CallerRunsPolicy会让提交任务的线程自己去执行,这个我们项目在用,能平滑流量;还有两种是直接丢弃任务或者丢弃队列中最老的任务。如果都不行,我们也可以根据业务自己实现一个。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/155534236
-
前言:这是一个一个经典的树形动态规划(Tree DP)问题,通常被称为 “最大子树和” 或 “带权树的最大连通子图和”采用动态规划和贪心算法!题目:在 X 森林里,上帝创建了生命之树。他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。上帝要在这棵树内选出一个节点集 S,使得对于 SS 中的任意两个点 a,b,都存在一个点列 a,v1,v2,⋯,vk,b 使得这个点列中的每个点都是 S 里面的元素,且序列中相邻两个点间有一条边相连。在这个前提下,上帝要使得 S 中的点所对应的整数的和尽量大。这个最大的和就是上帝给生命之树的评分。经过 atm 的努力,他已经知道了上帝给每棵树上每个节点上的整数。但是由于 atm 不擅长计算,他不知道怎样有效的求评分。他需要你为他写一个程序来计算一棵树的分数。集合 S 可以为空。输入描述第一行一个整数 n 表示这棵树有 n 个节点。第二行 n 个整数,依次表示每个节点的评分。接下来 n−1 行,每行 2 个整数 u,v,表示存在一条 u 到 v 的边。由于这是一棵树,所以是不存在环的。其中,0<n≤105, 每个节点的评分的绝对值不超过 106。输出描述输出一行一个数,表示上帝给这棵树的分数。输入输出样例示例输入51 -2 -3 4 54 23 11 22 5一键获取完整项目代码输出8一键获取完整项目代码运行限制最大运行时间:3s最大运行内存: 256M题目分析:一、题目描述给定一棵包含 n 个节点的无向树,每个节点有一个整数值(可正可负)。要求选择一个连通的子图(即若干相连的节点),使得这些节点的权值之和最大。返回这个最大和。二、问题输入结构:一棵树(无环连通无向图),可用邻接表表示。目标:找一个连通的节点集合,使其权值和最大。关键约束:所选节点必须构成连通子图(不能跳着选)。注意:这不是“最大独立集”,也不是“最长路径”,而是带权最大连通子图。三、观察树的性质:任意两个节点间有唯一路径 → 任何连通子图也是一棵树。最优解必有“根”:最大连通子图一定存在一个“最高点”(在 DFS 遍历时最先被访问的节点)。局部决策影响全局:若某个子树的总贡献为负,则不应纳入当前解。四、算法步骤构建邻接表存储树。从任意节点(如 1)开始 DFS。对每个节点:初始化当前和为自身权值;递归处理子节点;仅累加正贡献的子树;更新全局最大值;返回当前和供父节点使用。输出 maxSum。代码:package com.itdonghuang.Test; import java.util.*; public class JavaTest1 { static long maxsum = 0; public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); int[] val = new int[n + 1]; for (int i = 1; i <= n; i++) { val[i] = scan.nextInt(); } List<Integer>[] opp = new ArrayList[n + 1]; for (int i = 1; i <= n; i++) { opp[i] = new ArrayList<>(); } for (int i = 0; i < n - 1; i++) { int u = scan.nextInt(); int v = scan.nextInt(); opp[u].add(v); opp[v].add(u); } dfs(1, -1, opp, val); System.out.println(maxsum); scan.close(); } public static long dfs(int u, int parent, List<Integer>[] opp, int[] val) { long sum = val[u]; for (int v : opp[u]) { if (v == parent) continue; long childSum = dfs(v, u, opp, val); if (childSum > 0) { sum += childSum; } } if (sum > maxsum) { maxsum = sum; } return sum; }}一键获取完整项目代码java代码分析:一、初始化,赋值定义n、val、opp分别接受键盘输入的节点数、每个结点的评分、u<-->v的边opp是一个数组array,每个元素是List<Integer>,构成无向图Scanner scan = new Scanner(System.in); int n = scan.nextInt(); int[] val = new int[n + 1];for (int i = 1; i <= n; i++) { val[i] = scan.nextInt();} List<Integer>[] opp = new ArrayList[n + 1];for (int i = 1; i <= n; i++) { opp[i] = new ArrayList<>();}for (int i = 0; i < n - 1; i++) { int u = scan.nextInt(); int v = scan.nextInt(); opp[u].add(v); opp[v].add(u);}一键获取完整项目代码java二、dfs方法讲解 逐行详解第 1 行:long sum = val[u];含义:当前子树至少包含节点 u 自己。用 long,防止整数溢出(权值可能很大)。关键思想:我们计算的是 “必须包含当前节点 u 的最大连通子图和”。注意:这个子图必须包含 u,但可以选择性地包含它的某些子树。第 2–3 行:遍历邻居 + 跳过父节点for (int v : opp[u]) { if (v == parent) continue;一键获取完整项目代码javaopp[u] 是节点 u 的所有邻居(来自邻接表)。因为树是无向图,u 的邻居包括它的父节点和子节点。但我们是从根往下 DFS 的,所以要避免走回父节点,否则会无限递归或重复访问。举例:如果从 1 → 2 → 3,那么在 dfs(2, 1) 中,opp[2] 包含 1 和 3,必须跳过 1(因为它是父节点),只处理 3。第 4 行:long childSum = dfs(v, u, opp, val);递归调用:进入子节点 v,并告诉它:“你的父节点是 u”。返回值含义:childSum = 以 v 为根的子树中,包含 v 的最大连通子图的和。再次强调:这个值必须包含 v,但可能只包含 v 自己(如果子树都是负的)。第 5 行:if (childSum > 0) sum += childSum;这是整个算法最核心的贪心思想!为什么只加正数?如果某个子树的最大和是 负数(比如 -5),把它加到当前节点只会让总和变小。所以我们只“吸收”那些能带来正收益的子树。这相当于:不选那些负贡献的子树分支。类比数组版“最大子数组和”(Kadane 算法):如果前缀和 < 0,就丢掉,重新开始。这里同理:如果子树和 < 0,就“断开”,不选它。举个例子:val[u] = 3子树 A 贡献 +4 → 加上 → 总和变成 7子树 B 贡献 -2 → 不加 → 总和还是 7最终 sum = 3 + 4 = 7第 6 行:if (sum > maxsum) maxsum = sum;更新全局答案!sum 是“包含当前节点 u 的最大连通子图和”。而全局最优解一定是以某个节点为“最高点”的连通子图(因为树是连通无环的)。所以我们在每个节点都尝试一次,取最大值。这就是为什么不需要额外判断“路径是否跨子树”——因为任何连通子图都有一个“顶部节点”,我们会在那里计算它。第 7 行:return sum;返回值用途:告诉父节点,“如果你把我(以 u 为根的这部分)接上去,最多能给你增加 sum 的收益”。父节点会根据这个值决定是否“吸收”你。这是一个典型的 自底向上(bottom-up) 的信息传递过程。public static long dfs(int u, int parent, List<Integer>[] opp, int[] val) { long sum = val[u]; // 当前子树至少包含自己 for (int v : opp[u]) { if (v == parent) continue; // 避免回溯到父节点(防止死循环) long childSum = dfs(v, u, opp, val); // 递归计算以 v 为根的子树的最大“贡献值” if (childSum > 0) { sum += childSum; // 只有当子树贡献为正时,才合并进来 } } if (sum > maxsum) { maxsum = sum; // 更新全局最大子树和 } return sum; // 返回以 u 为根的子树能向上提供的最大和(用于父节点决策)}一键获取完整项目代码java结语:今天是我刷算法的第N天,每天刷算法题,写写项目,补充知识点。完善知识体系,加油加油!希望可以帮助到你!————————————————原文链接:https://blog.csdn.net/speaking_me/article/details/156127697
-
前言本文主要介绍与Java中抽象类和接口相关的部分知识。一、抽象类在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。在打印图形例子中,我们发现,父类Shape中的draw方法好像并没有什么实际工作,主要的绘制图形都是由Shape的各种子类的draw方法来完成的。像这种没有实际工作的方法,我们可以把它设计成一个抽象方法(abstract method),包含抽象方法的类我们称为抽象类(abstract class)。在Java中,一个类如果被abstract修饰称为抽象类,抽象类中被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。// 抽象类:被abstract修饰的类public abstract class Shape {// 抽象方法:被abstract修饰的方法,没有方法体abstract public void draw();abstract void calcArea();// 抽象类也是类,也可以增加普通方法和属性public double getArea(){return area;}protected double area; // 面积}一键获取完整项目代码java需要注意的是,抽象类也是类,内部是可以包含普通方法和属性的,甚至构造方法也可以。同时,抽象类有着如下的特性,首先它无法直接实例化对象。我们可以键入如下的代码:package demo; public abstract class Shape { public abstract void draw(); abstract void calcArea(); public double getArea() { return area; } protected double area;} 一键获取完整项目代码javapackage demo; public class Test { public static void main(String[] args) { Shape shape = new Shape(); }}一键获取完整项目代码java其运行结果如下: 其次抽象方法是不能被private修饰的。比如下面的代码:package demo; public abstract class Shape { private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } protected double area;} 一键获取完整项目代码java其运行结果如下: 再然后抽象方法无法被final和static修饰,因为其一定会被子类方法重写。我们键入如下的代码:package demo; public abstract class Shape { public abstract void draw(); //private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } abstract final void methodA(); abstract public static void methodB(); protected double area;} 一键获取完整项目代码java其运行结果如下: 并且,抽象类必须被继承,在继承之后子类要重写父类中的抽象方法,否则子类也是抽象类,必须使用abstract修饰,比如如下的代码:package demo; public abstract class Shape { public abstract void draw(); //private abstract void draw(); abstract void calcArea(); public double getArea() { return area; } //abstract final void methodA(); //abstract public static void methodB(); protected double area;} 一键获取完整项目代码javapackage demo; public class Rect extends Shape{ private double length; private double width; Rect(double length, double width){ this.length = length; this.width = width; } public void draw(){ System.out.println("矩形: length= "+length+" width= " + width); } public void calcArea(){ area = length * width; }}一键获取完整项目代码javapackage demo; public class Circle extends Shape{ private double r; final private static double PI = 3.14; public Circle(double r){ this.r = r; } public void draw(){ System.out.println("圆:r = "+r); } public void calcArea(){ area = PI * r * r; }}一键获取完整项目代码javapackage demo; public abstract class Triangle extends Shape { private double a; private double b; private double c; @Override public void draw() { System.out.println("三角形:a = "+a + " b = "+b+" c = "+c); }}一键获取完整项目代码java我们这样是可以正常编译通过的。最后,抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类;抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量。那么我们怎么去认识到抽象类的作用呢?抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类。然后让子类重写抽象类中的抽象方法。有些读者可能会说了,普通的类也可以被继承,普通的方法也可以被重写,为啥非得用抽象类和抽象方法呢?确实如此,但是使用抽象类相当于多了一重编译器的校验,使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。很多语法存在的意义都是为了预防出错,例如我们曾经用过的final也是类似,创建的变量用户不去修改,不就相当于常量嘛?但是加上final能够在不小心误修改的时候,让编译器及时提醒我们。充分利用编译器的校验,在实际开发中是非常有意义的。二、接口首先我们需要清楚接口究竟是什么?在现实生活中,接口的例子比比皆是,比如笔记本上的USB口,电源插座等。 电脑的USB口上,可以插U盘、鼠标、键盘……所有符合USB协议的设备;电源插座插孔上,可以插电脑、电视机、电饭煲……所有符合规范的设备。通过上述例子可以看出接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。在Java中,接口可以看成是多个类的公共规范,是一种引用数据类型。接口的定义格式与定义类的格式基本相同,将class关键字换成 interface关键字,就定义了一个接口。public interface 接口名称{// 抽象方法public abstract void method1(); // public abstract 是固定搭配,可以不写public void method2();abstract void method3();void method4();// 注意:在接口中上述写法都是抽象方法,更推荐方式4,代码更简洁}一键获取完整项目代码java创建接口时,接口的命名一般以大写字母I开头;接口的命名一般使用形容词词性的单词;阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性。对于接口来说,它有着如下的特性,第一是接口类型虽然是一种引用类型,但是我们不能直接new接口的对象。我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { public static void main(String[] args) { Shape shape = new Shape(); }}一键获取完整项目代码java其运行结果如下: 第二是接口中的每一个方法都是public的抽象方法,也就是说接口中的方法会被隐式指定为public abstract,并且只能是这个public abstract,其他修饰符就会报错。我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); public abstract void getArea( double a); private void calArea(double a); public static void main(String[] args) { }}一键获取完整项目代码java其运行结果如下: 第三是接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(){ System.out.println("draw()......."); } public static void main(String[] args) { }}一键获取完整项目代码java其运行结果如下: 第四是在重写接口中方法时,不能使用默认的访问权限,我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a);}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ void draw(){ System.out.println("实现draw()......"); } void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); test.draw(); }}一键获取完整项目代码java其运行结果如下: 第五是接口中可以含有变量,但是接口中的变量会被隐式指定为public static final变量,我们可以键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0;}一键获取完整项目代码javapackage demo2; public class Test{ public static void main(String[] args) { System.out.println(IShape.a); }}一键获取完整项目代码java其运行结果如下: 我们可以通过接口进行访问,说明a是具有static属性的。然后我们修改Test类的代码如下:package demo2; public class Test{ public static void main(String[] args) { System.out.println(IShape.a); IShape.a = 2.0; }}一键获取完整项目代码java其运行结果如下: 此时说明其具有final属性。第六则是接口中不能有静态代码块和构造方法,我们键入如下代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; public IShape(){ }}一键获取完整项目代码java其运行结果如下: 再修改代码如下:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; {}}一键获取完整项目代码java其运行结果如下: 第七是接口虽然不是类,但是接口编译完成后字节码文件的后缀也是.class;并且如果类中没有实现接口中所有的抽象方法,那么类就必须设置为抽象类。第八是接口中可以包含default方法,并且如果方法被default修饰,那么就可以有具体的实现。我们键入如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; default void calArea() { System.out.println("calArea()......"); }}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ public void draw(){ System.out.println("实现draw()......"); } public void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); test.calArea(); }}一键获取完整项目代码java其运行结果如下: 最后第九是如果接口中的方法被static修饰,那么也可以有具体的实现,我们修改为如下的代码:package demo2; import demo.Shape; public interface IShape { void draw(); void getArea( double a); double a = 3.0; static void calArea(){ System.out.println("calArea()......"); }}一键获取完整项目代码javapackage demo2; public class Test implements IShape{ public void draw(){ System.out.println("实现draw()......"); } public void getArea(double a){ System.out.println("实现getArea(double a)......"); } public static void main(String[] args) { Test test = new Test(); IShape.calArea(); }}一键获取完整项目代码java其运行结果如下: 刚刚我们在介绍接口特性的时候,相信读者已经发现,接口是不能直接使用的,我们必须有一个实现类来实现该接口,实现接口中的所有抽象方法。我们在上面是通过implements来实现的,其语法格式如下:public class 类名称 implements 接口名称{// ...}一键获取完整项目代码java需要注意的是,子类和父类之间是extends继承关系,类与接口之间是implements实现关系。我们可以看看这个实现笔记本电脑使用USB鼠标、USB键盘的例子,USB接口包含打开设备、关闭设备功能;笔记本类包含开机功能、关机功能、使用USB设备功能;鼠标类实现USB接口,并具备点击功能;键盘类实现USB接口,并具备输入功能。// USB接口public interface USB {void openDevice();void closeDevice();}// 鼠标类,实现USB接口public class Mouse implements USB {@Overridepublic void openDevice() {System.out.println("打开鼠标");}@Overridepublic void closeDevice() {System.out.println("关闭鼠标");}public void click(){System.out.println("鼠标点击");}}// 键盘类,实现USB接口public class KeyBoard implements USB {@Overridepublic void openDevice() {System.out.println("打开键盘");}@Overridepublic void closeDevice() {System.out.println("关闭键盘");}public void inPut(){System.out.println("键盘输入");}}// 笔记本类:使用USB设备public class Computer {public void powerOn(){System.out.println("打开笔记本电脑");}public void powerOff(){System.out.println("关闭笔记本电脑");}public void useDevice(USB usb){usb.openDevice();if(usb instanceof Mouse){Mouse mouse = (Mouse)usb;mouse.click();}else if(usb instanceof KeyBoard){KeyBoard keyBoard = (KeyBoard)usb;keyBoard.inPut();}usb.closeDevice();}}// 测试类:public class TestUSB {public static void main(String[] args) {Computer computer = new Computer();computer.powerOn();// 使用鼠标设备computer.useDevice(new Mouse());// 使用键盘设备computer.useDevice(new KeyBoard());computer.powerOff();}}一键获取完整项目代码java在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。下面通过类来表示一组动物:package demo3; public abstract class Animal { public String name; public int age; public Animal(String name, int age){ this.name = name; this.age = age; } public abstract void eat();}一键获取完整项目代码java在实现动物的时候,我们额外为它们创建对应的动作,跑、飞和游泳,但是不是说所有的动物都能实现这三种动作,所以说我们没法创建多个类来继承实现,但是我们可以创建多个接口来实现这三个动作。也就是如下的代码:package demo3; public interface IRunning { void run();}一键获取完整项目代码javapackage demo3; public interface IFlying { void fly();}一键获取完整项目代码javapackage demo3; public interface ISwimming { void swim();}一键获取完整项目代码java此时我们就可以创建一个鱼类,因为鱼只会游泳,所以他只能用ISwimming这个接口,也就是如下的代码:package demo3; public class Fish extends Animal implements ISwimming{ public Fish(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Fish.eat()......"); } public void swim(){ System.out.println("Fish.swim()......"); }}一键获取完整项目代码java需要注意的是,我们必须先继承父类再调用接口,这个顺序是不可以改变的。我们再来实现一个狗类,因为狗是既会跑,又会游泳的,所以有下面的代码:package demo3; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); }}一键获取完整项目代码java所以我们接口的出现成功解决了Java无法多继承的问题。最后我们来创建一个鸭类,代码如下:package demo3; public class Duck extends Animal implements IRunning, ISwimming, IFlying{ public Duck(String name, int age){ super(name,age); } public void eat(){ System.out.println("Duck.eat()......"); } @Override public void fly() { System.out.println("Duck.fly()......"); } @Override public void run() { System.out.println("Duck.run()......"); } @Override public void swim() { System.out.println("Duck.swim()......"); }}一键获取完整项目代码java我们再来调用一下这些方法:package demo3; public class Test { public static void main(String[] args) { func1(new Duck("ducky", 3)); func1(new Dog("woofy", 2)); func1(new Fish("poofy", 1)); run(new Duck("ducky", 3)); swim(new Duck("ducky", 3)); fly(new Duck("ducky", 3)); run(new Dog("woofy", 2)); swim(new Dog("woofy", 2)); swim(new Fish("poofy", 1)); } public static void func1(Animal animal){ animal.eat(); } public static void run(IRunning irun){ irun.run(); } public static void fly(IFlying ifly){ ifly.fly(); } public static void swim(ISwimming iswim){ iswim.swim(); }}一键获取完整项目代码java其运行结果如下: 在完成上面的知识掌握后,我们可以来实现一下接口的继承。在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即用接口可以达到多继承的目的。接口可以使用extends关键字继承一个接口,达到复用的效果。interface IRunning {void run();}interface ISwimming {void swim();}// 两栖的动物, 既能跑, 也能游interface IAmphibious extends IRunning, ISwimming {}class Frog implements IAmphibious {...}一键获取完整项目代码java通过接口继承创建一个新的接口IAmphibious表示两栖的,此时实现接口创建的Frog类,就继续要实现run方法,也需要实现swim方法。接口间的继承相当于把多个接口合并在一起。三、Object类Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。比如说如下的代码:class Person{}class Student{}public class Test {public static void main(String[] args) {function(new Person());function(new Student());}public static void function(Object obj) {System.out.println(obj);}}一键获取完整项目代码java在运行过后,它就会打印出如下的结果:Person@1b6d3586Student@4554617c一键获取完整项目代码java所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下这些: 对于整个Object类中的方法需要实现全部掌握。本文中我们主要来熟悉这几个方法,toString()方法,equals()方法,hashcode()方法。如果要打印对象中的内容,可以直接重写Object类中的toString()方法,之前已经讲过了,此处不再赘述。Object类中的toString()方法实现如下:public String toString() {return getClass().getName() + "@" + Integer.toHexString(hashCode());}一键获取完整项目代码java在Java中,==进行比较时:如果==左右两侧是基本类型变量,比较的是变量中值是否相同;如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同;如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的。在Object中的equals()方法如下:public boolean equals(Object obj) {return (this == obj); // 使用引用中的地址直接来进行比较}一键获取完整项目代码java我们借用一下之前实现的类,然后对代码作出如下的改动:package demo3; public class Test { public static void main(String[] args) { Dog dog1 = new Dog("woofy", 10); Dog dog2 = new Dog("woofy", 10); System.out.println(dog1 == dog2); System.out.println(dog1.equals(dog2)); }}一键获取完整项目代码java其运行结果如下: 我们此时可以通过重写这个equals()方法来实现我们想要的功能:package demo3; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); } public boolean equals(Object obj){ Dog dog = (Dog)obj; return this.name.equals(dog.name) && this.age == dog.age; }}一键获取完整项目代码java这时候再去运行这个代码,我们就能得到如下的结果: 所以如果说我们要比较对象中的内容是否相同,我们就一定要重写equals方法。然后我们再来看一下它的hashCode()方法。它在Object类中的定义如下:@IntrinsicCandidate public native int hashCode();一键获取完整项目代码java在它的属性中,我们看到了一个native关键字,这就说明它本身是由C或C++代码实现的,我们无法看到它的详细实现过程。我们首先键入如下的代码:package demo3; public class Test { public static void main(String[] args) { Dog dog1 = new Dog("woofy", 10); Dog dog2 = new Dog("woofy", 10); System.out.println(dog1.hashCode()); System.out.println(dog2.hashCode()); } public static void func1(Animal animal){ animal.eat(); } public static void run(IRunning irun){ irun.run(); } public static void fly(IFlying ifly){ ifly.fly(); } public static void swim(ISwimming iswim) { iswim.swim(); }}一键获取完整项目代码java其运行结果如下: 我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,所以二者的哈希值应该相同。我们可以和前面的方法一样,对这个方法进行重写,即如下的代码:package demo3; import java.util.Objects; public class Dog extends Animal implements IRunning, ISwimming{ public Dog(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Dog.eat()......"); } public void bark() { System.out.println("Dog.bark()......"); } @Override public void run() { System.out.println("Dog.run()......"); } @Override public void swim() { System.out.println("Dog.swim()......"); } public boolean equals(Object obj){ Dog dog = (Dog)obj; return this.name.equals(dog.name) && this.age == dog.age; } @Override public int hashCode() { return Objects.hash(name, age); }}一键获取完整项目代码java这时我们再次运行我们的代码,会有如下的结果: 所以说,我们可以知道hashcode方法用来确定对象在内存中存储的位置是否相同。这个方法会帮我们算了一个具体的对象位置,这里面涉及数据结构,但是我们还没讲解Java实现数据结构,没法讲述,所以我们只能说它是个内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。事实上hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置。四、内部类在外部类中,内部类定义位置与外部类成员所处的位置相同,因此称为成员内部类。首先我们先来看看静态内部类,也就是被static修饰的成员内部类。我们键入如下的代码就可以完成对其的定义:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test {}一键获取完整项目代码java那么我们该如何实现对其的实例化呢?我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass.InnerClass innerClass = new OutClass.InnerClass(); innerClass.test(); }}一键获取完整项目代码java其运行结果如下: 但是需要注意,我们在静态类内部是无法直接访问外部类的非静态成员的,如果一定要访问的话,我们就必须先先完成它的实例化。比如说如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; static class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ OutClass outClass = new OutClass(); System.out.println(outClass.data1); System.out.println(outClass.data2); System.out.println(outClass.data3); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass.InnerClass innerClass = new OutClass.InnerClass(); innerClass.test(); }}一键获取完整项目代码java其运行结果如下: 我们额外还需要注意创建静态内部类对象时,不需要先创建外部类对象。然后是实例内部类对象,我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { }}一键获取完整项目代码java那么我们该如何实例化一个实例内部类对象呢?我们键入如下的方法:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java其运行结果如下: 而如果说我们再添加打印数据的代码时,能否正常的进行运行呢?我们键入如下的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println(data1); System.out.println(data2); System.out.println(data3); System.out.println(data4); System.out.println(data5); System.out.println(data6); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java其运行的结果如下: 如果说我们此时内部类和外部类中有着同名的成员变量的话,我们该如何去区分和访问呢?这时可以看下我们下面的代码:package demo4; class OutClass{ public int data1 = 1; private int data2 = 2; public static int data3 = 3; class InnerClass{ public int data1 = 1000; public int data4 = 4; private int data5 = 5; public static int data6 = 6; public void test(){ System.out.println(data1); System.out.println(this.data1); System.out.println(OutClass.this.data1); System.out.println(data2); System.out.println(data3); System.out.println(data4); System.out.println(data5); System.out.println(data6); System.out.println("test方法执行了"); } }} public class Test { public static void main(String[] args) { OutClass outClass = new OutClass(); OutClass.InnerClass innerclass = outClass.new InnerClass(); innerclass.test(); }}一键获取完整项目代码java此时其运行结果如下: 所以我们可以作出如下的总结,外部类中的任何成员都可以在实例内部类方法中直接访问;实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束;在实例内部类方法中访问同名的成员时,优先访问自己的,如果要访问外部类同名的成员,必须通过外部类名称.this.同名成员的语法格式来访问;实例内部类对象必须在先有外部类对象前提下才能创建;实例内部类的非静态方法中包含了一个指向外部类对象的引用;外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象。然后我们再来看看局部内部类,它是定义在外部类的方法体或者{}中,该种内部类只能在其定义的位置使用,一般使用的非常少,此处简单了解下语法格式。我们键入如下的代码:package demo4; public class Test { public void testMethod(){ class Inner{ public int data1; public void func(){ System.out.println("func"); } } Inner inner = new Inner(); inner.func(); } public static void main(String[] args) { Test test = new Test(); test.testMethod(); }}一键获取完整项目代码java其运行结果如下: 需要注意的是,局部内部类只能在所定义的方法体内部使用,不能被public、static等修饰符修饰;编译器也有自己独立的字节码文件,命名格式为外部类名字$数字内部类名字.class,这种类几乎不会使用。最后是我们的匿名内部类,我们直接键入如下的代码:package demo4; class out{ public void test(){ System.out.println("test1......"); }} public class Test { public static void main(String[] args) { new out(){ public void test(){ System.out.println("test2......"); } }.test(); }}一键获取完整项目代码java其运行结果如下: 当然这种方式也可以在接口中实现。我们键入如下的代码:package demo4; interface IA{ void test();} public class Test { public static void main(String[] args) { new IA(){ public void test(){ System.out.println("test1......"); } }.test(); }}一键获取完整项目代码java其运行结果如下: 五、接口使用实例首先我们要介绍的接口就是Compareable接口,我们现在看看下面一段代码:package demo5; class Student{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1 < student2); }}一键获取完整项目代码java这段代码是无法正常执行的,因为我们最下面这行代码会产生报错,student1和student2这两个对象是无法通过简单的逻辑运算符去比较的。我们目前遇到的问题总结来说就是当前的自定义类,到底要按照什么样的规则去进行比较,并且这个比较的规则该如何去定义?我们首先先在这个自定类型后面添加一个implements Comparable,然后我们转到这个Compareable的声明部分:public interface Comparable<T> { public int compareTo(T o);}一键获取完整项目代码java我们按照它的形式进行修改,也就是在它的后面再添加一个<Student>即可,这个<>中的部分就是我们想要去比较的类型,也就是说我们想去比较哪个类就去写哪个类,它叫做泛型,我们之后会去详细讲解。我们可以将代码改为如下:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.age - o.age; }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1.compareTo(student2)); }}一键获取完整项目代码java再次运行,得到结果如下: 但是这种比较有一种相对大的缺陷,就是它一般用于固定的比较,不适合非常灵活的比较。比如说我们现在不想根据年龄比较了,而是根据姓名去比较的话,就会发生一些变化。我们可以去看一下String类中是否也有compareTo的声明,在查询后,我们可以看到的确是有的: public int compareTo(String anotherString) { byte v1[] = value; byte v2[] = anotherString.value; byte coder = coder(); if (coder == anotherString.coder()) { return coder == LATIN1 ? StringLatin1.compareTo(v1, v2) : StringUTF16.compareTo(v1, v2); } return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2) : StringUTF16.compareToLatin1(v1, v2); }一键获取完整项目代码java那我们直接更改一下我们刚才的方法中的return语句,代码如下:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student student1 = new Student("zhangsan", 18); Student student2 = new Student("lisi", 19); System.out.println(student1.compareTo(student2)); }}一键获取完整项目代码java我们此时再度运行代码,发现其结果变为如下: 这就说明如果按照姓名去比的话,我们的student1是要比student2要大的。所以就说明了我们刚刚提到的缺陷。通常来说,我们会在默认的比较上使用这种方式。那我们如果想要解决这个问题的话,就需要换一个接口来解决。我们现在来改变一下原来的问题,如果说我们现在不想只比较两个学生,而是多个学生的话,我们该如何操作?我们很容易的想到就是通过构建一个Student类型的数组,也就是如下的代码:package demo5; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); }}一键获取完整项目代码java然后我们又会想到在数组中我们有sort方法可以进行比较,那么能不能就直接通过sort方法实现它们的排序呢?我们键入如下的代码尝试实现:package demo5; import java.util.Arrays; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); Arrays.sort(students); System.out.println(Arrays.toString(students)); }}一键获取完整项目代码java其运行结果如下:可以看到它这个是成功为我们实现了排序。但是实际上我们是因为实现了compareTo方法的重写和Comparable方法,才成功的,如果我们将这两部分注释掉,就会出现下面的情况: 我们可以看到这里的报错是ClassCastException,这个叫做类型转换异常。我们可以看看他后面的信息:class demo5.Student cannot be cast to class java.lang.Comparable一键获取完整项目代码java这就是说我们的这个Student类没办法转换为Comparable的形式,我们点击下面的那个ComparableTimSort进去看看:if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0) runHi++; reverseRange(a, lo, runHi); } else { // Ascending while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0) runHi++; }一键获取完整项目代码java可以看到它这个是要把我们的Student强转成Comparable,然后去调用compareTo的,但是由于我们注释掉了代码,就没办法实现了。但在调用之前,我们必须先重写我们的compareTo,要不然还是会报错。当然我们也可以自己写一个排序的代码: package demo5; import java.util.Arrays; class Student implements Comparable <Student>{ public String name; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { return this.name.compareTo(o.name); }} public class Test { public static void main(String[] args) { Student[] students = new Student[3]; students[0] = new Student("zhangsan", 18); students[1] = new Student("lisi", 19); students[2] = new Student("wangwu", 17); //Arrays.sort(students); mySort(students); System.out.println(Arrays.toString(students)); } public static void mySort(Comparable[] array) { for (int bound = 0; bound < array.length; bound++) { for (int cur = array.length - 1; cur > bound; cur--) { if (array[cur - 1].compareTo(array[cur]) > 0) { Comparable tmp = array[cur - 1]; array[cur - 1] = array[cur]; array[cur] = tmp; } } } }}一键获取完整项目代码java其运行结果如下: Java中还内置了一个十分有用的接口,Object类中存在一个clone方法,调用这个方法可以创建一个对象的拷贝。但是要想合法调用clone方法, 必须要先实现Clonable接口, 否则就会抛出 CloneNotSupportedException异常,我们进入查看这个Cloneable接口:public interface Cloneable {}一键获取完整项目代码java发现这是一个空接口,它也叫标记接口,代表着当前类是可以被扩容的。但是它还是会报错,这涉及到了我们之后会提到的异常处理机制,我们之后会详细说明,我们只需要将代码改为如下的部分即可:package demo6; public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}一键获取完整项目代码javapackage demo6; public class Test { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("zhangsan", 10); Person person2 = (Person) person1.clone(); }}一键获取完整项目代码java总的来说是走了如下的过程:我们修改代码如下:package demo6; class Money{ public double money = 9.9;} public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Money m = new Money(); @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}一键获取完整项目代码javapackage demo6; public class Test { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("zhangsan", 10); Person person2 = (Person) person1.clone(); person2.m.money = 99.99; System.out.println(person1.m.money); System.out.println(person2.m.money); }}一键获取完整项目代码java其运行结果如下: 可以看到二者的钱都发生了改变,这是为什么?原因就如下图所示: 也就是说我们只克隆了人这个对象,却没有克隆钱这个对象。目前这种拷贝就叫做浅拷贝,预支对应的就是深拷贝,也就是说我们把钱这个对象也实现了克隆。要实现深拷贝,我们就需要在过程中完成对于钱的克隆,那么我们就可以修改代码如下:package demo6; class Money implements Cloneable { public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }} public class Person implements Cloneable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Money m = new Money(); @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Person tmp = (Person) super.clone(); tmp.m = (Money) this.m.clone(); return tmp; }}一键获取完整项目代码java再次执行,其结果如下:六、抽象类和接口的区别抽象类和接口都是 Java 中多态的常见使用方式。都需要重点掌握,同时又要认清两者的区别,其核心区别为抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法,子类必须重写所有的抽象方法。如之前写的Animal例子,此处的Animal中包含一个name这样的属性,这个属性在任何子类中都是存在的。因此此处的Animal只能作为一个抽象类,而不应该成为一个接口。对于二者具体的区分我们可以总结为如下的这个表格:总结本文系统介绍了Java中抽象类和接口的核心概念与应用。抽象类通过abstract修饰,包含抽象方法(无实现)和普通方法/属性,不能实例化,必须被子类继承并实现抽象方法。接口定义公共规范标准,所有方法默认为public abstract,支持多继承,可通过implements实现。文章详细对比了两者特性,并通过图形绘制、USB设备等实例演示实际应用。同时介绍了Object类的重要方法(toString、equals、hashCode)、内部类(静态/实例/局部/匿名)及常用接口(Comparable、Cloneable),最后强调抽象类与接口的核心区别在于抽象类可包含普通实现而接口只能定义规范。————————————————原文链接:https://blog.csdn.net/su17643217590/article/details/155354247
-
一、finally的使用说明1、finally的理解1、我们将一定要被执行的代码声明在finally结构中2、更深刻的理解:无论try中或者catch中是否存在仍未被处理的异常,无论try中或catch中是否存在return语句等,finally中声明的语句都一定要被执行。3、finally语句和catch语句是可选的,但是finally不能单独使用4、try-catch可以嵌套使用 2、什么样的代码我们一定要声明在finally中呢?我们在开发中,有一些资源,比如:(输入流、输出流、数据库连接、socket连接等资源),在使用完以后,必须显式的进行关闭操作,否则,GC不会自动的回收这些资源。进而导致内存泄漏。为了保证这些资源在使用完之后,不管是否出现了未被处理的异常的情况下,这些资源能被关闭。我们必须将这些操作声明在finally中 package trycatchfinally; import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException; /** * package:trycatchfinally * * @Author jimmy-yan * @Create 2024/11/19 11:59 */public class FinallyTest { public static void main(String[] args) { FinallyTest f = new FinallyTest(); f.test1(); } public void test1() { try { String str = "123"; str = "abc"; int i = Integer.parseInt(str); System.out.println(i); } catch (NumberFormatException e) { e.printStackTrace(); System.out.println(10 / 0); //在catch中抛出异常 } finally { System.out.println("程序执行结束"); } } public void test2() { FileInputStream fis = null; try { File file = new File("D:\\hello.txt"); fis = new FileInputStream(file); //可能报FileFonudException int data = fis.read(); //可能报IOException while (data != -1) { System.out.println((char) data); data = fis.read(); //可能报IOException } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); //可能报IOException } } catch (IOException e) { e.printStackTrace(); } } }} 一键获取完整项目代码java 二、异步处理的方式二:throws1、格式在方法的声明处,使用throws异常类型1,异常类型2,… 2、举例public void test() throws 异常类型1,异常类型2,..{//可能存在编译时异常}一键获取完整项目代码java 3、是否真正处理了异常?从编译是否能通过的角度看:看成是给出了异常万一要是出现时候的解决方案。此方案就是,继续向上抛出(throws)。但是,此throws的方式,仅是将可能出现的异常抛给了此方法的调用者,此调用者仍然需要考虑如何处理相关异常。从这个角度来看,throws的方式不算真正意义上处理了异常。 import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException; /** * package:PACKAGE_NAME * * @Author jimmy-yan * @Create 2024/11/19 13:58 */public class ThrowsTest { public static void main(String[] args) { test3(); } public static void test3() { try{ test2(); }catch (FileNotFoundException e){ e.printStackTrace(); }catch (IOException e){ e.printStackTrace(); } } public static void test2() throws FileNotFoundException, IOException { File file = new File("D:\\hello.txt"); FileInputStream fis = new FileInputStream(file); //可能报FileFonudException int data = fis.read(); //可能报IOException while (data != -1) { System.out.println((char) data); data = fis.read(); //可能报IOException } fis.close(); //可能报IOException }} 一键获取完整项目代码java 4、方法重写的要求(只针对编译型异常)子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或者是父类被重写的方法抛出异常类型的子类。 5、开发中,如何选择异常处理的两种方式?1、如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。2、如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。3、开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。 三、使用throw手动抛出异常对象1、为什么需要手动抛出异常在实际开发中,如果出现不满足具体场景的代码问题,我们就有必要手动抛出一个指定类型的异常对象。 2、如何理解 自动vs手动 抛出异常过程1:“抛”“自动抛”:程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,自动生成对应异常类的对象,并将此对象抛出。“手动抛”:程序在执行的过程当中,不满足指定条件的情况下,我们主动的使用"throw + 异常类的对象"方式抛出异常对象。 过程2:“抓”狭义上讲:try-catch的方式捕获异常,并处理。广义上讲:把“抓"理解为"处理"。则此时对应着异常处理的两种方式: try-catch-finathrows3、如何实现手动抛出异常在方法内部,满足指定条件的情况下,使用“throw 异常类的对象”的方式抛出。 /** * package:PACKAGE_NAME * * @Author jimmy-yan * @Create 2024/11/19 14:51 */public class Throw { public static void main(String[] args) { Throw t = new Throw(); t.regist(-9); System.out.println(t); } int id; public void regist(int id) { if (id > 0) { this.id = id; } else {// System.out.println("输入的非法id"); //todo 手动抛出异常 throw new RuntimeException("输入的id非法"); } } @Override public String toString() { return "Throw{" + "id=" + id + '}'; }} 一键获取完整项目代码java 注意一点:throws后的代码不能被执行,编译不通过————————————————原文链接:https://blog.csdn.net/YZL40514131/article/details/143879989
-
一、体会线程安全问题当我们编写一个多线程程序,要求两个线程对同一个变量(共享变量)进行修改,得到的结果是否与预期一致?创建两个线程,分别对共享变量(count)进行自增5万次操作,最后输出的结果理论上应为10万,但是实际上输出的结果是一个小于10万且不确定的数。读者可以自行实现一下该多线程程序,运行后看看结果是否符合预期。public class Demo14_threadSafety { private static int count = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 理论上输出的结果应是100000,实际输出的结果是0 // 原因是主线程 main 运行太快了,当 t1 和 t2 线程还在计算时,主线程已经打印结果、运行完毕了 System.out.println(count); } // 让主线程等待 t1 和 t2 线程,等到它们两个都执行完成再打印,故使用 join 方法 public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 在主线程中,通过 t1 和 t2 对象调用 join 方法 // 表示让主线程 main 等待 t1 线程和 t2 线程 t1.join(); t2.join(); // 当两个线程都执行完毕后主线程再继续执行打印操作 System.out.println(count); // 实际输出的结果小于100000,仍不符合预期 }}一键获取完整项目代码java二、线程安全的概念通过上面的一个例子,想必读者已经体会到线程安全问题了吧?那究竟什么是线程安全问题呢?其原因是什么?如何解决线程安全问题呢?不要急,且听小编慢慢道来~如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致,就说这个程序是线程安全的,否则是线程不安全的。上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作,那么结果是符合预期的;但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的,也可以说该程序存在线程安全问题。三、线程安全问题的原因究竟是哪里出问题导致程序出现线程安全问题呢?究其根本,罪魁祸首是 操作系统的线程调度有随机性/抢占式执行 。由于操作系统的线程调度是有随机性的,这就会存在这种情况:某一个线程还没执行完呢,就调度到其他线程去执行了,从而导致数据不正确。当然了,一个巴掌拍不响,还有以下三个导致线程不安全的原因:原子性:指 Java 语句,一条 Java 语句可能对应不止一条指令,若对应一条指令,就是原子的。可见性:一个线程对主内存(共享变量)的修改,可以及时被其他线程看到。有序性:一个线程观察其他线程中指令的执行顺序,由于 JVM 对指令进行了重排序,观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关,暂不讨论。之前的例子就是由于原子性没有得到保障而出现线程安全问题:public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count);}一键获取完整项目代码java1. “count ++” 这条语句对应多条指令:读取数据、计算结果、存储数据。2. t1 线程和 t2 线程分别执行一次“count++”语句,期望的结果是“count = 2”,其过程如下: 初始情况: 当线程执行“count ++”时,总共分三个步骤:load、update、save,由于线程调度的随机性/抢占式执行,可能会出现以下情况(可能出现的情况有很多种,这里只是其中一种): 这时候 t1 正在执行“count ++”这条语句,执行了“load 和 update”指令后,t1 的工作内存(寄存器)存着更新后的值,但是还未被写回内存中: 接着调度到 t2 线程并开始执行“count ++”语句,并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存,因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令: 当 t2 执行完成,内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令,但是 t1 线程寄存器中 count 的值也是 1 ,此时写回内存更新后 count 的值依然是 1 。 结果 count = 1,与预期的 count = 2 不符,因此存在线程安全问题,其原因是操作系统的随机线程调度和 count 语句存在非原子性。四、解决线程安全问题的方法从上面的例子我们知道,当一条语句的指令被拆开来执行的话是存在线程安全问题的,但是,当我们将“count ++”这条语句的三个指令都放在一起执行怎么样? 当线程调度的情况如下: 此时 t1 线程开始执行“count ++”语句的 load、update 和 save 指令。内存中的 count 为 0,t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1: 接着调度至 t2 线程,开始执行“count ++”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1,此时 t2 读取 count 并更新为 2,然后写回内存中。当 t2 执行完成,内存中的 count 就更新成 2 了: 可以发现,结果与预期相符!说明这个方法可行。可以将操作顺序改成先让 t1 线程完成“count ++”操作,再让 t2 线程完成该操作——即串行执行。现在我们对之前的例子进行优化:// 可以试着让 t1 线程先执行完后,再让 t2 线程执行,改成串行执行public static void main3(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count);}一键获取完整项目代码java刚刚是让一个线程一次性执行“count ++”这条语句的三个指令,也就是说,我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令(即执行过程中不可被打断——调度走)。这样就有效的解决了线程安全问题。而上述的操作,其实就是 Java 中的 加锁操作 。当一个线程执行一个非原子的语句时,通过加锁操作可以防止在执行过程中被调度走或被其他线程打断,若其他线程想要执行该语句,则要进入 阻塞等待 的状态,当线程执行完毕并将锁释放,操作系统这时唤醒等待中的线程,才可以执行该语句。就相当于上厕所:当厕所内没有人时(没有线程加锁),就可以使用;当厕所内有人时(已经有线程加锁了),那么就必须等里面的人出来后才能使用。注意:前一个线程解锁之后,并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。若 t1、t2 和 t3 三个线程竞争同一个锁,当 t1 线程获取到锁,t2 线程再尝试获取锁,接着 t3 线程尝试获取锁,此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后,t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁,而是和 t3 进行公平竞争。(不遵循先来后到原则)4.1 synchronized 关键字在处理由原子性导致的线程安全问题时,通常采用加锁操作。加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的,很多编程语言对其进行了封装,Java 中使用 synchronized 关键字来进行加锁 / 解锁操作,其底层是使用操作系统的 mutex lock 来实现的。Java 中的任何一个对象都可以用作“锁”。synchronized (锁对象){ ——> 进入代码块,相当于加锁操作 // 一些需要保护的逻辑} ——> 出了代码块,相当于解锁操作当多个线程针对同一个锁对象竞争的时候,加锁操作才有意义。对之前的例子进行加锁操作:public class Demo15_synchronized { private static int count = 0; public static void main1(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }}一键获取完整项目代码javasynchronized 关键字用来修饰 普通方法 时,相当于给 this 加锁;synchronized 关键字用来修饰 静态方法 时,相当于给 类对象 加锁。于是可以使用另一种写法:// 写法二:// 将 count++ 所包含的三个操作封装成一个 add 方法// 使用 synchronized 修饰 add 方法 class Counter { private int count = 0; synchronized public void add () { // synchronized 修饰普通方法相当于给 this 加锁 count++; } // 相当于: // public void add () { // synchronized (this) { // count++; // } // } public int get () { return count; }} public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.get());}一键获取完整项目代码java这样一来,就成功解决了多线程程序中由原子性导致的线程安全问题。4.2 volatile 关键字我们再来一个例子:让 t1 线程读取共享变量的值,然后让 t2 线程修改共享变量的值,我们可以写出如下程序:public class Demo17_volatile { private static int flag = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start(); // 运行后发现即使输入1,t1 线程并不会结束 }}一键获取完整项目代码java我们期望该程序运行后,输入非零的数如 1,t1 线程能够结束,实际并非如此,很显然,这也是出现了线程安全问题。这次出现问题的原因并非原子性,而是 可见性 。我们说过,可见性是一个线程对主内存(共享变量)的修改能够被其他线程及时看到,若不能被其他线程及时看到,就会出现数据错误,从而导致线程安全问题。如果我们使用锁来处理的话,好像并不能解决。我们先来认识一个东西:编译器优化。由于不能保证写代码的人每一次写代码都不会出错,所以开发 JDK 的先人们就让编译器 / JVM 能够根据代码的原有逻辑进行优化。在我们这个程序中:while {...load...cmp...} 先将数据从内存中 load 到寄存器中,然后在寄存器进行 “条件比较” 指令 cmp 。在短时间内用户还没来得及输入呢,但是这个循环语句能够执行成万上亿次且内存中的 flag 的值一直都是 0。那么这时候编译器 / JVM 就察觉到:既然一直读取 flag 都是同一个值,那干脆我直接读取寄存器算了(读取寄存器开销更小)。这样一来,当用户输入 1 的时候(存入内存),t1 线程就无法读取通过 t2 线程更新之后的值了,也就不会结束。在上面的程序中,t2 线程修改后的值无法被 t1 线程看到,这就是可见性导致的线程安全问题,由于编译器我们没办法更改,所以只好另寻他法。如果我们能让循环执行的慢一点,是不是就能解决问题了?尝试优化该程序:public static void main2(String[] args) { Thread t1 = new Thread(() -> { while (flag1 == 0) { // 当 flag1 为 0 时一直循环 try { Thread.sleep(1); // 放慢读取速度 } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag1 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag1 的值:"); flag1 = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java在运行优化版的程序后,发现问题被解决了,其原因是:使用了 sleep(1)—— 表示每一次读取的时候休眠 1毫秒,这对于读取操作的速度来说已经很慢了。因此不会触发编译器优化,也就不会出现可见性导致的线程安全问题了。但是,在实际开发环境中频繁使用 sleep 的话会导致程序效率下降,这样的话用户体验就会变差。Java 中使用 volatile 关键字来处理 可见性 导致的线程安全问题。使用 volatile 关键字来修饰共享变量,这样一来这个变量无论如何都不会被编译器优化。具体流程是:当 t1 线程读取被修饰的变量时,会强制读取主内存中变量最新的值;当 t2 线程修改被修饰的变量时,在工作内存(寄存器)中更新变量的值之后,立即将改变的值刷新至主内存。加上 volatile 关键字强制读取内存,虽然速度慢了,但是保证数据不出错。使用 volatile 关键字优化后的程序:// 使用 volatile 关键字来修饰被读取的变量,此时无论读取速度怎样,JVM 都不会对该变量进行优化private volatile static int flag = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java注意:volatile 关键字只能保证可见性,不能保证原子性,因此不能完全解决线程安全问题。当多线程中存在 ++、--、+=、-=、*= 和 /= 或者类似的操作符时就不能够使用 volatile 关键字来处理,需要使用 synchronized 关键字。五、死锁那么话又说回来,如果对锁使用不当,就很容易出现死锁。如果一个线程已经获取了锁,再次获取这个锁,能成功吗?会出现死锁吗?Object locker = new Object(); synchronized (locker) { synchronized (locker) { // 一些需要保护的逻辑 }}一键获取完整项目代码java按照前面的逻辑,第一次获取锁对象后,该锁对象已被占有,下一次获取该锁对象时应该会触发阻塞等待。而要想解除阻塞等待就得往下继续执行,但是要想往下执行就得将锁解开。这样不就构成死锁了嘛?不妨试试在你的 IDE 上运行一下~5.1 synchronized 的可重入性如果你在 IDE 上运行了刚刚的程序,可以发现程序是没问题的。按照我们这之前的逻辑,当锁对象第一次被获取到,其他的线程再想获取锁对象时就只能阻塞等待。但是,第二次获取锁对象的是同一个线程呀~ 因此并不会触发阻塞等待。Java 中的 synchronized 关键字是具有可重入性的,即对于同一个线程可以重复获取同一个锁对象。可重入锁内部包含了“线程持有者”和“计数器”两个信息,若线程加锁时发现锁已被占有且恰好是自己,此时仍可以获取到锁,让计数器自增即可;当计数器为 0 时,将锁释放,其他线程可以才获取到锁。这样就可以避免死锁的出现——开发 JDK 的前人们真的为我们考虑了太多😭5.2 死锁的概念当有两个线程为了保护两个不同的共享资源而使用两个不同的锁且这两个锁使用不当时,就会造成两个线程都在等待对方解锁,在没有外界干扰的情况下他们会一直相互等待,此时就是发生了死锁。死锁需要同时具备以下四个条件:互斥条件:要求同一个资源不能被多个线程同时占有。不可剥夺条件:当资源已被某个线程占有,其他线程只能等到该线程使用完并释放后才能获取,不可以强行打断并获取。持有并等待条件:有三个线程两个资源,当线程 1 已经占有资源 A(线程 2 尝试获取资源 A 但触发阻塞等待)且尝试获取资源 B 时(此时资源 B 已被线程 3 占有),线程 1 就会触发阻塞等待且不释放手中持有的资源 A 。循环等待/环路等待条件:有两个线程两个资源,线程 1 已占有资源 A 并想要获取资源 B ,但是资源 B 已被线程 2 占有,并且线程 2 在占有资源 B 的同时想要获取资源 A 。比如,有两个线程 t1 和 t2 以及两个锁 locker1 和 locker2,t1 先获取 locker1 并尝试获取 locker2;t2 先获取 locker2 并尝试获取 locker1:// 两个线程两把锁,每个线程获取到一把锁之后尝试获取对方的锁public static void main2(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 确保 t2 线程拿到 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); // 确保 t1 线程拿到 locker1 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java运行该程序会发现什么也没输出,程序却还在运行。这就是发生了死锁。 我们借助第三方工具来观察线程的状态: 5.3 如何避免死锁要想避免死锁,我们就要从死锁的四个条件入手,其中,互斥条件和不可剥夺条件基本上是无法打破的,因为这两个是 synchronized 锁的基本特性,因此我们选择从后两个条件入手。1. 持有并等待条件:通常是由于代码中的嵌套加锁导致的,因此选择将嵌套的加锁代码改成串行的加锁代码(先将已占有的资源释放掉,然后去获取另一个资源):// 避免死锁的写法:打破持有并等待条件——将嵌套加锁改成串行加锁public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t1 线程释放已占有的锁对象 locker1 // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t2 线程释放已占有的锁对象 locker2 // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java2. 循环等待/环路等待条件:这种情况一般都是双方都持有对方想要的锁但是都不肯释放,所以我们就调整一下顺序:让 t1 和 t2 获取锁的顺序都是 locker1 和 locker2,当 t1 已占有 locker1 时 t2 想获取只能阻塞等待,但这时 t1 可以获取 locker2,等待 t1 两把锁都获取到并释放之后,t2 被唤醒并且获取两把锁。// 防止死锁的写法:打破循环等待条件——调整加锁顺序public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 此时 t2 因 locker1 被 t1 获取而处于阻塞状态 BLOCKED // t1 休眠结束后,获取 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } } }); Thread t2 = new Thread(() -> { synchronized (locker1) { // t2 线程获取到锁对象 locker1 // 因 t1 先获取到 locker1 ,t2 此时处于阻塞状态 BLOCKED // 当 t1 结束执行后,t2 恢复执行并获取 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { // 休眠结束后继续获取 locker2 throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java今天暂且到这吧~————————————————原文链接:https://blog.csdn.net/REGARD712/article/details/155878355
-
Bean 生命周期的详细步骤第一阶段:Bean 的元数据配置与容器启动配置元数据:首先,你需要通过 XML、Java 注解(如 @Component, @Service, @Autowired)或 Java 配置类(@Configuration, @Bean)来定义 Bean。容器启动:Spring 容器(如 ApplicationContext)启动,加载并解析这些配置元数据,生成每个 Bean 的 BeanDefinition 对象,它包含了创建一个 Bean 所需的所有信息(如类名、作用域、是否懒加载等)。第二阶段:Bean 的实例化与初始化(核心生命周期)1. 实例化(Instantiation)描述:容器首先会调用 Bean 的构造函数(或工厂方法)来创建一个新的实例。此时只是一个简单的 Java 对象,还没有进行依赖注入,我们可以把它比作“新生儿”。扩展点:无直接扩展点。2. 依赖注入(Populate Properties)描述:Spring 根据配置(如 @Autowired, @Value)将所需的依赖注入到 Bean 的对应属性中。这一步填充了 Bean 的“血肉”。扩展点:无直接扩展点。3. Aware 接口回调(Aware Interface Injection)描述:如果 Bean 实现了各种 Aware 接口,Spring 会在此阶段回调相应的方法,将一些容器相关的对象(如 BeanNameAware, BeanFactoryAware, ApplicationContextAware)注入到 Bean 中。扩展点:BeanNameAware:设置 Bean 的 ID/Name。BeanFactoryAware:设置当前的 BeanFactory。ApplicationContextAware:设置当前的 ApplicationContext(功能最全)。EnvironmentAware:设置 Environment 对象(用于获取配置文件属性)等。4. BeanPostProcessor 前置处理描述:这是极其重要的扩展点。所有实现了 BeanPostProcessor 接口的 Bean,它们的 postProcessBeforeInitialization 方法会在这个阶段被调用。它可以对 Bean 进行包装或增强,返回一个可能是代理对象的 Bean。扩展点:BeanPostProcessor.postProcessBeforeInitialization(Object bean, String beanName)5. 初始化(Initialization)描述:Bean 的“成人礼”。在这个阶段,Bean 的初始化逻辑会被执行。a. InitializingBean 接口:如果 Bean 实现了 InitializingBean 接口,会调用其 afterPropertiesSet() 方法。b. 自定义初始化方法:调用通过 @Bean(initMethod = "...") 或 XML 中 init-method 属性指定的自定义初始化方法。扩展点:InitializingBean.afterPropertiesSet()自定义 init-method6. BeanPostProcessor 后置处理描述:这是另一个极其重要的扩展点。所有 BeanPostProcessor 的 postProcessAfterInitialization 方法会被调用。Spring AOP 就是基于此实现的。如果一个 Bean 需要被代理,通常在这里返回一个代理对象来包装目标 Bean。扩展点:BeanPostProcessor.postProcessAfterInitialization(Object bean, String beanName)7. Bean 就绪(Ready)描述:经过以上所有步骤,Bean 已经完全创建、初始化并可能被代理。它被存放在 Spring 容器(单例池)中,可以被应用程序正常获取和使用了。第三阶段:Bean 的使用与销毁8. 使用期描述:在应用程序运行期间,Bean 被其他组件依赖和调用,执行业务逻辑。9. 销毁(Destruction)描述:当 Spring 容器(通常是 ApplicationContext)被关闭时,它会开始销毁容器中的所有单例 Bean。a. DisposableBean 接口:如果 Bean 实现了 DisposableBean 接口,会调用其 destroy() 方法。b. 自定义销毁方法:调用通过 @Bean(destroyMethod = "...") 或 XML 中 destroy-method 属性指定的自定义销毁方法。常用于释放资源,如关闭数据库连接、文件句柄等。扩展点:DisposableBean.destroy()自定义 destroy-method特殊情况的处理作用域(Scope):上述生命周期主要针对单例(Singleton) Bean。对于原型(Prototype) 作用域的 Bean,Spring 容器只负责到第 6 步(初始化完成),之后就将 Bean 交给客户端,不再管理其生命周期,因此不会调用销毁方法。延迟初始化(Lazy):标记为 @Lazy 的 Bean,只有在第一次被请求时才会触发上述初始化过程,而不是在容器启动时。总结与记忆技巧可以把这个过程想象成一个人的一生:实例化:出生依赖注入:接受教育,获取生存技能(依赖)Aware 接口:获得身份ID、认识家庭和社会(容器环境)BeanPostProcessor(前):步入社会前的指导初始化:举行成人礼,开始独立承担责任BeanPostProcessor(后):步入社会后的包装/历练(可能被“代理”)就绪:成为社会栋梁,贡献力量销毁:退休,安享晚年,处理身后事————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/154697287
-
什么是阻塞队列?阻塞队列是一种特殊的队列,它在数据结构的基础上附加了两个额外的操作特性:阻塞插入:当队列已满时,尝试向队列中插入元素的线程会被阻塞,直到队列中有空闲位置。阻塞移除:当队列为空时,尝试从队列中获取元素的线程会被阻塞,直到队列中有新的元素被加入。简单来说,阻塞队列是一个线程安全的、支持阻塞等待的生产者-消费者模型的核心容器。阻塞队列的实现原理阻塞队列的实现原理主要依赖于 锁(Lock) 和 条件变量(Condition)。在Java中,这通常通过 ReentrantLock 和 Condition 来实现。我们以一个简单的有界数组阻塞队列为例,剖析其核心原理:核心组件一个队列:通常用数组或链表实现,用于存储元素。一把锁:一个 ReentrantLock,用于保证所有操作的线程安全性。两个条件变量:notEmpty:一个与锁绑定的条件,用于表示“队列非空”。当消费者因队列为空而等待时,会挂在这个条件上。当生产者放入一个新元素后,会唤醒挂在这个条件上的线程。notFull:一个与锁绑定的条件,用于表示“队列未满”。当生产者因队列已满而等待时,会挂在这个条件上。当消费者取走一个元素后,会唤醒挂在这个条件上的线程。核心方法原理put(E e) 方法(阻塞插入)获取锁。while (队列已满):调用 notFull.await() 释放锁并进入等待状态。当被其他线程唤醒并重新获得锁后,再次检查队列是否已满(防止虚假唤醒)。将元素 e 入队。调用 notEmpty.signal() 或 notEmpty.signalAll(),唤醒一个或所有正在 notEmpty 上等待的消费者线程。释放锁。take() 方法(阻塞移除)获取锁。while (队列为空):调用 notEmpty.await() 释放锁并进入等待状态。当被其他线程唤醒并重新获得锁后,再次检查队列是否为空。将队首元素出队。调用 notFull.signal() 或 notFull.signalAll(),唤醒一个或所有正在 notFull 上等待的生产者线程。释放锁。关键点总结:线程安全:所有对队列结构的修改都在锁的保护下进行。高效等待/通知:使用 Condition 的 await() 和 signal() 代替传统的 Object.wait() 和 Object.notify(),可以更精确地控制等待和唤醒的线程类型(生产者或消费者),避免了“惊群效应”。循环检查条件:在从 await() 返回后,必须重新检查条件(队列是否满/空),这是应对“虚假唤醒”的标准做法。如何使用阻塞队列实现生产者-消费者模型生产者-消费者模型是一种经典的多线程协作模式,它通过一个共享的缓冲区(即阻塞队列) 来解耦生产者和消费者,使他们不必直接通信,而是各自以不同的速率对缓冲区进行操作。阻塞队列天生就是为这个模型设计的,使用它来实现非常简单优雅。实现步骤创建阻塞队列:选择一个合适的阻塞队列实现,例如 ArrayBlockingQueue。创建生产者线程:生产者线程循环生产数据,并调用 queue.put(data) 将数据放入队列。如果队列满,put 方法会自动阻塞,直到有空间。创建消费者线程:消费者线程循环调用 queue.take() 从队列中获取数据。如果队列空,take 方法会自动阻塞,直到有数据可用。启动线程:启动生产者和消费者线程,它们会自动协作。代码示例import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue; public class ProducerConsumerExample { public static void main(String[] args) { // 1. 创建一个容量为10的阻塞队列 BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 2. 创建生产者线程 Thread producerThread = new Thread(() -> { try { int value = 0; while (true) { // 生产数据 queue.put(value); System.out.println("Produced: " + value); value++; // 模拟生产耗时 Thread.sleep(1000); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 3. 创建消费者线程 Thread consumerThread = new Thread(() -> { try { while (true) { // 消费数据 Integer value = queue.take(); System.out.println("Consumed: " + value); // 模拟消费耗时 Thread.sleep(2000); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 4. 启动线程 producerThread.start(); consumerThread.start(); }}一键获取完整项目代码代码分析生产者:每秒生产一个数字(0, 1, 2...),并放入队列。如果队列已满(本例中容量为10),生产者会在 put 方法处阻塞,等待消费者消费。消费者:每两秒从队列中取出一个数字。如果队列为空,消费者会在 take 方法处阻塞,等待生产者生产。运行结果:你会看到生产者生产的速度快于消费者,但由于队列的存在,生产者不会丢失数据。当队列满后,生产者会停下来等待。整个系统平稳运行,生产者和消费者速率不匹配的问题被阻塞队列完美解决。Java中的阻塞队列实现Java的 java.util.concurrent 包提供了多种现成的阻塞队列实现,可以直接使用:ArrayBlockingQueue:基于数组的有界阻塞队列。LinkedBlockingQueue:基于链表的阻塞队列,可选有界或无界。PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。它实现了数据的直接传递。DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。总结阻塞队列是一个线程安全的、支持阻塞插入和移除的队列,是生产者-消费者模型的理想载体。实现原理核心是锁+条件变量,通过精确的等待/通知机制来协调生产者和消费者的步调。使用方式极其简单,生产者调用 put,消费者调用 take,无需开发者手动处理线程同步和通信问题,大大简化了并发编程的难度。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153782844
-
一、核心原理1. 数据存储结构// 每个 Thread 对象内部都有一个 ThreadLocalMapThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap 内部使用 Entry 数组,Entry 继承自 WeakReference<ThreadLocal<?>>static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // 弱引用指向 ThreadLocal 实例 value = v; // 强引用指向实际存储的值 }}一键获取完整项目代码2. 关键设计线程隔离:每个线程有自己的 ThreadLocalMap 副本哈希表结构:使用开放地址法解决哈希冲突弱引用键:Entry 的 key(ThreadLocal 实例)是弱引用延迟清理:set / get 时自动清理过期条目二、源码分析1. set() 方法流程public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); // this指当前ThreadLocal实例 } else { createMap(t, value); }} private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 遍历查找合适的位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到相同的key,直接替换value if (k == key) { e.value = value; return; } // key已被回收,替换过期条目 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 清理并判断是否需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}一键获取完整项目代码2. get() 方法流程public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); // 返回初始值}一键获取完整项目代码三、使用场景1. 典型应用场景// 场景1:线程上下文信息传递(如Spring的RequestContextHolder)public class RequestContextHolder { private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>(); public static void setRequest(HttpServletRequest request) { requestHolder.set(request); } public static HttpServletRequest getRequest() { return requestHolder.get(); }} // 场景2:数据库连接管理public class ConnectionManager { private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> DriverManager.getConnection(url)); public static Connection getConnection() { return connectionHolder.get(); }} // 场景3:用户会话信息public class UserContext { private static ThreadLocal<UserInfo> userHolder = new ThreadLocal<>(); public static void setUser(UserInfo user) { userHolder.set(user); } public static UserInfo getUser() { return userHolder.get(); }} // 场景4:避免参数传递public class TransactionContext { private static ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>(); public static void beginTransaction() { transactionHolder.set(new Transaction()); } public static Transaction getTransaction() { return transactionHolder.get(); }}一键获取完整项目代码2. 使用建议声明为 private static final考虑使用 ThreadLocal.withInitial() 提供初始值在 finally 块中清理资源四、内存泄漏问题1. 泄漏原理强引用链:Thread → ThreadLocalMap → Entry[] → Entry → value (强引用) 弱引用: Entry → key (弱引用指向ThreadLocal) 泄漏场景:1. ThreadLocal实例被回收 → key=null2. 但value仍然被Entry强引用3. 线程池中线程长期存活 → value无法被回收4. 导致内存泄漏一键获取完整项目代码2. 解决方案对比// 方案1:手动remove(推荐)try { threadLocal.set(value); // ... 业务逻辑} finally { threadLocal.remove(); // 必须执行!} // 方案2:使用InheritableThreadLocal(父子线程传递)ThreadLocal<String> parent = new InheritableThreadLocal<>();parent.set("parent value"); new Thread(() -> { // 子线程可以获取父线程的值 System.out.println(parent.get()); // "parent value"}).start(); // 方案3:使用FastThreadLocal(Netty优化版)// 适用于高并发场景,避免了哈希冲突一键获取完整项目代码3. 最佳实践public class SafeThreadLocalExample { // 1. 使用static final修饰 private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); // 2. 包装为工具类 public static Date parse(String dateStr) throws ParseException { SimpleDateFormat sdf = DATE_FORMAT.get(); try { return sdf.parse(dateStr); } finally { // 注意:这里通常不需要remove,因为要重用SimpleDateFormat // 但如果是用完即弃的场景,应该remove } } // 3. 线程池场景必须清理 public void executeInThreadPool() { ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { UserContext.setUser(new UserInfo()); // ... 业务处理 } finally { UserContext.remove(); // 关键! } }); } }}一键获取完整项目代码五、注意事项线程池风险:线程复用导致数据污染继承问题:子线程默认无法访问父线程的ThreadLocal性能影响:哈希冲突时使用线性探测,可能影响性能空值处理:get()返回null时要考虑初始化六、替代方案方案适用场景优点缺点ThreadLocal线程隔离数据简单高效内存泄漏风险InheritableThreadLocal父子线程传递继承上下文线程池中失效TransmittableThreadLocal线程池传递线程池友好引入依赖参数传递简单场景无副作用代码冗余七、调试技巧// 查看ThreadLocalMap内容(调试用)public static void dumpThreadLocalMap(Thread thread) throws Exception { Field field = Thread.class.getDeclaredField("threadLocals"); field.setAccessible(true); Object map = field.get(thread); if (map != null) { Field tableField = map.getClass().getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(map); for (Object entry : table) { if (entry != null) { Field valueField = entry.getClass().getDeclaredField("value"); valueField.setAccessible(true); System.out.println("Key: " + ((WeakReference<?>) entry).get() + ", Value: " + valueField.get(entry)); } } }}一键获取完整项目代码ThreadLocal 是强大的线程隔离工具,但需要谨慎使用。在 Web 应用和线程池场景中,必须在 finally 块中调用 remove(),这是避免内存泄漏的关键。面试回答关于 ThreadLocal,我从原理、场景和内存泄漏三个方面来说一下我的理解。1. 首先,它的核心原理是什么?简单来说,ThreadLocal 是一个线程级别的变量隔离工具。它的设计目标就是让同一个变量,在不同的线程里有自己独立的副本,互不干扰。底层结构:每个线程(Thread对象)内部都有一个自己的 ThreadLocalMap(你可以把它想象成一个线程私有的、简易版的HashMap)。怎么存:当我们调用 ThreadLocal.set(value) 时,实际上是以当前的 ThreadLocal 实例自身作为 Key,要保存的值作为 Value,存入当前线程的那个 ThreadLocalMap 里。怎么取:调用 ThreadLocal.get() 时,也是用自己作为 Key,去当前线程的 Map 里查找对应的 Value。打个比方:就像去银行租保险箱。Thread 是银行,ThreadLocalMap 是银行里的一排保险箱,ThreadLocal 实例就是你手里那把特定的钥匙。你用这把钥匙(ThreadLocal实例)只能打开属于你的那个格子(当前线程的Map),存取自己的东西(Value),完全看不到别人格子的东西。不同的人(线程)即使用同一款钥匙(同一个ThreadLocal实例),打开的也是不同银行的格子,东西自然隔离了。2. 其次,它的典型使用场景有哪些?正是因为这种线程隔离的特性,它特别适合用来传递一些需要在线程整个生命周期内、多个方法间共享,但又不能(或不想)通过方法参数显式传递的数据。最常见的有两个场景:场景一:保存上下文信息(最经典)比如在 Web 应用 或 RPC 框架 中处理一个用户请求时,这个请求从进入系统到返回响应,全程可能由同一个线程处理。我们会把一些信息(比如用户ID、交易ID、语言环境)存到一个 ThreadLocal 里。这样,后续的任何业务方法、工具类,只要在同一个线程里,就能直接 get() 到这些信息,避免了在每一个方法签名上都加上这些参数,代码会简洁很多。场景二:管理线程安全的独享资源典型例子是 数据库连接 和 SimpleDateFormat。像 SimpleDateFormat 这个类,它不是线程安全的。如果做成全局共享,就要加锁,性能差。用 ThreadLocal 的话,每个线程都拥有自己的一个 SimpleDateFormat 实例,既避免了线程安全问题,又因为线程复用了这个实例,减少了创建对象的开销。类似的,在一些需要保证数据库连接线程隔离(比如事务管理)的场景,也会用到 ThreadLocal 来存放当前线程的连接。3. 最后,关于它的内存泄漏问题ThreadLocal 如果使用不当,确实可能导致内存泄漏。它的根源在于 ThreadLocalMap 中 Entry 的设计。问题根源:ThreadLocalMap 的 Key(也就是 ThreadLocal 实例)是一个 弱引用。这意味着,如果外界没有强引用指向这个 ThreadLocal 对象(比如我们把 ThreadLocal 变量设为了 null),下次垃圾回收时,这个 Key 就会被回收掉,于是 Map 里就出现了一个 Key 为 null,但 Value 依然存在的 Entry。这个 Value 是一个强引用,只要线程还活着(比如用的是线程池,线程会复用,一直不结束),这个 Value 对象就永远无法被回收,造成了内存泄漏。如何避免:良好习惯:每次使用完 ThreadLocal 后,一定要手动调用 remove() 方法。这不仅是清理当前值,更重要的是它会清理掉整个 Entry,这是最有效、最安全的做法。设计保障:ThreadLocal 本身也做了一些努力,比如在 set()、get()、remove() 的时候,会尝试去清理那些 Key 为 null 的过期 Entry。但这是一种“被动清理”,不能完全依赖。代码层面:尽量将 ThreadLocal 变量声明为 static final,这样它的生命周期就和类一样长,不会被轻易回收,减少了产生 null Key 的机会。但这并不能替代 remove(),因为线程池复用时,上一个任务的值可能会污染下一个任务。总结一下:内存泄漏的关键是 “弱Key + 强Value + 长生命周期线程” 的组合。所以,把 remove() 放在 finally 块里调用,是一个必须养成的编程习惯。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/156130616
-
概述Redis 主从复制是一种数据同步机制,它允许一个 Redis 服务器(称为 主服务器/Master)将其数据复制到一个或多个 Redis 服务器(称为 从服务器/Slave/Replica)。这是 Redis 实现高可用性、可扩展性和数据冗余的核心技术之一。一、核心作用数据冗余与备份:核心作用:从服务器是主服务器数据的实时热备份。当主服务器数据丢失或损坏时,可以从从服务器恢复,是实现数据持久化的另一种有效方式。读写分离与负载均衡:核心作用:主服务器通常处理写操作和强一致性读操作,而从服务器可以处理大量的读操作。通过将读请求分流到多个从服务器上,可以显著提升系统的整体读吞吐量和并发能力。注意:由于复制是异步的,从服务器上的数据可能存在毫秒级的延迟,适用于对数据一致性要求不非常严格的读场景(如缓存、报表查询)。高可用和故障恢复的基石:核心作用:主从复制是构建 Redis Sentinel(哨兵) 和 Redis Cluster(集群) 等高可用架构的基础。当主服务器发生故障时,可以通过哨兵自动将一个从服务器提升为新的主服务器,实现服务快速切换,保证业务的连续性。横向扩展读能力:核心作用:当读请求成为瓶颈时,可以简单地通过添加更多从服务器来线性扩展读性能,而无需升级主服务器硬件。二、详细工作原理整个复制过程可以分为三个阶段:连接建立阶段、数据同步阶段、命令传播阶段。阶段 1:连接建立与配置配置从节点:在从服务器配置文件 (redis.conf) 中设置 replicaof <master-ip> <master-port> 或在运行时使用 REPLICAOF 命令。建立连接:从服务器根据配置,向主服务器发起一个 Socket 连接,并发送 PING 命令检查通信是否正常。身份验证:如果主服务器设置了 requirepass,从服务器需要发送 AUTH 命令进行密码验证。端口监听:从服务器还会建立一个 复制积压缓冲区监听端口,等待主服务器后续发送数据。阶段 2:数据同步(全量/部分同步)这是复制过程中最核心、最复杂的部分。Redis 2.8 之后,使用 PSYNC 命令取代了旧的 SYNC 命令,支持部分重同步,极大地优化了断线重连后的效率。 全量同步:触发条件:从服务器是第一次连接主服务器,或者从服务器记录的复制偏移量已经不在主服务器的复制积压缓冲区中。过程:从服务器发送 PSYNC ? -1 命令请求全量同步。主服务器执行 BGSAVE 命令,在后台生成当前数据的 RDB 快照文件。主服务器将 RDB 文件通过网络发送给从服务器。在生成和传输RDB期间,新的写命令会被缓存在内存的复制客户端缓冲区中。从服务器清空自身旧数据,然后加载接收到的 RDB 文件,将自身数据状态更新到与主服务器执行 BGSAVE 时一致。主服务器将复制客户端缓冲区中积累的写命令发送给从服务器,从服务器执行这些命令,最终达到与主服务器完全一致的状态。缺点:非常消耗主服务器的 CPU、内存、磁盘 I/O 和网络带宽,尤其是数据量大时。 部分同步:触发条件:从服务器短时间断开后重连,并且它之前同步的偏移量仍然存在于主服务器的复制积压缓冲区中。过程:从服务器发送 PSYNC <runid> <offset> 命令,其中 runid 是主服务器的唯一ID,offset 是从服务器当前的复制偏移量。主服务器判断 runid 是否与自己一致,且 offset 之后的数据是否还在复制积压缓冲区内。如果条件满足,主服务器回复 +CONTINUE,然后仅将从 offset 到缓冲区末尾的写命令发送给从服务器。优点:效率极高,只传输少量缺失的数据,对资源影响小。关键概念解释:复制偏移量:主从服务器各自维护一个偏移量计数器。主服务器每次传播N个字节的命令,其偏移量就增加N。从服务器每次接收到N个字节,其偏移量也增加N。通过对比偏移量可以判断数据是否一致。复制积压缓冲区:主服务器维护的一个固定长度的先进先出队列。它持续记录最近传播的写命令。其大小通过 repl-backlog-size 配置,是决定能否进行部分同步的关键。服务器运行ID:每个Redis实例启动时都会生成一个唯一的运行ID。从服务器会记录主服务器的ID。当主服务器重启变更后,运行ID改变,从服务器会触发全量同步。阶段 3:命令传播(增量同步)数据同步完成后,复制进入稳定阶段。持续同步:主服务器每执行一个会修改数据集的写命令(如 SET、LPUSH 等),都会异步地将这个命令发送给所有连接的从服务器。从服务器执行:从服务器接收到命令后,会在自身的数据集上执行相同的命令,从而保持与主服务器的最终一致性。异步特性:整个命令传播过程是异步的。主服务器发送命令后不会等待从服务器的回复。这意味着在极端情况下,如果主服务器在命令发送后立即宕机,该命令可能丢失,导致从服务器数据稍旧。这是Redis复制在性能和强一致性之间做的权衡。三、重要特性与配置异步复制:默认且最常用的模式,性能好,但存在数据丢失的极小窗口期。可配置的“最小副本数”:通过 min-replicas-to-write 和 min-replicas-max-lag 配置,可以让主服务器在连接的从服务器数量不足或延迟过高时,拒绝执行写命令。这在一定程度上提高了数据的安全性,牺牲了部分可用性。无磁盘复制:通过 repl-diskless-sync 配置,主服务器可以直接将 RDB 内容通过网络发送给从服务器,而不需要先落盘。适用于磁盘IO慢的网络环境。级联复制:从服务器也可以有自己的从服务器,形成树状复制结构,可以减轻主服务器在传播命令时的网络压力。四、总结与形象比喻你可以将 Redis 主从复制理解为一个 “出版-订阅”模型 或 “领导-跟随”模型:主服务器 就像出版社,负责撰写和出版新书(写命令)。复制积压缓冲区 就像是出版社的近期稿件仓库。从服务器 就像各地的书店。全量同步 就像书店第一次进货,需要把出版社的所有库存书籍(RDB)全部运过来。部分同步 就像书店临时补货,只从出版社的近期稿件仓库里拿最新出版的那几本书。命令传播 就像出版社每出版一本新书,就立即寄送给所有订阅的书店。作用:这个系统让书店(从服务器)始终有书可卖(数据可读),即使总社(主服务器)暂时关闭,也能从其他大型书店(另一个从服务器)调货,保证了图书销售系统(Redis服务)的稳定和高效。面试回答Redis 主从复制主要用来实现数据的冗余备份、读写分离和高可用。它的核心就是让一个主节点的数据自动同步到一个或多个从节点上。 原理上,我把它分为三个阶段:建立连接阶段从节点启动后,会通过 slaveof 命令或者配置指向主节点,然后向主节点发送 PSYNC 命令请求同步。主节点收到请求后,会生成一个 RDB 快照文件(bgsave 方式),同时用缓冲区记录这期间的新写命令。数据同步阶段主节点把生成的 RDB 文件发送给从节点,从节点清空自己的旧数据,然后加载这个 RDB 来恢复数据。如果在生成 RDB 期间主节点有新的写操作,这些命令会先保存在一个叫“复制缓冲区”的地方。命令传播阶段RDB 同步完成之后,主节点会把复制缓冲区里的写命令以及后续的所有写命令,以 AOF 重放的方式发送给从节点,从节点执行这些命令,从而和主节点保持实时一致。之后主节点每执行一个写命令,都会异步发送给从节点。另外,Redis 2.8 之后支持了部分重同步:如果从节点短暂断连后又恢复,主节点可以根据复制偏移量和复制缓冲区,只发送断开期间缺失的那部分命令,而不需要全量同步,这大大提升了复制的效率。主从复制的作用主要有三点:数据备份从节点相当于主节点的一个实时备份,一旦主节点数据丢失,可以从从节点恢复。读写分离主节点负责写,从节点可以分担读请求,这样提升整体读的吞吐量,适合读多写少的场景。高可用基础主从复制是 Redis Sentinel 和 Redis Cluster 实现高可用的基础,主节点挂了之后,可以手动或自动将一个从节点提升为主节点,继续提供服务。如果小假的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!————————————————版权声明:本文为CSDN博主「程序员小假」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2402_87298751/article/details/155789785
-
1.概述Spring事务管理是Spring框架中用于确保数据库操作 原子性、一致性、隔离性和持久性(ACID)的核心机制。它通过声明式或编程式(本文略)方式管理事务,支持多种事务传播行为和隔离级别相较于编程式事务,声明式事务通过@Transactional注解实现事务管理,无需手动编写事务代码事务基本概念在全面解析MySQL(5)——“索引、事务、JDBC”三大核心一文中有介绍,本文不再赘述2.@Transactional作用:提供声明式事务管理。它简化了在应用程序中管理数据库事务的流程。开发者只需在方法或类上添加此注解,Spring框架就会自动处理事务的开启、提交和回滚,无需手动编写事务管理代码(如 begin、commit、rollback)级别:类 + 方法作为类注解:为类中所有public方法添加注解作为方法注解:默认仅对public方法生效@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test1") public String test1(String userName,String password) { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test1注册成功,userName:{},password:{}", userName, password); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:2.1 rollbackfor作用:指定哪些异常触发回滚,默认情况下在抛出 非受查异常(RuntimeException)/错误(Error) 时触发回滚抛出受查异常时@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test2") public String test2(String userName,String password) throws IOException { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test2注册成功,userName:{},password:{}", userName, password); throw new IOException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:抛出非受查异常时@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional @RequestMapping("/test3") public String test3(String userName,String password) { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test3注册成功,userName:{},password:{}", userName, password); throw new RuntimeException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:指定回滚类型@RequestMapping("/test")@RestController@Slf4jpublic class TestController { private final UserService userService; @Autowired public TestController(UserService userService) { this.userService = userService; } @Transactional(rollbackFor = Exception.class) @RequestMapping("/test4") public String test4(String userName,String password) throws IOException { UserInfo userInfo = new UserInfo(); userInfo.setUserName(userName); userInfo.setPassword(password); Integer result = userService.register(userInfo); if (result == 1){ log.info("test4注册成功,userName:{},password:{}", userName, password); throw new IOException(); } return "注册成功"; }}使用PostMan向后端发送请求:MySQL查询结果如下:后端日志日志如下:2.2 isolation作用:用于指定事务的隔离级别Isolation.DEFAULT:使用底层数据库默认的隔离级别Isolation.READ_UNCOMMITTED:读未提交Isolation.READ_COMMITTED:读已提交Isolation.REPEATABLE_READ:可重复读Isolation.SERIALIZABLE:串行化每种隔离级别的具体效果在全面解析MySQL(5)——“索引、事务、JDBC”三大核心一文中有介绍,本文不再赘述2.3 propagation作用:用于定义事务的传播行为,即当前事务方法被另一个事务方法调用时,事务应如何传播。Spring提供了7种传播行为,均基于Propagation枚举类实现2.3.1 Propagation.REQUIRED默认传播行为。如果当前存在事务,则加入该事务;如果不存在事务,则新建一个事务 2.3.2 Propagation.SUPPORTS如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行2.3.3 Propagation.MANDATORY强制要求当前存在事务并加入,否则抛出异常 2.3.4 Propagation.REQUIRES_NEW无论当前是否存在事务,都新建一个事务。新事务与当前事务独立,互不干扰2.3.5 Propagation.NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,则挂起该事务2.3.6 Propagation.NEVER强制要求当前不能存在事务,否则抛出异常2.3.7 Propagation.NESTED如果当前存在事务,则在嵌套事务中执行;如果不存在事务,则行为与Propagation.REQUIRED相同。嵌套事务的回滚不影响外部事务,但外部事务回滚会导致嵌套事务回滚(适用于需要部分回滚的场景)3.GiteeGitee地址:九转苍翎本文源码:spring-trans————————————————版权声明:本文为CSDN博主「九转苍翎」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/2401_89167985/article/details/155503362
-
1、git删除分支实现步骤【转载】cid:link_62、git branch如何delete方式【转载】cid:link_73、 input的accept属性让文件上传安全高效【转载】cid:link_04、HTML5的<input>标签的`type`属性值详解和代码示例【转载】cid:link_15、 python爬虫脚本HTTP 403 Forbidden错误怎么办?【转载】cid:link_26、Python实现将.py代码转换为带语法高亮的Word和PDF【转载】cid:link_87、Python多进程中避免死锁问题的六种策略【转载】cid:link_98、 Python实现将PDF转DOCX的超简单教程(附源码)【转载】cid:link_39、Python基于OpenPyxl实现Excel转PDF并精准控制分页【转载】cid:link_1010、Python获取Docker容器实时资源占用(CPU、内存、IO等)5种实现方式【转载】cid:link_1111、 Python flash URL访问参数配置【转载】cid:link_1212、 Python利用PyMobileDevice3控制iOS设备的完整教程【转载】cid:link_413、 Python基本语法总结大全(含java、js对比)【转载】cid:link_514、 Python自动化提取多个Word文档的文本【转载】cid:link_1315、mybatis-plus分表实现案例(附示例代码)【转载】https://bbs.huaweicloud.com/forum/thread-02126200655519113076-1-1.html
-
一、坑 1:Token 过期未处理,鉴权异常引发服务中断问题本质DeepSeek 的 Token 一般能用 30 天,好多人一开始就把 Token 写在代码里,根本没考虑过期的事。一旦 Token 过期,所有 API 调用全返回 401 鉴权失败,如果没做异常处理,就会直接导致依赖该接口的业务服务中断。更糟的是,有的代码连异常都不捕获,直接抛个运行时异常,把线程池堵死,最后服务都熔断了。典型错误代码12345678910111213141516171819202122232425// 错误示例:硬编码Token,无过期处理、无异常捕获public class DeepSeekClient { // 写死的 Token,过期直接失效 private static final String DEEPSEEK_TOKEN = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; public String callApi(String prompt) { // 直接拼 Token,不管过没过期 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + DEEPSEEK_TOKEN); headers.setContentType(MediaType.APPLICATION_JSON); // 拼请求体 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); RestTemplate restTemplate = new RestTemplate(); // 鉴权失败直接抛异常,服务跟着崩 ResponseEntity<String> response = restTemplate.postForEntity(API_URL, new HttpEntity<>(requestBody, headers), String.class); return response.getBody(); }}解决方案:实现 Token 自动刷新 + 异常兜底解决方案:实现 Token 自动刷新 + 异常兜底 核心思路:封装 Token 管理类,维护 Token 有效期,提前 1 天主动刷新;增加鉴权异常捕获,触发被动刷新;采用双重检查锁保证 Token 刷新的线程安全;增加降级策略,Token 刷新失败时触发告警并返回兜底响应。完整正确代码读取配置的工具类123456789101112131415161718192021222324import java.io.IOException;import java.io.InputStream;import java.util.Properties; // 读取配置文件的工具public class PropertiesUtils { private static final Properties props = new Properties(); static { try (InputStream in = PropertiesUtils.class.getClassLoader().getResourceAsStream("deepseek.properties")) { props.load(in); } catch (IOException e) { throw new RuntimeException("加载deepseek配置文件失败", e); } } public static String getProperty(String key) { String value = props.getProperty(key); if (value == null) { throw new RuntimeException("配置项[" + key + "]不存在"); } return value; }}然后是 Token 管理类和 API 调用客户端:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130import org.springframework.http.*;import org.springframework.web.client.HttpClientErrorException;import org.springframework.web.client.RestTemplate; import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReentrantLock; // Token 管理工具,自动刷新,线程安全public class DeepSeekTokenManager { // 从配置文件读 private static final String REFRESH_TOKEN = PropertiesUtils.getProperty("deepseek.refresh.token"); private static final String TOKEN_REFRESH_URL = "https://api.deepseek.com/v1/auth/refresh"; // 30天有效期,提前1天刷新,单位都是毫秒 private static final long TOKEN_VALID_PERIOD = 30 * 24 * 60 * 60 * 1000L; private static final long REFRESH_ADVANCE = 24 * 60 * 60 * 1000L; // 当前能用的Token、过期时间、刷新用的锁 private volatile String currentToken; private volatile long expireTime; private final ReentrantLock refreshLock = new ReentrantLock(); private final RestTemplate restTemplate = new RestTemplate(); // 单例模式 private static class SingletonHolder { private static final DeepSeekTokenManager INSTANCE = new DeepSeekTokenManager(); } public static DeepSeekTokenManager getInstance() { return SingletonHolder.INSTANCE; } // 初始化的时候就加载第一个Token private DeepSeekTokenManager() { refreshToken(); } // 拿能用的Token,自动判断要不要刷新 public String getValidToken() { long now = System.currentTimeMillis(); // 现在的时间加提前量,快到过期时间就刷新 if (now + REFRESH_ADVANCE >= expireTime) { refreshToken(); } return currentToken; } // 刷新Token private void refreshToken() { if (System.currentTimeMillis() + REFRESH_ADVANCE < expireTime) { return; } // 加锁后再检查一遍,防止刚释放锁别人又进来了 refreshLock.lock(); try { if (System.currentTimeMillis() + REFRESH_ADVANCE < expireTime) { return; } // 拼刷新Token的请求 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); Map<String, String> requestBody = new HashMap<>(); requestBody.put("refresh_token", REFRESH_TOKEN); ResponseEntity<Map> response = restTemplate.postForEntity( TOKEN_REFRESH_URL, new HttpEntity<>(requestBody, headers), Map.class ); if (response.getStatusCode() == HttpStatus.OK) { Map<String, Object> resBody = response.getBody(); this.currentToken = (String) resBody.get("access_token"); this.expireTime = System.currentTimeMillis() + TOKEN_VALID_PERIOD; System.out.println("Token刷新成功,新的过期时间:" + expireTime); } else { throw new RuntimeException("Token刷新失败,响应码:" + response.getStatusCode()); } } catch (Exception e) { System.err.println("Token刷新出问题了:" + e.getMessage()); // 临时延长10分钟,给运维留时间处理 this.expireTime = System.currentTimeMillis() + 10 * 60 * 1000L; throw new RuntimeException("Token刷新失败,已临时续命10分钟,赶紧查!", e); } finally { refreshLock.unlock(); } }} // DeepSeek API 调用客户端(集成Token管理)public class DeepSeekApiClient { private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; private final RestTemplate restTemplate = new RestTemplate(); private final DeepSeekTokenManager tokenManager = DeepSeekTokenManager.getInstance(); public String callDeepSeek(String prompt) { // 获取有效Token String token = tokenManager.getValidToken(); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + token); headers.setContentType(MediaType.APPLICATION_JSON); // 构建请求体 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); requestBody.put("timeout", 30000); // 30秒超时 HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); try { ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); } catch (HttpClientErrorException.Unauthorized e) { System.err.println("鉴权失败,强制刷新Token重试:" + e.getMessage()); tokenManager.refreshToken(); // 重试请求 headers.set("Authorization", "Bearer " + tokenManager.getValidToken()); HttpEntity<Map<String, Object>> retryReq = new HttpEntity<>(requestBody, headers); return restTemplate.postForEntity(API_URL, retryReq, String.class).getBody(); } catch (Exception e) { System.err.println("API调用出错:" + e.getMessage()); return "{\"choices\":[{\"message\":{\"content\":\"稍等一下,请求有点问题~\"}}]}"; } }}关键优化点说明配置化:用 properties 文件存 Token,改的时候不用动代码;主动+被动刷新:提前1天主动刷,漏了还有401被动刷,基本不会过期;线程安全:双重检查锁 + 可重入锁,多线程的时候只会有一个去刷新;降级兜底:刷新失败临时续命10分钟,API调用错了返回友好提示,服务不会直接崩。二、坑 2:并发调用线程不安全,数据错乱/连接泄漏问题本质好多人写代码的时候,RestTemplate 不配置连接池,还把 HttpHeaders 这种东西做成全局共享的。多线程一并发就出问题:多线程并发调用时,请求头/请求体数据错乱;RestTemplate 未配置连接池,高并发下出现连接泄漏、端口耗尽;未做线程池隔离,API 调用超时导致线程池阻塞,影响其他业务。典型错误代码123456789101112131415161718192021222324252627// 错误示例:共享非线程安全对象,无连接池,无线程池隔离public class UnsafeDeepSeekClient { // 错误:RestTemplate未配置连接池,高并发下连接泄漏 private static final RestTemplate restTemplate = new RestTemplate(); // 错误:共享HttpHeaders,多线程下数据错乱 private static final HttpHeaders sharedHeaders = new HttpHeaders(); static { sharedHeaders.set("Authorization", "Bearer sk-xxxxxxxx"); sharedHeaders.setContentType(MediaType.APPLICATION_JSON); } // 错误:无线程池,直接同步调用,超时阻塞主线程 public String concurrentCall(String prompt) { Map<String, Object> requestBody = new HashMap<>(); // HashMap也不是线程安全的 requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); // 多线程下sharedHeaders可能被篡改 HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, sharedHeaders); // 无超时配置,调用超时阻塞线程 ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); }}解决方案:线程池隔离 + 连接池配置 + 线程安全封装核心思路:配置 RestTemplate 连接池,限制最大连接数、超时时间,避免连接泄漏;使用线程池隔离 DeepSeek API 调用,避免影响核心业务;每个请求独立创建 HttpHeaders、HashMap,避免多线程共享;增加请求超时、线程池拒绝策略,保证服务稳定性。完整正确代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105import org.apache.http.client.config.RequestConfig;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClientBuilder;import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;import org.springframework.web.client.RestTemplate; import java.util.HashMap;import java.util.Map;import java.util.concurrent.*; // 线程安全的DeepSeek API客户端(含连接池、线程池配置)public class ThreadSafeDeepSeekClient { // 带连接池的RestTemplate private static final RestTemplate restTemplate; // 配置线程池:隔离DeepSeek API调用,避免影响核心业务 private static final ExecutorService deepSeekExecutor; static { // 1. 先配HTTP连接池,控制最大连接数 PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(100); // 总共最多100个连接 connManager.setDefaultMaxPerRoute(50); // 同一个地址最多50个连接 // 超时配置,别卡太久 RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // 连服务器5秒超时 .setConnectionRequestTimeout(3000) // 从连接池拿连接3秒超时 .setSocketTimeout(30000) // 读数据30秒超时 .build(); CloseableHttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connManager) .setDefaultRequestConfig(requestConfig) .build(); // 把连接池配置给RestTemplate HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); restTemplate = new RestTemplate(requestFactory); // 2. 再配个专用线程池 deepSeekExecutor = new ThreadPoolExecutor( 10, // 平时保持10个活线程 50, // 最多扩到50个线程 60L, TimeUnit.SECONDS, // 空闲线程60秒没人用就关掉 new LinkedBlockingQueue<>(1000), // 任务排队最多1000个 new ThreadFactory() { private int count = 0; @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("deepseek-api-thread-" + count++); thread.setDaemon(true); // 守护线程,程序关的时候不用等它 return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 任务满了就让调用者自己执行,别丢任务 ); } private final DeepSeekTokenManager tokenManager = DeepSeekTokenManager.getInstance(); // 异步调用DeepSeek API(线程安全) public CompletableFuture<String> asyncCall(String prompt) { // 把任务丢到专用线程池里 return CompletableFuture.supplyAsync(() -> { // 关键:每个请求自己建请求头,别共享 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + tokenManager.getValidToken()); headers.setContentType(MediaType.APPLICATION_JSON); // 关键:每个请求独立创建请求体,避免HashMap线程不安全问题 Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", prompt) )); requestBody.put("temperature", 0.7); // 随机性调中等 requestBody.put("max_tokens", 2000); // 最多返回2000个Token HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); try { ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class); return response.getBody(); } catch (Exception e) { System.err.println("线程" + Thread.currentThread().getName() + "调用出错:" + e.getMessage()); return "{\"choices\":[{\"message\":{\"content\":\"请求失败,稍后再试~\"}}]}"; } }, deepSeekExecutor); } // 关闭线程池(应用关闭时调用) public void shutdownExecutor() { deepSeekExecutor.shutdown(); try { // 等10秒,还没关完就强制关 if (!deepSeekExecutor.awaitTermination(10, TimeUnit.SECONDS)) { deepSeekExecutor.shutdownNow(); } } catch (InterruptedException e) { deepSeekExecutor.shutdownNow(); } }}关键优化点说明连接池限流:控制最大连接数,高并发的时候不会把服务器端口占满;线程池隔离:API调用出问题不会影响核心业务,拒绝策略选“调用者执行”,避免任务丢失;每个请求独立:请求头和请求体都自己建,彻底解决多线程数据乱的问题;异步提升效率:用CompletableFuture异步调用,主线程不用等,服务吞吐量直接上来。三、坑 3:超长文本未分块,触发 API 长度限制问题本质DeepSeek 的模型都有 Token 限制,比如 deepseek-chat 单轮最多大概 8192 个 Token。好多人不管文本多长都直接传,要么返回 400 说“超出长度”,要么自己瞎截断把关键信息切没了,模型回复得乱七八糟。更坑的是,有人按字数算长度,不知道中文和英文占的 Token 不一样,切完还是超。典型错误代码1234567891011121314151617// 错误示例:直接传入超长文本,无分块、无Token计算public class LongTextErrorClient { public String callLongText(String longContent) { Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", "deepseek-chat"); // 直接把超长文本丢进去,不管长度 requestBody.put("messages", Collections.singletonList( Map.of("role", "user", "content", longContent) )); HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + DeepSeekTokenManager.getInstance().getValidToken()); HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); RestTemplate restTemplate = new RestTemplate(); return restTemplate.postForEntity(API_URL, request, String.class).getBody(); }}解决方案:Token 计算 + 智能分块 + 结果拼接核心思路:实现 Token 计数器,准确计算文本对应的 Token 数(适配 DeepSeek 的 Token 编码规则);按模型最大 Token 限制,对超长文本进行智能分块(保留语义完整性,避免截断句子);分块调用 API 后,拼接所有分块的回复结果;多轮对话场景下,优先截断历史对话,保留最新上下文。完整正确代码先加依赖(Maven),这个工具能精准算 Token:12345<dependency> <groupId>com.knuddels</groupId> <artifactId>jtokkit</artifactId> <version>1.0.0</version></dependency>然后是长文本处理工具:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139import com.knuddels.jtokkit.Encodings;import com.knuddels.jtokkit.api.Encoding;import com.knuddels.jtokkit.api.EncodingRegistry;import com.knuddels.jtokkit.api.ModelType; import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.stream.Collectors; // 长文本处理工具,算Token、分块、拼结果一条龙public class LongTextProcessor { // 初始化Token计算器,DeepSeek用的编码和GPT-4一样 private static final EncodingRegistry registry = Encodings.newDefaultEncodingRegistry(); private static final Encoding encoding = registry.getEncodingForModel(ModelType.GPT_4); // 单轮最大Token数8192,留2000给模型回复,所以请求最多6192个Token private static final int MAX_REQUEST_TOKENS = 8192 - 2000; private final ThreadSafeDeepSeekClient deepSeekClient = new ThreadSafeDeepSeekClient(); // 算文本的Token数,比按字数准多了 public int countTokens(String text) { return encoding.countTokens(text); } // 超长文本分块,尽量不切句子 public List<String> splitLongText(String longText) { List<String> chunks = new ArrayList<>(); int totalTokens = countTokens(longText); // 没超限制就直接返回 if (totalTokens <= MAX_REQUEST_TOKENS) { chunks.add(longText); return chunks; } // 按中文和英文的句号分割句子,保持意思完整 String[] sentences = longText.split("(?<=[。!?.?!])"); StringBuilder currentChunk = new StringBuilder(); int currentTokens = 0; for (String sentence : sentences) { int sentenceTokens = countTokens(sentence); // 极端情况:一个句子就超了,那就按Token硬切 if (sentenceTokens > MAX_REQUEST_TOKENS) { chunks.addAll(splitOverLengthSentence(sentence, MAX_REQUEST_TOKENS)); continue; } // 加当前句子会超的话,先把之前的存起来 if (currentTokens + sentenceTokens > MAX_REQUEST_TOKENS) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; } currentChunk.append(sentence); currentTokens += sentenceTokens; } // 把最后一块加进去 if (currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); } return chunks; } // 单个句子超Token限制,按Token硬切 private List<String> splitOverLengthSentence(String sentence, int maxTokens) { List<String> subChunks = new ArrayList<>(); char[] chars = sentence.toCharArray(); StringBuilder subChunk = new StringBuilder(); int currentTokens = 0; for (char c : chars) { String charStr = String.valueOf(c); int charTokens = countTokens(charStr); if (currentTokens + charTokens > maxTokens) { subChunks.add(subChunk.toString().trim()); subChunk = new StringBuilder(); currentTokens = 0; } subChunk.append(c); currentTokens += charTokens; } if (subChunk.length() > 0) { subChunks.add(subChunk.toString().trim()); } return subChunks; } // 分块调用API,最后拼结果 public String processLongText(String longText) { List<String> chunks = splitLongText(longText); // 就一块的话直接调 if (chunks.size() == 1) { return deepSeekClient.asyncCall(chunks.get(0)).join(); } // 多块的话异步调用,效率高 List<CompletableFuture<String>> futures = chunks.stream() .map(chunk -> { // 告诉模型这是第几块,总共多少块,让它有上下文 String prompt = "请处理以下文本片段(一共" + chunks.size() + "段,这是第" + (chunks.indexOf(chunk) + 1) + "段):\n" + chunk; return deepSeekClient.asyncCall(prompt); }) .collect(Collectors.toList()); // 等所有调用都完成 CompletableFuture<Void> allDone = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); allDone.join(); // 拼结果 StringBuilder finalResult = new StringBuilder(); finalResult.append("超长文本处理结果(按片段拼接):\n"); for (CompletableFuture<String> future : futures) { try { String chunkRes = future.get(); // 提取回复内容,实际项目里用FastJSON或者Jackson解析更靠谱 String content = extractContent(chunkRes); finalResult.append(content).append("\n"); } catch (Exception e) { finalResult.append("【这段处理失败】:").append(e.getMessage()).append("\n"); } } return finalResult.toString(); } // 从API响应里把回复内容抠出来(简化版,实际用JSON库) private String extractContent(String response) { int contentStart = response.indexOf("\"content\":\"") + 10; int contentEnd = response.indexOf("\"}", contentStart); if (contentStart > 0 && contentEnd > contentStart) { return response.substring(contentStart, contentEnd); } return response; }}关键优化点说明精准Token计算:使用 jtokkit 库(适配 OpenAI/DeepSeek 的 Token 编码规则),准确计算文本 Token 数,避免按字符数分割导致的误差;语义化分块:优先按句子分割,保留文本语义完整性,避免截断导致的上下文丢失;极端情况处理:单个句子超出 Token 限制时,按 Token 数切割,保证分块后能正常调用 API;异步分块调用:多块文本异步调用 API,提高处理效率,最后拼接结果;上下文标识:给每个分块添加段数标识,让模型理解当前处理的是超长文本的一部分,提升回复质量。四、坑 4:模型名称配置错误问题本质不同模型的名称规范不同,若将其他模型的名称直接套用在 DeepSeek 上,会返回 404 错误(模型不存在)。解决方案搞个枚举类存DeepSeek的模型名:12345678910111213141516171819202122232425262728293031323334// DeepSeek模型名枚举,直接拿过来用public class DeepSeekModelEnum { // 通用对话、代码生成、推理增强,常用的就这三个 public static final String DEEPSEEK_CHAT = "deepseek-chat"; public static final String DEEPSEEK_CODER = "deepseek-coder"; public static final String DEEPSEEK_R1 = "deepseek-r1"; // 如果你之前用别的模型,用这个方法转成DeepSeek的 public static String convertFromOtherModel(String otherModelName) { switch (otherModelName.toLowerCase()) { // 之前用其他AI的对话场景,转成deepseek-chat case "gpt-3.5-turbo": case "ernie-bot": return DEEPSEEK_CHAT; // 之前用代码模型的,转成deepseek-coder case "code-davinci-002": return DEEPSEEK_CODER; // 其他情况默认用通用对话模型 default: return DEEPSEEK_CHAT; } }} // 调用示例public class ModelClient { public String callWithRightModel(String prompt) { Map<String, Object> requestBody = new HashMap<>(); // 直接用枚举里的模型名,肯定不会错 requestBody.put("model", DeepSeekModelEnum.DEEPSEEK_CHAT); // 其他参数... return ""; }}五、坑 5:响应参数解析错误问题本质虽然DeepSeek响应格式和OpenAI像,但有些字段不一样,比如finish_reason的取值、usage里的统计方式。有人直接抄其他AI的解析代码,结果要么字段拿不到,要么报解析异常。解决方案搞个专门的解析工具,兼容这些差异:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject; // DeepSeek响应解析工具类(适配字段差异)public class DeepSeekResponseParser { // 提取回复内容 public static String getContent(String responseJson) { try { JSONObject root = JSON.parseObject(responseJson); // 兼容DeepSeek和OpenAI的响应结构 if (root.containsKey("choices") && !root.getJSONArray("choices").isEmpty()) { JSONObject choice = root.getJSONArray("choices").getJSONObject(0); // DeepSeek的message字段与OpenAI一致,但需判空 if (choice.containsKey("message")) { return choice.getJSONObject("message").getString("content"); } } // 要是有error字段,直接抛异常 if (root.containsKey("error")) { throw new RuntimeException("API报错:" + root.getJSONObject("error").getString("message")); } return "没拿到有效回复"; } catch (Exception e) { throw new RuntimeException("解析响应失败:" + e.getMessage()); } } // 解析Token使用量(适配DeepSeek的usage字段) public static int getUsedTokens(String responseJson) { JSONObject root = JSON.parseObject(responseJson); if (root.containsKey("usage")) { return root.getJSONObject("usage").getIntValue("total_tokens"); } return 0; }} // 调用示例public class ParserClient { public void parseResponse(String response) { // 直接拿回复内容,不用自己处理JSON String content = DeepSeekResponseParser.getContent(response); // 看看用了多少Token int usedTokens = DeepSeekResponseParser.getUsedTokens(response); System.out.println("回复:" + content); System.out.println("消耗Token:" + usedTokens); }}六、坑 6:超时配置不匹配问题本质DeepSeek API 的响应速度与模型类型、文本长度相关(如 deepseek-coder 处理代码时响应较慢),若直接复用其他AI的超时配置(如 10 秒),会导致频繁超时;反之,超时配置过长会导致线程阻塞。解决方案123456789101112131415161718192021222324252627282930313233343536373839404142434445// 动态超时配置工具public class TimeoutConfig { // 不同模型的基础超时时间(毫秒) private static final int CHAT_TIMEOUT = 30000; // 对话模型30秒 private static final int CODER_TIMEOUT = 60000; // 代码模型60秒 private static final int R1_TIMEOUT = 45000; // 推理模型45秒 // 按模型拿基础超时 public static int getBaseTimeout(String modelName) { switch (modelName) { case DeepSeekModelEnum.DEEPSEEK_CODER: return CODER_TIMEOUT; case DeepSeekModelEnum.DEEPSEEK_R1: return R1_TIMEOUT; default: return CHAT_TIMEOUT; } } // 按文本长度加超时,长文本给更多时间 public static int getDynamicTimeout(String modelName, String text) { int baseTimeout = getBaseTimeout(modelName); int textLen = text.length(); // 每1000字加5秒,最多不超过基础超时的2倍(别无限加) int extraTimeout = Math.min((textLen / 1000) * 5000, baseTimeout); return baseTimeout + extraTimeout; }} // 调用示例public class TimeoutClient { public String callWithDynamicTimeout(String prompt) { String model = DeepSeekModelEnum.DEEPSEEK_CODER; // 按模型和文本长度算超时 int timeout = TimeoutConfig.getDynamicTimeout(model, prompt); Map<String, Object> requestBody = new HashMap<>(); requestBody.put("model", model); requestBody.put("messages", Collections.singletonList(Map.of("role", "user", "content", prompt))); requestBody.put("timeout", timeout); // 把算好的超时传进去 // 其他请求逻辑... return ""; }}七、坑 7:请求参数不兼容问题本质有些参数在别的模型里有用,但DeepSeek不支持。解决方案123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import java.util.HashMap;import java.util.List;import java.util.Map; // 请求参数适配工具public class ParamsAdapter { // DeepSeek不支持的参数列表 private static final List<String> UNSUPPORTED_PARAMS = List.of( "frequency_penalty", "presence_penalty", "logit_bias" ); // 适配参数,保证传过去的都能用 public static Map<String, Object> adapt(Map<String, Object> originalParams) { Map<String, Object> adaptedParams = new HashMap<>(originalParams); // 1. 把不支持的参数删掉 UNSUPPORTED_PARAMS.forEach(adaptedParams::remove); // 2. 修正temperature:只能0-1 if (adaptedParams.containsKey("temperature")) { double temp = (double) adaptedParams.get("temperature"); // 小于0取0,大于1取1,中间的不变 adaptedParams.put("temperature", Math.min(Math.max(temp, 0.0), 1.0)); } // 3. 修正max_tokens:最少10,最多4096 if (adaptedParams.containsKey("max_tokens")) { int maxTokens = (int) adaptedParams.get("max_tokens"); adaptedParams.put("max_tokens", Math.min(Math.max(maxTokens, 10), 4096)); } return adaptedParams; }} // 调用示例public class ParamsClient { public String callWithRightParams(String prompt) { // 原来的参数,可能有无效的 Map<String, Object> originalParams = new HashMap<>(); originalParams.put("model", DeepSeekModelEnum.DEEPSEEK_CHAT); originalParams.put("temperature", 1.5); // 超出DeepSeek的范围 originalParams.put("frequency_penalty", 0.5); // DeepSeek不支持 originalParams.put("messages", Collections.singletonList(Map.of("role", "user", "content", prompt))); // 适配后再传 Map<String, Object> adaptedParams = ParamsAdapter.adapt(originalParams); // 其他请求逻辑... return ""; }}八、坑 8:错误码处理不兼容问题本质同样是429错误,DeepSeek表示“请求太频繁,触发限流了”,但文心一言可能表示“Token用完了”;还有500错误,有人以为是自己代码的问题,其实是DeepSeek服务端的问题,白查半天。解决方案1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import org.springframework.web.client.HttpClientErrorException; // 错误码适配工具类public class DeepSeekErrorHandler { // DeepSeek常见错误码说明 public enum ErrorCode { TOKEN_EXPIRED(401, "Token过期或无效,该刷新了"), RATE_LIMIT(429, "请求太频繁,歇会儿再试"), TOO_LONG(400, "文本太长,超过Token限制了"), MODEL_NOT_FOUND(404, "模型名填错了"), SERVER_ERROR(500, "DeepSeek服务端出问题了"); private final int code; private final String desc; ErrorCode(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } } // 处理DeepSeek错误,返回适配的重试策略 public static boolean needRetry(Exception e) { if (e instanceof HttpClientErrorException) { int statusCode = ((HttpClientErrorException) e).getStatusCode().value(); // 限流和服务端错误可以重试,其他的别瞎试 return statusCode == ErrorCode.RATE_LIMIT.getCode() || statusCode == ErrorCode.SERVER_ERROR.getCode(); } // 网络超时、连接失败这些也可以重试 return e instanceof java.net.SocketTimeoutException || e instanceof java.net.ConnectException; } // 获取错误描述,适配不同模型的错误码 public static String getErrorMsg(Exception e) { if (e instanceof HttpClientErrorException) { HttpClientErrorException ex = (HttpClientErrorException) e; int statusCode = ex.getStatusCode().value(); // 匹配错误码 for (ErrorCode errorCode : ErrorCode.values()) { if (errorCode.getCode() == statusCode) { return errorCode.getDesc() + ",详情:" + ex.getResponseBodyAsString(); } } } // 其他错误直接返回信息 return "未知错误:" + e.getMessage(); }} // 调用示例,带重试逻辑public class RetryClient { private static final int MAX_RETRY = 3; // 最多重试3次 public String callWithRetry(String prompt) { int retryCount = 0; while (retryCount < MAX_RETRY) { try { // 调用API的逻辑 DeepSeekApiClient client = new DeepSeekApiClient(); return client.callDeepSeek(prompt); } catch (Exception e) { retryCount++; String errorMsg = DeepSeekErrorHandler.getErrorMsg(e); System.err.println("第" + retryCount + "次调用失败:" + errorMsg); // 不该重试就直接退出 if (!DeepSeekErrorHandler.needRetry(e)) { break; } // 指数退避重试:第1次等2秒,第2次等4秒,第3次等8秒 try { Thread.sleep((long) (Math.pow(2, retryCount) * 1000)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } return "{\"choices\":[{\"message\":{\"content\":\"试了好几次都不行,稍后再试吧~\"}}]}"; }}九、总结与最佳实践本文拆解了 Java 调用 DeepSeek API 的 8 个高频错误,从 Token 管理、并发安全、超长文本处理到跨模型适配,核心避坑思路可总结为:1. 鉴权层:主动防御 + 被动兜底避免硬编码 Token,对接配置中心实现动态刷新;结合主动刷新(提前检测有效期)和被动刷新(捕获 401 异常),保证 Token 有效性;增加降级策略,Token 刷新失败时临时延长有效期,避免服务中断。2. 并发层:隔离 + 安全配置 HTTP 连接池,限制最大连接数,避免端口耗尽;使用专用线程池隔离 API 调用,合理设置核心线程数、队列大小和拒绝策略;每个请求独立创建非线程安全对象(如 HttpHeaders、HashMap),杜绝数据错乱。3. 文本层:精准计算 + 语义分块使用专业 Token 计算库,避免按字符数分割导致的误差;优先按句子分块,保留语义完整性,极端情况按 Token 切割;分块调用时添加上下文标识,提升回复质量,最后拼接结果。4. 适配层:兼容差异 + 动态调整按模型类型适配超时时间、请求参数,过滤不支持的参数;适配错误码处理逻辑,针对不同错误码制定差异化重试策略;解析响应时兼容字段差异,避免解析异常。
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签