I'm Hunter.
lq920320 / blogs Goto Github PK
View Code? Open in Web Editor NEWBlogs of personal.
Blogs of personal.
在这里我来分享几种列表去重的方法,如有纰漏,请不吝赐教。
distinct()
方法distinct()
是Java 8 中 Stream 提供的方法,返回的是由该流中不同元素组成的流。distinct()
使用 hashCode()
和 eqauls()
方法来获取不同的元素。因此,需要去重的类必须实现 hashCode()
和 equals()
方法。换句话讲,我们可以通过重写定制的 hashCode()
和 equals()
方法来达到某些特殊需求的去重。
distinct()
方法声明如下:
Stream<T> distinct();
String
列表的去重因为 String
类已经覆写了 equals()
和 hashCode()
方法,所以可以去重成功。
@Test
public void listDistinctByStreamDistinct() {
// 1. 对于 String 列表去重
List<String> stringList = new ArrayList<String>() {{
add("A");
add("A");
add("B");
add("B");
add("C");
}};
out.print("去重前:");
for (String s : stringList) {
out.print(s);
}
out.println();
stringList = stringList.stream().distinct().collect(Collectors.toList());
out.print("去重后:");
for (String s : stringList) {
out.print(s);
}
out.println();
}
结果如下:
去重前:AABBC
去重后:ABC
注:代码中我们使用了 Lombok
插件的 @Data
注解,可自动覆写 equals()
以及 hashCode()
方法。
/**
* 定义一个实体类
*/
@Data
public class Student {
private String stuNo;
private String name;
}
@Test
public void listDistinctByStreamDistinct() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 1. 对于 Student 列表去重
List<Student> studentList = getStudentList();
out.print("去重前:");
out.println(objectMapper.writeValueAsString(studentList));
studentList = studentList.stream().distinct().collect(Collectors.toList());
out.print("去重后:");
out.println(objectMapper.writeValueAsString(studentList));
}
结果如下:
去重前:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"},{"stuNo":"001","name":"Tom"}]
去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"002","name":"Mike"}]
List<Object>
中 Object
某个属性去重 @Test
public void distinctByProperty1() throws JsonProcessingException {
// 这里第一种方法我们通过新创建一个只有不同元素列表来实现根据对象某个属性去重
ObjectMapper objectMapper = new ObjectMapper();
List<Student> studentList = getStudentList();
out.print("去重前 :");
out.println(objectMapper.writeValueAsString(studentList));
studentList = studentList.stream().distinct().collect(Collectors.toList());
out.print("distinct去重后:");
out.println(objectMapper.writeValueAsString(studentList));
// 这里我们引入了两个静态方法,以及通过 TreeSet<> 来达到获取不同元素的效果
// 1. import static java.util.stream.Collectors.collectingAndThen;
// 2. import static java.util.stream.Collectors.toCollection;
studentList = studentList.stream().collect(
collectingAndThen(
toCollection(() -> new TreeSet<>(Comparator.comparing(Student::getName))), ArrayList::new)
);
out.print("根据名字去重后 :");
out.println(objectMapper.writeValueAsString(studentList));
}
结果如下:
去重前 :[{"stuNo":"001","name":"Tom"},{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
distinct去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
根据名字去重后 :[{"stuNo":"001","name":"Tom"}]
filter()
方法我们首先创建一个方法作为 Stream.filter()
的参数,其返回类型为 Predicate
,原理就是判断一个元素能否加入到 Set
中去,代码如下:
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
使用如下:
@Test
public void distinctByProperty2() throws JsonProcessingException {
// 这里第二种方法我们通过过滤来实现根据对象某个属性去重
ObjectMapper objectMapper = new ObjectMapper();
List<Student> studentList = getStudentList();
out.print("去重前 :");
out.println(objectMapper.writeValueAsString(studentList));
studentList = studentList.stream().distinct().collect(Collectors.toList());
out.print("distinct去重后:");
out.println(objectMapper.writeValueAsString(studentList));
// 这里我们将 distinctByKey() 方法作为 filter() 的参数,过滤掉那些不能加入到 set 的元素
studentList = studentList.stream().filter(distinctByKey(Student::getName)).collect(Collectors.toList());
out.print("根据名字去重后 :");
out.println(objectMapper.writeValueAsString(studentList));
}
结果如下:
去重前 :[{"stuNo":"001","name":"Tom"},{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
distinct去重后:[{"stuNo":"001","name":"Tom"},{"stuNo":"003","name":"Tom"}]
根据名字去重后 :[{"stuNo":"001","name":"Tom"}]
以上便是我要分享的几种关于列表去重的方法,当然这里没有进行更为详尽的性能分析,希望以后会深入底层再重新分析一下。如有纰漏,还望不吝赐教。
代码地址:github
在过去的几年里,当有了关于人工智能(AI),大数据以及分析学的讨论之后,术语“深度学习”已然进入了商业语言的范畴。而且有充分的理由——这是通往AI的道路,而人工智能在开发使许多行业发生革命性变化的自主自学系统方面显现出巨大的前景。
谷歌的语音识别和图像识别算法,Netflix 和亚马逊针对你接下来想看的或者想买的决策上,以及MIT的研究员预测未来上,都用到了深度学习。一直以来,销售这些工具的不断增长的行业总是热衷于谈论其如何带来的革命。
在上一篇文章中我写了和关于AI和机器学习的不同之处。而机器学习经常被认为是人工智能的子学科,比较好的观点是,把它作为人工智能目前最先进的一个领域,如今正在为各个行业以及社会可以用来驱动变革的工具展示着最大的潜能。
那么接下来,可能对理解深度学习是前沿(技术)的前沿非常有帮助。ML涉及到几个人工智能核心的创意并集中于如何用这些创意,通过模仿人类自己决策的神经网络来解决现实世界中的问题。深度学习则是对机器学习工具及技术的较小子范畴更为专注,并把它们应用于解决一切需要“思考”的问题——无论是人类去思考还是人工去思考。
实质上深度学习意味着要给计算机系统“补给”大量数据,用以对其他数据做出决策。这类数据通过神经网络提供,就像机器学习的案例一样。这些逻辑网络对每一位通过的数据或问一系列的是非问题,或提取数字值,然后根据得到的答案进行分类。
因为深度学习专注于发展这些网络,它们便以深度神经网络(Deep Neural Networks)为人所知,逻辑网络的复杂性在于需要解决对大量级数据集的处理,比如谷歌的图片库,或者推特的推文流水量。
类似于此巨大的数据集,以及足够复杂的逻辑网络来解决它们的分类问题,故对于计算机来说,将其表示的图像和状态以很高的准确率呈现给人类是微不足道的。
图片是来说明工作原理的很好的例子,因为它们包含很多不同的元素,并且掌握一台计算机如何用它的单轨、专注计算的内核学习像人类那样理解这些图片,对我们来说并非易事。但是深度学习可以应用于任何数据格式——机器信号,音频,视频,口语,书面语——并且非常非常快地得出和人类相似的结论。让我们看一个实际的例子。
一个系统用以自动记录和报告某个特定车型的汽车在某条公路上有多少辆驶过。首先,会给它提供访问某个庞大的汽车类型数据库的权限,包括它们的外形,尺寸,甚至是引擎的声音。这(数据)可以手动编写,或者更为高级的情况,如果系统被编程为搜索互联网,那么就能自动收集,并提取其找到的数据。
接下来它会取一些需要处理的数据——真实世界的数据,其中包含各种视角,在这种情况下就要通过路边摄像头和麦克风来计算。通过对比从它的传感器得到数据和“学习”获得的数据,它可以根据驶过车辆的牌子和型号进行分类,而且具有一定准确性。
到目前为止都是非常简单的。“深度”在何处体现呢?系统随着时间的推移,它会获取更多经验,可以提升准确分类的概率,通过得到的新数据来“训练”自己。换句话说,它也可以像我们一样从错误中学习。比如,它不能根据一些车辆相似的外形和引擎声正确地判断它们的牌子和型号,忽略了另一个差异因素,它认为对决策的重要性很低。通过了解这个区别对于理解两辆车之间的差异是至关重要的,这会提高下一次得到正确结果的可能性。
可能结束这篇文章最好的方式便是给出为何有如此多的突破,然后给出更多的在当今如何使用深度学习的例子。一些优秀的应用正在投入使用或者正在工作,包括:
自动驾驶汽车的导航。利用传感器和板载分析,汽车适当地运用机器学习来学习识别障碍并对其作出反应。
给图像褪色。通过让计算机分辨物体以及了解它们在人类眼中的样子,图片和视频可以被计算机转成黑白的。
预测法律程序的结果。一个由英美研究人员开发的系统最近被证明,当为其提供案子的一些基本事实之后,该系统可以正确地预测法庭的判决。
精准医学。深度学习正被用于开发适合个体基因组的药物。
自动分析报告。系统可以分析数据并且用人类的自然语言提供意见报告,并提供我们可以轻易理解的图表。
打游戏。深度学习系统曾被教授打(然后赢得)游戏,比如棋盘游戏GO,和Atari的电子游戏Breakout。
当讨论(尤其是,出售)这些前沿技术的时候,很容易因为经常使用的炒作和夸张而得意忘形。但事实上,都是应该的。因为听到数据科学家谈论他们拥有的工具和技术并不常见,他们也不希望这么快出现看到这一景象,并且很多都是归功于机器学习和深度学习所取得的进步。
在使用查询语句时,有时候需要用到关联查询,查询结果会出现group by一列出现对应数行的情况。
sql如下:
select * from table group by 列字段;
合并查询出的列:
select GROUP_CONCAT(查询的字段 separator ';') from table group by 列字段;
其中GROUP_CONCAT()
方法可以直接使用GROUP_CONCAT(cloum)
默认以","相隔,亦可以自定义分割符,如GROUP_CONCAT(cloum separator ';')
。
人工智能(AI)和机器学习是当下最热门的两个词,而且含义上经常被互换使用。
虽然它们并不等同,但是有时候在感觉上也是会造成一定混乱的。所以我认为还是值得写一篇文章来解释它们的不同之处。
两者频繁出现的时刻都是在谈论的话题如大数据,分析学以及正席卷全球的技术变革的广泛冲击。
简言之,最好的答案是:
机器可以用一种我们认为“聪明”的方式去完成任务,人工智能则是其更宽泛的概念。
以及,
AI是基于我们应该仅让机器访问数据,然后它们便能自己学习这一创意,机器学习只是当前的一种应用。
人工智能已经存在很长时间了——希腊神话中也有机械人模仿人类行为的故事。早期的欧洲的计算机是被设想为“逻辑机器”的,由于类似基础算术和记忆的再现能力,工程师从根本上把自己的工作视为试图创造机械的大脑。
由于当代技术,这是很重要的,我们对于人类思维运作的理解也在发展,从而关于AI构成的概念亦发生了变化。AI领域的工作,集中在模仿人类的决策产生过程以及更多地以人类的方式去完成任务,而不是持续增长的复杂计算。
人工智能——旨在表现得智能的设备——经常被归为应用的或通用这两个基本组别的之一。应用AI更为常见——系统设计得更为智能地交易股票和股份,或者自动驾驶车辆的演示也属于这一类。
一般的人工智能——系统也好设备也好,理论上可以处理任何任务,然而并不常见。但这也是当今正在发生的最令人激动的可进步之处,同时也是带动机器学习发展的领域。经常作为AI的子领域被提及,把(机器学习)当做是目前最新的技术的确更为准确。
两项重要突破导致了机器学习的出现,而这也正驱动着AI以目前的(飞快)速度向前发展。
二者之一是——曾在1959年由Arthur Samuel提出这一概念,(机器学习)不仅仅是传授给计算机它们需要了解的一切以及怎样完成任务,更是产生了它们自学的可能性。
第二项突破则距今不远,那就是互联网的出现,以及数字信息在数量上的剧增,存储,并且可用于分析。
曾经这些创新提出之后,工程师们意识到不单单是教给计算机和机器怎么做事,甚至可以通过编程让它们像人类一样思考,然后将它们加入互联网以获得世界上所有的信息。
神经网络的发展已成为教授计算机用人类的方式思考和理解世界的关键,与此同时它们还有着优于我们的天赋,比如(计算)速度,准确性,以及毫无偏见。
一个神经网络就是一套计算机系统,旨在以和人类大脑分类信息的方式运行。依靠含有的“神经元”,它们可以学会辨认类似图片并且把图片进行分类。
实质上它运行在一套概率系统上,这套系统由数据支撑,并且可以发表声明,做出决定,或者给出有一定把握的预测。一个反馈回路可以通过传感或者被告知进行“学习”,这一附加属性,无论它的决定是否正确,都会改变未来所使用的方法。
机器学习应用可以阅读文章并判别作者的意图是给出投诉还是表达祝贺。它们还可以听一段音乐,然后判定这段音乐是否可能让人开心或者难过,之后找出其他的音乐来匹配情感。在一些例子中,它们甚至可以谱出自己的音乐来表达同样的主题,或者用它们熟知的一些会被那些发烧友喜爱的音乐原件。
这些所有的可能性都由基于机器学习和神经网络的系统提供。非常感谢科幻小说,我们或许可以与电子设备和数字信息相互交流、影响,这一设想也被照进了现实,正如我们和一个人类所做的那样自然。最后,AI的另一个领域——自然语言处理(Natural Language Processing (NLP))——近年来已经成为那些让人非常激动革新的原动力,并且是非常依赖ML的领域之一。
NLP应用试图去理解正常的人类交流,不论是书面还是口头的,并且用和人类相似的自然语言来回应。ML在这里用来帮助机器理解人类语言的中大量的细微差别之处,以及学习用一种能让某位特定听众理解的方式去回应对方。
人工智能——尤其是当今的机器学习(ML)自然能提供很多东西。由于其达成重复劳动自动化以及提供创造性眼光的承诺,从银行业到医疗与制造业等各个行业都在从中受益。因此,重要的是要记住,AI和ML是别的东西……而它们是正被出售的产品,一直都是,且有利可图。
机器学习当然已被营销人员抓住了机遇。AI已经出现了如此之久,而在其潜力真正被发掘之前,它甚至可能开始被视作“老一套”的东西。在“AI革命”开始出现一些错误的开始,然后“机器学习”这一术语给了营销人员一些新鲜、闪亮的东西,并且重要的是,牢牢扎根于此时此地。
事实就是我们最终会开发出与人类相似的AI,而这也经常被技术人员视为不可避免的东西。当然,今天我们以与日俱增的速度,未曾如此的靠近这一目标。近些年来我们所看到的许多令人激动的进展,正是由于我们如何设想AI运行的根本改变,同时也是ML所带来的改变。我希望这篇文章能够帮助一些人理解AI与ML之间的区别。
如果你的表类型是InnoDB,那么,新纪录的ID为15;如果你的表类型是MyISAM,那么,新纪录的ID为18。
这是因为:InnoDB类型的数据表将表最后的ID值保存在内存里面。所以,当我们重新启动服务器后,内存里面的数据清空,那么自增的ID将重新按照现有表的纪录计算;相反,如果是MyISAM类型的数据表,将最大纪录ID保持在文件里,这样,虽然,重启了服务器,下次插入新纪录的时候,自增ID通过读取文件而计算得到。
LeetCode
题目简单了解一下位运算在面试的准备过程中,刷算法题算是必修课,当然我也不例外。某天,我刷到了一道神奇的题目:
# 136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
我不禁眉头一皱,心说,这还不简单,三下五除二写下如下代码:
/**
* HashMap
*
* @param nums 数组
* @return 结果
*/
public int solution(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.remove(num);
} else {
map.put(num, 1);
}
}
return map.entrySet().iterator().next().getKey();
}
接着,我看到了另外一道题目:
# 137. 只出现一次的数字 II
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,3,2]
输出: 3
示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99
我不禁眉头又一皱,心说,好像是同样的套路,便写下了如下代码:
/**
* 使用Map,存储key以及出现次数
*
* @param nums 数组
* @return 出现一次的数字
*/
public int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
for (Integer key : map.keySet()) {
if (map.get(key) == 1) {
return key;
}
}
return 0;
}
然后,就出现了终极题目:
# 260. 只出现一次的数字 III
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
示例 :
输入: [1,2,1,3,2,5]
输出: [3,5]
注意:
1. 结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
2. 你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?
我不禁又皱了一下眉头,心说,嗯……接着便写下如下代码:
/**
* 使用Map,存储key以及出现次数
*
* @param nums 数组
* @return 出现一次的数字的数组
*/
public int[] singleNumber(int[] nums) {
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
int i = 0;
for (Integer key : map.keySet()) {
if (map.get(key) == 1) {
result[i] = key;
i++;
}
}
return result;
}
用几乎同一种思路做了三道题,不得不夸一下自己:
做完这三道题目,提交了答案之后,执行用时和内存消耗都只超过了 10% 的解题者。不由得眉头紧锁(终于知道自己为啥抬头纹这么深了),发现事情并没有这么简单……
之后我又找了一下其他解法,如下:
/**
* #136 根据题目描述,由于加上了时间复杂度必须是 O(n) ,并且空间复杂度为 O(1) 的条件,因此不能用排序方法,也不能使用 map 数据结构。答案是使用 位操作Bit Operation 来解此题。
* 将所有元素做异或运算,即a[1] ⊕ a[2] ⊕ a[3] ⊕ …⊕ a[n],所得的结果就是那个只出现一次的数字,时间复杂度为O(n)。
* 根据异或的性质 任何一个数字异或它自己都等于 0
*
* @param nums 数组
* @return 结果
*/
private int solution(int[] nums) {
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
}
/**
* #137 嗯……这个我们下面再做详解
* 这里使用了异或、与、取反这些运算
*
* @param nums 数组
* @return 出现一次的数字
*/
public int singleNumber2(int[] nums) {
int a = 0, b = 0;
int mask;
for (int num : nums) {
b ^= a & num;
a ^= num;
mask = ~(a & b);
a &= mask;
b &= mask;
}
return a;
}
/**
* #260 在这里把所有元素都异或,那么得到的结果就是那两个只出现一次的元素异或的结果。
* 然后,因为这两个只出现一次的元素一定是不相同的,所以这两个元素的二进制形式肯定至少有某一位是不同的,即一个为 0 ,另一个为 1 ,现在需要找到这一位。
* 根据异或的性质 任何一个数字异或它自己都等于 0 ,得到这个数字二进制形式中任意一个为 1 的位都是我们要找的那一位。
* 再然后,以这一位是 1 还是 0 为标准,将数组的 n 个元素分成两部分。
* 1. 将这一位为 0 的所有元素做异或,得出的数就是只出现一次的数中的一个
* 2. 将这一位为 1 的所有元素做异或,得出的数就是只出现一次的数中的另一个。
* 这样就解出题目。忽略寻找不同位的过程,总共遍历数组两次,时间复杂度为O(n)。
*
* 使用位运算
*
* @param nums 数组
* @return 只出现一次数字的数组
*/
public int[] singleNumber2(int[] nums) {
int diff = 0;
for (int num : nums) {
diff ^= num;
}
// 得到最低的有效位,即两个数不同的那一位
diff &= -diff;
int[] result = new int[2];
for (int num : nums) {
if ((num & diff) == 0) {
result[0] ^= num;
} else {
result[1] ^= num;
}
}
return result;
}
看完上面的解法,我脑海中只有问号的存在,啥意思啊?!
下面就让我们简单了解一下位运算并解析一下这三道题目。
异或逻辑的关系是:当AB不同时,输出P=1;当AB相同时,输出P=0。“⊕”是异或数学运算符号,异或逻辑也是与或非逻辑的组合,其逻辑表达式为:P=A⊕B。在计算机语言中,异或的符号为“ ^ ”。
异或运算 A ⊕ B 的真值表如下:
A | B | ⊕ |
---|---|---|
F | F | F |
F | T | T |
T | F | T |
T | T | F |
所以我们从 #136
题解中了解,通过异或运算,两个相同的元素结果为 0,而 任何数 与 0 进行异或操作,结果都为其本身。
“与”运算是计算机中一种基本的逻辑运算方式,符号表示为 “&”,参加运算的两个数据,按二进制位进行“与”运算。运算规则:0&0=0;0&1=0;1&0=0;1&1=1;即:两位同时为“1”,结果才为“1”,否则为0。另,负数按补码形式参加按位与运算。
与运算 A & B 的真值表如下:
A | B | & |
---|---|---|
F | F | F |
F | T | F |
T | F | F |
T | T | T |
“与运算”的特殊用途:
清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
取一个数的指定位
方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。例:设 X=10101110,取X的低4位,用 X & 0000 1111 = 0000 1110
即可得到;还可用来取 X 的2、4、6位。
参加运算的两个对象,按二进制位进行“或”运算。运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;即 :参加运算的两个对象只要有一个为1,其值为1。另,负数按补码形式参加按位或运算。
或运算 A | B 的真值表如下:
A | B | | |
---|---|---|
F | F | F |
F | T | T |
T | F | T |
T | T | T |
或运算特殊作用:
常用来对一个数据的某些位置1。
方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。
例:将 X=10100000 的低4位 置为1 ,用 X | 0000 1111 = 1010 1111
即可得到。
~
)参加运算的一个数据,按二进制位进行“取反”运算。运算规则:~1=0; ~0=1;即:对一个二进制数按位取反,即将0变1,1变0。
使一个数的最低位为零,可以表示为:a&~1
。1 的值为 1111111111111110,再按“与”运算,最低位一定为0。因为“”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。
OK,截止到这儿,三道题目中使用的位运算介绍完毕,那么这里我们插入一下 #137
的详细题解。
public int singleNumber2(int[] nums) {
// 这里我们改一下变量名
// 用 one 记录到当前处理的元素为止,二进制1出现“1次”(mod 3 之后的 1)的有哪些二进制位;
// 用 two 记录到当前计算的变量为止,二进制1出现“2次”(mod 3 之后的 2)的有哪些二进制位。
int one = 0, two = 0;
int mask;
for (int num : nums) {
// 由于 two 要考虑,one 的已有状态,和当前是否继续出现。所以要先算
two ^= one & num;
// one 就是一个0,1的二值位,在两个状态间转换
one ^= num;
// 当 one 和 two 中的某一位同时为1时表示该二进制位上1出现了3次,此时需要清零。
mask = ~(one & two);
// 清零操作
one &= mask;
two &= mask;
}
// 即用 二进制 模拟 三进制 运算。最终 one 记录的是最终结果。
return one;
}
首先考虑一个相对简单的问题,加入输入数组里面只有 0 和 1,我们要统计 1 出现的次数,当遇到 1 就次数加 1,遇到 0 就不变,当次数达到 k 时,统计次数又回归到 0。我们可以用 m 位来做这个计数工作,即 xm, xm−1, …, x1,只需要确保 2m > k 即可,接下来我们要考虑的问题就是,在每一次check元素的时候,做什么操作可以满足上述的条件。在开始计数之前,每一个计数位都初始化位0,然后遍历nums
,直到遇到第一个1,此时 x1 会变成1,继续遍历,直到遇到第二个1,此时 x1=0, x2=1,直到这里应该可以看出规律了。每遇到一个1,对于 xm, xm−1, …, x1,只有之前的所有位都为1的时候才需要改变自己的值,如果本来是1,就变成0,本来是0,就变成1 ,如果遇到的是0,就保持不变。搞清楚了这个逻辑,写出表达式就不难了。这里以 m = 3 为例给出 java
代码:
for(int num: nums) {
x3 ^= x2 & x1 & i;
x2 ^= x1 & i;
x1 ^= i;
// other operations
}
但是到这里还没有解决当 1 的次数到 k 时,计数值要重新返回到 0,也就是所有计数位都变成 0 这个问题。解决办法也是比较巧妙。
假设我们有一个标志变量,只有当计数值到 k 的时候这个标志变量才为 0,其余情况下都是 1,然后每一次check元素的时候都对每个计数位和标志变量做与操作,那么如果标志变量为 0,也就是计数值为 k 的时候,所有位都会变成 0, 反之,所有位都会保持不变,那么我们的目的也就达到了。
好,最后一个问题是怎么计算标志变量的值。将 k 转变为二进制,只有计数值达到 k,所有计数位才会和 k 的二进制一样,所以只需要将 k 的二进制位做 与操作 ,如果某个位为 0,就与该位 取反 之后的值做与操作。
以 k=3, m=2 为例,简要的 java
代码如下:
// where yj = xj if kj = 1,
// and yj = ~xj if kj = 0,
// k1, k2是 k 的二进制表示(j = 1 to 2).
mask = ~(y1 & y2);
x2 &= mask;
x1 &= mask;
将这两部分合起来就是解决这个问题的完整算法了。
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
例:a = a<< 2将a的二进制位左移2位,右补0,左移1位后a = a * 2;
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。操作数每右移一位,相当于该数除以2。
例如:a = a>> 2 将a的二进制位右移2位,左补0 or 补1得看被移数是正还是负。
以上就是我们常见的几种位运算了,其中左移、右移等操作,在 HashMap
的源码中也会经常看到,理解了这些位操作,对于理解源码也是有一定帮助的,当然也会帮助我们写出执行效率更高的代码。
从上面的部分示例中可以看出,位运算通常用来降低包含排列,计数等复杂度比较高的操作,当然也可以用来代替乘 2 除 2,判断素数,偶数,倍数等基本操作,但是我认为其意义在于前者,即用计数器来降低设计到排列或者计数的问题的复杂度。
最后一点,三道算法题中,#136
、#260
理解起来倒还好,#137 Single Number II
的题解可能需要费一点功夫,至少我还没有完全理解,但不能轻易放弃对不对,继续啃啊!
以上便是我个人的简单总结,如果有纰漏或者错误,欢迎进行指出及纠正。
顾名思义,EditorConfig就是编辑器配置,帮助开发人员在不同的编辑器和IDE之间定义和维护一致的编码样式,由用于定义编码样式的文件格式和一组文本编辑器插件组成,这些插件使编辑器能够读取文件格式并遵循定义的样式。EditorConfig文件易于阅读,并且与版本控制系统配合使用。
下面是一个.editorconfig文件的示例,为Python和Javascript文件设置了行尾以及缩进的样式。
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
当用IDE打开一个文件时,EditorConfig插件会在打开文件的目录和其每一级父节点查找.editorconfig文件,直到找到一个配置了root = true的配置文件。
EditorConfig文件使用INI格式。斜杠(/)作为路径分隔符,#或者;作为注释。路径支持通配符:
通配符 | 说明 |
---|---|
* | 匹配除/之外的任意字符 |
** | 匹配任意字符串 |
? | 匹配任意单个字符 |
[name] | 匹配name字符 |
[!name] | 不匹配name字符 |
[s1,s2,s3] | 匹配给定的字符串 |
[num1..num2] | 匹配num1到mun2直接的整数 |
EditorConfig支持以下属性:
属性 | 说明 |
---|---|
indent_style | 缩进使用tab或者space |
indent_size | 缩进为space时,缩进的字符数 |
tab_width | 缩进为tab时,缩进的宽度 |
end_of_line | 换行符的类型。lf, cr, crlf三种 |
charset | 文件的charset。有以下几种类型:latin1, utf-8, utf-8-bom, utf-16be, utf-16le |
trim_trailing_whitespace | 是否将行尾空格自动删除 |
insert_final_newline | 是否使文件以一个空白行结尾 |
root | 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件 |
要将EditorConfig与其中一个编辑器一起使用,需要安装一个插件。
要将EditorConfig与其中一个无头工具一起使用,也需要安装一个插件。
# http://editorconfig.org
root = true
[*]
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
# editorconfig.org
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
官网:https://editorconfig.org/
wiki文档:https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
var objA = {
id: 1,
name: "AAA"
};
var objB = {
id: 1,
name: "AAA"
};
/**
* 判断两个对象是否相等
*/
function isEquivalent(a, b) {
// Create arrays of property names
var aProps = Object.getOwnPropertyNames(a);
var bProps = Object.getOwnPropertyNames(b);
// If number of properties is different,
// objects are not equivalent
if (aProps.length != bProps.length) {
return false;
}
for (var i = 0; i < aProps.length; i++) {
var propName = aProps[i];
// If values of same property are not equal,
// objects are not equivalent
if (a[propName] !== b[propName]) {
return false;
}
}
// If we made it this far, objects
// are considered equivalent
return true;
}
// true
console.log(isEquivalent(objA, objB));
参考:https://elasticsearch.cn/question/974
将elasticsearch 目录下的 config/jvm.options 文件里把“-Dfile.encoding=UTF-8”改为“-Dfile.encoding=GBK”
然后重启 Elasticsearch 即可,修改之后报错原因为:
[2018-01-16T16:55:18,564][WARN ][o.e.t.n.Netty4Transport ] [node-1] exception caught on transport layer [org.elasticsearch.transport.netty4.NettyTcpChannel@1ff4527f], closing connection
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
在mybatis的xml文件中,“<=”(小于等于)的“<”会被认为是tag的一部分,这时就需要转换一下写法。
原符号 | < | <= | > | >= | & | ' | " |
---|---|---|---|---|---|---|---|
替换符号 | < |
<= |
> |
>= |
& |
' |
" |
@Autowire
和 @Resource
@Autowire
和 @Resource
都可以用来装配bean,都可以用于字段或setter方法。@Autowire
默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false。@Resource
默认按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。名称可以通过 name 属性指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名,当注解写在 setter 方法上时,默认取属性名进行装配。注意:如果 name 属性一旦指定,就只会按照名称进行装配。
@Autowire
和@Qualifier
配合使用效果和@Resource
一样:@Autowired(required = false) @Qualifier("example")
private Example example;
@Resource(name = "example")
private Example example;
@Resouce
装配顺序注解对比 | @Resource |
@Autowire |
---|---|---|
注解来源 | JDK | Spring |
装配方式 | 优先按名称 | 优先按类型 |
属性 | name、type | required |
链接:https://blog.csdn.net/u012102104/article/details/79481007
在页面中如何将table中的json数据导出为csv文件呢?下面一个方法即可搞定:
csvExport: function(jsonData) {
const replacer = (key, value) => (value === null ? "" : value);
const header = Object.keys(jsonData[0]);
var csv = jsonData.map(row =>
header
.map(fieldName => JSON.stringify(row[fieldName], replacer))
.join(",")
);
csv.unshift(header.join(","));
csv = csv.join("\r\n");
csv = "data:text/csv;charset=utf-8,\uFEFF" + csv;
console.log(csv);
const link = document.createElement("a");
link.href = encodeURI(csv);
link.download = `filename.csv`;
document.body.appendChild(link); // Required for FF
link.click(); // This will download the data file named 'my_data.csv'.
document.body.removeChild(link); // Required for FF
}
在这篇它帖子里我将列出具体的用于自学机器学习的路线图,你可以用来找到自己的定位并且决定下一步怎么做。
我想了很多关于框架和系统的(学习)方法(正如我在博客上表明的一样)。之前我的一篇帖子,“机器学习的自学导引(Self-Study Guide to Machine Learning)”,在社区中引起了不小的轰动,而这篇贴子我会当成是对之前自学编程**的一次扩充说明。
来让我们跳进来吧……
机器学习是研究的一个巨大领域。有如此繁多的算法、理论、技术以及经典问题需要去学习了解,以致于让人喘不过气。
机器学习同样是需要深度跨学科的。你会从一个倾向开发人员的人,转变成为倾向为统计学家,而且当有如此多先前的知识(需要学习)时确实令人泄气。
你需要的是一个结构化的方法,不仅能给出机器学习中细节的学习主题和等级的一张路线图,也能整合类似书籍和开源的一些受欢迎的资源。
结构化的方法把重点放在了你要学什么以及你何时需要学的问题上,并由此标明了那些压倒性的问题。另外通过给资料介绍按照实际用途排序,由此标明哪些地方容易受挫,简直为工程师和编程人员量身定制。
一份路线图可以让你给自己指引你在哪儿和你想去哪儿。
自学意味着按照你自己的步幅,你自己的目标,以及你自己的计划学习。
自学是机器学习最好的途径。但那并不是指你事事都要亲力亲为,恰恰相反,它是指(自学)对你而言是最有效率的一种方式,并且可以借力于网络上最好的课程、书籍以及有用的指导去学习。
自学同样兼容比如大学本科和研究生学习的较为正式的课程。这表明着自学正积极整合这些材料入你的知识库,并拥有着这一过程。在这个过程中,你可以更加深入地探索那些最打动你的区域(领域)。
机器学习是一门应用学科,就像编程。学习理论知识固然重要,但你也必须花费时间去实践理论。你必须练习,这很紧急。你需要建立起对进程、算法以及问题的直觉。
学习机器学习的结构化方法被分为如下四个能力的等级:
每一个能力等级都会面临一系列不同的问题,如下:
在能力等级中的每个级别都有一个单一的目标,并且有许多小任务促使他们完成这一目标。目标如下:
每个等级的目标决定了各种各样的计划安排来实现这些目标。你可以安排你自己的活动(这是极力推荐的),尽管下面是针对每个等级建议的计划。
入门者
中级
高级
这份路线图是一个非常有用的工具,在你征服机器学习的路上,你会在各个方面都用得上的:
我也为其他的工程师和程序员设计了这份指南。
这个方法不仅对于专业的程序员而且学习工程学,计算机科学或者类似学科的学生都非常有用。
我建议你把范围集中在分类和回归类型的问题以及相应的算法和工具上。这是两个最常见的基础机器学习问题,而且其他问题都可以缩小到这个范围内。
这有一些机器学习的子领域,比如计算机视觉,自然语言处理,导购系统或者强化学习。这些领域都可以被缩小至分类和回归问题,并且它们的学习(过程)特别适合当前的路线图结构。我会建议你除非你到了中级水准,否则不要进入这些领域。
~机器学习是一次旅行。你学知道你当前的位置以及你正试图去哪里。这是需要时间和努力的,但是这对你也有很多帮助。
下面是可以更加有效地让你在这篇指导以及学习机器学习的过程之外得到更多的3个技巧:
List<Integer> intList = new ArrayList<Integer>() {{
add(1);
add(2);
add(3);
add(4);
}};
Integer sum = intList.parallelStream().reduce(Integer::sum).orElse(0);
System.out.println(sum);
@Async
注解以及Future
类型在项目中看到使用了 @Async
和 Future
,一眼看上去有点陌生,于是便简单了解一下,下面就简要谈一下。事先说明,项目框架为 spring-boot,所以前提是spring-boot项目。
@Async
实现异步调用“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。
顾名思义,@Async
是用来实现异步的。基于@Async
的方法,称之为异步方法。这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。
那么在Spring中如何使用@Async
实现异步调用呢?
假如我们有一个Task类,其中有三个任务需要异步执行,那么我们就可以将这些任务方法标上@Async
注解,使其成为异步方法。代码如下:
@Component
public class AsyncTask {
private static Random random = new Random();
@Async
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
}
@Async
public void doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
}
@Async
public void doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
为了让@async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后我们可以写一个单元测试进行测试一下:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}
}
此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如:
原因是目前doTaskOne、doTaskTwo、doTaskThree三个函数的时候已经是异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。
注:@async所修饰的函数不要定义为static类型,这样异步调用不会生效。
Funture
类型那么问题来了,什么是Future
类型呢?
Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
它的接口定义如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
它声明这样的五个方法:
也就是说Future提供了三种功能:
对于上面的Task类,我们也可以简单修改一下,判断上述三个异步调用是否已经执行完成。类似于如下doTaskOne()
代码进行修改:
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}
然后我们再修改一下单元测试方法:
@Test
public void asyncTaskTest() throws Exception {
long start = System.currentTimeMillis();
Future<String> task1 = asyncTask.doTaskOne();
Future<String> task2 = asyncTask.doTaskTwo();
Future<String> task3 = asyncTask.doTaskThree();
// 三个任务都调用完成,退出循环等待
while (!task1.isDone() || !task2.isDone() || !task3.isDone()) {
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
输出结果如下:
开始做任务一
开始做任务二
开始做任务三
完成任务二,耗时:5352毫秒
完成任务一,耗时:7190毫秒
完成任务三,耗时:7525毫秒
任务全部完成,总耗时:8004毫秒
如果在业务场景中我们有异步以及需要知道异步方法执行结果的需求,那么@Async
以及Future
的组合会是个不错的选择。另外 Java 8 中添加了新类 CompletableFuture
感兴趣的同学也可以自行进行更多的尝试。
链接:
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!
一般来说,事务必须满足4个条件。
在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。
1、用 BEGIN, ROLLBACK, COMMIT来实现
2、直接用 SET 来改变 MySQL 的自动提交模式:
小结:不可重复读和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read uncommitted) | 是 | 是 | 是 |
读提交(read committed) | 否 | 是 | 是 |
可重复读(repeatable read) | 否 | 否 | 是 |
串行化(serilizable) | 否 | 否 | 否 |
注:MySQL默认的事务隔离级别为可重复读(repeatable read)。
补充:
MySQL锁的类型分为共享锁(又称读锁)、排他锁(又称写锁),此外还有悲观锁和乐观锁,以及表(级)锁、行(级)锁。
InnoDB引擎的锁机制:InnoDB支持事务,支持行锁和表锁用的比较多,Myisam不支持事务,只支持表锁。
说明:
// 共享锁
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE;
// 排他锁
SELECT * FROM table_name WHERE ... FOR UPDATE;
对于锁定行记录后需要进行更新操作的应用,应该使用Select...For update 方式,获取排它锁。(用共享锁,在读了之后再写会阻塞,会导致死锁)
这里说说Myisam:MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁。
悲观锁:
正如其名,指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
SELECT ... FOR UPDATE
或 LOCK IN SHARE MODE
同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X)。乐观锁:
相对于悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用决定如何去做(一般是回滚事务)。实现乐观锁一般有以下两种方式:
总结:两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。另外,高并发情况下个人认为乐观锁要好于悲观锁,因为悲观锁的机制使得各个线程等待时间过长,极其影响效率,乐观锁可以在一定程度上提高并发度。
表级锁(table-level locking):MyISAM和MEMORY存储引擎
行级锁(row-level locking) :InnoDB存储引擎
页面锁(page-level-locking):BDB存储引擎
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
最近几个月一直在做前端的项目,然后在安装依赖的时候便打出命令 npm install xxx
来进行局部或者全局安装就行了,甚至在包含package.json文件的项目中执行 npm ci
就自动引入依赖了。嗯,我就在想,我可以发布npm包吗?接着,便进行了一次毫无意义的尝试,仅作记录。
那么首先就是,怎么发布一个npm包呢?
步骤如下:
npm init
进行项目的初始化配置npm publish
便可以发布到 npm 公共仓库进行下载了(网站地址:https://www.npmjs.com/)既然没有什么意义,那趣味性一定要有,于是便参考了github上的一个项目:five ,我创建一个项目 two 。
接着便是完善这个项目了。除此之外,我搜了一下npm,发现“two”这个名字已经有人取了,便不得不再想一个,否则发布便会失败,然后就加了一个前缀“monkey-two”。
two.js
,然后所有的主要函数和实现都在此文件中完成test.js
文件,本项目的测试是用 mocha
来进行的two(); // 2
two.valueOf(); // 2
two() + two(); // 2 + 2 = 4
two.add(1); // 1 + 2 = 3
two.add(2); // 2 + 2 = 4
two.add(3); // 3 + 2 = 5
two.add(10, 5); // 10 + 5 = 15
two() - two(); // 2 - 2 = 0
two.subtract(1); // 1 - 2 = -1
two.subtract(2); // 2 - 2 = 0
two.subtract(3); // 3 - 2 = 1
two.subtract(10, 5); // 10 - 5 = 5
two() * two(); // 2 * 2 = 4
two.times(1); // 1 * 2 = 2
two.times(2); // 2 * 2 = 4
two.times(3); // 3 * 2 = 6
two.times(10, 5); // 10 * 5 = 50
two() / two(); // 2 / 2 = 1
two.divide(1); // 1 / 2 = 0.5
two.divide(2); // 2 / 2 = 1
two.divide(3); // 3 / 2 = 1.5
two.divide(10, 5); // 10 / 5 = 2
two.power(); // 2
two.power(3); // 8
two.power(10); // 1024
two.square(); // 1
two.square(4); // 2
two.square(1024); // 32
two.base(2); // 10
two.base(8); // 2
two.base(10); // 2
two.base(16); // 2
# the base is 2(二进制)
two.baseOf(); // 01 # default 1
two.baseOf(10); // 1010
two.upHigh(); // ²
two.downLow(); // ₂
two.roman(); // Ⅱ
two.chinese(); // 二
two.chinese("pinyin"); // èr
two.chinese("financial"); // 贰
two.japanese(); // 二
two.english(); // two
two.upperCase(); // TWO
two.repeat(); // 2
two.repeat(5); // 22222
two.repeat(10); // 2222222222
two.dayOfWeek(); // 周二
two.dayOfWeek("EN"); // Monday
two.monthOfYear(); // 二月
two.monthOfYear("EN"); // February
two.peace(); // ✌️
two.victory(); // ✌️
two.eyes(); // 👀
two.oclock(); // 🕑
two.oclockStatus(); // 🛌 # default 2:00 am, you should be sleeping in the bed.(默认是凌晨两点,你应该在睡觉。)
two.oclockStatus("PM"); // 👨💻 # 2:00 pm, you shoulding be coding.(传参表示下午两点,你居然不在写代码?!)
two.oclockStatus("pm"); // 👨💻
two.isTwo(); // true
two.isTwo(2); // true
two.isTwo(3); // false
two.bigger(1, 2); // 2
two.smaller(1, 2); // 1
完成项目的基本功能之后,就可以发布了,这时候执行:
npm publish
在这时,可能会报错,因为还没登录npm账户,那就执行 npm login
输入用户名密码即可:
最后,发布就行了,成功的截图如下:
验证的方法也很简单:
方法一:通过npm install 命令来检验是否可以安装
方法二:直接去npm网站进行搜索,记得要用全名搜索,并且要等一段时间再去搜,才能搜到。
接着,使用RunKit验证就行了:
前言: 本文主要介绍Superset的安装以及使用,更主要的是如何使用SQL Lab进行数据查询和数据可视化的任务。此外,文末也会介绍一些权限分配的一些应用场景。
merge()
的用法Java 8 最大的特性无异于更多地面向函数,比如引入了 lambda
等,可以更好地进行函数式编程。前段时间无意间发现了 map.merge()
方法,感觉还是很好用的,此文简单做一些相关介绍。首先我们先看一个例子。
merge()
怎么用?假设我们有这么一段业务逻辑,我有一个学生成绩对象的列表,对象包含学生姓名、科目、科目分数三个属性,要求求得每个学生的总成绩。加入列表如下:
private List<StudentScore> buildATestList() {
List<StudentScore> studentScoreList = new ArrayList<>();
StudentScore studentScore1 = new StudentScore() {{
setStuName("张三");
setSubject("语文");
setScore(70);
}};
StudentScore studentScore2 = new StudentScore() {{
setStuName("张三");
setSubject("数学");
setScore(80);
}};
StudentScore studentScore3 = new StudentScore() {{
setStuName("张三");
setSubject("英语");
setScore(65);
}};
StudentScore studentScore4 = new StudentScore() {{
setStuName("李四");
setSubject("语文");
setScore(68);
}};
StudentScore studentScore5 = new StudentScore() {{
setStuName("李四");
setSubject("数学");
setScore(70);
}};
StudentScore studentScore6 = new StudentScore() {{
setStuName("李四");
setSubject("英语");
setScore(90);
}};
StudentScore studentScore7 = new StudentScore() {{
setStuName("王五");
setSubject("语文");
setScore(80);
}};
StudentScore studentScore8 = new StudentScore() {{
setStuName("王五");
setSubject("数学");
setScore(85);
}};
StudentScore studentScore9 = new StudentScore() {{
setStuName("王五");
setSubject("英语");
setScore(70);
}};
studentScoreList.add(studentScore1);
studentScoreList.add(studentScore2);
studentScoreList.add(studentScore3);
studentScoreList.add(studentScore4);
studentScoreList.add(studentScore5);
studentScoreList.add(studentScore6);
studentScoreList.add(studentScore7);
studentScoreList.add(studentScore8);
studentScoreList.add(studentScore9);
return studentScoreList;
}
我们先看一下常规做法:
ObjectMapper objectMapper = new ObjectMapper();
List<StudentScore> studentScoreList = buildATestList();
Map<String, Integer> studentScoreMap = new HashMap<>();
studentScoreList.forEach(studentScore -> {
if (studentScoreMap.containsKey(studentScore.getStuName())) {
studentScoreMap.put(studentScore.getStuName(),
studentScoreMap.get(studentScore.getStuName()) + studentScore.getScore());
} else {
studentScoreMap.put(studentScore.getStuName(), studentScore.getScore());
}
});
System.out.println(objectMapper.writeValueAsString(studentScoreMap));
// 结果如下:
// {"李四":228,"张三":215,"王五":235}
然后再看一下 merge()
是怎么做的:
Map<String, Integer> studentScoreMap2 = new HashMap<>();
studentScoreList.forEach(studentScore -> studentScoreMap2.merge(
studentScore.getStuName(),
studentScore.getScore(),
Integer::sum));
System.out.println(objectMapper.writeValueAsString(studentScoreMap2));
// 结果如下:
// {"李四":228,"张三":215,"王五":235}
merge()
简介merge()
可以这么理解:它将新的值赋值到 key (如果不存在)或更新给定的key 值对应的 value,其源码如下:
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = this.get(key);
V newValue = oldValue == null ? value : remappingFunction.apply(oldValue, value);
if (newValue == null) {
this.remove(key);
} else {
this.put(key, newValue);
}
return newValue;
}
我们可以看到原理也是很简单的,该方法接收三个参数,一个 key 值,一个 value,一个 remappingFunction
,如果给定的key不存在,它就变成了 put(key, value)
。但是,如果 key 已经存在一些值,我们 remappingFunction
可以选择合并的方式,然后将合并得到的 newValue
赋值给原先的 key。
这个使用场景相对来说还是比较多的,比如分组求和这类的操作,虽然 stream 中有相关 groupingBy()
方法,但如果你想在循环中做一些其他操作的时候,merge()
还是一个挺不错的选择的。
除了 merge()
方法之外,我还看到了一些Java 8 中 map
相关的其他方法,比如 putIfAbsent
、compute()
、computeIfAbsent()
、computeIfPresent
,这些方法我们看名字应该就知道是什么意思了,故此处就不做过多介绍了,感兴趣的可以简单阅读一下源码(都还是挺易懂的),这里我们贴一下 compute()(Map.class)
的源码,其返回值是计算后得到的新值:
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = this.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
if (oldValue == null && !this.containsKey(key)) {
return null;
} else {
this.remove(key);
return null;
}
} else {
this.put(key, newValue);
return newValue;
}
}
本文简单介绍了一下 Map.merge()
的方法,除此之外,Java 8 中的 HashMap
实现方法使用了 TreeNode
和 红黑树,在源码阅读上可能有一点难度,不过原理上还是相似的,compute()
同理。所以,源码肯定是要看的,不懂的地方多读多练自然就理解了。
获取对象属性的方法:
obj.getClass().getFields()
怎么是无效的?
FieldUtils.getAllFields(obj.getClass())
是怎么实现的,以及能不能只获取到一部分field而不去获取两个final的class(this.class && obj.this.class)? FieldUtils的其他用法。
在浏览 github
的时候,偶然发现了 spring-boot
的源码中有这么一段代码:
public void applyTo(RepositoryRestConfiguration rest) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this::getBasePath).to(rest::setBasePath);
map.from(this::getDefaultPageSize).to(rest::setDefaultPageSize);
map.from(this::getMaxPageSize).to(rest::setMaxPageSize);
map.from(this::getPageParamName).to(rest::setPageParamName);
map.from(this::getLimitParamName).to(rest::setLimitParamName);
map.from(this::getSortParamName).to(rest::setSortParamName);
map.from(this::getDetectionStrategy).to(rest::setRepositoryDetectionStrategy);
map.from(this::getDefaultMediaType).to(rest::setDefaultMediaType);
map.from(this::getReturnBodyOnCreate).to(rest::setReturnBodyOnCreate);
map.from(this::getReturnBodyOnUpdate).to(rest::setReturnBodyOnUpdate);
map.from(this::getEnableEnumTranslation).to(rest::setEnableEnumTranslation);
}
嗯……这是什么用法,恕我见识浅薄,没怎么见过还有这么写的啊。看上去,像是对象属性值的复制,于是验证了一下,果然如此。另外,还学习了 PropertyMapper
的其他操作。
PropertyMapper
的用法PropertyMapper
是可用于将值从提供的源映射到目标的实用程序,简单来讲,就是属性值的拷贝。是自 spring-boot 2.0 版本新增的功能。主要用于在从@ConfigurationProperties
映射到第三方类时提供帮助。
setter
方法PropertyMapper
可以用于 setter
方法来对目标对象进行赋值,不仅可以通过 Supplier<T>
,而且可以直接通过确定的值来进行赋值:
@Test
public void propertyMapperTest() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
Student student1 = new Student() {{
setId(1);
setName("TOM-007");
}};
System.out.println(objectMapper.writeValueAsString(student1));
Student student2 = new Student();
// from(Supplier<T> supplier)
mapper.from(student1::getId).to(student2::setId);
mapper.from(student1::getName).to(student2::setName);
mapper.from(student1::getScore).to(student2::setScore);
// from(T value)
mapper.from(80).to(student2::setScore);
System.out.println(objectMapper.writeValueAsString(student2));
}
输出:
{"id":1,"name":"TOM-007","score":null}
{"id":1,"name":"TOM-007","score":80}
某天,在我执行某句关联查询的SQL的时候,突然报了一个如下错误,然后找到了解决的方案,在这里记录一下。
【执行的SQL】SELECT a.id, a.user_num, b.name FROM tablaA a LEFT JOIN tableB b on a.user_num = b.user_id
;
【错误信息】Illegal mix of collations (utf8_unicode_ci,IMPLICIT) and (utf8_general_ci,IMPLICIT) for operation '='
。
【错误分析】错误信息显示由于非法的字符排序规则(collations )混用而不能进行'='操作,那么就应该是一张表的collations是‘utf8_unicode_ci’,而另一张表的collations是‘utf8_general_ci’,那么要做的就是统一两张表的collations。
【更改后的SQL】SELECT a.id, a.user_num, b.name FROM tablaA a LEFT JOIN tableB b on a.user_num COLLATE utf8_general_ci = b.user_id COLLATE utf8_general_ci;
,在SQL关联的字段上加上统一的collations即可,比如COLLATE utf8_general_ci
。
【注】utf8_general_ci和utf8_unicode_ci的区别,前者校对速度快,但准确度稍差;后者准确度高,但校对速度稍慢。
https://dba.stackexchange.com/questions/24587/mysql-illegal-mix-of-collations
看到了一道比较有意思的面试题,之前并没有深入研究过,在这儿记录一下。
在 Java 中,ArrayList 的默认容量是10,而且每当其元素个数超过容量长度的时候,会自动进行扩容,扩容的大小为 原容量*0.5 + 1,比如原容量是10,一次扩容之后为16;同样的,Vector 的默认容量也是10,但其扩容大小是 原容量的一倍,即原容量是10,一次扩容之后为20;ArrayList以及Vector的加载因子都是1。
接着,HashMap的默认容量是多少呢?
HashMap 的默认容量大小为16,当然这个也可以在初始化Map的时候手动设置,但必须是2的幂,比如:
Map map = new HashMap(4);
另外,HashMap的加载因子为0.75,即元素个数超过容量长度的0.75倍之后,便会进行扩容,扩容大小为原容量的一倍,比如HashMap的容量为16,一次扩容后是容量为32。
此外还有HashSet,线程不安全,存取速度快,底层实现是一个HashMap(保存数据),实现Set接口,默认初始容量为16,加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容扩容增量:原容量的 1 倍,比如 HashSet的容量为16,一次扩容后是容量为32。
参考链接:
public class ObjectPropertySumTest {
@Data
private class Student {
private String name;
private Integer age;
}
@Test
public void objectPropertySum() {
List<Student> students = buildTestData();
// 7
System.out.println(students.size());
// 方式一:
int ageSum1 = students.stream().mapToInt(Student::getAge).sum();
// 70
System.out.println("年龄总和" + ageSum1);
// 方式二:
int ageSum2 = students.stream().map(Student::getAge).reduce(Integer::sum).orElse(0);
// 70
System.out.println("年龄总和" + ageSum2);
}
private List<Student> buildTestData() {
return new ArrayList<Student>() {{
add(new Student() {{
setName("A");
setAge(10);
}});
add(new Student() {{
setName("B");
setAge(10);
}});
add(new Student() {{
setName("C");
setAge(10);
}});
add(new Student() {{
setName("D");
setAge(10);
}});
add(new Student() {{
setName("E");
setAge(10);
}});
add(new Student() {{
setName("F");
setAge(10);
}});
add(new Student() {{
setName("G");
setAge(10);
}});
}};
}
}
索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据。
MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。
但实际上,索引也是一张表,保存了主键与索引字段,并指向实体表的记录。因此索引也会有它的缺点:虽然索引大大提高了查询速度,同时却也会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。建立索引也会占用磁盘空间的索引文件。
接下来此文便从MySQL中索引的语法、索引的分类、索引的实现原理、索引的使用策略、索引的优化、索引的优缺点几部分详细介绍一下索引。
CREATE INDEX indexName ON mytable(username(length));
如果是CHAR,VARCHAR类型,length可以小于字段实际长度;如果是BLOB和TEXT类型,必须指定 length。
ALTER TABLE tableName ADD INDEX indexName(columnName);
CREATE TABLE mytable(
ID INT NOT NULL,
username VARCHAR(16) NOT NULL,
(UNIQUE) INDEX indexName(username(length))
);
DROP (UNIQUE) INDEX indexName ON mytable;
SHOW INDEX FROM tableName;
注:这里在语句里标明了UNIQUE的命令,即为唯一索引相关的命令。下文会有详细介绍。
常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引
ALTER TABLE tableName ADD PRIMARY KEY(column);
ALTER TABLE tableName ADD UNIQUE indexName (column_list);
ALTER TABLE tableName ADD INDEX indexName(column_list);
ALTER TABLE tableName ADD FULLTEXT indexName(column_list);
ALTER TABLE tableName ADD INDEX indexName(col1, col2, col3);
组合索引遵循“最左前缀”原则,把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了col1,col1col2,col1col2col3三个索引,而col2或者col3是不能使用索引的。
在使用组合索引的时候可能因为列名长度过长而导致索引的key太大,导致效率降低,在允许的情况下,可以只取col1和col2的前几个字符作为索引,如下:
ALTER TABLE 'table_name' ADD INDEX index_name(col1(4),col2(3));
表示使用col1的前4个字符和col2的前3个字符作为索引。
MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BTree索引,B+Tree索引,哈希索引,全文索引等等。
只有memory(内存)存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。
FULLTEXT(全文)索引,仅可用于MyISAM和InnoDB,针对较大的数据,生成全文索引非常的消耗时间和空间。对于文本的大对象,或者较大的CHAR类型的数据,如果使用普通索引,那么匹配文本前几个字符还是可行的,但是想要匹配文本中间的几个单词,那么就要使用LIKE %word%来匹配,这样需要很长的时间来处理,响应时间会大大增加,这种情况,就可使用时FULLTEXT索引了,在生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时及根据这个单词的清单来索引。
BTree是平衡搜索多叉树,设树的度为d(d > 1),高度为h,那么BTree要满足以下条件:
在BTree的结构下,就可以使用二分查找的查找方式,查找复杂度为h*log(n),一般来说树的高度是很小的,一般为3左右,因此BTree是一个非常高效的查找结构。
B+Tree是BTree的一个变种,设树的度为d,h为树的高度,B+Tree和BTree的不同主要在于:
一般来说B+Tree比BTree更适合实现外存的索引结构,因为存储引擎的设计专家巧妙的利用了外存(磁盘)的存储结构,即磁盘的一个扇区是整数倍的page(页),页是存储中的一个单位,通常默认为4K,因此索引结构的节点被设计为一个页的大小,然后利用外存的“预读取”原则,每次读取的时候,把整个节点的数据读取到内存中,然后在内存中查找,已知内存的读取速度是外存读取I/O速度的几百倍,那么提升查找速度的关键就在于尽可能少的磁盘I/O,那么可以知道,每个节点中的key个数越多,那么树的高度越小,需要I/O的次数越少,因此一般来说B+Tree比BTree更快,因为B+Tree的非叶节点中不存储data,就可以存储更多的key。
什么时候要使用索引?
什么时候不要使用索引?
注:
在组合索引中不能有列的值为NULL,如果有,那么这一列对组合索引就是无效的;
在一个SELECT语句中,索引只能使用一次,如果在WHERE中使用了,那么在ORDER BY中就不要用了;
LIKE操作中,'%aaa%'不会使用索引,也就是索引会失效,但是‘aaa%’可以使用索引;
在索引的列上使用表达式或者函数会使索引失效,例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。
在查询条件中使用正则表达式时,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。
在查询条件中使用<>会导致索引失效。
在查询条件中使用IS NULL会导致索引失效。
在查询条件中使用OR连接多个条件会导致索引失效,这时应该改为两次查询,然后用UNION ALL连接起来。
尽量不要包括多列排序,如果一定要,最好为这队列构建组合索引;
只有当数据库里已经有了足够多的测试数据时,它的性能测试结果才有实际参考价值。如果在测试数据库里只有几百条数据记录,它们往往在执行完第一条查询命令之后就被全部加载到内存里,这将使后续的查询命令都执行得非常快--不管有没有使用索引。只有当数据库里的记录超过了1000条、数据总量也超过了MySQL服务器上的内存总量时,数据库的性能测试结果才有意义。
索引的最左前缀和和B+Tree中的“最左前缀原理”有关,举例来说就是如果设置了组合索引<col1,col2,col3>那么以下3中情况可以使用索引:col1,<col1,col2>,<col1,col2,col3>,其它的列,比如<col2,col3>,<col1,col3>,col2,col3等等都是不能使用索引的。
根据最左前缀原则,我们一般把排序分组频率最高的列放在最左边,以此类推。
在上面已经提到,使用LIKE进行模糊查询的时候,'%aaa%'不会使用索引,也就是索引会失效。如果是这种情况,只能使用全文索引来进行优化。
为检索的条件构建全文索引,然后使用
SELECT * FROM tablename MATCH(index_colum) ANGAINST(‘word’);
不要在列上使用函数,这将导致索引失效而进行全表扫描。
负向条件有:!=
、<>
、not in
、not exists
、not like
等等。
select * from article where status != 1 and status != 2;
可以使用 in 进行优化:
select * from article where status in (0, 3);
范围条件有:<
、<=
、>
、>=
、between
等。
范围列可以用到索引,但是范围列后面的列无法用到索引,索引最多用于一个范围列,如果查询条件中有两个范围列则无法全用到索引。
当查询条件两侧类型不匹配的时候会发生强制转换,强制转换可能导致索引失效而进行全表扫描。
如果 phone 字段是 varchar 类型,则下面的 SQL 不能命中索引:
select * from user where phone = 12345678901;
可以优化为:
select * from user where phone = '12345678901';
优势:可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
劣势:索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表。
1. #{} 和 ${} 的区别是什么?
答: #{} 是预编译处理,${} 是字符串替换。
mybatis 在处理 #{} 时,会将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值;mybatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
#{} 能够很大程度上防止 sql 注入;${} 方式无法防止 sql 注入。
${} 一般用于传入数据库对象,比如数据库表名。
能用 #{} 时尽量用 #{}。
mybatis 排序时使用 order by 动态参数时需要注意,使用 ${} 而不用 #{}。
2. 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
答: Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select>
、<insert>
、<update>
、<delete>
标签,都会被解析为一个MappedStatement对象。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
sql语句的名称是由:Mapper 接口的名称与对应的方法名称组成的。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
3. Mybatis是如何进行分页的?分页插件的原理是什么?
答: Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10;
4. Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
答: 第一种是使用 <resultMap>
标签,逐一定义列名和对象属性名之间的映射关系。第二种使用 sql 列的别名功能,将列别名写为对象属性名,比如T_NAME AS NAME
,对象属性名一般是 name
,小写,但是列名不区分大小写,Mybatis 忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe
,Mybatis 一样可以正常工作。有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
5. Xml映射文件中,除了常见的select|insert|update|delete标签之外,还有哪些标签?
答: 还有很多其他的标签,加上动态sql的9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind等,其中为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。
6. 简述Mybatis 的插件运行原理,以及如何编写一个插件?
答: Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,还需要在配置文件中配置你编写的插件。
7. 一级、二级缓存
答: 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空。
二级缓存:与一级缓存机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如Ehcache。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:<cache/>
对于缓存数据的更新机制,当某一个作用域(一级缓存 Session / 二级缓存 Namespace)进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
8. Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
答: Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnable=true|false
。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName()
,拦截器 invoke() 方法发现 a.getB() 是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()
方法的调用。这就是延迟加载的原理。
9. Mybatis映射文件中,如果A标签通过include引用了B标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
答: 虽然Mybatis解析Xml映射文件是按照顺序解析的,但是,被引用的B标签依然可以定义在任何地方,Mybatis都可以正确识别。
原理是,Mybatis解析A标签,发现A标签引用了B标签,但是B标签尚未解析到,尚不存在,此时,Mybatis会将A标签标记为未解析状态,然后继续解析余下的标签,包含B标签,待所有标签解析完毕,Mybatis会重新解析那些被标记为未解析的标签,此时再解析A标签时,B标签已经存在,A标签也就可以正常解析完成了。
10. 简述 Mybatis 的 XML 映射文件和 Mybatis 内部数据结构之间的映射关系?
答: Mybatis 将所有Xml配置信息都封装到 All-In-One 重量级对象 Configuration 内部。在Xml映射文件中,<parameterMap>
标签会被解析为 ParameterMap 对象,其每个子元素会被解析为 ParameterMapping 对象。<resultMap>
标签会被解析为 ResultMap 对象,其每个子元素会被解析为 ResultMapping 对象。每一个 <select>
、<insert>
、<update>
、<delete>
标签均会被解析为 MappedStatement 对象,标签内的 sql 会被解析为 BoundSql 对象。
神话: 一个管理很完善的软件项目,应该首先以系统化的方法进行需求开发,定义一份严谨的列表来描述程序的功能。设计完全遵循需求,并且完成得相当仔细,这样就让程序员的代码编写工作能够从头至尾直线型地工作。这也表明绝大多数代码编写后就已完美,测试通过后即可被抛到脑后。如果这样的神话是真的,那么代码被修改的唯一时机就是在软件维护阶段,而这一阶段只会在系统的最初版本交付用户之后。
现实情况: 在初始开发阶段,代码会有实质性的进化。在初始代码编写过程中,就会出现很多剧烈的改变,如同在代码维护阶段可以看到的那样。根据项目的规模不同,典型的项目花在编码、调试和单元测试上的时间会占到整个项目的30%到65%不等。如果代码编写和单元测试能够一帆风顺,这两个阶段所占项目时间的比例不会超过20%到30%。即使是管理完善的项目,每个月都有大约1/4的需求发生变化。需求的变化将不可避免地导致相关代码的改变——有时是实质性的代码改变。
另一个事实: 现在的开发增强了代码在构造阶段中改变的潜力。在旧式的软件生命周期中,项目成功与否的关键在于能否避免代码的改变。越来越多的现代开发方法已经放弃了对代码的前瞻性。如今的开发方法更多地以代码为中心,在整个项目生命周期中代码都会不断地演化。你可以期望代码的演化比以往任何时候更频繁。简单来讲,在新式的软件生命周期中,项目成功与否的关键在于能否应对需求的剧变。
软件演化就像生物进化一样,有些突变对物种是有益的,另外一些则是有害的。良性的软件演化使得代码得到了发展,就如猴子进化到穴居人再进化到我们人类。然而,有时演化的力量也会以另一种方式打击你的程序,甚至将它送入不断退化的螺旋形轨道。
区分软件演化类型的关键,就是程序的质量在这一过程中是提高了还是降低了。
区分软件演化类型的第二个标准,就是这样的演化是源于程序构建过程中的修改,还是维护过程中的修改。构建中的修改通常是由最初的开发人员完成,在这一阶段程序还没有被人们彻底遗忘。这时系统也未上线待售,因此,完成修正压力仅仅是来自于时间表——绝不会有500个愤怒的用户质问你为什么他们的系统会崩溃。出于同样的原因,构建期间的修改常常是随心所欲之作——系统处于高度动态阶段,出现错误的代价较小。这样的环境孕育着与维护期不同的软件演化风格。
软件演化的基本准则就是,演化应当提升程序的内在质量。
再庞大复杂的代码都可以通过重构加以改善。 ——Gerald Weinberg
要实现软件演化基本准则,最关键的策略就是重构,Martin Fowler 将其定义为“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”(Fowler 1999)。在现代编程理论中,“重构(refactoring)”一词源于Larry Constantine 在结构化程序设计中首次使用的“factoring”。当时指尽可能地将一个程序分解为多个组成部分。
有时,代码在维护过程中质量会降低,而有时代码在最初诞生的时候就先天不良。
代码重复 重复的代码几乎总是代表着对最初设计里彻底分解方面的一个失误。无论何时,如果需要对某个地方进行修改,你都不得不在另一个地方完成同样的修改——重复代码总会将你置于一种两线作战的尴尬境地。重复的代码同样违背了Andrew Hunt 和 Dave Thomas 所提出的“DRY原则”:不要重复自己“Don't Repeat Yourself”(Hunt and Thomas 2000)。我想还是David Parnas说得最为精辟:“复制粘贴即设计之缪。”
冗长的子程序 在面向对象的编程中,很少会需要用到长度超过一个屏幕的子程序。这样的子程序通常暗示程序员是在把一个结构化程序的脚塞进一只面向对象的鞋子里。
循环过长或嵌套太深 循环内部的复杂代码常常具备转换为子程序的潜质,这样的改动将有助于对代码的分解,并减少循环的复杂性。
内聚性太差的类 如果看到有某个类大包大揽了许多彼此无关的任务,那么这个类就该被拆分成多个类,每个类负责一组具有内在的相互关联的任务。
类的接口未能提供层次一致的抽象 即使是那些从诞生之日起就具有内聚接口的类也可能渐渐失去最初的一致性。
拥有太多参数的参数列表 如果一个程序被分解得很好,那么它的子程序应当小巧、定义精确,且不需要庞大的参数列表。
类的内部修改往往被局限于某个部分 有时一个类会有这两种或更多独立的功能。如果你发现自己要么修改类里的一部分,要么修改另一部分,但极少的修改会同时影响类中的两个部分,这就表明该类应该根据独立的功能被拆分为多个类。
变化导致对多个类的相同修改 如果发现自己常常对同一组类进行修改,这表明这些类中的代码应当被重新组织,使修改仅影响到其中的一个类。
对继承体系的同样修改 每次为某个类添加派生类时,都会发现自己不得不对另一个类做同样的操作。
case语句需要做相同的修改 尽管使用case语句本身不是坏事,但如果不得不在程序的多个部分里对类似的一组case语句做出相同的修改,那么就应当问问自己,使用继承是否是更明智的选择。
同时使用的相关数据并未以类的方式进行组织 如果看到自己常常对同样的一组数据进行操作,是否改将这些数据及操作组织到一个类里面。
成员函数使用其他类的特征比使用自身类的特征还要多 这一状况暗示着这一子程序应该被放到另一个类中,然后再原来的类里调用。
过多使用基本数据类型 基本数据类型可用于表示真实世界中实体的任意数量,如果程序中使用了整型这样的基本数据类型表示某种常见的实体,如货币,请考虑创建一个简单的Money类,这样编译器就可以对Money变量执行类型检查,你也可以对赋给Money的值添加安全检查等功能。
某个类无所事事
一系列传递流浪数据的子程序 看看自己的代码,把数据传递给某个子程序,是否仅仅就是为了让该子程序把数据转交给另一个子程序。这样传来传去的数据被称为“流浪数据/tramp data”。这样做并非错误,只是需要自检,如此传递特定数据,是否与每个子程序接口所表示的抽象概念一致。
中间人对象无事可做 如果看到某个类中的绝大部分代码只是去调用其他类中的成员函数,请考虑是否应该把这样的中间人(middleman)去掉,转而直接调用其他的类。
某个类同其他类关系过于亲密 如果需要使程序具备更强的可管理性,并最大限度地减少更改代码对周围的连带影响,那么封装(信息隐藏)可能是最强有力的工具了。只要发现某个类对另一个类的了解程度超过了应该的程度——包括派生类了解基类中过多的东西,那么宁可让代码因较强的封装而出错,也不要减弱封装。
子程序命名不当
数据成员被设置成公用 这样会模糊接口和实现之间的接线,其本身也违背了封装的原则,限制了类在未来可以发挥的灵活性。因此,请认真考虑把public数据成员藏在访问器子程序背后。
某个派生类仅使用了基类的很少一部分成员函数 因此应当考虑进行更完善的封装:把派生类相对于基类的关系从“is-a”转变为“has-a”。即把基类转换成原来的派生类的数据成员,然后仅仅为原来的派生类提供所需要的函数。
注释被用于解释难懂的代码 注释在程序中扮演了重要的角色,但它不应当被用来为拙劣代码的存在而辩护。有箴言为证:“不要为拙劣的代码编写文档——应当重写代码”(Kernighan and Plauger 1978)。
使用了全局变量 当你再度遇到某段使用了全局变量的代码时,请花点时间来重新检查一下这些代码。
在子程序调用前使用了设置代码(setup code),或在调用后使用了收尾代码(takedown code)
//C++示例:在子程序调用前后的设置代码和收尾代码——糟糕的做法
//在调用子程序之前的设置代码
WithdrawalTransaction withdrawal;
withdrawal.SetCustormerId( custormerId );
withdrawal.SetBalance( balance );
withdrawal.SetWithdrawalAmount( withdrawalAmount );
withdrawal.SetwithdrawalDate( withdrawalDate );
ProcessWithdrawal( withdrawal );
//在调用子程序之后的收尾代码
customerId = withdrawal.GetCustomerId();
balance = withdrawal.GetBalance();
withdrawalAmount = withdrawal.GetWithdrawalAmount();
withdrawalDate = withdrawal.GetWithdrawalDate();
程序中的一些代码似乎是在将来的某个时候才会用到的 在猜测程序将来有哪些功能可能被用到这方面,程序员已经声名狼藉了。
在日常的讨论中,“重构”一词更多被用来指那些弥补缺陷、增加功能、修改设计等工作,全然成为了对代码做了任何修改的同义词。这一术语的深刻内涵已惨遭稀释。修改本身并不是什么了不得的好事,但如果是程序员深思熟虑而为之,且遵循规范恰如其分,那么在不断的维护下,这样的修改必将成为代码质量稳步提升之关键,且能避免如今随处可见的代码因质量不断下降而最终灭亡的趋势。
用具名常量替代神秘数值
使变量的名词更为清晰且传递更多信息
将表达式内联化 把一个中间变量换成给它赋值的那个表达式本身。
用函数来代替表达式 用一个函数来代替表达式(这样一来,表达式就不会在代码中重复出现了)。
引入中间变量 要记住,给这个中间变量命名应能准确概括表达式的用途。
用多个单一用途的变量代替某个多用途变量 如果某个变量身兼数职——通常是i、j、temp、x——请用多个变量来让它们各司其职吧,各个变量还应该具有更为准确的变量名。
在局部用途中使用局部变量而不是参数 如果一个被用作输入的子程序参数在其内部又被用作局部变量,那么请直接创建一个局部变量来代替它。
将基础数据类型转化为类 如果一个基础数据类型需要额外的功能(例如更为严格的类型检查)或额外的数据,那么就把该数据转换为一个对象,然后再添加你所需要的类行为。
将一组类型码(type codes)转化为类或枚举类型
将一组类型码转换为一个基类及其相应的派生类 如果与不同类型相关联的不同代码片段有着不一样的功能,请考虑为该类创建一个基类,然后针对每个类型码创建派生类。例如对OutputType基类,就可以创建Screen、Printer和File这样的派生类。
将数组转换为对象 如果正在使用一个数组,其中的不同元素具有不同的类型,那么就应该用一个对象来替代它。将数组中的各个元素转化为该类的各个成员。
把群集(collection)封装起来 如果一个类返回一个群集,到处散布的多个群集实例将会带来同步问题。请让你的类返回一个只读群集,并且提供相应的为群集添加和删除元素的子程序。
用数据类来代替传统记录 建立一个包含记录成员的类。这样你可以集中完成对记录的错误检查、持久化和其他与该记录相关的操作。
分解布尔表达式 通过引入命名准确的中间变量来简化复杂的布尔表达式,通过变量名更好地说明表达式的含义。
将复杂布尔表达式转换成命名准确的布尔函数
合并条件语句不通部分中的重复代码片段 如果你有完全相同的代码同时出现在一个条件语句中的if语句块和else语句块中,那么就应该讲这段代码移到整个if-then-else语句块的后面。
使用break或return而不是循环控制变量 如果在循环中用到了一个类似done这样的控制循环的变量,请使用break或return来代替它。
在嵌套的if-then-else语句中一旦知道答案就立刻返回,而不是去赋一个返回值
用多态来替代条件语句(尤其是重复的case语句) 结构化程序里很多的case语句中的逻辑都可以被放到继承关系中,通过多态函数调用实现。
创建和使用null对象而不是去检测空值 有时,null对象可以有一些相关的通用功能或数据,诸如引用一个不知名字的resident对象时把它作为“occupant”。遇到这种情况,应该把处理null值的功能从客户代码中提出来,放到相应的类中。做法如下:设计一个Customer类,在resident未知时将其定义为“occupant”;而不是让Customer类的客户代码反复检测对象的名字是否已知,并在未知时用“occupant”代替它。
提取子程序或者方法 把内嵌的代码(inline code)从一个子程序中提取出来,并将其提炼为单独的子程序。
将子程序的代码内联化 如果子程序的程序体很简单,且含义不言自明,那么就在使用的时候直接使用这些代码。
将冗长的子程序转换为类 如果子程序太长,可以将其转换为类,然后进一步对之前的子程序进行分解,通过所得到的多个子程序来改善该代码的可读性。
用简单的算法替代复杂算法
增加参数 如果子程序需要从调用方获得更多的信息,可以增加它的参数从而为其提供信息。
删除参数 如果子程序已经不再使用某个参数,就删掉它。
将查询操作从修改操作中独立出来 通常,查询操作并不改变对象的状态。一次,一旦有了类似GetTotals()的操作改变了对象的状态,就应该将查询功能从状态改变中独立出来,提供两个独立的子程序。
合并相似的子程序,通过参数区分它们的功能 两个相似子程序唯一区别或许只是其中用到的常量值不同。请把它们合并到一起,然后将常量值通过参数传入。
将行为取决于参数的子程序拆分开来 如果一个子程序根据输入参数的值执行了不同的代码,请考虑将它拆分成几个可以被单独调用的、无须传递特定参数的子程序。
传递整个对象而非特定成员 如果发现有同一个对象的多个值被传递给了一个子程序,考虑是否可修改其接口使之接受整个对象。
传递特定成员而非整个对象
包装向下转型的操作 通常当子程序返回一个对象时,应当返回其已知的最精确的对象。这尤其适用于返回迭代器、群集、群集元素等的情况。
将值对象转化为引用对象 如果发现自己创建并维护着多个一模一样的大型复杂对象,请改变对这些对象的使用方式。即仅仅保留一份主拷贝(值对象),然后其他地方使用对该对象的引用(引用对象)。
将引用对象转化为值对象 如果看到自己对某个小型的简单对象进行了多次引用操作,请将这些对象都设置为值对象。
用数据初始化替代虚函数 如果有一组派生类,差别仅仅是虚函数返回的常量不同。与其派生类中覆盖成员函数,不如让派生类在初始化时设定适当的常量值,然后使用基类中的通用代码处理这些值。
指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
改变成员函数或成员数据的位置 请考虑对类的继承体做出修改。这些修改通常可以减少派生类的重复工作:
将特殊代码提取为派生类 如果某类中的一部分代码仅仅被其部分实例所使用,应该把这部分特殊的代码放到其派生类中。
将相似的代码结合起来放置到基类中 如果两个派生类中有相似的代码,将这些代码结合起来并放到基类中。
将成员函数放到另一个类中 在目标类中创建一个新的成员函数,然后从原类中将函数体移到目标类中。然后再旧的成员函数中调用新的成员函数。
将一个类变成两个 如果一个类同时具备两种或更多的截然不同的功能,请把这个类转化为多个类,使得每个类完成一种明确定义的功能。
删除类 如果某个类无所事事,就应该把该类的代码放到与所完成功能关系更为密切的另一个类中,然后把这个类删掉。
去除委托关系 有时类A调用了类B和类C,而实际上类A只应该调用类B,而B类应该调用类C。在这种情况下就应当考虑A对B的接口抽象是否合适。如果应该由B负责调用C,那么就应该只有B调用C。
去掉中间人 如果存在类A调用类B,类B调用类C的情况,有时让类A直接调用类C会更好。是否应当去掉类B,取决于怎么做才能最好地维护类B接口的完整性。
用委托代替继承 如果某类需要用到另一个类,但又打算获取该类接口更多的控制权,那么可以让基类成为原派生类的一个成员,并公开它的一组成员函数,以完成一种内聚的抽象。
用继承代替委托 如果某个类公开了委托类(成员类)所有成员函数,那么该类应该从委托类继承而来,而不是使用该类。
引入外部的成员函数 如果一个客户类需要被调用类的某个额外的成员函数,而你又无法去修改被调用类,那么可以通过在客户类(client class)中创建新成员函数的方式来提供此功能。
引入扩展类 如果一个类需要多个额外的成员函数,你同样无法修改该类,你可以创建一个新类。该类包括了原类的功能以及新增加的功能。要实现这点,你既可通过原类派生新类然后添加新的成员函数,也可以将原类进行包装,使新类调用所需要的成员函数。
对暴露在外的成员变量进行封装 如果数据成员是公用的,请将其改为私用,然后通过成员函数来访问该数据成员的值。
对于不能修改的类成员,删除相关的Set()成员函数
隐藏那些不会在类之外被用到的成员函数
封装不会使用的成员函数 如果发现自己往往只使用类接口的一部分,那么就为类创建新的接口,仅仅把那些必须的成员函数暴露给类的外部,需要注意,新的接口应该为类提供一致的抽象。
合并那些实现非常类似的基类和派生类 如果派生类并未提供更多的特殊化,那么就应该把它合并会基类。
为无法控制的数据创建明确的索引源 有时,你需要让特定系统来维护数据,而在其他需要使用该数据的对象中,你却无法方便或一致地访问这些数据。常见的例子如在GUI控件中维护的数据。在这样的情况下,你需要创建一个类,由该类里映射GUI控件中的数据,然后让GUI控件和其他代码将此类作为该数据的明确来源。
将单向的类联系改为双向的类联系 如果你有两个类,且它们各自需要用到对方的功能,但仅有一个类能访问另一个类。这时就应该将对两个类进行修改,使其相互调用。
将双向的类联系改为单向的类联系 如果有两个类,彼此都知道对方,但实际上只有一个类需要访问另一个类。这时就应该只让那个有实际需要的类能访问另一个类,而另一个类无法访问该类。
用Factory Method方式而不是简单地构造函数 在需要基于类型码创建对象,或者希望使用引用对象而非值对象的时候,应当使用Factory Method(函数)。
用异常取代错误处理代码,或者做相反方向的变换
与其将分解一个正常工作的系统比作替换水槽里面的塞子,倒不如把它看成是替换大脑中的一根神经。如果我们把软件维护称为“软件脑部外科手术”,工作起来会不会要轻松一些?
——Gerald Weinberg
保存初始代码 在开始重构之前,要保证你还能回到代码的初始状态。
重构的步伐请小些 有的重构的步伐比其他重构更大,到底什么能算成是一次重构并不明确。因此请把重构的步伐放小些,这样才能理解所做修改对程序的全部影响。
同一时间只做一项重构 有的重构会比其他的重构更为复杂。除非是对那些最为简单的重构,否则请在同一时间只做一项重构,在进入下一项重构之前,对代码重新编译并测试。
把要做的事情一条条列出来
设置一个停车场 在某次重构的路途上,你可能会发现你需要进行另一次重构。正在着手这次新的重构时,或许又发现第三个重构会给程序带来很多好处。为了处理这些并不需要立即对付的修改工作,你最好设置一个“停车场”,把你需要在未来某个时间进行而现在可以先放在一边的修改工作列出来。
多使用检查点 在重构的时候,很容易出现代码没有按照设想正常运行的情况。除了保存初始代码外,在重构中还应在多个地方设置检查点。这样一来,即使你编码时钻进了死胡同,你仍然可以让程序回到正常工作的状态。
利用编译器警告信息
重新测试 应该把重新测试作为检查所修改代码工作的补充。当然,这点要取决于从一切开始你是否就有一套优秀的测试用例。
增加测试用例 除了重新运行过去做过的那些测试,还应该增加新的单元测试来检验新引入的代码。如果重构是的一些测试用例已经过时,那么就删除这些用例。
检查对代码的修改 如果说在第一次运行程序的时候检查代码是必需的,那么在接下来的修改工作中,时刻关注代码则更为重要。当代码修改行数从1增加到5的时候,改错的可能性大大增加。在这之后,随着行数的增加,出错的几率可是逐渐降低了。
程序员对于很小的修改常常不以为然。他们不会用纸和笔来推敲程序,也不会让其他人来检查代码。有时甚至根本不会运行这些代码来验证修改工作的正确性。
根据重构风险级别来调整重构方法 对于那些有一定风险的重构,谨慎才能避免出错。务必一次只处理一项重构。除了完成通常要做的编译检查和单元测试之外,还应该让其他人来检查你的重构工作,或是针对重构采用结对编程。
不要只实现一部分功能,并指望将来的重构能完成它。——John Manzo
重构是一剂良药,但不是包治百病的灵丹妙药。
不要把重构当做先写后改的代名词 重构最大的问题在于被滥用。程序员们有时会说自己是在重构,而实际上他们所完成的工作仅仅是对无法运行的代码修修补补,希望能让程序跑起来。重构的含义是在不影响程序行为的前提下改进可运行的代码。那些修补破烂代码的程序员们不是在重构,而是在拼凑代码(hacking)。
避免用重构代替重写 有时,代码所需要的不是细微修改,而是直接一脚踢出门外,这样你就可以全部重新开始。如果发现自己处于大规模的重构之中,就应该问问自己是否应该把这部分代码推倒重来,重新设计,重新开发。
对任何特定程序都能带来好处的重构方法本应是无穷无尽的。和其他编程行为一样,重构同样受制于收益递减定律,同样也符合80/20法则。在斟酌哪种重构方法最为重要的时候,不妨考虑一下下面这些建议。
收益递减规律是指其他投入固定不变时,连续地增加某一种投入,所新增的产出最终会减少的规律。该规律另一种等价的说法是:超过某一水平之后边际投入的边际产出下降。
二八定律又名80/20定律、帕累托法则(定律)也叫巴莱特定律、最省力的法则、不平衡原则等,是19世纪末20世纪初意大利经济学家巴莱多发现的。他认为,在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%尽管是多数,却是次要的,因此又称二八定律。
在增加子程序时进行重构 在增加子程序时,检查一下相关的子程序是否都被合理地组织起来了。如果没有,那么就重构这些子程序。
在添加类的时候进行重构 添加一个类往往会使已有代码中的问题浮出水面。
在修补缺陷的时候进行重构 如果你在修补缺陷中有了一些心得体会,请把它运用到改善其他易于产生相似错误的代码上。
关注易于出错的代码 有的模块更容易出错,健壮性远逊于其他模块。尽管绝大部分人对这部分富于挑战性代码的自然反应都会是敬而远之,但集中处理这样的代码将是最为有效的重构策略。
关注高度复杂的模块 另一种方法就是关注最为复杂的模块。一项经典研究表明,当做维护的程序员们把改善代码的精力放在那些最为复杂的模块上时,程序的质量会有显著提升。
在维护环境下,改善你手中正在处理的代码 未予修改的代码是没有必要进行重构的。但如果你正在维护某部分代码,请确保代码在离开你的时候比来之前更健康。
定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界 “现实世界”通常会比你想象的更加混乱。这种状态或是源于复杂的业务规则,或是来自软硬件接口。对那些古董系统而言,常见的麻烦就是人们会要求那些拙劣编写的产品代码自始至终都能工作下去。
import lombok.Data;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;
/**
* @author liuqian
*/
public class RemoveDuplicates {
@Test
public void removeDuplicatesByName() {
Person person1 = new Person() {{
setId(1);
setName("person1");
}};
Person person2 = new Person() {{
setId(2);
setName("person2");
}};
Person person3 = new Person() {{
setId(3);
setName("person2");
}};
List<Person> personList = new ArrayList<>();
personList.add(person1);
personList.add(person2);
personList.add(person3);
System.out.println("====================before================");
for (Person person : personList) {
System.out.println(person.toString());
}
List<Person> unique = personList.stream().collect(
collectingAndThen(
toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getName))), ArrayList::new)
);
System.out.println("------------------after-------------------");
for (Person person : unique) {
System.out.println(person.toString());
}
}
@Data
private class Person {
private int id;
private String name;
@Override
public String toString() {
return "Person: id is " + id + ", name is " + name;
}
}
}
输出结果:
====================before================
Person: id is 1, name is person1
Person: id is 2, name is person2
Person: id is 3, name is person2
------------------after-------------------
Person: id is 1, name is person1
Person: id is 2, name is person2
机器人是否会梦到电子羊,这是一个好问题。而科学事实也已经发展到开始与科幻小说重合的地步。
虽然我们还没有自主的机器人为自身的生存危机挣扎——到目前为止——却越来越接近人们称之为“人工智能”的东西。
《银翼杀手》(英语:Do Androids Dream of Electric Sheep?),直译是《仿生人会梦见电子羊吗?》,是美国科幻小说作家菲利普·K·迪克的最重要作品之一,第一次出版是在1968年,并改编成1982年电影《银翼杀手》,而该书的很多元素和主题都用于2017年续集电影《银翼杀手2049》。
机器学习是人工智能的一个子领域,其中计算机算法用于从数据和信息中自主学习。在机器学习中,计算机不一定要明确编程,但要可以自行改变和改进它们的算法。
如今,机器学习算法可以让电脑能够与人类交流,能自动驾驶汽车,能编写并发布运动赛事报告,而且能够找到恐怖主义的嫌疑人。
我坚信机器学习将会严重影响大多数行业及其工作岗位,这也是为什么每个(产品/技术)经理至少应该掌握什么是机器学习以及它是如何演变的。
在这篇文章中,我准备了一个快速浏览机器学习起源及其最近的里程碑的时间之旅。
1950年——艾伦·图灵(Alan Turing)创造了“图灵测试”来确定计算机是否具有真正的智慧。要通过测试,计算机必须能够欺骗一个人类,让他相信它也是一个人类。
1952年——亚瑟·塞缪尔(Arthur Samuel)编写了第一个电脑学习程序。该程序是关于跳棋游戏的,旨在研究哪些走法构成了获胜策略然后将其纳入程序当中,计算机玩得次数越多则提高越多。
1957年——弗兰克·罗森布特(Frank Rosenblatt)设计了计算机的第一个神经网络(感知器),它可以模拟人脑的思维过程。
1967年——“邻近(最近邻)算法”(nearest neighbor)诞生,允许计算机开始使用非常基本的模式识别。这可以为出差的推销员制定路线,从一个随机城市开始,而能确保他们在短距离旅途期间到达所有城市。
1979年——斯坦福大学的学生发明了“斯坦福购物车”(Stanford Cart),可以在房间里自行导航识别障碍物。
1981年——杰拉德·德琼(Gerald Dejong)介绍了基于解释学习(EBL)的概念,其中计算机分析训练数据,并通过丢弃不重要的数据创建器可遵循的通用规则。
1985年——特里·塞杰瑙斯基(Terry Sejnowski)发明了NetTalk,它可以学习像婴儿一样发音。
20世纪90年代——(人们)致力于将机器学习从知识驱动转变为数据驱动方法。科学家们开始创建计算机程序来分析大料数据,并从结果中得出结论或“学习”。
1997年——IBM的深蓝计算机(Deep Blue)打败了国际象棋的世界冠军。
2006年——杰弗里·辛顿(Geoffrey Hinton)用属于“深度学习”来结束让计算机“看到”并区分图像、视频中的对象以及文本的新算法。
2010年——微软的Kinect可以30次每秒的速度追踪20个人的人物特征,让人们可以通过动作和手势与电脑进行互动。
2011年——IBM的沃森(能够使用自然语言来回答问题的人工智能系统)在《危险边缘》节目上打败了它的人类竞争者。《危险边缘》(英语:Jeopardy!)是由梅夫·格里芬在1964年创建的美国的电视智力竞赛节目。就像同一类的其它节目,节目涵盖了历史、语言、文学、艺术、科技、流行文化、体育、地理、文字游戏等多方面内容。然而,与这些节目不同的是,《危险边缘》采取一种独特的问答形式:参赛者须根据以答案形式提供的各种线索,以问题的形式作出正确的回答。
2011年——谷歌大脑被开发出来,它的深度神经网络可以像一只猫一样去发现和归类事物。
2012年——谷歌X实验室开发了一个机器学习算法,能够自动浏览YouTube来识别那些含有猫的视频。
2014年——Facebook开发了一个软件算法——DeepFace,该算法能够识别或验证照片上的个体,而且与人类能够达到的水平相同。
2015年——亚马逊推出了自己的机器学习平台。
2015年——微软创建了分布式的机器学习工具包,可以在多台计算机上高效地分配机器学习的问题。
2015年——超过3000名AI和机器人的研究人员,在史蒂芬·霍金、艾伦·马斯克和史蒂夫·沃兹尼亚克(等等)支持下,签署了一封公开信,警示人们在没有人为干预的情况下选择和参与目标,自动化武器的危险。
2016年——谷歌的人工智能算法打败了**棋盘游戏围棋的专业选手,围棋被公认为是世界上最复杂的棋盘游戏,比国际象棋要难上好多倍。AlphaGo由Google DeepMind开发,该算法在五场围棋比赛中均获得胜利。
那么我们是否越来越接近人工智能了呢?一些科学家认为这实际上是一个错误的问题。
他们认为一台计算机永远也无法像一个人类那样去“思考”,而将计算机的计算分析与算法与人类思维的诡计进行比较就像比较苹果和橘子,根本无法相比。
无论如何,计算机观察、理解,并且与世界互动的能力正在以惊人的速度增长。随着我们生产的数据量继续呈指数增长,我们的计算机处理、分析以及从数据增长和扩展的学习能力也会随之增强。
本文打算介绍几个不太容易说出其区别,或者用途的 Spring 注解,比如 @Component
与 @Bean
的比较,@ControllerAdvice
是如何处理自定义异常的等等。
@Component
和 @Bean
的区别是什么?@Component
注解作用于类,而 @Bean
注解作用于方法、@Component
通常是通过路径扫描来自动侦测以及自动装配到 Spring
容器中(我们可以使用 @ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring
的 bean
容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean
,@Bean
告诉了 Spring
这是某个类的实例,当我们需要用它的时候还给我。@Bean
注解比 @Component
注解的自定义性更强,而且很多地方我们只能通过 @Bean
注解来注册 bean
。比如当我们引用第三方库中的类需要装配到 Spring
容器时,只能通过 @Bean
来实现。@Bean
注解使用示例:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
@Component
注解使用示例:
@Component
public class ServiceImpl implements AService {
....
}
下面这个例子是通过 @Component
无法实现的:
@Bean
public OneService getService(status) {
case (status) {
when 1:
return new serviceImpl1();
when 2:
return new serviceImpl2();
when 3:
return new serviceImpl3();
}
}
Autowire
和 @Resource
的区别@Autowire
和 @Resource
都可以用来装配bean,都可以用于字段或setter方法。@Autowire
默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false。@Resource
默认按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。名称可以通过 name 属性指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名,当注解写在 setter 方法上时,默认取属性名进行装配。注意:如果 name 属性一旦指定,就只会按照名称进行装配。
@Autowire
和@Qualifier
配合使用效果和@Resource
一样:
@Autowired(required = false) @Qualifier("example")
private Example example;
@Resource(name = "example")
private Example example;
@Resource
装配顺序
@Component
:通用的注解,可标注任意类为 Spring
的组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component
注解标注。@Repository
:对应持久层即 Dao 层,主要用于数据库相关操作。@Service
:对应服务层,主要设计一些复杂的逻辑,需要用到 Dao 层。@Controller
:对应 Spring MVC 控制层,主要用来接受用户请求并调用 Service 层返回数据给前端页面。@Configuration
:声明该类为一个配置类,可以在此类中声明一个或多个 @Bean
方法。@Configuration
:配置类注解@Configuration
标明在一个类里可以声明一个或多个 @Bean
方法,并且可以由 Spring
容器处理,以便在运行时为这些 bean 生成 bean 定义和服务请求,例如:
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean ...
}
}
我们可以通过 AnnotationConfigApplicationContext
来注册 @Configuration
类:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
MyBean myBean = ctx.getBean(MyBean.class);
// use myBean ...
另外也可以通过组件扫描(component scanning)来加载,@Configuration
使用 @Component
进行原注解,因此 @Configuration
类也可以被组件扫描到(特别是使用 XML 的 <context:component-scan />
元素)。@Configuration
类不仅可以使用组件扫描进行引导,还可以使用 @ComponentScan
注解自行配置组件扫描:
@Configuration
@ComponentScan("com.acme.app.services")
public class AppConfig {
// various @Bean definitions ...
}
使用 @Configuration
的约束:
static
。@Bean
方法可能不会反过来创建更多的配置类。除了单独使用 @Configuration
注解,我们还可以结合一些外部的 bean 或者注解共同使用,比如 Environment
API,@PropertySource
,@Value
,@Profile
等等许多,这里就不做详细介绍了,更多的用法可以参看 Spring @Configuration
的相关文档 。
@ControllerAdvice
:处理全局异常利器在 Spring 3.2 中,新增了 @ControllerAdvice
、@RestControllerAdvice
、@RestController
注解,可以用于定义 @ExceptionHandler
、@InitBinder
、@ModelAttribute
,并应用到所有 @RequestMapping
、@PostMapping
、@GetMapping
等这些 Controller 层的注解中。
默认情况下,@ControllerAdvice
中的方法应用于全局所有的 Controller。而使用选择器 annotations()
,basePackageClasses()
和 basePackages()
(或其别名value()
)来定义更小范围的目标 Controller 子集。 如果声明了多个选择器,则应用 OR
逻辑,这意味着所选的控制器应匹配至少一个选择器。 请注意,选择器检查是在运行时执行的,因此添加许多选择器可能会对性能产生负面影响并增加复杂性。
@ControllerAdvice
我们最常使用的是结合 @ExceptionHandler
用于全局异常的处理。可以结合以下例子,我们可以捕获自定义的异常进行处理,并且可以自定义状态码返回:
@ControllerAdvice("com.developlee.errorhandle")
public class MyExceptionHandler {
/**
* 捕获CustomException
* @param e
* @return json格式类型
*/
@ResponseBody
@ExceptionHandler({CustomException.class}) //指定拦截异常的类型
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //自定义浏览器返回状态码
public Map<String, Object> customExceptionHandler(CustomException e) {
Map<String, Object> map = new HashMap<>();
map.put("code", e.getCode());
map.put("msg", e.getMsg());
return map;
}
}
更多信息可以参看 Spring @ControllerAdvice
的官方文档 。
@Component
, @Repository
, @Service
的区别注解 | 含义 |
---|---|
@component | 最普通的组件,可以被注入到 Spring 容器进行管理 |
@repository | 作用于持久层 |
@service | 作用于业务逻辑层 |
@controller | 作用于表现层(spring-mvc的注解) |
@Component
是一个通用的Spring容器管理的单例bean组件。而@Repository
, @Service
, @Controller
就是针对不同的使用场景所采取的特定功能化的注解组件。
因此,当你的一个类被@component所注解,那么就意味着同样可以用@Repository
, @Service
, @Controller
来替代它,同时这些注解会具备有更多的功能,而且功能各异。
最后,如果你不知道要在项目的业务层采用@Service
还是@Component
注解。那么,@Service
是一个更好的选择。
以上简单介绍了几种 Spring 中的几个注解及代码示例,就我个人而言,均是平时用到且不容易理解的几个,或者容易忽略的几个。当然,这篇文章并没有完全介绍完,在今后还会继续补充完善。
在java.commons.lang3的包中有许多方便好用的工具类,类似于处理字符串的StringUtils,处理日期的DateUtils等等,StringEscapeUtils也是其中的一员。
StringEscapeUtils是在java.commons.lang3的2.0版本中加入的工具类,在3.6版本中被标注为@deprecated,表明在之后的版本中则为过时状态,之后StringEscapeUtils类被移到java.commons.text包下。
StringEscapeUtils的主要功能就是为Java,Java Script,Html,XML进行转义与反转义。
除了列出的几个较常用的方法,还有escapeJson(String input) / unescapeJson(String input)、escapeCsv(String input) / unescapeCsv(String input)等等,可以看一下下面的执行例子,有个直观的认识。
在项目中引入java.commons.text包(版本号可以访问官网选择):
gradle:
// https://mvnrepository.com/artifact/org.apache.commons/commons-text
compile group: 'org.apache.commons', name: 'commons-text', version: '1.3'
maven:
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.3</version>
</dependency>
import org.apache.commons.text.StringEscapeUtils;
import org.junit.Test;
/**
* @author liuqian
* @date 2018/4/3 16:27
*/
public class EscapeTest {
@Test
public void escapeTest() {
System.out.println("转义/反转义Java字符串");
String javaString = "这是Java字符串";
System.out.println(StringEscapeUtils.escapeJava(javaString));
System.out.println(StringEscapeUtils.unescapeJava(StringEscapeUtils.escapeJava(javaString)));
System.out.println("-------------------------------------------------------------");
System.out.println("转义/反转义Json字符串");
String jsonString = "{\"keyword\": \"这是Json字符串\"}";
System.out.println(StringEscapeUtils.escapeJson(jsonString));
System.out.println(StringEscapeUtils.unescapeJson(StringEscapeUtils.escapeJson(jsonString)));
System.out.println("-------------------------------------------------------------");
//除了html4还有html3等格式
System.out.println("转义/反转义Html字符串");
String htmlString = "<strong>加粗字符</strong>";
System.out.println(StringEscapeUtils.escapeHtml4(htmlString));
System.out.println(StringEscapeUtils.unescapeHtml4(StringEscapeUtils.escapeHtml4(htmlString)));
System.out.println("-------------------------------------------------------------");
//除了xml10还有xml11等格式
System.out.println("转义/反转义xml字符串");
String xmlString = "<xml>\"xml字符串\"</xml>";
System.out.println(StringEscapeUtils.escapeXml10(xmlString));
System.out.println(StringEscapeUtils.unescapeXml(StringEscapeUtils.escapeXml10(xmlString)));
System.out.println("-------------------------------------------------------------");
System.out.println("转义/反转义csv字符串");
String csvString = "1997,Ford,E350,\"Super, luxurious truck\"";
System.out.println(StringEscapeUtils.escapeCsv(csvString));
System.out.println(StringEscapeUtils.unescapeCsv(StringEscapeUtils.escapeCsv(csvString)));
System.out.println("-------------------------------------------------------------");
System.out.println("转义/反转义Java Script字符串");
String jsString = "<script>alert('1111')</script>";
System.out.println(StringEscapeUtils.escapeEcmaScript(jsString));
System.out.println(StringEscapeUtils.unescapeEcmaScript(StringEscapeUtils.escapeEcmaScript(jsString)));
}
}
结果如下:
转义/反转义Java字符串
\u8FD9\u662FJava\u5B57\u7B26\u4E32
这是Java字符串
-------------------------------------------------------------
转义/反转义Json字符串
{\"keyword\": \"\u8FD9\u662FJson\u5B57\u7B26\u4E32\"}
{\"keyword\": \"这是Json字符串\"}
-------------------------------------------------------------
转义/反转义Html字符串
<strong>加粗字符</strong>
<strong>加粗字符</strong>
-------------------------------------------------------------
转义/反转义xml字符串
<xml>"xml字符串"</xml>
<xml>"xml字符串"</xml>
-------------------------------------------------------------
转义/反转义csv字符串
"1997,Ford,E350,""Super, luxurious truck"""
1997,Ford,E350,"Super, luxurious truck"
-------------------------------------------------------------
转义/反转义Java Script字符串
<script>alert(\'1111\')<\/script>
<script>alert('1111')</script>
什么是幂等?以及你是怎么理解的?
幂等(idempotent、idempotence) 是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。
HTTP GET 方法用于获取资源,不应有副作用,所以是幂等的。 请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。
HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。 比如:DELETE http://www.forum.com/article/4231
,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。
HTTP POST方法用于创建资源,所对应的URI并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。 两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。
HTTP PUT方法用于创建或更新操作,所对应的URI是要创建或更新的资源本身,有副作用,它应该满足幂等性。 对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。
简单来讲,就是分布式系统需要提供具有幂等性的对外接口,即用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。保证最终的结果一致性。
比如支付,用户购买商品使用约支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条。这种情况是不允许出现的。
在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
那么如何设计具有幂等性的接口呢?
方法一:单次支付请求,也就是直接支付了,不需要额外的数据库操作了,这个时候发起异步请求创建一个唯一的ticketId,就是门票,这张门票只能使用一次就作废,具体步骤如下:
1、异步请求获取门票
2、调用支付,传入门票
3、根据门票ID查询此次操作是否存在,如果存在则表示该操作已经执行过,直接返回结果;如果不存在,支付扣款,保存结果
4、返回结果到客户端
如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的。
方法二:分布式环境下各个服务相互调用
这边就要举例我们的系统了,我们支付的时候先要扣款,然后更新订单,这个地方就涉及到了订单服务以及支付服务了。用户调用支付,扣款成功后,更新对应订单状态,然后再保存流水。而在这个地方就没必要使用门票ticketId了,因为会比较的麻烦(支付状态:未支付,已支付)
步骤:
1、查询订单支付状态
2、如果已经支付,直接返回结果
3、如果未支付,则支付扣款并且保存流水
4、返回支付结果
如果步骤4通信失败,用户再次发起请求,那么最终结果还是一样的。
对于做过支付的朋友,幂等也可以称之为冲正,保证客户端与服务端的交易一致性,避免多次扣款。
保证系统的幂等性就是保证系统结果的最终一致性。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.