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

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


  • 首页

  • 归档

  • 搜索

十个实用MySQL函数

发表于 2018-01-03

本文首发于个人微信公众号《andyqian》,期待你的关注!

前言

继上一次《十个实用MySQL命令》后,今天奉上十个实用MySQL函数。下面都是一些比较常用且简单的函数,在工作中也是非常常用的。

函数

0. 显示当前时间
命令:select now()。
作用: 显示当前时间。
应用场景: 创建时间,修改时间等默认值。
例子:

1
2
3
4
5
6
7
复制代码mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2017-12-27 20:14:56 |
+---------------------+
1 row in set (0.00 sec)

1. 字符长度
命令:select char_length('andyqan')。
作用: 显示指定字符长度。
应用场景: 查看字符长度时。
例子:

1
2
3
4
5
6
7
复制代码mysql> select char_length('andyqian');
+-------------------------+
| char_length('andyqian') |
+-------------------------+
|                       8 |
+-------------------------+
1 row in set (0.00 sec)

2. 日期格式化
命令:select date_format(now(),'%y-%m-%d)。
作用:格式化日期。
应用场景:格式化日期时。
例子:

1
2
3
4
5
6
7
复制代码mysql> select date_format(now(),'%y-%m-%d');
+-------------------------------+
| date_format(now(),'%y-%m-%d') |
+-------------------------------+
| 17-12-28                      |
+-------------------------------+
1 row in set (0.00 sec)

这里支持的格式有:
%y:表示年(两位数),例如: 17 年。
%Y:表示4位数中的年,例如: 2017年
%m:表示月(1-12)
%d: 表示月中的天
%H: 小时(0-23)
%i: 分钟 (0-59)
%s: 秒 (0-59)

年月日时分秒: %y-%m-%d %H:%i:%s,
如下所示:

1
2
3
4
5
6
7
复制代码mysql> select DATE_FORMAT(now(),'%y-%m-%d %H:%i:%s');
+----------------------------------------+
| DATE_FORMAT(now(),'%y-%m-%d %H:%i:%s') |
+----------------------------------------+
| 17-12-27 20:28:54                      |
+----------------------------------------+
1 row in set (0.00 sec)

3. 添加/减少日期时间
命令:
DATE_ADD(date,interval expr unit)
DATE_SUB(date,interval expr unit)
作用: 增加/减少日期时间
应用场景:当前时间的前一天,前几分钟。 常用于数据统计。
例子:

1
2
3
4
5
6
7
复制代码mysql> select date_add(now(),interval 1 day);
+--------------------------------+
| date_add(now(),interval 1 day) |
+--------------------------------+
| 2017-12-28 20:10:17            |
+--------------------------------+
1 row in set (0.00 sec)

其中Date表示日期格式,其中就包括: 如
2017-12-27,now() 等格式。
expr:表示数量。
unit:表示单位,支持毫秒(microsecond),秒(second),小时(hour),天(day),周(week),年(year)等。

4. 类型转换
命令: CAST(expr AS type)
作用: 主要用于显示类型转换
应用场景:显示类型转换
例子:

1
2
3
4
5
6
7
复制代码mysql> select cast(18700000000 as char);
+---------------------------+
| cast(18700000000 as char) |
+---------------------------+
| 18700000000               |
+---------------------------+
1 row in set (0.00 sec)

需要注意的是,其中type支持的不是全部基本数据类型,支持的详细类型,请参考之前的文章《谈谈MySQL显示类型转换》。

5. 加密函数
命令: md5(data)
作用: 用于加密数据
应用场景:加密,一些隐私数据,例如银行卡号,身份证等需要存储密文,(当然,不建议使用数据库层加密,应该在应用层加密)
例子:

1
2
3
4
5
6
7
复制代码mysql> select md5("andyqian");
+----------------------------------+
| md5("andyqian")                  |
+----------------------------------+
| 8a6f60827608e7f1ae29d1abcecffc3a |
+----------------------------------+
1 row in set (0.00 sec)

注意事项: 如果目前你们数据库中的数据还是明文,此时可以使用数据库加密算法进行加密。

例如: (仅做演示):

1
复制代码update t_base_user set name=md5(name),updated_time=now() where id=1;

支持的加密函数有:

  1. md5()
  2. des_encrypt(加密) / des_decrypt(解密);
  3. sha1()
  4. password() 等等

这里不再一一介绍,有兴趣的同学,可以移步至官网进行详细了解。

6. 字符串连接
命令: concat(str,str2,str3)
作用:拼接字符串
应用场景:拼接字符串,例如在某些字段上同一加上指定字符串。
例子:

1
2
3
4
5
6
7
复制代码mysql> select concat("andy","qian");
+-----------------------+
| concat("andy","qian") |
+-----------------------+
| andyqian              |
+-----------------------+
1 row in set (0.00 sec)

该函数平时也还是用的比较多的,基本上场景是,在某些数据上,加上特定的字符串。方法如下:

7. JSON函数(5.7版本才支持)

命令:json_object(函数)
作用:转换json字符串
应用场景:指定数据转换json字符串
例子:

1
2
3
4
5
6
7
复制代码mysql> select json_object("name","andyqian","database","MySQL");
+---------------------------------------------------+
| json_object("name","andyqian","database","MySQL") |
+---------------------------------------------------+
| {"name": "andyqian", "database": "MySQL"}         |
+---------------------------------------------------+
1 row in set (0.00 sec)

其中包括json_array:

1
2
3
4
5
6
7
复制代码mysql> select json_array("name","andyqian","database","MySQL");
+--------------------------------------------------+
| json_array("name","andyqian","database","MySQL") |
+--------------------------------------------------+
| ["name", "andyqian", "database", "MySQL"]        |
+--------------------------------------------------+
1 row in set (0.00 sec)

判断是否为json字符串的json_valid():

1
复制代码select json_valid('{"name": "andyqian", "database": "MySQL"}');

其中为有效json字符串时为1。
无效json字符串时为0.
还有很多方法,不再一一演示。

8. 聚合函数
命令: sum(),count(),avg(),max(),min()
作用:统计,求平均值,最大,最小值
应用场景:这类函数非常常见,主要用于数据统计,在SQL优化方面,也适用。
例子:

1
2
3
4
5
6
7
复制代码mysql> select max(id) from t_base_user;
+---------+
| max(id) |
+---------+
|       2 |
+---------+
1 row in set (0.00 sec)

这里有个小技巧,如果主键是有序递增的,当需要有多少用户时,可以使用max(id)来代替count(*)函数。

9. distinct()
命令: distinct
作用:去重
应用场景:需要统计类型,状态,计算区分度时。
例子:

1
2
3
4
5
6
7
复制代码mysql> select count(distinct(name))/count(*) from t_base_user;
+--------------------------------+
| count(distinct(name))/count(*) |
+--------------------------------+
|                         0.6667 |
+--------------------------------+
1 row in set (0.00 sec)

上述为计算区分度的例子,如有不明白的,可以参考之前的文章《写会MySQL索引》

最后: 祝大家晚安!

相关阅读:

浅谈MySQL表结构设计

谈谈MySQL显示类型转换

说说 MySQL JSON 数据类型

浅谈MySQL Online DDL(下)

这里写图片描述

扫码关注,一起进步

个人博客: www.andyqian.com

本文转载自: 掘金

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

python +splinter自动刷新抢票

发表于 2018-01-02

一年一度的春运又来了,

今年我自己写了个抢票脚本。

python +splinter自动刷新抢票,可以成功抢到(依赖自己的网络环境太厉害,还有机器的好坏),

但是感觉不是很完美,

有大神请指导完善一下(或者有没有别的好点的思路)

不胜感谢

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
复制代码# -*- coding: utf-8 -*-
"""
@author: liuyw
"""
from splinter.browser import Browser
from time import sleep
import traceback
import time, sys


class huoche(object):
"""docstring for huoche"""
driver_name=''
executable_path=''
#用户名,密码
username = u"xxx@qq.com"
passwd = u"xxxx"
# cookies值得自己去找, 下面两个分别是上海, 太原南
starts = u"%u4E0A%u6D77%2CSHH"
ends = u"%u592A%u539F%2CTYV"
# 时间格式2018-01-19
dtime = u"2018-01-19"
# 车次,选择第几趟,0则从上之下依次点击
order = 0
###乘客名
users = [u"xxx",u"xxx"]
##席位
xb = u"二等座"
pz=u"成人票"

"""网址"""
ticket_url = "https://kyfw.12306.cn/otn/leftTicket/init"
login_url = "https://kyfw.12306.cn/otn/login/init"
initmy_url = "https://kyfw.12306.cn/otn/index/initMy12306"
buy="https://kyfw.12306.cn/otn/confirmPassenger/initDc"
login_url='https://kyfw.12306.cn/otn/login/init'
def __init__(self):
self.driver_name='chrome'
self.executable_path='/usr/local/bin/chromedriver'


def login(self):
self.driver.visit(self.login_url)
self.driver.fill("loginUserDTO.user_name", self.username)
# sleep(1)
self.driver.fill("userDTO.password", self.passwd)
print u"等待验证码,自行输入..."
while True:
if self.driver.url != self.initmy_url:
sleep(1)
else:
break

def start(self):
self.driver=Browser(driver_name=self.driver_name,executable_path=self.executable_path)
self.driver.driver.set_window_size(1400, 1000)
self.login()
# sleep(1)
self.driver.visit(self.ticket_url)
try:
print u"购票页面开始..."
# sleep(1)
# 加载查询信息
self.driver.cookies.add({"_jc_save_fromStation": self.starts})
self.driver.cookies.add({"_jc_save_toStation": self.ends})
self.driver.cookies.add({"_jc_save_fromDate": self.dtime})

self.driver.reload()

count=0
if self.order!=0:
while self.driver.url==self.ticket_url:
self.driver.find_by_text(u"查询").click()
count += 1
print u"循环点击查询... 第 %s 次" % count
# sleep(1)
try:
self.driver.find_by_text(u"预订")[self.order - 1].click()
except Exception as e:
print e
print u"还没开始预订"
continue
else:
while self.driver.url == self.ticket_url:
self.driver.find_by_text(u"查询").click()
count += 1
print u"循环点击查询... 第 %s 次" % count
# sleep(0.8)
try:
for i in self.driver.find_by_text(u"预订"):
i.click()
sleep(1)
except Exception as e:
print e
print u"还没开始预订 %s" %count
continue
print u"开始预订..."
# sleep(3)
# self.driver.reload()
sleep(1)
print u'开始选择用户...'
for user in self.users:
self.driver.find_by_text(user).last.click()

print u"提交订单..."
sleep(1)
# self.driver.find_by_text(self.pz).click()
# self.driver.find_by_id('').select(self.pz)
# # sleep(1)
# self.driver.find_by_text(self.xb).click()
# sleep(1)
self.driver.find_by_id('submitOrder_id').click()
# print u"开始选座..."
# self.driver.find_by_id('1D').last.click()
# self.driver.find_by_id('1F').last.click()

sleep(1.5)
print u"确认选座..."
self.driver.find_by_id('qr_submit_id').click()




except Exception as e:
print e






if __name__ == '__main__':
huoche=huoche()
huoche.start()

本文转载自: 掘金

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

在 Java 中使用 redis

发表于 2018-01-01

redis 支持很多语言的客户端。在官方网站上有支持的所有的 redis 客户端列表。

因为平时使用 java 作为开发语言,所以这里描述一下如何通过 java 来连接和操作 redis 服务器。在官方文档中, Java 推荐的 redis 客户端是 Jedis ,这里我们也用这个客户端对 redis 服务器进行操作。


引入依赖

首先我们建立一个 maven 工程,在工程的 pom.xml 文件中加入 Jedis 的依赖引用。为了方便测试,还加入了 Junit 依赖。文件内容如下。

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<project 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/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.x9710.common</groupId>
<artifactId>redis-util</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

创建连接类

建立 redis 连接类 com.x9710.common.redis.RedisConnection 。内容如下
package com.x9710.common.redis;

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
复制代码import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisConnection {
/**
* redis 连接池配置信息
*/
private JedisPoolConfig jedisPoolConfig;
/**
* redis 服务器地址
*/
private String ip;

/**
* redis 服务器端口
*/
private Integer port;

/**
* redis 服务器密码
*/
private String pwd;

/**
* redis 服务器连接超时时间
*/
private Integer timeOut;

/**
* redis 连接客户端名称
*/
private String clientName = null;

private JedisPool jedisPool;

public void setJedisPoolConfig(JedisPoolConfig jedisPoolConfig) {
this.jedisPoolConfig = jedisPoolConfig;
}

public void setIp(String ip) {
this.ip = ip;
}

public void setPort(Integer port) {
this.port = port;
}

public void setPwd(String pwd) {
this.pwd = pwd;
}

public void setTimeOut(Integer timeOut) {
this.timeOut = timeOut;
}

public void setClientName(String clientName) {
this.clientName = clientName;
}

private void buildConnection() {
if (jedisPool == null) {
if (jedisPoolConfig == null) {
jedisPool = new JedisPool(new JedisPoolConfig(), ip, port, timeOut, pwd, 0, clientName);
} else {
jedisPool = new JedisPool(jedisPoolConfig, ip, port, timeOut, pwd, 0, clientName);
}
}
}

public Jedis getJedis() {
buildConnection();
if (jedisPool != null) {
return jedisPool.getResource();
}
return null;
}

}


编写测试

用一个测试类 com.x9710.common.redis.test.RedisConnectionTest 来测试 rdis 连接功能.

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
复制代码package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;

public class RedisConnectionTest {
private RedisConnection redisConnection;

@Before
public void before() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//设置 redis 连接池最大连接数量
jedisPoolConfig.setMaxTotal(50);
//设置 redis 连接池最大空闲连接数量
jedisPoolConfig.setMaxIdle(10);
//设置 redis 连接池最小空闲连接数量
jedisPoolConfig.setMinIdle(1);
redisConnection = new RedisConnection();
redisConnection.setIp("10.110.2.56");
redisConnection.setPort(52981);
redisConnection.setPwd("hhSbcpotThgWdnxJNhrzwstSP20DvYOldkjf");
redisConnection.setClientName(Thread.currentThread().getName());
redisConnection.setTimeOut(600);
redisConnection.setJedisPoolConfig(jedisPoolConfig);
}

@Test
public void testPutGet() {
Jedis jedis = redisConnection.getJedis();
try {
jedis.select(1);
jedis.set("name","grace");
Assert.assertTrue("grace".equals(jedis.get("name")));
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}

在 ide 环境中执行测试用例,结果如下。

测试用例执行结果

现在,我们就在 Java 中利用 Jedit 客户端建立和 redis 的连接并且可以执行操作。对应的代码发布到了 GitHub 中

原文发表在简书中,原始链接

本文转载自: 掘金

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

5个酷毙的Python工具

发表于 2017-12-30

工欲善其事必先利其器,一个好的工具能让起到事半功倍的效果,Python社区提供了足够多的优秀工具来帮助开发者更方便的实现某些想法,下面这几个工具给我的工作也带来了很多便利,推荐给追求美好事物的你。

Python Tutor

Python Tutor 是由 Philip Guo 开发的一个免费教育工具,可帮助学生攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。通过这个工具,教师或学生可以直接在 Web 浏览器中编写 Python 代码,并逐步可视化地运行程序。如果你不知道代码在内存中是如何运行的,不妨把它拷贝到Tutor里可视化执行一遍,加深理解。

地址:www.pythontutor.com/

IPython

IPython 是一个 for Humans 的 Python 交互式 shell,用了它之后你就不想再用自带的 Python shell 了,IPython 支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多实用功能和函数,同时它也是科学计算和交互可视化的最佳平台。在公众号【Pyhon之禅】回复 “ipython” 获取《IPython交互式编程和数据可视化教程》。

地址:ipython.org/

Jupyter Notebook

Jupyter Notebook 就像一个草稿本,能将文本注释、数学方程、代码和可视化内容全部组合到一个易于共享的文档中,以 Web 页面的方式展示。它是数据分析、机器学习的必备工具。在公众号【Pyhon之禅】回复 “jupyter” 给你看一个基于 jupyter 写的 Python 教程。

地址:jupyter.org/

Anaconda

Python 虽好,可总是会遇到各种包管理和 Python 版本问题,特别是 Windows 平台很多包无法正常安装,为了解决这些问题,Anoconda 出现了,Anoconda 包含了一个包管理工具和一个Python管理环境,同时附带了一大批常用数据科学包,也是数据分析的标配。之前在公众号有介绍过 Anaconda

地址:www.anaconda.com/

Skulpt

Skulpt 是一个用 Javascript 实现的在线 Python 执行环境,它可以让你轻松在浏览器中运行 Python 代码。使用 skulpt 结合 CodeMirror 编辑器即可实现一个基本的在线Python编辑和运行环境。

地址:www.skulpt.org/

本文转载自: 掘金

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

C语言结构体内存布局问题

发表于 2017-12-29

引言

C语言结构体内存布局是一个老生常谈的问题,网上也看了一些资料,有些说的比较模糊,有些是错误的。本人借鉴了前人的文章,经过实践,总结了一些规则,如有错误,希望指正,不胜感激。

实际环境

  • 系统环境 macOS Sierra(10.12.4)
  • IDE Xcode(8.3)

概述

影响结构体内存布局有位域和**#pragma pack预处理宏**两个情况,下面分情况说明。

正常情况

结构体字节对齐的细节和具体的编译器实现相关,但一般来说遵循3个准则:

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小(sizeof)所整除。
  2. 结构体每个成员相对结构体首地址的偏移量offset都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
  3. 结构体的总大小sizeof为结构体最宽基本成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

下面的demo会为大家解释以上规则:

代码

1
2
3
4
5
复制代码struct student {
char name[5];
double weight;
int age;
};
1
2
3
4
5
复制代码struct school {
short age;
char name[7];
struct student lilei;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
struct student lilei = {"lilei",112.33,20};
printf("size of struct student: %lu\n",sizeof(lilei));
printf("address of student name: %u\n",lilei.name);
printf("address of student weight: %u\n",&lilei.weight);
printf("address of student age: %u\n",&lilei.age);

struct school shengli = {70,"shengli",lilei};
printf("size of struct school: %lu\n",sizeof(shengli));
printf("address of school age: %u\n",&shengli.age);
printf("address of school name: %u\n",shengli.name);
printf("address of school student: %u\n",&shengli.lilei);
}
return 0;
}

输出结果

解释规则

  1. 编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,做为结构体的首地址。(在本demo中struct school 包含 struct student,所以最宽的基本数据类型为double,sizeof(double)为8,1606416152/8 = 200802019,1606416112/8 = 200802014)。
  2. 为结构体的每一个成员开辟空间之前,编译器首先检查预开辟空间首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节(这也是为什么struct student weight成员的首地址是1606416160而不是1606416157,**但有很重要的一点要注意,这里的成员为基本数据类型,不包括char类型数组和结构体成员,char类型数组按1字节对齐,结构体成员存储的起始位置要从自身内部最大成员大小的整数倍地址开始存储,**比如struct a里有struct b成员,b里有char,int,double等成员,那b存储的起始位置应该从8的整数倍开始。通过struct school成员内存分布可以看出来,school.name的首地址是1606416114,而不是1606416119,school.student的首地址是1606416128,能被8整除,不能被24整除)。
  3. 结构体的总大小包括填充字节,最后一个成员出了满足上面两条之外,还必须满足第三条,否则必须在最后填充一定字节以满足要求(这也是为什么struct student占用字节数为24而不是20的原因)。

内存分布

student

school

扩展

细心的朋友可能发现&shengli.lilei(等效于shengli.lilei.name)的数值并不等于lilei.name,也就是说struct school shengli里的成员struct student lilei 和 struct student lilei并不是指向同一块内存空间,是值拷贝开辟的一块新的内存空间,也就是说struct是值类型而不是引用类型数据结构。还有通过内存地址可以发现两个结构体变量的内存空间是在内存栈上连续分配的。

位域

结构体使用位域的主要目的是压缩存储,位域成员不能单独被取sizeof值。C99规定int,unsigned int,bool可以作为位域类型,但编译器几乎都对此做了扩展,允许其它类型存在。结构体中含有位域字段,除了要遵循上面3个准则,还要遵循以下4个规则:

  1. 如果相邻位域字端的类型相同,且位宽之和小于类型的sizeof大小,则后一个字段将紧邻前一个字段存储,直到不能容纳为止。
  2. 如果相邻位域字段的类型相同,但位宽之和大于类型的sizeof大小,则后一个字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。
  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式。
  4. 如果位域字段之间穿插着非位域字段,则不进行压缩。

下面的demo会为大家解释以上规则:

代码

1
2
3
4
5
6
复制代码typedef struct A {
char f1:3;
char f2:4;
char f3:5;
char f4:4;
}a;
1
2
3
4
复制代码typedef struct B {
char f1:3;
short f2:13;
}b;
1
2
3
4
5
复制代码typedef struct C {
char f1:3;
char f2;
char f3:5;
}c;
1
2
3
4
5
6
复制代码typedef struct D {
char f1:3;
char :0;
char :4;
char f3:5;
}d;
1
2
3
复制代码typedef struct E {
int f1:3;
}e;
1
2
3
4
5
6
7
8
9
10
11
复制代码int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
printf("size of struct A: %lu\n",sizeof(a));
printf("size of struct B: %lu\n",sizeof(b));
printf("size of struct C: %lu\n",sizeof(c));
printf("size of struct D: %lu\n",sizeof(d));
printf("size of struct E: %lu\n",sizeof(e));
}
return 0;
}

输出结果

解释规则

  1. struct A中所有位域成员类型都为char,第一个字节只能容纳f1和f2,f3从下一个字节开始存储,第二个字节不能容纳f4,所以f4也要从下一个字节开始存储,因此sizeof(a)结果为3。
  2. struct B中位域成员类型不同,进行了压缩,因此sizeof(b)结果为2(不压缩方式没有进行验证,很抱歉)。
  3. struct C中位域成员之间有非位域类型成员,不进行压缩,因此sizeof(c)结果为3。
  4. struct D中有无名位域成员,char f1:3占3个bit,char :0移到下1个字节(移动单位和具体位域类型有关,short移到下2个字节,int移到下4个字节),char :4占4个bit,然后不能容纳char f3:5,所以要存到下1个字节,因此sizeof(d)结果为3。
  5. 可能有人会疑惑,为什么sizeof(e)结果为4,不应该是只占用1个字节么?不要忘了上面提到的准则3。

注意事项

  1. 位域的地址不能访问,因此不允许将&运算符用于位域。不能使用指向位域的指针也不能使用位域的数组(数组是种特殊指针)。
  2. 位域不能作为函数的返回结果。
  3. 位域以定义的类型为单位,且位域的长度不能超过所定义类型的长度。例如定义int a:33是不被允许的。
  4. 位域可以不指定位域名,但不能访问无名的位域。无名的位域只用做填充或调整位置,占位大小取决于该类型。例如char:0表示整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始存放,同理short:0和int:0分别代表整个位域向后推两个和四个字节。当空位域的长度为具体数值N时(例如 int:2),该变量仅用来占N位。

pragma pack预处理宏

编译器的#pragma pack指令也是用来调整结构体对齐方式的,不同编译器名称和用法略有不同。使用伪指令#pragma pack(n),编译器将按照n个字节对齐,其取值为1、2、4、8、16,默认是8,使用伪指令#pragma pack(),取消自定义字节对齐方式。如果设置#pragma pack(1),就是让结构体没有填充字节,实现空间“无缝存储”,这对跨平台传输数据来说是友好和兼容的。结构体中含有#pragma pack预处理宏,除了要遵循上面3个准则,还要遵循以下2个规则:

  1. 对于结构体成员存放的起始地址的偏移量,如果n大于等于该成员类型所占用的字节数,那么偏移量必须满足默认的对齐方式,如果n小于该成员类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。即是说,结构体成员的偏移量应该取二者的最小值,公式如下:

offsetof(item) = min(n, sizeof(item))
2. 对于结构体的总大小,如果n大于所有成员类型所占用的字节数,那么结构的总大小必须为占用空间最大成员占用空间数的倍数,否则必须为n的倍数。

用法

1
2
3
4
5
复制代码#pragma pack(push)  //packing stack入栈,设置当前对齐方式
#pragma pack(pop) //packing stack出栈,取消当前对齐方式
#pragma pack(n) //n=1,2,4,8,16保存当前对齐方式,设置按n字节对齐
#pragma pack() //等效于pack(pop)
#pragma pack(push,n)//等效于pack(push) + pack(n)

代码

1
2
3
4
5
6
7
8
9
复制代码#pragma pack(4)

typedef struct F {
int f1;
double f2;
char f3;
}f;

#pragma pack()
1
2
3
4
5
6
7
复制代码#pragma pack(16)

typedef struct G {
int f1;
double f2;
char f3;
}g;
1
2
3
4
5
6
7
8
复制代码int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
printf("size of struct D: %lu\n",sizeof(f));
printf("size of struct E: %lu\n",sizeof(g));
}
return 0;
}

输出结果

解释规则

  1. struct F设置的对齐方式为4,min(4, sizeof(int)) = 4,f1占4个字节,偏移量为0,min(4, sizeof(double)) = 4,f2占4个字节,偏移量为4,min(4, sizeof(char)) = 1,f3占1个字节,偏移量为12,最后整个结构体满足准则3,sizeof(f) = 16。
  2. struct G设置的对齐方式为16,比结构体中所有成员类型都要大,相当于没有生效,因此sizeof(f) = 24。

总结

位域和**#pragma pack预处理宏的结构体在遵循3个准则**的前提下,有自己的相应规则也要遵守。结构体成员在排列时数据类型要遵循从小到大排列,这样能尽可能的节省空间。

参考链接

blog.sina.cn/dpool/blog/…
c.biancheng.net/cpp/html/46…
hubingforever.blog.163.com/blog/static…

本文转载自: 掘金

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

Python & 机器学习之项目实践

发表于 2017-12-29

机器学习是一项经验技能,经验越多越好。在项目建立的过程中,实践是掌握机器学习的最佳手段。在实践过程中,通过实际操作加深对分类和回归问题的每一个步骤的理解,达到学习机器学习的目的。

预测模型项目模板

不能只通过阅读来掌握机器学习的技能,需要进行大量的练习。本文将介绍一个通用的机器学习的项目模板,创建这个模板总共有六个步骤。通过本文将学到:

  • 端到端地预测(分类与回归)模型的项目结构。
  • 如何将前面学到的内容引入到项目中。
  • 如何通过这个项目模板来得到一个高准确度的模板。

机器学习是针对数据进行自动挖掘,找出数据的内在规律,并应用这个规律来预测新数据,如图19-1所示。

图片描述

图19-1
在项目中实践机器学习

端到端地解决机器学习的问题是非常重要的。可以学习机器学习的知识,可以实践机器学习的某个方面,但是只有针对某一个问题,从问题定义开始到模型部署为止,通过实践机器学习的各个方面,才能真正掌握并应用机器学习来解决实际问题。

在部署一个项目时,全程参与到项目中可以更加深入地思考如何使用模型,以及勇于尝试用机器学习解决问题的各个方面,而不仅仅是参与到自己感兴趣或擅长的方面。一个很好的实践机器学习项目的方法是,使用从 UCI机器学习仓库(archive.ics.uci.edu/ml/datasets…) 获取的数据集开启一个机器学习项目。如果从一个数据集开始实践机器学习,应该如何将学到的所有技巧和方法整合到一起来处理机器学习的问题呢?

分类或回归模型的机器学习项目可以分成以下六个步骤:

(1)定义问题。
(2)理解数据。
(3)数据准备。
(4)评估算法。
(5)优化模型。
(6)结果部署。

有时这些步骤可能被合并或进一步分解,但通常是按上述六个步骤来开展机器学习项目的。为了符合Python的习惯,在下面的Python项目模板中,按照这六个步骤分解整个项目,在接下来的部分会明确各个步骤或子步骤中所要实现的功能。

机器学习项目的Python模板

下面会给出一个机器学习项目的Python模板。代码如下:

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
复制代码# Python机器学习项目的模板

# 1. 定义问题
# a) 导入类库
# b) 导入数据集

# 2. 理解数据
# a) 描述性统计
# b) 数据可视化

# 3. 数据准备
# a) 数据清洗
# b) 特征选择
# c) 数据转换

# 4. 评估算法
# a) 分离数据集
# b) 定义模型评估标准
# c) 算法审查
# d) 算法比较

# 5. 优化模型
# a) 算法调参
# b) 集成算法

# 6. 结果部署
# a) 预测评估数据集
# b) 利用整个数据集生成模型
# c) 序列化模型

当有新的机器学习项目时,新建一个Python文件,并将这个模板粘贴进去,再按照前面章节介绍的方法将其填充到每一个步骤中。

各步骤的详细说明

接下来将详细介绍项目模板的各个步骤。

步骤1:定义问题

主要是导入在机器学习项目中所需要的类库和数据集等,以便完成机器学习的项目,包括导入Python的类库、类和方法,以及导入数据。同时这也是所有的配置参数的配置模块。当数据集过大时,可以在这里对数据集进行瘦身处理,理想状态是可以在1分钟内,甚至是30秒内完成模型的建立或可视化数据集。

步骤2:理解数据

这是加强对数据理解的步骤,包括通过描述性统计来分析数据和通过可视化来观察数据。在这一步需要花费时间多问几个问题,设定假设条件并调查分析一下,这对模型的建立会有很大的帮助。

步骤3:数据准备

数据准备主要是预处理数据,以便让数据可以更好地展示问题,以及熟悉输入与输出结果的关系。包括:

  • 通过删除重复数据、标记错误数值,甚至标记错误的输入数据来清洗数据。
  • 特征选择,包括移除多余的特征属性和增加新的特征属性。
  • 数据转化,对数据尺度进行调整,或者调整数据的分布,以便更好地展示问题。

要不断地重复这个步骤和下一个步骤,直到找到足够准确的算法生成模型。

步骤4:评估算法

评估算法主要是为了寻找最佳的算法子集,包括:

  • 分离出评估数据集,以便于验证模型。
  • 定义模型评估标准,用来评估算法模型。
  • 抽样审查线性算法和非线性算法。
  • 比较算法的准确度。

在面对一个机器学习的问题的时候,需要花费大量的时间在评估算法和准备数据上,直到找到3~5种准确度足够的算法为止。

步骤5:优化模型

当得到一个准确度足够的算法列表后,要从中找出最合适的算法,通常有两种方法可以提高算法的准确度:

  • 对每一种算法进行调参,得到最佳结果。
  • 使用集合算法来提高算法模型的准确度。

步骤6:结果部署

一旦认为模型的准确度足够高,就可以将这个模型序列化,以便有新数据时使用该模型来预测数据。

  • 通过验证数据集来验证被优化过的模型。
  • 通过整个数据集来生成模型。
  • 将模型序列化,以便于预测新数据。

做到这一步的时候,就可以将模型展示并发布给相关人员。当有新数据产生时,就可以采用这个模型来预测新数据。

使用模板的小技巧

快速执行一遍:首先要快速地在项目中将模板中的每一个步骤执行一遍,这样会加强对项目每一部分的理解并给如何改进带来灵感。

循环:整个流程不是线性的,而是循环进行的,要花费大量的时间来重复各个步骤,尤其是步骤3或步骤4(或步骤3~步骤5),直到找到一个准确度足够的模型,或者达到预定的周期。

尝试每一个步骤:跳过某个步骤很简单,尤其是不熟悉、不擅长的步骤。坚持在这个模板的每一个步骤中做些工作,即使这些工作不能提高算法的准确度,但也许在后面的操作就可以改进并提高算法的准确度。即使觉得这个步骤不适用,也不要跳过这个步骤,而是减少该步骤所做的贡献。

定向准确度:机器学习项目的目标是得到一个准确度足够高的模型。每一个步骤都要为实现这个目标做出贡献。要确保每次改变都会给结果带来正向的影响,或者对其他的步骤带来正向的影响。在整个项目的每个步骤中,准确度只能向变好的方向移动。 按需适用:可以按照项目的需要来修改步骤,尤其是对模板中的各个步骤非常熟悉之后。需要把握的原则是,每一次改进都以提高算法模型的准确度为前提。

总结

本章介绍了预测模型项目的模板,这个模板适用于分类或回归问题。接下来将介绍机器学习中的一个回归问题的项目,这个项目比前面介绍的鸢尾花的例子更加复杂,会利用到本章介绍的每个步骤。

回归项目实例

机器学习是一项经验技能,实践是掌握机器学习、提高利用机器学习解决问题的能力的有效方法之一。那么如何通过机器学习来解决问题呢?本章将通过一个实例来一步一步地介绍一个回归问题。本章主要介绍以下内容:

  • 如何端到端地完成一个回归问题的模型。
  • 如何通过数据转换提高模型的准确度。
  • 如何通过调参提高模型的准确度。
  • 如何通过集成算法提高模型的准确度。

定义问题

在这个项目中将分析研究波士顿房价(Boston House Price)数据集,这个数据集中的每一行数据都是对波士顿周边或城镇房价的描述。数据是1978年统计收集的。数据中包含以下14个特征和506条数据(UCI机器学习仓库中的定义)。

  • CRIM:城镇人均犯罪率。
  • ZN:住宅用地所占比例。
  • INDUS:城镇中非住宅用地所占比例。
  • CHAS:CHAS虚拟变量,用于回归分析。
  • NOX:环保指数。
  • RM:每栋住宅的房间数。
  • AGE:1940年以前建成的自住单位的比例。
  • DIS:距离5个波士顿的就业中心的加权距离。
  • RAD:距离高速公路的便利指数。
  • TAX:每一万美元的不动产税率。
  • PRTATIO:城镇中的教师学生比例。
  • B:城镇中的黑人比例。
  • LSTAT:地区中有多少房东属于低收入人群。
  • MEDV:自住房屋房价中位数。

通过对这些特征属性的描述,我们可以发现输入的特征属性的度量单位是不统一的,也许需要对数据进行度量单位的调整。

导入数据

首先导入在项目中需要的类库。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码# 导入类库
import numpy as np
from numpy import arange
from matplotlib import pyplot
from pandas import read_csv
from pandas import set_option
from pandas.plotting import scatter_matrix
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import ElasticNet
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.svm import SVR
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestRegressor
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.ensemble import AdaBoostRegressor
from sklearn.metrics import mean_squared_error

接下来导入数据集到Python中,这个数据集也可以从UCI机器学习仓库下载,在导入数据集时还设定了数据属性特征的名字。代码如下:

1
2
3
4
5
复制代码# 导入数据
filename = 'housing.csv'
names = ['CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS',
'RAD', 'TAX', 'PRTATIO', 'B', 'LSTAT', 'MEDV']
data = read_csv(filename, names=names, delim_whitespace=True)

在这里对每一个特征属性设定了一个名称,以便于在后面的程序中使用它们。因为CSV文件是使用空格键做分隔符的,因此读入CSV文件时指定分隔符为空格键(delim_whitespace=True)。

理解数据

对导入的数据进行分析,便于构建合适的模型。

首先看一下数据维度,例如数据集中有多少条记录、有多少个数据特征。代码如下:

1
2
复制代码# 数据维度
print(dataset.shape)

执行之后我们可以看到总共有506条记录和14个特征属性,这与UCI提供的信息一致。

1
复制代码(506, 14)

再查看各个特征属性的字段类型。代码如下:

1
2
复制代码# 特征属性的字段类型
print(dataset.dtypes)

可以看到所有的特征属性都是数字,而且大部分特征属性都是浮点数,也有一部分特征属性是整数类型的。执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码CRIM       float64
ZN float64
INDUS float64
CHAS int64
NOX float64
RM float64
AGE float64
DIS float64
RAD int64
TAX float64
PRTATIO float64
B float64
LSTAT float64
MEDV float64
dtype: object

接下来对数据进行一次简单的查看,在这里我们查看一下最开始的30条记录。代码如下:

1
2
3
复制代码# 查看最开始的30条记录
set_option('display.line_width', 120)
print(dataset.head(30))

这里指定输出的宽度为120个字符,以确保将所有特征属性值显示在一行内。而且这些数据不是用相同的单位存储的,进行后面的操作时,也许需要将数据整理为相同的度量单位。执行结果如图20-1所示。

图片描述
图20-1

接下来看一下数据的描述性统计信息。代码如下:

1
2
3
复制代码# 描述性统计信息
set_option('precision', 1)
print(dataset.describe())

在描述性统计信息中包含数据的最大值、最小值、中位值、四分位值等,分析这些数据能够加深对数据分布、数据结构等的理解。结果如图20-2所示。

图片描述

图20-2
接下来看一下数据特征之间的两两关联关系,这里查看数据的皮尔逊相关系数。代码如下:

1
2
3
复制代码# 关联关系
set_option('precision', 2)
print(dataset.corr(method='pearson'))

执行结果如图20-3所示。

图片描述

图20-3
通过上面的结果可以看到,有些特征属性之间具有强关联关系(>0.7或<-0.7),如:

  • NOX与INDUS之间的皮尔逊相关系数是0.76。
  • DIS与INDUS之间的皮尔逊相关系数是-0.71。
  • TAX与INDUS之间的皮尔逊相关系数是0.72。
  • AGE与NOX之间的皮尔逊相关系数是0.73。
  • DIS与NOX之间的皮尔逊相关系数是-0.77。

数据可视化

单一特征图表

首先查看每一个数据特征单独的分布图,多查看几种不同的图表有助于发现更好的方法。我们可以通过查看各个数据特征的直方图,来感受一下数据的分布情况。代码如下:

1
2
3
复制代码# 直方图
dataset.hist(sharex=False, sharey=False, xlabelsize=1, ylabelsize=1)
pyplot.show()

执行结果如图20-4所示,从图中可以看到有些数据呈指数分布,如CRIM、ZN、AGE和B;有些数据特征呈双峰分布,如RAD和TAX。

图片描述

图20-4
通过密度图可以展示这些数据的特征属性,密度图比直方图更加平滑地展示了这些数据特征。代码如下:

1
2
3
复制代码# 密度图
dataset.plot(kind='density', subplots=True, layout=(4,4), sharex=False, fontsize=1)
pyplot.show()

在密度图中,指定layout=(4, 4),这说明要画一个四行四列的图形。执行结果如图20-5所示。

图片描述

图20-5
通过箱线图可以查看每一个数据特征的状况,也可以很方便地看出数据分布的偏态程度。代码如下:

1
2
3
复制代码#箱线图
dataset.plot(kind='box', subplots=True, layout=(4,4), sharex=False, sharey=False, fontsize=8)
pyplot.show()

执行结果如图20-6所示。

图片描述`

图20-6
多重数据图表

接下来利用多重数据图表来查看不同数据特征之间的相互影响关系。首先看一下散点矩阵图。代码如下:

1
2
3
复制代码# 散点矩阵图
scatter_matrix(dataset)
pyplot.show()

通过散点矩阵图可以看到,虽然有些数据特征之间的关联关系很强,但是这些数据分布结构也很好。即使不是线性分布结构,也是可以很方便进行预测的分布结构,执行结果如图20-7所示。

图片描述

图20-7
再看一下数据相互影响的相关矩阵图。代码如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码# 相关矩阵图
fig = pyplot.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(dataset.corr(), vmin=-1, vmax=1, interpolation='none')
fig.colorbar(cax)
ticks = np.arange(0, 14, 1)
ax.set_xticks(ticks)
ax.set_yticks(ticks)
ax.set_xticklabels(names)
ax.set_yticklabels(names)
pyplot.show()

执行结果如图20-8所示,根据图例可以看到,数据特征属性之间的两两相关性,有些属性之间是强相关的,建议在后续的处理中移除这些特征属性,以提高算法的准确度。

图片描述

图20-8
思路总结

通过数据的相关性和数据的分布等发现,数据集中的数据结构比较复杂,需要考虑对数据进行转换,以提高模型的准确度。可以尝试从以下几个方面对数据进行处理:

  • 通过特征选择来减少大部分相关性高的特征。
  • 通过标准化数据来降低不同数据度量单位带来的影响。
  • 通过正态化数据来降低不同的数据分布结构,以提高算法的准确度。

可以进一步查看数据的可能性分级(离散化),它可以帮助提高决策树算法的准确度。

分离评估数据集

分离出一个评估数据集是一个很好的主意,这样可以确保分离出的数据集与训练模型的数据集完全隔离,有助于最终判断和报告模型的准确度。在进行到项目的最后一步处理时,会使用这个评估数据集来确认模型的准确度。这里分离出20%的数据作为评估数据集,80%的数据作为训练数据集。代码如下:

1
2
3
4
5
6
7
复制代码# 分离数据集
array = dataset.values
X = array[:, 0:13]
Y = array[:, 13]
validation_size = 0.2
seed = 7
X_train, X_validation, Y_train, Y_validation = train_test_split(X, Y,test_size=validation_size, random_state=seed)

评估算法

评估算法——原始数据

分析完数据不能立刻选择出哪个算法对需要解决的问题最有效。我们直观上认为,由于部分数据的线性分布,线性回归算法和弹性网络回归算法对解决问题可能比较有效。另外,由于数据的离散化,通过决策树算法或支持向量机算法也许可以生成高准确度的模型。到这里,依然不清楚哪个算法会生成准确度最高的模型,因此需要设计一个评估框架来选择合适的算法。我们采用10折交叉验证来分离数据,通过均方误差来比较算法的准确度。均方误差越趋近于0,算法准确度越高。代码如下:

1
2
3
4
复制代码# 评估算法 —— 评估标准
num_folds = 10
seed = 7
scoring = 'neg_mean_squared_error'

对原始数据不做任何处理,对算法进行一个评估,形成一个算法的评估基准。这个基准值是对后续算法改善优劣比较的基准值。我们选择三个线性算法和三个非线性算法来进行比较。

线性算法:线性回归(LR)、套索回归(LASSO)和弹性网络回归(EN)。 非线性算法:分类与回归树(CART)、支持向量机(SVM)和K近邻算法(KNN)。

算法模型初始化的代码如下:

1
2
3
4
5
6
7
8
复制代码# 评估算法 - baseline
models = {}
models['LR'] = LinearRegression()
models['LASSO'] = Lasso()
models['EN'] = ElasticNet()
models['KNN'] = KNeighborsRegressor()
models['CART'] = DecisionTreeRegressor()
models['SVM'] = SVR()

对所有的算法使用默认参数,并比较算法的准确度,此处比较的是均方误差的均值和标准方差。代码如下:

1
2
3
4
5
6
7
复制代码# 评估算法
results = []
for key in models:
kfold = KFold(n_splits=num_folds, random_state=seed)
cv_result = cross_val_score(models[key], X_train, Y_train, cv=kfold, scoring=scoring)
results.append(cv_result)
print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))

从执行结果来看,线性回归(LR)具有最优的MSE,接下来是分类与回归树 (CART)算法。执行结果如下:

1
2
3
4
5
6
复制代码LR: -21.379856 (9.414264)
LASSO: -26.423561 (11.651110)
EN: -27.502259 (12.305022)
KNN: -41.896488 (13.901688)
CART: -26.608476 (12.250800)
SVM: -85.518342 (31.994798)

再查看所有的10折交叉分离验证的结果。代码如下:

1
2
3
4
5
6
7
复制代码#评估算法——箱线图
fig = pyplot.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(models.keys())
pyplot.show()

执行结果如图20-9所示,从图中可以看到,线性算法的分布比较类似,并且K近邻算法的结果分布非常紧凑。

图片描述

图20-9
不同的数据度量单位,也许是K近邻算法和支持向量机算法表现不佳的主要原因。下面将对数据进行正态化处理,再次比较算法的结果。

评估算法——正态化数据

在这里猜测也许因为原始数据中不同特征属性的度量单位不一样,导致有的算法的结果不是很好。接下来通过对数据进行正态化,再次评估这些算法。在这里对训练数据集进行数据转换处理,将所有的数据特征值转化成“0”为中位值、标准差为“1”的数据。对数据正态化时,为了防止数据泄露,采用Pipeline来正态化数据和对模型进行评估。为了与前面的结果进行比较,此处采用相同的评估框架来评估算法模型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码# 评估算法——正态化数据
pipelines = {}
pipelines['ScalerLR'] = Pipeline([('Scaler', StandardScaler()), ('LR', LinearRegression())])
pipelines['ScalerLASSO'] = Pipeline([('Scaler', StandardScaler()), ('LASSO', Lasso())])
pipelines['ScalerEN'] = Pipeline([('Scaler',
StandardScaler()), ('EN', ElasticNet())])
pipelines['ScalerKNN'] = Pipeline([('Scaler',
StandardScaler()), ('KNN', KNeighborsRegressor())])
pipelines['ScalerCART'] = Pipeline([('Scaler',
StandardScaler()), ('CART', DecisionTreeRegressor())])
pipelines['ScalerSVM'] = Pipeline([('Scaler',
StandardScaler()), ('SVM', SVR())])
results = []
for key in pipelines:
kfold = KFold(n_splits=num_folds, random_state=seed)
cv_result = cross_val_score(pipelines[key], X_train, Y_train, cv=kfold, scoring=scoring)
results.append(cv_result)
print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))

执行后发现K近邻算法具有最优的MSE。执行结果如下:

1
2
3
4
5
6
复制代码ScalerLR: -21.379856 (9.414264)
ScalerLASSO: -26.607314 (8.978761)
ScalerEN: -27.932372 (10.587490)
ScalerKNN: -20.107620 (12.376949)
ScalerCART: -26.978716 (12.164366)
ScalerSVM: -29.633086 (17.009186)

接下来看一下所有的10折交叉分离验证的结果。代码如下:

1
2
3
4
5
6
7
复制代码#评估算法——箱线图
fig = pyplot.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(models.keys())
pyplot.show()

执行结果,生成的箱线图如图20-10所示,可以看到K近邻算法具有最优的MSE和最紧凑的数据分布。

图片描述
调参改善算法

目前来看,K近邻算法对做过数据转换的数据集有很好的结果,但是是否可以进一步对结果做一些优化呢?K近邻算法的默认参数近邻个数(n_neighbors)是5,下面通过网格搜索算法来优化参数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码# 调参改善算法——KNN
scaler = StandardScaler().fit(X_train)
rescaledX = scaler.transform(X_train)
param_grid = {'n_neighbors': [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]}
model = KNeighborsRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model,
param_grid=param_grid, scoring=scoring, cv=kfold)
grid_result = grid.fit(X=rescaledX, y=Y_train)

print('最优:%s 使用%s' % (grid_result.best_score_, grid_result.best_params_))
cv_results =
zip(grid_result.cv_results_['mean_test_score'],

grid_result.cv_results_['std_test_score'],
grid_result.cv_results_['params'])
for mean, std, param in cv_results:
print('%f (%f) with %r' % (mean, std, param))

最优结果——K近邻算法的默认参数近邻个数(n_neighbors)是3。执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码最优:-18.1721369637 使用{'n_neighbors': 3}
-20.208663 (15.029652) with {'n_neighbors': 1}
-18.172137 (12.950570) with {'n_neighbors': 3}
-20.131163 (12.203697) with {'n_neighbors': 5}
-20.575845 (12.345886) with {'n_neighbors': 7}
-20.368264 (11.621738) with {'n_neighbors': 9}
-21.009204 (11.610012) with {'n_neighbors': 11}
-21.151809 (11.943318) with {'n_neighbors': 13}
-21.557400 (11.536339) with {'n_neighbors': 15}
-22.789938 (11.566861) with {'n_neighbors': 17}
-23.871873 (11.340389) with {'n_neighbors': 19}
-24.361362 (11.914786) with {'n_neighbors': 21}

集成算法

除调参之外,提高模型准确度的方法是使用集成算法。下面会对表现比较好的线性回归、K近邻、分类与回归树算法进行集成,来看看算法能否提高。

装袋算法:随机森林(RF)和极端随机树(ET)。 提升算法:AdaBoost(AB)和随机梯度上升(GBM)。

依然采用和前面同样的评估框架和正态化之后的数据来分析相关的算法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码# 集成算法
ensembles = {}
ensembles['ScaledAB'] = Pipeline([('Scaler',
StandardScaler()), ('AB', AdaBoostRegressor())])
ensembles['ScaledAB-KNN'] = Pipeline([('Scaler',
StandardScaler()), ('ABKNN', AdaBoostRegressor
(base_estimator= KNeighborsRegressor(n_neighbors=3)))])
ensembles['ScaledAB-LR'] = Pipeline([('Scaler',
StandardScaler()), ('ABLR',
AdaBoostRegressor(LinearRegression()))])
ensembles['ScaledRFR'] = Pipeline([('Scaler',
StandardScaler()), ('RFR', RandomForestRegressor())])
ensembles['ScaledETR'] = Pipeline([('Scaler',
StandardScaler()), ('ETR', ExtraTreesRegressor())])
ensembles['ScaledGBR'] = Pipeline([('Scaler',
StandardScaler()), ('RBR', GradientBoostingRegressor())])

results = []
for key in ensembles:
kfold = KFold(n_splits=num_folds, random_state=seed)
cv_result = cross_val_score(ensembles[key], X_train, Y_train, cv=kfold, scoring=scoring)
results.append(cv_result)
print('%s: %f (%f)' % (key, cv_result.mean(), cv_result.std()))

与前面的线性算法和非线性算法相比,这次的准确度都有了较大的提高。执行结果如下:

1
2
3
4
5
6
复制代码ScaledAB: -15.244803 (6.272186)
ScaledAB-KNN: -15.794844 (10.565933)
ScaledAB-LR: -24.108881 (10.165026)
ScaledRFR: -13.279674 (6.724465)
ScaledETR: -10.464980 (5.476443)
ScaledGBR: -10.256544 (4.605660)

接下来通过箱线图看一下集成算法在10折交叉验证中均方误差的分布状况。代码如下:

1
2
3
4
5
6
7
复制代码# 集成算法——箱线图
fig = pyplot.figure()
fig.suptitle('Algorithm Comparison')
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(ensembles.keys())
pyplot.show()

执行结果如图20-11所示,随机梯度上升算法和极端随机树算法具有较高的中位值和分布状况。

图片描述

图20-11
集成算法调参

集成算法都有一个参数n_estimators,这是一个很好的可以用来调整的参数。对于集成参数来说,n_estimators会带来更准确的结果,当然这也有一定的限度。下面对随机梯度上升(GBM)和极端随机树(ET)算法进行调参,再次比较这两个算法模型的准确度,来确定最终的算法模型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码# 集成算法GBM——调参
caler = StandardScaler().fit(X_train)
rescaledX = scaler.transform(X_train)
param_grid = {'n_estimators': [10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900]}
model = GradientBoostingRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model,
param_grid=param_grid, scoring=scoring, cv=kfold)
grid_result = grid.fit(X=rescaledX, y=Y_train)
print('最优:%s 使用%s' % (grid_result.best_score_,
grid_result.best_params_))

# 集成算法ET——调参
scaler = StandardScaler().fit(X_train)
rescaledX = scaler.transform(X_train)
param_grid = {'n_estimators': [5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]}
model = ExtraTreesRegressor()
kfold = KFold(n_splits=num_folds, random_state=seed)
grid = GridSearchCV(estimator=model, param_grid=param_grid, scoring=scoring, cv=kfold)
grid_result = grid.fit(X=rescaledX, y=Y_train)
print('最优:%s 使用%s' % (grid_result.best_score_, grid_result.best_params_))

对于随机梯度上升(GBM)算法来说,最优的n_estimators是500;对于极端随机树(ET)算法来说,最优的n_estimators是80。执行结果,极端随机树(ET)算法略优于随机梯度上升(GBM)算法,因此采用极端随机树(ET)算法来训练最终的模型。执行结果如下:

1
2
复制代码最优:-9.3078229754 使用{'n_estimators': 500}
最优:-8.99113433246 使用{'n_estimators': 80}

也许需要执行多次这个过程才能找到最优参数。这里有一个技巧,当最优参数是param_grid的边界值时,有必要调整param_grid进行下一次调参。

确定最终模型

我们已经确定了使用极端随机树(ET)算法来生成模型,下面就对该算法进行训练和生成模型,并计算模型的准确度。代码如下:

1
2
3
4
5
复制代码#训练模型
caler = StandardScaler().fit(X_train)
rescaledX = scaler.transform(X_train)
gbr = ExtraTreesRegressor(n_estimators=80)
gbr.fit(X=rescaledX, y=Y_train)

再通过评估数据集来评估算法的准确度。

1
2
3
4
复制代码# 评估算法模型
rescaledX_validation = scaler.transform(X_validation)
predictions = gbr.predict(rescaledX_validation)
print(mean_squared_error(Y_validation, predictions))

执行结果如下:

1
复制代码14.077038511

总结

本项目实例从问题定义开始,直到最后的模型生成为止,完成了一个完整的机器学习项目。通过这个项目,理解了上一章中介绍的机器学习项目的模板,以及整个机器学习模型建立的流程。接下来会介绍一个机器学习的二分类问题,以进一步加深对这个模板的理解。

编者按:《机器学习——Python实践》不同于很多讲解机器学习的书籍,本书以实践为导向,使用 scikit-learn 作为编程框架,强调简单、快速地建立模型,解决实际项目问题。读者通过对《机器学习——Python实践》的学习,可以迅速上手实践机器学习,并利用机器学习解决实际问题。《机器学习——Python实践》非常适合于项目经理、有意从事机器学习开发的程序员,以及高校相关专业在的读学生阅读。订购链接:item.jd.com/12252293.ht…

这里写图片描述

本文转载自: 掘金

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

死磕java底层(三)—反射、动态代理和注解

发表于 2017-12-28

1.反射介绍

1.1反射

反射是指程序可以访问,检测,修改它本身状态或行为的一种能力。

1.2java的反射机制

java的反射机制是指在程序运行状态中,给定任意一个类,都可以获取到这个类的属性和方法;给定任意一个对象都可以调用这个对象的属性和方法,这种动态的获取类的信息和调用对象的方法的功能称之为java的反射机制。
一言以蔽之:反射机制可以让你在程序运行时,拿到任意一个类的属性和方法并调用它。

1.3java反射的主要功能

  • 运行时构造一个类的对象;
  • 运行时获取一个类所具有的的成员变量和方法;
  • 运行时调用任意一个对象的方法;
  • 生成动态代理;
    其实反射最主要的功能我觉得是与框架搭配使用。

1.4java类类型

想要理解反射首先需要知道Class这个类,它的全称是java.lang.Class类。java是面向对象的语言,讲究万物皆对象,即使强大到一个类,它依然是另一个类(Class类)的对象,换句话说,普通类是Class类的对象,即Class是所有类的类(There is a class named Class)。
对于普通的对象,我们一般会这样创建:

1
复制代码Code code1 = new Code();

上面说了,所有的类都是Class的对象,那么如何表示呢,可不可以通过如下方式呢:

1
复制代码Class c = new Class();

但是我们查看Class的源码时,是这样写的:

1
2
3
复制代码private Class(ClassLoader loader) { 
classLoader = loader;
}

可以看到构造器是私有的,只有JVM才可以调用这个构造函数创建Class的对象,因此不可以像普通类一样new一个Class对象,虽然我们不能new一个Class对象,但是却可以通过已有的类得到一个Class对象,共有三种方式,如下:

1
2
3
复制代码Class c1 = Test.class; 这说明任何一个类都有一个隐含的静态成员变量class,这种方式是通过获取类的静态成员变量class得到的
Class c2 = test.getClass(); test是Test类的一个对象,这种方式是通过一个类的对象的getClass()方法获得的
Class c3 = Class.forName("com.catchu.me.reflect.Test"); 这种方法是Class类调用forName方法,通过一个类的全量限定名获得

这里,c1、c2、c3都是Class的对象,他们是完全一样的,而且有个学名,叫做Test的类类型(class type)。
这里就让人奇怪了,前面不是说Test是Class的对象吗,而c1、c2、c3也是Class的对象,那么Test和c1、c2、c3不就一样了吗?为什么还叫Test什么类类型?这里不要纠结于它们是否相同,只要理解类类型是干什么的就好了,顾名思义,类类型就是类的类型,也就是描述一个类是什么,都有哪些东西,所以我们可以通过类类型知道一个类的属性和方法,并且可以调用一个类的属性和方法,这就是反射的基础。
示例代码:

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

public static void main(String[] args) throws ClassNotFoundException {
Class<Test> class1 = Test.class;
System.out.println("类名1:"+class1.getName());

Test Test = new Test();
Class<? extends Test> class2 = Test.getClass();
System.out.println("类名2:"+class2.getName());

Class<?> class3 = Class.forName("com.catchu.me.reflect.Test");
System.out.println("类名3:"+class3.getName());
if(class1==class2){
System.out.println("class1==class2");
}
if(class1==class3){
System.out.println("class1==class3");
}
}
}

输出结果:

1
2
3
4
5
复制代码类名1:com.catchu.me.reflect.Test
类名2:com.catchu.me.reflect.Test
类名3:com.catchu.me.reflect.Test
class1==class2
class1==class3

2.反射的操作

java的反射操作主要是用到了java.lang.Class类和java.lang.reflect反射包下的类,上面说到我们已经可以拿到一个类的Class信息,根据这个Class我们就可以使用某些方法来操作(获取)类的以下信息:

2.1操作构造函数

万物皆对象,类的构造函数是java.lang.reflect.Constructor类的对象,通过Class的下列方法可以获取构造函数对象:

1
2
3
4
5
6
7
8
复制代码public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) //  获得该类所有的构造器,不包括其父类的构造器
public Constructor<T> getConstructor(Class<?>... parameterTypes) // 获得该类所有public构造器,包括父类

//具体
Constructor<?>[] allConstructors = class1.getDeclaredConstructors();//获取class对象的所有声明构造函数
Constructor<?>[] publicConstructors = class1.getConstructors();//获取class对象public构造函数
Constructor<?> constructor = class1.getDeclaredConstructor(String.class);//获取指定声明构造函数(局部变量是一个字符串类型的)
Constructor publicConstructor = class1.getConstructor(String.class);//获取指定声明的public构造函数

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public class TestConstructor {
public static void main(String[] args) throws Exception{
Class<?> personClass = Class.forName("com.catchu.me.reflect.Person");
//获取所有的构造函数,包括私有的,不包括父类的
Constructor<?>[] allConstructors = personClass.getDeclaredConstructors();
//获取所有公有的构造函数,包括父类的
Constructor<?>[] publicConstructors = personClass.getConstructors();
System.out.println("遍历之后的构造函数:");
for(Constructor c1 : allConstructors){
System.out.println(c1);
}

Constructor<?> c2 = personClass.getDeclaredConstructor(String.class);
c2.setAccessible(true); //设置是否可访问,因为该构造器是private的,所以要手动设置允许访问,如果构造器是public的就不用设置
Object person = c2.newInstance("刘俊重"); //使用反射创建Person类的对象,并传入参数
System.out.println(person.toString());
}
}

Person类如下,为测出效果包含一个私有构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class Person {
private int age;
private String name;
public Person() {
}
private Person(String name){
this.name = name;
}
public Person(int age,String name){
this.age = age;
this.name = name;
}
//省略set/get方法
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}

测试结果如下:

1
2
3
4
5
复制代码遍历之后的构造函数:
public com.catchu.me.reflect.Person(int,java.lang.String)
private com.catchu.me.reflect.Person(java.lang.String)
public com.catchu.me.reflect.Person()
Person{age=0, name='刘俊重'}

由上面可以看到我们在获得某个类的Class类类型之后,可以通过反射包中的方法获取到这个类的构造函数,进而可以创建该类的对象。

2.2操作成员变量

万物皆对象,类的成员变量是java.lang.reflect.Field类的对象,通过Class类的以下方法可以获取某个类的成员变量,值得一提的是变量是包含两部分的,变量类型和变量名:

1
2
3
4
5
6
7
8
复制代码public Field getDeclaredField(String name) // 获得该类自身声明的所有变量,不包括其父类的变量
public Field getField(String name) // 获得该类自所有的public成员变量,包括其父类变量

//具体实现
Field[] allFields = class1.getDeclaredFields();//获取class对象的所有属性
Field[] publicFields = class1.getFields();//获取class对象的public属性
Field ageField = class1.getDeclaredField("age");//获取class指定属性
Field desField = class1.getField("des");//获取class指定的public属性

示例代码如下:

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
复制代码public class TestField {
public static void main(String[] args) throws Exception{
Class<Person> personClass = Person.class;
//获取所有的成员变量,包含私有的
Field[] allFields = personClass.getDeclaredFields();
//获取所有公有的成员变量,包含父类的
Field[] publicFields = personClass.getFields();
System.out.println("所有的成员变量:");
for(Field f : allFields){
System.out.println(f);
}
//获取某个变量的值
//创建对象的实例
Constructor<Person> c = personClass.getDeclaredConstructor(String.class);
c.setAccessible(true); //因为该构造函数时私有的,需要在这里设置成可访问的
Person person = c.newInstance("刘俊重");
//获取变量name对象
Field field = personClass.getDeclaredField("name");
field.setAccessible(true); //因为变量name是私有的,需要在这里设置成可访问的
//注意对比下面这两行,官方对field.get(Object obj)方法的解释是返回对象obj字段field的值
Object value = field.get(person);
//String name = person.getName();
System.out.println("获取的变量的值是:"+value);
}
}

输出结果如下:

1
2
3
4
复制代码所有的成员变量:
private int com.catchu.me.reflect.Person.age
private java.lang.String com.catchu.me.reflect.Person.name
获取的变量的值是:刘俊重

这里要注意field.get(person)方法,我们根据对象获取属性的常规方法是通过:String name = person.getName(),反射中可以通过:字段.get(对象),这也是获取对象的某个字段,有点类似于invoke方法。

2.3操作成员方法

万物皆对象,类的成员方法是java.lang.reflect.Method的对象,通过java.lang.Class类的以下方法可以获取到类的成员方法,通过方法类Method提供的一些方法,又可以调用获取到的成员方法。

1
2
3
4
5
6
7
8
复制代码public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到该类所有的方法,不包括父类的 
public Method getMethod(String name, Class<?>... parameterTypes) // 得到该类所有的public方法,包括父类的

//具体使用
Method[] methods = class1.getDeclaredMethods();//获取class对象的所有声明方法
Method[] allMethods = class1.getMethods();//获取class对象的所有public方法 包括父类的方法
Method method = class1.getMethod("info", String.class);//返回此class1对应的public修饰的方法名是info的,包含一个String类型变量的方法
Method declaredMethod = class1.getDeclaredMethod("info", String.class);//返回此Class对象对应类的、带指定形参列表的方法

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public class TestMethod {
public static void main(String[] args) throws Exception {
Person person = new Person();
Class<? extends Person> personClass = person.getClass();
Method[] allMethods = personClass.getDeclaredMethods();
Method[] publicMethods = personClass.getMethods();
System.out.println("遍历所有的方法:");
for(Method m : allMethods){
System.out.println(m);
}

//下面是测试通过反射调用函数
//通过反射创建实例对象,默认调无参构造函数
Person person2 = personClass.newInstance();
//获取要调用的方法,要调用study方法,包含int和String参数,注意int和Integer在这有区别
Method method = personClass.getMethod("study", int.class, String.class);
Object o = method.invoke(person2, 18, "刘俊重");
}
}

测试结果:

1
2
3
4
5
6
7
8
复制代码遍历所有的方法:
public java.lang.String com.catchu.me.reflect.Person.toString()
public java.lang.String com.catchu.me.reflect.Person.getName()
public void com.catchu.me.reflect.Person.setName(java.lang.String)
public void com.catchu.me.reflect.Person.study(int,java.lang.String)
public int com.catchu.me.reflect.Person.getAge()
public void com.catchu.me.reflect.Person.setAge(int)
我叫刘俊重,我今年18,我在学习反射

注意:Object o = method.invoke(person2, 18, “刘俊重”);就是调用person2对象的method方法,格式是:方法名.invoke(对象,参数),类似于获取成员变量值时的get方法。
由上面可以看出反射的强大:通过反射我们可以获取到类类型,通过Class类型我们可以获取到构造函数,进而实例化new出一个对象;通过反射我们可以获取到成员变量和成员方法,通过实例出的对象又可以获取到这些成员变量的值或调用成员方法。这才只是反射的一部分,通过反射我们还可以判断类,变量,方法,是否包含某些特定注解,还可以通过反射来动态代理去调用其它方法,跟注解和动态代理挂起勾会有无限的想象空间,比如spring框架,底层就是通过这些原理。下面在说几个反射常用的API,最后会介绍反射跟注解和动态代理的结合使用。

2.4其它方法

  • 注解中常用的方法:
1
2
3
4
复制代码Annotation[] annotations = (Annotation[]) class1.getAnnotations();//获取class对象的所有注解 
Annotation annotation = (Annotation) class1.getAnnotation(Deprecated.class);//获取class对象指定注解
Type genericSuperclass = class1.getGenericSuperclass();//获取class对象的直接超类的
Type Type[] interfaceTypes = class1.getGenericInterfaces();//获取class对象的所有接口的type集合
  • 获取Class对象其它信息的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码boolean isPrimitive = class1.isPrimitive();//判断是否是基础类型 
boolean isArray = class1.isArray();//判断是否是集合类
boolean isAnnotation = class1.isAnnotation();//判断是否是注解类
boolean isInterface = class1.isInterface();//判断是否是接口类
boolean isEnum = class1.isEnum();//判断是否是枚举类
boolean isAnonymousClass = class1.isAnonymousClass();//判断是否是匿名内部类
boolean isAnnotationPresent = class1.isAnnotationPresent(Deprecated.class);//判断是否被某个注解类修饰
String className = class1.getName();//获取class名字 包含包名路径
Package aPackage = class1.getPackage();//获取class的包信息
String simpleName = class1.getSimpleName();//获取class类名
int modifiers = class1.getModifiers();//获取class访问权限
Class<?>[] declaredClasses = class1.getDeclaredClasses();//内部类
Class<?> declaringClass = class1.getDeclaringClass();//外部类
ClassLoader ClassLoader = class1.getClassLoader() 返回类加载器

getSuperclass():获取某类所有的父类
getInterfaces():获取某类所有实现的接口

3.动态代理

代理的操作是通过java.lang.reflect.Proxy 类中实现的,通过Proxy的newProxyInstance()方法可以创建一个代理对象,如下:

1
复制代码public static Object newProxyInstance(ClassLoader loader,类<?>[] interfaces,InvocationHandler h)

不要看到这里面一大坨晦涩的屎代码就害怕,这里面是有技巧的,其实都是模板,需要什么,我们传什么过去就可以了。可以看到需要三个参数,类加载器,接口和调用处理者。我们在上面已经能拿到Class类了,使用class.getClassLoader就可以获取类加载器,使用class.getgetInterfaces()可以获取所有的接口,那现在要写的不就是新建一个InvocationHandler对象了吗?事实上,我们动态代理的核心代码也就是在这里面写的。我上面说的模板,其实就是下面这几步:

  1. 书写代理类和代理方法,在代理方法中实现代理Proxy.newProxyInstance();
  2. 代理中需要的参数分别为:被代理的类的类加载器class.getClassLoader(),被代理类的所有实现接口new Class[] { Interface.class },句柄方法new InvocationHandler();
  3. 在句柄方法中重写invoke方法,invoke方法的输入有3个参数Object proxy(代理类对象), Method method(被代理类的方法),Object[] args(被代理类方法的传入参数),在这个方法中,我们可以定制化的写我们的业务;
  4. 获取代理类,强转成被代理的接口;
  5. 最后,我们可以像没被代理一样,调用接口的任何方法,方法被调用后,方法名和参数列表将被传入代理类的invoke方法中,进行新业务的逻辑流程。
    看下面的示例代码:
    接口PersonInterface:
1
2
3
4
复制代码public interface PersonInterface {
void doSomething();
void saySomething();
}

接口的实现类:

1
2
3
4
5
6
7
8
9
10
复制代码public class PersonImpl implements PersonInterface {
@Override
public void doSomething() {
System.out.println("人类在做事");
}
@Override
public void saySomething() {
System.out.println("人类在说话");
}
}

代理类:

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
复制代码/**
* @author 刘俊重
*/
public class PersonProxy {
public static void main(String[] args) {
final PersonImpl person = new PersonImpl();
PersonInterface proxyPerson = (PersonInterface) Proxy.newProxyInstance(PersonImpl.class.getClassLoader(),
PersonImpl.class.getInterfaces(), new InvocationHandler() {
//在下面的invoke方法里面写我们的业务
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName()=="doSomething"){
person.doSomething();
System.out.println("通过常规方法调用了实现类");
}else{
method.invoke(person,args);
System.out.println("通过反射机制调用了实现类");
}
return null;
}
});
proxyPerson.doSomething();
proxyPerson.saySomething();
}
}

执行结果如下:

1
2
3
4
复制代码人类在做事
通过常规方法调用了实现类
人类在说话
通过反射机制调用了实现类

在我们通过proxyPerson.doSomething()调用的时候,其实不是立马进入实现类的doSomething方法,而是带着方法名,参数进入到了我们的代理方法invoke里面,在这里面我进行了一次判断,如果等于”doSomething”就使用常规方法调用,否则使用反射的方法调用。这样看似还是平时的调用,但是每次执行都要走我们的代理方法里面,我们可以在这里面做些“手脚”,加入我们的业务处理。
可以看下面另一个示例,比如天猫一件衣服正常卖50,现在你是我的vip用户,可以给你打折扣10块,其它业务都是相同的,只有这里便宜了10,重新服务提供者就很麻烦,用代理可以解决这个问题。
接口SaleService:

1
2
3
4
复制代码public interface SaleService {
//根据尺码返回衣服的大小
int clothes(String size);
}

接口实现类SaleServiceImpl:

1
2
3
4
5
6
7
8
复制代码public class SaleServiceImpl implements SaleService {
@Override
public int clothes(String size) {
System.out.println("衣服大小"+size);
//模拟从数据库取衣服价格
return 50;
}
}

普通无折扣的调用测试:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码/**
* @author 刘俊重
* @Description 普通用户
*/
public class OrdinaryCustom {
public static void main(String[] args) {
SaleService saleService = new SaleServiceImpl();
int money = saleService.clothes("XXl");
System.out.println("价格是:"+money);

}
}

输出结果:

1
2
复制代码衣服大小XXl
价格是:50

代理类ProxySale:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码/**
* @author 刘俊重
* @Description 代理类
*/
public class ProxySale {
//对接口方法进行代理
public static <T> T getProxy(final int discount, final Class<SaleServiceImpl> implementClasses, Class<SaleService> interfaceClasses){
return (T)Proxy.newProxyInstance(implementClasses.getClassLoader(),
implementClasses.getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//调用原始对象的方法,获取未打折之前的价格
int price = (int) method.invoke(implementClasses.newInstance(), args);
return price-discount;
}

});
}
}

vip用户测试类VipCustom:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码/**
* @author 刘俊重
* @Description Vip用户,有打折优惠
*/
public class VipCustom {
public static void main(String[] args) {
//vip用户,打10元折扣
int discount = 10;
SaleService saleService = ProxySale.getProxy(discount, SaleServiceImpl.class, SaleService.class);
int money = saleService.clothes("xxl");
System.out.println("价格是:"+money);
}
}

输出结果是:

1
2
复制代码衣服大小xxl
价格是:40

可以看到,在未修改服务提供者的情况下,我们在代理类里面做了手脚,结果符合预期。

4.注解

4.1概念及作用

  1. 概念
  • 注解即元数据,就是源代码的元数据
  • 注解在代码中添加信息提供了一种形式化的方法,可以在后续中更方便的 使用这些数据
  • Annotation是一种应用于类、方法、参数、变量、构造器及包声明中的特殊修饰符。它是一种由JSR-175标准选择用来描述元数据的一种工具。
  1. 作用
  • 生成文档
  • 跟踪代码依赖性,实现替代配置文件功能,减少配置。如Spring中的一些注解
  • 在编译时进行格式检查,如@Override等
  • 每当你创建描述符性质的类或者接口时,一旦其中包含重复性的工作,就可以考虑使用注解来简化与自动化该过程。

4.2java注解

  • 什么是java注解?
    在java语法中,使用@符号作为开头,并在@后面紧跟注解名。被运用于类,接口,方法和字段之上,java中的注解包是java.lang.annotation,例如:
1
2
3
4
复制代码@Override
void myMethod() {
......
}

这其中@Override就是注解。这个注解的作用也就是告诉编译器,myMethod()方法覆盖了父类中的myMethod()方法。

  • java中内置的注解
    java中有三个内置的注解:
1
2
3
复制代码@Override:表示当前的方法定义将覆盖超类中的方法,如果出现错误,编译器就会报错。
@Deprecated:如果使用此注解,编译器会出现警告信息。
@SuppressWarnings:忽略编译器的警告信息。
  • 元注解
    元注解的作用就是负责注解其他注解(自定义注解的时候用到的)。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
    Java5.0定义的4个元注解:
1
2
3
4
复制代码@Target
@Retention
@Documented
@Inherited

java8加了两个新注解,后续我会讲到。

  • @Target

@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:

类型 用途
CONSTRUCTOR 用于描述构造器
FIELD 用于描述域
LOCAL_VARIABLE 用于描述局部变量
METHOD 用于描述方法
PACKAGE 用于描述包
PARAMETER 用于描述参数
TYPE 用于描述类、接口(包括注解类型) 或enum声明

比如定义下面一个注解,它就只能用在方法上,因为已经限定了它是方法级别的注解,如果用在类或者其它上面,编译阶段就会报错:

1
2
3
复制代码@Target({ElementType.METHOD})
public @interface MyMethodAnnotation {
}

测试类MyClass:

1
2
3
4
5
6
7
复制代码//@MyMethodAnnotation  报错,方法级别注解不能注在类头上
public class MyClass {
@MyMethodAnnotation
public void myTestMethod(){
//
}
}
  • @Retention

@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。

作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:

类型 用途 说明
SOURCE 在源文件中有效(即源文件保留) 仅出现在源代码中,而被编译器丢弃
CLASS 在class文件中有效(即class保留) 被编译在class文件中
RUNTIME 在运行时有效(即运行时保留) 编译在class文件中

示例:

1
2
3
4
5
复制代码@Target({ElementType.TYPE})  //用在描述类、接口或enum
@Retention(RetentionPolicy.RUNTIME) //运行时有效
public @interface MyClassAnnotation {
String value(); //这个MyClassAnnotation注解有个value属性,将来可以设置/获取值
}
  • @Documented

@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

作用:将注解包含在javadoc中

1
2
3
复制代码java.lang.annotation.Documented
@Documented
public @interface MyCustomAnnotation { //Annotation body}
  • @Inherited

是一个标记注解,阐述了某个被标注的类型是被继承的,使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类,@Inherited annotation类型是被标注过的class的子类所继承。类并不从实现的接口继承annotation,方法不从它所重载的方法继承annotation,当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

作用:允许子类继承父类中的注解
示例,这里的MyParentClass 使用的注解标注了@Inherited,所以子类可以继承这个注解信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码java.lang.annotation.Inherited
@Inherited
public @interface MyCustomAnnotation {
}

@MyCustomAnnotation
public class MyParentClass {
...
}

public class MyChildClass extends MyParentClass {
...
}

4.3自定义注解

格式

1
2
3
复制代码public @interface 注解名{
定义体
}
注解参数的可支持数据类型:
  • 所有基本数据类型(int,float,double,boolean,byte,char,long,short)
  • String 类型
  • Class类型
  • enum类型
  • Annotation类型
  • 以上所有类型的数组
规则
  • 修饰符只能是public 或默认(default)
  • 参数成员只能用基本类型byte,short,int,long,float,double,boolean八种基本类型和String,Enum,Class,annotations及这些类型的数组
  • 如果只有一个参数成员,最好将名称设为”value”
  • 注解元素必须有确定的值,可以在注解中定义默认值,也可以使用注解时指定,非基本类型的值不可为null,常使用空字符串或0作默认值
  • 在表现一个元素存在或缺失的状态时,定义一下特殊值来表示,如空字符串或负值

示例:

1
2
3
4
5
6
7
8
复制代码@Target(ElementType.FIELD)
@Retention(value=RetentionPolicy.RUNTIME)
@Documented
public @interface MyFieldAnnotation {
int id() default 0;

String name() default "";
}

定义了一个用在字段上的,运行时有效的名为MyFieldAnnotation的注解,它有两个属性,int类型的id(id后面记得带括号)默认值是0,还有一个String类型的name,默认值是””。

4.4注解的操作—注解处理类库

在上面我们已经知道了怎么自定义一个注解了,但是光定义没用啊,重要的是我要使用它,使用的方法也很简单,最上面讲反射的时候也提到过几个这样的方法了,比如:class1.isAnnotation(),其实他们统统是java.lang.reflect包下的AnnotatedElement接口里面的方法,这个接口主要有以下几个实现类:

  • Class:类定义
  • Constructor:构造器定义
  • Field:累的成员变量定义
  • Method:类的方法定义
  • Package:类的包定义
    java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
    AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:
  1. 方法1: T getAnnotation(Class annotationClass): 返回程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
  2. 方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
  3. 方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
  4. 方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

总结:定义注解用的是java.lang.annotation.Annotation,操作注解是用java.lang.reflect.AnnotatedElement

测试代码CustomClassAnnotation如下:

1
2
3
4
5
6
7
8
9
10
复制代码/**
* @author 刘俊重
* @Description 自定义类注解
*/
@Target(ElementType.TYPE) //作用在类,枚举或接口上
@Retention(RetentionPolicy.RUNTIME) //运行时有效
@Documented //文档可见
public @interface CustomClassAnnotation {
String value(); //获取注解名称
}

FruitName类如下:

1
2
3
4
5
6
7
8
9
10
复制代码/**
* @author 刘俊重
* @Description 字段注解(字符串类型的)
*/
@Target(ElementType.FIELD) //用在字段上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
String name() default "";
}

FruitColor类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码/**
* @author 刘俊重
* @Description 字段注解(枚举类型的)
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
//颜色枚举
enum Color{BLUE,RED,GREEN};

//颜色属性
Color color() default Color.RED;
}

Fruit实体类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码/**
* @author 刘俊重
* @Description Fruit实体类
*/
@CustomClassAnnotation(value="fruit")
public class Fruit{

@FruitName(name="apple")
private String name;

@FruitColor(color= FruitColor.Color.RED)
private String color;

}

测试类TestAnnotation如下:

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
复制代码/**
* @author 刘俊重
* @Description 测试类
*/
public class TestAnnotation {
public static void main(String[] args) {
Class<Fruit> clazz = Fruit.class; //反射获取Class对象
CustomClassAnnotation annotation = clazz.getAnnotation(CustomClassAnnotation.class); //拿到Fruit类的注解
if(null!=annotation && "fruit".equals(annotation.value())){
System.out.println("Fruit类的注解名是======"+annotation.value());
//获取所有的属性遍历,拿到每一个属性的值
Field[] allFields = clazz.getDeclaredFields();
for(Field field : allFields){
if(field.isAnnotationPresent(FruitName.class)){
//判断是否存在FruitName注解
FruitName fruitName = field.getAnnotation(FruitName.class);
System.out.println("水果名称====="+fruitName.name());
}
if(field.isAnnotationPresent(FruitColor.class)){
FruitColor fruitColor = field.getAnnotation(FruitColor.class);
System.out.println("水果颜色====="+fruitColor.color());
}
}
}else{
System.out.println("注解值不对,请检查");
}
}
}

总结:通过注解可以获取到类名,接口名,方法名,属性名,再搭配反射可以动态的生成对象,再搭配动态代理,去动态的去调用某些方法,这基本上也就是spring框架底层实现原理的一部分了。
注:本文一些内容引用自http://t.cn/RK8ci8w ,原作者反射和注解写的确实不错,我对部分内容进行了完善并重写了部分示例;动态代理部分我加的,考虑着把反射,代理,注解放在一块来说。

附一下个人微信公众号,欢迎跟我交流。

java开发日记

本文转载自: 掘金

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

布隆过滤器 Google Guava类库源码分析及基于Red

发表于 2017-12-28

一、背景知识

在网上已经有很多关于布隆过滤器的介绍了,这里就不再赘述,下面简单地提炼几个要点:

  1. 布隆过滤器是用来判断一个元素是否出现在给定集合中的重要工具,具有快速,比哈希表更节省空间等优点,而缺点在于有一定的误识别率(false-positive,假阳性),亦即,它可能会把不是集合内的元素判定为存在于集合内,不过这样的概率相当小,在大部分的生产环境中是可以接受的;
  2. 其原理比较简单,如下图所示,S集合中有n个元素,利用k个哈希函数,将S中的每个元素映射到一个长度为m的位(bit)数组B中不同的位置上,这些位置上的二进制数均置为1,如果待检测的元素经过这k个哈希函数的映射后,发现其k个位置上的二进制数不全是1,那么这个元素一定不在集合S中,反之,该元素可能是S中的某一个元素(参考1);
  3. 综上描述,那么到底需要多少个哈希函数,以及创建长度为多少的bit数组比较合适,为了估算出k和m的值,在构造一个布隆过滤器时,需要传入两个参数,即可以接受的误判率fpp和元素总个数n(不一定完全精确)。至于参数估计的方法,有兴趣的同学可以参考维基英文页面,下面直接给出公式:
  4. 哈希函数的要求尽量满足平均分布,这样既降低误判发生的概率,又可以充分利用bit数组的空间;
  5. 根据论文《Less Hashing, Same Performance: Building a Better Bloom Filter》提出的一个技巧,可以用2个哈希函数来模拟k个哈希函数,即gi(x) = h1(x) + ih2(x) ,其中0<=i<=k-1;
  6. 在吴军博士的《数学之美》一书中展示了不同情况下的误判率,例如,假定一个元素用16位比特,8个哈希函数,那么假阳性的概率是万分之五,这已经相当小了。

首先我们来了解一下一些已经有相应实现的开源类库,如Google的Guava类库,Twitter的Algebird类库,和ScalaNLP breeze等等,其中Guava 11.0版本中增加了BloomFilter类,它使用了Funnel和Sink的设计,增强了泛化的能力,使其可以支持任何数据类型,其利用murmur3 hash来做哈希映射函数,不过它底层并没有使用传统的java.util.BitSet来做bit数组,而是用long型数组进行了重新封装,大部分操作均基于位的运算,因此能达到一个非常好的性能;Algebird是基于Scala开发的类库,其实现的布隆过滤器仅支持字符串作为元素类型,同样也采用了Murmur
hash3,底层则是采用一种叫“EWAHCompressedBitmap”的数据结构,它专门为内存占用而优化,适合稀疏数据的场景,是一种可以替代BitSet的实现,不过缺点是随机访问的性能比较差;breeze是ScalaNLP下的核心类库,包括线性代数,数值计算和优化,是一个强大的机器学习工具,它实现的布隆过滤器没有使用通用的Murmur hash3算法,而是直接使用了对象的hash值,bit数组则是采用了BitSet,但是和Scala的通病一样,当编译成Java代码时分配了过多的对象,这会在内存占用上带来额外的开销。

下面我们就Guava类库中实现布隆过滤器的源码作详细分析,最后出于灵活性和解耦等因素的考虑,我们想要把布隆过滤器从JVM中拿出来,于是利用了Redis自带的Bitmaps作为底层的bit数组进行重构,另外随着插入的元素越来越多,当实际数量远远大于创建时设置的预计数量时,布隆过滤器的误判率会越来越高,因此在重构的过程中增加了自动扩容的特性,最后通过测试验证其正确性。

二、布隆过滤器在Guava中的实现

Guava中,布隆过滤器的实现主要涉及到2个类,BloomFilter和BloomFilterStrategies,首先来看一下BloomFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
php复制代码 /** The bit set of the BloomFilter (not necessarily power of 2!) */
  private final BitArray bits;
 
  /** Number of hashes per element */
  private final int numHashFunctions;
 
  /** The funnel to translate Ts to bytes */
  private final Funnel<? super T> funnel;
 
  /**
   * The strategy we employ to map an element T to {@code numHashFunctions} bit indexes.
   */
  private final Strategy strategy;

这是它的4个成员变量

  • BitArrays是定义在BloomFilterStrategies中的内部类,封装了布隆过滤器底层bit数组的操作,后文详述;
  • numHashFunctions表示哈希函数的个数,即上文提到的k;
  • Funnel,这是Guava中定义的一个接口,它和PrimitiveSink配套使用,主要是把任意类型的数据转化成Java基本数据类型(primitive value,如char,byte,int……),默认用java.nio.ByteBuffer实现,最终均转化为byte数组;
  • Strategy是定义在BloomFilter类内部的接口,代码如下,有3个方法,put(插入元素),mightContain(判定元素是否存在)和ordinal方法(可以理解为枚举类中那个默认方法)
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
aspectj复制代码interface Strategy extends java.io.Serializable {
 
    /**
     * Sets {@code numHashFunctions} bits of the given bit array, by hashing a user element.
     *
     * <p>Returns whether any bits changed as a result of this operation.
     */
    <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits);
 
    /**
     * Queries {@code numHashFunctions} bits of the given bit array, by hashing a user element;
     * returns {@code true} if and only if all selected bits are set.
     */
    <T> boolean mightContain(
        T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits);
 
    /**
     * Identifier used to encode this strategy, when marshalled as part of a BloomFilter. Only
     * values in the [-128, 127] range are valid for the compact serial form. Non-negative values
     * are reserved for enums defined in BloomFilterStrategies; negative values are reserved for any
     * custom, stateful strategy we may define (e.g. any kind of strategy that would depend on user
     * input).
     */
    int ordinal();
  }

对于创建布隆过滤器,BloomFilter并没有公有的构造函数,只有一个私有构造函数,而对外它提供了5个重载的create方法,在缺省情况下误判率设定为3%,采用BloomFilterStrategies.MURMUR128_MITZ_64的实现。其中4个create方法最终都调用了同一个create方法,由它来负责调用私有构造函数,其源码如下:

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
java复制代码static <T> BloomFilter<T> create(
      Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
    checkNotNull(funnel);
    checkArgument(
        expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);
 
    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }
    /*
     * TODO(user): Put a warning in the javadoc about tiny fpp values, since the resulting size
     * is proportional to -log(p), but there is not much of a point after all, e.g.
     * optimalM(1000, 0.0000000000000001) = 76680 which is less than 10kb. Who cares!
     */
    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      return new BloomFilter<T>(new BitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
  }

在create中接受了4个参数,funnel(输入的数据),expectedInsertions(预计插入的元素总数),fpp(期望误判率),strategy(实现Strategy的实例),然后它计算了bit数组的长度以及哈希函数的个数(公式参考前文),最后用numBits创建了BitArray,并调用了构造函数完成赋值操作。

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码static long optimalNumOfBits(long n, double p) {
    if (p == 0) {
      p = Double.MIN_VALUE;
    }
    return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }
 
static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }

接着再来看一下BloomFilterStrategies类,首先它是实现了BloomFilter.Strategy 接口的一个枚举类,其次它有两个2枚举值,MURMUR128_MITZ_32和MURMUR128_MITZ_64,分别对应了32位哈希映射函数,和64位哈希映射函数,后者使用了murmur3 hash生成的所有128位,具有更大的空间,不过原理是相通的,我们选择默认的MURMUR128_MITZ_64来分析:

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
java复制代码MURMUR128_MITZ_64() {
    @Override
    public <T> boolean put(
        T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);
 
      boolean bitsChanged = false;
      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
        combinedHash += hash2;
      }
      return bitsChanged;
    }
 
    @Override
    public <T> boolean mightContain(
        T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);
 
      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
          return false;
        }
        combinedHash += hash2;
      }
      return true;
    }

抽象来看,put是写,mightContain是读,两个方法的代码有一点相似,都是先利用murmur3 hash对输入的funnel计算得到128位的字节数组,然后高低分别取8个字节(64位)创建2个long型整数hash1,hash2作为哈希值。循环体内采用了2个函数模拟其他函数的思想,即上文提到的gi(x) = h1(x) + ih2(x) ,这相当于每次累加hash2,然后通过基于bitSize取模的方式在bit数组中索引。

这里之所以要和Long.MAX_VALUE进行按位与的操作,是因为在除数和被除数符号不一致的情况下计算所得的结果是有差别的,在程序语言里,“%”准确来说是取余运算(C,C++和Java均如此,python是取模),如-5%3=-2,而取模的数学定义是x mod y=x-y[x/y](向下取整),所以-5 mod 3= -5-3*(-2)=1,因此当哈希值为负数的时候,其取余的结果为负(bitSize始终为正数),这样就不方便在bit数组中取值,因此通过Long.MAX_VALUE(二进制为0111…1111),直接将开头的符号位去掉,从而转变为正数。当然也可以取绝对值,在另一个MURMUR128_MITZ_32的实现中就是这么做的。

在put方法中,先是将索引位置上的二进制置为1,然后用bitsChanged记录插入结果,如果返回true表明没有重复插入成功,而mightContain方法则是将索引位置上的数值取出,并判断是否为0,只要其中出现一个0,那么立即判断为不存在。

最后再说一下底层bit数组的实现,主要代码如下:

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
axapta复制代码 static final class BitArray {
    final long[] data;
    long bitCount;
 
    BitArray(long bits) {
      this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
    }
 
    // Used by serialization
    BitArray(long[] data) {
      checkArgument(data.length > 0, "data length is zero!");
      this.data = data;
      long bitCount = 0;
      for (long value : data) {
        bitCount += Long.bitCount(value);
      }
      this.bitCount = bitCount;
    }
 
    /** Returns true if the bit changed value. */
    boolean set(long index) {
      if (!get(index)) {
        data[(int) (index >>> 6)] |= (1L << index);
        bitCount++;
        return true;
      }
      return false;
    }
 
    boolean get(long index) {
      return (data[(int) (index >>> 6)] & (1L << index)) != 0;
    }
 
    /** Number of bits */
    long bitSize() {
      return (long) data.length * Long.SIZE;
    }
...
}

之前也提到了Guava没有使用java.util.BitSet,而是封装了一个long型的数组,另外还有一个long型整数,用来统计数组中已经占用(置为1)的数量,在第一个构造函数中,它把传入的long型整数按长度64分段(例如129分为3段),段数作为数组的长度,你可以想象成由若干个64位数组拼接成一个超长的数组,它的长度就是64乘以段数,即bitSize,在第二个构造函数中利用Long.bitCount方法来统计对应二进制编码中的1个数,这个方法在JDK1.5中就有了,其算法设计得非常精妙,有精力的同学可以自行研究。

另外两个重要的方法是set和get,在get方法中,参考put和mightContain方法,传入的参数index是经过bitSize取模的,因此一定能落在这个超长数组的范围之内,为了获取index对应索引位置上的值,首先将其无符号右移6位,并且强制转换成int型,这相当于除以64向下取整的操作,也就是换算成段数,得到该段上的数值之后,又将1左移index位,最后进行按位与的操作,如果结果等于0,那么返回false,从而在mightContain中判断为不存在。在set方法中,首先调用了get方法判断是否已经存在,如果不存在,则用同样的逻辑取出data数组中对应索引位置的数值,然后按位或并赋值回去。

到这里,对Guava中布隆过滤器的实现就基本讨论完了,简单总结一下:

1.BloomFilter类的作用在于接收输入,利用公式完成对参数的估算,最后初始化Strategy接口的实例;

2.BloomFilterStrategies是一个枚举类,具有两个实现了Strategy接口的成员,分别为MURMUR128_MITZ_32和MURMUR128_MITZ_64,另外封装了long型的数组作为布隆过滤器底层的bit数组,其中在get和set方法中完成核心的位运算。

三、利用Redis Bitmaps进行重构

通过上面的分析,主要算法和逻辑的部分大体都是一样的,真正需要重构的部分是底层位数组的实现,在Guava中是封装了一个long型的数组,而对于redis来说,本身自带了Bitmaps的“数据结构”(本质上还是一个字符串),已经提供了位操作的接口,因此重构本身并不复杂,相对比较复杂的是,之前提到的实现自动扩容特性。

这里实现自动扩容的思想是,在redis中记录一个自增的游标cursor,如果当前key对应的Bitmaps已经达到饱和状态,则cursor自增,同时用其生成一个新的key,并创建规模同等的Bitmaps。然后在get的时候,需要判断该元素是否存在于任意一个Bitmaps中。于是整个的逻辑就变成,一个元素在每个Bitmaps中都不存在时,才能插入当前cursor对应key的Bitmaps中。

下面是代码的实现部分。

首先,为了简化redis的操作,定义了2个函数式接口,分别执行单条命令和pipeline,另外还实现了一个简单的工具类

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
java复制代码@FunctionalInterface
public interface JedisExecutor<T> {
    T execute(Jedis jedis);
}
 
@FunctionalInterface
public interface PipelineExecutor {
    void load(Pipeline pipeline);
}
 
public class JedisUtils {
 
    private static final GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
 
    private JedisPool jedisPool;
 
    public JedisUtils() {
        jedisPool = new JedisPool(poolConfig, "localhost", 6379);
    }
 
    public <T> T execute(JedisExecutor<T> jedisExecutor) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedisExecutor.execute(jedis);
        }
    }
 
    public List<Object> pipeline(List<PipelineExecutor> pipelineExecutors) {
        try (Jedis jedis = jedisPool.getResource()) {
            Pipeline pipeline = jedis.pipelined();
            for (PipelineExecutor executor : pipelineExecutors)
                executor.load(pipeline);
            return pipeline.syncAndReturnAll();
        }
    }
}

其次在Strategy中,对put和mightContain作了一点修改,其中被注释的部分是Guava中的实现。为了简化,这里我们只接受String对象。

这里先把所有的随机函数对应的索引位置收集到一个数组中,然后交由底层的RedisBitmaps处理get或set,具体过程后面会详细说明。

bits.ensureCapacityInternal()方法,即表示自动扩容,这个函数名是从ArrayList中搬过来的。

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
java复制代码    @Override
    public boolean put(String string, int numHashFunctions, RedisBitmaps bits) {
        long bitSize = bits.bitSize();
        byte[] bytes = Hashing.murmur3_128().hashString(string, Charsets.UTF_8).asBytes();
        long hash1 = lowerEight(bytes);
        long hash2 = upperEight(bytes);
 
        boolean bitsChanged = false;
        long combinedHash = hash1;
//        for (int i = 0; i < numHashFunctions; i++) {
//            bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
//            combinedHash += hash2;
//        }
        long[] offsets = new long[numHashFunctions];
        for (int i = 0; i < numHashFunctions; i++) {
            offsets[i] = (combinedHash & Long.MAX_VALUE) % bitSize;
            combinedHash += hash2;
        }
        bitsChanged = bits.set(offsets);
        bits.ensureCapacityInternal();//自动扩容
        return bitsChanged;
    }
 
    @Override
    public boolean mightContain(String object, int numHashFunctions, RedisBitmaps bits) {
        long bitSize = bits.bitSize();
        byte[] bytes = Hashing.murmur3_128().hashString(object, Charsets.UTF_8).asBytes();
        long hash1 = lowerEight(bytes);
        long hash2 = upperEight(bytes);
        long combinedHash = hash1;
//        for (int i = 0; i < numHashFunctions; i++) {
//            if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
//                return false;
//            }
//            combinedHash += hash2;
//        }
//        return true;
        long[] offsets = new long[numHashFunctions];
        for (int i = 0; i < numHashFunctions; i++) {
            offsets[i] = (combinedHash & Long.MAX_VALUE) % bitSize;
            combinedHash += hash2;
        }
        return bits.get(offsets);
    }

最后,也是最重要的RedisBitmaps,其中bitSize用了Guava布隆过滤器中计算Long型数组长度的方法,得到bitSize之后使用setbit命令初始化一个全部为0的位数组。get(long offset)和set(long offset),这两个与Guava布隆过滤器中的逻辑类似,这里就不再赘述了,而get(long[] offsets)方法中,所有的offset要与每一个cursor对应的Bitmaps进行判断,若全部命中,那么这个元素就可能存在于该Bitmaps,反之若不能完全命中,则表示该元素不存在于任何一个Bitmaps,所以当满足这个条件,在set(long[]
offsets)方法中,就可以插入到当前key的Bitmaps中了。

在ensureCapacityInternal方法,判断需要扩容的条件是bitCount*2>bitSize,bitCount表示一个Bitmaps中“1”出现的个数,也就是当“1”出现的个数超过总数的一半时,进行扩容操作——首先使用incr命令对cursor自增,然后使用新的key创建一个新的Bitmaps。

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
processing复制代码class RedisBitmaps {
 
    private static final String BASE_KEY = "bloomfilter";
    private static final String CURSOR = "cursor";
 
    private JedisUtils jedisUtils;
    private long bitSize;
 
    RedisBitmaps(long bits) {
        this.jedisUtils = new JedisUtils();
        this.bitSize = LongMath.divide(bits, 64, RoundingMode.CEILING) * Long.SIZE;//位数组的长度,相当于n个long的长度
        if (bitCount() == 0) {
            jedisUtils.execute((jedis -> jedis.setbit(currentKey(), bitSize - 1, false)));
        }
    }
 
   boolean get(long[] offsets) {
        for (long i = 0; i < cursor() + 1; i++) {
            final long cursor = i;
            //只要有一个cursor对应的bitmap中,offsets全部命中,则表示可能存在
            boolean match = Arrays.stream(offsets).boxed()
                    .map(offset -> jedisUtils.execute(jedis -> jedis.getbit(genkey(cursor), offset)))
                    .allMatch(b -> (Boolean) b);
            if (match)
                return true;
        }
        return false;
    }
 
    boolean get(final long offset) {
        return jedisUtils.execute(jedis -> jedis.getbit(currentKey(), offset));
    }
 
    boolean set(long[] offsets) {
        if (cursor() > 0 && get(offsets)) {
            return false;
        }
        boolean bitsChanged = false;
        for (long offset : offsets)
            bitsChanged |= set(offset);
        return bitsChanged;
    }
 
    boolean set(long offset) {
        if (!get(offset)) {
            jedisUtils.execute(jedis -> jedis.setbit(currentKey(), offset, true));
            return true;
        }
        return false;
    }
 
    long bitCount() {
        return jedisUtils.execute(jedis -> jedis.bitcount(currentKey()));
    }
 
    long bitSize() {
        return this.bitSize;
    }
 
    private String currentKey() {
        return genkey(cursor());
    }
 
    private String genkey(long cursor) {
        return BASE_KEY + "-" + cursor;
    }
 
    private Long cursor() {
        String cursor = jedisUtils.execute(jedis -> jedis.get(CURSOR));
        return cursor == null ? 0 : Longs.tryParse(cursor);
    }
 
    void ensureCapacityInternal() {
        if (bitCount() * 2 > bitSize())
            grow();
    }
 
    void grow() {
        Long cursor = jedisUtils.execute(jedis -> jedis.incr(CURSOR));
        jedisUtils.execute((jedis -> jedis.setbit(genkey(cursor), bitSize - 1, false)));
    }
 
    void reset() {
        String[] keys = LongStream.range(0, cursor() + 1).boxed().map(this::genkey).toArray(String[]::new);
        jedisUtils.execute(jedis -> jedis.del(keys));
        jedisUtils.execute(jedis -> jedis.set(CURSOR, "0"));
        jedisUtils.execute(jedis -> jedis.setbit(currentKey(), bitSize - 1, false));
    }
 
    private PipelineExecutor apply(PipelineExecutor executor) {
        return executor;
    }
}

下面我们做一个单元测试来验证其正确性。

如果我们插入的数量等于原预计总数,RedisBloomFilter扩容了1次,而两个布隆过滤器的结果一致,都为false,true,false。

如果插入的数量为原预计总数的3倍,RedisBloomFilter扩容了3次,并且仍判断正确,而Guava布隆过滤器则在判断str3时出现误判。

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
pgsql复制代码public class TestRedisBloomFilter {
 
    private static final int TOTAL = 10000;
    private static final double FPP = 0.0005;
 
    @Test
    public void test() {
        RedisBloomFilter redisBloomFilter = RedisBloomFilter.create(TOTAL, FPP);
        redisBloomFilter.resetBitmap();
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), TOTAL, FPP);
 
        IntStream.range(0, /* 3* */TOTAL).boxed()
                .map(i -> Hashing.md5().hashInt(i).toString())
                .collect(toList()).forEach(s -> {
            redisBloomFilter.put(s);
            bloomFilter.put(s);
        });
 
        String str1 = Hashing.md5().hashInt(99999).toString();
        String str2 = Hashing.md5().hashInt(9999).toString();
        String str3 = "abcdefghijklmnopqrstuvwxyz123456";
        System.out.println(redisBloomFilter.mightContain(str1) + ":" + bloomFilter.mightContain(str1));
        System.out.println(redisBloomFilter.mightContain(str2) + ":" + bloomFilter.mightContain(str2));
        System.out.println(redisBloomFilter.mightContain(str3) + ":" + bloomFilter.mightContain(str3));
    }
}
>>
grow bloomfilter-1
false:false
true:true
false:false
>>
grow bloomfilter-1
grow bloomfilter-2
grow bloomfilter-3
false:false
true:true
false:true

综上,本文利用了Guava布隆过滤器的思想,并结合Redis中的Bitmaps等特性实现了支持动态扩容的布隆过滤器,它将布隆过滤器底层的位数据装载到了Redis数据库中,这样的好处在于可以部署在更复杂的多应用或分布式系统中,还可以利用Redis完成持久化,定时过期等功能。

四、参考文献

  1. 吴军. 数学之美[M]. 人民邮电出版社, 2012.
  2. Kirsch A, Mitzenmacher M. Less hashing, same performance: building a better bloom filter[C]//ESA. 2006, 6: 456-467.
  3. Bloom Filters for the Perplexed, https://sagi.io/2017/07/bloom-filters-for-the-perplexed/
  4. Google Guava, https://github.com/google/guava

Loading Likes…

本文转载自: 掘金

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

记一次latency问题排查:谈Go的公平调度的缺陷 记一次

发表于 2017-12-28

记一次latency问题排查:谈Go的公平调度的缺陷

2017-11-19

我们有一个对延迟很敏感的模块,这个模块需要访问网络中的另一台机器去取一个时间戳。实现一次分布式事务,需要执行这个操作两次,如果这里拿时间戳慢了,整个事务的延迟就会上升。理论上内网环境同机房一次网络的round trip应该在0.5ms以内,大部分简单读请求应该落在1ms,80%请求的延迟预期也是4ms以内。有客户反鐀说这里有30ms以上的延时,在内网环境用sysbench跑OLTP测试了一下能够复现,于是查了一下这个问题。

在opentracing里面观察,这一步确实存在较大的延时,日志里面也有打印大量的慢log,影响了事务整体的完成时间。首先方向是确定慢在哪里,到底是网络有问题还是runtime有问题。

一个同事观察到,跑这个模块的benchmark时,额外开启1000个goroutine的worker,每秒钟tick空转,跟只跑benchmark对比,发现前者的延迟要比后者高出许多。怀疑runtime有问题。

另一个同事先单独跑测试,并观察网络,会发现网络重传对结果有明显影响;然后改到客户端服务器在同一机器上再测试,排除掉网络干扰,结果发现进程之间会相互影响;再将服务器和客户端分别绑到不同核之后,服务端处理时间比较稳定,而客户端仍然出现高延迟。到这一步的结论基本上是:runtime和网络都不能保证稳定。

然而runtime的影响能使达到几十ms级别么?看起来不太合理。在我印象中,怎么都应该控制在微秒数量级,即便回到Go语言1.0的版本,stop the world的GC也不见得这么挫。何况现在都优化到1.9了,GC早就不会stop the world的了。于是我使用go tool trace工具,继续分析问题。不看不知道,一看还真吓了一跳。

抓的这一段图,红线箭头是指收到网络消息,ublock了goroutine的Read操作。注意,从网络消息可读,到读网络的goroutine被再次调度,中间花费了4.368ms !!! 我甚至找到一些更极端的场景,从收到网络消息可读,到goroutine实际被唤醒,花费了19ms。这里先插入讲一下业务情况,出于性能考虑,业务实现是做了batch的,所以请求都会通过channel转发到一个goroutine上,由那个goroutine去batch的发请求。显然这个goroutine是非常关键的,因为其它的goroutine都是依赖于它。这里ms级别的调度延迟,直接会对业务整体延迟产生很大影响。

然后说下goroutine的调度时机,goroutine是协程,如果可以执行,会一直执行,直到阻塞才会放弃CPU。比如执行遇到锁了,或者读channel,或者读io请求等等。goroutine被切换出去之后,如果条件满足了,会被丢到ready队列里面去排队,等待被再次运行。然而,具体什么时候会被执行,是不确定的,跟排队的任务队列长度,排在它前面的任务要执行的时间,以及当时的负载情况等等很多因素有关。

这里的问题不是GC,而是调度。最终的延迟问题是跟Go的调度设计相关的,主要是协程的公平调度策略:

  • 不可以抢占
  • 没有优先级的概念

由于不可以抢占,假设网络消息好了,但这个时刻所有的CPU上面都正好有goroutine在跑着,并不能将谁踢掉,于是读网络的那个goroutine并没机会被唤醒。

由于没有优先级的概念,假设终于有goroutine阻塞并让出CPU了,这时让谁执行完全是看调度器的心情,读网络的那个goroutine运气不好,又没被唤醒。

只要goroutine不走到函数调用,是没有机会触发调度,不会让出CPU的。

Go声称可以开千千万万个goroutine,其实是有开销的:越多的goroutine被“公平”调度,越可能影响其中重要goroutine的唤醒,进而影响整体延迟。

回头再想之前同事那个测试,空跑worker会影响延时,也就能解释通了:由于被调度概率均等,越多无关的goroutine,则干活的那个goroutine被调度的概率越低,于是导致延迟增加。

Go的垃圾回收虽然不stop the world,仍然可能影响延迟:GC是可以打断goroutine,要求让出CPU的,而什么时候goroutine被再次调回来又看脸。

有太多太多的因素来影响调度,使整个runtime内的延迟变得不可控。平时压力小时调度上可能看不出什么来,然而尤其在压力大的时候,就表现得越差。

本文转载自: 掘金

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

国内第一套 Clojure 视频课程,Let Lisp Ro

发表于 2017-12-27

Learn Clojure: The Easy Way

陆陆续续用了4个月把这套视频终于录制完成了,算是在 2017 年底完成了自己一个小心愿,让国内学习 Clojure 有更多的选择。

这套视频共 7 讲,不需要什么基础,能跟着视频里讲解实际动手操作就行。如果你想尝试一下古老的 Lisp,这套视频应该不会让你失望。

  • 课程地址: segmentfault.com/ls/16500000…

PS:这套视频收费¥30,大家可以先选择预览后决定是否购买。 PPS:为什么不 free ?答:轻易获得的东西一般都不太珍惜,收费一方面是为了严格要求我自己保证质量,另一方面是让听众有所重视,不至于放在硬盘某个地方吃灰。

如果你有 Clojure 基础,只对某些知识点比较模糊,也可以选择下面的视频进行针对性学习。

  1. Clojure 初相识,售价:¥4.04
  2. Clojure 数据类型以及 REPL 测试技巧,售价:¥4.04
  3. 再谈 lazy 与 persistent,售价:¥10.24
  4. 多态、自定义数据类型与 Java 互操作,售价:¥5.00
  5. 深入浅出Transducer,售价:¥5.00
  6. Clojure并发编程,售价:¥5.00
  7. 深入浅出宏编程,售价:¥10.24

Why Lisp

我是 2013 年从 SICP 开始接触 Lisp,下面这个视频是我阅读 SICP 的一些收获,感兴趣的可以看看。

  • From SICP to Lisp, 主要介绍 SICP 阅读心得以及为什么要学习 Lisp

联系

  • SICP/LISP 交流QQ群:119845407
  • 公众号:KeepWritingCodes

KeepWritingCodes 微信公众号

本文转载自: 掘金

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

1…902903904…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%