开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

二叉树就是这么简单 一、二叉树就是这么简单 二、动态创建二叉

发表于 2018-03-24

一、二叉树就是这么简单

本文撇开一些非常苦涩、难以理解的概念来讲讲二叉树,仅入门观看(或复习)….

首先,我们来讲讲什么是树:

  • 树是一种非线性的数据结构,相对于线性的数据结构(链表、数组)而言,树的平均运行时间更短(往往与树相关的排序时间复杂度都不会高)

在现实生活中,我们一般的树长这个样子的:

但是在编程的世界中,我们一般把树**“倒”**过来看,这样容易我们分析:

一般的树是有很多很多个分支的,分支下又有很多很多个分支,如果在程序中研究这个会非常麻烦。因为本来树就是非线性的,而我们计算机的内存是线性存储的,太过复杂的话我们无法设计出来的。

因此,我们先来研究简单又经常用的—> 二叉树

1.1树的一些概念

我就拿上面的图来进行画来讲解了:

二叉树的意思就是说:每个节点不能多于有两个儿子,上面的图就是一颗二叉树。

  • 一棵树至少会有一个节点(根节点)
  • 树由节点组成,每个节点的数据结构是这样的:
  • 因此,我们定义树的时候往往是**->定义节点->节点连接起来就成了树**,而节点的定义就是:一个数据、两个指针(如果有节点就指向节点、没有节点就指向null)

1.2静态创建二叉树

上面说了,树是由若干个节点组成,节点连接起来就成了树,而节点由一个数据、两个指针组成

  • 因此,创建树实际上就是创建节点,然后连接节点

首先,使用Java类定义节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码

public class TreeNode {

// 左节点(儿子)
private TreeNode lefTreeNode;

// 右节点(儿子)
private TreeNode rightNode;

// 数据
private int value;


}

下面我们就拿这个二叉树为例来构建吧:

为了方便构建,我就给了它一个带参数的构造方法和set、get方法了:

1
2
3
4
5
复制代码
public TreeNode(int value) {

this.value = value;
}

那么我们现在就创建了5个节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码

public static void main(String[] args) {

//根节点-->10
TreeNode treeNode1 = new TreeNode(10);

//左孩子-->9
TreeNode treeNode2 = new TreeNode(9);

//右孩子-->20
TreeNode treeNode3 = new TreeNode(20);

//20的左孩子-->15
TreeNode treeNode4 = new TreeNode(15);

//20的右孩子-->35
TreeNode treeNode5 = new TreeNode(35)

}

它们目前的状态是这样子的:

于是下面我们去把它连起来:

1
2
3
4
5
6
7
8
9
复制代码

//根节点的左右孩子
treeNode1.setLefTreeNode(treeNode2);
treeNode1.setRightNode(treeNode3);

//20节点的左右孩子
treeNode3.setLefTreeNode(treeNode4);
treeNode3.setRightNode(treeNode5);

连接完之后,那么我们的树就创建完成了。

1.3遍历二叉树

上面说我们的树创建完成了,那怎么证明呢??我们如果可以像数组一样遍历它(看它的数据),那就说明它创建完成了~

值得说明的是:二叉树遍历有三种方式

  • 先序遍历
    • 先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
  • 中序遍历
    • 先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
  • 后序遍历
    • 先访问左节点,然后访问右节点,最后访问根节点(左->右->根)

以上面的二叉树为例:

  • 如果是先序遍历:10->9->20->15->35
  • 如果是中序遍历:9->10->15->20->35
    • 可能需要解释地方:访问完10节点过后,去找的是20节点,但20下还有子节点,因此先访问的是20的左儿子15节点。由于15节点没有儿子了。所以就返回20节点,访问20节点。最后访问35节点
  • 如果是后序遍历:9->15->35->20->10
    • 可能需要解释地方:先访问9节点,随后应该访问的是20节点,但20下还有子节点,因此先访问的是20的左儿子15节点。由于15节点没有儿子了。所以就去访问35节点,由于35节点也没有儿子了,所以返回20节点,最终返回10节点

一句话总结:先序(根->左->右),中序(左->根->右),后序(左->右->根)。如果访问有孩子的节点,先处理孩子的,随后返回

无论先中后遍历,每个节点的遍历如果访问有孩子的节点,先处理孩子的(逻辑是一样的)

  • 因此我们很容易想到递归
  • 递归的出口就是:当没有子节点了,就返回

因此,我们可以写出这样的先序遍历代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
/**
* 先序遍历
* @param rootTreeNode 根节点
*/
public static void preTraverseBTree(TreeNode rootTreeNode) {

if (rootTreeNode != null) {

//访问根节点
System.out.println(rootTreeNode.getValue());

//访问左节点
preTraverseBTree(rootTreeNode.getLefTreeNode());

//访问右节点
preTraverseBTree(rootTreeNode.getRightNode());
}
}

结果跟我们刚才说的是一样的:

我们再用中序遍历调用一遍吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
/**
* 中序遍历
* @param rootTreeNode 根节点
*/
public static void inTraverseBTree(TreeNode rootTreeNode) {

if (rootTreeNode != null) {

//访问左节点
inTraverseBTree(rootTreeNode.getLefTreeNode());

//访问根节点
System.out.println(rootTreeNode.getValue());

//访问右节点
inTraverseBTree(rootTreeNode.getRightNode());
}
}

结果跟我们刚才说的是一样的:

有意思的是:通过先序和中序或者中序和后序我们可以还原出原始的二叉树,但是通过先序和后续是无法还原出原始的二叉树的

  • 也就是说:通过中序和先序或者中序和后序我们就可以确定一颗二叉树了!

二、动态创建二叉树

上面我们是手动创建二叉树的,一般地:都是给出一个数组给你,让你将数组变成一个二叉树,此时就需要我们动态创建二叉树了。

二叉树中还有一种特殊的二叉树:二叉查找树(binary search tree)

  • 定义:当前根节点的左边全部比根节点小,当前根节点的右边全部比根节点大。
    • 明眼人可以看出,这对我们来找一个数是非常方便快捷的

往往我们动态创建二叉树都是创建二叉查找树

2.1动态创建二叉树体验

假设我们有一个数组:int[] arrays = {3, 2, 1, 4, 5};

那么创建二叉树的步骤是这样的:

  • 首先将3作为根节点

  • 随后2进来了,我们跟3做比较,比3小,那么放在3的左边

  • 随后1进来了,我们跟3做比较,比3小,那么放在3的左边,此时3的左边有2了,因此跟2比,比2小,放在2的左边

  • 随后4进来了,我们跟3做比较,比3大,那么放在3的右边

  • 随后5进来了,我们跟3做比较,比3大,那么放在3的右边,此时3的右边有4了,因此跟4比,比4大,放在4的右边

那么我们的二叉查找树就建立成功了,无论任何一颗子树,左边都比根要小,右边比根要大

2.2代码实现

我们的代码实现也很简单,如果比当前根节点要小,那么放到当前根节点左边,如果比当前根节点要大,那么放到当前根节点右边。

因为是动态创建的,因此我们得用一个类来表示根节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码

public class TreeRoot {

private TreeNode treeRoot;

public TreeNode getTreeRoot() {
return treeRoot;
}

public void setTreeRoot(TreeNode treeRoot) {
this.treeRoot = treeRoot;
}
}

比较与根谁大,大的往右边,小的往左边:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码

/**
* 动态创建二叉查找树
*
* @param treeRoot 根节点
* @param value 节点的值
*/
public static void createTree(TreeRoot treeRoot, int value) {


//如果树根为空(第一次访问),将第一个值作为根节点
if (treeRoot.getTreeRoot() == null) {
TreeNode treeNode = new TreeNode(value);
treeRoot.setTreeRoot(treeNode);

} else {

//当前树根
TreeNode tempRoot = treeRoot.getTreeRoot();

while (tempRoot != null) {
//当前值大于根值,往右边走
if (value > tempRoot.getValue()) {

//右边没有树根,那就直接插入
if (tempRoot.getRightNode() == null) {
tempRoot.setRightNode(new TreeNode(value));
return ;
} else {
//如果右边有树根,到右边的树根去
tempRoot = tempRoot.getRightNode();
}
} else {
//左没有树根,那就直接插入
if (tempRoot.getLefTreeNode() == null) {
tempRoot.setLefTreeNode(new TreeNode(value));

return;
} else {
//如果左有树根,到左边的树根去
tempRoot = tempRoot.getLefTreeNode();
}
}
}
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码
int[] arrays = {2, 3, 1, 4, 5};

//动态创建树

TreeRoot root = new TreeRoot();
for (int value : arrays) {
createTree(root, value);
}

//先序遍历树
preTraverseBTree(root.getTreeRoot());
System.out.println("---------------公众号:Java3y");

//中序遍历树
inTraverseBTree(root.getTreeRoot());
System.out.println("---------------公众号:Java3y");

三、查询二叉查找树相关

3.1查询树的深度

查询树的深度我们可以这样想:左边的子树和右边的字数比,谁大就返回谁,那么再接上根节点+1就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码
public static int getHeight(TreeNode treeNode) {

if (treeNode == null) {
return 0;
} else {

//左边的子树深度
int left = getHeight(treeNode.getLefTreeNode());

//右边的子树深度
int right = getHeight(treeNode.getRightNode());


int max = left;

if (right > max) {
max = right;
}
return max + 1;
}
}

3.1查询树的最大值

从上面先序遍历二叉查找树的时候,细心的同学可能会发现:中序遍历二叉查找树得到的结果是排好顺序的~

那么,如果我们的二叉树不是二叉查找树,我们要怎么查询他的最大值呢?

可以这样:

  • 左边找最大值->递归
  • 右边找最大值->递归
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码

/**
* 找出树的最大值
*
* @param rootTreeNode
*/
public static int getMax(TreeNode rootTreeNode) {

if (rootTreeNode == null) {
return -1;
} else {
//找出左边的最大值
int left = getMax(rootTreeNode.getLefTreeNode());

//找出右边的最大值
int right = getMax(rootTreeNode.getRightNode());

//与当前根节点比较
int currentRootValue = rootTreeNode.getValue();

//假设左边的最大
int max = left;


if (right > max) {
max = right;
}
if (currentRootValue > max) {
max = currentRootValue;
}

return max ;


}
}

四、最后

无论是在遍历树、查找深度、查找最大值都用到了递归,递归在非线性的数据结构中是用得非常多的…

树的应用也非常广泛,此篇简单地说明了树的数据结构,高级的东西我也没弄懂,可能以后用到的时候会继续深入…

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

刚接触一个 Laravel 项目,你可以从这些地方入手

发表于 2018-03-22

这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击 协同翻译介绍 。


当你接手一个新项目的时候,可能会感到无从下手,如果不熟悉编程,则更是如此。那么,我们该从哪儿入手呢?项目代码的哪些部分我们需要着重了解?下面我们看看 Laravel 项目的几个通用的部分。

项目文档

面对新项目时,文档可能是最有帮助的。如果项目包含文档,恭喜你,你非常幸运。但是,也别高兴地太早,因为文档可能早已经过时或覆盖不全面。项目文档通常编写在 readme 文件中、wiki,或者发布在 Confluence 和 Google Docs 之类共享平台上。如果你基于一个项目做开发,不要犹如,请积极的为项目文档做贡献:补充空白部分或者使其表达得更清晰明了。 zyxcba 翻译于 3天前 1 重译 由 Summer 审阅
如果你不够幸运的话(大多数时候都是如此),你接触的项目没有任何文档。缺少文档并不完全是一件坏事,因为在这种情况下,你有机会亲自为你的团队撰写文档。你和你的同事,以及你带来的新开发者,都将会在未来对你感激不尽。

撰写文档确实不是一件有趣的工作,但它对于保持项目的长期运行是很有必要的。项目文档不仅要列举使用的技术和初始安装方法,同时也应该阐述项目 “为什么这样” 以及 “如何进行” ,这通常不能清晰地用代码自身表达出来。某些高层次的设计选择及其原因也应该被写入文档,以帮助更好地理解代码。
ckmessi 翻译于 3天前 0 重译 由 Summer 审阅
composer.json


Composer 是一个 PHP 包管理工具,在过去的几年中帮助推动了 PHP 生态系统的快速前进。 Laravel 从版本4开始使用 Composer ,所以在项目基本都存在 composer.json 文件。你能够在项目根目录下找到 composer.json 文件和 composer.lock 文件。 lock 文件包含了项目中所需要的所有依赖包的准确版本,而 JSON 文件显示了依赖包的发布内容。目前,我们只对 JSON 文件中的版本信息感兴趣,如果你想学习这些文件的更多知识,可以阅读 这里。 在浏览 composer.json 文件时,注意到有一个 require 区块,看起来内容类似如下所示。

1
2
3
4
5
6
7
8
复制代码{
"require": {
"php": ">=7.1.3",
"fideloper/proxy": "~4.0",
"laravel/framework": "5.6.*",
"laravel/tinker": "~1.0"
}
}

在这个样例中,我们有一个基于 Laravel 5.6 的项目。它同时依赖于另外两个包,以及不低于7.1.3版本的 PHP 。在你的项目中,你很可能会看到更多依赖包,并且版本号可能会有所变化。

ckmessi 翻译于 3天前 0 重译 由 Summer 审阅 现在你知道了项目中依赖了哪些扩展包,去搞明白它们各自的功能。我推荐从 Laravel 依赖开始,因为它们拥有详细的文档。且文档就发布在网络上,很容易就能找到:https://laravel-china.org/docs/laravel/{VERSION} 和 https://laravel.com/api/{VERSION},如下这种链接
laravel-china.org/docs/larave… 和 laravel.com/api/5.6。 文档 docs 对 laravel 功能及各个主要部分的工作原理作了比较全面的介绍。同时
api 文档将 laravel 框架中所用到的类及方法以清单的形式呈现出来。
在查看了 Laravel 文档之后,可以继续查看其它依赖的文档。你可以前往 Packagist (这是 Composer 所使用的扩展包仓库)获取关于依赖的更多信息,各扩展对应的地址为https://packagist.org/packages/{VENDOR}/{PACKAGE},比如 packagist.org/packages/fi…。

在每一个 Packagist 的项目主页上,展示了扩展包的介绍、版本号、仓库地址(如 GitHub)、完整的 readme 文件,以及其他一些有用的信息。从项目主页上获得的信息足够使你了解这个扩展包是什么,在你的项目中又承担哪部分功能。通过这种方式,继续去了解你项目应用的 composer.json 文件中所罗列出的其他依赖。

zyxcba 翻译于 3天前 0 重译 由 Summer 审阅
路由
–

路由是应用某个具体功能的入口。路由表现为一个链接,浏览器访问链接时,最终由绑定的控制器或闭包来处理。由路由找到具体对应的控制器,就能清楚控制器所依赖的其他模块以及实现的具体功能。遇到新的路由,继续重复这一动作,就能逐步搞清楚整个应用是怎么工作的。
你可以在项目的如下位置找到路由配置文件:

  • Laravel 5.3+ routes/*.php

  • Laravel 5.0-5.2 app/Http/routes.php

  • Laravel 4.2 app/routes.php

    zyxcba 翻译于 3天前 0 重译 由 Summer 审阅

    路由 “陷阱”

某些时候,根据具体 URL 定位路由需要费些脑子。

比如 URI /users/123/profile。你可能想要去搜索 users/{id}/profile 的路由定义。事实上,它是定义在 路由分组 中,这使得路由比较难定位。

1
2
3
复制代码Route::prefix('users')->group(function () {
Route::get('{id}/profile', 'UsersController@profile');
});

在这个例子中,users 和 {id}/profile 并没有被写在一起,这是难以定位的原因。如果路由不多,还能比较轻易的找出。但是,当路由文件有成百上千条定义时,这将会变得非常困难。
另外一个坑是 Route::resource() (还有新版本中的 Route::apiResource())。

zyxcba 翻译于 3天前 0 重译 由 Summer 审阅 Route::resource() 将自动根据指定参数生成路由。举个例子,在路由文件中添加代码 Route::resource('dogs', 'DogController'); 将完成与下述代码相同的功能。

1
2
3
4
5
6
7
8
9
复制代码Route::group(['prefix' => 'dogs'], function () {
Route::get('/', 'DogsController@index')->name('dogs.index');
Route::get('create', 'DogsController@create')->name('dogs.create');
Route::post('/', 'DogsController@store')->name('dogs.store');
Route::get('{id}', 'DogsController@show')->name('dogs.show');
Route::get('{id}/edit', 'DogsController@edit')->name('dogs.edit');
Route::put('{id}', 'DogsController@update')->name('dogs.update');
Route::delete('{id}', 'DogsController@destroy')->name('dogs.destroy');
});

然而,如果你尝试查找类似 dogs/{id}/edit 的内容,这是找不到的,因为它的定义是作为 Route::resource() 的其中一部分。 ckmessi 翻译于 3天前 0 重译 由 Summer 审阅 有时通过 Route::resource() 方式直接定义路由是挺方便的,但我更倾向于单独地定义每一个路由,这样能使每个 URI 更容易被直接搜索到。了解更多路由资源和资源控制器的相关信息,可以查阅这些 文档 。 预览项目中的所有路由的最简单方式是使用 artisan 命令 route:list :

1
复制代码php artisan route:list

route:list 命令提供了每个路由的完整细节,包括 HTTP 请求方式,具体的 URI ,路由名称,动作信息(也就是控制器及其方法),以及为每个路由配置的中间件信息。

ckmessi 翻译于 3天前 0 重译 由 Summer 审阅
服务提供者


服务提供者是 Laravel 释放魔法之地。 官方文档 给出了总结:

服务提供者是所有 Laravel 应用程序引导中心。你的应用程序以及 Laravel 的所有核心服务都是通过服务提供器进行引导。

在这里,我们说的「引导」其实是指注册,比如注册服务容器绑定、事件监听器、中间件,甚至是路由的注册。服务提供者是配置你应用程序的中心。

你可以浏览位于 app/providers 目录下的所有应用程序服务提供者。围绕应用自定义增加的相关代码,理应在这里。例如,一些情况下要查找视图合成器,宏,并做配置调整。
在旧版本的 Laravel 中,如 4.2,你会在 global.php 文件中发现类似的功能,因为那时服务提供者通常只在包中使用。

3en 翻译于 3天前 0 重译 由 Summer 审阅
测试
–

代码库包含的测试套件能向你展示应用程序如何工作以及接下来的响应。对应用的边界处理情况,它可以提供有价值的线索。当然,就像代码库文档一样,应用配套的测试文件有可能不存在,或者很少,甚至是无用的过时文件。

同写项目文档一样,写应用配套测试同样可以更好的学习项目应用,提升代码质量。你可能偶然发现并修复一些缺陷,移除无用的代码,或者为项目中重要的类新增测试覆盖。

3en 翻译于 3天前 0 重译 由 Summer 审阅
利器
–

对 Laravel 开发者而言,Barry vd. Heuvel 发布的 Laravel Debugbar 是值得拥有的调试和追溯工具。它功能强大,安装便易。可以将应用程序中所发生的事情一览无余:经过的路由和控制器,数据库查询和执行时间,数据展示,异常,查看执行内容和执行过程时间线等等。尝试过使用这个包后,你将在之后的 Laravel 应用开发中对它爱不释手。
尾声
–

在这篇文章中,我提出了一些方法,方便你很快上手新的 Laravel 项目代码。这篇文章并非一份包含所有细节的清单,只是一个起步。我鼓励你使用这些建议,看看它能把你带到哪里。如果您有任何交流的想法,我很乐意听到它们!欢迎随时联系 Twitter。 3en 翻译于 3天前 0 重译 由 Summer 审阅

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

当Java遇上C++:使用JNA传递复杂数据结构

发表于 2018-03-21


最近在UMStor的开发过程中,需要写一个C/C++库的Java SDK。试想,如果用Java完完全全重新写一个对应的SDK,不免工作量太大,于是我搜了一下,是否有可能让Java访问C/C++库中的接口(.dll, .so)。
JNI

JNI(Java Native Interface)是一种技术,通过这种技术可以做到以下两点:

– Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。– Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。

我们都知道承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体平台上,所以虚拟机本身无法做到平台无关。然而,有了JNI技术,就可以对Java层屏蔽具体的虚拟机实现上的差异了。这样,就能实现Java本身的平台无关特性。其实Java一直在使用JNI技术,只是我们平时较少用到罢了。

JNI的使用并不简单,如果已有一个编译好的.dll/.so文件,如果使用JNI技术调用,我们首先需要使用C语言另外写一个.dll/.so共享库,使用SUN规定的数据结构替代C语言的数据结构,调用已有的 dll/so中公布的函 数。然后再在Java中载入这个库dll/so,最后编写Java native函数作为链接库中函数的代理。经过这些繁琐的步骤才能在Java中调用本地代码。

JNA

JNA(Java Native Access)是建立在JNI技术基础之上的一个Java类库,它使我们可以方便地使用java直接访问动态链接库中的函数。我们不需要重写我们的动态链接库文件,而是有直接调用的API,大大简化了我们的工作量。但是JNA一般只适用于较为简单的C/C++库,如果接口、数据结构复杂的话就不推荐。而且JNA也只提供了C/C++对Java的接口转化。

SWIG

SWIG(Simplified Wrapper and Interface Generator),是一款开源软件,其目的是将C/C++编写的函数库封装成其他语言的接口,包括:Java, Python, Perl, Ruby, C#, PHP等诸多主流编程语言。SWIG底层仍然还是JNI,如果我们只是要访问简单的C/C++接口,那么JNA更合适;但是如果接口较为复杂,那SWIG就是最佳方案。

我所要面对的C/C++库的接口比较复杂,用到了不少自定义的结构体,按理说我应该使用SWIG才对,不过由于一些原因,我还是使用了JNA来开发这个Java SDK,算是给自己挖了一个不小的坑。

本文会着重介绍如何用JNA传递复杂的数据结构,因此JNA的入门教程在这里不再赘述,大家可以自行百度,应该一搜一大把。

基本类型对照表

下表是JNA官网 给出的基本类型的C语言和Java类型对照表:


这张对照表一般已经能够满足跨平台、跨语言调用的数据类型转换需求,因为如果我们要做跨语言调用,应当尽量使用基本和简单的数据类型,而不要过多使用复杂结构体传递数据,因为C语言的结构体中,每个成员会执行对齐操作与前一个成员保持字节对齐,也就是说,成员的书写顺序会影响结构体占用的空间大小,因此会在Java端定义结构体时会造成不小的麻烦。

比如,如果跨语言调用的函数中参数包含stat这个结构体:我们都知道,stat这个结构体是用来描述linux文件系统的文件元数据的基本结构,但麻烦的是,这个结构体的成员定义次序在不同的机型上并不相同,如果我们在Java端重写这个结构体,会产生兼容性问题。

结构体定义

有时候我们需要在Java端访问某个C/C++结构体中的成员,我们就需要在Java端复写这个结构体,在复写的时候需要注意两点:

  • 需要在结构体定义中定义2个内部类ByReference和ByValue,来实现指针类型接口和值类型接口
  • 重写getFieldOrder()来告诉C/C++的成员取值次序

下面我们通过一个栗子来看一下在Java只不过怎么模拟定义一个C/C++结构体:

C/C++代码

1
2
3
4
5
6
7
8
9
复制代码typedef struct A {
B* b;
void* args;
int len;
};

typedef struct B {
int obj_type;
};

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码/* 结构体A定义 */ public static A extends Structure {

//构造函数定义 public A() {
super();
}

public A(Pointer _a) {
super(_a);
}

//结构体成员定义 public B.ByReference b;
PointerByReference args;
int len;

//添加2个内部类,分别实现指针类型接口、值类型接口 public static class ByReference extends A implements Structure.ByReference {}
public static class ByValue extends A implements Structure.ByValue{}

//定义取值次序,需要与C/C++中对齐,不然会出现NoSuchFieldError @Override protected List getFieldOrder() {
return Arrays.asList(new String[]{"b", "args", "len"});
}
}

/* 结构体B定义 */ public static B extends Structure {
public B() {
super();
}

public B(Pointer _b) {
super(_b);
}

int obj_type;

public static class ByReference extends B implements Structure.ByReference {}
public static class ByValue extends B implements Structure.ByValue{}

@Override protected List getFieldOrder() {
return Arrays.asList(new String[]{"obj_type"});
}
}

结构体传递

如果需要在Java端访问某个结构体的成员,需要使用ByReference(指针、引用)或是ByValue(拷贝参数);如果只是起到数据传递,不关心具体内部结构,可以使用PointerByReference和Pointer。

test_myFun(struct A a)

1
复制代码test_myFun(A.ByValue a)

test_myFun(struct A *a)

1
复制代码test_myFun(A.ByReference a)

test_myFun(struct A **a)

1
复制代码test_myFun(A.ByReference[] a)

test_myFun(A a)

1
复制代码test_myFun(Pointer a)

test_myFun(A *a)

1
复制代码test_myFun(PointerByReference a)

test_myFun(A **a)

1
复制代码test_myFun(PointerByReference[] a)

回调函数

我们有时候会在C/C++中顶一个一个带回调函数的函数,例如:

1
2
3
复制代码typedef bool (*test_cb)(const char *name);

int test_myFun(test_cb cb, const char *name, uint32_t flag);

如果我们需要在Java端调用test_myFun函数,则我们需要在Java端定义一个与test\_cb相同的回调接口:

1
2
3
复制代码public interface testCallback extends Callback { 
//invoke对应test_cb,注意参数顺序需要保持一致 boolean invoke(String name);
}

定义该回调接口的实现:

1
2
3
4
5
6
复制代码public class testCallbackImpl implements testCallback {
@Override public int invoke(String name) {
System.out.printf("Invoke Callback " + name + " successfully!");
return true;
}
}

test_myFun函数Java端定义:

1
复制代码int testMyFun(Callback cb, String name, int flag);

调用实例:

1
复制代码int rc = testMyFun(new testCallbackImpl(), "helloworld", 1);

总结

其实我们可以看到,JNA使用的主要难点在于结构体定义和传递,只要弄清楚如何对付结构体,剩下的事情也就水到渠成了。说了这么多,其实就想告诉大家,在做跨语言调用时,尽量还是要封装一下函数、结构体,让数据传递时更为简单。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

高效传输数据格式以及基于HTTP2的RPC框架---gRPC

发表于 2018-03-21

ProtoBuffer的介绍

google有一款非常高效的数据传输格式框架ProtoBuffer。在java中使用protobuffer作为序列化效率比jdk自身的serializable接口效率高的多(github上有个对于序列号性能的研究github.com/eishay/jvm-…),这在缓存的时候效率非常高。当然,如此优秀的数据格式框架并不是仅仅使用在缓存上的,既然压缩(姑且将其简单理解为压缩算法吧)如此高效,那么使用在网络IO传输中比JSON或许XML而言效率也为提升很多吧。

gRPC的介绍

gRPC是google开发的一款RPC框架,RPC Server与RPC Clinet之间的数据传输就是刚刚提到的ProtoBuffer,并且该RPC框架还是基于HTTP2的。因此,HTTP2的多路复用,基于流的传输在gRPC上也有相应的实现。

ProtoBuffer格式的定义

PrototBuffer的官方文档:developers.google.com/protocol-bu…
笔者贴出在实际使用中的格式定义文件.proto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
abnf复制代码syntax = "proto3";

package vsig;

service VSIGProto {
rpc setAcl (ACLRequest) returns (Reply) {}
rpc openNtp (NTPConfig) returns (Reply) {}
}
message ACLRequest {
string extranetIp = 1;
int32 devType = 2;
string intranetIp = 3;
}
message InterfaceInfoRequest {
string name =1;
string ip = 2;
string mask=3;
string gateway=4;
}
message NTPInfoRequest{
bool state = 1;
string ntpServerIp =2;
int32 ntpServerPort =3;
}
message NTPConfig{
NTPInfoRequest ntpInfo = 1;
InterfaceInfoRequest br0Info = 2;
}
message Reply {
string message = 1;
}

.proto文件中主要定义了三部分东西:

  • RPC的方法名以及接受的参数和返回的参数
  • RPC方法接受参数的格式
  • RPC方法返回的格式

一个方法仅能接受一个参数,因为笔者定义NTPConfig里面又包含了两个对象,这样保证了openNtp方法仅接收了一个对象

对于定义的message,每个值都有一个唯一的number类型的数字,根据官方文档的解释:它是用于以消息二进制格式标识字段,并且在使用过程中不能随便更改,否则会导致数据无法还原。同时,如果数字定义为115则使用一个字节来存储,而162047需要使用两个字节来存储。

gRPC Server的实现

定义好.proto之后就可以使用该文件来使用grpc客户端与服务器端了,gRPC的客户端与服务器端必须使用同一个.proto文件

gRPC支持众多常见的编程语言,笔者使用java与node两种语言实现gRPC。

NodeJS gRPC Server实现

package.json:

1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"name": "grpc-examples",
"version": "0.1.0",
"dependencies": {
"async": "^1.5.2",
"google-protobuf": "^3.0.0",
"grpc": "^1.0.0",
"lodash": "^4.6.1",
"minimist": "^1.2.0"
}
}

gRPC服务器的编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
php复制代码//上述定义的.proto的路径
const PROTO_PATH = '../../vsig.proto';

const grpc = require( 'grpc' );
//最后vsig是.proto中的package
const proto = grpc.load( PROTO_PATH ).vsig;
//定义rpc Server的ip与端口
const rpcHost = '127.0.0.1';
const rpcPort = 50051;

//定义方法的映射,因为方法最终是在该类中实现的,因此定义改类与.proto中的方法的映射。左边为.proto中的方法名,右边为实现
const methodCover = {
setAcl: setAcl,
openNtp: openNTP
};

function setAcl( call, callback ) {
//call.request即为该方法在.proto中定义的参数接收的message对象
console.log( call.request);
//该回调即为对客户端的方法,参数1是error,参数二与.proto中方法的返回值对应
callback( null, {
message: "rpc call setAcl method success"
} )
}

function openNTP( call, callback ) {
const ntpInfo = call.request;
console.log(ntpInfo);
callback(null,{
message:"rpc call openNTP call success"
})
}
function main() {
var server = new grpc.Server();
//VSIGProto即为.proto中的server的名称,参数二为方法映射
server.addProtoService( proto.VSIGProto.service, methodCover );
const grpcIn = grpc.ServerCredentials.createInsecure();
//绑定端口
server.bind( rpcHost + ":" + rpcPort, grpcIn );
//启动
server.start();
}

main();

Java gRPC Server的实现

gRPC Client的实现

如上定义好服务端之后,将监听指定的端口,客户端只需要对改端口发送请求即可。

Node gRPC Client的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
reasonml复制代码const PROTO_PATH = '../vsig.proto';

const grpc = require( 'grpc' );
//最后vsig是.proto中的package
const proto = grpc.load( PROTO_PATH ).vsig;
const rpcHost = '127.0.0.1';
const rpcPort = 50051;
//VSIGProto是.proto中service中的VSIGProto
const client = new proto.VSIGProto( rpcHost + ":" + rpcPort, grpc.credentials.createInsecure() );

class RpcClient {
setAcl( acl, cb ) {
//执行rpc调用
client.setAcl( acl, function( err, response ) {
cb( err, response )
} );
}
//封装RPC的方法
openNTP( ntpconfig, cb ) {
//执行rpc调用
client.openNtp( ntpconfig, function( err, response ) {
cb( err, response );
} );
}
}
const rpcClient = new RpcClient();
module.exports = rpcClient;

Java gRPC Client的实现

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Shiro入门这篇就够了【Shiro的基础知识、回顾URL拦

发表于 2018-03-21

前言

本文主要讲解的知识点有以下:

  • 权限管理的基础知识
    • 模型
    • 粗粒度和细粒度的概念
  • 回顾URL拦截的实现
  • Shiro的介绍与简单入门

一、Shiro基础知识

在学习Shiro这个框架之前,首先我们要先了解Shiro需要的基础知识:权限管理

1.1什么是权限管理?

只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

对权限的管理又分为两大类别:

  • 用户认证
  • 用户授权

1.1.1用户认证

用户认证,用户去访问系统,系统要验证用户身份的合法性

最常用的用户身份验证的方法:1、用户名密码方式、2、指纹打卡机、3、基于证书验证方法。。系统验证用户身份合法,用户方可访问系统的资源。

举个例子:

  • 当我们输入了自己的淘宝的账户和密码,才能打开购物车

用户认证的流程:

  • 判断该资源能否不认证就能访问【登陆页面、首页】
  • 如果该资源需要认证后才能访问,那么判断该访问者是否认证了
  • 如果还没有认证,那么需要返回到【登陆页面】进行认证
  • 认证通过后才能访问资源

这里写图片描述

从用户认证我们可以抽取出这么几个概念

  • subject主体:理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证
  • principal身份信息:通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)【我们可以选择身份证认证、学生证认证等等都是我们的身份信息】
  • credential凭证信息:可以是密码 、证书、指纹。

总结:主体在进行身份认证时需要提供身份信息和凭证信息。

1.1.2用户授权

用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。

用户授权的流程

  • 到达了用户授权环节,当然是需要用户认证之后了
  • 用户访问资源,系统判断该用户是否有权限去操作该资源
  • 如果该用户有权限才能够访问,如果没有权限就不能访问了

这里写图片描述

授权的过程可以简单理解为:主体认证之后,系统进行访问控制

subject必须具备资源的访问权限才可访问该资源..

权限/许可(permission) :针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除

资源可以分为两种

  • 资源类型:系统的用户信息就是资源类型,相当于java类。
  • 资源实例:系统中id为001的用户就是资源实例,相当于new的java对象。

1.2权限管理模型

一般地,我们可以抽取出这么几个模型:

  • 主体(账号、密码)
  • 资源(资源名称、访问地址)
  • 权限(权限名称、资源id)
  • 角色(角色名称)
  • 角色和权限关系(角色id、权限id)
  • 主体和角色关系(主体id、角色id)

这里写图片描述

通常企业开发中将资源和权限表合并为一张权限表,如下:

  • 资源(资源名称、访问地址)
  • 权限(权限名称、资源id)

合并为:

  • 权限(权限名称、资源名称、资源访问地址)

这里写图片描述

1.3分配权限

用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可。

通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中。把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)

1.3.1基于角色访问控制

RBAC(role based access control),基于角色的访问控制。

1
2
3
4
5
6
复制代码
//如果该user是部门经理则可以访问if中的代码
if(user.hasRole('部门经理')){
//系统资源内容
//用户报表查看
}

角色针对人划分的,人作为用户在系统中属于活动内容,如果该 角色可以访问的资源出现变更,需要修改你的代码了,

1
2
3
4
5
复制代码
if(user.hasRole('部门经理') || user.hasRole('总经理') ){
//系统资源内容
//用户报表查看
}

基于角色的访问控制是不利于系统维护(可扩展性不强)。

1.3.2基于资源的访问控制

RBAC(Resource based access control),基于资源的访问控制。

资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。

1
2
3
4
5
6
7
复制代码
对资源的访问需要具有permission权限,代码可以写为:

if(user.hasPermission ('用户报表查看(权限标识符)')){
//系统资源内容
//用户报表查看
}

建议使用基于资源的访问控制实现权限管理。


二、 粗粒度和细粒度权限

细粒度权限管理:对资源实例的权限管理。资源实例就资源类型的具体化,比如:用户id为001的修改连接,1110班的用户信息、行政部的员工。细粒度权限管理就是数据级别的权限管理。

粗粒度权限管理比如:超级管理员可以访问户添加页面、用户信息等全部页面。部门管理员可以访问用户信息页面包括 页面中所有按钮。

粗粒度和细粒度例子:

1
2
3
4
5
6
7
8
9
10
复制代码
系统有一个用户列表查询页面,对用户列表查询分权限,

如果粗颗粒管理,张三和李四都有用户列表查询的权限,张三和李四都可以访问用户列表查询。

进一步进行细颗粒管理,张三(行政部)和李四(开发部)只可以查询自己本部门的用户信息。

张三只能查看行政部 的用户信息,李四只能查看开发部门的用户信息。

细粒度权限管理就是数据级别的权限管理。

2.1如何实现粗粒度权限管理?

粗粒度权限管理比较容易将权限管理的代码抽取出来在系统架构级别统一处理。比如:通过springmvc的拦截器实现授权。

对细粒度权限管理在数据级别是没有共性可言,针对细粒度权限管理就是系统业务逻辑的一部分,在业务层去处理相对比较简单

比如:部门经理只查询本部门员工信息,在service接口提供一个部门id的参数,controller中根据当前用户的信息得到该 用户属于哪个部门,调用service时将部门id传入service,实现该用户只查询本部门的员工。

2.1.1基于URL拦截

基于url拦截的方式实现在实际开发中比较常用的一种方式。

对于web系统,通过filter过虑器实现url拦截,也可以springmvc的拦截器实现基于url的拦截。

2.2.2使用权限管理框架实现

对于粗粒度权限管理,建议使用优秀权限管理框架来实现,节省开发成功,提高开发效率。

shiro就是一个优秀权限管理框架。

三、回顾URL拦截

我们在学习的路途上也是使用过几次URL对权限进行拦截的

当时我们做了权限的增删该查的管理系统,但是在权限表中是没有把资源添加进去,我们使用的是Map集合来进行替代的。
blog.csdn.net/hon_3y/arti…

随后,我们学习了动态代理和注解,我们也做了一个基于注解的拦截

  • 在Controller得到service对象的时候,service工厂返回的是一个动态代理对象回去
  • Controller拿着代理对象去调用方法,代理对象就会去解析该方法上是否有注解
  • 如果有注解,那么就需要我们进行判断该主体是否认证了,如果认证了就判断该主体是否有权限
  • 当我们解析出该主体的权限和我们注解的权限是一致的时候,才放行!

blog.csdn.net/hon_3y/arti…

流程:

这里写图片描述

3.1认证的JavaBean

我们之前认证都是放在默认的Javabean对象上的,现在既然我们准备学Shiro了,我们就得专业一点,弄一个专门存储认证信息的JavaBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
复制代码

/**
* 用户身份信息,存入session 由于tomcat将session会序列化在本地硬盘上,所以使用Serializable接口
*
* @author Thinkpad
*
*/
public class ActiveUser implements java.io.Serializable {
private String userid;//用户id(主键)
private String usercode;// 用户账号
private String username;// 用户名称

private List<SysPermission> menus;// 菜单
private List<SysPermission> permissions;// 权限

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}


public String getUsercode() {
return usercode;
}

public void setUsercode(String usercode) {
this.usercode = usercode;
}

public String getUserid() {
return userid;
}

public void setUserid(String userid) {
this.userid = userid;
}

public List<SysPermission> getMenus() {
return menus;
}

public void setMenus(List<SysPermission> menus) {
this.menus = menus;
}

public List<SysPermission> getPermissions() {
return permissions;
}

public void setPermissions(List<SysPermission> permissions) {
this.permissions = permissions;
}


}

认证的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码
@Override
public ActiveUser authenticat(String userCode, String password)
throws Exception {
/**
认证过程:
根据用户身份(账号)查询数据库,如果查询不到用户不存在
对输入的密码 和数据库密码 进行比对,如果一致,认证通过
*/
//根据用户账号查询数据库
SysUser sysUser = this.findSysUserByUserCode(userCode);

if(sysUser == null){
//抛出异常
throw new CustomException("用户账号不存在");
}

//数据库密码 (md5密码 )
String password_db = sysUser.getPassword();

//对输入的密码 和数据库密码 进行比对,如果一致,认证通过
//对页面输入的密码 进行md5加密
String password_input_md5 = new MD5().getMD5ofStr(password);
if(!password_input_md5.equalsIgnoreCase(password_db)){
//抛出异常
throw new CustomException("用户名或密码 错误");
}
//得到用户id
String userid = sysUser.getId();
//根据用户id查询菜单
List<SysPermission> menus =this.findMenuListByUserId(userid);

//根据用户id查询权限url
List<SysPermission> permissions = this.findPermissionListByUserId(userid);

//认证通过,返回用户身份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(sysUser.getId());
activeUser.setUsercode(userCode);
activeUser.setUsername(sysUser.getUsername());//用户名称

//放入权限范围的菜单和url
activeUser.setMenus(menus);
activeUser.setPermissions(permissions);

return activeUser;
}

Controller处理认证,如果身份认证成功,那么把认证信息存储在Session中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码	@RequestMapping("/login")
public String login(HttpSession session, String randomcode,String usercode,String password)throws Exception{
//校验验证码,防止恶性攻击
//从session获取正确验证码
String validateCode = (String) session.getAttribute("validateCode");

//输入的验证和session中的验证进行对比
if(!randomcode.equals(validateCode)){
//抛出异常
throw new CustomException("验证码输入错误");
}

//调用service校验用户账号和密码的正确性
ActiveUser activeUser = sysService.authenticat(usercode, password);

//如果service校验通过,将用户身份记录到session
session.setAttribute("activeUser", activeUser);
//重定向到商品查询页面
return "redirect:/first.action";
}

身份认证拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码

//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {

//得到请求的url
String url = request.getRequestURI();
//判断是否是公开 地址
//实际开发中需要公开 地址配置在配置文件中
//从配置中取逆名访问url
List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
//遍历公开 地址,如果是公开 地址则放行
for(String open_url:open_urls){
if(url.indexOf(open_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//判断用户身份在session中是否存在
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
//如果用户身份在session中存在放行
if(activeUser!=null){
return true;
}
//执行到这里拦截,跳转到登陆页面,用户进行身份认证
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);

//如果返回false表示拦截不继续执行handler,如果返回true表示放行
return false;
}

授权拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
复制代码
//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {

//得到请求的url
String url = request.getRequestURI();
//判断是否是公开 地址
//实际开发中需要公开 地址配置在配置文件中
//从配置中取逆名访问url

List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
//遍历公开 地址,如果是公开 地址则放行
for(String open_url:open_urls){
if(url.indexOf(open_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//从配置文件中获取公共访问地址
List<String> common_urls = ResourcesUtil.gekeyList("commonURL");
//遍历公用 地址,如果是公用 地址则放行
for(String common_url:common_urls){
if(url.indexOf(common_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//获取session
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
//从session中取权限范围的url
List<SysPermission> permissions = activeUser.getPermissions();
for(SysPermission sysPermission:permissions){
//权限的url
String permission_url = sysPermission.getUrl();
if(url.indexOf(permission_url)>=0){
//如果是权限的url 地址则放行
return true;
}
}

//执行到这里拦截,跳转到无权访问的提示页面
request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);

//如果返回false表示拦截不继续执行handler,如果返回true表示放行
return false;
}

拦截器配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码

<!--拦截器 -->
<mvc:interceptors>

<mvc:interceptor>
<!-- 用户认证拦截 -->
<mvc:mapping path="/**" />
<bean class="cn.itcast.ssm.controller.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
<mvc:interceptor>
<!-- 授权拦截 -->
<mvc:mapping path="/**" />
<bean class="cn.itcast.ssm.controller.interceptor.PermissionInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>

四、什么是Shiro

shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。

spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。

Shiro架构:

这里写图片描述

  • subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
  • securityManager:安全管理器,主体进行认证和授权都 是通过securityManager进行。
  • authenticator:认证器,主体进行认证最终通过authenticator进行的。
  • authorizer:授权器,主体进行授权最终通过authorizer进行的。
  • sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
  • SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
  • cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
  • realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。

cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

  • 比如md5散列算法。

五、为什么使用Shiro

我们在使用URL拦截的时候,要将所有的URL都配置起来,繁琐、不易维护

而我们的Shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。

六、Shiro认证

6.1导入jar包

我们使用的是Maven的坐标就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.3</version>
</dependency>

当然了,我们也可以把Shiro相关的jar包全部导入进去

1
2
3
4
5
6
复制代码
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.2.3</version>
</dependency>

6.2Shiro认证流程

这里写图片描述

6.2.1通过配置文件创建工厂

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
复制代码
// 用户登陆和退出
@Test
public void testLoginAndLogout() {

// 创建securityManager工厂,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-first.ini");

// 创建SecurityManager
SecurityManager securityManager = factory.getInstance();

// 将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);

// 从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();

// 在认证提交前准备token(令牌)
// 这里的账号和密码 将来是由用户输入进去
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"111111");
try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();

System.out.println("是否认证通过:" + isAuthenticated);

// 退出操作
subject.logout();

// 是否认证通过
isAuthenticated = subject.isAuthenticated();

System.out.println("是否认证通过:" + isAuthenticated);

}

这里写图片描述

6.3小结

ModularRealmAuthenticator作用进行认证,需要调用realm查询用户信息(在数据库中存在用户信息)
ModularRealmAuthenticator进行密码对比(认证过程)。
realm:需要根据token中的身份信息去查询数据库(入门程序使用ini配置文件),如果查到用户返回认证信息,如果查询不到返回null。

6.4自定义realm

从第一个认证程序我们可以看见,我们所说的流程,是认证器去找realm去查询我们相对应的数据。而默认的realm是直接去与配置文件来比对的,一般地,我们在开发中都是让realm去数据库中比对。
因此,我们需要自定义realm

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码
public class CustomRealm extends AuthorizingRealm {

// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}

// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {

// token是用户输入的
// 第一步从token中取出身份信息
String userCode = (String) token.getPrincipal();

// 第二步:根据用户输入的userCode从数据库查询
// ....


// 如果查询不到返回null
//数据库中用户账号是zhangsansan
/*if(!userCode.equals("zhangsansan")){//
return null;
}*/


// 模拟从数据库查询到密码
String password = "111112";

// 如果查询到返回认证信息AuthenticationInfo

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, this.getName());

return simpleAuthenticationInfo;
}

// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}

}

6.5配置realm

需要在shiro-realm.ini配置realm注入到securityManager中。

这里写图片描述

6.6测试自定义realm

同上边的入门程序,需要更改ini配置文件路径:

1
2
3
4
复制代码
同上边的入门程序,需要更改ini配置文件路径:
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-realm.ini");

6.7散列算法

我们如果知道md5,我们就会知道md5是不可逆的,但是如果设置了一些安全性比较低的密码:111111…即时是不可逆的,但还是可以通过暴力算法来得到md5对应的明文…

建议对md5进行散列时加salt(盐),进行加密相当 于对原始密码+盐进行散列。\

正常使用时散列方法:

  • 在程序中对原始密码+盐进行散列,将散列值存储到数据库中,并且还要将盐也要存储在数据库中。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码
public class MD5Test {

public static void main(String[] args) {

//原始 密码
String source = "111111";
//盐
String salt = "qwerty";
//散列次数
int hashIterations = 2;
//上边散列1次:f3694f162729b7d0254c6e40260bf15c
//上边散列2次:36f2dfa24d0a9fa97276abbe13e596fc


//构造方法中:
//第一个参数:明文,原始密码
//第二个参数:盐,通过使用随机数
//第三个参数:散列的次数,比如散列两次,相当 于md5(md5(''))
Md5Hash md5Hash = new Md5Hash(source, salt, hashIterations);

String password_md5 = md5Hash.toString();
System.out.println(password_md5);
//第一个参数:散列算法
SimpleHash simpleHash = new SimpleHash("md5", source, salt, hashIterations);
System.out.println(simpleHash.toString());
}

}

6.8自定义realm支持md5

自定义realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码
public class CustomRealmMd5 extends AuthorizingRealm {

// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealmMd5");
}

// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {

// token是用户输入的
// 第一步从token中取出身份信息
String userCode = (String) token.getPrincipal();

// 第二步:根据用户输入的userCode从数据库查询
// ....

// 如果查询不到返回null
// 数据库中用户账号是zhangsansan
/*
* if(!userCode.equals("zhangsansan")){// return null; }
*/

// 模拟从数据库查询到密码,散列值
String password = "f3694f162729b7d0254c6e40260bf15c";
// 从数据库获取salt
String salt = "qwerty";
//上边散列值和盐对应的明文:111111

// 如果查询到返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, ByteSource.Util.bytes(salt), this.getName());

return simpleAuthenticationInfo;
}

// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}

}

配置文件:

这里写图片描述

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码
// 自定义realm实现散列值匹配
@Test
public void testCustomRealmMd5() {

// 创建securityManager工厂,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-realm-md5.ini");

// 创建SecurityManager
SecurityManager securityManager = factory.getInstance();

// 将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);

// 从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();

// 在认证提交前准备token(令牌)
// 这里的账号和密码 将来是由用户输入进去
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"222222");

try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();

System.out.println("是否认证通过:" + isAuthenticated);

}

七、总结

  • 用户认证和用户授权是Shiro的基础,用户认证其实上就是登陆操作、用户授权实际上就是对资源拦截的操作。
  • 权限管理的模型一般我们都将资源放在权限表中进行管理起来。
  • 我们可以基于角色拦截,也可以基于资源拦截。要是基于角色拦截的话,那么如果角色的权限发生变化了,那就需要修改代码了**。推荐使用基于资源进行拦截**
  • 这次URL拦截,我们使用一个JavaBean来封装所有的认证信息。当用户登陆了之后,我们就把用户对菜单栏的访问、对资源的访问权限都封装到该JavaBean中
  • 当使用拦截器进行用户认证的时候,我们只要判断Session域有没有JavaBen对象即可了。
  • 当时候拦截器进行用户授权的时候,我们要判断JavaBean中的权限是否能够访问该资源。
  • 以前URL拦截的方式需要把所有的URL都在数据库进行管理。非常麻烦,不易维护。
  • 我们希望Shiro去认证的时候是通过realm去数据库查询数据的。而我们reaml默认是查询配置文件的数据的。
  • 因此,我们需要自定义reaml,使得它是去数据库查询数据。只要继承AuthorizingRealm类就行了。
  • 当然了,自定义后的reaml也需要在配置文件中写上我们的自定义reaml的位置的。
  • 散列算法就是为了让密码不被别人给破解。我们可对原始的密码加盐再进行散列,这就加大了破解的难度了。
  • 自定义的reaml也是支持散列算法的,相同的,还是需要我们在配置文件中配置一下就好了。

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

递归就这么简单 递归介绍

发表于 2018-03-20

递归介绍

本来预算此章节是继续写快速排序的,然而编写快速排序往往是递归来写的,并且递归可能不是那么好理解,于是就有了这篇文章。

在上面提到了递归这么一个词,递归在程序语言中简单的理解是:方法自己调用自己

递归其实和循环是非常像的,循环都可以改写成递归,递归未必能改写成循环,这是一个充分不必要的条件。

  • 那么,有了循环,为什么还要用递归呢??在某些情况下(费波纳切数列,汉诺塔),使用递归会比循环简单很多很多
  • 话说多了也无益,让我们来感受一下递归吧。

我们初学编程的时候肯定会做过类似的练习:

  • 1+2+3+4+....+100(n)求和
  • 给出一个数组,求该数组内部的最大值

我们要记住的是,想要用递归必须知道两个条件:

  • 递归出口(终止递归的条件)
  • 递归表达式(规律)

技巧:在递归中常常是将问题切割成两个部分(1和整体的思想),这能够让我们快速找到递归表达式(规律)

一、求和

如果我们使用for循环来进行求和1+2+3+4+....+100,那是很简单的:

1
2
3
4
5
6
7
8
复制代码
int sum = 0;
for (int i = 1; i <= 100; i++) {

sum = sum + i;

}
System.out.println("公众号:Java3y:" + sum);

前面我说了,for循环都可以使用递归来进行改写,而使用递归必须要知道两个条件:1、递归出口,2、递归表达式(规律)

首先,我们来找出它的规律:1+2+3+...+n,这是一个求和的运算,那么我们可以假设X=1+2+3+...+n,可以将1+2+3+...+(n-1)看成是一个整体。而这个整体做的事又和我们的**初始目的(求和)**相同。以我们的高中数学知识我们又可以将上面的式子看成X=sum(n-1)+n

好了,我们找到我们的递归表达式(规律),它就是sum(n-1)+n,那递归出口呢,这个题目的递归出口就有很多了,我列举一下:

  • 如果n=1时,那么就返回1
  • 如果n=2时,那么就返回3(1+2)
  • 如果n=3时,那么就返回6(1+2+3)

当然了,我肯定是使用一个最简单的递归出口了:if(n=1) return 1

递归表达式和递归出口我们都找到了,下面就代码演示:

递归出口为1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
public static void main(String[] args) {
System.out.println("公众号:Java3y:" + sum(100));
}

/**
*
* @param n 要加到的数字,比如题目的100
* @return
*/
public static int sum(int n) {

if (n == 1) {
return 1;
} else {
return sum(n - 1) + n;
}
}

递归出口为4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码
public static void main(String[] args) {
System.out.println("公众号:Java3y:" + sum(100));
}

/**
*
* @param n 要加到的数字,比如题目的100
* @return
*/
public static int sum(int n) {

//如果递归出口为4,(1+2+3+4)
if (n == 4) {
return 10;
} else {
return sum(n - 1) + n;
}
}

结果都是一样的。

二、数组内部的最大值

如果使用的是循环,那么我们通常这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2};

//将数组的第一个假设是最大值
int max = arrays[0];

for (int i = 1; i < arrays.length; i++) {

if (arrays[i] > max) {
max = arrays[i];
}
}

System.out.println("公众号:Java3y:" + max);

那如果我们用递归的话,那怎么用弄呢?首先还是先要找到递归表达式(规律)和递归出口

  • 我们又可以运用1和整体的思想来找到规律
    • 将数组第一个数->2与数组后面的数->{3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}进行切割,将数组后面的数看成是一个整体X={3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2},那么我们就可以看成是第一个数和一个整体进行比较if(2>X) return 2 else(2<X) return X
    • 而我们要做的就是找出这个整体的最大值与2进行比较。找出整体的最大值又是和我们的**初始目的(找出最大值)**是一样的
    • 也就可以写成if( 2>findMax() )return 2 else return findMax()
  • 递归出口,如果数组只有1个元素时,那么这个数组最大值就是它了。

使用到数组的时候,我们通常为数组设定左边界和右边界,这样比较好地进行切割

  • L表示左边界,往往表示的是数组第一个元素,也就会赋值为0(角标为0是数组的第一个元素)
  • R表示右边界,往往表示的是数组的长度,也就会赋值为arrays.length-1(长度-1在角标中才是代表最后一个元素)

那么可以看看我们递归的写法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码

public static void main(String[] args) {

int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};

System.out.println("公众号:Java3y:" + findMax(arrays, 0, arrays.length - 1));


}


/**
* 递归,找出数组最大的值
* @param arrays 数组
* @param L 左边界,第一个数
* @param R 右边界,数组的长度
* @return
*/

public static int findMax(int[] arrays, int L, int R) {

//如果该数组只有一个数,那么最大的就是该数组第一个值了
if (L == R) {
return arrays[L];
} else {

int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出整体的最大值

if (a > b) {
return a;
} else {
return b;
}
}

}

三、冒泡排序递归写法

在冒泡排序章节中给出了C语言的递归实现冒泡排序,那么现在我们已经使用递归的基本思路了,我们使用Java来重写一下看看:

冒泡排序:俩俩交换,在第一趟排序中能够将最大值排到最后面,外层循环控制排序趟数,内层循环控制比较次数

以递归的思想来进行改造:

  • 当第一趟排序后,我们可以将数组最后一位(R)和数组前面的数(L,R-1)进行切割,数组前面的数(L,R-1)看成是一个整体,这个整体又是和我们的**初始目的(找出最大值,与当前趟数的末尾处交换)**是一样的
  • 递归出口:当只有一个元素时,即不用比较了:L==R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码
public static void main(String[] args) {

int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};
bubbleSort(arrays, 0, arrays.length - 1);
System.out.println("公众号:Java3y:" + arrays);


}

public static void bubbleSort(int[] arrays, int L, int R) {

int temp;

//如果只有一个元素了,那什么都不用干
if (L == R) ;

else {
for (int i = L; i < R; i++) {
if (arrays[i] > arrays[i + 1]) {
temp = arrays[i];
arrays[i] = arrays[i + 1];
arrays[i + 1] = temp;
}
}

//第一趟排序后已经将最大值放到数组最后面了

//接下来是排序"整体"的数据了
bubbleSort(arrays, L, R - 1);

}
}

四、斐波那契数列

接触过C语言的同学很可能就知道什么是费波纳切数列了,因为往往做练习题的时候它就会出现,它也是递归的典型应用。

菲波那切数列长这个样子:{1 1 2 3 5 8 13 21 34 55..... n }

数学好的同学可能很容易就找到规律了:前两项之和等于第三项

例如:

1
2
3
复制代码 	1 + 1 = 2
2 + 3 = 5
13 + 21 = 34

如果让我们求出第n项是多少,那么我们就可以很简单写出对应的递归表达式了:Z = (n-2) + (n-1)

递归出口在本题目是需要有两个的,因为它是前两项加起来才得出第三项的值

同样地,那么我们的递归出口可以写成这样:if(n==1) retrun 1 if(n==2) return 2

下面就来看一下完整的代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码
public static void main(String[] args) {

int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);

int fibonacci = fibonacci(10);
System.out.println("公众号:Java3y:" + fibonacci);


}

public static int fibonacci(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 1;
} else {
return (fibonacci(n - 1) + fibonacci(n - 2));
}

}

五、汉诺塔算法

图片来源百度百科:

玩汉诺塔的规则很简单:

  • 有三根柱子,原始装满大小不一的盘子的柱子我们称为A,还有两根空的柱子,我们分别称为B和C(任选)
  • 最终的目的就是将A柱子的盘子全部移到C柱子中
    • 移动的时候有个规则:一次只能移动一个盘子,小的盘子不能在大的盘子上面(反过来:大的盘子不能在小的盘子上面)

我们下面就来玩一下:

  • 只有一个盘子:
    • 将A柱子的盘子直接移动到C柱子中
    • 完成游戏
  • 只有两个盘子:
    • 将A柱子上的小盘子移动到B柱子中
    • 将A柱子上的大盘子移动到C柱子中
    • 最后将在B柱子的小盘子移动到C柱子大盘子中
    • 完成游戏
  • 只有三个盘子:
    • 将A柱子小的盘子移动到C柱子中
    • 将A柱子上的中盘子移动到B柱子中
    • 将C柱子小盘子移动到B柱子中盘子中
    • 将A柱子的大盘子移动到C柱子中
    • 将B柱子的小盘子移动到A柱子中
    • 将B柱子的中盘子移动到C柱子中
    • 最后将A柱子的小盘子移动到C柱子中
    • 完成游戏

…………………..
从前三次玩法中我们就可以发现的规律:

  • 想要将最大的盘子移动到C柱子,就必须将其余的盘子移到B柱子处(借助B柱子将最大盘子移动到C柱子中[除了最大盘子,将所有盘子移动到B柱子中])[递归表达式]
  • 当C柱子有了最大盘子时,所有的盘子在B柱子。现在的目的就是借助A柱子将B柱子的盘子都放到C柱子中(和上面是一样的,已经发生递归了)
  • 当只有一个盘子时,就可以直接移动到C柱子了(递归出口)
    • A柱子称之为起始柱子,B柱子称之为中转柱子,C柱子称之为目标柱子
    • 从上面的描述我们可以发现,起始柱子、中转柱子它们的角色是会变的(A柱子开始是起始柱子,第二轮后就变成了中转柱子了。B柱子开始是目标柱子,第二轮后就变成了起始柱子。总之,起始柱子、中转柱子的角色是不停切换的)

简单来说就分成三步:

  1. 把 n-1 号盘子移动到中转柱子
  2. 把最大盘子从起点移到目标柱子
  3. 把中转柱子的n-1号盘子也移到目标柱子

那么就可以写代码测试一下了(回看上面玩的过程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
复制代码
public static void main(String[] args) {

int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};
//bubbleSort(arrays, 0, arrays.length - 1);

//int fibonacci = fibonacci(10);

hanoi(3, 'A', 'B', 'C');

System.out.println("公众号:Java3y" );


}

/**
* 汉诺塔
* @param n n个盘子
* @param start 起始柱子
* @param transfer 中转柱子
* @param target 目标柱子
*/
public static void hanoi(int n, char start, char transfer, char target) {


//只有一个盘子,直接搬到目标柱子
if (n == 1) {
System.out.println(start + "---->" + target);
} else {

//起始柱子借助目标柱子将盘子都移动到中转柱子中(除了最大的盘子)
hanoi(n - 1, start, target, transfer);
System.out.println(start + "---->" + target);

//中转柱子借助起始柱子将盘子都移动到目标柱子中
hanoi(n - 1, transfer, start, target);

}
}

我们来测试一下看写得对不对:

参考资料:

  • www.zhihu.com/question/24…

六、总结

递归的确是一个比较难理解的东西,好几次都把我绕进去了….

要使用递归首先要知道两件事:

  • 递归出口(终止递归的条件)
  • 递归表达式(规律)

在递归中常常用”整体“的思想,在汉诺塔例子中也不例外:将最大盘的盘子看成1,上面的盘子看成一个整体。那么我们在演算的时候就很清晰了:将”整体“搬到B柱子,将最大的盘子搬到C柱子,最后将B柱子的盘子搬到C柱子中

因为我们人脑无法演算那么多的步骤,递归是用计算机来干的,只要我们找到了递归表达式和递归出口就要相信计算机能帮我们搞掂。

在编程语言中,递归的本质是方法自己调用自己,只是参数不一样罢了。

最后,我们来看一下如果是5个盘子,要运多少次才能运完:

PS:如果有更好的理解方法,或者我理解错的地方大家可以在评论下留言一起交流哦!

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Maven系列二settingxml 配置详解 setti

发表于 2018-03-19

文件存放位置

全局配置: ${M2_HOME}/conf/settings.xml

用户配置: ${user.home}/.m2/settings.xml

note:用户配置优先于全局配置。${user.home} 和和所有其他系统属性只能在3.0+版本上使用。请注意windows和Linux使用变量的区别。

settings.xml详解

声明规范

1
2
3
复制代码<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

localRepository

1
2
复制代码 <!-- 本地仓库的路径。默认值为${user.home}/.m2/repository。 -->
<localRepository>usr/local/maven</localRepository>

interactiveMode

1
2
复制代码 <!--Maven是否需要和用户交互以获得输入。如果Maven需要和用户交互以获得输入,则设置成true,反之则应为false。默认为true。-->
<interactiveMode>true</interactiveMode>

usePluginRegistry

1
2
复制代码<!--Maven是否需要使用plugin-registry.xml文件来管理插件版本。如果需要让Maven使用文件${user.home}/.m2/plugin-registry.xml来管理插件版本,则设为true。默认为false。-->
<usePluginRegistry>false</usePluginRegistry>

offline

1
2
复制代码 <!--表示Maven是否需要在离线模式下运行。如果构建系统需要在离线模式下运行,则为true,默认为false。当由于网络设置原因或者安全因素,构建服务器不能连接远程仓库的时候,该配置就十分有用。 -->
<offline>false</offline>

pluginGroups

1
2
3
4
5
复制代码<!--当插件的组织Id(groupId)没有显式提供时,供搜寻插件组织Id(groupId)的列表。该元素包含一个pluginGroup元素列表,每个子元素包含了一个组织Id(groupId)。当我们使用某个插件,并且没有在命令行为其提供组织Id(groupId)的时候,Maven就会使用该列表。默认情况下该列表包含了org.apache.maven.plugins和org.codehaus.mojo -->
<pluginGroups>
<!--plugin的组织Id(groupId) -->
<pluginGroup>org.codehaus.mojo</pluginGroup>
</pluginGroups>

proxies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码<!--用来配置不同的代理,多代理profiles 可以应对笔记本或移动设备的工作环境:通过简单的设置profile id就可以很容易的更换整个代理配置。 -->
<proxies>
<!--代理元素包含配置代理时需要的信息-->
<proxy>
<!--代理的唯一定义符,用来区分不同的代理元素。-->
<id>myproxy</id>
<!--该代理是否是激活的那个。true则激活代理。当我们声明了一组代理,而某个时候只需要激活一个代理的时候,该元素就可以派上用处。 -->
<active>true</active>
<!--代理的协议。 协议://主机名:端口,分隔成离散的元素以方便配置。-->
<protocol>http</protocol>
<!--代理的主机名。协议://主机名:端口,分隔成离散的元素以方便配置。 -->
<host>proxy.somewhere.com</host>
<!--代理的端口。协议://主机名:端口,分隔成离散的元素以方便配置。 -->
<port>8080</port>
<!--代理的用户名,用户名和密码表示代理服务器认证的登录名和密码。 -->
<username>proxyuser</username>
<!--代理的密码,用户名和密码表示代理服务器认证的登录名和密码。 -->
<password>somepassword</password>
<!--不该被代理的主机名列表。该列表的分隔符由代理服务器指定;例子中使用了竖线分隔符,使用逗号分隔也很常见。-->
<nonProxyHosts>*.google.com|ibiblio.org</nonProxyHosts>
</proxy>
</proxies>

servers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码<!--配置服务端的一些设置。一些设置如安全证书不应该和pom.xml一起分发。这种类型的信息应该存在于构建服务器上的settings.xml文件中。-->
<servers>
<!--服务器元素包含配置服务器时需要的信息 -->
<server>
<!--这是server的id(注意不是用户登陆的id),该id与distributionManagement中repository元素的id相匹配。-->
<id>server001</id>
<!--鉴权用户名。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。 -->
<username>my_login</username>
<!--鉴权密码 。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。密码加密功能已被添加到2.1.0 +。详情请访问密码加密页面-->
<password>my_password</password>
<!--鉴权时使用的私钥位置。和前两个元素类似,私钥位置和私钥密码指定了一个私钥的路径(默认是${user.home}/.ssh/id_dsa)以及如果需要的话,一个密语。将来passphrase和password元素可能会被提取到外部,但目前它们必须在settings.xml文件以纯文本的形式声明。 -->
<privateKey>${usr.home}/.ssh/id_dsa</privateKey>
<!--鉴权时使用的私钥密码。-->
<passphrase>some_passphrase</passphrase>
<!--文件被创建时的权限。如果在部署的时候会创建一个仓库文件或者目录,这时候就可以使用权限(permission)。这两个元素合法的值是一个三位数字,其对应了unix文件系统的权限,如664,或者775。 -->
<filePermissions>664</filePermissions>
<!--目录被创建时的权限。 -->
<directoryPermissions>775</directoryPermissions>
</server>
</servers>

mirrors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码<!--为仓库列表配置的下载镜像列表。高级设置请参阅镜像设置页面 -->
<mirrors>
<!--给定仓库的下载镜像。 -->
<mirror>
<!--该镜像的唯一标识符。id用来区分不同的mirror元素。 -->
<id>planetmirror.com</id>
<!--镜像名称 -->
<name>PlanetMirror Australia</name>
<!--该镜像的URL。构建系统会优先考虑使用该URL,而非使用默认的服务器URL。 -->
<url>http://downloads.planetmirror.com/pub/maven2</url>
<!--被镜像的服务器的id。例如,如果我们要设置了一个Maven中央仓库(http://repo.maven.apache.org/maven2/)的镜像,就需要将该元素设置成central。这必须和中央仓库的id central完全一致。-->
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

profiles

1
2
3
4
5
6
复制代码 <!--根据环境参数来调整构建配置的列表。settings.xml中的profile元素是pom.xml中profile元素的裁剪版本。它包含了id,activation, repositories, pluginRepositories和 properties元素。这里的profile元素只包含这五个子元素是因为这里只关心构建系统这个整体(这正是settings.xml文件的角色定位),而非单独的项目对象模型设置。如果一个settings中的profile被激活,它的值会覆盖任何其它定义在POM中或者profile.xml中的带有相同id的profile。 -->
<profiles>
<!--根据环境参数来调整的构件的配置-->
<profile>
<!--该配置的唯一标识符。 -->
<id>test</id>

Activation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码<!--自动触发profile的条件逻辑。Activation是profile的开启钥匙。如POM中的profile一样,profile的力量来自于它能够在某些特定的环境中自动使用某些特定的值;这些环境通过activation元素指定。activation元素并不是激活profile的唯一方式。settings.xml文件中的activeProfile元素可以包含profile的id。profile也可以通过在命令行,使用-P标记和逗号分隔的列表来显式的激活(如,-P test)。-->
<activation>
<!--profile默认是否激活的标识-->
<activeByDefault>false</activeByDefault>
<!--当匹配的jdk被检测到,profile被激活。例如,1.4激活JDK1.4,1.4.0_2,而!1.4激活所有版本不是以1.4开头的JDK。-->
<jdk>1.5</jdk>
<!--当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。-->
<os>
<!--激活profile的操作系统的名字 -->
<name>Windows XP</name>
<!--激活profile的操作系统所属家族(如 'windows') -->
<family>Windows</family>
<!--激活profile的操作系统体系结构 -->
<arch>x86</arch>
<!--激活profile的操作系统版本-->
<version>5.1.2600</version>
</os>
<!--如果Maven检测到某一个属性(其值可以在POM中通过${name}引用),其拥有对应的name = 值,Profile就会被激活。如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段-->
<property>
<!--激活profile的属性的名称-->
<name>mavenVersion</name>
<!--激活profile的属性的值 -->
<value>2.0.3</value>
</property>
<!--提供一个文件名,通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在,如果不存在则激活profile。另一方面,exists则会检查文件是否存在,如果存在则激活profile。-->
<file>
<!--如果指定的文件存在,则激活profile。 -->
<exists>${basedir}/file2.properties</exists>
<!--如果指定的文件不存在,则激活profile。-->
<missing>${basedir}/file1.properties</missing>
</file>
</activation>

Properties

1
2
3
4
5
6
7
8
9
10
复制代码 <!--对应profile的扩展属性列表。Maven属性和Ant中的属性一样,可以用来存放一些值。这些值可以在POM中的任何地方使用标记${X}来使用,这里X是指属性的名称。属性有五种不同的形式,并且都能在settings.xml文件中访问。
1. env.X: 在一个变量前加上"env."的前缀,会返回一个shell环境变量。例如,"env.PATH"指代了$path环境变量(在Windows上是%PATH%)。
2. project.x:指代了POM中对应的元素值。例如: <project><version>1.0</version></project>通过${project.version}获得version的值。
3. settings.x: 指代了settings.xml中对应元素的值。例如:<settings><offline>false</offline></settings>通过 ${settings.offline}获得offline的值。
4. Java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用该形式访问,例如 ${java.home}。
5. x: 在<properties/>元素中,或者外部文件中设置,以${someVar}的形式使用。 -->
<properties>
<user.install>${user.home}/our-project</user.install>
</properties>
note:如果该profile被激活,则可以再POM中使用${user.install}。

Repositories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码  <!--远程仓库列表,它是Maven用来填充构建系统本地仓库所使用的一组远程项目。 -->
<repositories>
<!--包含需要连接到远程仓库的信息 -->
<repository>
<!--远程仓库唯一标识-->
<id>codehausSnapshots</id>
<!--远程仓库名称 -->
<name>Codehaus Snapshots</name>
<!--如何处理远程仓库里发布版本的下载-->
<releases>
<!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
<enabled>false</enabled>
<!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 -->
<updatePolicy>always</updatePolicy>
<!--当Maven验证构件校验文件失败时该怎么做-ignore(忽略),fail(失败),或者warn(警告)。-->
<checksumPolicy>warn</checksumPolicy>
</releases>
<!--如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素-->
<snapshots>
<enabled/><updatePolicy/><checksumPolicy/>
</snapshots>
<!--远程仓库URL,按protocol://hostname/path形式 -->
<url>http://snapshots.maven.codehaus.org/maven2</url>
<!--用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认的布局;然而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 -->
<layout>default</layout>
</repository>
</repositories>
<!--发现插件的远程仓库列表。仓库是两种主要构件的家。第一种构件被用作其它构件的依赖。这是中央仓库中存储的大部分构件类型。另外一种构件类型是插件。Maven插件是一种特殊类型的构件。由于这个原因,插件仓库独立于其它仓库。pluginRepositories元素的结构和repositories元素的结构类似。每个pluginRepository元素指定一个Maven可以用来寻找新插件的远程地址。-->
<pluginRepositories>
<!--包含需要连接到远程插件仓库的信息.参见profiles/profile/repositories/repository元素的说明-->
<pluginRepository>
<releases>
<enabled/><updatePolicy/><checksumPolicy/>
</releases>
<snapshots>
<enabled/><updatePolicy/><checksumPolicy/>
</snapshots>
<id/><name/><url/><layout/>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>

activeProfiles

1
2
3
4
5
6
7
复制代码<!--手动激活profiles的列表,按照profile被应用的顺序定义activeProfile。 该元素包含了一组activeProfile元素,每个activeProfile都含有一个profile id。任何在activeProfile中定义的profile id,不论环境设置如何,其对应的
profile都会被激活。如果没有匹配的profile,则什么都不会发生。例如,env-test是一个activeProfile,则在pom.xml(或者profile.xml)中对应id的profile会被激活。如果运行过程中找不到这样一个profile,Maven则会像往常一样运行。 -->
<activeProfiles>
<!-- -->
<activeProfile>env-test</activeProfile>
</activeProfiles>
</settings>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Lucene就是这么简单 什么是Lucene?? 为什么我们

发表于 2018-03-19

什么是Lucene??

Lucene是apache软件基金会发布的一个开放源代码的全文检索引擎工具包,由资深全文检索专家Doug Cutting所撰写,它是一个全文检索引擎的架构,提供了完整的创建索引和查询索引,以及部分文本分析的引擎,Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎,Lucene在全文检索领域是一个经典的祖先,现在很多检索引擎都是在其基础上创建的,思想是相通的。

Lucene是根据关健字来搜索的文本搜索工具,只能在某个网站内部搜索文本内容,不能跨网站搜索

既然谈到了网站内部的搜索,那么我们就谈谈我们熟悉的百度、google那些搜索引擎又是基于什么搜索的呢….

这里写图片描述

这里写图片描述

从图上已经看得很清楚,baidu、google等搜索引擎其实是通过网络爬虫的程序来进行搜索的…


为什么我们要用Lucene?

在介绍Lucene的时候,我们已经说了:Lucene又不是搜索引擎,仅仅是在网站内部进行文本的搜索。那我们为什么要学他呢???

我们之前编写纳税服务系统的时候,其实就已经使用过SQL来进行站内的搜索..

既然SQL能做的功能,我们还要学Lucene,为什么呢???

我们来看看我们用SQL来搜索的话,有什么缺点:

  • (1)SQL只能针对数据库表搜索,不能直接针对硬盘上的文本搜索
  • (2)SQL没有相关度排名
  • (3)SQL搜索结果没有关健字高亮显示
  • (4)SQL需要数据库的支持,数据库本身需要内存开销较大,例如:Oracle
  • (5)SQL搜索有时较慢,尤其是数据库不在本地时,超慢,例如:Oracle

这里写图片描述

我们来看看在baidu中搜索Lucene为关键字搜索出的内容是怎么样的:

这里写图片描述

以上所说的,我们如果使用SQL的话,是做不到的。因此我们就学习Lucene来帮我们在站内根据文本关键字来进行搜索数据!


我们如果网站需要根据关键字来进行搜索,可以使用SQL,也可以使用Lucene…那么我们Lucene和SQL是一样的,都是在持久层中编写代码的。。

这里写图片描述

一、快速入门

接下来,我们就讲解怎么使用Lucene了…..在讲解Lucene的API之前,我们首先来讲讲Lucene存放的究竟是什么内容…我们的SQL使用的是数据库中的内存,在硬盘中为DBF文件…那么我们Lucene内部又是什么东西呢??

Lucene中存的就是一系列的二进制压缩文件和一些控制文件,它们位于计算机的硬盘上,
这些内容统称为索引库,索引库有二部份组成:

  • (1)原始记录
    • 存入到索引库中的原始文本,例如:我是钟福成
  • (2)词汇表
    • 按照一定的拆分策略(即分词器)将原始记录中的每个字符拆开后,存入一个供将来搜索的表

也就是说:Lucene存放数据的地方我们通常称之为索引库,索引库又分为两部分组成:原始记录和词汇表….

1.1原始记录和词汇表

当我们想要把数据存到索引库的时候,我们首先存入的是将数据存到原始记录上面去….

又由于我们给用户使用的时候,用户使用的是关键字来进行查询我们的具体记录。因此,我们需要把我们原始存进的数据进行拆分!将拆分出来的数据存进词汇表中。

词汇表就是类似于我们在学Oracle中的索引表,拆分的时候会给出对应的索引值。

一旦用户根据关键字来进行搜索,那么程序就先去查询词汇表中有没有该关键字,如果有该关键字就定位到原始记录表中,将符合条件的原始记录返回给用户查看。

我们查看以下的图方便理解:

这里写图片描述

到了这里,有人可能就会疑问:难道原始记录拆分的数据都是一个一个汉字进行拆分的吗??然后在词汇表中不就有很多的关键字了???

其实,我们在存到原始记录表中的时候,可以指定我们使用哪种算法来将数据拆分,存到词汇表中…..我们的图是Lucene的标准分词算法,一个一个汉字进行拆分。我们可以使用别的分词算法,两个两个拆分或者其他的算法。

1.2编写第一个Lucene程序

首先,我们来导入Lucene的必要开发包:

  • lucene-core-3.0.2.jar【Lucene核心】
  • lucene-analyzers-3.0.2.jar【分词器】
  • lucene-highlighter-3.0.2.jar【Lucene会将搜索出来的字,高亮显示,提示用户】
  • lucene-memory-3.0.2.jar【索引库优化策略】

创建User对象,User对象封装了数据….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码
/**
* Created by ozc on 2017/7/12.
*/
public class User {


private String id ;
private String userName;
private String sal;

public User() {

}
public User(String id, String userName, String sal) {
this.id = id;
this.userName = userName;
this.sal = sal;
}
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getSal() {
return sal;
}

public void setSal(String sal) {
this.sal = sal;
}
}

我们想要使用Lucene来查询出站内的数据,首先我们得要有个索引库吧!于是我们先创建索引库,将我们的数据存到索引库中。

创建索引库的步骤:

  • 1)创建JavaBean对象
  • 2)创建Docment对象
  • 3)将JavaBean对象所有的属性值,均放到Document对象中去,属性名可以和JavaBean相同或不同
  • 4)创建IndexWriter对象
  • 5)将Document对象通过IndexWriter对象写入索引库中
  • 6)关闭IndexWriter对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
复制代码
@Test
public void createIndexDB() throws Exception {

//把数据填充到JavaBean对象中
User user = new User("1", "钟福成", "未来的程序员");

//创建Document对象【导入的是Lucene包下的Document对象】
Document document = new Document();

//将JavaBean对象所有的属性值,均放到Document对象中去,属性名可以和JavaBean相同或不同


/**
* 向Document对象加入一个字段
* 参数一:字段的关键字
* 参数二:字符的值
* 参数三:是否要存储到原始记录表中
* YES表示是
* NO表示否
* 参数四:是否需要将存储的数据拆分到词汇表中
* ANALYZED表示拆分
* NOT_ANALYZED表示不拆分
*
* */
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));

//创建IndexWriter对象
//目录指定为E:/createIndexDB
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));

//使用标准的分词算法对原始记录表进行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);

//LIMITED默认是1W个
IndexWriter.MaxFieldLength maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
/**
* IndexWriter将我们的document对象写到硬盘中
*
* 参数一:Directory d,写到硬盘中的目录路径是什么
* 参数二:Analyzer a, 以何种算法来对document中的原始记录表数据进行拆分成词汇表
* 参数三:MaxFieldLength mfl 最多将文本拆分出多少个词汇
*
* */
IndexWriter indexWriter = new IndexWriter(directory, analyzer, maxFieldLength);

//将Document对象通过IndexWriter对象写入索引库中
indexWriter.addDocument(document);

//关闭IndexWriter对象
indexWriter.close();

}

这里写图片描述

程序执行完,我们就会在硬盘中见到我们的索引库。

这里写图片描述

那我们现在是不知道记录是否真真正正存储到索引库中的,因为我们看不见。索引库存放的数据放在cfs文件下,我们也是不能打开cfs文件的。

于是,我们现在用一个关键字,把索引库的数据读取。看看读取数据是否成功。

根据关键字查询索引库中的内容:

  • 1)创建IndexSearcher对象
  • 2)创建QueryParser对象
  • 3)创建Query对象来封装关键字
  • 4)用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
  • 5)获取符合条件的编号
  • 6)用indexSearcher对象去索引库中查询编号对应的Document对象
  • 7)将Document对象中的所有属性取出,再封装回JavaBean对象中去,并加入到集合中保存,以备将之用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码
@Test
public void findIndexDB() throws Exception {

/**
* 参数一: IndexSearcher(Directory path)查询以xxx目录的索引库
*
* */
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//创建IndexSearcher对象
IndexSearcher indexSearcher = new IndexSearcher(directory);

//创建QueryParser对象
/**
* 参数一: Version matchVersion 版本号【和上面是一样的】
* 参数二:String f,【要查询的字段】
* 参数三:Analyzer a【使用的拆词算法】
* */
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", analyzer);

//给出要查询的关键字
String keyWords = "钟";

//创建Query对象来封装关键字
Query query = queryParser.parse(keyWords);

//用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
TopDocs topDocs = indexSearcher.search(query, 100);

//获取符合条件的编号

for (int i = 0; i < topDocs.scoreDocs.length; i++) {

ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher对象去索引库中查询编号对应的Document对象
Document document = indexSearcher.doc(no);

//将Document对象中的所有属性取出,再封装回JavaBean对象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");

User user = new User(id, userName, sal);
System.out.println(user);

}

这里写图片描述

效果:

这里写图片描述


1.3进一步说明Lucene代码

我们的Lucene程序就是大概这么一个思路:将JavaBean对象封装到Document对象中,然后通过IndexWriter把document写入到索引库中。当用户需要查询的时候,就使用IndexSearcher从索引库中读取数据,找到对应的Document对象,从而解析里边的内容,再封装到JavaBean对象中让我们使用。

这里写图片描述

二、对Lucene代码优化

我们再次看回我们上一篇快速入门写过的代码,我来截取一些有代表性的:

以下代码在把数据填充到索引库,和从索引库查询数据的时候,都出现了。是重复代码!

1
2
3
4
5
复制代码
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));

//使用标准的分词算法对原始记录表进行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);

以下的代码其实就是将JavaBean的数据封装到Document对象中,我们是可以通过反射来对其进行封装….如果不封装的话,我们如果有很多JavaBean都要添加到Document对象中,就会出现很多类似的代码。

1
2
3
4
复制代码
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));

以下代码就是从Document对象中把数据取出来,封装到JavaBean去。如果JavaBean中有很多属性,也是需要我们写很多次类似代码….

1
2
3
4
5
6
7
复制代码

//将Document对象中的所有属性取出,再封装回JavaBean对象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");
User user = new User(id, userName, sal);

2.1编写Lucene工具类

在编写工具类的时候,值得注意的地方:

  • 当我们得到了对象的属性的时候,就可以把属性的get方法封装起来
  • 得到get方法,就可以调用它,得到对应的值
  • 在操作对象的属性时,我们要使用暴力访问
  • 如果有属性,值,对象这三个变量,我们记得使用BeanUtils组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
复制代码
import org.apache.commons.beanutils.BeanUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* Created by ozc on 2017/7/12.
*/

/**
* 使用单例事例模式
* */
public class LuceneUtils {
private static Directory directory;
private static Analyzer analyzer;
private static IndexWriter.MaxFieldLength maxFieldLength;

private LuceneUtils() {}

static{
try {
directory = FSDirectory.open(new File("E:/createIndexDB"));
analyzer = new StandardAnalyzer(Version.LUCENE_30);
maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
} catch (Exception e) {
e.printStackTrace();

}
}

public static Directory getDirectory() {
return directory;
}

public static Analyzer getAnalyzer() {
return analyzer;
}

public static IndexWriter.MaxFieldLength getMaxFieldLength() {
return maxFieldLength;
}

/**
* @param object 传入的JavaBean类型
* @return 返回Document对象
*/
public static Document javaBean2Document(Object object) {
try {
Document document = new Document();
//得到JavaBean的字节码文件对象
Class<?> aClass = object.getClass();

//通过字节码文件对象得到对应的属性【全部的属性,不能仅仅调用getFields()】
Field[] fields = aClass.getDeclaredFields();

//得到每个属性的名字
for (Field field : fields) {
String name = field.getName();
//得到属性的值【也就是调用getter方法获取对应的值】
String method = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
//得到对应的值【就是得到具体的方法,然后调用就行了。因为是get方法,没有参数】
Method aClassMethod = aClass.getDeclaredMethod(method, null);
String value = aClassMethod.invoke(object).toString();
System.out.println(value);


//把数据封装到Document对象中。
document.add(new org.apache.lucene.document.Field(name, value, org.apache.lucene.document.Field.Store.YES, org.apache.lucene.document.Field.Index.ANALYZED));
}
return document;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}


/**
* @param aClass 要解析的对象类型,要用户传入进来
* @param document 将Document对象传入进来
* @return 返回一个JavaBean
*/
public static Object Document2JavaBean(Document document, Class aClass) {
try {
//创建该JavaBean对象
Object obj = aClass.newInstance();
//得到该JavaBean所有的成员变量
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {

//设置允许暴力访问
field.setAccessible(true);
String name = field.getName();
String value = document.get(name);
//使用BeanUtils把数据封装到Bean中
BeanUtils.setProperty(obj, name, value);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Test
public void test() {
User user = new User();
LuceneUtils.javaBean2Document(user);
}

}

2.2使用LuceneUtils改造程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码
@Test
public void createIndexDB() throws Exception {
//把数据填充到JavaBean对象中
User user = new User("2", "钟福成2", "未来的程序员2");
Document document = LuceneUtils.javaBean2Document(user);
/**
* IndexWriter将我们的document对象写到硬盘中
*
* 参数一:Directory d,写到硬盘中的目录路径是什么
* 参数二:Analyzer a, 以何种算法来对document中的原始记录表数据进行拆分成词汇表
* 参数三:MaxFieldLength mfl 最多将文本拆分出多少个词汇
*
* */
IndexWriter indexWriter = new IndexWriter(LuceneUtils.getDirectory(), LuceneUtils.getAnalyzer(), LuceneUtils.getMaxFieldLength());

//将Document对象通过IndexWriter对象写入索引库中
indexWriter.addDocument(document);
//关闭IndexWriter对象
indexWriter.close();
}


@Test
public void findIndexDB() throws Exception {


//创建IndexSearcher对象
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtils.getDirectory());
//创建QueryParser对象
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", LuceneUtils.getAnalyzer());
//给出要查询的关键字
String keyWords = "钟";
//创建Query对象来封装关键字
Query query = queryParser.parse(keyWords);
//用IndexSearcher对象去索引库中查询符合条件的前100条记录,不足100条记录的以实际为准
TopDocs topDocs = indexSearcher.search(query, 100);
//获取符合条件的编号
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher对象去索引库中查询编号对应的Document对象
Document document = indexSearcher.doc(no);
//将Document对象中的所有属性取出,再封装回JavaBean对象中去
User user = (User) LuceneUtils.Document2JavaBean(document, User.class);
System.out.println(user);

}
}

三、索引库优化

我们已经可以创建索引库并且从索引库读取对象的数据了。其实索引库还有地方可以优化的….

3.1合并文件

我们把数据添加到索引库中的时候,每添加一次,都会帮我们自动创建一个cfs文件…

这里写图片描述

这样其实不好,因为如果数据量一大,我们的硬盘就有非常非常多的cfs文件了…..其实索引库会帮我们自动合并文件的,默认是10个。

如果,我们想要修改默认的值,我们可以通过以下的代码修改:

1
2
3
4
5
6
复制代码
//索引库优化
indexWriter.optimize();

//设置合并因子为3,每当有3个cfs文件,就合并
indexWriter.setMergeFactor(3);

3.2设置内存索引库

我们的目前的程序是直接与文件进行操作,这样对IO的开销其实是比较大的。而且速度相对较慢….我们可以使用内存索引库来提高我们的读写效率…

对于内存索引库而言,它的速度是很快的,因为我们直接操作内存…但是呢,我们要将内存索引库是要到硬盘索引库中保存起来的。当我们读取数据的时候,先要把硬盘索引库的数据同步到内存索引库中去的。

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
Article article = new Article(1,"培训","传智是一家Java培训机构");
Document document = LuceneUtil.javabean2document(article);

Directory fsDirectory = FSDirectory.open(new File("E:/indexDBDBDBDBDBDBDBDB"));
Directory ramDirectory = new RAMDirectory(fsDirectory);

IndexWriter fsIndexWriter = new IndexWriter(fsDirectory,LuceneUtil.getAnalyzer(),true,LuceneUtil.getMaxFieldLength());
IndexWriter ramIndexWriter = new IndexWriter(ramDirectory,LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());

ramIndexWriter.addDocument(document);
ramIndexWriter.close();

fsIndexWriter.addIndexesNoOptimize(ramDirectory);
fsIndexWriter.close();

四、分词器

我们在前面中就已经说过了,在把数据存到索引库的时候,我们会使用某些算法,将原始记录表的数据存到词汇表中…..那么这些算法总和我们可以称之为分词器

分词器: ** 采用一种算法,将中英文本中的字符拆分开来,形成词汇,以待用户输入关健字后搜索**

对于为什么要使用分词器,我们也明确地说过:由于用户不可能把我们的原始记录数据完完整整地记录下来,于是他们在搜索的时候,是通过关键字进行对原始记录表的查询….此时,我们就采用分词器来最大限度地匹配相关的数据

这里写图片描述

4.1分词器流程

  • 1
    复制代码 步一:按分词器拆分出词汇
  • 1
    复制代码 步二:去除停用词和禁用词
  • 1
    复制代码 步三:如果有英文,把英文字母转为小写,即搜索不分大小写

4.2分词器API

我们在选择分词算法的时候,我们会发现有非常非常多地分词器API,我们可以用以下代码来看看该分词器是怎么将数据分割的:

1
2
3
4
5
6
7
8
9
10
复制代码
private static void testAnalyzer(Analyzer analyzer, String text) throws Exception {
System.out.println("当前使用的分词器:" + analyzer.getClass());
TokenStream tokenStream = analyzer.tokenStream("content",new StringReader(text));
tokenStream.addAttribute(TermAttribute.class);
while (tokenStream.incrementToken()) {
TermAttribute termAttribute = tokenStream.getAttribute(TermAttribute.class);
System.out.println(termAttribute.term());
}
}

在实验完之后,我们就可以选择恰当的分词算法了….

4.3IKAnalyzer分词器

这是一个第三方的分词器,我们如果要使用的话需要导入对应的jar包

  • IKAnalyzer3.2.0Stable.jar
  • 步二:将IKAnalyzer.cfg.xml和stopword.dic和xxx.dic文件复制到MyEclipse的src目录下,再进行配置,在配置时,首行需要一个空行

这个第三方的分词器有什么好呢????他是中文首选的分词器…也就是说:他是按照中文的词语来进行拆分的!


五、对搜索结果进行处理

5.1搜索结果高亮

我们在使用SQL时,搜索出来的数据是没有高亮的…而我们使用Lucene,搜索出来的内容我们可以设置关键字为高亮…这样一来就更加注重用户体验了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码
String keywords = "钟福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);

//设置关键字高亮
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);

for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);

//设置内容高亮
String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);

Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}

5.2搜索结果摘要

如果我们搜索出来的文章内容太大了,而我们只想显示部分的内容,那么我们可以对其进行摘要…

值得注意的是:搜索结果摘要需要与设置高亮一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码
String keywords = "钟福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);

Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);

//设置摘要
Fragmenter fragmenter = new SimpleFragmenter(4);
highlighter.setTextFragmenter(fragmenter);

for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);

String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);

Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}

5.3搜索结果排序

我们搜索引擎肯定用得也不少,使用不同的搜索引擎来搜索相同的内容。他们首页的排行顺序也会不同…这就是它们内部用了搜索结果排序….

影响网页的排序有非常多种:

  • 1
    复制代码 head/meta/【keywords关键字】
  • 1
    复制代码 网页的标签整洁
  • 1
    复制代码 网页执行速度
  • 1
    复制代码 采用div+css
  • 1
    复制代码 等等等等

而在Lucene中我们就可以设置相关度得分来使不同的结果对其进行排序:

1
2
3
4
5
6
复制代码
IndexWriter indexWriter = new IndexWriter(LuceneUtil.getDirectory(),LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());
//为结果设置得分
document.setBoost(20F);
indexWriter.addDocument(document);
indexWriter.close();

当然了,我们也可以按单个字段排序:

1
2
3
复制代码	//true表示降序
Sort sort = new Sort(new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);

也可以按多个字段排序:在多字段排序中,只有第一个字段排序结果相同时,第二个字段排序才有作用 提倡用数值型排序

1
2
3
复制代码
Sort sort = new Sort(new SortField("count",SortField.INT,true),new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);

5.4条件搜索

在我们的例子中,我们使用的是根据一个关键字来对某个字段的内容进行搜索。语法类似于下面:

1
复制代码	QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());

其实,我们也可以使用关键字来对多个字段进行搜索,也就是多条件搜索。我们实际中常常用到的是多条件搜索,多条件搜索可以使用我们最大限度匹配对应的数据!

1
2
复制代码
QueryParser queryParser = new MultiFieldQueryParser(LuceneUtil.getVersion(),new String[]{"content","title"},LuceneUtil.getAnalyzer());

六、总结

  • Lucene是全文索引引擎的祖先,后面的Solr、Elasticsearch都是基于Lucene的(后面会有一篇讲Elasticsearch的,敬请期待~)
  • Lucene中存的就是一系列的二进制压缩文件和一些控制文件,这些内容统称为索引库,索引库又分了两个部分:
    • 原始记录
    • 词汇表
  • 了解索引库的优化方式:1、合并文件 2、设置内存索引库
  • Lucene的分词器有非常多种,选择自己适合的一种进行分词
  • 查询出来的结果可对其设置高亮、摘要、排序

这篇这是Lucene的冰山一角,一般现在用的可能都是Solr、Elasticsearch的了,但想要更加深入了解Lucene可翻阅其他资料哦~

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务架构之基于容器编排的Dev/Ops流程

发表于 2018-03-14

我们整个 DevOps 流程是建立在容器编排的基础上的,目的是简化流程和实现自动化 CI/CD 和自动化运维。当中会有很多没有想到的地方,可能也不太适用于复杂场景。

随着 DevOps 和 SRE 概念的流行,越来越多的 developer 和 operater 们摒弃传统的开发部署流程,转向了如下图所示的无线循环模式:

在我理解 DevOps 包含三个大块:敏捷开发(Agile)、持续集成与交付(CI/CD)、自动运维(ITSM)。

在容器化的时代,我们是如何实现 DepOps 或者 SRE 的呢?下面我就来分享一下沪江学习产品线团队基于容器编排的 DevOps 流程。

敏捷开发

大道至简,所有血的教训告诉我们,不要把简单的事情复杂化。换句话说,不要用复杂的方法处理简单的事情。我对敏捷的理解是「快」和「微」。快指迭代快,开发快,上线快,性能快。微指微服务、微镜像。围绕这两点,在开发阶段我们需要做以下几件事:

应用微服务化

这是个比较大的概念,不在这里讨论,有兴趣可以参考我的其他文章。但只有应用小了,才有可能快起来。

给 Docker 镜像瘦身

为了让 Docker 启动和运行得快,首先就是要对 Docker 瘦身。由于所有的应用全部会统一为 Java 语言开发,所以我们以 Java 为例,选用了 jre-alpine 作为我们的基础镜像,下面是 Dockerfile 的例子:

使用上述 Dockerfile 生成的镜像平均只有 80 多 MB,启动时间几乎在 5 秒内。使用 Alpine 镜像虽然减小了体积,但缺少一些工具命令,例如 curl 等,可以根据需要酌情安装。另外遇到的一个坑是时区问题:由于 Docker 镜像内的时区是 UTC 时间,和宿主机的东 8 区不一致,所以必须安装 timezone 工具并设置 TZ,才能使容器内时间和宿主机保持一致,对数据库的写入和日志的输出都是非常必要的一环。

把所有环境配置包含在镜像中

早在虚拟机时代,我们已经做到了使用包含依赖的虚拟机镜像来加速部署,那么为什么要止步于此呢?我们可以更进一步,把服务本身也包含在镜像中,Docker 用了更轻量的方式已经实现了这一点。

这里我们还要介绍一个概念,要让制作的镜像,能在所有安装了 Docker 的服务器上运行,而不在乎宿主机的操作系统及环境。借用 Java 的一句话来说:一次制作,多平台运行。所以,我们还会把所有环境的配置文件,以不同的文件名全部放入镜像中,通过参数来选择 Docker 启动时使用的环境配置文件。

值得注意的是,如果开发的应用是基于 Spring 框架的话,这个功能很好实现。但如果是其他语言开发,会有一定的开发量。

本文以默认 Java 开发当所有的开发工作完成后,推荐程序目录结构是这样的:

持续集成与交付

自动化的持续集成和交付在整个 DevOps 流中起了重要的角色,他是衔接开发和运维的桥梁。如果这一环做的不好,无法支撑大量微服务的快速的迭代和高效运维。在这一环节,我们需要灵活的运用工具,尽量减少人参与,当然仍然需要围绕「快」和「微」做文章。

如何减少人工参与到持续集成与持续交付呢?我们最希望的开发过程是:对着计算机说出我们的想要的功能,计算机按照套路,自动编码,自动发布到测试环境,自动运行测试脚本,自动上线。当然,目前时代要实现自动编码的过程还需要发明那只「猫」。

但只要对测试有足够信心,我们完全可以实现一种境界:在炎热的下午,轻松地提交自己编写的代码,去休息室喝杯咖啡,回来后看见自己的代码已经被应用在生产环境上了。在容器时代,我们可以很快速的实现这一梦想,其具体步骤如下图:

Gitfolw 与 Anti-Gitflown

持续集成的第一步是提交代码(Code Commit),VCS 也由 CVS,SVN 进化到如今的 Git,自然不得不说一下 Gitflow。谈起无人不晓的 Gitflow,大家一定会大谈其优点:支持多团队,设置多国家的开发人员并行开发,减小代码冲突或脏代码的上线概率。它的大致流程如下:

Gitflow 给我们展示了复杂团队在处理不通代码版本的优雅解决方案,它需要feature、develop、release、hotfix、master 5 条分支来处理不同时段的并行开发。但这真的合适于一个不超过 20 人的本地合作团队开发吗?我们的开发团队不足 6 人,每个人负责 3 个以上的微服务,几乎不可能在同个项目上安排两个以上的同学并行开发。

在初期我们准守规定并使用标准的 Gitflow 流程,开发人员立刻发现一个问题,他们需要在至少 3 条分支上来回的 merge 代码,且不会有任何代码冲突(因为就一个人开发),降低了开发的效率。这让我意识到,Gitflow 模式也许并不适合于小团队微服务的世界,一种反 Gitflow 模式的想法出现在脑海中。我决定对Gitflow 进行瘦身,化繁至简。

我们把 5 条分支简化为 3 条分支,其中 Master 分支的作用只是维护了最新的线上版本的作用,Dev 分支为开发的主要分支,所有的镜像是以此分支的代码为源头生成的。这时开发的过程变为:

  • 开发人员从 Dev 分支中 checkout 新的 feature 分支,在 feature 分支上进行开发
  • 当开发完成后 merge 回 Dev 分支中,根据 Dev 分支的代码打成镜像,部署 QA 环境交给 QA 人员测试
  • 测试中如有 bug 便在新分支中修复问题循环步骤 2
  • 测试完成 merge 回 Master 分支

如此一来,只有从 Feature 把代码 merge 到 Dev 分支的一次 merge 动作,大大提升可开发效率。

使用Jenkins Pipeline

Jenkins 作为老牌 CI/CD 工具,能够帮我们自动化完成代码编译、上传静态代码分析、制作镜像、部署测试环境、冒烟测试、部署上线等步骤。尤其是Jenkins 2.0 引入 Pipeline 概念后,以上步骤变的如此行云流水。它让我们从步骤 3 开始,完全可以无人值守完成整个集成和发布过程。

工欲善其事必先利其器,首先我们必须要在 Jenkins 上安装插件 :

  • Pipeline Plugin(如果使用Jenkins2.0默认安装)
  • Git
  • Sonar Scaner
  • Docker Pipeline Plugin

Marathon

如果你第一次接触 Jenkins Pipeline,可以从github.com/jenkinsci/p … AL.md找到帮助。

现在,我们开始编写 Groove 代码。基于容器编排的 Pipeline 分为如下几个步骤:

1、检出代码

这个步骤使用 Git 插件,把开发好的代码检出。

2、Maven 构建 Java 代码

由于我们使用的是 Spring Boot 框架,生成物应该是一个可执行的 jar 包。

3、静态代码分析

通过 Sonar Scaner 插件,通知 Sonar 对代码库进行静态扫描。

4、制作 Docker 镜像

此步骤会调用 Docker Pipeline 插件通过预先写好的 Dockerfile,把 jar 包和配置文件、三方依赖包一起打入 Docker 镜像中,并上传到私有 Docker 镜像仓库中。

5、部署测试环境

通过事先写好的部署文件,用 Marathon 插件通知 Marathon 集群,在测试环境中部署生成好的镜像。

6、自动化测试

运行事先测试人员写好的自动化测试脚本来检验程序是否运行正常。

7、人工测试

如果对自动化测试不放心,此时可选择结束 Pipeline,进行人工测试。为了说明整个流程,我们这里选择跳过人工测试环节。

8、部署生产环境

当所有测试通过后,Pipeline 自动发布生产环境。

最后我们来看看整个 Pipeline 的过程:

容器编排配置文档化

在介绍敏捷开发时,曾介绍过根据不同环境的配置参数部署到不同的环境。如何告知部署程序用什么样的配置文件启动服务,每个环境又用多少 CPU,内存和 instance 呢?

下面我们就来介绍一下容器编排的配置文件。由于我们使用 Mesos+Marathon的容器编排方式,部署的重任从以前的写部署脚本变成了写一个 Marathon 的配置,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码{
"id": "/appName",
"cpus": 2,
"mem": 2048.0,
"instances": 2,
"args": [
"--spring.profiles.active=qa"
],
"labels": {
"HAPROXY_GROUP": "external",
"HAPROXY_0_VHOST": "xxx.hujiang.com"
},
"container": {
"type": "DOCKER",
"docker": {
"image": "imageName",
"network": "USER",
"forcePullImage": true,
"portMappings": [
{
"containerPort": 12345,
"hostPort": 0,
"protocol": "tcp",
"servicePort": 12345} ]
},
"volumes": [
{
"containerPath": "/app/log",
"hostPath": "/home/logs/appName",
"mode": "RW" }]},
"ipAddress": {
"networkName": "calico-net"
},
"healthChecks": [
{
"gracePeriodSeconds": 300,
"ignoreHttp1xx": true,
"intervalSeconds": 20,
"maxConsecutiveFailures": 3,
"path": "/health_check",
"portIndex": 0,
"protocol": "HTTP",
"timeoutSeconds": 20
}
],
"uris": [
"file:///etc/docker.tar.gz"
]
}

我们把这个配置内容保存为不同的 Json 文件,每个对应的环境都有一套配置文件。例如 Marathon-qa.json,Marathon-prod.json。当 Pipeline 部署时,可以通过Jenkins Marathon 插件,根据选择不同的环境,调用部署配置,从而达到自动部署的目的。

自动化流程和部署上线分离与管理

开发部署如此的简单快捷,是不是每个人都能方便的使用呢?答案是否定的,并不是因为技术上有难度,而是在于权限。在理想的情况下,通过这套流程的确可以做到在提交代码后,喝杯咖啡的时间就能看见自己的代码已经被千万用户使用了。

但风险过大,我们并不是每个人都能像 Rambo 一样 bug 的存在,大多数的情况还需要使用规范和流程来约束。就像自动化测试取代不了人工黑盒测试一样,部署测试后也不能直接上生产环境,在测试通过后还是需要有个人工确认和部署生产的过程。

所以我们需要把自动化流程和最后的部署上线工作分开来,分别变成两个 Job,并给后者单独分配权限,让有权限的人来做最后的部署工作。这个人可以是 Team leader、开发经理,也可以是运维伙伴,取决于公司的组织结构。

那这个部署的 Job 具体干什么呢?在容器编排时代,结合镜像既构建物的思想,部署 Job 不会从代码编译开始工作,而是把一个充分测试且通过的镜像版本,通过 Marathon Plugin 部署到产线环境中去。这里是 Deploy_only 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码 node('docker-qa'){
if (ReleaseVersion ==""){
echo "发布版本不能为空"
return
}
stage "Prepare image"
def moduleName = "${ApplicationModule}".toLowerCase()
def resDockerImage = imageName + ":latest"
def desDockerImage = imageName + ":${ReleaseVersion}"
if (GenDockerVersion =="true"){
sh "docker pull ${resDockerImage}"
sh "docker tag ${resDockerImage} ${desDockerImage}"
sh "docker push ${desDockerImage}"
sh "docker rmi -f ${resDockerImage} ${desDockerImage}"
}

stage "Deploy on Mesos"
git branch: 'dev', changelog: false, credentialsId: 'deploy-key', url: 'git@gitlab.xxx.com:lms/xxx-test.git'
//Get the right marathon url
echo "DeployDC: " + DeployDC
marathon_url = ""
if (DeployDC=="AA") {
if (DeployEnv == "prod"){
input "Are you sure to deploy to production?"
marathon_url = "${marathon_AA_prod}"
}else if (DeployEnv == "yz") {
marathon_url = "${marathon_AA_yz}"
}
}else if ("${DeployDC}"=="BB"){
if ("${DeployEnv}" == "prod"){
input "Are you sure to deploy to production?"
marathon_url = "${marathon_BB_prod}"
}else if ("${DeployEnv}" == "yz") {
marathon_url = "${marathon_BB_yz}"
}
}
marathon docker: imageName, dockerForcePull: true, forceUpdate: true, url: marathon_url, filename: "${DeployEnv}-deploy.json"
}

为什么不把这个文件跟随应用项目一起放到 scripts 下呢?因为把部署和应用分开后,可以由两拨人进行维护,兼顾公司的组织架构。

自动化运维

在 DevOps 的最后阶段是运维阶段。在容器时代,如何对庞大的镜像制品进行运维呢?我们的目标是尽量实现自动化运维,这里主要讲述两点:

容器的监控

容器的监控大致有两种方式:物理机上安装其他服务监控本机上的所有容器;通过 Mesos 或 Kubernates 自带 API 监控容器状态。两种方式其实都需要在物理机上安装相应的监控软件或 Agent。

在我们团队目前使用 cAdvisor + InfluxDB + Grafana 的组合套件实现对容器的监控。

首先需要在 Mesos 集群中所有的 Agent 安装 cAdvisor 。他负责把宿主机上所有运行中的容器数据以数据点(data point)形式发送给时序数据库(InfluxDB),下面是 cAdvisor 监控的一些数据点:

这些数据点经过 Grafana 整理,展示在界面上,这样我们就能掌握具体容器的性能指标了。下面是一个 Grafana 的截图:

除了对容器本身的监控,宿主机的监控也是必不可少的。由于监控的点有很多,这里不一一例举。

自动伸缩

有了监控指标只是实现了自动化运维的第一步,当业务请求发生大量增加或减少,通过人工监测是不能及时的进行相应的,况且还不一定有那么多的人,7×24 小时的监控。一定需要有一套根据监控数据自行伸缩容的机制。在学习产品线,我们针对容器编排的 Mesos+Marathon 框架,开发了一套针对应用本身的自动扩容微服务。其原理如下:

  • 通过 Restful 的接口通知 AutoScaler 程序需要监控的应用服务。
  • AutoScaler 程序开始读取每台 Agent 上部署相关应用的 Metrics 数据,其中包括 CPU,内存的使用状况。
  • 当发现有应用过于繁忙(其表现形式大多是 CPU 占用过高或内存占用过大)时调用 Marathon API 将其扩容
  • Marathon 收到消息后,立刻通知 Mesos 集群发布新的应用,从而缓解当前的繁忙状况。

结束语

DevOps 和 SRE 并不是一个渴望而不可及的概念,它们需要在不同的环境中落地。我们整个 DevOps 流程是建立在容器编排的基础上的,目的是简化流程和实现自动化 CI/CD 和自动化运维。当中会有很多没有想到的地方,可能也不太适用于复杂场景。其次,本文中的例子也做了相应的隐私处理,可能无法直接使用。希望大家能通过我们在实践中产生的成功和遇到的问题,提炼出适合自己的 DevOps 流程。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java:这是一份详细&全面的HashMap 17 源码分

发表于 2018-03-14

前言

  • HashMap 在 Java 和 Android 开发中非常常见
  • 今天,我将带来HashMap 的全部源码分析,希望你们会喜欢。
  1. 本文基于版本 JDK 1.7,即 Java 7
  2. 关于版本 JDK 1.8,即 Java 8,具体请看文章Java源码分析:关于 HashMap 1.8 的重大更新

目录

示意图


  1. 简介

  • 类定义
1
2
3
复制代码public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
  • 主要介绍

示意图

  • HashMap 的实现在 JDK 1.7 和 JDK 1.8 差别较大
  • 今天,我将主要讲解 JDK 1.7 中 HashMap 的源码解析

关于 JDK 1.8 中 HashMap 的源码解析请看文章:Java源码分析:关于 HashMap 1.8 的重大更新


  1. 数据结构

2.1 具体描述

HashMap 采用的数据结构 = 数组(主) + 单链表(副),具体描述如下

该数据结构方式也称:拉链法

示意图

2.2 示意图

示意图

2.3 存储流程

注:为了让大家有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出

示意图

2.4 数组元素 & 链表节点的 实现类

  • HashMap中的数组元素 & 链表节点 采用 Entry类 实现,如下图所示

示意图

  1. 即 HashMap的本质 = 1个存储Entry类对象的数组 + 多个单链表
  2. Entry对象本质 = 1个映射(键 - 值对),属性包括:键(key)、值(value) & 下1节点( next) = 单链表的指针 = 也是一个Entry对象,用于解决hash冲突
  • 该类的源码分析如下

具体分析请看注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
复制代码/** 
* Entry类实现了Map.Entry接口
* 即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 键
V value; // 值
Entry<K,V> next; // 指向下一个节点 ,也是一个Entry对象,从而形成解决hash冲突的单链表
int hash; // hash值

/**
* 构造方法,创建一个Entry
* 参数:哈希值h,键值k,值v、下一个节点n
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}

// 返回 与 此项 对应的键
public final K getKey() {
return key;
}

// 返回 与 此项 对应的值
public final V getValue() {
return value;
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

/**
* equals()
* 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}

/**
* hashCode()
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}

public final String toString() {
return getKey() + "=" + getValue();
}

/**
* 当向HashMap中添加元素时,即调用put(k,v)时,
* 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
* 此处没做任何处理
*/
void recordAccess(HashMap<K,V> m) {
}

/**
* 当从HashMap中删除了一个Entry时,会调用该函数
* 此处没做任何处理
*/
void recordRemoval(HashMap<K,V> m) {
}

}

  1. 具体使用

3.1 主要使用API(方法、函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码V get(Object key); // 获得指定键的值
V put(K key, V value); // 添加键值对
void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对

boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true

Set<K> keySet(); // 单独抽取key序列,将所有key生成一个Set
Collection<V> values(); // 单独value序列,将所有value生成一个Collection

void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空

3.2 使用流程

  • 在具体使用时,主要流程是:
  1. 声明1个 HashMap的对象
  2. 向 HashMap 添加数据(成对 放入 键 - 值对)
  3. 获取 HashMap 的某个数据
  4. 获取 HashMap 的全部数据:遍历HashMap
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
复制代码import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class HashMapTest {

public static void main(String[] args) {
/**
* 1. 声明1个 HashMap的对象
*/
Map<String, Integer> map = new HashMap<String, Integer>();

/**
* 2. 向HashMap添加数据(成对 放入 键 - 值对)
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);

/**
* 3. 获取 HashMap 的某个数据
*/
System.out.println("key = 产品经理时的值为:" + map.get("产品经理"));

/**
* 4. 获取 HashMap 的全部数据:遍历HashMap
* 核心思想:
* 步骤1:获得key-value对(Entry) 或 key 或 value的Set集合
* 步骤2:遍历上述Set集合(使用for循环 、 迭代器(Iterator)均可)
* 方法共有3种:分别针对 key-value对(Entry) 或 key 或 value
*/

// 方法1:获得key-value的Set集合 再遍历
System.out.println("方法1");
// 1. 获得key-value对(Entry)的Set集合
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

// 2. 遍历Set集合,从而获取key-value
// 2.1 通过for循环
for(Map.Entry<String, Integer> entry : entrySet){
System.out.print(entry.getKey());
System.out.println(entry.getValue());
}
System.out.println("----------");
// 2.2 通过迭代器:先获得key-value对(Entry)的Iterator,再循环遍历
Iterator iter1 = entrySet.iterator();
while (iter1.hasNext()) {
// 遍历时,需先获取entry,再分别获取key、value
Map.Entry entry = (Map.Entry) iter1.next();
System.out.print((String) entry.getKey());
System.out.println((Integer) entry.getValue());
}


// 方法2:获得key的Set集合 再遍历
System.out.println("方法2");

// 1. 获得key的Set集合
Set<String> keySet = map.keySet();

// 2. 遍历Set集合,从而获取key,再获取value
// 2.1 通过for循环
for(String key : keySet){
System.out.print(key);
System.out.println(map.get(key));
}

System.out.println("----------");

// 2.2 通过迭代器:先获得key的Iterator,再循环遍历
Iterator iter2 = keySet.iterator();
String key = null;
while (iter2.hasNext()) {
key = (String)iter2.next();
System.out.print(key);
System.out.println(map.get(key));
}


// 方法3:获得value的Set集合 再遍历
System.out.println("方法3");

// 1. 获得value的Set集合
Collection valueSet = map.values();

// 2. 遍历Set集合,从而获取value
// 2.1 获得values 的Iterator
Iterator iter3 = valueSet.iterator();
// 2.2 通过遍历,直接获取value
while (iter3.hasNext()) {
System.out.println(iter3.next());
}

}


}

// 注:对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高
// 原因:
// 1. 对于 遍历keySet 、valueSet,实质上 = 遍历了2次:1 = 转为 iterator 迭代器遍历、2 = 从 HashMap 中取出 key 的 value 操作(通过 key 值 hashCode 和 equals 索引)
// 2. 对于 遍历 entrySet ,实质 = 遍历了1次 = 获取存储实体Entry(存储了key 和 value )
  • 运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码方法1
Java2
iOS3
数据挖掘4
Android1
产品经理5
----------
Java2
iOS3
数据挖掘4
Android1
产品经理5
方法2
Java2
iOS3
数据挖掘4
Android1
产品经理5
----------
Java2
iOS3
数据挖掘4
Android1
产品经理5
方法3
2
3
4
1
5

下面,我们按照上述的使用过程,对一个个步骤进行源码解析


  1. 基础知识:HashMap中的重要参数(变量)

  • 在进行真正的源码分析前,先讲解HashMap中的重要参数(变量)
  • HashMap中的主要参数 = 容量、加载因子、扩容阈值
  • 具体介绍如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码// 1. 容量(capacity): HashMap中数组的长度
// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
// b. 初始容量 = 哈希表创建时的容量
// 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;

// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
// 实际加载因子
final float loadFactor;
// 默认加载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;

// 4. 其他
// 存储数据的Entry类型 数组,长度 = 2的幂
// HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// HashMap的大小,即 HashMap中存储的键值对的数量
transient int size;
  • 参数示意图

示意图

  • 此处 详细说明 加载因子

示意图


  1. 源码分析

  • 本次的源码分析主要是根据 使用步骤 进行相关函数的详细分析
  • 主要分析内容如下:

示意图

  • 下面,我将对每个步骤内容的主要方法进行详细分析

步骤1:声明1个 HashMap的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
复制代码/**
* 函数使用原型
*/
Map<String,Integer> map = new HashMap<String,Integer>();

/**
* 源码分析:主要是HashMap的构造函数 = 4个
* 仅贴出关于HashMap构造函数的源码
*/
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{

// 省略上节阐述的参数

/**
* 构造函数1:默认构造函数(无参)
* 加载因子 & 容量 = 默认 = 0.75、16
*/
public HashMap() {
// 实际上是调用构造函数3:指定“容量大小”和“加载因子”的构造函数
// 传入的指定容量 & 加载因子 = 默认
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

/**
* 构造函数2:指定“容量大小”的构造函数
* 加载因子 = 默认 = 0.75 、容量 = 指定大小
*/
public HashMap(int initialCapacity) {
// 实际上是调用指定“容量大小”和“加载因子”的构造函数
// 只是在传入的加载因子参数 = 默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

/**
* 构造函数3:指定“容量大小”和“加载因子”的构造函数
* 加载因子 & 容量 = 自己指定
*/
public HashMap(int initialCapacity, float loadFactor) {

// HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;

// 设置 加载因子
this.loadFactor = loadFactor;
// 设置 扩容阈值 = 初始容量
// 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算,下面会详细讲解
threshold = initialCapacity;

init(); // 一个空方法用于未来的子对象扩展
}

/**
* 构造函数4:包含“子Map”的构造函数
* 即 构造出来的HashMap包含传入Map的映射关系
* 加载因子 & 容量 = 默认
*/

public HashMap(Map<? extends K, ? extends V> m) {

// 设置容量大小 & 加载因子 = 默认
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

// 该方法用于初始化 数组 & 阈值,下面会详细说明
inflateTable(threshold);

// 将传入的子Map中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
}
  • 注:
    1. 此处仅用于接收初始容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表,即初始化存储数组table
    2. 此处先给出结论:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时。下面会详细说明

至此,关于HashMap的构造函数讲解完毕。


步骤2:向HashMap添加数据(成对 放入 键 - 值对)

  • 添加数据的流程如下

注:为了让大家有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出

示意图

  • 源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码 /**
* 函数使用原型
*/
map.put("Android", 1);
map.put("Java", 2);
map.put("iOS", 3);
map.put("数据挖掘", 4);
map.put("产品经理", 5);

/**
* 源码分析:主要分析: HashMap的put函数
*/
public V put(K key, V value)
(分析1)// 1. 若 哈希表未初始化(即 table为空)
// 则使用 构造函数时设置的阈值(即初始容量) 初始化 数组table
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 2. 判断key是否为空值null
(分析2)// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
// (本质:key = Null时,hash值 = 0,故存放到table[0]中)
// 该位置永远只有1个value,新传进来的value会覆盖旧的value
if (key == null)
return putForNullKey(value);

(分析3) // 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
// a. 根据键值key计算hash值
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置
int i = indexFor(hash, table.length);

// 3. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
(分析4)// 3.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //并返回旧的value
}
}

modCount++;

(分析5)// 3.2 若 该key不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
return null;
}
  • 根据源码分析所作出的流程图

示意图

  • 下面,我将根据上述流程的5个分析点进行详细讲解

分析1:初始化哈希表

即 初始化数组(table)、扩容阈值(threshold)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码   /**
* 函数使用原型
*/
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}

/**
* 源码分析:inflateTable(threshold);
*/
private void inflateTable(int toSize) {

// 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂
// 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
int capacity = roundUpToPowerOf2(toSize);->>分析1

// 2. 重新计算阈值 threshold = 容量 * 加载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

// 3. 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)
// 即 哈希表的容量大小 = 数组大小(长度)
table = new Entry[capacity]; //用该容量初始化table

initHashSeedAsNeeded(capacity);
}

/**
* 分析1:roundUpToPowerOf2(toSize)
* 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
* 特别注意:容量大小必须为2的幂,该原因在下面的讲解会详细分析
*/

private static int roundUpToPowerOf2(int number) {

//若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:>传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY ?
MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
  • 再次强调:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时

分析2:当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码   /**
* 函数使用原型
*/
if (key == null)
return putForNullKey(value);

/**
* 源码分析:putForNullKey(value)
*/
private V putForNullKey(V value) {
// 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
// 1. 若有:则用新value 替换 旧value;同时返回旧的value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;

// 2 .若无key==null的键,那么调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]中
addEntry(0, null, value, 0);
// 注:
// a. addEntry()的第1个参数 = hash值 = 传入0
// b. 即 说明:当key = null时,也有hash值 = 0,所以HashMap的key 可为null
// c. 对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// d. 此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,
return null;

}

从此处可以看出:

  • HashMap的键key 可为null(区别于 HashTable的key 不可为null)
  • HashMap的键key 可为null且只能为1个,但值value可为null且为多个

分析3:计算存放数组 table 中的位置(即 数组下标 or 索引)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码   /**
* 函数使用原型
* 主要分为2步:计算hash值、根据hash值再计算得出最后数组位置
*/
// a. 根据键值key计算hash值 ->> 分析1
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2
int i = indexFor(hash, table.length);

/**
* 源码分析1:hash(key)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/

// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

// JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
// 1. 取hashCode值: h = key.hashCode()
// 2. 高位参与低位的运算:h ^ (h >>> 16)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// a. 当key = null时,hash值 = 0,所以HashMap的key 可为null
// 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
// b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
}

/**
* 函数源码分析2:indexFor(hash, table.length)
* JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
*/
static int indexFor(int h, int length) {
return h & (length-1);
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
}
  • 总结 计算存放在数组 table 中的位置(即数组下标、索引)的过程
    示意图

在了解 如何计算存放数组table 中的位置 后,所谓 知其然 而 需知其所以然,下面我将讲解为什么要这样计算,即主要解答以下3个问题:

  1. 为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
  2. 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
  3. 为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

在回答这3个问题前,请大家记住一个核心思想:

所有处理的根本目的,都是为了提高 存储key-value的数组下标位置 的随机性 & 分布均匀性,尽量避免出现hash值冲突。即:对于不同key,存储的数组下标位置要尽可能不一样

问题1:为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

  • 结论:容易出现 哈希码 与 数组大小范围不匹配的情况,即 计算出来的哈希码可能 不在数组大小范围内,从而导致无法匹配存储位置
  • 原因描述

示意图

  • 为了解决 “哈希码与数组大小范围不匹配” 的问题,HashMap给出了解决方案:哈希码 与运算(&) (数组长度-1);请继续问题2

问题2:为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

  • 结论:根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题
  • 具体解决方案描述

示意图

问题3:为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

  • 结论:加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
  • 具体描述

示意图

至此,关于怎么计算 key-value 值存储在HashMap数组位置 & 为什么要这么计算,讲解完毕。


分析4:若对应的key已存在,则 使用 新value 替换 旧value

注:当发生 Hash冲突时,为了保证 键key的唯一性哈希表并不会马上在链表中插入新数据,而是先查找该 key是否已存在,若已存在,则替换即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码   /**
* 函数使用原型
*/
// 2. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 2.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //并返回旧的value
}
}

modCount++;

// 2.2 若 该key不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
return null;
  • 此处无复杂的源码分析,但此处的分析点主要有2个:替换流程 & key是否存在(即key值的对比)

分析1:替换流程

具体如下图:

示意图

分析2:key值的比较

采用 equals() 或 “==” 进行比较,下面给出其介绍 & 与 “==”使用的对比

示意图


分析5:若对应的key不存在,则将该“key-value”添加到数组table的对应位置中

  • 函数源码分析如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
复制代码      /**
* 函数使用原型
*/
// 2. 判断该key对应的值是否已存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 2.1 若该key对应的值已存在,则用新的value取代旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;

// 2.2 若 该key对应的值不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);

/**
* 源码分析:addEntry(hash, key, value, i)
* 作用:添加键值对(Entry )到 HashMap中
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
// 参数3 = 插入数组table的索引位置 = 数组下标

// 1. 插入前,先判断容量是否足够
// 1.1 若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // a. 扩容2倍 --> 分析1
hash = (null != key) ? hash(key) : 0; // b. 重新计算该Key对应的hash值
bucketIndex = indexFor(hash, table.length); // c. 重新计算该Key对应的hash值的存储数组下标位置
}

// 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中--> 分析2
createEntry(hash, key, value, bucketIndex);
}

/**
* 分析1:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {

// 1. 保存旧数组(old table)
Entry[] oldTable = table;

// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;

// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}

// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];

// 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1
transfer(newTable);

// 6. 新数组table引用到HashMap的table属性上
table = newTable;

// 7. 重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}

/**
* 分析1.1:transfer(newTable);
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
*/
void transfer(Entry[] newTable) {
// 1. src引用了旧数组
Entry[] src = table;

// 2. 获取新数组的大小 = 获取新容量大小
int newCapacity = newTable.length;

// 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (int j = 0; j < src.length; j++) {
// 3.1 取得旧数组的每个元素
Entry<K,V> e = src[j];
if (e != null) {
// 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
src[j] = null;

do {
// 3.3 遍历 以该数组元素为首 的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
// 3.4 重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
// 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
// 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
e.next = newTable[i];
newTable[i] = e;
// 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
} while (e != null);
// 如此不断循环,直到遍历完数组上的所有数据元素
}
}
}

/**
* 分析2:createEntry(hash, key, value, bucketIndex);
* 作用: 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
*/
void createEntry(int hash, K key, V value, int bucketIndex) {

// 1. 把table中该位置原来的Entry保存
Entry<K,V> e = table[bucketIndex];

// 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
// 即 在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突)
table[bucketIndex] = new Entry<>(hash, key, value, e);

// 3. 哈希表的键值对数量计数增加
size++;
}

此处有2点需特别注意:键值对的添加方式 & 扩容机制

1. 键值对的添加方式:单链表的头插法

  • 即 将该位置(数组上)原来的数据放在该位置的(链表)下1个节点中(next)、在该位置(数组上)放入需插入的数据-> 从而形成链表
  • 如下示意图

示意图

2. 扩容机制

  • 具体流程如下:

示意图

  • 扩容过程中的转移数据示意图如下

示意图

在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况

设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

  • 此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 = 线程不安全

下面最后1节会对上述情况详细说明

总结

  • 向 HashMap 添加数据(成对 放入 键 - 值对)的全流程

示意图

  • 示意图
    示意图

至此,关于 “向 HashMap 添加数据(成对 放入 键 - 值对)“讲解完毕


步骤3:从HashMap中获取数据

  • 假如理解了上述put()函数的原理,那么get()函数非常好理解,因为二者的过程原理几乎相同
  • get()函数的流程如下:

示意图

  • 具体源码分析如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
复制代码/**
* 函数原型
* 作用:根据键key,向HashMap获取对应的值
*/
map.get(key);


/**
* 源码分析
*/
public V get(Object key) {

// 1. 当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键
if (key == null)
return getForNullKey(); --> 分析1

// 2. 当key ≠ null时,去获得对应值 -->分析2
Entry<K,V> entry = getEntry(key);

return null == entry ? null : entry.getValue();
}


/**
* 分析1:getForNullKey()
* 作用:当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键
*/
private V getForNullKey() {

if (size == 0) {
return null;
}

// 遍历以table[0]为头结点的链表,寻找 key==null 对应的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {

// 从table[0]中取key==null的value值
if (e.key == null)
return e.value;
}
return null;
}

/**
* 分析2:getEntry(key)
* 作用:当key ≠ null时,去获得对应值
*/
final Entry<K,V> getEntry(Object key) {

if (size == 0) {
return null;
}

// 1. 根据key值,通过hash()计算出对应的hash值
int hash = (key == null) ? 0 : hash(key);

// 2. 根据hash值计算出对应的数组下标
// 3. 遍历 以该数组下标的数组元素为头结点的链表所有节点,寻找该key对应的值
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {

Object k;
// 若 hash值 & key 相等,则证明该Entry = 我们要的键值对
// 通过equals()判断key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

至此,关于 “向 HashMap 获取数据 “讲解完毕


步骤4:对HashMap的其他操作

即 对其余使用API(函数、方法)的源码分析

  • HashMap除了核心的put()、get()函数,还有以下主要使用的函数方法
1
2
3
4
5
6
7
8
9
复制代码void clear(); // 清除哈希表中的所有键值对
int size(); // 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空

void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中
V remove(Object key); // 删除该键值对

boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true
  • 下面将简单介绍上面几个函数的源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
复制代码  /**
* 函数:isEmpty()
* 作用:判断HashMap是否为空,即无键值对;size == 0时 表示为 空
*/

public boolean isEmpty() {
return size == 0;
}

/**
* 函数:size()
* 作用:返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
*/

public int size() {
return size;
}

/**
* 函数:clear()
* 作用:清空哈希表,即删除所有键值对
* 原理:将数组table中存储的Entry全部置为null、size置为0
*/
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}

/**
* 函数:putAll(Map<? extends K, ? extends V> m)
* 作用:将指定Map中的键值对 复制到 此Map中
* 原理:类似Put函数
*/

public void putAll(Map<? extends K, ? extends V> m) {
// 1. 统计需复制多少个键值对
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;

// 2. 若table还没初始化,先用刚刚统计的复制数去初始化table
if (table == EMPTY_TABLE) {
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}

// 3. 若需复制的数目 > 阈值,则需先扩容
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
// 4. 开始复制(实际上不断调用Put函数插入)
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}

/**
* 函数:remove(Object key)
* 作用:删除该键值对
*/

public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
// 1. 计算hash值
int hash = (key == null) ? 0 : hash(key);
// 2. 计算存储的数组下标位置
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;

while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
// 若删除的是table数组中的元素(即链表的头结点)
// 则删除操作 = 将头结点的next引用存入table[i]中
if (prev == e)
table[i] = next;

//否则 将以table[i]为头结点的链表中,当前Entry的前1个Entry中的next 设置为 当前Entry的next(即删除当前Entry = 直接跳过当前Entry)
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}

return e;
}

/**
* 函数:containsKey(Object key)
* 作用:判断是否存在该键的键值对;是 则返回true
* 原理:调用get(),判断是否为Null
*/
public boolean containsKey(Object key) {
return getEntry(key) != null;
}

/**
* 函数:containsValue(Object value)
* 作用:判断是否存在该值的键值对;是 则返回true
*/
public boolean containsValue(Object value) {
// 若value为空,则调用containsNullValue()
if (value == null)
return containsNullValue();

// 若value不为空,则遍历链表中的每个Entry,通过equals()比较values 判断是否存在
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;//返回true
return false;
}

// value为空时调用的方法
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}

至此,关于HashMap的底层原理 & 主要使用API(函数、方法)讲解完毕。


  1. 源码总结

下面,用3个图总结整个源码内容:

总结内容 = 数据结构、主要参数、添加 & 查询数据流程、扩容机制

  • 数据结构 & 主要参数

示意图

  • 添加 & 查询数据流程

示意图

  • 扩容机制

示意图


  1. 与 JDK 1.8的区别

HashMap 的实现在 JDK 1.7 和 JDK 1.8 差别较大,具体区别如下

JDK 1.8 的优化目的主要是:减少 Hash冲突 & 提高哈希表的存、取效率;关于 JDK 1.8 中 HashMap 的源码解析请看文章:Java源码分析:关于 HashMap 1.8 的重大更新

7.1 数据结构

示意图

7.2 存入数据时(获取数据 类似)

示意图

7.3 扩容机制

示意图


  1. 额外补充:关于HashMap的其他问题

  • 有几个小问题需要在此补充

示意图

  • 具体如下

8.1 哈希表如何解决Hash冲突

示意图

8.2 为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

  • 具体解答如下

示意图

  • 下面主要讲解 HashMap 线程不安全的其中一个重要原因:多线程下容易出现resize()死循环
  • 本质 = 并发 执行 put()操作导致触发 扩容行为,从而导致 环形链表,使得在获取数据遍历链表时形成死循环,即Infinite Loop*
  • 先看扩容的源码分析resize()

关于resize()的源码分析已在上文详细分析,此处仅作重点分析:transfer()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
复制代码/**
* 源码分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {

// 1. 保存旧数组(old table)
Entry[] oldTable = table;

// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;

// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}

// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];

// 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1
transfer(newTable);

// 6. 新数组table引用到HashMap的table属性上
table = newTable;

// 7. 重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}

/**
* 分析1.1:transfer(newTable);
* 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
* 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
*/
void transfer(Entry[] newTable) {
// 1. src引用了旧数组
Entry[] src = table;

// 2. 获取新数组的大小 = 获取新容量大小
int newCapacity = newTable.length;

// 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
for (int j = 0; j < src.length; j++) {
// 3.1 取得旧数组的每个元素
Entry<K,V> e = src[j];
if (e != null) {
// 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
src[j] = null;

do {
// 3.3 遍历 以该数组元素为首 的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
// 3.3 重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
// 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
// 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
e.next = newTable[i];
newTable[i] = e;
// 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
} while (e != null);
// 如此不断循环,直到遍历完数组上的所有数据元素
}
}
}

从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况

设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

  • 此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态,具体请看下图:

初始状态、步骤1、步骤2

示意图

示意图

示意图

注:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况。

但 JDK 1.8 还是线程不安全,因为 无加同步锁保护

8.3 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

示意图

8.4 HashMap 中的 key若 Object类型, 则需实现哪些方法?

示意图

至此,关于HashMap的所有知识讲解完毕。


  1. 总结

  • 本文主要讲解 Java的 HashMap源码 & 相关知识
  • 下面我将继续对Java、 Android中的其他知识 深入讲解 ,有兴趣可以继续关注Carson_Ho的安卓开发笔记

请点赞!因为你的鼓励是我写作的最大动力!


欢迎关注carson_ho的微信公众号

示意图

示意图

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…896897898…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%