优雅处理并发:Java CompletableFuture最佳实践
第1章:引言
大家好,我是小黑,今天,小黑要和大家聊聊CompletableFuture,这个Java 8引入的强大工具。
在Java传统的Future模式里,咱们都知道,一旦开始了一个异步操作,就只能等它结束,无法知道执行情况,也不能手动完成或者取消。而CompletableFuture呢,就像它的名字一样,是可以"完全控制"的Future。它提供了更多的控制,比如可以手动完成,可以处理异常,还可以把多个Future组合起来,进行更复杂的异步逻辑处理。
对于现代Java程序员来说,掌握CompletableFuture是必不可少的。无论是提高程序的响应性能,还是编写更加清晰、更具可读性的代码,它都能大显身手。
第2章:基本概念解读
那么,CompletableFuture到底是什么呢?简单来说,它是一种异步编程工具,可以帮助咱们在未来的某个时刻完成一个计算结果。与Future最大的不同是,它可以被显式地完成,意味着咱们可以在任何时候设置它的值。
让我们来看一个简单的例子。假设小黑要从网上查询某个产品的价格,这是一个耗时的操作,使用CompletableFuture,咱们就可以异步地完成这个任务:
import java.util.concurrent.CompletableFuture;public class CompletableFutureDemo {public static void main(String[] args) {// 创建一个CompletableFuture实例CompletableFuture<String> futurePrice = CompletableFuture.supplyAsync(() -> {// 模拟耗时操作,比如调用外部APIsimulateDelay();return "100元";});// 在这里,咱们可以做一些其他的事情,不必等待价格查询的结果doSomethingElse();// 当结果准备好后,获取它String price = futurePrice.join();System.out.println("价格是:" + price);}private static void simulateDelay() {try {Thread.sleep(1000); // 模拟1秒的延迟} catch (InterruptedException e) {Thread.currentThread().interrupt();}}private static void doSomethingElse() {// 做一些其他的事情System.out.println("小黑在做其他的事情...");}
}
在这个例子中,supplyAsync
方法创建了一个异步操作,模拟了一个耗时的价格查询过程。在查询价格的同时,主线程可以继续执行其他任务,比如doSomethingElse
方法里的内容。当价格查询完成后,可以使用join
方法来获取结果。这样的处理方式,让整个程序的执行效率大大提升,而且代码也更简洁明了。
CompletableFuture的美在于,它提供了一种新的编程范式,让咱们能够以声明式的方式描述复杂的异步逻辑。从上面的例子可以看出,CompletableFuture不仅让代码更加简洁,还让逻辑更加清晰,易于理解和维护。
第3章:创建CompletableFuture
1. 使用supplyAsync
最常见的创建方式是使用CompletableFuture.supplyAsync()
。这个方法需要一个Supplier
函数接口,通常用于执行异步计算。来看看小黑怎么用:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 模拟耗时的计算simulateTask("数据加载中");return "结果";
});
这个例子中,simulateTask
模拟了一个耗时操作,比如从数据库加载数据。使用supplyAsync
,咱们就能在另一个线程中执行这个任务,而主线程可以继续做其他事情。
2. 使用runAsync
如果咱们不关心异步任务的结果,只想执行一个异步操作,那就可以用runAsync
。它接受一个Runnable
函数接口,不返回任何结果:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {simulateTask("正在执行一些处理");
});
在这个例子里,simulateTask
只是执行了一些操作,比如记录日志或者发送通知,但不返回任何内容。
3. 手动完成
有时候,咱们可能需要手动完成一个Future。比如,基于某些条件判断,决定是否提前返回结果。这时候可以用complete
方法:
CompletableFuture<String> manualFuture = new CompletableFuture<>();
// 在某些条件下手动完成Future
if (checkCondition()) {manualFuture.complete("手动结果");
}
如果checkCondition
返回true
,那么这个Future就会被立即完成,否则它将保持未完成状态。
4. 组合使用
CompletableFuture真正的魅力在于它的组合能力。假设小黑有两个独立的异步任务,咱们可以这样组合它们:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {simulateTask("加载用户数据");return "用户小黑";
});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {simulateTask("加载配置信息");return "配置信息";
});// 组合两个future,等待它们都完成
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (user, config) -> {return "处理结果: " + user + "," + config;
});
在这个例子中,thenCombine
用于组合future1
和future2
的结果。只有当这两个Future都完成时,才会调用thenCombine
里的函数。
第4章:异步操作和链式调用
异步操作的力量
异步操作是指在一个线程中启动一个任务,让它在另一个线程中运行,从而不阻塞当前线程的执行。这在处理耗时任务时特别有用。举个例子,假设咱们要查询数据库,然后处理查询结果。如果同步执行,整个程序都得等着数据库查询完成,这就浪费了宝贵的时间。但如果用CompletableFuture实现异步,就可以在查询数据库的同时做其他事情。
链式调用的魅力
链式调用则是指一系列操作依次执行,前一个操作的结果作为下一个操作的输入。CompletableFuture支持多种链式调用方法,比如thenApply
, thenAccept
和thenRun
。
thenApply
用于处理和转换CompletableFuture的结果。thenAccept
用于消费CompletableFuture的结果,不返回新的CompletableFuture。thenRun
则不关心前一个任务的结果,只是在前一个任务执行完后,执行一些后续操作。
来看看小黑准备的例子:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {simulateTask("查询数据库");return "查询结果";
});future.thenApply(result -> {// 对结果进行处理return "处理后的结果:" + result;
}).thenAccept(processedResult -> {// 消费处理后的结果System.out.println("最终结果:" + processedResult);
}).thenRun(() -> {// 执行一些不需要前一个结果的操作System.out.println("所有操作完成");
});
在这个例子里,小黑用supplyAsync
启动了一个异步任务来查询数据库。然后用thenApply
处理查询结果,用thenAccept
消费处理后的结果,最后用thenRun
标记所有操作完成。
通过这种方式,咱们可以构建出复杂的异步逻辑,而代码却依然保持清晰和易于管理。这就是CompletableFuture的魅力所在。
第5章:异常处理
基本异常处理
在CompletableFuture的世界里,如果异步操作失败了,异常会被捕获并存储在Future对象中。咱们可以使用exceptionally
方法来处理这些异常。这个方法会返回一个新的CompletableFuture,它会在原来的Future抛出异常时执行。
来看个例子:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {if (new Random().nextBoolean()) {throw new RuntimeException("出错啦!");}return "正常结果";
}).exceptionally(ex -> {return "错误的回退结果:" + ex.getMessage();
});future.thenAccept(System.out::println);
这里,小黑创建了一个可能会失败的异步操作。如果抛出异常,exceptionally
方法就会被调用,返回一个包含错误信息的回退结果。
细粒度的异常处理
有时候,咱们可能需要更细粒度的控制,比如只处理特定类型的异常,或者在异常发生时还想继续其他操作。这时候,可以用handle
方法。它可以同时处理正常的结果和异常情况。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {if (new Random().nextBoolean()) {throw new RuntimeException("出错啦!");}return "正常结果";
}).handle((result, ex) -> {if (ex != null) {return "处理异常:" + ex.getMessage();}return "处理结果:" + result;
});future.thenAccept(System.out::println);
在这个例子中,无论异步操作是成功还是失败,handle
方法都会被调用。如果有异常,它会处理异常;如果没有,就处理正常结果。
管道式异常处理
CompletableFuture还允许咱们创建一个异常处理的“管道”,这样就可以把多个异步操作链接起来,并在链的任意位置处理异常。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 第一个异步操作return "第一步结果";
}).thenApply(result -> {// 第二个异步操作,可能会出错throw new RuntimeException("第二步出错啦!");
}).exceptionally(ex -> {// 处理异常return "在第二步捕获异常:" + ex.getMessage();
}).thenApply(result -> {// 第三个异步操作return "第三步使用结果:" + result;
});future.thenAccept(System.out::println);
在这个例子中,小黑创建了一个包含三个步骤的异步操作链。如果第二步出错,异常会被捕获并处理,然后处理结果被传递到第三步。
第6章:组合与依赖
组合多个Future
最常用的方法之一是thenCombine
。这个方法允许你组合两个独立的CompletableFuture,并且当它们都完成时,可以对它们的结果进行一些操作。
来看个例子:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {simulateTask("加载用户信息");return "用户小黑";
});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {simulateTask("加载订单数据");return "订单123";
});CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (userInfo, orderInfo) -> {return "合并结果:" + userInfo + "," + orderInfo;
});combinedFuture.thenAccept(System.out::println);
在这个例子中,future1
和future2
代表两个独立的异步操作。只有当两者都完成时,thenCombine
里面的函数才会执行,并且合并它们的结果。
依赖关系的处理
如果你的一个异步操作依赖于另一个异步操作的结果,那么可以使用thenCompose
方法。这个方法允许你在一个Future完成后,以其结果为基础启动另一个异步操作。
CompletableFuture<String> masterFuture = CompletableFuture.supplyAsync(() -> {simulateTask("获取主数据");return "主数据结果";
});CompletableFuture<String> dependentFuture = masterFuture.thenCompose(result -> {return CompletableFuture.supplyAsync(() -> {simulateTask("处理依赖于" + result + "的数据");return "处理后的数据";});
});dependentFuture.thenAccept(System.out::println);
这个例子中,dependentFuture
的执行依赖于masterFuture
的结果。
处理多个Future
有时候,咱们可能有多个异步操作,需要等所有操作都完成后再进行下一步。这时候,可以使用CompletableFuture.allOf
。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {simulateTask("任务一");return "结果一";
});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {simulateTask("任务二");return "结果二";
});CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);allFutures.thenRun(() -> {System.out.println("所有任务完成");
});
allOf
会等待所有提供的Futures完成,然后执行后续操作。
第7章:最佳实践
1. 明智地选择异步任务执行方式
CompletableFuture提供了多种执行异步任务的方法,比如runAsync
和supplyAsync
。默认情况下,它们使用公共的ForkJoinPool,但在某些场景下,你可能想要使用自定义的线程池来更好地控制资源。
ExecutorService customExecutor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {return "使用自定义线程池";
}, customExecutor);
这样做可以让你更好地管理线程资源,尤其是在处理大量异步任务时。
2. 谨慎处理阻塞操作
如果你的CompletableFuture链中包含阻塞调用,如数据库操作或文件I/O,最好是将这些操作放在独立的线程池中,避免阻塞ForkJoinPool中的线程。
ExecutorService dbExecutor = Executors.newCachedThreadPool();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {// 这里是阻塞的数据库操作simulateTask("数据库操作");
}, dbExecutor);
这样可以防止长时间的阻塞操作占用过多的计算资源,影响整体性能。
3. 组合异步操作时的错误处理
当你组合多个CompletableFuture时,记得对每一个Future都进行错误处理。这样可以避免一个未捕获的异常破坏整个操作链。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "任务1").exceptionally(ex -> "默认值1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "任务2").exceptionally(ex -> "默认值2");CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " 和 " + result2);
这样做确保了即使其中一个操作失败,整个链也可以继续执行。
4. 避免过多的链式调用
虽然链式调用是CompletableFuture的一个强大特性,但过度使用可能会导致代码难以理解和维护。建议把复杂的逻辑分解成多个方法或类。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "原始数据").thenApply(this::step1).thenApply(this::step2).thenApply(this::step3);// 将每个步骤的逻辑封装在不同的方法中
private String step1(String data) {return "处理1:" + data;
}private String step2(String data) {return "处理2:" + data;
}private String step3(String data) {return "处理3:" + data;
}
第8章:总结
-
异步编程的强大工具:CompletableFuture为Java异步编程提供了强大的支持,让处理并发任务变得更简单、更灵活。
-
简化复杂逻辑:通过链式调用和组合多个异步任务,CompletableFuture能够帮助咱们以清晰的方式处理复杂的业务逻辑。
-
异常处理的优雅方式:CompletableFuture提供了一套完整的异常处理框架,让咱们能够更好地控制和管理异步代码中的错误情况。
相关文章:
优雅处理并发:Java CompletableFuture最佳实践
第1章:引言 大家好,我是小黑,今天,小黑要和大家聊聊CompletableFuture,这个Java 8引入的强大工具。 在Java传统的Future模式里,咱们都知道,一旦开始了一个异步操作,就只能等它结束…...
熟悉HDFS常用操作
1. 利用Hadoop提供的Shell命令完成下列任务 (1)向HDFS中上传任意文本文件,如果指定的文件在HDFS中已经存在,由用户指定是追加到原有文件末尾还是覆盖原有的文件。 #检查文件是否存在./bin/hdfs dfs -test -e text.txt echo $? #结果是1 代表已存在 #根据结果判断出文件已存…...
Adobe XD是什么?探索这款创新的用户体验设计工具
Adobexd是一种基于矢量的设计工具,主要用于设计移动和Web应用程序的用户界面(UI)。与Photoshop或ilustrator等其他Adobe产品相比,它相当轻。对于对快速设计和原型迭代感兴趣的界面设计师来说,轻量级并不是一件坏事。 在早期,Adob…...
java常用应用程序编程接口(API)——ArrayList概述及使用案例
前言: 学习了ArrayList,整理下ArrayList的常用功能及基本案例。打好基础,daydayup! 如果需要了解API是什么并如何使用,可以看这篇文章。 java常用应用程序编程接口(API)——String概述及使用案…...
2024年了,Layui再战三年有问题不?
v2.9.3 2023-12-31 2023 收官。 form 优化 input 组件圆角时后缀存在方框的问题 #1467 bxjt123优化 select 搜索面板打开逻辑,以适配文字直接粘贴触发搜索的情况 #1498 Sight-wcgtable 修复非常规列设置 field 表头选项时,导出 excel 出现合计行错位的…...
消息队列-RocketMQ-概览与搭建
RocketMQ 领域模型 RockeMQ整体结构预览 RocketMQ 中的一些概念 Topic:主题,可以理解为类别、分类的概念 MessageQueue:消息队列,存储数据的一个容器(队列索引数据),默认每个 Topic 下有 4 个队…...
Vue3技术解析(小册子)
随着 Vue 3 正式版本的发布,未来 Vue 3 将会成为前端的主流框架,这个毋庸置疑。Vue 3 在使用方面会兼容部分 Vue 2.x 的特性,比如 options API。 所以,究竟是要先学习 Vue 2 打好基础,还是直接学习 Vue 3 呢ÿ…...
即将消失的五种编程语言?
1. Ruby Ruby 在 1999 年发布后立即受到程序员们的热捧,它能够快速构建应用程序的特性给程序员留下了非常深刻的印象。紧随其后,备受欢迎的 Ruby on Rails 框架于 2004 年发布,由于 Ruby 和 Rails 这两个名称在当时几乎成为了同义词…...
c++学习:STL库(框架)+字符串模板类string+vector容器+list链表
目录 stl库 常用组件包括 字符串库 字符串模板类string 头文件 最常用的字符串模板类 字符串类型 模板原型 模板的成员数据类型 模板成员函数 有些函数会有重载,可以去下面网址查看std::basic_string - cppreference.comhttps://zh.cppreference.com/w/cp…...
2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷④
2023年全国职业院校技能大赛(高职组) “云计算应用”赛项赛卷4 目录 需要竞赛软件包环境以及备赛资源可私信博主!!! 2023年全国职业院校技能大赛(高职组) “云计算应用”赛项赛卷4 模块一 …...
使用Scikit Learn 进行识别手写数字
使用Scikit Learn 进行识别手写数字 作者:i阿极 作者简介:数据分析领域优质创作者、多项比赛获奖者:博主个人首页 😊😊😊如果觉得文章不错或能帮助到你学习,可以点赞👍收藏…...
GB/T 15036-2018 实木地板检测
实木地板是指未经拼接、覆贴的单块木材直接加工而成的地板,实木地板具有脚感舒适,环保等优良的性能,在家庭装修中被广泛使用,尤其是在国内很受欢迎。 GB/T 15036-2018 实木地板测试介绍: 测试项目 测试方法 外观 G…...
基于ElementUI封装的下拉树选择可搜索单选多选清空功能
效果: 组件代码 /*** 树形下拉选择组件,下拉框展示树形结构,提供选择某节点功能,方便其他模块调用* author wy* date 2024-01-03 * 调用示例:* <tree-select * :height"400" // 下拉框中树形高度* …...
计算机网络-各层协议
大家在搞嵌入式开发的时候基本都了解过七层网络协议、五层网络协议、四层网络协议,那么今天让我们更加的深入了解一下: 历史发展介绍 OSI七层模型由ISO国际标准化组织提出的通信标准。TCP/IP四层模型是OSI七层模型的简化版,OSI在它被官方完…...
LeetCode 84:柱状图中的最大矩形
一、题目描述 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 求在该柱状图中,能够勾勒出来的矩形的最大面积。 示例 1: 输入:heights [2,1,5,6,2,3] 输出:10 解释:…...
老生重谈:大模型的「幻觉」问题
一、什么是大模型「幻觉」 大模型的幻觉问题通常指的是模型在处理输入时可能会产生一些看似合理但实际上是错误的输出,这可能是因为模型在训练时过度拟合了训练数据,导致对噪声或特定样本的过度敏感。 "大数据幻觉"指的是在处理大规模数据时…...
golang实现skiplist 跳表
跳表 package mainimport ("errors""math""math/rand" )func main() {// 双向链表///**先理解查找过程Level 3: 1 6Level 2: 1 3 6Level 1: 1 2 3 4 6比如 查找2 ; 从高层往下找;如果查找的值比当前值小 说明没有可查找的值2比1大 往当前…...
尝试OmniverseFarm的最基础操作
目标 尝试OmniverseFarm的最基础操作。本地机器作为Queue和Agent,同时在本地提交任务。 主要参考了官方文档: Farm Queue — Omniverse Farm latest documentation Farm Agent — Omniverse Farm latest documentation Farm Examples — Omniverse Far…...
第28关 k8s监控实战之Prometheus(二)
------> 课程视频同步分享在今日头条和B站 大家好,我是博哥爱运维。 这节课我们用prometheus-operator来安装整套prometheus服务 https://github.com/prometheus-operator/kube-prometheus/releases 开始安装 1. 解压下载的代码包 wget https://github.com/…...
基于 SpringBoot + magic-api + Vue3 + Element Plus + amis3.0 快速开发管理系统
Tansci-Boot 基于 SpringBoot2 magic-api Vue3 Element Plus amis3.0 快速开发管理系统 Tansci-Boot 是一个前后端分离后台管理系统, 前端集成 amis 低代码前端框架,后端集成 magic-api 的接口快速开发框架。包含基础权限、安全认证、以及常用的一…...
Kafka(四)Broker
目录 1 配置Broker1.1 Broker的配置broker.id0listererszookeeper.connectlog.dirslog.dir/tmp/kafka-logsnum.recovery.threads.per.data.dir1auto.create.topics.enabletrueauto.leader.rebalance.enabletrue, leader.imbalance.check.interval.seconds300, leader.imbalance…...
代码随想录第五十二天——最长递增子序列,最长连续递增序列,最长重复子数组
leetcode 300. 最长递增子序列 题目链接:最长递增子序列 dp数组及下标的含义 dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度递推公式 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 1 的最大值 所以if (nums[i] > nums[j]) dp[i]…...
【大数据架构】OLAP实时分析引擎选型
OLAP引擎面临的挑战 常见OLAP引擎对比 OLAP分析场景中,一般认为QPS达到1000就算高并发,而不是像电商、抢红包等业务场景中,10W以上才算高并发,毕竟数据分析场景,数据海量,计算复杂,QPS能够达到1…...
代码随想录刷题题Day29
刷题的第二十九天,希望自己能够不断坚持下去,迎来蜕变。😀😀😀 刷题语言:C Day29 任务 ● 01背包问题,你该了解这些! ● 01背包问题,你该了解这些! 滚动数组 …...
CVE-2023-51385 OpenSSH ProxyCommand命令注入漏洞
一、背景介绍 ProxyCommand 是 OpenSSH ssh_config 文件中的一个配置选项,它允许通过代理服务器建立 SSH 连接,从而在没有直接网络访问权限的情况下访问目标服务器。这对于需要经过跳板机、堡垒机或代理服务器才能访问的目标主机非常有用。 二、漏洞简…...
如何寻找到相对完整的真正的游戏的源码 用来学习?
在游戏开发的学习之路上,理论与实践是并重的两个方面。对于许多热衷于游戏开发的学习者来说,能够接触到真实的、完整的游戏源码无疑是一个极好的学习机会。但问题来了:我们该如何寻找到这些珍贵的资源呢? 开源游戏项目 GitHub:地…...
数模学习day11-系统聚类法
本文参考辽宁石油化工大学于晶贤教授的演示文档聚类分析之系统聚类法及其SPSS实现。 目录 1.样品与样品间的距离 2.指标和指标间的“距离” 相关系数 夹角余弦 3.类与类间的距离 (1)类间距离 (2)类间距离定义方式 1.最短…...
SpringBoot+Redis实现接口防刷功能
场景描述: 在实际开发中,当前端请求后台时,如果后端处理比较慢,但是用户是不知情的,此时后端仍在处理,但是前端用户以为没点到,那么再次点击又发起请求,就会导致在短时间内有很多请求…...
TensorRT加速推理入门-1:Pytorch转ONNX
这篇文章,用于记录将TransReID的pytorch模型转换为onnx的学习过程,期间参考和学习了许多大佬编写的博客,在参考文章这一章节中都已列出,非常感谢。 1. 在pytorch下使用ONNX主要步骤 1.1. 环境准备 安装onnxruntime包 安装教程可…...
springboot常用扩展点
当涉及到Spring Boot的扩展和自定义时,Spring Boot提供了一些扩展点,使开发人员可以根据自己的需求轻松地扩展和定制Spring Boot的行为。本篇博客将介绍几个常用的Spring Boot扩展点,并提供相应的代码示例。 1. 自定义Starter(面试常问) Sp…...
wordpress海外建站/南京疫情最新消息
转:nohup 和>/dev/null 2>&1 一、用途:nohup表示永久运行。&表示后台运行 在应用Unix/Linux时,我们一般想让某个程序在后台运行,nohup ./start-mysql.sh & 该命令的一般形式为:nohup command & …...
手机网站制作教程视频教程/肇庆网络推广
Kubernetes 全面拥抱微服务架构,其具备良好的横向扩容能力,并构建在 Google 15 年生产环境经验、每周运行数 10 亿个容器的目标基础之上。Kubernetes 很好的结合了来自社区的创意和最佳实践。Kubernetes 是目前唯一被业界广泛认可的 Docker 分布式解决方…...
中建八局劳务派遣招聘/成都seo专家
关于复合索引中的2个索引列谁在前谁在后的进一步讨论--实践篇: 上一次在长老的QQ群里边说了这么一个例子: create table test_pk( id varchar2(10), create_dt date);alter table test_pk modify (id varchar2 (30 ));insert into test_pk select obje…...
哪里有做网站公司/seo怎么学
爱情没有如果很多人喜欢给爱情做假设。 他说,如果你可以温柔点,我可以更爱你,不会轻易放弃你。 她说,如果你可以体贴点,我可以更珍惜你,我也不会离开你。 但,终究在一个时间的定格中交错走开。 …...
留言网站建设/百度seo关键词优化推荐
在Delphi中,选择一个文件夹的操作主要有两种方法。一种是通过“打开”对话框(OpenDialog)控件,通过定位一个文件来间接实现。另一种是利用Delphi提供的SelectDirectory函数。这个函数是在FileCtrl单元中定义的。 第二种方法还有一…...
网站开发 图标/单页网站怎么优化
1.enumerate:返回2个值,1是当前的for循环的第几轮,2是循环得到的数值enumerate works by supplying a corresponding index to each element in the list that you pass it. Each time you go through the loop, index will be one greater, …...