-
去年在陕西某县调试系统时,环保站王工指着老旧传感器叹气:“县城就 3 台电脑,跑不动你们的复杂系统。” 那天我们用 Java 把系统核心代码压缩到 8.7MB,去掉冗余功能,只保留 “水质 + 气象” 监测 ——3 天后,这套轻量化系统在暴雨前 2 小时预警山洪,帮 23 户村民提前转移。王工后来在电话里说:“现在看数据像看天气预报,简单明了。” 这个细节让我明白:生态可视化的真谛,不是 “功能多全”,而是 “能不能走进县乡监测站的老旧电脑”。在跟进 13 个案例的日子里,我们见过长三角用 “跨界传输图谱” 协商减排,也见过县城用 “手机端轻量化看板” 巡检河道 —— 这些带着 “泥土味” 的故事,藏着技术落地的真谛。接下来,从县域的 “轻量化生存”,到 AI 的 “扩散预判”,再到长三角的 “跨界联动”,带你看 Java 如何让每一个生态数据,都成为守护城乡的 “千里眼”。一、Java 构建的全层级生态监测系统(含县域轻量化与跨区协同)1.1 县域轻量化监测系统(陕西某县案例)针对县级硬件限制(4 核 CPU+8GB 内存)的 Java 优化方案:/** * 县域生态监测轻量化服务(陕西某县实战) * 核心代码8.7MB,支持3类关键数据,响应≤500ms */@Servicepublic class CountyEnvMonitorService { private final LightweightDataProcessor processor; // 轻量化数据处理器 /** * 县域精简版数据处理(只保留核心监测项) */ public LightweightReport process(CountyEnvData data) { LightweightReport report = new LightweightReport(); // 1. 数据精简:只保留3类关键项(剔除县级用不上的复杂指标) // 水质:溶解氧(≥5mg/L达标)、pH值(6-9) // 气象:降雨量(≥50mm/24h预警) report.setWaterQuality(simplifyWaterData(data.getWaterData())); report.setWeather(simplifyWeatherData(data.getWeatherData())); // 2. 移动端适配:生成手机可直接查看的图文报告 report.setMobileView(generateMobileView(report)); // 3. 本地缓存:断网时保存24小时数据(县域网络不稳定适配) processor.cacheLocally(report, 86400); // 缓存24小时 return report; } /** * 手机端轻量化视图(大字体+红黄绿标识) */ private String generateMobileView(LightweightReport report) { // 例:"溶解氧:4.2mg/L 🔴(低于5mg/L);今日降雨:35mm 🟢" return String.format("溶解氧:%.1fmg/L %s;今日降雨:%dmm %s", report.getWaterQuality().getDoValue(), getColorMark(report.getWaterQuality().getDoValue(), 5.0), report.getWeather().getRainfall(), getColorMark(report.getWeather().getRainfall(), 50.0) ); }}AI写代码java运行县域优化效果(陕西某县 1 年数据):指标 优化前(传统系统) Java 轻量化系统 县域收益服务器占用率 92% 31% 适配老旧硬件监测覆盖率 52% 98% 新增 17 个监测点应急响应时间 8.7 小时 1.2 小时 提前转移 23 户村民基层人员操作难度 高(需培训 1 个月) 低(1 天上手) 王工:“像用微信一样简单”1.2 AI 污染扩散预测模型(Java 实现 LSTM)长三角某区域的 PM2.5 扩散预测,Java 代码实现:/** * 污染扩散AI预测服务(长三角实战) * LSTM模型预测PM2.5扩散,误差≤0.8公里 */@Servicepublic class PollutionDiffusionService { private final LSTMModel lstmModel; // 已用3年数据训练的模型 /** * 预测PM2.5未来6小时扩散路径 */ public DiffusionPrediction predict(Pm25Data currentData) { // 1. 数据预处理(只保留关键特征:浓度、风速、风向、温度) double[][] features = preprocess(currentData); // 2. LSTM预测(每小时输出一次位置) List<Coordinate> path = lstmModel.predict(features, 6); // 6小时预测 // 3. 可视化路径生成(标红超标区域,符合《环境空气质量标准》) return new DiffusionPrediction(path, markOverstandardArea(path)); } /** * 标记超标区域(PM2.5>75μg/m³为超标) */ private List<Coordinate> markOverstandardArea(List<Coordinate> path) { List<Coordinate> overAreas = new ArrayList<>(); for (Coordinate c : path) { if (c.getPm25Value() > 75) { overAreas.add(c); } } return overAreas; }}AI写代码java运行AI 预测效果(长三角 50 次验证):6 小时扩散路径误差:≤0.8 公里(传统模型 2.3 公里)超标区域预判准确率:89%应急决策提前:14 小时(如某次跨界传输预警)1.3 长三角跨界生态数据协同(16 市联动)Java 实现的跨区域数据同步与可视化:协同效果:跨界污染协商时间:从 3 天缩至 4 小时联合减排执行率:从 62% 提升至 91%2023 年长三角重污染天数:比 2022 年减少 21 天二、Java 驱动的全场景决策支持(含移动端与 AI 预警)2.1 移动端轻量化巡检系统(县城河道巡检案例)某县城用 Java 开发的手机端看板:功能:实时显示河道 pH 值、沿岸噪声(大字体 + 红黄绿标识)操作:拍照上传异常点,系统自动定位经纬度效果:巡检效率提升 2.3 倍,漏检率从 31% 降至 4%2.2 不同层级城市的应用差异城市层级 核心功能 Java 技术适配 生态效果县域 手机端巡检 + 3 类数据监测 轻量化代码 + 本地缓存 山洪预警提前 2 小时地级市 多源融合 + AI 扩散预测 分布式处理 + LSTM 模型 污染控制面积扩 1.8 倍城市群 跨界传输图谱 + 协同决策 跨区数据网关 + 共享看板 联合减排效率升 91%三、实战案例:从县城到城市群的生态守护3.1 陕西某县:轻量化系统的山洪预警痛点:硬件老旧,监测覆盖率 52%,山洪预警滞后Java 方案:8.7MB 轻量化系统 + 手机端看板,保留核心数据结果:覆盖率 98%,暴雨前 2 小时预警,转移 23 户村民3.2 长三角:跨界传输图谱的协同减排痛点:PM2.5 跨界传输责任难划,协商低效方案:Java 跨区网关 + 扩散预测,生成 “传输路径图”结果:重污染天数减 21 天,联合减排执行率 91%原文链接:https://blog.csdn.net/atgfg/article/details/155643096
-
1.概述Redis是一个基于内存的数据库,这意味着其主要数据存储和操作均在内存中进行。这种设计使得Redis能够提供极快的读写速度(通常达到微秒级别),适用于高性能场景,如缓存然而,由于内存的易失性(断电后数据会丢失),Redis提供了持久化机制:将内存中的数据保存到磁盘中,确保数据在Redis服务重启或崩溃后能够恢复。通过持久化,可以避免数据丢失,提高数据的可靠性Redis提供两种持久化方式RDB(Redis Database):生成数据集的快照实现持久化AOF(Append Only File):记录所有写操作命令,以追加方式写入文件2.RDBRDB指的是Redis的一种持久化机制,其核心是生成Redis数据在某个时间点的快照2.1 快照原理由于Redis是单线程应用程序,在线上环境时,不仅要处理来自客户端的请求,还要执行内存快照操作(进行文件IO)。单线程同时处理客户端请求和文件IO时会严重降低服务器性能,甚至阻塞客户端请求。因此,Redis使用 fork 和 写实拷贝(Copy On Write) 机制来实现快照持久化fork Redis在进行RDB持久化时会调用fork函数来创建一个子进程负责完成,父进程则继续处理客户端请求。子进程在创建之初和父进程共享同一数据段 Linux操作系统的内存空间被分为很多种片段,每个片段又被分为很多个页面,每个页面4KB写实拷贝 当父进程对数据段中的某一数据页面进行修改操作时,Linux操作系统会将该数据页面复制一份分离出来,然后对该页面进行修改,最后父进程指向指向修改后的页面。随着被修改的页面越来越多,内存空间不断膨胀,最多达到原来的两倍 从子进程被创建出来的那一刻起,直至拷贝结束,子进程始终指向原始的数据段且所有原数据段不会被修改。所以,在整个拷贝过程中 RDB快照 = 子进程看到的所有数据页面的瞬间状态集合 拷贝完成后,子进程会被销毁,同时没有指针指向的数据页面也会被销毁2.2 触发机制Redis RDB的触发机制分为自动触发和手动触发两种方式自动触发在redis.conf中通过save指令配置阈值。当在指定时间内发生足够数量的键修改时自动触发bgsave正常关闭Redis# 默认执行save(阻塞式)> shutdown# 或> shutdown save# 触发流程:1. 停止接受新连接2. 执行save(不是bgsave)3. 保存完成后退出AI写代码bash手动触发save命令:同步阻塞式触发,执行期间Redis服务器不处理任何请求,直到RDB文件创建完成(不推荐)bgsave命令:异步非阻塞式触发,Redis会fork一个子进程执行持久化操作,主进程继续处理请求2.3 文件处理 RDB文件保存在dir配置指定的目录下(默认/var/lib/redis),文件名通过dbfilename配置指定(默认dump.sql) 在RDB备份过程中,fork出的子进程会将内存数据写入临时文件,临时文件默认命名规则为temp-< pid >.rdb,其中< pid >是子进程的进程ID。当子进程完成RDB文件写入后,Redis会用原子性的rename操作将临时文件重命名为正式RDB文件并删除原文件2.4 优缺点优点恢复速度快:RDB是数据的二进制快照,恢复时直接加载到内存备份时对服务影响小:使用bgsave命令时,Redis通过fork子进程在后台保存数据,主进程可以继续处理客户端请求,几乎无阻塞存储高效:RDB 文件使用二进制格式并支持LZF压缩缺点非实时一致性:RDB保存的是某个瞬间的快照,如果保存过程中有大量写入,快照可能不反映完全一致的业务状态可能丢失更多数据:如果Redis意外宕机,从上一次RDB保存到宕机之间的所有数据修改都会丢失3.AOFAOF持久化通过将Redis服务器接收到的每个写命令追加到文件末尾来实现# 开启AOFappendonly yesAI写代码bash3.1 工作流程命令追加:当Redis执行写命令时,该命令会以Redis协议格式追加到内存中的AOF缓冲区(aof_buf)。缓冲区会根据配置策略决定何时将内容同步到磁盘文件写入与同步:AOF缓冲区内容会被写入到AOF文件,具体同步到磁盘的时机由appendfsync参数控制:always:每次写命令后同步,数据安全性最高但性能影响较大everysec:每秒同步一次,平衡性能与安全性(默认配置)no:由操作系统决定同步时机,性能最好但可能丢失较多数据3.2 重写机制作用:解决AOF文件不断增长导致的存储空间占用和恢复效率问题。通过重写,可以生成一个更紧凑的AOF文件,仅包含重建当前数据集所需的最小命令集合(例如,对同一个键多次修改会记录多条命令,而重写机制会合并这些操作,仅保留最终状态的命令) 父进程通过fork创建一个子进程来完成AOF文件的重写,确保主进程继续处理客户端请求。子进程会读取当前数据库的快照数据,并将其转换为一系列Redis命令写入新的临时AOF文件 在重写过程中,主进程会将新接收到的写命令同时写入现有的AOF 缓冲区aof_buf(保证原有 AOF 文件正常更新)和AOF重写缓冲区aof_rewrite_buf(保证新命令不会丢失) 当子进程完成重写后,会通知主进程。主进程会将 AOF 重写缓冲区中的命令追加到新生成的临时 AOF 文件中,最后原子性地替换旧文件 在Redis7.0.15版本,AOF文件保存在dir + appenddirname配置指定的目录下(默认/var/lib/redis/appendonlydir)。文件前缀名通过appendfilename配置指定(默认appendonly)appendonly.aof.1.base.rdb:作为Redis AOF(Append-Only File)持久化机制的基准文件,存储某一时刻数据库的完整快照。格式为RDB,体积较小且加载速度快,用于重建数据的基础状态appendonly.aof.1.incr.aof和appendonly.aof.2.incr.aof:记录基准文件生成后的增量写操作命令,以文本形式追加存储。多个增量文件按操作顺序编号(如.1.incr.aof、.2.incr.aof),Redis 重启时会按顺序重放这些命令以恢复最新数据appendonly.aof.manifest:描述AOF文件的组成和顺序的清单文件4.混合持久化 Redis 混合持久化结合了 RDB(快照)和 AOF(日志)两种持久化方式的优势,在保证数据安全性的同时兼顾性能# 开启混合持久化aof-use-rdb-preamble yesAI写代码bash基础RDB文件优先加载:appendonly.aof.1.base.rdb作为全量快照数据文件,会优先被加载。该文件包含某一时间点的完整数据快照,恢复时作为基准数据集增量AOF文件后续应用:appendonly.aof.1.incr.aof作为增量操作日志,在基础RDB加载完成后被重放。该文件记录自 RDB 快照生成后的所有写操作,用于恢复最新数据状态原文链接:https://blog.csdn.net/2401_89167985/article/details/147985399
-
1. 引入依赖首先,需要确认项目中是否直接或者间接引入过spring-web依赖,如果没有引入过,需要在pom.xml中添加以下代码引入依赖:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.3.24.RELEASE</version></dependency>2. 发送GET请求使用RestTemplate发送GET请求主要有getForObject()和getForEntity()2个方法,每个方法分别提供了3种不同的重载。2.1 使用getForObject发送GET请求(无参数)使用getForObject()实现:RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getCurrentEnv";String response = restTemplate.getForObject(url, String.class);System.out.println(response);假设以上接口返回的报文为:{ "serverAddress": "www.example.dev.com", "env": "dev"}也可以直接解析为自定义的类型:import lombok.Getter;import lombok.Setter;@Getter@Setterpublic class EnvInfo { private String serverAddress; private String env;}RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getCurrentEnv";EnvInfo response = restTemplate.getForObject(url, EnvInfo.class);System.out.println(JSON.toJSONString(response));2.2 使用getForEntity发送GET请求(无参数)也可以使用getForEntity()实现和2.1同样的功能,代码如下所示:RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getCurrentEnv";ResponseEntity<EnvInfo> responseEntity = restTemplate.getForEntity(url, EnvInfo.class);if (responseEntity.getStatusCode() == HttpStatus.OK) { EnvInfo response = responseEntity.getBody(); System.out.println(JSON.toJSONString(response));}2.3 使用getForObject发送GET请求(带参数)第一种方法是直接在url上拼接上参数,如下所示:RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getDataList?pageIndex=1&pageSize=20";EnvInfo response = restTemplate.getForObject(url, EnvInfo.class);System.out.println(JSON.toJSONString(response));第二种方法是使用占位符添加参数,如下所示:RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getDataList?pageIndex={1}&pageSize={2}";EnvInfo response = restTemplate.getForObject(url, EnvInfo.class, 1, 20);System.out.println(JSON.toJSONString(response));以上代码也可以替换为:RestTemplate restTemplate = new RestTemplate();String url = "https://www.example.com/getDataList?pageIndex={pageIndex}&pageSize={pageSize}";Map<String, String> uriVariables = new HashMap<>();uriVariables.put("pageIndex", "1");uriVariables.put("pageSize", "20");EnvInfo response = restTemplate.getForObject(url, EnvInfo.class, uriVariables);System.out.println(JSON.toJSONString(response));注意事项:uriVariables中的key必须和url中的占位符名称一致,否则会抛出异常:java.lang.IllegalArgumentException: Map has no value for 'pageIndex'第三种方法是使用UriComponentsBuilder添加参数,该种方法相比于前两种方法更加灵活,可以实现动态添加参数,代码如下所示:RestTemplate restTemplate = new RestTemplate();String httpUrl = "https://www.example.com/getDataList";UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl);uriComponentsBuilder.queryParam("pageIndex", 1);uriComponentsBuilder.queryParam("pageSize", 20);String url = uriComponentsBuilder.toUriString();EnvInfo response = restTemplate.getForObject(url, EnvInfo.class);System.out.println(JSON.toJSONString(response));2.4 使用getForEntity发送GET请求(带参数)也可以使用getForEntity()实现和2.3同样的功能,代码如下所示:RestTemplate restTemplate = new RestTemplate();String httpUrl = "https://www.example.com/getDataList";UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl);uriComponentsBuilder.queryParam("pageIndex", 1);uriComponentsBuilder.queryParam("pageSize", 20);String url = uriComponentsBuilder.toUriString();ResponseEntity<EnvInfo> responseEntity = restTemplate.getForEntity(url, EnvInfo.class);if (responseEntity.getStatusCode() == HttpStatus.OK) { EnvInfo response = responseEntity.getBody(); System.out.println(JSON.toJSONString(response));}2.5 getForObject与getForEntity的区别getForEntity()与getForObject()相比,返回值用了ResponseEntity进行封装,可以多获取到以下2种信息:HTTP状态码Response Headers代码示例:RestTemplate restTemplate = new RestTemplate();String httpUrl = "https://www.example.com/getDataList";UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl);uriComponentsBuilder.queryParam("pageIndex", 1);uriComponentsBuilder.queryParam("pageSize", 20);String url = uriComponentsBuilder.toUriString();ResponseEntity<EnvInfo> responseEntity = restTemplate.getForEntity(url, EnvInfo.class);System.out.println("statusCode: " + responseEntity.getStatusCode().toString());System.out.println("statusCodeValue: " + responseEntity.getStatusCodeValue());responseEntity.getHeaders().forEach((key, values) -> { System.out.println(key + ": " + values);});输出结果:statusCode: 200statusCodeValue: 200Server: [openresty]Date: [Thu, 10 Apr 2025 05:39:02 GMT]Content-Type: [application/json]Transfer-Encoding: [chunked]Connection: [keep-alive]其中Response Headers输出部分和Chrome浏览器Network中的Response Headers是一致的:2.6 发送GET请求(带参数及请求头)一般情况下,请求第三方接口都需要签名、时间戳等请求头,但getForObject()和getForEntity()都不支持,此时需要使用exchange()方法,代码如下所示:RestTemplate restTemplate = new RestTemplate();String httpUrl = "https://www.example.com/getDataList";UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(httpUrl);uriComponentsBuilder.queryParam("pageIndex", 1);uriComponentsBuilder.queryParam("pageSize", 20);HttpHeaders headers = new HttpHeaders();headers.set("signature", "3045022100875efcef9eb54626bb0168a6baa7c61265d0001d49243f");headers.set("timestamp", String.valueOf(System.currentTimeMillis()));String url = uriComponentsBuilder.toUriString();ResponseEntity<EnvInfo> responseEntity = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), EnvInfo.class);if (responseEntity.getStatusCode() == HttpStatus.OK) { EnvInfo response = responseEntity.getBody(); System.out.println(JSON.toJSONString(response));}3. 发送POST请求使用RestTemplate发送POST请求主要有postForObject()和postForEntity()2个方法,每个方法分别提供了3种不同的重载。3.1 发送POST请求(带参数、json方式)使用postForObject()实现:RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);LoginParams loginParams = new LoginParams();loginParams.setUsername("zhangsan");loginParams.setPassword("123456");HttpEntity<LoginParams> request = new HttpEntity<>(loginParams, headers);String url = "https://www.example.com/login";String response = restTemplate.postForObject(url, request, String.class);System.out.println(response);LoginParams的定义如下所示:import lombok.Getter;import lombok.Setter;@Getter@Setterpublic class LoginParams { private String username; private String password;}假设以上接口返回的报文为:{ "code": 200, "expire": "2025-04-11 14:42:22", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NDQzNTM3NDIsImlkZW50aXR5"}也可以直接解析为自定义的类型:import lombok.Getter;import lombok.Setter;@Getter@Setterpublic class LoginResponse { private Integer code; private String expire; private String token;}RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON_UTF8);LoginParams loginParams = new LoginParams();loginParams.setUsername("zhangsan");loginParams.setPassword("123456");HttpEntity<LoginParams> request = new HttpEntity<>(loginParams, headers);String url = "https://www.example.com/login";LoginResponse response = restTemplate.postForObject(url, request, LoginResponse.class);System.out.println(JSON.toJSONString(response));也可以使用postForEntity()实现同样的功能,代码如下所示:ResponseEntity<LoginResponse> responseEntity = restTemplate.postForEntity(url, request, LoginResponse.class);if (responseEntity.getStatusCode() == HttpStatus.OK) { LoginResponse response = responseEntity.getBody(); System.out.println(JSON.toJSONString(response));}3.2 发送POST请求(带参数、form表单方式)使用postForObject()实现:RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);MultiValueMap<String, String> map = new LinkedMultiValueMap<>();map.add("username", "zhangsan");map.add("password", "123456");HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);String url = "https://www.example.com/login";LoginResponse response = restTemplate.postForObject(url, request, LoginResponse.class);System.out.println(JSON.toJSONString(response));也可以使用postForEntity()实现同样的功能,代码如下所示:ResponseEntity<LoginResponse> responseEntity = restTemplate.postForEntity(url, request, LoginResponse.class);if (responseEntity.getStatusCode() == HttpStatus.OK) { LoginResponse response = responseEntity.getBody(); System.out.println(JSON.toJSONString(response));}3.3 postForObject与postForEntity的区别postForObject()与postForEntity()的区别,与getForEntity()与getForObject()的区别一样,返回值用了ResponseEntity进行封装。4. 超时时间设置如果需要自定义HTTP请求的连接超时时间和数据传输超时时间,代码如下所示:import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.web.client.RestTemplate;@Configurationpublic class RestTemplateConfig { @Value("${restTemplate.connectTimeout:5000}") private int connectTimeout; @Value("${restTemplate.readTimeout:10000}") private int readTimeout; @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory(); simpleClientHttpRequestFactory.setConnectTimeout(connectTimeout); simpleClientHttpRequestFactory.setReadTimeout(readTimeout); return new RestTemplate(simpleClientHttpRequestFactory); }}转载自https://www.cnblogs.com/zwwhnly/p/18820159
-
1. 前言最近在自测接口时,发现一个问题:字段类型定义的是Date,但接口返回值里却是时间戳(1744959978674),而不是预期的2025-04-18 15:06:18。private Date useTime;{ "code": "200", "message": "", "result": [ { "id": 93817601, "useTime": 1744959978674 } ]}这种返回值,无法快速的知道是哪个时间,如果想知道时间对不对,还得找一个时间戳转换工具做下转换才能确定,非常不方便。因此想让接口直接返回预期的2025-04-18 15:06:18格式。刚开始,在字段上添加了@JsonFormat注解,发现没生效,返回的还是时间戳:import com.fasterxml.jackson.annotation.JsonFormat;@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")private Date useTime;然后,改成了@JSONField注解,发现生效了,达到了预期的结果:import com.alibaba.fastjson.annotation.JSONField;@JSONField(format = "yyyy-MM-dd HH:mm:ss")private Date useTime;{ "code": "200", "message": "", "result": [ { "id": 93817601, "useTime": "2025-04-18 15:06:18" } ]}那么问题来了,为啥@JSONField生效,@JsonFormat不生效?2. 原因分析默认情况下,Spring Boot使用的JSON消息转换器是Jackson的MappingJackson2HttpMessageConverter,核心依赖为:<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.11.3</version></dependency>现在使用Jackson的@JsonFormat注解不生效,说明Spring Boot没有使用默认的MappingJackson2HttpMessageConverter。使用fastjson的@JSONField注解生效了,说明Spring Boot使用的是fastjson下的JSON消息转换器,也就是FastJsonHttpMessageConverter,依赖为:<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version></dependency>那么怎么找到代码在哪配置的呢?第一步,先在项目中全局搜索FastJsonHttpMessageConverter(Windows快捷键:Ctrl+Shift+F),不过大概率是搜索不到,因为公司里的项目一般都继承自公司公共的xxx-spring-boot-starter。第二步,连按2次Shift键搜索FastJsonHttpMessageConverter,然后查找该类的引用或者子类(子类很可能是公司底层框架中写的)。然后,很可能会找到类似下面的代码:@Configurationpublic class FastJsonMessageConverterConfig { @Bean public HttpMessageConverters customConverters() { FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); return new HttpMessageConverters(new HttpMessageConverter[]{fastJsonHttpMessageConverter}); }}以上代码显式注册了一个FastJsonHttpMessageConverter,并通过HttpMessageConverters覆盖了默认的HTTP 消息转换器(Jackson的MappingJackson2HttpMessageConverter),所以Spring MVC将只使用fastjson处理JSON序列化/反序列化。这也是@JSONField生效,@JsonFormat不生效的根本原因。3. 默认行为及全局配置fastjson 1.2.36及以上版本,默认将日期序列化为时间戳(如1744959978674),如果要默认将日期序列化为yyyy-MM-dd HH:mm:ss(如2025-04-18 15:06:18),需要启用WriteDateUseDateFormat特性:@Configurationpublic class FastJsonMessageConverterConfig { @Bean public HttpMessageConverters customConverters() { FastJsonConfig fastJsonConfig = new FastJsonConfig(); // 启用日期格式化特性(禁用时间戳) fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat); // 设置日期格式(不指定时,默认为yyyy-MM-dd HH:mm:ss,但即使与默认值一致,也建议明确指定) fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); return new HttpMessageConverters(new HttpMessageConverter[]{fastJsonHttpMessageConverter}); }}如果某个日期字段有特殊序列化要求,可以使用@JSONField注解灵活配置(该注解会覆盖全局配置):import com.alibaba.fastjson.annotation.JSONField;@JSONField(format = "yyyy-MM-dd")private Date anotherUseTime;注意事项:修改全局配置需慎重,如果一个老项目,原来日期类型返回的都是时间戳,突然全部改为返回字符串,可能会造成调用方报错。转载自https://www.cnblogs.com/zwwhnly/p/18838062
-
1. 踩坑经历假设有这样一个业务场景,需要对各个城市的订单量排序,排序规则为:先根据订单量倒序排列,再根据城市名称正序排列。示例代码:import lombok.Getter;import lombok.Setter;import lombok.ToString;@Getter@Setter@ToStringpublic class OrderStatisticsInfo { private String cityName; private Integer orderCount; public OrderStatisticsInfo(String cityName, Integer orderCount) { this.cityName = cityName; this.orderCount = orderCount; }}public static void main(String[] args) { List<OrderStatisticsInfo> orderStatisticsInfoList = Arrays.asList( new OrderStatisticsInfo("上海", 1000), new OrderStatisticsInfo("北京", 1000), new OrderStatisticsInfo("成都", 700), new OrderStatisticsInfo("常州", 700), new OrderStatisticsInfo("广州", 900), new OrderStatisticsInfo("深圳", 800) ); orderStatisticsInfoList.sort(Comparator.comparing(OrderStatisticsInfo::getOrderCount, Comparator.reverseOrder()) .thenComparing(OrderStatisticsInfo::getCityName)); orderStatisticsInfoList.forEach(System.out::println);}预期结果:北京 1000上海 1000广州 900深圳 800常州 700成都 700实际结果:OrderStatisticsInfo(cityName=上海, orderCount=1000)OrderStatisticsInfo(cityName=北京, orderCount=1000)OrderStatisticsInfo(cityName=广州, orderCount=900)OrderStatisticsInfo(cityName=深圳, orderCount=800)OrderStatisticsInfo(cityName=常州, orderCount=700)OrderStatisticsInfo(cityName=成都, orderCount=700)从以上结果可以看出,根据订单量倒序排列没啥问题,但根据城市名称正序排列不符合预期:上海竟然排到了北京的前面,但常州与成都的顺序又是对的。2. 原因分析Comparator.comparing对字符串类型进行排序时,默认使用的是字符串的自然排序,即String的compareTo方法,该方法是基于Unicode编码值进行比较的,未考虑语言特定的字符顺序(如中文拼音)。先看下String的compareTo方法的源码:public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2;}以上海与北京的比较为例,先比较第一个字符,也就是字符上和字符北,字符上对应的Unicode编码值是19978,因此c1 = 19978,字符北对应的Unicode编码值是21271,因此c2 = 21271,因为c1 != c2,所以返回值为-1293,也就是说上海小于北京(要排在北京的前面),不符合预期。以常州与成都的比较为例,先比较第一个字符,也就是字符常和字符成,字符常对应的Unicode编码值是24120,因此c1 = 24120,字符成对应的Unicode编码值是25104,因此c2 = 25104,因为c1 != c2,所以返回值为-984,也就是说常州小于成都(要排在成都的前面),符合预期。可以通过Character.codePointAt方法获取字符的Unicode编码值:// 输出:19978System.out.println(Character.codePointAt("上海", 0));// 输出:21271System.out.println(Character.codePointAt("北京", 0));// 输出:24120System.out.println(Character.codePointAt("常州", 0));// 输出:25104System.out.println(Character.codePointAt("成都", 0));3. 解决方案Java提供了本地化的排序规则,可以按特定语言规则排序(如中文拼音),代码如下所示:orderStatisticsInfoList.sort(Comparator.comparing(OrderStatisticsInfo::getOrderCount, Comparator.reverseOrder()) .thenComparing(OrderStatisticsInfo::getCityName, Collator.getInstance(Locale.CHINA)));orderStatisticsInfoList.forEach(System.out::println);此时的输出结果为:OrderStatisticsInfo(cityName=北京, orderCount=1000)OrderStatisticsInfo(cityName=上海, orderCount=1000)OrderStatisticsInfo(cityName=广州, orderCount=900)OrderStatisticsInfo(cityName=深圳, orderCount=800)OrderStatisticsInfo(cityName=常州, orderCount=700)OrderStatisticsInfo(cityName=成都, orderCount=700)可以看到,北京排到了上海的前面,符合预期。上述代码指定了Collator.getInstance(Locale.CHINA),在排序比较时不再执行String的compareTo方法,而是执行Collator的compare方法,实际上是RuleBasedCollator的compare方法。可以执行以下代码单独看下上海与北京的比较结果:Collator collator = Collator.getInstance(Locale.CHINA);// 输出:1,代表上海大于北京,也就是要排在北京的后面System.out.println(collator.compare("上海", "北京"));转载自https://www.cnblogs.com/zwwhnly/p/18846441
-
1. 前言在我接触过的大部分Java项目中,经常看到使用@Autowired注解进行字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService;}在IDEA中,以上代码@Autowired注解下会显示波浪线,鼠标悬停后提示:Field injection is not recommended,翻译过来就是不建议使用字段注入。关于该提示问题,有直接修改IDEA设置关闭该提示的,有替换为使用@Resource注解的,但这都不是该问题的本质。该问题的本质是Spring官方推荐使用构造器注入,IDEA作为一款智能化的IDE,针对该项进行了检测并给以提示。所以该提示背后的本质问题是:为什么Spring官方推荐构造器注入而不是字段注入?2. 推荐构造器注入的理由相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题2.1 支持不可变性构造器注入允许将依赖字段声明为final,确保对象一旦创建,其依赖关系不再被修改。字段注入无法使用final,依赖可能在对象生命周期中被意外修改,破坏状态一致性。构造器注入示例:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; @Autowired public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; }}说明:如果Spring版本是4.3或者更高版本且只有一个构造器,构造器上的@Autowired注解可以省略。2.2 依赖明确构造器注入通过在类的构造函数中显式声明依赖,并且强制要求在创建对象时必须提供所有必须的依赖项,通过构造函数参数,使用者对该类的依赖一目了然。字段注入通过在类的字段上直接使用@Autowired注解注入依赖,依赖关系隐藏在类的内部,使用者无法直接看到该类的依赖。2.3 单元测试友好构造器注入允许直接通过new创建对象,无需依赖Spring容器或反射,降低了测试复杂度。字段注入需要依赖Spring容器或反射,增加了测试复杂度。2.4 循环依赖检测前置,提前暴露问题构造器注入在应用启动时直接暴露循环依赖,强制开发者通过设计解决问题。字段注入在应用启动时不会暴露循环依赖,直到实际调用时才可能暴露问题,增加调试难度。示例:假设项目中有以下两个Service存在循环依赖:import org.springframework.stereotype.Service;@Servicepublic class OrderService { private final PaymentService paymentService; public OrderService(PaymentService paymentService) { this.paymentService = paymentService; }}import org.springframework.stereotype.Service;@Servicepublic class PaymentService { private final OrderService orderService; public PaymentService(OrderService orderService) { this.orderService = orderService; }}此时启动项目会报错,抛出org.springframework.beans.factory.BeanCurrentlyInCreationException异常,大致的异常信息如下所示:Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference?将以上两个Service修改为字段注入:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class OrderService { @Autowired private PaymentService paymentService;}import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class PaymentService { @Autowired private OrderService orderService;}此时启动项目不会报错,可以启动成功。3. @RequiredArgsConstructor注解的使用及原理为了避免样板化代码或者为了简化代码,有的项目中可能会使用@RequiredArgsConstructor注解来代替显式的构造方法:import lombok.RequiredArgsConstructor;import org.springframework.stereotype.Service;@RequiredArgsConstructor@Servicepublic class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService;}接下来简单讲解下@RequiredArgsConstructor注解的原理。@RequiredArgsConstructor注解用于在编译时自动生成包含特定字段的构造方法。字段筛选逻辑如下所示:被final修饰的未显式初始化的非静态字段被@NonNull注解标记的未显式初始化的非静态字段示例:import lombok.NonNull;import lombok.RequiredArgsConstructor;@RequiredArgsConstructorpublic class User { private final String name; @NonNull private Integer age; private final String address = ""; private String email; private static String city; @NonNull private String sex = "男";}以上代码在编译时自动生成的构造方法如下所示:public User(String name, @NonNull Integer age) { if (age == null) { throw new NullPointerException("age is marked non-null but is null"); } else { this.name = name; this.age = age; }}从生成的构造方法可以看出:1)如果字段被lombok.NonNull注解标记,在生成的构造方法内会做null值检查。2)address字段虽然被final修饰,但因为已初始化,所以未包含在构造方法中。3)email字段既没被final修饰,也没被lombok.NonNull注解标记,所以未包含在构造方法中。4)city字段是静态字段,所以未包含在构造方法中。5)sex字段虽然被lombok.NonNull注解标记,但因为已初始化,所以未包含在构造方法中。4. 总结@Autowired注解在IDEA中提示:Field injection is not recommended,其背后的本质问题是:Spring官方推荐构造器注入而不是字段注入。而Spring官方推荐构造器注入,是因为相比字段注入,构造器注入有以下几个优点:支持不可变性依赖明确单元测试友好循环依赖检测前置,提前暴露问题使用构造器注入时,为了避免样板化代码或者为了简化代码,可以使用@RequiredArgsConstructor注解来代替显式的构造方法,因为@RequiredArgsConstructor注解可以在编译时自动生成包含特定字段的构造方法。至于项目中要不要使用构造器注入,使用显式的构造方法还是使用@RequiredArgsConstructor注解来简化代码,可以根据个人喜好及团队规范自行决定。转载自https://www.cnblogs.com/zwwhnly/p/18907966
-
在日常的开发工作中,我们经常使用到Java Stream,特别是Stream API中提供的Collectors.toList()收集器,但有些场景下,我们需要将集合转换为Map,这时候就需要使用到Stream API中提供的另一个收集器:Collectors.toMap,它可以将流中的元素映射为键值对,并收集到一个Map中。1. 三种主要的重载方法Collectors.toMap有3种重载方法,分别是:1)两个参数的重载方法(最简单的形式)public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);}2)三个参数的重载方法(包含冲突处理)public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);}3)四个参数的重载方法(指定Map实现)public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) { BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);}接下来,我们结合使用示例详细讲解。2. 使用示例2.1 将对象的某些属性转换为Map假设有一个城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市名称,转换方法如下所示:@Getter@Setterpublic class City { private Integer cityId; private String cityName; public City(Integer cityId, String cityName) { this.cityId = cityId; this.cityName = cityName; }}List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);输出结果: 2.2 将对象列表转换为Map(ID -> 对象)仍然使用上面的城市列表,需要将其转换为Map,其中Key为城市ID、Value为城市对象,转换方法如下所示:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"));Map<Integer, City> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, city -> city));City city = cityMap.get(1);System.out.println("城市ID: " + city.getCityId());System.out.println("城市名称: " + city.getCityName());输出结果如下所示:城市ID: 1城市名称: 北京上面的写法等价于:Map<Integer, City> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, Function.identity()));因为Function.identity()内部实现是下面这样的:static <T> Function<T, T> identity() { return t -> t;}2.3 键冲突处理假设上面的城市列表中有一个ID重复的城市:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(4, "天津"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println("城市ID: 4, 城市名称: " + cityMap.get(4));此时运行代码,会抛出java.lang.IllegalStateException异常,如下图所示:有3种常见的键冲突处理方式,分别是保留旧值、使用新值和合并值,接下来一一讲解。1)方式一:保留旧值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue));输出结果:城市ID: 4, 城市名称: 深圳2)方式二:使用新值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> newValue));输出结果:城市ID: 4, 城市名称: 天津3)方式三:合并值Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (oldValue, newValue) -> oldValue + ", " + newValue));输出结果:城市ID: 4, 城市名称: 深圳, 天津2.4 数据分组聚合假设有一个销售记录列表,需要将其转换为Map,其中Key为销售员、Value为该销售员的总销售额,转换方法如下所示:@Getter@Setterpublic class SalesRecord { private String salesPerson; private BigDecimal amount; public SalesRecord(String salesPerson, BigDecimal amount) { this.salesPerson = salesPerson; this.amount = amount; }}List<SalesRecord> salesRecordList = Arrays.asList( new SalesRecord("张三", new BigDecimal("1000")), new SalesRecord("李四", new BigDecimal("2000")), new SalesRecord("张三", new BigDecimal("980")));Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::add));System.out.println(salesRecordMap);输出结果: 上面的例子是销售额累加,也可以只取最小值:Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::min));此时的输出结果: 或者只取最大值:Map<String, BigDecimal> salesRecordMap = salesRecordList.stream() .collect(Collectors.toMap(SalesRecord::getSalesPerson, SalesRecord::getAmount, BigDecimal::max));此时的输出结果: 2.5 指定Map实现默认情况下,Collectors.toMap是将结果收集到HashMap中,如果有需要,我们也可以指定成TreeMap或者LinkedHashMap。如果想要保持插入顺序,可以指定使用LinkedHashMap:List<City> cityList = Arrays.asList( new City(2, "上海"), new City(1, "北京"), new City(4, "深圳"), new City(3, "广州"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (existing, replacement) -> existing, LinkedHashMap::new));System.out.println(cityMap);输出结果: 如果想要按键排序,可以指定使用TreeMap:List<City> cityList = Arrays.asList( new City(2, "上海"), new City(1, "北京"), new City(4, "深圳"), new City(3, "广州"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName, (existing, replacement) -> existing, TreeMap::new));System.out.println(cityMap);输出结果: 3. 注意事项3.1 空异常如果valueMapper中取出的值有null值,会抛出java.lang.NullPointerException异常,如下示例:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(5, null));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);运行以上代码会抛出异常,如下图所示:有两种解决方案,第一种解决方案是过滤null值:Map<Integer, String> cityMap = cityList.stream() .filter(city -> city.getCityName() != null) .collect(Collectors.toMap(City::getCityId, City::getCityName));第二种解决方案是提供默认值:Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, city -> Optional.ofNullable(city.getCityName()).orElse("未知")));3.2 键重复异常如果出现重复键,且没有提供mergeFunction参数,会抛出java.lang.IllegalStateException异常,如下示例:List<City> cityList = Arrays.asList( new City(1, "北京"), new City(2, "上海"), new City(3, "广州"), new City(4, "深圳"), new City(4, "天津"));Map<Integer, String> cityMap = cityList.stream() .collect(Collectors.toMap(City::getCityId, City::getCityName));System.out.println(cityMap);运行以上代码会抛出异常,如下图所示:解决方案见本篇文章2.3 键冲突处理部分。4. 总结Collectors.toMap是Stream API中提供的一个非常方便的收集器,它可以将流中的元素映射为键值对,并收集到一个Map中。它适用于一对一映射的场景,但在使用时,要注意避免java.lang.NullPointerException异常和java.lang.IllegalStateException异常。转载自https://www.cnblogs.com/zwwhnly/p/19403765
-
Java 泛型的设计有个独特之处:类型信息只存在于编译期,运行时会被彻底擦除。这种 “擦除” 机制让很多开发者困惑:为什么List<String>和List<Integer>在运行时是同一个类型?为什么不能用基本类型作为泛型参数?为什么创建泛型数组会报错?今天我们就从泛型擦除的底层原理讲起,彻底搞懂这些问题,看清泛型的 “真面目”。一、泛型擦除:Java 泛型的 “编译期幻术” 泛型是 Java 5 引入的特性,但为了兼容之前的版本(Java 5 之前没有泛型),Java 采用了类型擦除(Type Erasure) 的实现方式:编译时检查泛型类型合法性,运行时擦除所有泛型信息。也就是说,泛型只在编译期起作用,运行时 JVM 根本不知道泛型参数的存在。1. 擦除的核心过程:从泛型到原始类型泛型擦除的本质是将泛型类型替换为其原始类型(Raw Type),具体规则:若泛型参数有上限(如<T extends Number>),则擦除为该上限类型;若泛型参数无上限(如<T>),则擦除为Object;若有多个上限(如<T extends A & B>),则擦除为第一个上限类型。示例:泛型类擦除前后对比// 泛型类定义public class Box<T extends Number> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; }} // 擦除后(编译为字节码的实际类型)public class Box { // 去掉泛型参数<T extends Number> private Number value; // T被替换为上限Number public Number getValue() { return value; } // 返回值类型变为Number public void setValue(Number value) { this.value = value; } // 参数类型变为Number}一键获取完整项目代码java2. 为什么需要擦除?—— 兼容性妥协 Java 5 之前的代码没有泛型,大量使用原始类型(如List而非List<String>)。为了让这些旧代码能与新的泛型代码无缝交互,Java 必须保证:泛型类在运行时的类型与非泛型类兼容。例如,Java 5 之前的List和 Java 5 之后的List<String>,在运行时必须是同一个类型(都是List.class),否则旧代码无法操作新的泛型集合。擦除机制正是为了实现这种兼容性。3. 擦除后的 “类型安全” 如何保证? 擦除会移除泛型信息,那运行时的类型安全怎么保证?答案是:编译器在擦除的同时,自动添加类型检查和转型代码。// 泛型代码List<String> list = new ArrayList<>();list.add("hello");String str = list.get(0); // 擦除后(编译器生成的实际代码)List list = new ArrayList();list.add("hello"); // 编译时检查:确保添加的是StringString str = (String) list.get(0); // 自动添加转型代码一键获取完整项目代码java编译期:检查add("hello")是否符合List<String>的类型约束,若添加123会直接报错;运行期:通过自动生成的(String)转型代码,保证取出的元素类型正确(若因特殊操作导致类型不匹配,仍会抛ClassCastException)。泛型擦除原理图解二、泛型擦除带来的限制:这些操作为什么不允许? 擦除机制虽然保证了兼容性,但也给泛型带来了诸多限制。理解这些限制的根源,才能避免开发中的 “坑”。限制 1:不能用基本类型作为泛型参数 你可能注意到,List<int>会编译报错,必须用List<Integer>。这是因为:泛型擦除后会替换为 Object 或上限类型,而基本类型(int、double 等)不是 Object 的子类,无法转型。若声明List<int>,擦除后应为List<Object>,但int是基本类型,不能直接存储在Object数组中(需要装箱为 Integer);编译器为了避免这种矛盾,直接禁止基本类型作为泛型参数,强制使用包装类(Integer、Double 等)。反例(编译报错):// 错误:基本类型不能作为泛型参数List<int> intList = new ArrayList<>(); // 编译报错Map<double, boolean> map = new HashMap<>(); // 编译报错 // 正确:使用包装类List<Integer> intList = new ArrayList<>();Map<Double, Boolean> map = new HashMap<>();一键获取完整项目代码java限制 2:不能实例化泛型类型(new T()) 无法在泛型类中直接创建泛型参数的实例(new T()),因为擦除后T会被替换为Object或上限类型,编译器无法确定具体类型。反例(编译报错):public class Box<T> { public Box() { // 错误:不能实例化泛型类型T T value = new T(); // 编译报错 }}一键获取完整项目代码java原因:擦除后T变为Object,new T()会被视为new Object(),这显然不符合预期(我们想要的是T的实例,而非 Object)。解决方案:通过反射创建实例(需传入 Class 对象):public class Box<T> { private T value; // 传入Class对象,通过反射创建实例 public Box(Class<T> clazz) throws InstantiationException, IllegalAccessException { value = clazz.newInstance(); // 合法 }} // 使用Box<String> box = new Box<>(String.class); // 需显式传入Class对象一键获取完整项目代码java限制 3:不能创建泛型数组(new T[]) 无法直接创建泛型数组(new T[10]),因为擦除后数组的实际类型是Object[],会导致类型安全问题。反例(编译报错):public class ArrayBox<T> { public void createArray() { // 错误:不能创建泛型数组 T[] array = new T[10]; // 编译报错 }}一键获取完整项目代码java原因:擦除后T[]变为Object[],若将其赋值给具体类型的数组(如String[]),再存入其他类型元素,会在运行时引发隐藏的ClassCastException:// 假设允许创建T[],擦除后实际为Object[]Object[] array = new Object[10];String[] strArray = (String[]) array; // 编译不报错(危险!)strArray[0] = 123; // 运行时抛ArrayStoreException(int不能存到String数组)一键获取完整项目代码java编译器为了避免这种隐藏的风险,直接禁止创建泛型数组。解决方案:用ArrayList<T>代替泛型数组(推荐,无需处理类型问题);创建Object[]数组,使用时手动转型(需谨慎,可能引发异常):public class ArrayBox<T> { private Object[] array; public ArrayBox(int size) { array = new Object[size]; // 创建Object数组 } public T get(int index) { return (T) array[index]; // 取出时转型 } public void set(int index, T value) { array[index] = value; // 存入时自动装箱 }}一键获取完整项目代码java限制 4:不能用instanceof判断泛型类型 instanceof是运行时类型检查,而泛型类型在运行时已被擦除,因此无法用instanceof判断泛型参数。反例(编译报错):List<String> list = new ArrayList<>();// 错误:不能用instanceof判断泛型类型if (list instanceof List<String>) { // 编译报错 // ...}一键获取完整项目代码java原因:运行时List<String>和List<Integer>都是List类型,instanceof无法区分。替代方案:若需判断集合元素类型,可通过泛型类的Class参数(需手动传入):public class GenericChecker<T> { private Class<T> clazz; public GenericChecker(Class<T> clazz) { this.clazz = clazz; } // 检查集合元素是否为T类型 public boolean check(List<?> list) { for (Object obj : list) { if (!clazz.isInstance(obj)) { return false; } } return true; }} // 使用GenericChecker<String> checker = new GenericChecker<>(String.class);List<Object> list = Arrays.asList("a", "b", 123);System.out.println(checker.check(list)); // false(包含Integer)一键获取完整项目代码java限制 5:静态变量 / 方法不能引用泛型类的类型参数 泛型类的类型参数是实例级别的(每个实例可以有不同的类型参数),而静态成员是类级别的(所有实例共享),因此静态变量 / 方法不能使用泛型类的类型参数。反例(编译报错):原因:擦除后泛型类的类型参数消失,静态成员无法关联到具体的类型参数(不同实例的T可能不同)。注意:静态泛型方法是允许的,因为它有自己的泛型参数(独立于类的类型参数):public class StaticBox<T> { // 正确:静态泛型方法有自己的类型参数S public static <S> S create(S obj) { return obj; }}一键获取完整项目代码java泛型限制图解三、泛型擦除的 “后遗症”:桥接方法(Bridge Method) 擦除会导致一个隐藏问题:泛型类的方法重写可能在擦除后变得不兼容。为了解决这个问题,编译器会自动生成桥接方法(Bridge Method)。桥接方法的产生场景假设有泛型父类和子类:// 泛型父类class Parent<T> { public void setValue(T value) {}} // 子类指定泛型参数为Stringclass Child extends Parent<String> { @Override public void setValue(String value) {} // 重写父类方法}一键获取完整项目代码java 擦除后,父类的setValue(T)变为setValue(Object),而子类的setValue(String)与父类的setValue(Object)参数类型不同(不满足重写条件)。这会导致多态失效:Parent<String> parent = new Child();parent.setValue("hello"); // 期望调用子类的setValue(String)一键获取完整项目代码java为了保证多态正确,编译器会为子类自动生成桥接方法:class Child extends Parent { // 编译器生成的桥接方法(重写父类的setValue(Object)) public void setValue(Object value) { setValue((String) value); // 调用子类实际的setValue(String) } // 子类自己的方法 public void setValue(String value) {}}一键获取完整项目代码java 桥接方法的作用是:在擦除后仍保持方法重写的多态性,确保父类引用调用方法时能正确指向子类实现。桥接方法验证通过反射可以看到编译器生成的桥接方法:import java.lang.reflect.Method; public class BridgeDemo { public static void main(String[] args) { for (Method method : Child.class.getMethods()) { if (method.getName().equals("setValue")) { System.out.println("方法:" + method); System.out.println("是否桥接方法:" + method.isBridge()); } } }} // 输出结果:// 方法:public void Child.setValue(java.lang.String)// 是否桥接方法:false// 方法:public void Child.setValue(java.lang.Object)// 是否桥接方法:true一键获取完整项目代码java 可以清晰看到,子类有两个setValue方法,其中setValue(Object)是桥接方法(isBridge()返回 true)。四、总结:理解擦除,用好泛型 泛型擦除是 Java 为了兼容性做出的妥协,它既带来了便利(兼容旧代码),也带来了限制(类型信息丢失)。核心要点:擦除原理:编译时检查泛型类型,运行时将泛型参数替换为上限或 Object,同时自动添加类型检查和转型代码。核心限制:不能用基本类型作为泛型参数(擦除后无法兼容 Object);不能实例化泛型类型(new T())和创建泛型数组(new T[]);不能用instanceof判断泛型类型(运行时无类型信息);静态成员不能引用泛型类的类型参数(静态与实例的级别冲突)。桥接方法:编译器自动生成,用于解决擦除后方法重写的多态性问题。 理解泛型擦除,不仅能避免开发中的常见错误,更能让你明白 Java 泛型的设计哲学 —— 在兼容性和类型安全之间寻找平衡。虽然泛型有诸多限制,但合理使用(结合通配符、反射等)仍能写出灵活且安全的代码。记住:泛型是编译期的 “语法糖”,运行时它的 “真面目” 是原始类型。————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152211655
-
在 JavaScript 中,将时间戳(通常为毫秒或秒的 Unix 时间戳)转换为日期格式是一个常见需求。可以通过原生 Date 对象、日期格式化方法或第三方库(如 Moment.js 或 date-fns)实现。以下是详细的中文讲解,介绍多种转换方法,包含代码示例、使用场景和注意事项。1. 时间戳简介定义:时间戳是以 1970-01-01 00:00:00 UTC(Unix 纪元)为起点的秒数或毫秒数。毫秒时间戳:如 1630454400000(毫秒,JavaScript 默认)。秒时间戳:如 1630454400(需乘以 1000 转为毫秒)。目标:将时间戳转换为可读格式,如 YYYY-MM-DD HH:mm:ss 或 2021-09-01 08:00:00。2. 转换方法方法 1:使用原生 Date 对象描述:通过 new Date(timestamp) 创建日期对象,再提取年月日等部分。适用场景:简单转换,适合不需要复杂格式化的场景。代码示例:// 毫秒时间戳const timestamp = 1630454400000; // 2021-09-01 00:00:00 UTCconst date = new Date(timestamp);// 提取日期部分const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需 +1const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');// 格式化const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;console.log(formattedDate); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:padStart(2, '0') 确保月份、日期等为两位数。使用 UTC 方法(如 getUTCFullYear)可避免时区影响:const utcDate = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;console.log(utcDate); // 输出: 2021-09-01运行项目并下载源码javascript运行方法 2:使用 toLocaleString()描述:Date 对象的 toLocaleString() 方法提供本地化格式,支持自定义选项。适用场景:需要本地化日期格式或简单格式化。代码示例:const timestamp = 1630454400000;const date = new Date(timestamp);// 默认本地化格式console.log(date.toLocaleString('zh-CN')); // 输出: 2021/9/1 08:00:00(中国时区)// 自定义格式const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false};console.log(date.toLocaleString('zh-CN', options)); // 输出: 2021-09-01 08:00:00运行项目并下载源码javascript运行说明:toLocaleString 根据地区(zh-CN、en-US)调整格式。options 参数支持灵活定制。注意:格式因浏览器和地区不同而异。方法 3:自定义格式化函数描述:编写函数根据指定格式(如 YYYY-MM-DD)转换时间戳。适用场景:需要统一、可控的日期格式。代码示例:function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { const date = new Date(timestamp); const map = { 'YYYY': date.getFullYear(), 'MM': String(date.getMonth() + 1).padStart(2, '0'), 'DD': String(date.getDate()).padStart(2, '0'), 'HH': String(date.getHours()).padStart(2, '0'), 'mm': String(date.getMinutes()).padStart(2, '0'), 'ss': String(date.getSeconds()).padStart(2, '0') }; return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);}const timestamp = 1630454400000;console.log(formatDate(timestamp)); // 输出: 2021-09-01 00:00:00console.log(formatDate(timestamp, 'YYYY/MM/DD')); // 输出: 2021/09/01运行项目并下载源码javascript运行说明:支持自定义格式,灵活性高。可扩展支持更多格式(如 YYYY年MM月DD日)。方法 4:使用 Moment.js 库描述:Moment.js 是一个强大的日期处理库,支持丰富的格式化选项。适用场景:复杂日期操作或需要兼容旧项目。代码示例:// HTML: <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>const timestamp = 1630454400000;const formatted = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00// 本地化moment.locale('zh-cn');console.log(moment(timestamp).format('LLL')); // 输出: 2021年9月1日 08:00运行项目并下载源码javascript运行说明:需要引入 Moment.js(CDN 或 NPM:npm install moment)。支持多种格式和本地化,但库体积较大(约 70KB 压缩版)。注意:Moment.js 已进入维护模式,推荐新项目使用 date-fns。方法 5:使用 date-fns 库描述:date-fns 是现代、轻量的日期处理库,模块化设计。适用场景:新项目,需轻量且现代化的日期处理。代码示例:// NPM: npm install date-fnsimport { format } from 'date-fns';const timestamp = 1630454400000;const formatted = format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:按需导入,体积小(仅导入所需函数)。支持丰富的格式化选项,类似 Moment.js 但更轻量。方法 6:使用 jQuery(结合 DOM)描述:结合 jQuery 从 DOM 获取时间戳并转换。适用场景:项目已使用 jQuery,需处理用户输入的时间戳。代码示例:// HTML: <input type="text" id="timestamp" value="1630454400000"><button onclick="format()">转换</button>$(document).ready(function() { window.format = function() { const timestamp = $('#timestamp').val(); const date = new Date(Number(timestamp)); const formatted = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; console.log(formatted); // 输出: 2021-09-01 };});运行项目并下载源码javascript运行说明:需引入 jQuery:<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>运行项目并下载源码html13. 综合示例以下是一个完整示例,展示多种转换方法:<!DOCTYPE html><html><head> <title>时间戳转换</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/date-fns@2.30.0/dist/date-fns.min.js"></script> <style> body { font-family: Arial; padding: 20px; } input, button { margin: 10px; padding: 8px; } </style></head><body> <input type="text" id="timestamp" value="1630454400000" placeholder="输入时间戳"> <button onclick="convert()">转换</button> <div id="output"></div> <script> function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { const date = new Date(timestamp); const map = { 'YYYY': date.getFullYear(), 'MM': String(date.getMonth() + 1).padStart(2, '0'), 'DD': String(date.getDate()).padStart(2, '0'), 'HH': String(date.getHours()).padStart(2, '0'), 'mm': String(date.getMinutes()).padStart(2, '0'), 'ss': String(date.getSeconds()).padStart(2, '0') }; return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]); } function convert() { const timestamp = Number(document.getElementById('timestamp').value); if (isNaN(timestamp)) { alert('请输入有效时间戳!'); return; } const date = new Date(timestamp); const output = ` <p>原生 Date: ${formatDate(timestamp)}</p> <p>toLocaleString: ${date.toLocaleString('zh-CN')}</p> <p>Moment.js: ${moment(timestamp).format('YYYY-MM-DD HH:mm:ss')}</p> <p>date-fns: ${format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')}</p> `; document.getElementById('output').innerHTML = output; } </script></body></html>运行项目并下载源码html4. 方法对比方法 依赖 优点 缺点原生 Date 无 无依赖,简单实现 需手动格式化,代码稍长toLocaleString() 无 本地化支持,灵活选项 格式因浏览器/地区不同自定义格式化 无 完全控制格式,灵活 需编写额外代码Moment.js Moment.js 功能强大,易用,支持本地化 体积大,维护模式date-fns date-fns 轻量,模块化,现代 需额外引入库jQuery jQuery 适合 jQuery 项目,简洁 DOM 操作 需引入 jQuery,增加依赖5. 注意事项时间戳单位:JavaScript Date 接受毫秒时间戳,秒时间戳需乘以 1000:const seconds = 1630454400;const date = new Date(seconds * 1000);运行项目并下载源码javascript运行12时区处理:默认使用本地时区,需用 UTC 方法(如 getUTCFullYear)处理 UTC 时间。库如 Moment.js/date-fns 支持时区插件。输入验证:检查时间戳是否有效:if (isNaN(timestamp) || timestamp < 0) { throw new Error('无效时间戳');}运行项目并下载源码javascript运行性能:原生方法最轻量,适合简单场景。Moment.js 体积大,date-fns 更适合新项目。浏览器兼容性:Date 和 toLocaleString 广泛支持。padStart 是 ES2017,IE 不支持,需 polyfill。安全性:用户输入时间戳需验证,防止异常值或恶意输入。6. 总结首选方法:原生 Date + 自定义格式化,简单无依赖。复杂场景:使用 date-fns(轻量现代)或 Moment.js(功能全面)。jQuery 项目:结合 jQuery 处理 DOM 输入。选择依据:无依赖:原生 Date 或 toLocaleString。复杂格式:自定义函数或 date-fns。本地化:toLocaleString 或 Moment.js。测试:验证不同时间戳(毫秒/秒)、时区和格式————————————————原文链接:https://blog.csdn.net/m0_57545130/article/details/152307313
-
在 JavaScript 中,将时间戳(通常为毫秒或秒的 Unix 时间戳)转换为日期格式是一个常见需求。可以通过原生 Date 对象、日期格式化方法或第三方库(如 Moment.js 或 date-fns)实现。以下是详细的中文讲解,介绍多种转换方法,包含代码示例、使用场景和注意事项。1. 时间戳简介定义:时间戳是以 1970-01-01 00:00:00 UTC(Unix 纪元)为起点的秒数或毫秒数。毫秒时间戳:如 1630454400000(毫秒,JavaScript 默认)。秒时间戳:如 1630454400(需乘以 1000 转为毫秒)。目标:将时间戳转换为可读格式,如 YYYY-MM-DD HH:mm:ss 或 2021-09-01 08:00:00。2. 转换方法方法 1:使用原生 Date 对象描述:通过 new Date(timestamp) 创建日期对象,再提取年月日等部分。适用场景:简单转换,适合不需要复杂格式化的场景。代码示例:// 毫秒时间戳const timestamp = 1630454400000; // 2021-09-01 00:00:00 UTCconst date = new Date(timestamp);// 提取日期部分const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需 +1const day = String(date.getDate()).padStart(2, '0');const hours = String(date.getHours()).padStart(2, '0');const minutes = String(date.getMinutes()).padStart(2, '0');const seconds = String(date.getSeconds()).padStart(2, '0');// 格式化const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;console.log(formattedDate); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:padStart(2, '0') 确保月份、日期等为两位数。使用 UTC 方法(如 getUTCFullYear)可避免时区影响:const utcDate = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;console.log(utcDate); // 输出: 2021-09-01运行项目并下载源码javascript运行方法 2:使用 toLocaleString()描述:Date 对象的 toLocaleString() 方法提供本地化格式,支持自定义选项。适用场景:需要本地化日期格式或简单格式化。代码示例:const timestamp = 1630454400000;const date = new Date(timestamp);// 默认本地化格式console.log(date.toLocaleString('zh-CN')); // 输出: 2021/9/1 08:00:00(中国时区)// 自定义格式const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false};console.log(date.toLocaleString('zh-CN', options)); // 输出: 2021-09-01 08:00:00运行项目并下载源码javascript运行说明:toLocaleString 根据地区(zh-CN、en-US)调整格式。options 参数支持灵活定制。注意:格式因浏览器和地区不同而异。方法 3:自定义格式化函数描述:编写函数根据指定格式(如 YYYY-MM-DD)转换时间戳。适用场景:需要统一、可控的日期格式。代码示例:function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { const date = new Date(timestamp); const map = { 'YYYY': date.getFullYear(), 'MM': String(date.getMonth() + 1).padStart(2, '0'), 'DD': String(date.getDate()).padStart(2, '0'), 'HH': String(date.getHours()).padStart(2, '0'), 'mm': String(date.getMinutes()).padStart(2, '0'), 'ss': String(date.getSeconds()).padStart(2, '0') }; return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);}const timestamp = 1630454400000;console.log(formatDate(timestamp)); // 输出: 2021-09-01 00:00:00console.log(formatDate(timestamp, 'YYYY/MM/DD')); // 输出: 2021/09/01运行项目并下载源码javascript运行说明:支持自定义格式,灵活性高。可扩展支持更多格式(如 YYYY年MM月DD日)。方法 4:使用 Moment.js 库描述:Moment.js 是一个强大的日期处理库,支持丰富的格式化选项。适用场景:复杂日期操作或需要兼容旧项目。代码示例:// HTML: <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>const timestamp = 1630454400000;const formatted = moment(timestamp).format('YYYY-MM-DD HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00// 本地化moment.locale('zh-cn');console.log(moment(timestamp).format('LLL')); // 输出: 2021年9月1日 08:00运行项目并下载源码javascript运行说明:需要引入 Moment.js(CDN 或 NPM:npm install moment)。支持多种格式和本地化,但库体积较大(约 70KB 压缩版)。注意:Moment.js 已进入维护模式,推荐新项目使用 date-fns。方法 5:使用 date-fns 库描述:date-fns 是现代、轻量的日期处理库,模块化设计。适用场景:新项目,需轻量且现代化的日期处理。代码示例:// NPM: npm install date-fnsimport { format } from 'date-fns';const timestamp = 1630454400000;const formatted = format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss');console.log(formatted); // 输出: 2021-09-01 00:00:00运行项目并下载源码javascript运行说明:按需导入,体积小(仅导入所需函数)。支持丰富的格式化选项,类似 Moment.js 但更轻量。方法 6:使用 jQuery(结合 DOM)描述:结合 jQuery 从 DOM 获取时间戳并转换。适用场景:项目已使用 jQuery,需处理用户输入的时间戳。代码示例:// HTML: <input type="text" id="timestamp" value="1630454400000"><button onclick="format()">转换</button>$(document).ready(function() { window.format = function() { const timestamp = $('#timestamp').val(); const date = new Date(Number(timestamp)); const formatted = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; console.log(formatted); // 输出: 2021-09-01 };});运行项目并下载源码javascript运行说明:需引入 jQuery:<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>运行项目并下载源码html13. 综合示例以下是一个完整示例,展示多种转换方法:<!DOCTYPE html><html><head> <title>时间戳转换</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/date-fns@2.30.0/dist/date-fns.min.js"></script> <style> body { font-family: Arial; padding: 20px; } input, button { margin: 10px; padding: 8px; } </style></head><body> <input type="text" id="timestamp" value="1630454400000" placeholder="输入时间戳"> <button onclick="convert()">转换</button> <div id="output"></div> <script> function formatDate(timestamp, format = 'YYYY-MM-DD HH:mm:ss') { const date = new Date(timestamp); const map = { 'YYYY': date.getFullYear(), 'MM': String(date.getMonth() + 1).padStart(2, '0'), 'DD': String(date.getDate()).padStart(2, '0'), 'HH': String(date.getHours()).padStart(2, '0'), 'mm': String(date.getMinutes()).padStart(2, '0'), 'ss': String(date.getSeconds()).padStart(2, '0') }; return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]); } function convert() { const timestamp = Number(document.getElementById('timestamp').value); if (isNaN(timestamp)) { alert('请输入有效时间戳!'); return; } const date = new Date(timestamp); const output = ` <p>原生 Date: ${formatDate(timestamp)}</p> <p>toLocaleString: ${date.toLocaleString('zh-CN')}</p> <p>Moment.js: ${moment(timestamp).format('YYYY-MM-DD HH:mm:ss')}</p> <p>date-fns: ${format(new Date(timestamp), 'yyyy-MM-dd HH:mm:ss')}</p> `; document.getElementById('output').innerHTML = output; } </script></body></html>运行项目并下载源码html4. 方法对比方法 依赖 优点 缺点原生 Date 无 无依赖,简单实现 需手动格式化,代码稍长toLocaleString() 无 本地化支持,灵活选项 格式因浏览器/地区不同自定义格式化 无 完全控制格式,灵活 需编写额外代码Moment.js Moment.js 功能强大,易用,支持本地化 体积大,维护模式date-fns date-fns 轻量,模块化,现代 需额外引入库jQuery jQuery 适合 jQuery 项目,简洁 DOM 操作 需引入 jQuery,增加依赖5. 注意事项时间戳单位:JavaScript Date 接受毫秒时间戳,秒时间戳需乘以 1000:const seconds = 1630454400;const date = new Date(seconds * 1000);运行项目并下载源码javascript运行12时区处理:默认使用本地时区,需用 UTC 方法(如 getUTCFullYear)处理 UTC 时间。库如 Moment.js/date-fns 支持时区插件。输入验证:检查时间戳是否有效:if (isNaN(timestamp) || timestamp < 0) { throw new Error('无效时间戳');}运行项目并下载源码javascript运行性能:原生方法最轻量,适合简单场景。Moment.js 体积大,date-fns 更适合新项目。浏览器兼容性:Date 和 toLocaleString 广泛支持。padStart 是 ES2017,IE 不支持,需 polyfill。安全性:用户输入时间戳需验证,防止异常值或恶意输入。6. 总结首选方法:原生 Date + 自定义格式化,简单无依赖。复杂场景:使用 date-fns(轻量现代)或 Moment.js(功能全面)。jQuery 项目:结合 jQuery 处理 DOM 输入。选择依据:无依赖:原生 Date 或 toLocaleString。复杂格式:自定义函数或 date-fns。本地化:toLocaleString 或 Moment.js。测试:验证不同时间戳(毫秒/秒)、时区和格式————————————————原文链接:https://blog.csdn.net/m0_57545130/article/details/152307313
-
一、数据类型:Java 的 “数据分类标准”1.1 数据类型总览介绍:Java 数据类型分为基本数据类型(直接存储数据值)和引用数据类型(存储对象内存地址),两者内存存储方式不同,决定了使用场景的差异。作用:通过明确数据类型,约束变量存储的数据格式,避免数据混乱,同时优化内存占用(如用byte存储小范围整数,比int更省内存)。核心点总结:基本类型是 Java 语法的 “基础数据单元”,引用类型是基于基本类型构建的复杂数据结构;基本类型的取值范围和字节数是固定的,需根据数据大小选择合适类型(如存储 “年龄” 用byte,存储 “身份证号” 用long)。1.2 基本数据类型(4 类 8 种)介绍:按功能分为整型、浮点型、字符型、布尔型,共 8 种具体类型,每种类型有固定的字节数和取值范围。作用:满足不同场景下的数据存储需求(如整数用整型,小数用浮点型,逻辑判断用布尔型)。使用方式代码与详情: public static void main(String[] args) { // 1. 整型(存储整数) byte age = 25; // 1字节,范围:-128~127(适合小范围整数) short height = 175; // 2字节,范围:-32768~32767 int score = 95; // 4字节,范围:-2³¹~2³¹-1(整数默认类型) long id = 1234567890123L; // 8字节,范围:-2⁶³~2⁶³-1(需加L后缀) // 2. 浮点型(存储小数) float weight = 62.5f; // 4字节,单精度(需加f后缀) double pi = 3.1415926; // 8字节,双精度(小数默认类型,精度更高) // 3. 字符型(存储单个字符) char gender = '男'; // 2字节,范围:0~65535(支持中文) char letter = 'A'; // 对应ASCII码65 // 4. 布尔型(存储逻辑值) boolean isPass = true; // 1字节,仅true/false两个值(用于判断) // 输出验证 System.out.println("年龄:" + age); System.out.println("圆周率:" + pi); System.out.println("是否及格:" + isPass); }}一键获取完整项目代码java123456789101112131415161718192021222324整型中int是默认类型,long需加L后缀(小写l易与1混淆,不推荐);浮点型中double是默认类型,float需加f后缀;char类型本质是数值类型,可直接参与运算(如’A’ + 1结果为 66,对应’B’);boolean类型不能用 0/1 代替false/true,仅能存储逻辑值。1.3 引用数据类型(以 String 为例)介绍:String 是 Java 专门处理字符串的引用类型,字符串是 “多个字符的组合”,需用双引号包裹(如"Hello")。作用:存储文本信息(如用户名、地址),提供丰富的字符串处理方法(如截取、替换、判断相等)。使用方式代码: public static void main(String[] args) { // 方式1:简化写法(推荐),字符串常量池存储(相同内容仅存一份,省内存) String name1 = "Java编程"; String name2 = "Java编程"; }一键获取完整项目代码java12345二、类型转换:解决 “数据类型不匹配” 问题2.1 自动类型转换(隐式转换)介绍:当 “小范围类型数据” 赋值给 “大范围类型变量” 时,Java 自动完成转换,无需手动干预(如byte→int)。作用:避免简单赋值场景的语法错误,简化代码编写(如无需手动转换byte到int)。使用方式代码:public static void main(String[] args) { byte a = 10; // 小范围(1字节) int b = a; // 自动转换:byte→int(4字节),无需手动处理 double c = b; // 自动转换:int→double(8字节) System.out.println("b = " + b); // 10 System.out.println("c = " + c); // 10.0 // char与int的自动转换(char范围0~65535,int范围更大) char ch = 'A'; int chValue = ch; // 自动转换:char→int,值为65 System.out.println("'A'对应的ASCII码:" + chValue);}一键获取完整项目代码java12345678910111213自动转换的前提是 “小范围→大范围”,常见顺序:byte→short→int→long→float→double、char→int;char类型可自动转换为int(因char的取值范围在int的正区间内),但byte不能自动转换为char(byte有负值)。2.2 表达式中的自动类型提升介绍:在算术表达式中,所有变量会先提升到 “表达式中的最高类型”,再参与运算,结果类型与最高类型一致。作用:避免运算过程中的数据溢出,保证运算精度(如byte参与运算时先转int,避免字节级运算溢出)。使用方式代码: public static void main(String[] args) { // 示例1:byte+short→int byte a = 5; short b = 10; // int c = a + b; // 正确:a和b先转int,结果为int // byte d = a + b; // 错误:结果是int,不能直接赋值给byte // 示例2:int+double→double int num1 = 3; double num2 = 2.5; double result = num1 + num2; // 正确:num1先转double,结果为double System.out.println("3 + 2.5 = " + result); // 5.5 // 示例3:char+int→int char ch = 'a'; // ASCII码97 int sum = ch + 3; // ch先转int,结果为100(对应'd') System.out.println("'a' + 3 = " + sum + "(对应字符'd')"); }一键获取完整项目代码java123456789101112131415161718表达式提升的关键规则:byte/short/char参与运算时,先自动转为int;其他类型按 “范围从低到高” 提升到最高类型;若需将表达式结果赋值给小范围类型,需手动强制转换(如byte d = (byte)(a + b)),但需注意数据溢出风险。2.3 强制类型转换(显式转换)介绍:当 “大范围类型数据” 赋值给 “小范围类型变量” 时,需手动指定目标类型(如double→int),可能导致精度损失或数据溢出。作用:在明确数据范围可控的场景下,实现 “大范围→小范围” 的转换(如将double小数转为int整数)。使用方式代码: public static void main(String[] args) { // 示例1:double→int(精度损失) double price = 98.9; int pay = (int) price; // 强制转换:丢弃小数部分,结果为98 System.out.println("支付金额(整数):" + pay); // 示例2:int→byte(数据溢出风险) int num1 = 150; // int范围:-2147483648~2147483647 byte num2 = (byte) num1; // byte范围:-128~127,150超出范围,结果为-106(溢出) System.out.println("int 150强制转为byte:" + num2); // 示例3:表达式结果强制转换 int a = 10; int b = 3; double avg1 = a / b; // 3.0(int/int=int,再自动转double) double avg2 = (double)a / b; // 3.333...(a先转double,结果为double) System.out.println("10/3(int运算):" + avg1); System.out.println("10/3(强制转double):" + avg2); }一键获取完整项目代码java强制转换语法:目标类型 变量名 = (目标类型) 待转换数据;,括号不可省略;风险提示:浮点型转整型会 “直接丢弃小数”(非四舍五入),若源数据超出目标类型范围,会导致数据溢出(结果为异常值),需谨慎使用;优化建议:转换前可先判断数据范围(如if (num1 <= 127 && num1 >= -128)),避免溢出。三、运算符:Java 的 “计算与判断工具”3.1 算术运算符介绍:用于实现基本的算术运算(加、减、乘、除、取余),其中+还可用于字符串连接。作用:处理数值计算(如求总和、平均值)和字符串拼接(如拼接用户信息)。使用方式代码: public static void main(String[] args) { // 1. 基本算术运算 int a = 10; int b = 3; System.out.println("a + b = " + (a + b)); // 13 System.out.println("a - b = " + (a - b)); // 7 System.out.println("a * b = " + (a * b)); // 30 System.out.println("a / b = " + (a / b)); // 3(整数相除,丢弃小数) System.out.println("a % b = " + (a % b)); // 1(取余,结果符号与被除数一致) // 2. 字符串连接(+两边有字符串则为连接符) String name = "张三"; int age = 20; System.out.println("姓名:" + name + ",年龄:" + age); // 姓名:张三,年龄:20 // 3. 数值拆分(取余+除法) int num = 789; int ge = num % 10; // 个位:9 int shi = num / 10 % 10; // 十位:8 int bai = num / 100; // 百位:7 System.out.println("三位数" + num + "拆分:百位" + bai + ",十位" + shi + ",个位" + ge); }一键获取完整项目代码java整数相除(/)结果为整数,若需小数结果,需将其中一个操作数转为浮点型(如(double)a / b);取余(%)的结果符号与 “被除数” 一致(如-10 % 3 = -1,10 % -3 = 1);+的双重作用:两边都是数值则为加法,有一个是字符串则为连接(如1 + “2” = “12”,而非3)。3.2 自增自减运算符(++/–)介绍:用于对变量值 “加 1”(++)或 “减 1”(–),分为 “前缀”(先变后用)和 “后缀”(先用后变)两种用法。作用:简化变量自增 / 自减的代码(如i = i + 1可简写为i++),常用于循环变量更新。使用方式代码: public static void main(String[] args) { // 1. 后缀自增(先用后变) int num1 = 5; System.out.println("num1++ = " + num1++); // 5(先输出5,再num1=6) System.out.println("num1 = " + num1); // 6 // 2. 前缀自增(先变后用) int num2 = 5; System.out.println("++num2 = " + ++num2); // 6(先num2=6,再输出6) System.out.println("num2 = " + num2); // 6 // 3. 自减示例(与自增逻辑一致) int num3 = 5; System.out.println("num3-- = " + num3--); // 5(先用后变,num3=4) System.out.println("--num3 = " + --num3); // 3(先变后用,num3=3) }一键获取完整项目代码java核心区别:后缀(变量++)是 “先使用变量当前值,再更新变量”;前缀(++变量)是 “先更新变量,再使用变量新值”;注意事项:++/–只能用于变量(如5++错误),且不建议在复杂表达式中使用(如a = b++ + ++b,结果易混淆)。3.3 赋值运算符介绍:用于给变量赋值,分为 “基本赋值”(=)和 “复合赋值”(+=、-=等),复合赋值会自动进行强制转换。作用:简化变量更新的代码(如a = a + 3可简写为a += 3),同时避免类型转换错误。使用方式代码:public static void main(String[] args) { // 1. 基本赋值(=) int x = 10; System.out.println("x = " + x); // 10 // 2. 复合赋值(自动强制转换) byte y = 5; y += 3; // 等价于 y = (byte)(y + 3),自动强制转换,避免错误 // y = y + 3; // 错误:y+3是int,不能直接赋值给byte System.out.println("y += 3 后:" + y); // 8 int z = 20; z -= 5; // 等价于 z = z - 5 → 15 z *= 2; // 等价于 z = z * 2 → 30 z /= 4; // 等价于 z = z / 4 → 7(整数相除) z %= 3; // 等价于 z = z % 3 → 1 System.out.println("z经过一系列操作后:" + z); // 1 }一键获取完整项目代码java复合赋值运算符(+=、-=、*=、/=、%=)的优势:自动对结果进行强制转换,适合小范围类型变量更新;基本赋值(=)的右结合性:如a = b = c = 5,从右到左执行,最终a、b、c均为 5。3.4 关系运算符介绍:用于判断两个数据的关系(大小、相等),结果仅为true(成立)或false(不成立),常用于分支和循环的条件判断。作用:构建条件表达式(如score >= 60判断是否及格),控制程序流程。使用方式代码:public static void main(String[] args) { int score = 85; int passLine = 60; // 关系运算结果为boolean System.out.println("score > passLine:" + (score > passLine)); // true System.out.println("score >= 90:" + (score >= 90)); // false System.out.println("score == passLine:" + (score == passLine)); // false System.out.println("score != passLine:" + (score != passLine)); // true // 结合分支使用 if (score >= passLine) { System.out.println("考试及格"); } else { System.out.println("考试不及格"); } // 注意:判断相等用==,不能用=(=是赋值) int a = 5; int b = 5; // if (a = b) { ... } // 错误:a = b是赋值,结果为5(int),不能作为条件 if (a == b) { System.out.println("a和b相等"); } }一键获取完整项目代码java关系运算符的结果是boolean类型,只能用于条件判断(如if、while的条件);易错点:判断 “相等” 用==,单个=是赋值运算符(如if (a = b)是语法错误,因赋值结果是数值,非boolean);描述区间需用&&连接两个关系表达式(如score >= 60 && score <= 100,不能写60 <= score <= 100)。3.5 逻辑运算符(处理 boolean 值)介绍:用于对boolean类型的结果进行逻辑运算(与、或、非、异或),其中&&和||具有 “短路特性”。作用:组合多个条件表达式(如 “年龄≥18 且身份证有效”),实现复杂逻辑判断。使用方式代码:public static void main(String[] args) {int age = 20;boolean hasId = true; // 1. 逻辑与(&&):两边都为true,结果才为true(短路:左边false则右边不执行) boolean canVote = age >= 18 && hasId; System.out.println("是否能投票:" + canVote); // true // 2. 逻辑或(||):两边有一个为true,结果就为true(短路:左边true则右边不执行) boolean isAdult = age >= 18 || hasId; System.out.println("是否成年:" + isAdult); // true // 3. 逻辑非(!):取反(true→false,false→true) boolean isMinor = !isAdult; System.out.println("是否未成年:" + isMinor); // false // 4. 短路特性演示 int num = 5; // &&短路:左边false,右边num++不执行,num仍为5 boolean flag1 = (num > 10) && (num++ > 0); System.out.println("flag1 = " + flag1 + ",num = " + num); // false,5 // ||短路:左边true,右边num++不执行,num仍为5 boolean flag2 = (num < 10) || (num++ > 0); System.out.println("flag2 = " + flag2 + ",num = " + num); // true,5}一键获取完整项目代码java短路特性是&&和||的关键:&&左边为false时,右边表达式不执行;||左边为true时,右边表达式不执行(可提高效率,避免无效运算);逻辑非(!)是单目运算符,仅需一个操作数(如!hasId);逻辑异或(^)较少用,规则是 “两边不同为true,相同为false”。3.6 三元运算符介绍:由 “条件表达式” 和 “两个结果值” 组成,是简化if-else的语法,格式为条件 ? 值1 : 值2。作用:在 “二选一” 场景下简化代码(如 “判断及格与否,返回对应评语”),比if-else更简洁。使用方式代码: public static void main(String[] args) { // 示例1:判断成绩是否及格,返回评语 int score = 75; String result = score >= 60 ? "及格" : "不及格"; System.out.println("成绩评定:" + result); // 及格 // 示例2:求两个数的最大值 int num1 = 20; int num2 = 35; int max = num1 > num2 ? num1 : num2; System.out.println(num1 + "和" + num2 + "的最大值:" + max); // 35 // 示例3:嵌套使用(不推荐,可读性低,复杂场景用if-else) int score2 = 92; String grade = score2 >= 90 ? "优秀" : (score2 >= 80 ? "良好" : "及格"); System.out.println("成绩等级:" + grade); // 优秀 }一键获取完整项目代码java执行逻辑:先判断条件,条件为true返回值1,为false返回值2;语法要求:值1和值2必须为同一类型(或可自动转换为同一类型),如score >= 60 ? “及格” : 0是错误的(字符串与整数类型不匹配);使用建议:简单二选一场景用三元运算符,复杂逻辑(如嵌套超过 2 层)用if-else,保证代码可读性。四、键盘录入:实现 “用户交互”4.1 Scanner 类的作用与使用步骤介绍:java.util.Scanner是 Java 提供的键盘录入工具类,用于获取用户从键盘输入的字符串、整数、小数等数据。作用:实现程序与用户的交互(如让用户输入用户名、年龄),为动态数据处理提供输入支持。使用方式代码(完整步骤):// 步骤1:导入Scanner类(JDK11+可省略,IDEA自动导入)import java.util.Scanner;public class ScannerDemo { public static void main(String[] args) { // 步骤2:创建Scanner对象(System.in表示从键盘输入) Scanner sc = new Scanner(System.in); // 步骤3:提示用户输入(提升用户体验,可选) System.out.print("请输入您的姓名:"); // 步骤4:获取输入的字符串(next():获取空格前的内容) String name = sc.next(); System.out.print("请输入您的年龄:"); // 获取输入的整数 int age = sc.nextInt(); System.out.print("请输入您的身高(米):"); // 获取输入的小数 double height = sc.nextDouble(); // 步骤5:使用输入的数据 System.out.println("\n=== 用户信息 ==="); System.out.println("姓名:" + name); System.out.println("年龄:" + age + "岁"); System.out.println("身高:" + height + "米"); // 步骤6:关闭Scanner(释放资源,可选但推荐) sc.close(); }}一键获取完整项目代码java核心步骤:导包→创建对象→获取输入→使用数据→关闭对象;常用获取方法:next()(字符串,空格截断)、nextInt()(整数)、nextDouble()(小数)、nextLine()(整行字符串,含空格);注意事项:nextInt()/nextDouble()后若直接用nextLine(),会读取到 “换行符”,需先调用sc.nextLine()清空换行符(如sc.nextInt(); sc.nextLine(); String addr = sc.nextLine();)。————————————————原文链接:https://blog.csdn.net/kong7906928/article/details/156065332
-
目录1.继承2.子类访问父类的成员变量3.子类访问父类的成员方法4.super关键字5.初始化顺序6.final关键字引言面向对象三大特性:封装,继承和多态。今天我们就来聊聊继承。1.继承1.1为什么需要继承因为我们在创建类时可能会大量用到重复的字段和方法,为了解决这个问题,面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。1.2继承的语法继承需要使用到extends关键字修饰符 class 子类 extends 父类{// 主体}一键获取完整项目代码java1232.子类访问父类的成员变量子类继承了父类的方法和字段,子类在方法中就可以直接访问父类的成员吗?2.1子类和父类不存在同名成员变量class Base { public int a = 1; public int b = 2;}class Derievd extends Base{ public int c = 3; public void func() { System.out.println(this.a);// 访问从父类中继承下来的a System.out.println(this.b);// 访问从父类中继承下来的b }}public class Test { public static void main(String[] args) { Derievd derievd = new Derievd(); derievd.func(); }}一键获取完整项目代码java运行结果:2.2子类和父类成员变量同名代码如下:class Base { public int a = 1; public int b = 2;}class Derievd extends Base{ public int c = 3; public int a = 100; public void func() { System.out.println(this.a);//访问子类自己新增加的a System.out.println(this.b);//访问从父类中继承下来的b }}public class Test { public static void main(String[] args) { Derievd derievd = new Derievd(); derievd.func(); }}一键获取完整项目代码java运行结果:得出结论:当子类和父类成员同名时,优先访问子类自己的成员。特性:遵循就近原则,子类有的优先使用自己的,没有再去父类中找。2.3如果一定要访问父类的成员怎么做?方法1:使用super关键字方法2:初始化一个父类对象,用父类对象的引用访问父类的成员变量。class Base { public int a = 1; public int b = 2;}class Derievd extends Base{ public int c = 3; public int a = 100; //子类当中 如何访问父类的成员 public void func() { //方法1 System.out.println("父类的a: "+super.a); System.out.println(this.a); //方法2 Base base = new Base(); System.out.println(base.a); }}public class Test { public static void main(String[] args) { Derievd derievd = new Derievd(); derievd.func(); }}一键获取完整项目代码java执行结果:3子类中访问父类的成员方法3.1成员方法名字不同class Base { public void methodA(){ System.out.println("Base中的methodA()"); }}class Derived extends Base{ public void methodB(){ System.out.println("Derived中的methodB()方法"); } public void func(){ this.methodB(); // 访问子类自己的methodB() this.methodA(); // 访问父类继承的methodA() }}public class Test { public static void main(String[] args) { Derived derived = new Derived(); derived.func(); }}一键获取完整项目代码java执行结果:3.2成员方法名字相同class Base { public void methodA(){ System.out.println("Base中的methodA()"); }}class Derived extends Base{ public void methodA(){ System.out.println("Derived中的methodA()方法"); } public void func(){ this.methodA(); // 访问子类自己的methodA() }}public class Test { public static void main(String[] args) { Derived derived = new Derived(); derived.func(); }}一键获取完整项目代码java执行结果:结论:【和成员变量同名时相同】当子类和父类成员同名时,优先访问子类自己的成员。特性:遵循就近原则,子类有的优先使用自己的,没有再去父类中找。3.3如果一定要访问父类的成员怎么做?【和上面的方法相同】方法1:使用super关键字方法2:初始化一个父类对象,用父类对象的引用访问父类的成员变量。class Base { public void methodA(){ System.out.println("Base中的methodA()"); }}class Derived extends Base{ public void methodA(){ System.out.println("Derived中的methodA()方法"); } public void func(){ //方法1 super.methodA(); // 访问子类自己的methodA() //方法2 Base base = new Base(); base.methodA(); }}public class Test { public static void main(String[] args) { Derived derived = new Derived(); derived.func(); }}一键获取完整项目代码java执行结果:4.super关键字因为子类可能会和父类成员同名,如果明确要访问父类的成员,就需要使用super关键字。public void testExtends(){ super.name = "zhangsan";//访问从父类继承下来的成员变量 super.age = 18;//访问从父类继承下来的成员变量 super.eat();///访问从父类继承下来的成员方法 color = "黄色";//访问子类自己的成员变量 run();//访问子类自己的成员方法}一键获取完整项目代码java4.1子类构造方法在继承性关系下,构造子类对象时,需要先调用基类构造方法,然后执行子类的构造方法。即先要把继承于父类的成员初始化完,才能初始化子类自己的成员。class Animal { String name; int age; public Animal(String name, int age) { this.name = name; this.age = age; }}public class Dog extends Animal{ String color; public Dog(String name, int age,String color) { super(name, age); this.color = color; }}一键获取完整项目代码java注意事项:若父类无显示构造方法(没写构造方法)或默认构造方法(即无参数构造方法),子类构造方法第一行默认有隐藏的super()调用,即调用子类构造方法时,先调用基类的构造方法。若父类构造方法有带参数,子类构造方法需要自己定义super()。在子类构造方法中,super()必须是第一条语句。在子类构造方法中,super()只能出现一次,并且不能和this()同时出现。第四点解释,当我们重载子类构造方法时,this()调用,会调用重载的子类构造方法。而this()和super()在构造方法中,都必须是第一条语句,所有不能同时出现。代码如下:public class Dog extends Animal{ String color; //方法的重载 public Dog() { this("green");//调用带有一个参数的构造方法 System.out.println("使用了不带参数的构造方法"); } public Dog(String color) { this.color = color; System.out.println("使用了带有一个参数的构造方法"); } public static void main(String[] args) { Dog dog = new Dog();//调用无参构造方法 System.out.println(dog.color); System.out.println("=============="); Dog dog1 = new Dog("red");//调用带有一个参数的构造方法 System.out.println(dog.color); }}一键获取完整项目代码java运行结果: 4.2super和thissuper和tthis都可以在成员方法中用来访问:成员变量和调用其他成员方法,都可以作为构造方法的第一条语句。【相同点】1.都只能在类的非静态方法中使用,用来访问非静态成员变量和调用其他成员方法。2.在构造方法中调用时,必须是构造方法的第一条语句,并且不能同时存在。【不同点】1,this是当前对象的引用,当前对象即调用实例方法的对象,super相当于子类对象从父类继承下来的部分成员的引用。2.在非静态成员方法中,this用来访问本类的方法和属性,super用来访问从父类继承下来的方法和属性。3.在构造方法中:this()用于调用本类的构造方法,super()用于调用父类的构造方法,两者在构造方法中不能同时存在。4.在继承体系下,构造方法中一定会存在super()调用,用户没有写编译器也会自动增加,但this()用户不写则没有。5.初始化顺序5.1没有继承关系时的执行顺序我们再来回顾下初始化执行顺序,没有继承关系时的执行顺序:class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; System.out.println("构造方法执行"); } { System.out.println("实例代码块执行"); } static { System.out.println("静态代码块执行"); }}public class Test { public static void main(String[] args) { Person person1 = new Person("bit",10); System.out.println("============================"); Person person2 = new Person("gaobo",20); }}一键获取完整项目代码java运行结果:结论分析:1.被static修饰的静态代码块先执行,并且只执行一次,在类的加载阶段执行。2.当有对象创建时,才会执行实例代码块,执行完实例代码块,最后执行构造方法。5.1继承关系下的执行顺序class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; System.out.println("构造方法执行"); } { System.out.println("实例代码块执行"); } static { System.out.println("静态代码块执行"); }}class Student extends Person{ public Student(String name,int age) { super(name,age); System.out.println("Student:构造方法执行"); } { System.out.println("Student:实例代码块执行"); } static { System.out.println("Student:静态代码块执行"); }}public class Test { public static void main(String[] args) { Student student1 = new Student("张三",19); System.out.println("==========================="); Student student2 = new Student("gaobo",20); }}一键获取完整项目代码java执行结果:结论分析:1.父类静态代码块先执行,执行完再执行子类静态代码块。2.父类实例代码块和构造方法先执行。3.再执行子类实例代码块和构造方法。4.静态代码块只执行一次。6.final关键字final可以用来修饰变量、成员方法和类。1.被final修饰的变量或字段,表示常量,不能再被改变。final int a = 10;a = 20; // 编译出错一键获取完整项目代码java2.被final修饰的类,不能被继承final class Person {}class Student extends Person{}一键获取完整项目代码java3.被final修饰的方法,不能重写————————————————原文链接:https://blog.csdn.net/xifeng191/article/details/155503019
-
前言:注解到底是什么? 你是否经常在 Java 代码中看到@Override、@Deprecated这样的标记?这些就是注解 —— 一种给代码 "贴标签" 的机制。注解本身不直接影响代码执行,但能通过工具(如编译器)或框架(如 Spring)赋予代码额外含义。 自定义注解则是让我们根据业务需求创建专属 "标签",结合反射机制能实现强大的动态逻辑(比如日志记录、权限校验、ORM 映射等)。本文将从基础到实战,带你掌握自定义注解的定义、元注解的作用,以及如何通过反射让注解 "生效"。一、自定义注解基础:@interface 关键字 自定义注解使用 @interface 关键字定义,本质上是一种特殊的接口(编译后会生成继承 java.lang.annotation.Annotation 的接口)。1.1 最简单的自定义注解// 定义一个空注解public @interface MyFirstAnnotation {}一键获取完整项目代码java这个注解没有任何属性,仅作为标记使用。可以直接标注在类、方法等元素上:@MyFirstAnnotationpublic class Demo { @MyFirstAnnotation public void test() {}}一键获取完整项目代码java1.2 带属性的注解注解可以包含 "属性"(类似接口的抽象方法),使用时需要为属性赋值(除非有默认值)。public @interface UserInfo { // 字符串属性 String name(); // 整数属性,带默认值 int age() default 18; // 数组属性 String[] hobbies() default {"coding"};}一键获取完整项目代码java使用时的语法(属性名 = 值):@UserInfo(name = "张三", age = 20, hobbies = {"篮球", "游戏"})public class Person {}一键获取完整项目代码java特殊规则:若属性名是 value,且只有这一个属性需要赋值,可省略属性名:@MyAnnotation("test")数组属性若只有一个元素,可省略大括号:hobbies = "足球"二、元注解:注解的 "注解" 元注解是用于修饰注解的注解,规定了自定义注解的使用范围、生命周期等特性。Java 内置了 4 种元注解:@Target、@Retention、@Documented、@Inherited。2.1 @Target:指定注解能修饰哪些元素 @Target 限制注解可标注的目标(如类、方法、字段等),参数是 ElementType 枚举数组,常用值:ElementType 作用范围TYPE 类、接口、枚举METHOD 方法FIELD 成员变量(包括枚举常量)PARAMETER 方法参数CONSTRUCTOR 构造方法LOCAL_VARIABLE 局部变量示例:限制注解只能用于类和方法import java.lang.annotation.Target;import java.lang.annotation.ElementType; @Target({ElementType.TYPE, ElementType.METHOD}) // 可修饰类和方法public @interface Log {}一键获取完整项目代码java如果把 @Log 标注在字段上,编译器会直接报错:public class Demo { @Log // 编译错误:@Log不适用于字段 private String name;}一键获取完整项目代码java 图示:@Target 的作用范围限制 2.2 @Retention:指定注解的生命周期@Retention 决定注解保留到哪个阶段(源码、字节码、运行时),参数是 RetentionPolicy 枚举,必须指定:RetentionPolicy 生命周期说明 能否被反射获取SOURCE 仅存在于源码中,编译后丢弃(如@Override) 不能CLASS 保留到字节码中,但 JVM 运行时不加载(默认值) 不能RUNTIME 保留到运行时,JVM 加载,可通过反射获取 能示例:让注解在运行时可被反射获取import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) // 关键:保留到运行时public @interface Permission { String value();}一键获取完整项目代码java为什么 RUNTIME 重要?反射是在程序运行时动态获取类信息的机制,只有 RUNTIME 级别的注解才能被反射读取,这是注解与反射结合的核心前提。图示:注解的生命周期流程 2.3 @Documented:让注解出现在 API 文档中 默认情况下,javadoc 生成的文档不会包含注解信息。@Documented 修饰的注解会被包含在文档中。示例:import java.lang.annotation.Documented; @Documented // 生成文档时包含该注解public @interface Description { String value();} /** * 测试类 * @Description 这是一个测试类 */@Description("测试类")public class Test {}一键获取完整项目代码java生成的 javadoc 中,Test 类的文档会显示 @Description("测试类")。2.4 @Inherited:让注解可被继承@Inherited 表示注解具有继承性:如果父类被该注解标注,子类会自动继承该注解(仅对类注解有效,方法 / 字段注解不继承)。示例:import java.lang.annotation.Inherited; @Inherited // 允许继承@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface InheritedAnnotation {} // 父类标注注解@InheritedAnnotationclass Parent {} // 子类未标注,但会继承父类的@InheritedAnnotationclass Child extends Parent {}一键获取完整项目代码java通过反射验证:public class Test { public static void main(String[] args) { System.out.println(Child.class.isAnnotationPresent(InheritedAnnotation.class)); // 输出:true }}一键获取完整项目代码java 图示:@Inherited 的继承效果 三、注解 + 反射:让注解 "生效" 注解本身只是标记,必须通过反射获取注解信息并执行逻辑,才能真正发挥作用。反射提供了以下核心方法(在 Class、Method、Field 等类中):方法 作用getAnnotation(Class) 获取指定类型的注解实例getAnnotations() 获取所有注解(包括继承的)isAnnotationPresent(Class) 判断是否存在指定注解实战案例:用注解实现方法权限校验需求:定义 @RequiresPermission 注解,标记方法需要的权限;通过反射调用方法前检查当前用户是否有权限,无权限则抛出异常。步骤 1:定义注解import java.lang.annotation.*; @Target(ElementType.METHOD) // 仅用于方法@Retention(RetentionPolicy.RUNTIME) // 运行时可反射获取public @interface RequiresPermission { String[] value(); // 所需权限列表}一键获取完整项目代码java步骤 2:使用注解标注方法public class UserService { // 需要"user:query"权限 @RequiresPermission("user:query") public void queryUser() { System.out.println("查询用户成功"); } // 需要"user:add"或"admin"权限 @RequiresPermission({"user:add", "admin"}) public void addUser() { System.out.println("新增用户成功"); }}一键获取完整项目代码java步骤 3:反射 + 注解实现权限校验import java.lang.reflect.Method;import java.util.Arrays;import java.util.HashSet;import java.util.Set; public class PermissionChecker { // 模拟当前用户拥有的权限 private static final Set<String> CURRENT_USER_PERMISSIONS = new HashSet<>(Arrays.asList("user:query")); // 反射调用方法并校验权限 public static void invokeWithCheck(Object obj, String methodName) throws Exception { // 1. 获取方法对象 Method method = obj.getClass().getMethod(methodName); // 2. 检查方法是否有@RequiresPermission注解 if (method.isAnnotationPresent(RequiresPermission.class)) { // 3. 获取注解实例 RequiresPermission annotation = method.getAnnotation(RequiresPermission.class); // 4. 获取注解的权限列表 String[] requiredPermissions = annotation.value(); // 5. 校验权限 boolean hasPermission = false; for (String permission : requiredPermissions) { if (CURRENT_USER_PERMISSIONS.contains(permission)) { hasPermission = true; break; } } if (!hasPermission) { throw new SecurityException("权限不足,需要:" + Arrays.toString(requiredPermissions)); } } // 6. 权限通过,调用方法 method.invoke(obj); } public static void main(String[] args) throws Exception { UserService service = new UserService(); invokeWithCheck(service, "queryUser"); // 成功:查询用户成功 invokeWithCheck(service, "addUser"); // 失败:抛出SecurityException }}一键获取完整项目代码java执行结果:查询用户成功Exception in thread "main" java.lang.SecurityException: 权限不足,需要:[user:add, admin]一键获取完整项目代码bash四、底层原理:注解本质与反射获取机制注解的本质:@interface 编译后会生成一个继承 java.lang.annotation.Annotation 的接口,例如:// 编译后自动生成的代码(简化)public interface MyAnnotation extends Annotation { String value(); int age() default 18;}一键获取完整项目代码java注解实例的生成:当 JVM 加载被注解的类时,会通过动态代理生成注解接口的实现类实例(保存注解属性值)。反射获取注解的过程:反射通过 getAnnotation() 方法从类 / 方法的元数据中获取代理实例,从而读取属性值。五、应用场景总结注解 + 反射的组合在框架中被广泛使用:日志记录:通过注解标记需要记录日志的方法,反射拦截并打印日志(如 Spring 的@Log)。ORM 映射:用注解关联 Java 类与数据库表(如 JPA 的@Entity、@Column)。依赖注入:标记需要注入的对象(如 Spring 的@Autowired)。AOP 切面:通过注解定义切入点(如 Spring 的@Before、@After)。参数校验:验证方法参数合法性(如 Jakarta 的@NotNull、@Size)。结语 自定义注解是 Java 中 "声明式编程" 的核心体现,结合反射能极大简化代码逻辑、提高灵活性。掌握元注解的作用(尤其是@Target和@Retention)是定义有效注解的前提,而反射则是让注解从 "标记" 变为 "可执行逻辑" 的桥梁。尝试在项目中用注解解决重复逻辑(如日志、权限),你会感受到它的强大!————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152256018
-
前言Java是一门面向对象的语言,在学习它之前需要先了解什么是对象?对象就是现实生活中的实体,比如,冰箱,洗衣机,人等等。 什么是面向对象?面向对象是一种解题思想,就是依靠对象之间的交互完成任务。比如,人把衣服给洗衣机,这里人和洗衣机就是对象。我们只需要使用洗衣机就可以,不需要关注洗衣机是怎么洗衣服的。这就是依靠对象之间的交互来完成任务。 类的定义和使用什么是类类是⽤来对⼀个实体(对象)来进行描述的,主要描述该实体(对象)具有哪些属性(外观尺寸等),哪些功能(用来⼲啥),描述完成后计算机就可以识别了。类可以分为两部分:一部分是字段(属性)也叫成员变量,另一部分是行为也叫成员方法。 类的定义类的定义需要用class关键字,代码如下: class ClassName{field; // 字段(属性) 或者 成员变量method; // ⾏为 或者 成员⽅法}运行项目并下载源码java运行 类的实例化什么是实例化定义了⼀个类,就相当于在计算机中定义了⼀种新的类型,与int,double类似,只不过int和double是java语言自带的内置类型,而类是用户⾃定义的类型。用类类型来创建对象的过程就是类的实例化,需要用到new关键字加上类名来实例化对象。代码如下: public class Main{public static void main(String[] args) {ClassName classname = new ClassName(); //通过new实例化对象}}运行项目并下载源码java运行 访问对象的成员比如我们定义一个Student类,代码如下: public class Student {//属性,成员变量 public String name; public int age; //行为 成员方法 public void eat(){ System.out.println(name+"正在吃饭....."); } public void sleep(){ System.out.println(name+"正在睡觉....."); }}运行项目并下载源码java运行 这时候就可以通过对象的引用加’ . '号来访问对象当中的成员和成员方法。代码如下: public class Test { public static void main(String[] args) { Student student = new Student(); student.name = "zhangsan"; student.age = 18; student.eat(); student.sleep(); System.out.println(student.name); System.out.println(student.age); }}运行项目并下载源码java运行 运行结果: this关键字使用this引用的原因如下代码定义了⼀个Date类,Date类中包含3个属性分别是year,month,day。分别使⽤setDay和printDate对进行设置和打印⽇期. public class Date {public int year;public int month;public int day;public void setDay(int y, int m, int d){year = y;month = m;day = d;}public void printDate(){System.out.println(year + "/" + month + "/" + day);}public static void main(String[] args) {// 构造三个⽇期类型的对象 d1 d2 d3Date d1 = new Date();Date d2 = new Date();Date d3 = new Date();// 对d1,d2,d3的⽇期设置d1.setDay(2020,9,15);d2.setDay(2020,9,16);d3.setDay(2020,9,17);// 打印⽇期中的内容d1.printDate();d2.printDate();d3.printDate();}}运行项目并下载源码java运行 这里提出两个疑问:1.如形参名不小心与成员变量名相同,会发生什么?2.三个对象都在调⽤setDate和printDate函数,但是这两个函数中没有任何有关对象的说明,setDate和printDate函数如何知道打印的是那个对象的数据呢? public void setDay(int year, int month, int day){year = year;month = month;day = day;}运行项目并下载源码java运行 这时候运行代码就会发现,形参自己给自己赋值。那如何解决这个问题呢?这时候我们只要在成员变量前加上this.就能一劳永逸的解决这个问题。代码如下 public void setDay(int year, int month, int day){this.year = year;this.month = month;this.day = day;运行项目并下载源码java运行 this是什么this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。这就很好的回答了第二个疑问。 this的特性1.this只能在成员方法中使用2.this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型3.在"成员方法"中,this只能引用当前对象,不能再引用其他对象4.this是“成员方法”第⼀个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器会负责将调用成员方法对象的引用传递给该成员方法,this负责来接收this隐藏参数演示如下: 对象的构造及初始化默认初始化如果没有对成员变量进行初始化,系统会给一个对应的默认值。默认值如下: 就地初始化在声明成员变量时就给出初始值 public class Student { public String name = "zhangsan"; public int age = 18; public void eat(){ System.out.println(name+"正在吃饭....."); } public void sleep(){ System.out.println(name+"正在睡觉....."); }}运行项目并下载源码java运行 构造方法初始化构造方法概述构造方法是一种特殊的成员方法,名字要和类名必须相同,无返回值,创建对象时会由编译器自动调用。 public class Student { public String name; public int age; //无参构造方法 public Student(){ System.out.println("不带参数的构造方法....."); //this.("qiqi",18); } //有两个参数的构造方法 public Student(String name,int age){ this.name = name; this.age = age; System.out.println("调用了带有3个参数的构造方法....."); } public void eat(){ System.out.println(name+"正在吃饭....."); } public void sleep(){ System.out.println(name+"正在睡觉....."); } public static void main(String[] args) { Student student = new Student("zhangsan",18); }}运行项目并下载源码java运行 创建构造方法的快捷方法右键鼠标,点击Generate , 再点击Constructor , 然后按住Ctrl键选中所有成员变量,最后点击OK即可。 系统就会生成对应构造方法: 创建成员方法的快捷方法Getter and Setter是创建成员方法的快捷方式,也是一样选中所有成员变量,最后点击OK即可————————————————原文链接:https://blog.csdn.net/xifeng191/article/details/155202007
-
1. Java 1.0(1996年1月23日)核心功能:首个正式版本,支持面向对象编程、垃圾回收、网络编程。包含基础类库(java.lang、java.io、java.awt)。支持Applet(浏览器嵌入的小程序)。关键特性:跨平台(Write Once, Run Anywhere)。基础集合类(Vector、Hashtable)。AWT(Abstract Window Toolkit,用于GUI开发)。2. Java 1.1(1997年2月19日)核心功能:引入内部类(Inner Classes)。增强GUI支持(Swing框架的前身)。关键特性:Reflection(反射机制)。RMI(Remote Method Invocation,远程方法调用)。JDBC(数据库连接)的早期版本。3. Java 1.2(Java 2, 1998年12月8日)核心功能:正式更名为Java 2,分为三个平台:J2SE(标准版)、J2EE(企业版)、J2ME(微型版)。引入集合框架(java.util.Collections)。关键特性:Swing GUI框架(替代AWT)。校验编译器(javac改进)。严格的异常处理(必须声明或捕获检查型异常)。4. Java 1.3(2000年5月8日)核心功能:增强JIT编译器性能。引入日志框架(java.util.logging)。关键特性:HotSpot JVM(Oracle的高性能JVM)。音频API(javax.sound)。5. Java 1.4(2002年2月6日)核心功能:正则表达式(java.util.regex)。遍历器模式(Iterator)。关键特性:Assertion(断言,assert关键字)。集成Apache的XML解析库(javax.xml)。6. Java 5(Java 5.0, 2004年9月30日)核心功能:重大升级,引入多项语法和功能革新。关键特性:泛型(Generics)。枚举(Enums)。注解(Annotations,如@Override)。自动装箱/拆箱(Autoboxing/Unboxing)。增强for循环(for-each)。可变参数(Varargs)。Concurrent包(java.util.concurrent,多线程优化)。7. Java 6(2006年12月11日)核心功能:改进性能和兼容性。关键特性:Scripting API(支持JavaScript等脚本语言)。Pluggable Annotation(自定义注解处理器)。JDBC 4.0(支持类型安全查询)。Drag-and-Drop API。8. Java 7(2011年7月28日)核心功能:语法和API的进一步简化。关键特性:Try-with-resources(自动资源管理)。钻石操作符(<>推断泛型类型)。NIO 2.0(增强文件系统API,java.nio.file)。Switch支持字符串(预览功能)。Fork/Join框架(并行任务处理)。9. Java 8(2014年3月18日)核心功能:函数式编程支持,引发Java生态巨大变革。关键特性:Lambda表达式(->语法)。Stream API(集合数据处理)。默认方法(接口中的默认实现)。新的日期时间API(java.time包)。Optional类(避免空指针异常)。重复注解(@Repeatable)。10. Java 9(2017年9月21日)核心功能:模块化系统(JPMS),Java首次引入模块化。关键特性:Jigsaw项目(模块化JDK,module-info.java)。JShell(交互式命令行工具)。HTTP客户端(java.net.http)。私有接口方法(接口内部可见方法)。集合工厂方法(List.of()等不可变集合)。11. Java 10(2018年3月20日)核心功能:短期发布周期(每6个月一次)的首个版本。关键特性:局部变量类型推断(var关键字)。垃圾回收器改进(G1成为默认GC)。并行Full GC(ZGC的预览)。12. Java 11(2018年9月25日,LTS)核心功能:长期支持(LTS)版本,企业级首选。关键特性:Epsilon垃圾收集器(无操作GC,用于测试)。HTTP客户端正式版(Java 9的预览功能升级)。Unicode 8.0支持。Deprecate Nashorn引擎(JavaScript引擎)。13. Java 14(2020年3月17日)核心功能:预览特性的快速迭代。关键特性:Records(数据类,预览)。Switch表达式(表达式式switch,预览)。Pattern Matching for instanceof(预览)。文本块(多行字符串,"""语法)。14. Java 15(2020年9月15日)核心功能:增强预览特性。关键特性:Records正式版。文本块正式版。隐藏类(jdk.internal.vm.hidden)。密封类(sealed,预览)。15. Java 16(2021年3月16日)核心功能:新特性正式化。关键特性:密封类正式版。弃用remove()方法(集合的remove()改为removeIf())。Vector API(Incubator)(SIMD指令支持)。模式匹配增强(instanceof与类型匹配结合)。16. Java 17(2021年9月14日,LTS)核心功能:LTS版本,企业长期支持。关键特性:Sealed Classes正式版。Switch表达式正式版。移除Java EE模块(如java.xml.bind)。强封装JDK内部API(--add-opens)。Pattern Matching for instanceof正式版。17. Java 18(2022年3月15日)核心功能:改进性能和可维护性。关键特性:虚拟线程(Virtual Threads)(预览,轻量级线程)。结构化并发框架(StructuredConcurrent API)。模式匹配switch(switch支持类型匹配)。18. Java 19(2022年9月19日)核心功能:语言和API优化。关键特性:虚拟线程(Virtual Threads)第二版(改进调度)。结构化并发增强(StructuredConcurrent改进)。记录模式(Record Patterns)(解构record数据)。19. Java 20(2023年3月21日)核心功能:性能和工具改进。关键特性:虚拟线程(Virtual Threads)正式版。强封装JDK内部API增强(--illegal-access)。模式匹配switch正式版。Vector API第二版(SIMD优化)。表格总结:Java版本关键特性对比版本 发布时间 LTS 核心特性 关键功能Java 1.0 1996年1月 否 首个版本 跨平台、基础类库、Applet支持Java 1.1 1997年2月 否 内部类、反射、RMI 增强GUI和网络功能Java 1.2 1998年12月 否 Java 2命名、集合框架、Swing GUI现代化、模块化结构Java 5 2004年9月 否 泛型、注解、枚举、增强for循环 语法革新,奠定现代Java基础Java 8 2014年3月 否 Lambda、Stream API、新日期API 函数式编程支持,生态转折点Java 9 2017年9月 否 模块化系统(JPMS)、JShell 模块化JDK,开发工具增强Java 11 2018年9月 是 HTTP客户端正式版、Epsilon GC 长期支持,企业级首选Java 14 2020年3月 否 Records、文本块、Switch表达式预览 预览特性快速迭代Java 17 2021年9月 是 Sealed Classes、Switch表达式正式版、移除Java EE模块 企业级LTS,语法和API现代化Java 20 2023年3月 否 虚拟线程正式版、模式匹配switch正式版 并发性能提升,SIMD优化关键总结LTS版本:Java 8、11、17 是企业长期支持版本。核心演进:语法革新:从Java 5的泛型到Java 8的Lambda,再到Java 14的Records。并发优化:Java 8的CompletableFuture到Java 17的虚拟线程。模块化:Java 9的JPMS彻底改变JDK结构。性能提升:G1 GC、ZGC、Vector API等持续优化。未来方向:虚拟线程(轻量级并发)、模式匹配、结构化并发框架的进一步发展。————————————————原文链接:https://blog.csdn.net/zp357252539/article/details/147495182
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签