代码拉取完成,页面将自动刷新
{"meta":{"title":"ZhongLeiDev","subtitle":"不忘初心,方得始终","description":"","author":"ZhongLeiDev","url":"https://www.smartonline.net.cn","root":"/"},"pages":[{"title":"","date":"2019-11-20T05:49:45.000Z","updated":"2019-11-23T06:17:52.477Z","comments":true,"path":"about/index.html","permalink":"https://www.smartonline.net.cn/about/index.html","excerpt":"","text":"ZhongLeiDev don't worry, be happy~ 这个家伙很懒,什么都没有写(o°ω°o) ~"},{"title":"我的相册","date":"2019-11-20T05:49:45.000Z","updated":"2019-11-26T12:17:21.577Z","comments":true,"path":"albums/index.html","permalink":"https://www.smartonline.net.cn/albums/index.html","excerpt":"","text":"2019-10 甘肃 2018-10 西安 2018-10 洛阳 2018-9 厦门 2018-7 广州 2017-11 潮汕 2017-8 上海 2017-1 大冶 2016-11 桂林 2016-10 成都 2016-10 重庆 2016-4 南京 2015-11 惠州"},{"title":"读后感始化页面","date":"2019-11-19T01:03:22.000Z","updated":"2019-11-20T08:00:00.077Z","comments":true,"path":"bookreading/index.html","permalink":"https://www.smartonline.net.cn/bookreading/index.html","excerpt":"","text":""},{"title":"所有分类","date":"2019-11-20T06:14:16.000Z","updated":"2019-11-20T07:44:12.933Z","comments":true,"path":"categories/index.html","permalink":"https://www.smartonline.net.cn/categories/index.html","excerpt":"","text":""},{"title":"所有标签","date":"2019-11-20T07:43:27.000Z","updated":"2019-11-20T07:45:00.917Z","comments":true,"path":"tags/index.html","permalink":"https://www.smartonline.net.cn/tags/index.html","excerpt":"","text":""},{"title":"写作初始化页面","date":"2019-11-19T01:04:14.000Z","updated":"2019-11-20T08:50:19.226Z","comments":true,"path":"writing/index.html","permalink":"https://www.smartonline.net.cn/writing/index.html","excerpt":"","text":""},{"title":"感悟初始化页面","date":"2019-11-20T05:46:06.000Z","updated":"2019-11-20T08:02:42.602Z","comments":true,"path":"thinking/index.html","permalink":"https://www.smartonline.net.cn/thinking/index.html","excerpt":"","text":""}],"posts":[{"title":"MySQL基础","slug":"MySQL基础","date":"2019-12-16T08:10:44.000Z","updated":"2019-12-16T08:18:16.261Z","comments":true,"path":"2019/12/16/MySQL基础/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/16/MySQL%E5%9F%BA%E7%A1%80/","excerpt":"关系数据库的几种设计范式介绍 1、第一范式(1NF) 在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。","text":"关系数据库的几种设计范式介绍 1、第一范式(1NF) 在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。例如,对于图3-2 中的员工信息表,不能将员工信息都放在一列中显示,也不能将其中的两列或多列在一列中显示;员工信息表的每一行只表示一个员工的信息,一个员工的信息在表中只出现一次。简而言之,第一范式就是无重复的列。 2、第二范式(2NF) 第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被唯一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的唯一标识。如图3-2 员工信息表中加上了员工编号(emp_id)列,因为每个员工的员工编号是唯一的,因此每个员工可以被唯一区分。这个唯一属性列被称为主关键字或主键、主码。 第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的唯一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。 3、第三范式(3NF) 满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。那么在图3-2的员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。 为什么要有数据库:文件 管理数据的问题: 1.数据冗余和不一致性 2.大数据访问困难 3.数据孤立 4.完整性和原子性 5.并发访问异常 6.安全性问题 由此,有了数据库。。。 数据库概述 数据库:指的是以一定方式存储在一起、能为多个用户共享,具有尽可能小的冗余度的特点、是与应用程序彼此独立的数据集合。 数据库服务器的基本概念 数据库 DBMS 数据库管理系统(能够操作和管理数据库的大型软件) RDBMS 关系式数据库(=DBMS) 1.数据以表格的形式出现 2.每行为各种记录名称 3.每列为记录名称所对应的数据域 4.许多的行和列组成一张表单 5.若干的表单组成database 数据表 数据(记录) 字段(id name ….) 类型(定义字段中的内容) 主键(检索时用) 数据库的三种基本形式 层次模型: 按照层次结构的形式组织数据库数据的模型 缺点:冗余数据 网状模型: 按照网状结构的形式组织数据库数据的模型 缺点:后期维护困难 关系模型: 按照关系结构的形式组织数据库数据的模型 sql语句: 结构化查询语句 sql类型: DML 数据操作语言:用来操作数据库中的数据 INSERT DELETE SELECT UPDATE DDL 数据描述语言:用来建立数据库、定义数据关系 CREATE DROP ALTER DCL 数据控制语言:用来控制数据库组建的权限 GRANT REVOKE 关系式数据库结构 文件逻辑关系: 上层:文件 底层:二进制的方式存储在硬盘的数据块中 中间层:文件系统 数据库逻辑关系: 上层:数据表 底层:文件 中间层:存储引擎(提供存储、创建、更新、查询数据的实现方法) mysql 支持的存储引擎: MYISAM 默认引擎、插入和查询速度较快 不支持事务、行级锁和外键约束等功能 注: 事务:一段sql语句的批处理、为了保证数据原子性 锁:行级锁(冲突少、速度慢);表级锁(冲突多、速度快);页级锁(折中方案) 约束 域约束:数据类型约束 外键约束:引用完整性约束 主键约束:某字段能惟一标识此字段所属的实体,并且不允许为空 一张表中只能有一个主键 惟一性约束:每一行的某字段都不允许出现相同值,可以为空 一张表中可以有多个 检查性约束: age: int INNODB 支持事务、行级锁和外键约束等功能 MEMORY 工作在内存中,通过散列保存数据,速度快、不能永久保存数据 数据的存储和查询 存储管理器(实现存储的功能,通过DDL创建数据表的结构,再通过DML来保存数据) 事务管理器:提供事务功能 文件管理器:保存数据库数据和文件的对应关系 权限及完整性管理器:设置存储权限 缓冲管理器:管理缓冲空间 查询管理器(实现查询的功能,接收用户的查询请求、理解用户查询请求,将查询请求提交给存储管理器、实现最终存储) DDL 、DML解释器 查询执行引擎 数据库工作模式: 单进程多线程的工作模式 守护线程 应用线程(用户线程) 补充:apache的工作模式: 一个进程处理一个请求 一个线程处理一个请求 一个线程处理多个请求 mysql优化 1.垂直扩展 2.线程重用 3.缓存机制(nosql < memcache redis mongodb >) SMP : 对称多处理器架构 3. E-R模型 实体关系建模 实体:数据对象,即看得见摸得着 联系:表示一个或多个实体之间的关联 属性:实体的某一特性 数据库—基本概述 数据库的版本 1.社区版 2.企业版 3.集群版 数据库的安装 专用软件包管理器(二进制) deb 、rpm等 mysql MySQL客户端程序和共享库 mysql-server MySQL服务器需要的相关程序 源代码软件包(编译安装) configure、cmake 数据库常用的配置选项 -DCMAKE_INSTALL_PREFIX=/usr/local/mysql —-指定残可安装路径(默认的就是/usr/local/mysql) -DMYSQL_DATADIR=/data/mysql —-mysql的数据文件路径 -DSYSCONFDIR=/etc —-配置文件路径 -DWITH_INNOBASE_STORAGE_ENGINE=1 —-使用INNOBASE存储引擎 -DWITH_ARCHIVE_STORAGE_ENGINE=1 —-常应用于日志记录和聚合分析,不支持索引 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 —-黑洞存储引擎 -DWITHOUT_EXAMPLE_STORAGE_ENGINE=1 编译过程中取消一些存储引擎指令介绍 -DWITHOUT_FEDERATED_STORAGE_ENGINE=1 -DWITHOUT_PARTITION_STORAGE_ENGINE=1 -DWITH_READLINE=1 —-支持批量导入mysql数据 -DWITH_SSL=system —-mysql支持ssl会话,实现基于ssl的数据复 -DWITH_ZLIB=system —-压缩库 -DWITH_LIBWRAP=0 —-是否可以基于WRAP实现访问控制 -DMYSQL_TCP_PORT=3306 —-默认端口 -DMYSQL_UNIX_ADDR=/tmp/mysql.sock —-默认套接字文件路径 -DENABLED_LOCAL_INFILE=1 —-是否启用LOCAL_INFILE功能 -DEXTRA_CHARSETS=all —-是否支持额外的字符集 -DDEFAULT_CHARSET=utf8 —-默认编码机制 -DDEFAULT_COLLATION=utf8_general_ci —-设定默认语言的排序规则 -DWITH_DEBUG=0 —-DEBUG功能设置 -DENABLE_PROFILING=1 —-性能分析功能是否启用 服务:mysqld 端口:3306 主配置文件:/etc/my.cnf 脚本:mysql_install_db mysqld_safe 数据目录 :/var/lib/mysql 套接字文件:/var/lib/mysql/mysql.sock 当意外关闭数据库时,再开启时假如开启不了,找到这个,删除再启动 进程文件:/var/run/mysqld/mysqld.pid 登录及退出mysql环境 a) 设置密码 1mysqladmin -uroot password ‘123’ b) 登录 1mysql -u 用户名 -p -p 用户密码 -h 登陆位置(主机名或ip地址) -P 端口号(3306改了就不是了) -S 套接字文件(/var/lib/mysql/mysql.sock) c) 退出 1exit d) 创建登录用户 1create user 用户名@’%’ identified by ‘密码’ e) 修改密码 12set password=password(‘新密码’)set password for 用户@登录位置=password(‘新密码’) — — — root用户为其他用户找回密码 当管理员把自己密码忘记了,怎么找回??? 1)关闭数据库 2)修改主配置文件(/etc/my.cnf)<—— skip-grant-tables 3)启动数据库 4)空密码登录并修改密码 1update mysql.user set password=password(‘新密码’) where user=’root’; 5)删除skip-grant-tables,重启数据库验证新密码 SQL语句 关于库的操作: Mysql命令 功能 show databases 查看服务器中当前有哪些数据库 use 数据库名 选择所使用的数据库 create database 数据库名 创建数据库 drop database 数据库名 删除指定的数据库 关于表的操作 MySQL命令 功能 create table 表名 (字段1 类型1,…) 在当前数据库中创建数据表 show tables 显示当前数据库中有哪些数据表 describe 表名 显示当前或指定数据库中指定数据表的结构(字段)信息 drop table 表名 删除当前或指定数据库中指定的数据表 alter table 旧表名 rename 新表名 修改数据表的名称 alter table 表名 modify 字段 类型 修改字段的类型 alter table 表名 change 旧字段名 新字段名 类型 修改字段 alter table 表名 add 字段 类型(first/after) 增加字段 alter table 表名 drop 字段 删除字段 MySQL命令 功能 insert into 表名(字段1,字段2,……) values(字段1的值, 字段2的值,……) 向数据表中插入新的记录 update 表名 set 字段名 =新数据 where 条件表达式 修改、更新数据表中的记录 select 字段名1,字段名2……from 表名 where 条件表达式 从数据表中查找符合条件的记录 select * from 表名 显示当前数据库的表中的记录 delete from 表名 where 条件表达式 between…and… 在数据表中删除指定的记录指定范围 delete from 表名 将当前数据库表中记录清空 注:库和表的删除用drop,记录删除用delete 权限 grant 权限1,权限2,…… on 数据库.数据表 to 用户@登录位置 (identified by ‘密码’); revoke 取消的权限1,取消的权限2,…… on 数据库.数据表 from 用户@登录位置; show grants for 用户@登录位置; 备份和还原 冷备份:把数据库关闭,离线备份(使用cp、tar等命令直接备份数据库所存放的目录) FRM 结构 MYI 索引 MYD 数据 快照备份:(利用逻辑卷) 逻辑备份: mysqldump 备份:mysqldump -u 用户名 -p 数据库名 > /备份路径/备份文件名(备份单个数据库) mysqldump -u 用户名 -p 数据库名 表名 > /备份路径/备份文件名(备份数据表) –databases 库1,库2 (此时还原—>mysql < 备份文件) –all-databases —备份服务器中的所有数据库内容 还原:mysql 数据库 < 备份文件 mysqlhotcopy 备份:mysqlhotcopy –flushlog -u=’用户’ -p=’密码’ –regexp=正则 备份目录 还原:cp -rpf 备份目录 数据目录(/var/lib/mysql) 补充的备份机制 1.日志备份 >mysql show global variables like ‘%log%’ 列出mysql中和日志相关的变量 错误日志 服务器启动和关闭时的信息 服务器运行过程中的错误信息 从服务器启动从服务器进程时产生的信息 log-error 错误日志的路径 一般日志(不启用) 记录用户对数据库的查询操作 general-log=ON 启动一般查询日志 log=ON 全局日志开关 log-output 日志的记录类型 慢查询日志 记录需要较长时间的查询操作 log-slow-queries=保存路径 启动慢查询日志,并设置个路径 二进制日志 所有对数据库状态更改的操作(create、drop、update等) log-bin=位置 启动二进制日志 >mysql show binary logs 查看当前使用的二进制日志 >mysql show binlog events in ‘二进制日志(mysql-bin.000001)’ 查看二进制日志的内容 还原:(mysqlbinlog) 按时间还原: mysqlbinlog –start-datetime ‘YY-MM-DD HH:MM:SS’ –stop-datetime ‘YY-MM-DD HH:MM:SS’ 二进制日志 | mysql(-urot -p) 按文件大小还原: –start-position –stop-position 事务日志:记录事务相关的日志信息 中继日志:记录从服务器的备份信息 2.多机备份 主从配置:实时备份 主主配置:(配置了2遍的主从)实时备份、负载均衡 多从一主:实时备份(更多的备份节点) 多主一从:实时备份、节约成本","categories":[],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://www.smartonline.net.cn/tags/mysql/"}]},{"title":"Java正则表达式","slug":"正则表达式","date":"2019-12-16T07:40:17.000Z","updated":"2020-01-03T09:19:21.295Z","comments":true,"path":"2019/12/16/正则表达式/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/16/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/","excerpt":"正则表达式定义了字符串的模式。 正则表达式可以用来搜索、编辑或处理文本。 正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。","text":"正则表达式定义了字符串的模式。 正则表达式可以用来搜索、编辑或处理文本。 正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。 正则表达式实例一个字符串其实就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 “Hello World” 字符串。 .(点号)也是一个正则表达式,它匹配任何一个字符如:”a” 或 “1”。 下表列出了一些正则表达式的实例及描述: 正则表达式 描述 this is text 匹配字符串 “this is text” this\\s+is\\s+text 注意字符串中的 \\s+。匹配单词 “this” 后面的 \\s+ 可以匹配多个空格,之后匹配 is 字符串,再之后 \\s+ 匹配多个空格然后再跟上 text 字符串。可以匹配这个实例:this is text ^\\d+(.\\d+)? ^ 定义了以什么开始\\d+ 匹配一个或多个数字? 设置括号内的选项是可选的. 匹配 “.”可以匹配的实例:”5”, “1.5” 和 “2.21”。 Java 正则表达式和 Perl 的是最为相似的。 java.util.regex 包主要包括以下三个类: Pattern 类: pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。 Matcher 类: Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。 PatternSyntaxException: PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 以下实例中使用了正则表达式 .*runoob.* 用于查找字符串中是否包了 runoob 子串: 实例123456789import java.util.regex.*; class RegexExample1{ public static void main(String args[]){ String content = \"I am noob \" + \"from runoob.com.\"; String pattern = \".*runoob.*\"; boolean isMatch = Pattern.matches(pattern, content); System.out.println(\"字符串中是否包含了 'runoob' 子字符串? \" + isMatch); } } 实例输出结果为: 1字符串中是否包含了 'runoob' 子字符串? true 捕获组捕获组是把多个字符当一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 例如,正则表达式 (dog) 创建了单一分组,组里包含”d”,”o”,和”g”。 捕获组是通过从左至右计算其开括号来编号。例如,在表达式((A)(B(C))),有四个这样的组: ((A)(B(C))) (A) (B(C)) (C) 可以通过调用 matcher 对象的 groupCount 方法来查看表达式有多少个分组。groupCount 方法返回一个 int 值,表示matcher对象当前有多个捕获组。 还有一个特殊的组(group(0)),它总是代表整个表达式。该组不包括在 groupCount 的返回值中。 实例下面的例子说明如何从一个给定的字符串中找到数字串: 1234567891011121314151617181920212223242526import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegexMatches{ public static void main( String args[] ){ // 按指定模式在字符串查找 String line = \"This order was placed for QT3000! OK?\"; String pattern = \"(\\\\D*)(\\\\d+)(.*)\"; // 创建 Pattern 对象 Pattern r = Pattern.compile(pattern); // 现在创建 matcher 对象 Matcher m = r.matcher(line); if (m.find( )) { System.out.println(\"Found value: \" + m.group(0) ); System.out.println(\"Found value: \" + m.group(1) ); System.out.println(\"Found value: \" + m.group(2) ); System.out.println(\"Found value: \" + m.group(3) ); } else { System.out.println(\"NO MATCH\"); } }} 以上实例编译运行结果如下: 1234Found value: This order was placed for QT3000! OK?Found value: This order was placed for QTFound value: 3000Found value: ! OK? 正则表达式语法在其他语言中,\\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。 在 Java 中,\\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。 所以,在其他的语言中(如Perl),一个反斜杠 \\ 就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \\ 代表其他语言中的一个 \\,这也就是为什么表示一位数字的正则表达式是 \\d,而表示一个普通的反斜杠是 \\\\。 字符 说明 \\ 将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,”n”匹配字符”n”。”\\n”匹配换行符。序列”\\\\“匹配”\\“,”\\(“匹配”(“。 ^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与”\\n”或”\\r”之后的位置匹配。 $ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与”\\n”或”\\r”之前的位置匹配。 * 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配”z”和”zoo”。* 等效于 {0,}。 + 一次或多次匹配前面的字符或子表达式。例如,”zo+”与”zo”和”zoo”匹配,但与”z”不匹配。+ 等效于 {1,}。 ? 零次或一次匹配前面的字符或子表达式。例如,”do(es)?”匹配”do”或”does”中的”do”。? 等效于 {0,1}。 {n} n 是非负整数。正好匹配 n 次。例如,”o{2}”与”Bob”中的”o”不匹配,但与”food”中的两个”o”匹配。 {n,} n 是非负整数。至少匹配 n 次。例如,”o{2,}”不匹配”Bob”中的”o”,而匹配”foooood”中的所有 o。”o{1,}”等效于”o+”。”o{0,}”等效于”o*”。 {n,m} m 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次。例如,”o{1,3}”匹配”fooooood”中的头三个 o。’o{0,1}’ 等效于 ‘o?’。注意:您不能将空格插入逗号和数字之间。 ? 当此字符紧随任何其他限定符(、+、?、{*n}、{n,}、{n,m})之后时,匹配模式是”非贪心的”。”非贪心的”模式匹配搜索到的、尽可能短的字符串,而默认的”贪心的”模式匹配搜索到的、尽可能长的字符串。例如,在字符串”oooo”中,”o+?”只匹配单个”o”,而”o+”匹配所有”o”。 . 匹配除”\\r\\n”之外的任何单个字符。若要匹配包括”\\r\\n”在内的任意字符,请使用诸如”[\\s\\S]”之类的模式。 (pattern) 匹配 pattern 并捕获该匹配的子表达式。可以使用 $0…$9 属性从结果”匹配”集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用”(“或者”)“。 (?:pattern) 匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用”or”字符 (|) 组合模式部件的情况很有用。例如,’industr(?:y|ies) 是比 ‘industry|industries’ 更经济的表达式。 (?=pattern) 执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,’Windows (?=95|98|NT|2000)’ 匹配”Windows 2000”中的”Windows”,但不匹配”Windows 3.1”中的”Windows”。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。 (?!pattern) 执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,’Windows (?!95|98|NT|2000)’ 匹配”Windows 3.1”中的 “Windows”,但不匹配”Windows 2000”中的”Windows”。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。 x|y 匹配 x 或 y。例如,’z|food’ 匹配”z”或”food”。’(z|f)ood’ 匹配”zood”或”food”。 [xyz] 字符集。匹配包含的任一字符。例如,”[abc]”匹配”plain”中的”a”。 [^xyz] 反向字符集。匹配未包含的任何字符。例如,”[^abc]”匹配”plain”中”p”,”l”,”i”,”n”。 [a-z] 字符范围。匹配指定范围内的任何字符。例如,”[a-z]”匹配”a”到”z”范围内的任何小写字母。 [^a-z] 反向范围字符。匹配不在指定的范围内的任何字符。例如,”[^a-z]”匹配任何不在”a”到”z”范围内的任何字符。 \\b 匹配一个字边界,即字与空格间的位置。例如,”er\\b”匹配”never”中的”er”,但不匹配”verb”中的”er”。 \\B 非字边界匹配。”er\\B”匹配”verb”中的”er”,但不匹配”never”中的”er”。 \\cx 匹配 x 指示的控制字符。例如,\\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是”c”字符本身。 \\d 数字字符匹配。等效于 [0-9]。 \\D 非数字字符匹配。等效于 [^0-9]。 \\f 换页符匹配。等效于 \\x0c 和 \\cL。 \\n 换行符匹配。等效于 \\x0a 和 \\cJ。 \\r 匹配一个回车符。等效于 \\x0d 和 \\cM。 \\s 匹配任何空白字符,包括空格、制表符、换页符等。与 [ \\f\\n\\r\\t\\v] 等效。 \\S 匹配任何非空白字符。与 [^ \\f\\n\\r\\t\\v] 等效。 \\t 制表符匹配。与 \\x09 和 \\cI 等效。 \\v 垂直制表符匹配。与 \\x0b 和 \\cK 等效。 \\w 匹配任何字类字符,包括下划线。与”[A-Za-z0-9_]”等效。 \\W 与任何非单词字符匹配。与”[^A-Za-z0-9_]”等效。 \\xn 匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,”\\x41”匹配”A”。”\\x041”与”\\x04”&”1”等效。允许在正则表达式中使用 ASCII 代码。 *num* 匹配 num*,此处的 *num 是一个正整数。到捕获匹配的反向引用。例如,”(.)\\1”匹配两个连续的相同字符。 *n* 标识一个八进制转义码或反向引用。如果 *n* 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n是八进制转义码。 *nm* 标识一个八进制转义码或反向引用。如果 *nm* 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 *nm* 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 *nm* 匹配八进制值 nm*,其中 *n 和 m 是八进制数字 (0-7)。 \\nml 当 n 是八进制数 (0-3),m 和 l 是八进制数 (0-7) 时,匹配八进制转义码 nml。 \\un 匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\\u00A9 匹配版权符号 (©)。 根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线,表示正则表达式受到保护,不被 Java 字节码编译器解释。例如,当解释为正则表达式时,字符串字面值 “\\b” 与单个退格字符匹配,而 “\\b” 与单词边界匹配。字符串字面值 “(hello)“ 是非法的,将导致编译时错误;要与字符串 (hello) 匹配,必须使用字符串字面值 “\\(hello\\)”。 Matcher 类的方法索引方法索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配: 序号 方法及说明 1 public int start() 返回以前匹配的初始索引。 2 public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引 3 public int end() 返回最后匹配字符之后的偏移量。 4 public int end(int group) 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 研究方法研究方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式: 序号 方法及说明 1 public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。 2 public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。 3 public boolean find(int start**)** 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 4 public boolean matches() 尝试将整个区域与模式匹配。 替换方法替换方法是替换输入字符串里文本的方法: 序号 方法及说明 1 public Matcher appendReplacement(StringBuffer sb, String replacement) 实现非终端添加和替换步骤。 2 public StringBuffer appendTail(StringBuffer sb) 实现终端添加和替换步骤。 3 public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 4 public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。 5 public static String quoteReplacement(String s) 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。 start 和 end 方法下面是一个对单词 “cat” 出现在输入字符串中出现次数进行计数的例子: 12345678910111213141516171819202122import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegexMatches{ private static final String REGEX = \"\\\\bcat\\\\b\"; private static final String INPUT = \"cat cat cat cattie cat\"; public static void main( String args[] ){ Pattern p = Pattern.compile(REGEX); Matcher m = p.matcher(INPUT); // 获取 matcher 对象 int count = 0; while(m.find()) { count++; System.out.println(\"Match number \"+count); System.out.println(\"start(): \"+m.start()); System.out.println(\"end(): \"+m.end()); } }} 以上实例编译运行结果如下: 123456789101112Match number 1start(): 0end(): 3Match number 2start(): 4end(): 7Match number 3start(): 8end(): 11Match number 4start(): 19end(): 22 可以看到这个例子是使用单词边界,以确保字母 “c” “a” “t” 并非仅是一个较长的词的子串。它也提供了一些关于输入字符串中匹配发生位置的有用信息。 Start 方法返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引,end 方法最后一个匹配字符的索引加 1。 matches 和 lookingAt 方法matches 和 lookingAt 方法都用来尝试匹配一个输入序列模式。它们的不同是 matches 要求整个序列都匹配,而lookingAt 不要求。 lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。 这两个方法经常在输入字符串的开始使用。 我们通过下面这个例子,来解释这个功能: 123456789101112131415161718192021222324252627import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegexMatches{ private static final String REGEX = \"foo\"; private static final String INPUT = \"fooooooooooooooooo\"; private static final String INPUT2 = \"ooooofoooooooooooo\"; private static Pattern pattern; private static Matcher matcher; private static Matcher matcher2; public static void main( String args[] ){ pattern = Pattern.compile(REGEX); matcher = pattern.matcher(INPUT); matcher2 = pattern.matcher(INPUT2); System.out.println(\"Current REGEX is: \"+REGEX); System.out.println(\"Current INPUT is: \"+INPUT); System.out.println(\"Current INPUT2 is: \"+INPUT2); System.out.println(\"lookingAt(): \"+matcher.lookingAt()); System.out.println(\"matches(): \"+matcher.matches()); System.out.println(\"lookingAt(): \"+matcher2.lookingAt()); }} 以上实例编译运行结果如下: 123456Current REGEX is: fooCurrent INPUT is: foooooooooooooooooCurrent INPUT2 is: ooooofoooooooooooolookingAt(): truematches(): falselookingAt(): false replaceFirst 和 replaceAll 方法replaceFirst 和 replaceAll 方法用来替换匹配正则表达式的文本。不同的是,replaceFirst 替换首次匹配,replaceAll 替换所有匹配。 下面的例子来解释这个功能: 123456789101112131415161718import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegexMatches{ private static String REGEX = \"dog\"; private static String INPUT = \"The dog says meow. \" + \"All dogs say meow.\"; private static String REPLACE = \"cat\"; public static void main(String[] args) { Pattern p = Pattern.compile(REGEX); // get a matcher object Matcher m = p.matcher(INPUT); INPUT = m.replaceAll(REPLACE); System.out.println(INPUT); }} 以上实例编译运行结果如下: 1The cat says meow. All cats say meow. appendReplacement 和 appendTail 方法Matcher 类也提供了appendReplacement 和 appendTail 方法用于文本替换: 看下面的例子来解释这个功能: 1234567891011121314151617181920import java.util.regex.Matcher;import java.util.regex.Pattern; public class RegexMatches{ private static String REGEX = \"a*b\"; private static String INPUT = \"aabfooaabfooabfoobkkk\"; private static String REPLACE = \"-\"; public static void main(String[] args) { Pattern p = Pattern.compile(REGEX); // 获取 matcher 对象 Matcher m = p.matcher(INPUT); StringBuffer sb = new StringBuffer(); while(m.find()){ m.appendReplacement(sb,REPLACE); } m.appendTail(sb); System.out.println(sb.toString()); }} 以上实例编译运行结果如下: 1-foo-foo-foo-kkk PatternSyntaxException 类的方法PatternSyntaxException 是一个非强制异常类,它指示一个正则表达式模式中的语法错误。 PatternSyntaxException 类提供了下面的方法来帮助我们查看发生了什么错误。 序号 方法及说明 1 public String getDescription() 获取错误的描述。 2 public int getIndex() 获取错误的索引。 3 public String getPattern() 获取错误的正则表达式模式。 4 public String getMessage() 返回多行字符串,包含语法错误及其索引的描述、错误的正则表达式模式和模式中错误索引的可视化指示。","categories":[],"tags":[{"name":"java","slug":"java","permalink":"https://www.smartonline.net.cn/tags/java/"}]},{"title":"《Python编程:从入门到实践》","slug":"PythonProgram","date":"2019-12-16T07:11:39.000Z","updated":"2019-12-16T07:42:29.400Z","comments":true,"path":"2019/12/16/PythonProgram/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/16/PythonProgram/","excerpt":"【1】变量的命名和使用在 Python 中使用变量时,需要遵守一些规则和指南。违反这些规则将引发错误,而指南旨在让你编写的代码更容易阅读和理解。请务必牢记下述有关变量的规则。 变量名只能包含字母、数字和下划线。变量名可以字母或下划线打头,但不能以数字打头,例如,可将变量命名为 message_1 ,但不能将其命名为 1_message 。","text":"【1】变量的命名和使用在 Python 中使用变量时,需要遵守一些规则和指南。违反这些规则将引发错误,而指南旨在让你编写的代码更容易阅读和理解。请务必牢记下述有关变量的规则。 变量名只能包含字母、数字和下划线。变量名可以字母或下划线打头,但不能以数字打头,例如,可将变量命名为 message_1 ,但不能将其命名为 1_message 。 变量名不能包含空格,但可使用下划线来分隔其中的单词。例如,变量名 greeting_message 可行,但变量名 greeting message 会引发错误。 不要将 Python 关键字和函数名用作变量名,即不要使用 Python 保留用于特殊用途的单词,如 print (请参见附录 A.4 )。 变量名应既简短又具有描述性。例如, name 比 n 好, student_name 比 s_n 好, name_length 比 length_of_persons_name 好。 慎用小写字母 l 和大写字母 O ,因为它们可能被人错看成数字 1 和 0 。 【2】字符串 大小写转换: name = “ada lovelace” print(name.title()) print(name.upper()) print(name.lower()) 输出: Ada Lovelace ADA LOVELACE ada lovelace 存储数据时,方法 lower() 很有用。很多时候,你无法依靠用户来提供正确的大小写,因此需要 将字符串先转换为小写,再存储它们。以后需要显示这些信息时,再将其转换为最合适的大小写 方式。 合并(拼接)字符串: 使用 “+”: first_name = “ada” last_name = “lovelace” full_name = first_name + “ “ + last_name print(full_name) 使用制表符或换行符来添加空白 >>> print(“Languages:\\n\\tPython\\n\\tC\\n\\tJavaScript”) Languages: Python C JavaScript 删除空白 你还可以剔除字符串开头的空白,或同时剔除字符串两端的空白。为此,可分别使用方法 lstrip() 和 strip() : >>> favorite_language = ‘ python ‘ >>> favorite_language.rstrip() ‘ python’ >>> favorite_language.lstrip() ‘python ‘ >>> favorite_language.strip() ‘python’ 单引号和双引号 ‘ Albert Einstein said, “A person who never made a mistake never tried anything new“ ‘ “ Albert Einstein said, ‘A person who never made a mistake never tried anything new‘ “ 【3】数字 整数 >>> 2 + 3 5 >>> 3 - 2 1 >>> 2 * 3 6 >>> 3 / 2 1.5 在终端会话中, Python 直接返回运算结果。 Python 使用两个乘号表示乘方运算: >>> 3 ** 2 9 >>> 3 ** 3 27 >>> 10 ** 6 1000000 浮点数 >>> 0.1 + 0.1 0.2 >>> 0.2 + 0.2 0.4 >>> 2 * 0.1 0.2 >>> 2 * 0.2 0.4 使用函数 str() 将数值转化为字符串,避免类型错误 age = 23 message = “Happy “ + str(age) + “rd Birthday!” print(message) 【4】注释# 向大家问好 print(“Hello Python people!”) 【5】python 之禅>>>import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Python 程序员笃信代码可以编写得漂亮而优雅。编程是要解决问题的,设计良好、高效而漂亮的解决方案都会让程序员心生敬意。随着你对 Python 的认识越来越深入,并使用它来编写越来越多的代码,有一天也许会有人站在你后面惊呼: “ 哇,代码编写得真是漂亮! Explicit is better than implicit. Simple is better than complex. 如果有两个解决方案,一个简单,一个复杂,但都行之有效,就选择简单的解决方案吧。这样,你编写的代码将更容易维护,你或他人以后改进这些代码时也会更容易。 Complex is better than complicated. 现实是复杂的,有时候可能没有简单的解决方案。在这种情况下,就选择最简单可行的解决方案吧。 Flat is better than nested. Sparse is better than dense. Readability counts. 即便是复杂的代码,也要让它易于理解。开发的项目涉及复杂代码时,一定要为这些代码编写有益的注释。 Special cases aren’t special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one– and preferably only one –obvious way to do it. 如果让两名 Python 程序员去解决同一个问题,他们提供的解决方案应大致相同。这并不是说编程没有创意空间,而是恰恰相反!然而,大部分编程工作都是使用常见解决方案来解 决简单的小问题,但这些小问题都包含在更庞大、更有创意空间的项目中。在你的程序中,各种具体细节对其他 Python 程序员来说都应易于理解。 Although that way may not be obvious at first unless you’re Dutch. Now is better than never. 你可以将余生都用来学习 Python 和编程的纷繁难懂之处,但这样你什么项目都完不成。不要企图编写完美无缺的代码;先编写行之有效的代码,再决定是对其做进一步改进,还是 转而去编写新代码。 Although never is often better than *right* now. If the implementation is hard to explain, it’s a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea – let’s do more of those! 【6】列表列表 由一系列按特定顺序排列的元素组成。你可以创建包含字母表中所有字母、数字 0~9 或所有家庭成员姓名的列表;也可以将任何东西加入列表中,其中的元素之间可以没有任何关系。鉴于列表通常包含多个元素,给列表指定一个表示复数的名称(如 letters 、 digits 或 names )是个不错的主意。列表是可修改的。 bicycles = [‘trek’, ‘cannondale’, ‘redline’, ‘specialized’] print(bicycles) print(bicycles[0]) #访问第一个元素(下标从0开始) print(bicycles[-1]) #访问最后一个元素(-1为最后一个,-2为倒数第二个,以此类推) 列表增删改查 motorcycles = [‘honda’, ‘yamaha’, ‘suzuki’] print(motorcycles) motorcycles[0] = ‘ducati’ #修改第一个元素 print(motorcycles) motorcycles.append(‘ducati’) #在列表末尾添加元素 print(motorcycles) motorcycles.insert(0, ‘ducati’) # 在索引 0 处添加元素,并将值 ‘ducati’ 存储到这个地方。这种操作将列表中既有的每个元素都右移一个位置 print(motorcycles) del motorcycles[0] #删除索引 0 处的元素 print(motorcycles) popped_motorcycle = motorcycles.pop() #弹出栈顶元素,删除列表末尾元素 print(motorcycles) print(popped_motorcycle) first_owned = motorcycles.pop(0) #弹出指定索引处的元素 print(‘The first motorcycle I owned was a ‘ + first_owned.title() + ‘.’) #每当你使用 pop() 时,被弹出的元素就不再在列表中了。 motorcycles.remove(‘ducati’) # 根据值删除元素 print(motorcycles) #方法 remove() 只删除第一个指定的值。如果要删除的值可能在列表中出现多次,就需要使用循环来判断是否删除了所有这样的值。 【7】组织列表cars = [‘bmw’,’audi’,’toyota’,‘subaru’] cars.sort() #按字母顺序永久排列 cars.sort(reverse = True) #按字母顺序相反的顺序永久排列 print(sorted(cars)) #按特定顺序临时排列 cars.reverse() #永久反转列表排列顺序 cars.reverse() #反转恢复 len(cars) #获取列表长度 【8】操作列表Python根据缩进来判断代码行与前一个代码行的关系。例如: magicians = [‘alice’,’david’,’carolina’] for magician in magicians: print(magician.title() + “,that was great trick!”) #开头缩进,属于前面的 for 循环 print(“I can’t wait to see you again!”) #开头未缩进,不属于 for 循环 【9】range 的使用for value in range(1,5): #打印数字1到4 print(value) # 1,2,3,4 numbers = list(range(1,6)) #创建一个列表 print(numbers) #[1,2,3,4,5] even_numbers = list(range(2,11,2)) #指定步长 print(even_numbers) #[2,4,6,8,10] squares = [value\\2 for value in range(1,11)**] #列表解析 print(squares) 【10】切片players = [‘charles’, ‘martina’, ‘michael’, ‘florence’, ‘eli’] print(players[0:3]) #切片,输出下标 0~2(3之前)的值,即 [‘charles’, ‘martina’, ‘michael’] print(players[:3]) #从头开始到3之前 print(players[-3:]) #输出最后3个元素 my_foods = [‘pizza’, ‘falafel’, ‘carrot cake’] friend_foods = my_foods[:] #复制列表,生成一个独立的列表副本,与原列表不相关 【11】元组python将不能修改的值称为不可变的,而不可变的列表被称为元组。 dimensions = (200, 50) print(dimensions[0]) #元组第一个元素,200 print(dimensions[1]) #元组第二个元素,50 for dimension in dimensions: #遍历元组 print(dimension) dimensions = (400, 100) #虽然不能修改元组的元素(dimensions[0]=400是非法的),但可以 给存储元组的变量赋值 print(dimensions[0]) #元组第一个元素,400 print(dimensions[1]) #元组第二个元素,100 【12】格式设置1.PEP 8 建议每级缩进都使用四个空格,这既可提高可读性,又留下了足够的多级缩进空间。 \\2. PEP 8 还建议注释的行长都不超过 72 字符,因为有些工具为大型项目自动生成文档时,会在每行注释开头添加格式化字符。 3.要将程序的不同部分分开,可使用空行。 【13】python 语法 >>> age_0 >= 21 and age_1 >= 21 #and >>> age_0 >= 21 or age_1 >= 21 #or >>> requested_toppings = [‘mushrooms’, ‘onions’, ‘pineapple’] >>> ‘mushrooms’ in requested_toppings #in if user not in banned_users: #not in print(user.title() + “, you can post a response if you wish.”) >>>game_active = True #bool if-elseif-else: age = 12 if age < 4: price = 0 elif age < 18: price = 5 elif age < 65: price = 10 elif age >= 65: price = 5 print(“Your admission cost is $” + str(price) + “.”) 【14】字典alien_0 = {‘color’: ‘green’, ‘points’: 5} #字典以键-值对的模式存储数据,类似于 Map print(alien_0[‘color’]) #获取 alien_0 中 color 键对应的值,即 green print(alien_0[‘points’]) alien_0[‘x_position’] = 0 #动态添加新键 x_position alien_0[‘y_position’] = 25 alien_0 = {} #创建空字典 alien_0[‘color’] = ‘green’ #为字典赋值 alien_0[‘points’] = 5 print(alien_0) alien_0[‘color’] = ‘red’ #动态修改 alien_0[‘color’] 的值为 red del alien_0[‘color’] #删除键值 ‘color’ favorite_languages = { #多对象存储 ‘jen’: ‘python’, ‘sarah’: ‘c’, ‘edward’: ‘ruby’, ‘phil’: ‘python’, } for name, language in favorite_languages.**items**(): #遍历字典 print(name.title() + “‘s favorite language is “ + language.title() + “.”) for name in favorite_languages.**keys**(): #遍历所有键 print(name.title()) for name in sorted**(favorite_languages.keys()):** #按特定的顺序遍历字典 print(name.title() + “, thank you for taking the poll.”) for language in favorite_languages.**values**(): #遍历所有的值 print(language.title()) for language in set**(favorite_languages.values()):** #通过对包含重复元素的列表调用 set() ,可让 Python 找出列表中独一无二的元素,并使用这些元素来创建一个集合。 print(language.title()) alien_0 = {‘color’: ‘green’, ‘points’: 5} alien_1 = {‘color’: ‘yellow’, ‘points’: 10} alien_2 = {‘color’: ‘red’, ‘points’: 15} aliens = [alien_0, alien_1, alien_2] #字典嵌套 pizza = { ‘crust’: ‘thick’, ‘toppings’: [‘mushrooms’, ‘extra cheese’], #字典中存储列表 } 【15】用户输入和 while 循环message = input(“Tell me something, and I will repeat it back to you: “) #用户输入(python3),python2.7使用 raw_input() print(message) >>> age = input(“How old are you? “) How old are you? 21 >>> age = int(age) #int() 将字符串转换成了数值表示 >>> age >= 18 True >>> 4 % 3 #求模运算 1 While 使用: prompt = “\\nPlease enter the name of a city you have visited:” prompt += “\\n(Enter ‘quit’ when you are finished.) “ while True: city = input(prompt) if city == ‘quit’: break #直接退出循环 else: print(“I’d love to go to “ + city.title() + “!”) -————————————- current_number = 0 while current_number < 10: current_number += 1 if current_number % 2 == 0: continue #返回到循环开头继续执行,并不直接退出 print(current_number) 【16】函数def describe_pet(pet_name, animal_type=’dog’): “”” 显示宠物的信息 “”” print(“\\nI have a “ + animal_type + “.”) print(“My “ + animal_type + “‘s name is “ + pet_name.title() + “.”) # 一条名为 Willie 的小狗 describe_pet(‘willie’) #以下为等效调用 describe_pet(pet_name=’willie’) # 一只名为 Harry 的仓鼠 describe_pet(‘harry’, ‘hamster’) describe_pet(pet_name=’harry’, animal_type=’hamster’) describe_pet(animal_type=’hamster’, pet_name=’harry’) -———————————————————————— def build_person(first_name, last_name): “”” 返回一个字典,其中包含有关一个人的信息 “”” person = {‘first’: first_name, ‘last’: last_name} #返回字典 return person -———————————————————————— print_models(unprinted_designs[:], completed_models) #切片生成列表副本进行操作,原 列表不受影响 -———————————————————————— def make_pizza(*toppings): #形参名 *toppings 中的星号让 Python 创建一个名为 toppings 的 空元组,并将收到的所有值都封装到这个元组中。 “”” 概述要制作的比萨 “”” print(“\\nMaking a pizza with the following toppings:”) for topping in toppings: print(“- “ + topping) make_pizza(‘pepperoni’) make_pizza(‘mushrooms’, ‘green peppers’, ‘extra cheese’) -————————————————————————- def build_profile(first, last, \\user_info): #形参 **user_info 中的两个星号让 Python 创建一 个名为 user_info 的空字典,并将收到的所有名称 — 值对都封装到这个字典中。 “”” 创建一个字典,其中包含我们知道的有关用户的一切 “”” profile = {} profile[‘first_name’] = first profile[‘last_name’] = last for key, value in user_info.items(): profile[key] = value return profile user_profile = build_profile(‘albert’, ‘einstein’, location=’princeton’, field=’physics’) print(user_profile) 【17】函数与模块导入import pizza pizza.make_pizza(16, ‘pepperoni’) pizza.make_pizza(12, ‘mushrooms’, ‘green peppers’, ‘extra cheese’) Python 读取这个文件时,代码行 import pizza 让 Python 打开文件 pizza.py ,并将其中的所有函数都复制到这个程序中。你看不到复制的代码,因为这个程序运行时, Python 在幕后复制这些代码。你只需知道,在 making_pizzas.py 中,可以使用 pizza.py 中定义的所有函数。 from module_name import function_name make_pizza(16, ‘pepperoni’) make_pizza(12, ‘mushrooms’, ‘green peppers’, ‘extra cheese’) 若使用这种语法,调用函数时就无需使用句点。由于我们在 import 语句中显式地导入了函数 make_pizza() ,因此调用它时只需指定其名称。 from pizza import make_pizza as mp mp(16, ‘pepperoni’) mp(12, ‘mushrooms’, ‘green peppers’, ‘extra cheese’) 上面的 import 语句将函数 make_pizza() 重命名为 mp() ;在这个程序中,每当需要调用 make-_pizza() 时,都可简写成 mp() ,而 Python 将运行 make_pizza() 中的代码,这可避免与这个程序可能包含的函数 make_pizza() 混淆。 import pizza as p p.make_pizza(16, ‘pepperoni’) p.make_pizza(12, ‘mushrooms’, ‘green peppers’, ‘extra cheese’) 上述 import 语句给模块 pizza 指定了别名 p ,但该模块中所有函数的名称都没变。 from pizza import * make_pizza(16, ‘pepperoni’) make_pizza(12, ‘mushrooms’, ‘green peppers’, ‘extra cheese’) import 语句中的星号让 Python 将模块 pizza 中的每个函数都复制到这个程序文件中。 【18】类 class Dog(): “”” 一次模拟小狗的简单尝试 “”” def init(self, name, age): #初始化方法(相当于构造函数)前后都是两个下划线 “”” 初始化属性 name 和 age””” self.name = name self.age = age def sit(self): # self 代表类实例的本身,相当于 this “”” 模拟小狗被命令时蹲下 “”” print(self.name.title() + “ is now sitting.”) def roll_over(self): “”” 模拟小狗被命令时打滚 “”” print(self.name.title() + “ rolled over!”) my_dog = Dog(‘willie’, 6) #根据类创建实例 my_dog.sit() #调用方法 my_dog.roll_over() 【19】类的继承python3: -——————————————————————————— class Car(): #父类 “”” 一次模拟汽车的简单尝试 “”” def init(self, make, model, year): self.make = make self.model = model self.year = year self.odometer_reading = 0 def get_descriptive_name(self): long_name = str(self.year) + ‘ ‘ + self.make + ‘ ‘ + self.model return long_name.title() def read_odometer(self): print(“This car has “ + str(self.odometer_reading) + “ miles on it.”) def update_odometer(self, mileage): if mileage >= self.odometer_reading: self.odometer_reading = mileage else: print(“You can’t roll back an odometer!”) def increment_odometer(self, miles): self.odometer_reading += miles -———————————————————————————— class ElectricCar(Car): #子类 “”” 电动汽车的独特之处 “”” def init(self, make, model, year): “”” 初始化父类的属性 “”” super().init(make, model, year) my_tesla = ElectricCar(‘tesla’, ‘model s’, 2016) print(my_tesla.get_descriptive_name()) python2.7: -————————————————————————————– class Car(object): #父类,需指定object def init(self, make, model, year): –snip– class ElectricCar(Car): #子类 def init(self, make, model, year): super(ElectricCar, self).init(make, model, year) –snip– 【20】从模块中导入类导入单个类: from car import Car import 语句让 Python 打开模块 car ,并导入其中的 Car 类。这样我们就可以使用 Car 类了,就像它是在这个文件中定义的一样。 导入多个类: from car import Car, ElectricCar 从一个模块中导入多个类时,用逗号分隔了各个类。导入必要的类后,就可根据需要创建每个类的任意数量的实例。 导入整个模块: import car my_beetle = car.Car(‘volkswagen’, ‘beetle’, 2016) print(my_beetle.get_descriptive_name()) my_tesla = car.ElectricCar(‘tesla’, ‘roadster’, 2016) print(my_tesla.get_descriptive_name()) 导入了整个 car 模块,并使用语法 module_name.class_name 访问需要的类。 导入模块中所有的类: from module_name import * 不推荐使用这种导入方式,其原因有二。首先,如果只要看一下文件开头的 import 语句,就能清楚地知道程序使用了哪些类,将大有裨益;但这种导入方式没有明确地指出你使用了模块中的哪些类这种导入方式还可能引发名称方面的困惑。如果你不小心导入了一个与程序文件中其他东西同名的类将引发难以诊断的错误。 在一个模块中导入另一个模块: from car import Car from electric_car import ElectricCar 从模块 car 中导入了 Car 类,并从模块 electric_car 中导入 ElectricCar 类。 【21】Python标准库举例from collections import OrderedDict favorite_languages = OrderedDict() # OrderedDict 实例记录了键 — 值对的添加顺序 favorite_languages[‘jen’] = ‘python’ favorite_languages[‘sarah’] = ‘c’ favorite_languages[‘edward’] = ‘ruby’ favorite_languages[‘phil’] = ‘python’ for name, language in favorite_languages.items(): print(name.title() + “‘s favorite language is “ + language.title() + “.”) 【22】读取文件with open(‘pi_digits.txt’) as file_object: #打开文件并返回文件对象 file_object contents = file_object.read() #读取文件对象的所有内容 print(contents) with open(‘text_files**/**filename.txt’) as file_object: #Linux文件路径 with open(‘text_files****filename.txt’) as file_object: #Windows文件路径 -—————————————- filename = ‘pi_digits.txt’ with open(filename) as file_object: for line in file_object: #逐行读取 print(line.rstrip()) #行的末尾都有一个看不见的换行符,而 print 语句也会加上 一个换行符,用rstrip()函数可以去除换行符 -—————————————- filename = ‘pi_digits.txt’ with open(filename) as file_object: lines = file_object.readlines() # readlines() 从文件中读取每一行,并将其存储在一个列表中 for line in lines: print(line.rstrip()) 【23】写入文件filename = ‘programming.txt’ with open(filename, ‘w’**) as file_object:** #以 写入模式 打开这个文件 file_object.write(“I love programming.”) 打开文件的模式有:读取模式 ( ‘r’ )、 写入模式 ( ‘w’ )、 附加模式 ( ‘a’ )或让你能够读取和写入文件的模式( ‘r+’ ). 以写入( ‘w’ )模式打开文件时千万要小心,因为如果指定的文件已经存在, Python 将在返回文件对象前清空该文件**。** filename = ‘programming.txt’ with open(filename, ‘w’) as file_object: file_object.write(“I love programming.\\n“) #写入多行,换行符 \\n file_object.write(“I love creating new games.\\n“) filename = ‘programming.txt’ with open(filename, ‘a’**)** as file_object: #以附加模式写入文件,原来的文件不清空 file_object.write(“I also love finding meaning in large datasets.\\n”) file_object.write(“I love creating apps that can run in a browser.\\n”) 【24】异常print(“Give me two numbers, and I’ll divide them.”) print(“Enter ‘q’ to quit.”) while True: first_number = input(“\\nFirst number: “) if first_number == ‘q’: break second_number = input(“Second number: “) try: answer = int(first_number) / int(second_number) except ZeroDivisionError: print(“You can’t divide by 0!”) else: print(answer) 【25】存储数据import json numbers = [2, 3, 5, 7, 11, 13] filename = ‘numbers.json’ with open(filename, ‘w’) as f_obj: json.dump(numbers, f_obj) #用 json.dump() 来存储数字列表 with open(filename) as f_obj: numbers = json.load(f_obj) #用函数 json.load() 加载存储在numbers.json 中的信 息,并将其存储到变量 numbers 中 print(numbers) 【26】测试函数-——————————————————- import unittest #导入单元测试模块 from name_function import get_formatted_name class NamesTestCase(unittest.TestCase): #单元测试类继承自 unittest.TestCase “”” 测试 name_function.py””” def test_first_last_name(self): #待测试的方法均要求以 test 开头 “”” 能够正确地处理像 Janis Joplin 这样的姓名吗? “”” formatted_name = get_formatted_name(‘janis’, ‘joplin’) self.assertEqual(formatted_name, ‘Janis Joplin’) #用断言判断测试结果是否正确 unittest.main() #让 Python 运行这个文件中的测试 -————————————————————– import unittest from survey import AnonymousSurvey class TestAnonymousSurvey(unittest.TestCase): “”” 针对 AnonymousSurvey 类的测试 “”” def setUp(self): “”” 创建一个调查对象和一组答案,供使用的测试方法使用 “”” question = “What language did you first learn to speak?” self.my_survey = AnonymousSurvey(question) self.responses = [‘English’, ‘Spanish’, ‘Mandarin’] def test_store_single_response(self): “”” 测试单个答案会被妥善地存储 “”” self.my_survey.store_response(self.responses[0]) self.assertIn(self.responses[0], self.my_survey.responses) def test_store_three_responses(self): “”” 测试三个答案会被妥善地存储 “”” for response in self.responses: self.my_survey.store_response(response) for response in self.responses: self.assertIn(response, self.my_survey.responses) unittest.main() unittest.TestCase 类包含方法 **setUp()** ,让我们**只需创建这些对象一次**,并在每个测试方法中使用它们。如果你在 TestCase 类中包含了方法 setUp() , Python 将先运行它,再运行各个以 test_ 打头的方法。这样,在你编写的每个测试方法中都可使用在方法 setUp() 中创建的对象了。【27】项目:外星人入侵安装pip:https://pip.pypa.io/en/stable/installing/ 安装 Pygame:**$ sudo apt-get install python-pygam** 具体项目编程过程略。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"},{"name":"python","slug":"python","permalink":"https://www.smartonline.net.cn/tags/python/"}]},{"title":"Netty相关","slug":"Netty","date":"2019-12-12T06:38:07.000Z","updated":"2019-12-12T09:07:17.095Z","comments":true,"path":"2019/12/12/Netty/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/12/Netty/","excerpt":"记录 Netty 学习及使用过程中的问题。","text":"记录 Netty 学习及使用过程中的问题。 【1】默认情况下,Netty服务端起多少线程?何时启动? 默认创建2倍CPU个数的NioEventLoop(当nThread参数为默认值0时)线程,在首次调用execute方法时启动线程。如果是当前NioEventLoop线程,则先判断当前线程是否已经启动,未启动则启动线程;如果是外部线程调用execute方法,则调用startThread()方法,判断当前线程是否启动,未启动则启动当前线程。 【2】Netty是如何解决jdk空轮询bug的? 判断当前时间减去Selector阻塞获取的时间与超时时间比较,如果大于超时时间,则表明是一次正常的轮询,selectCnt++,否则表明此次轮询为空轮询。当空轮询次数大于一个阈值(默认512)时,创建一个新的Selector,并将旧的参数全部复制到新的Selector上,使用新的Selector进行后续的操作,取消旧的Selector继续进行空轮询操作。 【3】Netty如何保证异步串行无锁化? 首先当前成调用Channel相关方法的时候,使用inEventLoop方法判断是内部线程还是外部线程,Netty将外部线程封装成Task,放入一个特定的队列中,在NioEventLoop执行的特殊阶段挨个执行这些外部线程(Task),保证异步串行无锁化。 【4】Netty是在哪里检测有新连接介入的? 【5】新连接是怎样注册到NioEventLoop线程的?","categories":[],"tags":[{"name":"Java","slug":"Java","permalink":"https://www.smartonline.net.cn/tags/Java/"}]},{"title":"Fiddler 简单使用","slug":"Fiddler","date":"2019-12-10T06:26:16.000Z","updated":"2019-12-12T05:51:06.343Z","comments":true,"path":"2019/12/10/Fiddler/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/Fiddler/","excerpt":"本篇记录一下使用 Fiddler 抓取网站 WebAPI 做一个抓取网页图片的小爬虫程序的大致过程,涉及到的步骤大致有以下几步: Fiddler 获取网页访问特定网址的动作极其参数和返回,对于 JSON 格式的 WebAPI 返回值进行分析。 通过在线 JSON 转 POJO 工具进行 Java Bean 的生成。 使用 Retrofit2 封装对 WebAPI 的访问,结合SpringBoot JPA将信息存储到 MySQL 数据库。","text":"本篇记录一下使用 Fiddler 抓取网站 WebAPI 做一个抓取网页图片的小爬虫程序的大致过程,涉及到的步骤大致有以下几步: Fiddler 获取网页访问特定网址的动作极其参数和返回,对于 JSON 格式的 WebAPI 返回值进行分析。 通过在线 JSON 转 POJO 工具进行 Java Bean 的生成。 使用 Retrofit2 封装对 WebAPI 的访问,结合SpringBoot JPA将信息存储到 MySQL 数据库。 对于未登录状态的 WebAPI 访问,进行 Cookie 的获取以及设置。 多线程并发查询,加快数据的获取速度。 已获取到的WebAPI示例。 一、Fiddler抓取WebAPI接口 Fiddler是一款网络抓包分析工具,与WireShark一样,都可以抓取主机访问网络的数据流,具体的使用过程网上有很多资料,这里就不详细描述了。 在这里以下面的网址为例,来分析网站相关页面的WebAPI: https://bcy.net/ 打开Fiddler,设置过滤条件,将与我们的目标返回无关的信息过滤掉,然后访问上面的主页,即可在Fiddler界面上看到访问链接以及返回信息: 二、JSON返回值在线解析生成JavaBean 双击左侧的访问链接,在右侧就会显示Request和Response,其中Request中有访问的Header详细信息,Response中有返回数据的具体内容。我们这里以JSON数据为例: 在网上搜索在线JSON转实体类网站,将Response返回的JSON数据输入解析框,设置好Class名称以及包名,生成JavaBean即可下载由JSON转化成的Java POJO,后面就可以使用这些POJO结合Retrofit的Gson解析获取最终的存储对象。 三、Retrofit2对WebAPI访问的封装,以及SpringBoot JPA 对获取到的结果的存储 Retrofit2是一个网络访问框架,具体的使用细节网上也有很多资料,这里也不详细展开,下面主要分析大致的使用过程以及遇到的一些问题。 首先设计访问接口的Service: 12345678910111213141516171819202122232425262728293031323334public interface BcyRequestService { /** * RxJava 模式下获取指定的 Tag 标签下的 Works * @param circleId 指定 Tag 的 ID(eg: circle_id=71978) * @param since 起始页(eg: since=rec:3) * @param sortType 排序规则(eg: sort_type=1) * @param gridType 单页显示数量(eg: grid_type=10) * @return JsonBean */ @GET(\"common/circleFeed\") Observable<TagsAnalyResult> getBcyTagDetail( @Query(\"circle_id\") String circleId, @Query(\"since\") String since, @Query(\"sort_type\") String sortType, @Query(\"grid_type\") String gridType ); /** * 获取指定的 Tag 标签下的 Works * @param circleId 指定 Tag 的 ID(eg: circle_id=71978) * @param since 起始页(eg: since=rec:3) * @param sortType 排序规则(eg: sort_type=1) * @param gridType 单页显示数量(eg: grid_type=10) * @return JsonBean */ @GET(\"common/circleFeed\") Call<TagsAnalyResult> getBcyTagDetailCommon( @Query(\"circle_id\") String circleId, @Query(\"since\") String since, @Query(\"sort_type\") String sortType, @Query(\"grid_type\") String gridType );} 然后对Retrofit2客户端进行初始化: 123456789101112private void initRetrofitAPI() { Retrofit retrofit = new Retrofit.Builder() // 设置OKHttpClient,如果不设置会提供一个默认的 .client(getClient()) //设置baseUrl .baseUrl(BASE_API_URL) //添加Gson转换器 .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build(); bcyApiService = retrofit.create(BcyRequestService.class);} 上面的 getClient() 方法是自定义的一个HttpClient,主要是针对Cookie的获取与设置进行了一些修改: 123456789101112131415161718192021222324252627282930313233343536373839404142/** * 初始化 OkHttpClient *添加Cookie拦截器 * @return */ private static OkHttpClient getClient() { OkHttpClient.Builder client = new OkHttpClient().newBuilder(); client.interceptors().add(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { // 获取 Cookie okhttp3.Response resp = chain.proceed(chain.request()); List<String> cookies = resp.headers(\"Set-Cookie\"); String cookieStr = \"\"; if (cookies != null && cookies.size() > 0) { for (int i = 0; i < cookies.size(); i++) { cookieStr += cookies.get(i); } log.info(\"COOKIE-SET->\" + cookieStr); UserUtil.getSingletonInstance().setCookie(cookieStr.split(\";\")[0]); } return resp; } }); client.interceptors().add(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { // 设置 Cookie String cookieStr = UserUtil.getSingletonInstance().getCookie(); log.info(\"COOKIE-GET->\" + cookieStr); if (!cookieStr.isEmpty()) { return chain.proceed(chain.request().newBuilder().header(\"Cookie\", cookieStr).build()); } return chain.proceed(chain.request()); } }); client.connectTimeout(TIMOUT, TimeUnit.MILLISECONDS); client.writeTimeout(TIMOUT, TimeUnit.MILLISECONDS); client.readTimeout(TIMOUT, TimeUnit.MILLISECONDS); return client.build(); } 最后进行WebAPI的调用: 1234567891011121314151617181920212223242526272829303132/**同步访问*/ public void getBcyTagDetailsCommon(String id, int page, BcyCallback<Picture> callback) throws IOException { Call<TagsAnalyResult> call = bcyApiService.getBcyTagDetailCommon(id,\"rec:\"+ String.valueOf(page), \"1\", \"10\"); int items_count = 0; if (call != null) { Response<TagsAnalyResult> response = call.execute(); for (Items items : response.body().getData().getItems()) { for (Image_list img : items.getItem_detail().getImage_list()) { Picture picture = new Picture(); picture.setUserName(items.getItem_detail().getUname()); picture.setUid(String.valueOf(items.getItem_detail().getUid())); picture.setAlbumName(items.getItem_detail().getWork()); picture.setAlbumId(String.valueOf(items.getItem_detail().getWid())); picture.setCover(items.getItem_detail().getCover()); picture.setStars(items.getItem_detail().getLike_count()); StringBuilder tags = new StringBuilder(); for (Post_tags tag : items.getItem_detail().getPost_tags()) { tags.append(tag.getTag_name()); tags.append(\";\"); } picture.setTags(tags.toString()); picture.setPath(img.getPath()); picture.setOriginPath(covert2OriginPath(img.getPath())); callback.onNet(picture); items_count ++; } } callback.onSuccess(\"Completed! ItemsCount=\" + String.valueOf(items_count)); } else { callback.onFailure(\"CallbackBULL->[since=rec:\" + String.valueOf(page) + \"; circle_id=\" + id + \"]\"); } } 获取到Picture对象之后就可以进行对象的存储了。 基本逻辑已经确立,在进行WebAPI查询的过程中可能存在一个比较严重的异常: 1com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_OBJECT at line 1 column 57992 path $.data.items[19].item_detail.replies[0] 这个异常产生的原因是:在使用JSON字符串产生JavaBean时: 123456789101112131415161718192021222324252627282930313233343536373839public class Item_detail { private String item_id; private long uid; private String uname; private String avatar; private String odin_uid; private int value_user; private String vu_description; private String follow_state; private List<Rights> rights; private long ctime; private String type; private String plain; private int word_count; private String cover; private List<Multi> multi; private int pic_num; private String work; private long wid; private String real_name; private String work_cover; private List<Post_tags> post_tags; private int like_count; private boolean user_liked; private int reply_count; private int share_count; private List<Props> props; private List<String> replies; //此处存在异常 private int visible_level; private boolean user_favored; private List<Image_list> image_list; private Top_list_detail top_list_detail; private Extra_properties extra_properties; private String editor_status; private boolean repostable; private int repost_count; private int visible_status; private String visible_status_msg;} 上述 “private List replies;” 参数将Replie类型解析成为了String类型。 由于我们获取到的JSON返回是用一个特定的Request得到的,所以获取到的JSON字符串中的列表很可能为空,比如像下面的一个列表: 1replies : [] 这种情况下,Gson就会将replies列表解析成为List。我们不知道列表里面具体的类型是什么,所以当生成的JavaBean中出现String列表的时候就要注意了,绝大部分情况下这个列表不是String列表,而是有一个实体类与之对应,我们能做的就是遇到这个错误的时候找到对应的实体类,并将String列表修改成为对应的实体列表。 当然,如果已知具体的WebAPI定义,则不存在这个问题。 四、Cookie设置 在进行Request访问时,会从服务端获取Cookie,再将Cookie中的用户信息设置到Request的Cookie中,达到认证的功能,以下为Response中获取的Cookie以及Request Header中发送的Cookie示例: Request Header: 12345678910GET https://bcy.net/apiv3/common/hotCircleList?offset=20&_signature=aHtaaAAgEBWDbr3j6eLWfmh7WnAADWO HTTP/1.1Host: bcy.netUser-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0Accept: */*Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflate, brReferer: https://bcy.net/X-Requested-With: XMLHttpRequestConnection: keep-aliveCookie: tt_webid=6765672592922707470; Hm_lvt_330d168f9714e3aa16c5661e62c00232=1575255918,1575350791,1575681218,1575889227; SLARDAR_WEB_ID=cd3a2238-841b-4e08-b0c1-6e077f3ae418; lang_set=zh; _ga=GA1.2.734520056.1575263761; msh=36tNx_itQKzpF5GLXfBTVteDxk8; passport_auth_status=e3d3ca9ace75ac8ef9c00b53a30bc837%2C; sid_guard=e64f91b94ddd3984e4fbf4fed0d03d12%7C1575266305%7C5184000%7CFri%2C+31-Jan-2020+05%3A58%3A25+GMT; uid_tt=708b99c250828a189dce12014f8aa356; sid_tt=e64f91b94ddd3984e4fbf4fed0d03d12; sessionid=e64f91b94ddd3984e4fbf4fed0d03d12; main_sid=e8e9945d541b8d962ea745142ade2547; __tea_sdk__ssid=undefined; _bcy_user_id=U2FsdGVkX181UhlS1LtAL1QLNCWhGcQ9annTwWC4+Y9epGqhfOuRucw7bG53x39Ir/aB5WPSHpE12Y9e7HPKOh1Qom0CfKObsfqjQV9AkcA=; _csrf_token=156c84b03e3a2808639f8fe166752c11; mobile_set=no Response Cookie: 12345678910Response sent 203 bytes of Cookie data: Set-Cookie: _bcy_user_id=U2FsdGVkX19ubdzzra5Tgm3KwME9JQ9OfpCFJv/rkGFYdgwhONE8I/gtcEB50/xfInpY2Mya73UajXPQZ1NGxzKSVUuJLPPseohWU//YKrw=; path=/; expires=Thu, 12 Dec 2019 04:35:24 GMT; domain=.bcy.net; secure; httponlyResponse sent 95 bytes of Cookie data: Set-Cookie: mobile_set=no; path=/; expires=Thu, 12 Dec 2019 04:35:24 GMT; domain=.bcy.net; secure; httponlyResponse sent 100 bytes of Cookie data: Set-Cookie: tt_webid=6765672592922707470; path=/; expires=Sun, 15 Dec 2019 15:52:12 GMT; domain=.bcy.net; secureThis response did not contain a P3P Header. 需要注意的是,我们初次访问一个WebAPI接口的时候,Request Header Cookie中的: 1_bcy_user_id=U2FsdGVkX181UhlS1LtAL1QLNCWhGcQ9annTwWC4+Y9epGqhfOuRucw7bG53x39Ir/aB5WPSHpE12Y9e7HPKOh1Qom0CfKObsfqjQV9AkcA=; _csrf_token=156c84b03e3a2808639f8fe166752c11; 字段是为空的,需要在Response Cookie中的 “Set-Cookie” 获取,是一种先登录,后访问的模式。具体的设置Request Header Cookie方法以及获取Response Cookie的方法在Retrofit2的初始化getClient()方法里面有详细的过程。 五、多线程并发查询 在SpringBoot的入口类开启异步: 123456789@EnableAsync@SpringBootApplicationpublic class BcyappserverApplication { public static void main(String[] args) { SpringApplication.run(BcyappserverApplication.class, args); }} 之后再需要异步处理的方法上添加异步注解: 123456@Async public void asynGetBcyTagDetailPictures(String tagId, int pageIndex, CountDownLatch countDownLatch) { getBcyTagDetailCommon(tagId, pageIndex); countDownLatch.countDown(); log.info(\"CountDownLatch --- \"+countDownLatch.getCount() + \" --- completed!\"); } 这里的 CountDownLatch 对象保证多线程访问时所有的线程任务全部处理完毕之后才退出主线程,避免子线程刚开启时主线程退出造成所有的子线程全部退出,任务不能正常执行的情况。 对于多线程数据存储的情况,将存储方法用 synchronized 加锁: 12345private synchronized void savePicture(Picture pic) { if ( pictureRepository.findByPath(pic.getPath()) == null) { pictureRepository.save(pic); }} 避免数据的重复插入。 六、已获取到的WebAPI示例 1234567891011121314151617181920212223【1】HotTag获取https://bcy.net/apiv3/common/hotTags【2】hotCircleList获取https://bcy.net/apiv3/common/hotCircleList?offset=20【3】circle解析https://bcy.net/apiv3/common/circleFeed?circle_id=71978&since=rec:3&sort_type=1&grid_type=10【4】用户Works获取https://bcy.net/apiv3/user/selfPosts?uid=221571【5】Search搜索https://bcy.net/search/all?k=%E6%B5%85%E6%B5%85ASAKI【6】RankWeekly周榜首页:https://bcy.net/apiv3/rank/list/channelItemInfo?channel_id=6618800694038102275&ttype=cos&sub_type=week详情页:https://bcy.net/apiv3/rank/list/itemInfo?p=2&ttype=cos&sub_type=week&date=20191202【7】RankLastDay日榜首页:https://bcy.net/apiv3/rank/list/channelItemInfo?channel_id=6618800694038102275&ttype=cos&sub_type=lastday【8】详情页:https://bcy.net/apiv3/rank/list/itemInfo?p=2&ttype=cos&sub_type=lastday&date=20191202【9】RankNewPeople新人榜首页:https://bcy.net/apiv3/rank/list/channelItemInfo?channel_id=6618800694038102275&ttype=cos&sub_type=newPeople详情页:https://bcy.net/apiv3/rank/list/itemInfo?p=2&ttype=cos&sub_type=newPeople&date=20191202【10】用户作品获取:以最后一个Item的since标记为下一页的请求起始https://bcy.net/apiv3/user/selfPosts?uid=221571https://bcy.net/apiv3/user/selfPosts?uid=221571&since=6218245164293447438https://bcy.net/apiv3/user/selfPosts?uid=221571&since=6168849853523713806","categories":[],"tags":[{"name":"java","slug":"java","permalink":"https://www.smartonline.net.cn/tags/java/"}]},{"title":"《乡土中国》","slug":"乡土中国","date":"2019-12-10T06:21:27.000Z","updated":"2019-12-16T14:22:09.614Z","comments":true,"path":"2019/12/10/乡土中国/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/%E4%B9%A1%E5%9C%9F%E4%B8%AD%E5%9B%BD/","excerpt":"一、核心概念 礼俗社会:没有具体目的,只是因为在一起生长而发生的社会 法理社会:为了完成一件任务而结合的社会 特殊语言:亲密社群中所使用的象征体系的一部分,如表情动作等,如行话 时间阻隔:个人的今昔之隔,社会的世代之隔","text":"一、核心概念 礼俗社会:没有具体目的,只是因为在一起生长而发生的社会 法理社会:为了完成一件任务而结合的社会 特殊语言:亲密社群中所使用的象征体系的一部分,如表情动作等,如行话 时间阻隔:个人的今昔之隔,社会的世代之隔 学习:改造本能,反复的做,靠时间的磨练,使一个人惯于新的做法,即打破个人今昔之隔 文化:社会共同的经验的积累 (西方)团体格局:捆柴似的组成的有一定界限的团体中,人与人之间关系相同,等级事先规定的社会组织所形成的的格局。(一个人可有多重身份) (乡土中国)差序格局:以“己”为中心,具有同心圆性质的,通过有差等的次序和一根根私人联系而构成的网络,富有伸缩性。与“伦”类似 个人主义:团体中,一方面是平等观念,指在同一团体中各分子的地位相等,个人不能侵犯大家的权利;一方面是宪法观念,指团体不能抹杀个人,只能在个人们所愿意交出的一分权利上控制个人 自我主义:在差序格局中,随时随地是有一个“己”为中心的 代理者:团体格局中执行团体意志的人 社会圈子:差序格局中的基本社群,即“小家族” 事业组织:乡土中国的家庭或家族,有生育、政治、经济、宗教等功能。 生活堡垒:西洋家庭团体中,夫妇是主轴,共同经营生育事务,子女在这团体中是配角、成长后便离开。他们政治经济宗教等功能有其他团体负担。两性之间的感情是凝合的力量,感情的发展,使他们的家庭成了获取生活上安慰的中心 感情定向:文化所规定个人感情可以发展的方向 亚普落式:宇宙的安排有一个完善的秩序,这个秩序超于人力的创造,人不过是去接受它,安于其位,维持它;但是人连维持它的力量都没有,天堂遗失了,黄金时代过去了 浮士德式:把冲突看做存在的基础,生命是阻碍的克服;没有了阻碍,生命也就失去了意义。他们把前途看成无尽的创造过程,不断的变 礼:社会公认对的行为规范,从教化中养成了个人的敬畏之感,使人服膺,靠传统推行 法:靠国家推行 传统:社会所积累的经验,用来维持“礼”这种规范的无形权力 横暴权力:从社会冲突方面,权力表现在社会不同团体或阶层间主从的形态里,权力是维持这种关系所必需的手段,是压迫性质的,有经济约束 同意权力:从社会合作方面,由于社会分工使得每个人都不能不求人地生活。其权力的基础是社会契约,权利与义务要相统一 教化权力:发生在社会继替的过程中,教化性的权力,是爸爸式的,为了教化而教化,带来长老统治 时势权力:“时势造英雄”,他提得出办法,有能力组织新的试验,能获得别人的信任。这种人可以支配其他的群众,发生了一种权力 长老统治:难以用民主和不民主来衡量中国社会下的权宜之计 社会继替:社会成员新陈代谢的过程 社会变迁:社会结构本身的变动 血缘:人和人的权利和义务根据亲属关系来决定,是身份社会的基础 地缘:血缘的空间投影,从商业里发展出来的社会关系,是契约社会的基础 欲望:人类在取舍之间的根据,规定了人类行为的方向,是文化事实 需要:自觉的生存条件 二、研究方法1.研究方法:社区分析 《乡土中国》是“社会结构的分析,偏于通论性质,在理论上总结并开导实地研究”。社区分析是一全盘社会结构的格式作为研究对象,在一定时空坐落中描画出这一地方人民所赖以生活的社会结构。在这一层上可以说是和历史学工作相同的。第二步是比较研究,在此产生了“格式”的概念。在这一方面,又与人类学相通。 2.研究范围:乡村社区、文化传递、家族制度、道德观念、权力结构、社会规范、社会变迁等诸多方面观察,剖析了乡土社会的结构及其特点。 3.研究目的:通过提炼出一些具体认识的概念,帮助我们理解中国的乡土社会。 4.有关书籍:《江村经济》、《生育制度》 三、读后感 为什么我的眼里常含泪水?因为我对这土地爱的深沉。–艾青 《乡土中国》中说道:“从土里长出过光荣的历史,自然也会受到土的束缚。”中国人的历史发源于土,人与人的关系亦然。在乡土社会之中,稳定的结构以自然环境为背景;即使是放在现代社会来说,我们中国人之间的“关系”也如此。 城里人认为乡下人是“愚”的。何为愚?“愚”就是文盲,不识字。可是乡土社会之中人与人之间根本不需要文字阿。文字是时间传递和空间传播的工具,语言则是特殊的文字。语言使用于社群之中,在特殊情景下使用,这样便衍生出了“行话”,在面对面的关系上,你的一颦一簇,我都能理解。因此文字下乡如果没有发生乡土性的基层变化,是很难推行的。 在人与人的沟通之中,产生了社会结构。与西洋人的“团体格局”不同,我们的社会结构称作“差序格局”。这是一种依赖亲缘和地缘关系,通过有差等的次序和一根根私人联系而构成的网络,富有伸缩性。我们常常以自己为中心,家则是以自己为石子荡漾出的涟漪。以一场我小表弟的“满月酒”为例,说大不大说小不小,那么请谁便是一个有趣的话题。而我发现这样的宴席的宴请客人常常更倾向于地缘关系主导,父系家庭为主轴。因此我十分赞同费老先生说的“家族常常往父系方向延伸”这一说法。这是一种自古以来便有的“伦”。 正是因为“伦”的存在,我们有了男女有别的意识,正是因为差异的碰撞而产生了火花,我们有了家庭甚至说——家族。在这类问题上,与社会结构的研究方法类似,费老再次与西方作对比。 西方家庭团体中,夫妇是主轴,两性之间的感情是凝合的力量,两性感情的发展,使他们的家庭成了获取生活上安慰的中心,即“生活堡垒”。而我们的家庭的主轴则是在父子之间,配轴在婆媳之间,这两轴却都被事业的需要而排斥了普通的感情,因此在农村里,夫妇之间感情的淡漠也是日常可见的现象。乡村中的夫妻往往不需要多说几句话也能亲密无间的合作,这足以说明,稳定社会关系的力量,不是感情,而是了解。亲密感觉和激动性的感情不相同。它是契洽,发生持续作用;它是无言的不像感情奔放时铿然有声,歌哭哀号是激动时不缺的配合。 社会秩序规范着个性,为了秩序的维持,一切足以引起破坏秩序的要素都被遏制着,可见“礼治秩序”是社会稳定不可或缺的因素。乡土中国中人治的力量大于法治。在这样的背景下,衍生出了权力。秦王扫六合,那是横暴权力的专制;阶级斗争红,那是长老权力的生长;新新时代好,这是时势权力的开启。当然还有西方在社会契约的制约下形成的同意权力。这四种权力其实都是由文化传统的沉淀而来,当旧有格局不在适应时代发展的大浪潮时,用浮士德的观点来看,冲突引起变更,这也许正是历史风云诡谲形成的迷人之处吧。 可是这些个权力如何在那段时间维系的呢?答案是:血缘加上地缘。血缘是稳定力量,地缘只是血缘的投影,而诸如“籍贯”等只是血缘的空间投影。一个没有当地籍贯的人,是不会被认作是本地人的,这是小农经济养出来的我们的为数不多的固执。在这里我们又可以回到最初土地的问题上。中国人在土里过了一辈子,血与肉都是泥土滋养的,如果你没有亲手抚摸过这片土地,又怎能说你是它的孩子? 走遍千山万水,为何还是最眷恋这片土地。 因为它融于我们血浓于水的亲情,包容了我们邻里打打闹闹的感情。从欲望到需要,我们也许会在车水马龙之中逐渐迷失自我。但是请你一定记得,土地的本味即是我们中国文化的风味,无论你走到哪,翻过喜马拉雅山或是穿越太平洋,都不要忘记我们的土地。 链接:https://www.jianshu.com/p/635ff6ff6eb7","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《代谢增长论》","slug":"代谢增长论","date":"2019-12-10T06:20:45.000Z","updated":"2019-12-16T14:08:51.940Z","comments":true,"path":"2019/12/10/代谢增长论/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/%E4%BB%A3%E8%B0%A2%E5%A2%9E%E9%95%BF%E8%AE%BA/","excerpt":"【1】历史作为检验经济学理论的自然实验:历史可以看做最好的自然实验。 【2】即使后来被证明是错误的思路,也给了后人“此路不通”的警示。","text":"【1】历史作为检验经济学理论的自然实验:历史可以看做最好的自然实验。 【2】即使后来被证明是错误的思路,也给了后人“此路不通”的警示。 【3】哈里斯人为:猪肉是一种可口的食物,能量转化率非常高,但猪不爱运动,喜欢潮湿,而对一个游牧民族来说,干旱草场的载畜量有限,要经常迁徙。若喜欢上猪肉的话,老百姓就不愿意迁徙,就会危及到他们的生活方式。所以当猪肉变成一种贵族的奢侈品,价格昂贵到大众无法消费的时候,就变成了一种禁忌。 【4】李约瑟讲到:在传统中国的许多发明中,凡是通过吸收人力增加产量的技术,就很容易被吸收,而节省人力的机械技术却难以在中国推广。 【5】发现一种界线,便是发现一种规律。 【6】稳定与发展,安全与机会是此消彼长的关系,有如鱼和熊掌不可兼得。 【7】在环境涨落明显,资源有限的条件下,保守者将战胜冒险者;在环境涨落不大、不断出现新资源的条件下,爱好风险的人更有机会探索新的资源。 【8】一个非常紧迫的任务就是要尽可能地搜集和保存生物基因,建立保存和积累这些基因的长期的文明宝库,很难将它们未来何时有价值。(云南种质资源库?!) 【9】新的更一般的理论可以把对立派别中的有益部分作为特殊情形包括在新理论之中。 【10】翻万卷书,游万里路,会百家才。 【11】毕达哥拉斯和柏拉图的信念:世界的规律可以用数学来完美地描述。 【12】中国的经验科学强调整体的复杂性,但是始终不去寻找解开复杂,简化运动的科学方法。 【13】科学实践上总是取应用最广泛、计算最简单的一种,除非新的实验事实迫使人们做进一步地修正。 【14】科学部门的出现成为第四次社会大分工,即信息生产和物质生产的分离。 【15】科学发展所依赖的基本规律可以作为历史现象分析的重要标准之一。 【16】对技术革命的分类论:1)技术路线论 2)技术倾向论 3)综合体系论 【17】技术革命分期论: 第一次技术革命—-工作机革命(1764~1830) 第二次技术革命—-传输机革命(1830~1945) –集中全力解决物质、能量、信息的传输问题。(交通网、能源网、信息网络、天基互联、地基增强) 第三次技术革命—-控制机革命(1945至今) 【18】能否健康消化外国技术,取决于自己科技政策和组织体制的适宜,而不在资本和技术本身。 【19】各部门、企业之间的隔绝和保密是技术进步的大敌。 【20】现实的经济系统是市场经济与计划经济的有机结合。 【21】只要我们把所有权和管理权分开处理,经济体制的改革就易于解放思想,广开门路。 【22】井田制的瓦解不是生产力发展引起的生产关系的变革,而是土地危机的加深逐渐造成的农业经济结构的恶化。 【23】从现代科学的观点来看,事务适度的稳定性和进化度是任何一个生物物种或社会组织能否具有生命力的首要特征。 【24】生态破坏是难以逆转的演化过程。 【25】我国农业发展的限制: 多山少地的自然条件形成的精耕细作的生产方式 缺少优良海港和路上交通网 【26】从长远来看,正确的方针是不能因为交通困难而要求牧区粮食自给,反对南粮北调,反对进口粮食;而是恰恰相反,应该大力发展交通网,以促进多种经营、区域分工,发展商品生产和规模经济。如果能够充分利用国际市场,则我国的林、牧、渔业和经济作物会大有发展的余地,即使粮食生产暂时缩减也是值得的,粮食可以部分依赖进口。算大账,这是更有利的。 【27】那高出地平面约10米的地上黄河比核战争的威胁更为现实。 【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】我们可以建立科学界和政府间的紧密合作,在新技术刚刚进入实用阶段,还未达到垄断的规模经济时,就立即不失时机的加以引进和推广,配合标准指定、金融扶助、税收优待等配套措施,以达到迅速赶超和占领市场的目的。(5G?!) 【64】经济能否持续增长,取决于新的技术更新能否发现更大的资源,以摆脱旧资源报酬递减的限制。 【65】我国小农经济的资源限制和保守倾向,阻碍了劳动分工和科学文化的多样发展。 【66】我国的法制建设是和经济发展并进的共生演化过程,而不是全盘西化的移植过程。这也是我国改革的重要经验之一。 【67】在文明发展的早期,生态环境对技术的选择起决定作用,但工业革命产生以后,技术选择日益受国际竞争的格局和科学教育的影响。技术成型后,文化制度的演化又会对生态环境产生深远的影响。这种相互作用在某一历史时期总能识别主要的决定机制。 【68】社会演化是:生态—技术—经济—文化—制度相互作用的多层次、多样化和演化的。 【69】两个认识:1)观察—问题—模型—检验 2)特殊—一般—特殊。 【70】社会演化并非一个“不可避免”的决定性过程。资本主义与科学的产生在历史长河中是一个罕见的时间。(文明分岔演化) 【71】复杂系统比简单系统更不稳定是普遍正确的。 【72】保守族群和进取族群的混合社会比两个进取族群的混合社会更加稳定。 【73】在一个社会主义国家,可行的改革方案必须是小心细致的混合经济模式。开放的程度和方式取决于历史的国际环境与自身的竞争力。不稳定不仅意味着风险,也意味着机会。重点不是消灭差别,而是管理合理的差别,使其不至于扩大为社会冲突。 【74】我国对分权试验及新制度的渐进演化持宽容的态度,这是与东欧俄罗斯相比经济改革取得成功的主要原因。 【75】创新和科技进步本质上是不稳定的,其经济特征表现为创造性毁灭和技术更替。 【76】开放竞争的方向决定之后,时机的把握和战略的选择就成为竞争胜败的决定性因素。 【77】历史因素会构成政府行为的“路径依赖”。 【78】如果国有企业或者乡镇企业在新产品市场上取得成功,就用股份制的办法承认新兴的企业家和经理人的贡献;如果企业改革失败,就让国有银行承担主要的改制成本。 【79】计划经济的主要问题不是缺乏竞争激励,而是闭关政策所造成的技术停滞和组织老化。 【80】影响上市公司业绩的主要因素是科技、管理、公司战略和规模经济等,所有制形式的影响并不显著。 【81】如何判断不同的指标体系的真实性和误差水平呢?——–多数老百姓的生活感觉比经济学家的理论更加靠谱。 【82】高价商品不用排队,不等于改善人民生活,更不等于改善经济的国际竞争力。 【83】邓公大胆放开出国留学政策,相信中国改革开放的成功最终会吸引留学生回国,使中国在获得西方技术的同时也赢得了中国知识分子的人心。 【84】分工加市场不等于协作。 【85】在国际竞争的条件下,降低利率可能引发资本外逃,流向投资回报率更高的地区。并不一定如教科书所说的,降低利息等于降低投资成本,必然增加投资,刺激经济增长。 【86】政府和非营利性的大学等社会组织在产业研发的初期起重大作用,在产业衰落的转型期也起重要作用。 【87】我国的有为政府和以天下为己任的科学家、实业家,有教育的工人、农民合作奋斗的积极性,是西方经济学所讲的劳力、资本、资源三要素之外,更重要的比较优势。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《八次危机》","slug":"八次危机","date":"2019-12-10T06:20:13.000Z","updated":"2019-12-16T11:43:10.984Z","comments":true,"path":"2019/12/10/八次危机/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/%E5%85%AB%E6%AC%A1%E5%8D%B1%E6%9C%BA/","excerpt":"","text":"","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"自动更新模块问题记录","slug":"自动更新模块","date":"2019-12-10T01:42:05.000Z","updated":"2019-12-10T03:13:29.956Z","comments":true,"path":"2019/12/10/自动更新模块/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E6%A8%A1%E5%9D%97/","excerpt":"ESOP上线使用已经有一段时间了,期间也遇到了许许多多的问题,最近在解决问题的时候发现一个当初设计的时候存在的一个十分严重的错误,再此记录一下问题与解决方法,给自己一个提醒,在后续的工作中注意这方面的问题。","text":"ESOP上线使用已经有一段时间了,期间也遇到了许许多多的问题,最近在解决问题的时候发现一个当初设计的时候存在的一个十分严重的错误,再此记录一下问题与解决方法,给自己一个提醒,在后续的工作中注意这方面的问题。 问题一:配置文件读取当前配置文件存储与读取有两种方式:1)创建properties文件;2)使用Preferences对象存储 当初使用properties文件时虽然已经将properties文件的读取范围限制到最小,仅读取“globleconfig.properties”文件,并且限制操作为只读取,不写入,然而在最近的使用过程中依然出现了properties文件被破坏的情况,分析原因,发现当初在写properties的读写方法时存在一个严重的缺陷: 12345678910111213141516171819202122232425private static String readValue(String filePath, String key) { String res = \"\"; Properties props = new Properties(); InputStream in = null; try { in = new BufferedInputStream(new FileInputStream(filePath)); props.load(in); String value = props.getProperty(key); if (value != null) { res = value; } } catch (Exception e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return res; } 当初在读写时,InputStream在操作完成之后并未释放! 问题二:Launcher程序需要更新时的处理此问题与问题一也是相关的,当“globleconfig.properties”文件被破坏,造成配置文件内容清空的时候,Launcher启动时会直接卡在更新界面不能向下执行,涉及到Launcher的逻辑: 1234567891011121314151617181920212223242526public LauncherHandle() { if ((!FileUtil.getAppNameFromProperties().isEmpty()&& (!FileUtil.getLineCodeFromProperties().isEmpty())&& (!FileUtil.getDisplayNumber().isEmpty())) { if (CommonUtil.getValueFromPreference(FileUtil.getAppNameFromProperties()) .isEmpty()) { CommonUtil.saveValue2Preference(FileUtil.getAppNameFromProperties(), FileUtil.getDefaultPath(FileUtil.getAppNameFromProperties())); } if (!CommonUtil.getValueFromPreference(\"LINECODE\").equals( FileUtil.getLineCodeFromProperties())) { CommonUtil.saveValue2Preference(\"LINECODE\", FileUtil.getLineCodeFromProperties()); } if (!CommonUtil.getValueFromPreference(\"DNUMBER\").equals( FileUtil.getDisplayNumber())) { CommonUtil.saveValue2Preference(\"DNUMBER\", FileUtil.getDisplayNumber()); } if (!CommonUtil.getValueFromPreference(\"MYAPP\").equals( FileUtil.getAppNameFromProperties())) { CommonUtil.saveValue2Preference(\"MYAPP\", FileUtil.getAppNameFromProperties()); } } else { System.out.println(\"GlobleSetting file crashed!\"); } 当时设计的时候并未考虑“globleconfig.properties”被破坏并清空的情况,所以在构造函数中未添加对properties文件的内容非空检测,造成后续将properties文件中的配置写入Preference时将空值写入,覆盖了之前的正常值。 那么问题来了,当初在设计自动更新程序的时候是采用以下的模式: 默认Launcher加载器是不更新的,每次启动时,ESOP程序通过Launcher查询SMES,比对当前版本后由Launcher通过FTP的方式将更新文件拉取下来,完成显示端程序的更新,这个过程是单向的,只能由Launcher来更新。在Launcher本身出现异常的时候就无法通过自动更新程序来解决了。 此时有两种解决方式: 1)由Launcher更新ESOP程序,再在ESOP程序内设置配置文件反过来更新Launcher 这种方式可以覆盖之前已安装的硬件设备的程序,但是存在一个大的风险点:如果更新之后的Launcher程序由于BUG将前端访问逻辑截断,则会完全堵死由自动更新程序进行后续更新操作的可能性。 2)重新设计Launcher自更新逻辑,可以先自动更新Launcher,再由Launcher更新后续ESOP的主程序 这种方式可以对Launcher程序有很灵活的把控,但是对于已安装的硬件设备,由于自更新逻辑未添加,则比较难处理。 综合上述两种处理方式,可以使用第一种方案的方式将更新过自更新逻辑的Launcher文件更新到已安装的硬件,后续安装的新终端则直接使用第二种方案,实现Launcher与ESOP的同步更新。 这里还要注意一点,设计Launcher程序的时候要做到尽量精简,尽量降低Launcher出现BUG的可能性,由于大量硬件已安装,所以这里的更新程序不能像手机中的APP更新程序,在一个大版本更新生成的时候提示用户先卸载旧程序,再下载新程序来更新程序到最新版本。这也是一个需要注意的十分重要的问题。 问题三:文件拷贝时的路径问题在文件拷贝的时候,对于更新后的文件,之前默认使用的是相对路径,但是拷贝之后发现使用相对路径很容易出现问题,后面就使用绝对路径来进行程序的拷贝与读取,以下是获取Jar包所在绝对路径的方法: 1234567private static String getRealPathOfTheJar() { String jarWholePath = FileUtils.class.getProtectionDomain().getCodeSource().getLocation().getFile(); try { jarWholePath = java.net.URLDecoder.decode(jarWholePath, \"UTF-8\"); } catch (UnsupportedEncodingException e) { System.out.println(e.toString()); } return new File(jarWholePath).getParentFile().getAbsolutePath(); } 其中 FileUtils.class 为Jar包里面的类文件。","categories":[],"tags":[{"name":"raspberry","slug":"raspberry","permalink":"https://www.smartonline.net.cn/tags/raspberry/"},{"name":"problems","slug":"problems","permalink":"https://www.smartonline.net.cn/tags/problems/"}]},{"title":"Redis缓存雪崩和缓存击穿","slug":"缓存击穿","date":"2019-12-10T01:15:25.000Z","updated":"2019-12-10T01:20:40.705Z","comments":true,"path":"2019/12/10/缓存击穿/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF/","excerpt":"Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。","text":"Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题无解。如果对数据的一致性要求很高,那么就不能使用缓存。 另外的一些典型问题就是,缓存穿透、缓存雪崩和缓存击穿。目前,业界也都有比较流行的解决方案。本篇文章,并不是要更加完美的解决这三个问题,也不是要颠覆业界流行的解决方案。而是,从实际代码操作,来演示这三个问题现象。之所以要这么做,是因为,仅仅看这些问题的学术解释,脑袋里很难有一个很形象的概念,有了实际的代码演示,可以加深对这些问题的理解和认识。 缓存穿透缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。 代码流程: 参数传入对象主键ID根据key从缓存中获取对象如果对象不为空,直接返回如果对象为空,进行数据库查询如果从数据库查询出的对象不为空,则放入缓存(设定过期时间)想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。 小编在工作中,会采用缓存空值的方式,也就是【代码流程】中第5步,如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒。 缓存雪崩缓存雪崩,是指在某一个时间段,缓存集中过期失效。 产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。 小编在做电商项目的时候,一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。 其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。 缓存击穿缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。 小编在做电商项目的时候,把这货就成为“爆款”。 其实,大多数情况下这种爆款很难对数据库服务器造成压垮性的压力。达到这个级别的公司没有几家的。所以,务实主义的小编,对主打商品都是早早的做好了准备,让缓存永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。 大道至简,mutex key互斥锁真心用不上。 结束语在流行的问题面前一定有流行的解决方案,但有时候,也要根据自己的实际情况酌情处理。大胆设计,说不定你的解决方案就会被流行呢? 原文链接:https://baijiahao.baidu.com/s?id=1619572269435584821&wfr=spider&for=pc","categories":[],"tags":[{"name":"redis","slug":"redis","permalink":"https://www.smartonline.net.cn/tags/redis/"}]},{"title":"Redis分布式锁的正确实现方式(Java版)","slug":"RedisLock","date":"2019-12-10T00:44:51.000Z","updated":"2019-12-10T01:13:10.215Z","comments":true,"path":"2019/12/10/RedisLock/","link":"","permalink":"https://www.smartonline.net.cn/2019/12/10/RedisLock/","excerpt":"前言分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。","text":"前言分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。 可靠性首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性。在任意时刻,只有一个客户端能持有锁。 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。 代码实现组件依赖首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码: 12345<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version></dependency> 加锁代码正确姿势Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现: 1234567891011121314151617181920212223242526public class RedisTool { private static final String LOCK_SUCCESS = \"OK\"; private static final String SET_IF_NOT_EXIST = \"NX\"; private static final String SET_WITH_EXPIRE_TIME = \"PX\"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }} 可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参: 第一个为key,我们使用key来当锁,因为key是唯一的。 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作; 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。 第五个为time,与第四个参数相呼应,代表key的过期时间。 总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。 心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。 错误示例1比较常见的错误示例就是使用jedis.setnx()和jedis.expire()组合实现加锁,代码如下: 123456789public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁 jedis.expire(lockKey, expireTime); }} setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。 错误示例2这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下: 12345678910111213141516171819202122232425public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { long expires = System.currentTimeMillis() + expireTime; String expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功 if (jedis.setnx(lockKey, expiresStr) == 1) { return true; } // 如果锁存在,获取锁的过期时间 String currentValueStr = jedis.get(lockKey); if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间 String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁 return true; } } // 其他情况,一律返回加锁失败 return false;} 那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。 解锁代码正确姿势还是先展示代码,再带大家慢慢解释为什么这样实现: 123456789101112131415161718192021222324public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = \"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end\"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }} 可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。 那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。 错误示例1最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。 123public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey);} 错误示例2这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下: 123456789public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 若在此时,这把锁突然不是这个客户端的,则会误解锁 jedis.del(lockKey); }} 如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。 总结本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。 如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。 参考阅读[1] Distributed locks with Redis [2] EVAL command [3] Redisson 原文链接:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/#%E5%8F%82%E8%80%83%E9%98%85%E8%AF%BB","categories":[],"tags":[{"name":"java","slug":"java","permalink":"https://www.smartonline.net.cn/tags/java/"},{"name":"redis","slug":"redis","permalink":"https://www.smartonline.net.cn/tags/redis/"}]},{"title":"布隆过滤器","slug":"BloomFilter","date":"2019-11-25T06:26:43.000Z","updated":"2019-11-25T10:30:08.651Z","comments":true,"path":"2019/11/25/BloomFilter/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/25/BloomFilter/","excerpt":"布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,false positive rate(误报率)越大,但是false negative (漏报)是不可能的。","text":"布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,false positive rate(误报率)越大,但是false negative (漏报)是不可能的。 本文将详解布隆过滤器的相关算法和参数设计,在此之前希望大家可以先通过谷歌黑板报的数学之美系列二十一 - 布隆过滤器(Bloom Filter)来得到些基础知识。 一. 算法描述 一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个。在下面的介绍中n为元素数,m为布隆过滤器或哈希表的slot数,k为布隆过滤器重hash function数。 为了add一个元素,用k个hash function将它hash得到bloom filter中k个bit位,将这k个bit位置1。 为了query一个元素,即判断它是否在集合中,用k个hash function将它hash得到k个bit位。若这k bits全为1,则此元素在集合中;若其中任一位不为1,则此元素比不在集合中(因为如果在,则在add时已经把对应的k个bits位置为1)。 不允许remove元素,因为那样的话会把相应的k个bits位置为0,而其中很有可能有其他元素对应的位。因此remove会引入false negative,这是绝对不被允许的。 当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数。 当add的元素过多时,即n/m过大时(n是元素数,m是bloom filter的bits数),会导致false positive过高,此时就需要重新组建filter,但这种情况相对少见。 二. 时间和空间上的优势 当可以承受一些误报时,布隆过滤器比其它表示集合的数据结构有着很大的空间优势。例如self-balance BST, tries, hash table或者array, chain,它们中大多数至少都要存储元素本身,对于小整数需要少量的bits,对于字符串则需要任意多的bits(tries是个例外,因为对于有相同prefixes的元素可以共享存储空间);而chain结构还需要为存储指针付出额外的代价。对于一个有1%误报率和一个最优k值的布隆过滤器来说,无论元素的类型及大小,每个元素只需要9.6 bits来存储。这个优点一部分继承自array的紧凑性,一部分来源于它的概率性。如果你认为1%的误报率太高,那么对每个元素每增加4.8 bits,我们就可将误报率降低为原来的1/10。add和query的时间复杂度都为O(k),与集合中元素的多少无关,这是其他数据结构都不能完成的。 如果可能元素范围不是很大,并且大多数都在集合中,则使用确定性的bit array远远胜过使用布隆过滤器。因为bit array对于每个可能的元素空间上只需要1 bit,add和query的时间复杂度只有O(1)。注意到这样一个哈希表(bit array)只有在忽略collision并且只存储元素是否在其中的二进制信息时,才会获得空间和时间上的优势,而在此情况下,它就有效地称为了k=1的布隆过滤器。 而当考虑到collision时,对于有m个slot的bit array或者其他哈希表(即k=1的布隆过滤器),如果想要保证1%的误判率,则这个bit array只能存储m/100个元素,因而有大量的空间被浪费,同时也会使得空间复杂度急剧上升,这显然不是space efficient的。解决的方法很简单,使用k>1的布隆过滤器,即k个hash function将每个元素改为对应于k个bits,因为误判度会降低很多,并且如果参数k和m选取得好,一半的m可被置为为1,这充分说明了布隆过滤器的space efficient性。 三. 举例说明 以垃圾邮件过滤中黑白名单为例:现有1亿个email的黑名单,每个都拥有8 bytes的指纹信息,则可能的元素范围为 ,对于bit array来说是根本不可能的范围,而且元素的数量(即email列表)为 ,相比于元素范围过于稀疏,而且还没有考虑到哈希表中的collision问题。 若采用哈希表,由于大多数采用open addressing来解决collision,而此时的search时间复杂度为 : 即若哈希表半满(n/m = 1/2),则每次search需要probe 2次,因此在保证效率的情况下哈希表的存储效率最好不超过50%。此时每个元素占8 bytes,总空间为: 若采用Perfect hashing(这里可以采用Perfect hashing是因为主要操作是search/query,而并不是add和remove),虽然保证worst-case也只有一次probe,但是空间利用率更低,一般情况下为50%,worst-case时有不到一半的概率为25%。 若采用布隆过滤器,取k=8。因为n为1亿,所以总共需要 被置位为1,又因为在保证误判率低且k和m选取合适时,空间利用率为50%(后面会解释),所以总空间为: 所需空间比上述哈希结构小得多,并且误判率在万分之一以下。 四. 误判概率的证明和计算 假设布隆过滤器中的hash function满足simple uniform hashing假设:每个元素都等概率地hash到m个slot中的任何一个,与其它元素被hash到哪个slot无关。若m为bit数,则对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为: 则k个hash function中没有一个对其置位的概率为: 如果插入了n个元素,但都未将其置位的概率为: 则此位被置位的概率为: 现在考虑query阶段,若对应某个待query元素的k bits全部置位为1,则可判定其在集合中。因此将某元素误判的概率为: 由于 ,并且 当m很大时趋近于0,所以 从上式中可以看出,当m增大或n减小时,都会使得误判率减小,这也符合直觉。 现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为: 设 , 则简化为 ,两边取对数 , 两边对k求导 下面求最值 因此,即当 时误判率最低,此时误判率为: 可以看出若要使得误判率≤1/2,则: 这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。 五. 设计和应用布隆过滤器的方法 应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。 系统首先要计算需要的内存大小m bits: 再由m,n得到hash function的个数: 至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。 根据公式,当k最优时: 因此可验证当P=1%时,存储每个元素需要9.6 bits: 而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits: 这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的 才是每个元素对应的为1的bit位数。 从而使得P(error)最小时,我们注意到: 中的 ,即 此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"}]},{"title":"《EffectiveJava》(五)","slug":"EffectiveJava4","date":"2019-11-23T09:10:09.000Z","updated":"2019-11-23T09:11:28.553Z","comments":true,"path":"2019/11/23/EffectiveJava4/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/EffectiveJava4/","excerpt":"【68】executor 和 task 优先于线程创建工作队列: 12345ExecutorService executor = Executors.newSingleThreadExecutor();executor.execute(runnable);//执行提交一个 runnable 方法executor.shutdown();//优雅地终止","text":"【68】executor 和 task 优先于线程创建工作队列: 12345ExecutorService executor = Executors.newSingleThreadExecutor();executor.execute(runnable);//执行提交一个 runnable 方法executor.shutdown();//优雅地终止 如果编写的是小程序,或者是轻载的服务器,使用 Executors.newCachedThreadPool 通常是个不错的选择,因为它不需要配置,并且在一般的情况下能够正确的完成工作。但在大负载的产品服务其中,最好使用 Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用 ThreadPoolExecutor。 ScheduledThradPoolExecutor 是用来代替 java.util.Timer 的东西。timer 只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果 timer 唯一的线程抛出未被捕获的异常,timer 就会停止执行。被调度的线程池 executor 支持多个线程,并优雅地从抛出未受检异常的任务中恢复。 【69】并发工具优先于 wait 和 notify在并发情境中,除非不得以,否则应该优先使用 ConcurrentHashMap,而不是使用 Collection.-synchronizedMap 或者 Hashtable。只要用并发 Map 替换老式的同步 Map,就可以极大的提升并发应用程序的性能。更一般地,应该优先使用并发集合,而不是使用外部同步集合。 同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。最常用的同步器是 Count-DownLatch 和 Semaphore,较不常用的是 CyclicBarrier 和 Exchanger。 倒计数锁存器(CountDownLatch)是一次性的障碍,允许一个或者多个线程等待一个或者其它线程来做某些事情。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657public class ConcurrentTimer { private ConcurrentTimer() { } // Noninstantiable public static long time(Executor executor, int concurrency, final Runnable action) throws InterruptedException { final CountDownLatch ready = new CountDownLatch(concurrency); final CountDownLatch start = new CountDownLatch(1); final CountDownLatch done = new CountDownLatch(concurrency); for (int i = 0; i < concurrency; i++) { executor.execute(new Runnable() { public void run() { ready.countDown(); // Tell timer we're ready try { start.await(); // Wait till peers are ready action.run(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { done.countDown(); // Tell timer we're done } } }); } ready.await(); // Wait for all workers to be ready long startNanos = System.nanoTime(); start.countDown(); // And they're off! done.await(); // Wait for all workers to finish return System.nanoTime() - startNanos; }} 对于间歇式的定时,始终应该优先使用 System.nanoTime,而不是使用 System.currentTime-Mills。System.nanoTime 更加准确也更加精确,它不受系统的实时时钟的调整所影响。 使用 wait 方法的标准模式: 1234567891011synchronized (obj) { while (<condition does not hold>) { obj.wait();// (Release lock,and reacquires on wakeup) ... // Perform action appropriate to condition }} 简而言之,直接使用 wait 和 notify 就像用 “并发汇编语言” 进行编程一样,而 java.util.concurr-ent则提供了更高级的语言。没有理由在新代码中使用 wait 和 notify,即使有,也是极少的。如果你在维护使用 wait 和 notify 的代码,务必确保始终是利用标准的模式从 while 循环的内部调用 wait。一般情况下,你应该优先使用 notifyAll,而不是使用 notify。如果使用 notify,请一定要小心,以确保程序的活性。 【70】线程安全性的文档化线程安全性的级别: 1.不可变的(immutable)——这个类的实力是不可变的。所以不需要外部的同步。这样的例子包括 String、Long 和 BigInteger。 2.无条件的线程安全(unconditionally thread-safe)——这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用而无需任何外部同步。其例子包括 Random 和 Co-ncurrentHashMap。 3.有条件的线程安全(conditionally thread-safe)——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件线程安全相同。这样的例子包括 Collection.synchroni-zed 包装返回的集合,它的迭代器要求外部同步。 4.非线程安全(not thread-safe)——这个类的实例是可变的。为了并发的使用他们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。 5.线程对立的(thread-hostile)——这个类不能安全的被多个线程并发使用,即使所有的方法调用都被外部同步包围。在 Java 平台类库中,线程对立的类或者方法非常少。System.runFinalizers-OnExit 方法是线程对立的,但已经被废除了。 私有锁对象: 1234567891011private final Object lock = new Object(); public void foo() { synchronized(lock) { ... }} 私有锁对象不能被这个类的客户端程序所访问,所以它们不可能妨碍对象的同步。 简而言之,每个类都应该利用详细的说明或者线程安全注解,清楚地在文档中说明它的线程安全属性。synchronized 修饰符与这个文档毫无关系。有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁”。如果你编写的是无条件的线程安全类,就应该考虑使用私有的锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。 【71】慎用延迟初始化延迟初始化(lazy initialization)是延迟到需要的域的值时才将它初始化的这种行为。在大多数情况下,正常的初始化要优先于延迟初始化。 正常初始化: 1private final FieldType field = computeFieldValue(); 利用延迟优化来破坏初始化的循环,就要使用同步访问方法: 12345678910111213private FieldType field;synchronized FieldType getField() { if (field == null) { field = computeFieldValue(); } return field;} 如果出于性能的考虑而需要对静态域使用延迟初始化,就使用 lazy initialization holder class 模式: 1234567private static class FieldHolder { static final FieldType field = computeFieldValue();}static FieldType getField() { return **FieldHolder.field**;} 如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double check idiom): 12345678910111213141516171819202122232425private volatile FieldType field;FieldType getField() { FieldType result = field; if (result == null) { // First check (no locking) synchronized(this) { result = field; if (result == null) { // Second check (with locking) field = result = computeFieldValue(); } } } return result;} 如果需要延迟初始化一个可以接受重复初始化的实例域,就可以使用单重检查模式: 123456789101112131415private volatile FieldType field;FieldType getField() { FieldType result = field; if (result == null) { // single check field = result = computeFieldValue(); } return result;} 简而言之,大多数的域应该正常的进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式;对于静态域,则使用 lazy initialization holder class idiom。对于可以接受重复初始化的实例域,也可以考虑单重检查模式。 【72】不要依赖于线程调度器任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。要编写健壮的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。保持可运行线程数量尽可能少的主要方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。如果线程没有在做有意义的工作,就不应该运行。 对于大多数程序员来说,Thread.yield 的唯一用途是在测试期间人为地增加程序的并发性。但是它从来不能保证一定可行。因此,应该使用 Thread.sleep(1) 代替 Thread.yield 来进行并发测试。 简而言之,不要让应用程序的正确性依赖于线程调度器。否则结果得到的应用程序将既不健壮,也不具有可移植性。作为推论,不要依赖于 Thread.yield 或者线程优先级。这些设施仅仅对调度器做些暗示。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本不能工作的程序。 【73】避免使用线程组我们最好把线程组看作是一个不成功的试验,你可以忽略它们,就当它们更笨不存在一样。 十二、序列化: 【74】谨慎地实现 Serializable 接口对象序列化提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上。实现序列化的代价有: (1)实现 Serializable 接口而付出最大的代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。 序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符有关,通常也被称为序列版本 UID。如果没有现实的指定这个 UID,系统会自动生成该标识号,它的值与类的各个部分都有着复杂的关系,如果类改变,这个 UID 也会跟着改变,结果就会出现兼容性问题。 (2)第二个代价是,它增加了出现 BUG 和安全漏洞的可能性。 (3)第三个代价是,随着类发行新的版本,相关的测试负担也增加了。 根据经验,比如 Date 和 BigInteger 这样的值应该实现 Serializable,大多数的集合类也应该如此。代表活动的实体类,比如线程池,一般不应该实现 Serializable。 为了继承而设计的类应该尽可能地少地去实现 Serializable 接口,用户的接口也应该尽可能少的继承 Serializable 接口。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061import java.util.concurrent.atomic.*;public abstract class AbstractFoo { private int x, y; // Our state // This enum and field are used to track initialization private enum State { NEW, INITIALIZING, INITIALIZED }; private final AtomicReference<State> init = new AtomicReference<State>(State.NEW); public AbstractFoo(int x, int y) { initialize(x, y); } // This constructor and the following method allow // subclass's readObject method to initialize our state. protected AbstractFoo() { } protected final void initialize(int x, int y) { if (!init.compareAndSet(State.NEW, State.INITIALIZING)) throw new IllegalStateException( \"Already initialized\"); this.x = x; this.y = y; // Do anything else the original constructor did init.set(State.INITIALIZED); } // These methods provide access to internal state so it can // be manually serialized by subclass's writeObject method. protected final int getX() { checkInit(); return x; } protected final int getY() { checkInit(); return y; } // Must call from all public and protected instance methods private void checkInit() { if (init.get() != State.INITIALIZED) throw new IllegalStateException(\"Uninitialized\"); } // Remainder omitted} checkInit 方法在其它方法工作之前调用,用来保证子类初始化之前调用相关方法就可以快速而干净地失败。 【75】考虑使用自定义的序列化形式一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。 对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。如果一个对象物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式(POJO类?)。 即使你确定了默认的序列化形式是合适的,通常还必须提供一个 readObject 方法以保证约束关系和安全性。 transient 修饰符表明这个实例域将从一个类的默认序列化形式中省略掉: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283// StringList with a reasonable custom serialized formimport java.io.*;public final class StringList implements Serializable { private transient int size = 0; private transient Entry head = null; // No longer Serializable! private static class Entry { String data; Entry next; Entry previous; } // Appends the specified string to the list public final void add(String s) { // Implementation omitted } /** * Serialize this {@code StringList} instance. * * @serialData The size of the list (the number of strings * it contains) is emitted ({@code int}), followed by all of * its elements (each a {@code String}), in the proper * sequence. */ private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(size); // Write out all elements in the proper order. for (Entry e = head; e != null; e = e.next) s.writeObject(e.data); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int numElements = s.readInt(); // Read in all elements and insert them in list for (int i = 0; i < numElements; i++) add((String) s.readObject()); } private static final long serialVersionUID = 93248094385L; // Remainder omitted} 尽管 StringList 的所有域都是瞬时的(transient),但 writeObject 方法的首要任务仍是调用 de-faultWriteObject,readObject 方法的首要任务仍是调用 defaultReadObject。如果所有的域都是瞬时的,从技术角度而言,不调用 defaultWriteObject 和 defaultReadObject 也是允许的,但是不推荐这样做。 不管你选择了那种序列化形式,都要为自己编写的每个可序列化的类声明一个显示的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源。 private static final long serialVersionUID = randomLongValue; 【76】保护性地编写 readObject 方法readObject 方法实际上相当于另一个公有的构造器,如同其它的构造器一样,它也要求注意同样的所有注意事项。构造器必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷贝,同样的,readObject 方法也需要这样做。 readObject 方法可以首先调用 defaultReadObject,然后检查被反序列化之后的对象有效性。如果有效性检测失败,readObject 方法就抛出一个 InvalidObjectException 异常,是反序列化过程不能成功完成: 123456789101112131415// readObject method with validity checkingprivate void readObject(ObjectInputStream s) throws IOException,ClassNotFoundException { s.defaultReadObject(); // Check that our invariants are satisfied if (start.compareTo(end) > 0) { throw new **InvalidObjectException**(start + \" after \" + end); }} 编写更加健壮的 readObject 方法的指导方针: 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类。 对于任何约束条件,如果检查失败,则抛出一个 InvalidObjectException 异常。这些检查动作应该跟在所有的保护性拷贝之后。 如果整个对象图在被反序列化之后必须进行验证,就应该使用 ObjectInputValidation 接口。 无论直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。 【77】对于实例控制,枚举类型优先于 readResolvereadResolve 特性允许你用 readObject 创建的实例代替另一个实例。对于一个正在被反实例化的对象,如果它的类定义了一个 readResolve 方法,并且具备正确的声明,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象。 readResolve 的可访问性很重要。如果把 readResolve 方法放在一个 final 类上,它就应该是私有的。如果把 readResolve 方法放在一个非 final 对象上,就必须认真考虑它的可访问性。 总而言之,你应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个 readResolve 方法,并确保该类的所有实例域都为基本类型,或者是 transient 的。 【78】考虑用序列化代理代替序列化实例12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import java.util.*;import java.io.*;public final class Period implements Serializable { private final Date start; private final Date end; /** * @param start the beginning of the period * @param end the end of the period; must not precede start * @throws IllegalArgumentException if start is after end * @throws NullPointerException if start or end is null */ public Period(Date start, Date end) { this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) throw new IllegalArgumentException( start + \" after \" + end); } public Date start () { return new Date(start.getTime()); } public Date end () { return new Date(end.getTime()); } public String toString() { return start + \" - \" + end; } // Serialization proxy for Period class private static class SerializationProxy implements Serializable { private final Date start; private final Date end; SerializationProxy(Period p) { this.start = p.start; this.end = p.end; } private static final long serialVersionUID = 234098243823485285L; // Any number will do (Item 75) // readResolve method for Period.SerializationProxy private Object readResolve() { return new Period(start, end); // Uses public constructor } } // writeReplace method for the serialization proxy pattern private Object writeReplace() { return new SerializationProxy(this); } // readObject method for the serialization proxy pattern private void readObject(ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException(\"Proxy required\"); }} 总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写 readObject 或者 write-Object 方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《EffectiveJava》(四)","slug":"EffectiveJava3","date":"2019-11-23T08:57:33.000Z","updated":"2019-11-23T09:12:01.267Z","comments":true,"path":"2019/11/23/EffectiveJava3/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/EffectiveJava3/","excerpt":"【47】了解和使用库不要重复发明轮子。一般而言,类库的代码可能比你自己编写的代码更好一些,并且会随着时间的推移不断改进。从经济角度的分析表明:类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能够给予的投入。 强烈需要了解的类库: java.lang java.util java.io","text":"【47】了解和使用库不要重复发明轮子。一般而言,类库的代码可能比你自己编写的代码更好一些,并且会随着时间的推移不断改进。从经济角度的分析表明:类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能够给予的投入。 强烈需要了解的类库: java.lang java.util java.io 特别推荐: java.util.Collections java.util.concurrent 【48】如果需要精确的答案,请避免使用 float 和 doublefloat 和 double 类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。要让一个 float 或者 double 精确地表示 0.1(或任何10的任何其它负数次方值)是不可能的。 如果想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal(数值超过18位数字则必须使用它),它允许你完全控制舍入。另外对于小数值也可以使用int 或者 long。以下为一个使用 BigDecimal 的例子: 12345678910111213141516171819final BigDecimal TEN_CENTS = new BigDecimal(“.10”);int itemsBought = 0;BigDecimal funds = new BigDecimal(“1.00”);for (BigDecimal price = TEN_CENTS;funds.compareTo(price) >= 0;price = price.add(TEN_CENTS)) { itemsBought ++; funds = funds.subtract(price);}System.out.println(itemsBought + \"items bought.\");System.out.println(“Money left over:$” + funds); 【49】基本类型优先于装箱基本类型Java有一个类型系统由两部分组成,包含基本类型,如 int、double、boolean,和引用类型,如 String 和 List。每个基本类型都有一个对应的引用类型,称作装箱基本类型。如: int —> Integer,double —> Double,boolean —> Boolean 基本类型和装箱基本类型之间的三个主要区别: <1>基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。 <2>基本类型只有功能完备的值,而每个装箱基本类型还有一个非功能值:null。 <3>基本类型通常比装箱基本类型更节约时间和空间。 当程序用 == 操作符比较两个装箱基本类型时,它做了一个同一性比较,这几乎肯定不是你所希望的。当程序进行涉及装箱和拆箱基本类型混合类型计算时,它会进行拆箱,会抛出 NullPointer-Exception 异常。最后,当程序装箱了基本类型时,会导致高开销和不必要的对象创建。 123456789Integer i = new Integer(42);Integer j = new Integer(42);Interger f;if (i == j) //返回 falseif (f == 42) //抛出 NullPointerException,因为 f 初始值为 null 【50】如果其它类型更合适,则尽量避免使用字符串字符串不适合代替其它的值类型。比如说表达数值的字符串应该转化为适当的数值类型,比如说int、float、BigInteger。如果它是一个“是—或—否”这种问题的答案,就应该转变为 boolean 类型。 字符串不适合代替枚举类型。枚举类型比字符串更加适合用来表示枚举的常量。 字符串不适合代替聚集类型。如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。例如: 1String compoundKey = className + \"#\" + i.next(); 如果分隔符出现在某个域中,结果会出现混乱。使用时也需要解析字符串得到三个单独的域,造成性能损失。更好的做法是,简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类。 1234567891011class CompoundKey { String className: String seperator; String add; ...} 【51】当心字符串连接的性能为连接 n 个字符串而重复地使用字符串连接操作符,需要 n 的平方级的时间。这是由于字符串不可变而导致的不幸结果。当两个字符串被连接在一起时,它们的内容都要被拷贝。 不要使用字符串连接操作符(+)来合并多个字符串,除非性能无关紧要。相反,应该使用 Strin-gBuilder 的 append 方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。 【52】通过接口引用对象应该使用接口而不是用类作为参数的类型。更一般的讲,应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。只有当你利用构造器创建某个对象的时候,才需要真正引用这个对象的类。 例外情况: (1)如果没有合适的接口类型存在,完全可以用类而不是接口来引用对象。 (2)对象属于一个框架,而框架的基本类型是类。 (3)类实现了接口,但是它提供了接口中不存在的额外方法。 【53】接口优先于反射机制待添加。。。 【54】谨慎地使用本地方法在使用本地方法之前务必三思。极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资源或者遗留代码库,也要尽可能少用本地代码,并且要全面进行测试。本地代码的一个 BUG 就有可能破坏整个应用程序。 【55】谨慎地进行优化三条与优化有关的格言: 1.很多计算机上的过失都被归咎于效率(没必要达到的效率),而不是任何其他原因——甚至盲目的做傻事。 2.不要去计较效率上的一些小小的得失,在 %97 的情况下,不成熟的优化才是一切问题的根源。 3.1不要进行优化。 3.2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的优化方案之前,请不要进行优化。 总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计 API 、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的底层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。 【56】遵守普遍接受的命名惯例把标准的命名惯例当做一种内在的机制来看待,并且学着用它们作为第二特征。字面惯例是非常直接和明确的;语法惯例则更加复杂和松散。“如果长期养成的习惯用法与此不同,请不要盲目遵从这些命名惯例。”请运用常识。 十、异常:【57】只针对异常的情况才使用异常设计良好的 API 不应该强迫它的客户端为了正常的控制流而使用异常。如果类具有“状态相关”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的“状态测试”方法,即指示是否可以调用这个状态相关的方法。例如: 12345for (Iterator<Foo> i = collection.iterator;i.hasNext;){ //状态测试方法“hasNext()” Foo foo = i.next();//状态相关方法“next()”} 另一种提供单独的状态测试方法的做法是,如果“状态相关”的方法被调用时,该对象处于不适当的状态之中,他就会返回一个可识别的值,比如 null,但这种方法对于 Iterator 而言并不合适,因为Null 是 next 方法的合法返回值。 【58】对可恢复的情况使用受检异常,对编程错误使用运行时异常Java 程序设计语言提供了三种可抛出结构:受检的异常、运行时异常和错误。 在决定使用受检异常或是未受检异常时,主要的原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个 catch 子句中处理该异常或者将它传播出去。 因为受检的异常往往指明了可恢复的条件,所以对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获取一些有助于恢复的信息。例如,假设因为用户没有存储足够的钱,他企图在一个收费电话上呼叫就会失败,于是抛出受检异常。这个异常应该提供一个访问方法,以便允许客户查询所缺的费用金额,从而将这个数值传递给电话用户。 【59】避免不必要的使用受检的异常在异常处理的时候,都会接触到受检异常(checked exception)和非受检异常(unchecked-exception)这两种异常类型。非受检异常指的是java.lang.RuntimeException和java.lang.Error类及其子类,所有其他的异常类都称为受检异常。两种类型的异常在作用上并没有差别,唯一的差别就在于使用受检异常时的合法性要在编译时刻由编译器来检查。正因为如此,受检异常在使用的时候需要比非受检异常更多的代码来避免编译错误。 “把受检异常变成未受检异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法是,其中第一个方法返回一个 boolean,表明是否应该抛出异常。这种 API 重构,把下面的调用序列: 1234567891011try { obj.action();} catch (TheCheckedException e) { //Handle exceptional condition ...} 重构为: 1234567891011if (obj.actionPermitted(args)) { obj.action(args);} else { //Handle exceptional condition ...} 【60】优先使用标准的异常 【61】抛出与抽象相对应的异常如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况。 为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation),如下所示: 123456789try { // Use lower-level abstraction to do our bidding} catch (LowerLevelException e) { throw new HigherLevelException(...);} 一种特殊的异常转译形式称为异常链,如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法来获得低层的异常: 123456789try { // Use lower-level abstraction to do our bidding} catch (LowerLevelException cause) { throw new HigherLevelException(cause);} 高层异常的构造器将原因传到支持连的超级构造器,因此它最终将被传给 Throwable 的其中一个运行异常链的构造器,例如 Throwable(Throwable): 123456789class HighLevelException(Throwable cause) { HigherLevelException(Throwable cause) { super(cause); }} 总而言之,如果不能阻止或处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析。 【62】每个方法抛出的异常都要有文档始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确的记录下抛出每个异常的条件。 总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于受检异常和非受检异常,以及对于抽象的和具体的方法也都一样。要为每个受检异常提供单独的 throws 子句,不要为未受检异常提供 throws 子句。 【63】在细节消息中包含能捕获失败的信息为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。为了确保在异常的细节消息中包含足够的能够捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息。然后,有了这些信息,只要把他们放到消息描述中,就可以自动产生细节消息。例如,IndexOutOfBoundsException 并不是有个 String 构造器,而是有个这样的构造器: 123456789101112public IndexOutOfBoundsException(int lowerBound,int upperBound,int index){ super(\"Lower bound:\" + lowerBound + “,Upper bound:” + upperBound + \", Index:\" + index); this.lowerBound = lowerBound; this.upperBound = upperBound; this.index = index;} 为异常的“失败捕获”信息(在上例中的 lowerBound、upperBound 和 index 方法)提供一些访问方法是合适的。提供这样的访问方法对于受检的异常,比对于未受检的异常更为重要,因为失败——捕获信息对于从失败中恢复是非常有用的。 【64】努力使失败保持原子性一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。 实现这种效果的途径: 1.设计一个不可变的对象。如果对象是不可变的,失败原子性就是显然的。 2.对于在可变对象上执行操作的方法: 2.1在执行操作之前检查参数的有效性。 2.2调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。 2.3编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。这种办法主要用于永久性的数据结构。(例如数据库的回滚) 2.4在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。 【65】不要忽略异常空的 catch 块会使异常达不到应有的目的: 1234567try { ...} catch (SomeException e) {} 不管异常代表了可预见的异常条件,还是编程错误,用空的 catch 块忽略它,将会导致程序在遇到错误的情况下悄然地执行下去。而会在将来的某个点上突然失败。 十一、并发:【66】同步访问共享的可变数据读取一个非 long 或 double 类型的变量,可以保证返回的值是某个线程保存在改变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此。 为了在线程之间进行可靠的通信,也是为了互斥访问,同步是必要的。 要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询一个 boolean 域,这个域一开始为 false ,但是可以通过第二个线程设置为 true ,表示第一个线程将中止自己。由于 boolean 域的读和写操作都是原子的,程序员在访问这个域的时候不再使用同步: 12345678910111213141516171819202122232425262728293031public class StopThread { private static boolean stopRequested;//boolean域 public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { public void run() { int i = 0; while (!stopRequested) { i ++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true; }} 由于没有同步,就不能保证后台线程何时“看到”主线程对 stopRequested 的值所做的改变。因此上述代码永远不会终止,修正的一种方法是同步访问 stopRequested 域: 12345678910111213141516171819202122232425262728293031323334353637383940414243public class StopThread {private static boolean stopRequested;//boolean域 private static synchronized void requestStop() { //同步写操作 stopRequested = true; } private static synchronized boolean stopRequested() { //同步读操作 return stopRequested; } public static void main(String[] args) throws InterruptedException { Thread backgroundThread = new Thread(new Runnable() { public void run() { int i = 0; while (**!stopRequested()**) { i ++; } } }); backgroundThread.start(); TimeUnit.SECONDS.sleep(1); requestStop(); }} 如果读和写操作没有都被同步,同步就不会起作用。上述的 boolean 域的读写操作即使没有同步也是原子的,因此这些方法的同步只是为了它的通信效果(让其它线程立即知道改变),而不是为了互斥访问。起着同样的作用的修饰符是 volatile,它虽然不执行互斥访问,但是它可以保证任何一个县城在读取该域的时候都将看到最近刚刚被写入的值,一个需要特别注意的点是,使用 volatile 修饰符的变量进行其它操作时,需要注意此操作的原子性(总之谨慎地使用 volatile)。 【67】避免过度同步简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般地讲,要尽量限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中。 (暂时还是不能很好地理解,后续再慢慢理解)","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《EffectiveJava》(三)","slug":"EffectiveJava2","date":"2019-11-23T08:57:22.000Z","updated":"2019-11-23T09:14:12.341Z","comments":true,"path":"2019/11/23/EffectiveJava2/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/EffectiveJava2/","excerpt":"【27】优先考虑泛型方法静态工具方法尤其适合泛型化。如实现构造器: 12345public static <V,K> HashMap<V,K> newHashMap() {return new HashMap<K,V>();}","text":"【27】优先考虑泛型方法静态工具方法尤其适合泛型化。如实现构造器: 12345public static <V,K> HashMap<V,K> newHashMap() {return new HashMap<K,V>();} 调用 Map<String,List> anagrams = newHashMap() 来实现 anagrams 的生成。 对于 public static <T extends Comparable> T max(List list) {…};类型限制<T extends Comparable>,可以读作 “ 针对可以与自身进行比较的每个类型 T “,这与互比性的概念或多或少有些一致。 【28】利用有限制通配符来提升 API 的灵活性确定了子类型后,每个类型便都是自身的子类型,即便它没有将自身扩展。 为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型就没有什么好处了:因为你需要的是严格的类型匹配,这不是用任何通配符而得到的。 PECS 表示 producer-extends,consumer-super。 如果参数化类型表示一个 T 生产者,就使用<? extends T>;如果它表示一个 T 消费者,就使用<? super T>。例如: 1234567public void pushAll(Iterable<? extends E> src) { for (E e:src) push(e);} 上述方法中的 src 参数产生 E 实例供 Stack 使用,因此 src 相应的类型为 Iterable<? super E>; 1234567public void popAll(Collection<E> dst) { while ( ! isEmpty) dst.add(pop());} 上述方法中 dst 参数通过 Stack 消费 E 实例,因此 dst 相应的类型为 Collection<? super E>。 一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代它。如果是无限制的类型参数(),就用无限制的通配符取代它(<?>);如果是有限制的类型参数,就用有限制的通配符取代它。 【29】优先考虑类型安全的异构容器cast 方法是 Java 的 cast 操作符的动态模拟。它只检测它的参数是否为 Class 对象所表示的类型实例。如果是,就返回参数;否则就抛出 ClassCastException 异常。 集合 API (如 Set 和 Map)说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用 Class 对象作为键。以这种方式使用的 Class 对象称作类型令牌。你也可以使用定制的键类型。例如,用一个 DatabaseRow 类型表示一个数据库行(容器),用泛型 Column 作为它的键。 七、枚举和注解:【30】用 enum 代替 int 常量Java 枚举类型背后的基本想法非常简单:它们就是通过公有的静态 final 域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的 final。Java 枚举本质上是 int 值。 为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器,例如: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public enum Planet { MERCURY (3.302e+23, 2.439e6), //使用构造器 VENUS (4.869e+24, 6.052e6), EARTH (5.975e+24, 6.378e6), MARS (6.419e+23, 3.393e6), JUPITER (1.899e+27, 7.149e7), SATURN (5.685e+26, 6.027e7), URANUS (8.683e+25, 2.556e7), NEPTUNE (1.024e+26, 2.477e7); private final double mass; private final double radius; private final double surfaceGravity; private static final double G = 6.67300E-11; //内部构造器,外部不能访问 Planet(double mass, double radius) { this.mass = mass; this.radius = radius; surfaceGravity = G * mass / (radius * radius); } public double mass() { return mass; } public double radius() { return radius; } public double surfaceGravity() { return surfaceGravity; } public double surfaceWeight(double mass) { return mass * surfaceGravity; }} 每当需要一组固定常量的时候,应该使用枚举。枚举中的常量集不一定要始终保持不变。如果多个枚举常量同时共享相同的行为,则考虑策略枚举: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061enum PayrollDay { MONDAY(PayType.WEEKDAY), //使用策略一 ... SUNDAY(PayType.WEEKEND); //使用策略二 private final PayType payType; PayrollDay(PayType payType) { this.payType = payType; } double pay(double hoursWorked,double payRate) { return payType.pay(hoursWorked,payRate); } //策略枚举 private enum PayType { WEEKDAY { //策略一:WEEKDAY 策略 double overtimePay(double hours,double payRate) { ... } }, WEEKEND { //策略二:WEEKEND 策略 double overtimePay(double hours,double payRate) { ... } }; ... //枚举内部声明的抽象构造方法 abstract double overtimePay(double hours,double payRate); double pay(double hours,double payRate) { ... } }} 【31】用实例域代替序数永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。很少用到,故不详细说明了。 【32】用 EnumSet 代替位域集合的位域表示法: 12345678910111213public class Text { public static final int STYLE_BOLD = 1<<0;// 1 public static final int STYLE_ITALIC = 1<<1;// 2 public static final int STYLE_UNDERLINE = 1<<2;// 4 public static final int STYLE_STRIKETHROUGH = 1<<3;// 8 public void applyStyles(int styles) {...}} 调用: 1text.applyStyles(STYLE_BOLD | STYLE_ITALIC); 用枚举代替位域: 1234567public class Text { public enum Style { BOLD,ITALIC,UNDERLINE,STRIKETHROUGH } public void applyStyles(Set<style> styles) {...}} 调用: 1text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC)); 【33】用 EnumMap 代替序数索引最好不要用序数来索引数组,而要使用 EnumMap。如果你所表示的这种关系是多维的,就使用 EnumMap<…, EnumMap<…>>。应用程序的程序员在一般的情况下都不使用 Enum.ordinal 即使要用也很少,因此这是一种特殊情况。 123456789101112131415Map<Herb.Type,Set<Herb>> herbsByType = new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);for (Herb.Type t:Herb.Type.values()) { herbsByType.put(t,new HashSet<Herb>());}for (Herb h:garden) { herbsByType.get(h.type).add(h);}System.out.println(herbsByType); 【34】用接口模拟可伸缩的枚举虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟。这样允许客户端编写自己的枚举来实现接口。如果 API 是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举。例如: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455public interface Operation { //接口 double apply(double x,double y);}public enum BasicOperation implements Operation { //扩展接口的基本运算类型 PLUS (\"+\") { //加法运算 public double apply(double x,double y) { return x+y;} }, MINUS (\"-\") {...}, //减法运算,省略 TIMES (\"*\") {...}, //乘法运算,省略 DEVIDE (\"/\") {...}; //除法运算,省略 private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; }}public enum ExtendedOperation implements Operation { //扩展接口的扩展运算类型 EXP(\"^\") { //计算 x 的 y 次方的结果 public double apply(double x,double y) { return Math.pow(x,y); } }, REMINDER(\"%\") {...}; //求余运算 ... ...} 【35】注解优先于命名模式【36】坚持使用 Override 注解Override 注解只能用在方法声明中,它表示被注解的方法声明覆盖了超类型中的一个声明。如果坚持使用这个注解,可以防止一大类的非法错误。例如: 1234567891011121314151617public class Bigram { private final char first; private final char second; ... public boolean equals(Bigram b) { //结果是重载而非覆盖 equals 方法 return b.first == first && b.second == second; } ...} 为了覆盖 Object.equals,必须定义一个参数为 Object 类型的 equals 方法,但是 Bigram 的 equals 方法的参数并不是 Object 类型,因此 Bigram 从 Object 继承了 equals 方法,而非覆盖了equals 方法。需要改成正确的覆盖方式: 123456789101112131415161718192021222324252627public class Bigram { private final char first; private final char second; ... @Override //当加入注解时,会给出覆盖 equals 方法提示 public boolean equals(**Object o**) { //覆盖 equals 方法 if (!(o instanceof Bigram)) { return false; } Bigram b = (Bigram) 0; return b.first == first && b.second == second; } ...} 【37】用标记接口定义类型标记接口是没有包含方法声明的接口,而只是指明一个类实现了某种属性的接口。例如,考虑 Serializable 接口。通过实现这个接口,类表明它的实例可以被写到 ObjectOutputStream (或者“被序列化”)。 序列化的原因:很多时候,由于通信协议的原因,在传输的过程中,复杂的类对象是不支持传来传去的,所以一般来说要转化成流的形式,放在包中传来传去。 标记接口的例子: 定义标记接口: 1public interface RandomAccess {} 标记接口继承: 1234567891011121314151617public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable //继承Random-Access { //... }public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable //未继承Random-Access { //... } 标记接口使用: 12345678910111213141516171819202122232425import java.util.List;import java.util.RandomAccess;public class SourceLearning{ public void iteratorElements(List<String> list) { //判断 list 是否为 RandomAccess 实例,以采取不同的处理策略 if (list instanceof **RandomAccess**) { ... } else { ... } }} 八、方法:【38】检查参数的有效性每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制,应该把这些限制写到文档中,并且在这个方法的开头处,通过显示的检查来实施这些限制。养成这样的的习惯是非常重要的。只要有效检查有一次失败,你为必要的有效性检查所付出的努力便都可以连本带利得到偿还了。 【39】必要时进行保护性拷贝如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性的拷贝这些组件。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当的修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。实例: 我们定义了一个Person类,该类的字段均为私有的,同时没有添加可以修改字段的方法: 123456789101112131415161718192021222324252627public class Person { private String name; private Date birth; public Person(String name, Date birth) { this.name = name; this.birth = birth; } public Date getBirth() { return birth; } public String getName() { return name; } } 该类看似是一个不可变类,实际上是这样的吗?可以测试一下: 1234567891011121314151617public static void main(String[] args) { Person p = new Person(\"Benson\",new Date(1990, 4, 13)); System.out.println(p.getName()); System.out.println(p.getBirth().getYear()); Date hole = p.getBirth(); //得到 Date 变量,但 Date 变量是可变的 hole.setYear(2013); //birth 改变 System.out.println(p.getName()); System.out.println(p.getBirth().getYear()); } 输出为: 1234567Benson 1990 Benson 2013 Person 实例被改变了,虽然 Person 实例中没有可以改变 Person 类的方法给外部访问,但是 Person 类中具有一个可变的属性 Date ,外部客户端可以先得到这个 Date 属性,再调用 Date 的 set() 方法来改变 Person 类,造成了类的可变性。 使用保护性拷贝: 12345678910111213public class Person { ... public Date getBirth() { return new Date(birth.toString()); //保护性拷贝 } ... } 之后再调用 Date hole = p.getBirth(); 得到的变量 hole 是 Person 属性 birth 的一个副本,之后再 调用 hole.set() 方法就不会改变 birth 属性的值了,对 Person 类就有了一个很好的保护。 【40】谨慎的设计方法签名(1)谨慎地选择方法的名称。方法的名称应该始终遵循标准的命名习惯。 (2)不要过于追求提供便利的的方法。每个方法都应该尽其所能。方法太多会使类难以学习、使用、文档化、测试和维护。 (3)避免过长的参数列表。目标是四个,或者更少。缩短参数列表的方法: <1>将方法分解为多个方法。 <2>创建辅助类。例如 draw(int x, int y, String name)中的(int x,int y)代表一个点,就可以用 draw(Point point,String name)来代替。 <3>从对象构建到方法调用都采用 Builder 模式。例如 fruit.addName(“Apple”).addColor(“Red”) (4)对于参数类型,要优先使用借口而不是类。如果使用的是类而不是接口,则限制了客户端只能传入特定的实现,如果碰巧输入的数据是以其他的形式存在,则就会导致不必要的,可能非常昂贵的拷贝操作。例如如果使用 Map 接口作为参数,就可以使你传入一个 Hashtable、HashMap、Tr-eeMap 或者任何有待于将来编写的 Map 实现。 (5)对于 boolean 参数,要优先使用两个元素的枚举类型。它使代码更易于阅读和编写,也使以后更易于添加更多的选项。 【41】慎用重载对于重载方法的选择是静态的,对于被覆盖的方法的选择则是动态的。 调用重载方法是在编译时作出决定的,选择被覆盖的方法的正确版本是在运行时进行的。 对于以下变量 collections : 1234567Collection<?> collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String,String>().values() };for (Collection<?> c : collections) { System.out.println(classify(c));} 对于 for 循环的三次迭代,Collection<?> 为参数的编译时类型,而 HashSet,Array-List,HashMap<String,String> 为三个参数的运行时类型。 重载的例子:如构造函数, 123public Test () { };public Test (String name) { }; 覆盖的例子:如子类Override复写, 12345678class Wine { String name() { return \"wine\"; }}class SparklingWine extends Wine { @Override String name() { return \"sparkling wine\"; }} 重载的安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,保守的策略是根本不要重载它。这项限制并不麻烦,因为你始终可以给方发起不同的名称,而不使用重载机制。 对于构造器,你没有选择使用不同名称的机会;一个类的多个构造器总是重载的。在许多情况下,可以选择导出静态工厂,而不是构造器。 总而言之,“能够重载方法”并不意味着“应该重载方法”。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法,但是在某些情况下,特别是涉及构造器的时候,就必定要重载 这种情况下,至少要避免这种情形:同一组参数只需要经过类型转换就可以传递给不同的重载方法。如果不可避免,就应当保证:当传递同样的参数时,所有重载方法的行为必须一致。 【42】慎用可变参数在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该被过度滥用。如果使用不当,会产生混乱的结果。 定义可变参数方法的一个好的例子: 1234567891011121314151617static int min(int firstArg,int... remainingArgs) { int min = firstArg; for (int arg:remainingArgs) { if (arg < min) { min = arg; } return min; }} 在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。假设确定对某个方法 95% 的调用会有 3 个或者更少的参数,就该声明 5 个重载,如下所示: 123456789public void foo() { };public void foo(int a1) { };public void foo(int a1,int a2) { };public void foo(int a1,int a2,int a3) { };public void foo(int a1,int a2,int a3,int... rest) { }; 【43】返回零长度的数组或者集合,而不是 null返回类型为数组或者为集合的方法不应该返回 null,而应该返回一个零长度的数组或者集合。集合值的方法可以做成在每当需要返回空集合的时候都返回同一个不可变的空集合。Collections.empty-Set、emptyList 和 emptyMap 提供的正是我们需要的。 12345678910111213public List<Cheese> getCheeseList() { if (cheeseInStock.isEmpty()) { return Collections.emptyList();//返回空集合,而不是 null } else { return new ArrayList<Cheese>(cheeseInStock); }} 【44】为所有导出的 API 元素编写文档注释Javadoc 的 {@code} 标签:使代码片段以代码字体进行呈现,并限制 HTML 标记和嵌套的 Jav-adoc 标签在代码片段中进行处理。后一种属性正是允许我们在代码片段中使用小于号(<)的东西,虽然它是一个 HTML 元字符。 Javadoc 的 {@literal} 标签也是为了产生包含 HTML 元字符的文档,比如小于号(<)、大于号(>)以及“与”(&)。除了它不以代码字体渲染文本之外,其余方面就像 {@code} 标签一样。例如: * The triangle inequality is { @literal |x + y|<|x| + |y| } 产生的文档为: “The triangle inequality is |x + y|<|x| + |y| ” 文档注释在源代码和产生的文档中都应该是易于阅读的。如果无法让两者都易读,产生的文档的可读性要优先于源码的可读性。 九、通用程序设计:【45】将局部变量的作用域最小化将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。几乎每个局部变量的声明都应该包含一个初始化表达式。如果还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。 如果在循环终止之后不再需要循环变量的内容,for 循环就优先于 while 循环。以下展示一个“剪切—粘贴”错误: 1234567891011121314151617Iterator<Element> i = c.iterator();while (i.hasNext()) { doSomething(i.next());}...Iterator<Element> i2 = c2.iterator();while (i.hasNext()) { //BUG! 变量 i 的作用域使 i 仍在有效范围 doSomethingElse();} 使用 for 循环可以避免这种问题: 1234567891011for (Iterator<Element> i = c.iterator();i.hasNext();) { doSomething(i.next());}for (Iterator<Element> i = c2.iterator();i.hasNext();) { //上一个变量 i 作用域已结束,不影响 doSomething(i.next());} 【46】for-each 循环优先于传统的 for 循环for-each 循环在简洁性和预防 BUG 方面有着传统的 for 循环无法比拟的优势,并且没有性能损失。应该尽可能地使用 for-each 循环。for-each 循环不仅让你遍历集合和数组,还让你遍历任何实现Iterable 接口的对象。例如: 使用 for 循环: 1234567891011for (Iterator<Suit> i = suits.iterator();i.hasNext();) { Suit suit = i.next(); for (Iterator<Rank> j = ranks.iterator();j.hasNext();) { deck.add(new Card(suit,j.next())); }} 使用 for-each 循环代替: 123456789for (Suit suit:suits) { for (Rank rank:ranks) { deck.add(new Card(suit,j.next())); }} 三种常见的不能使用 for-each 循环的情况: 过滤——如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove 方法。 转换——如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。 平行迭代——如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《EffectiveJava》(二)","slug":"EffectiveJava1","date":"2019-11-23T08:57:10.000Z","updated":"2019-11-26T13:52:45.344Z","comments":true,"path":"2019/11/23/EffectiveJava1/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/EffectiveJava1/","excerpt":"【15】使可变性最小化不可变类只是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。为了使类成为不可变,要遵循下面五条规则: <1>不要提供任何会修改对象状态的方法。 <2>保证类不会被扩展。 <3>使所有的域都是 final 的。 <4>使所有的域都成为私有的。 <5>确保对于任何可变组件的互斥访问。","text":"【15】使可变性最小化不可变类只是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。为了使类成为不可变,要遵循下面五条规则: <1>不要提供任何会修改对象状态的方法。 <2>保证类不会被扩展。 <3>使所有的域都是 final 的。 <4>使所有的域都成为私有的。 <5>确保对于任何可变组件的互斥访问。 不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时它们不会遭到破坏。这无疑是获得线程安全最容易的办法。 不可变类真正唯一的缺点是,对于每一个不同的值都需要一个单独的对象,创建这种对象的代价可能会很高。这时候就需要为不可变类提供一个共有的可变配套类来进行复杂的多阶段操作,例如: StringBuilder 类就是 String 类的可变配套类,用来改变 String 对象,以提升性能。 【16】复合优先于继承继承的功能非常强大,但也存在诸多问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。 复合设计:不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例,现有类成为了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有实例中的对应方法,并返回结果。例如: 12345678910111213141516171819202122232425262728293031323334//转发类(中间层),包含转发方法public class ForwardingSet<E> implements Set<E> { private final Set<E> s;//现有类的实例 public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear; } public boolean add(E e) { return s.add(e); }...}//复合类public class InstrumentedSet<E> extends ForwardingSet<E>{ private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @override public boolean add(E e){ addCount++; return super.add(e); }} 【17】要么为继承而设计,并提供文档说明,要么就禁止继承为了允许继承,类还需遵守其他一些约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用,因为超类的构造器是在子类的构造器之前运行的,所以子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种方法可以禁止子类化: <1>把这个类声明为 final 的。 <2>把类中所有的构造器都变成私有的,包括包级私有的,并增加一些公有的静态工厂来替代构造器。 你可以机械地消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的 “辅助方法(helper method)” 中,并让每个可覆盖方法调用它的私有辅助方法。然后,用 “直接调用可覆盖方法的私有辅助方法” 来代替 “可覆盖方法的每个自用调用” 。 【18】接口优于抽象类接口和抽象类机制之间的区别:抽象类允许包含某些方法的实现,但是接口则不允许;一个更重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类,而任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,而不管这个类是处于类层次的哪个位置。 抽象类的演变比接口的演变要容易得多。如果在后续的发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现。然后,该抽象类的所有实现都将提供这个新的方法,对于接口,这样做是行不通的。 通过对导出的每一个重要接口都提供一个抽象的骨架实现(skeletal implementation)类,可以把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。例如: 接口1: 123456789public interface IFoo { void foo(); void add(); void del();} 接口2: 12345public interface IBar { void bar();} 接口1的抽象骨架实现: 12345678910111213141516171819202122232425public abstract class AbstractFoo implements IFoo{ private String val; public String getVal() {**//自定义 get 方法,扩展接口1的实现** return val; } public void setVal(String val) {**//自定义 set 方法,扩展接口1的实现** this.val = val; } public void foo() {**//实现接口1的 foo() 方法** System.out.println(\"AbstarctFoo\"); } } 新的测试类: 123456789101112131415161718192021222324252627public class FooBar extends AbstractFoo implements IBar{ @Override public void bar() {**//实现 IBar 接口方法 bar()** // yingkhtodo Auto-generated method stub } @Override public void add() {**//实现 AbstractFoo 中未实现的接口1的 add() 方法** // yingkhtodo Auto-generated method stub } @Override public void del() {**//实现 AbstractFoo 中未实现的接口1的 del() 方法** // yingkhtodo Auto-generated method stub }} FooBar 类继承自 AbstractFoo 类,可以调用父类的 set()、get()、foo() 方法,也可以自己实现接口1和 IBar 接口的方法,其中接口1 foo() 方法在抽骨架价类 AbstractFoo 中实现,不需要再次实现,并且其它继承自 AbstractFoo 类的任意子类都不需要实现接口1的 foo() 方法, AbstractFoo 类接管了所有与接口1实现的相关工作。 【19】接口只用于定义类型接口应该只被用来定义类型,它们不应该被用来导出常量。如果常量被看做枚举类型的成员,就应该用枚举类型(enum type)来导出这些常量,否则,应该使用不可实例化的工具类(utility-class)来导出常量,如: 1234567891011public class PhysicalConstants { private PhysicalConstants() { } //Prevents instantiation public static final double AVOGADROS_NUMBER = 6.02214199e23; public static final double BOLTZMANN_CONSTANT = 1.3806503e-23; ...} 【20】类层次优于标签类标签类很少有使用的时候,当你想要编写一个包含显式标签域的类时,应该考虑一下,这个标签域是否可以被取消,这个类是否可以被层次类来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。例如,标签类如下: 1234567891011121314151617181920212223242526272829303132333435363738394041class Figure { enum Shape { RECTANGLE,CIRCLE }; final Shape shape;//标签域,用来表示图形类别 //矩形参数 double length; double width; //圆形参数 double radius Figure (double radius) { shape = Shape.CIRCLE; this.radiu = radius; } ... double area() { switch(shape) { case CIRCLE: return MATH.PI * (radius * radius); ... } }} 转换为层次类如下: 123456789101112131415161718192021abstract class Figure { abstract double area();}class Circle extends Figure { final double radius; Circle(Double radius) { this.radius = radius;} double area() { return Math.PI * (radius * radius);}}class Rectangle extends Figure {...} 【21】用函数对象表示策略有些语言支持函数指针(function pointer)、代理(delegate)、lambda表达式,或者支持类似机制,允许程序把 “调用特殊函数的能力 “ 存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。 函数指针的主要用途就是实现策略(Strategy)模式。为了在 JAVA 中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略被使用一次时,通常使用匿名类来声明和实现这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态 final 域被导出,其类型为该策略接口。 策略接口示例: 12345public interface Comparator<T> { public int compare(T t1,T t2);} 匿名类示例: 123456789Array.sort(stringArray,new Comparator<String>() { public int compare(String s1,s2) { return s1.length() - s2.length(); }}); 导出静态域实现策略示例: 12345678910111213141516171819class Host { private static class StrLenCmp implements Comparator<String>,Serializable { public int compare(String s1,String s2) { return s1.length() - s2.length(); }}//公有静态域public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();} 【22】优先考虑静态成员类嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。 静态成员类是最简单的一种嵌套类,它可以访问外围类的所有成员,包括那些声明为私有的成员。 非静态成员类每个实例都隐含着与外围类的一个外围实例相关联,在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的 this 构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,想要创建非静态成员类的实例是不可能的。 如果声明成员类不要求访问外围实例,就要始终把 static 修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。 匿名类出现在表达式中必须保持简短(10行或更少),否则会影响程序可读性。匿名类的一种常见用法是动态地创建函数对象(sort(A ,new B(){ … }););另一种常见用法是创建过程对象,如 Runnable、Thread 或者 ThreadTask 实例。 六、泛型:【23】请不要在新代码中使用原生态类型使用原生态类型会在运行时导致异常,因此不要在新代码中使用。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。Set 是个参数化类型,表示可以包含任何对象类型的一个集合;Set<?> 则是一个通配符类型,表示包含某种位置对象类型的一个集合;Set 则是个原生态类型,它脱离了泛型系统。前两种是安全的,最后一种不安全。 不要新代码中使用原生态类型,这条规则有两种例外: a.在类文字中必须使用原生类型,如 List.class、String[].class 合法,但是 List.class 不合法。 b.由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符上使用 instanceof 操作符是非法的。用无限制通配符类型代替原生态类型,对 instanceof 操作符的行为不会产生任何影响 下面是利用泛型来使用 instanceof 操作符的首选方法: 1234567if (o instanceof Set) { Set<?> m = (Set <?>) o; ...} 【24】消除非受检警告用泛型编程时,会遇到许多编译器警告,每一条警告都表示可能在运行时抛出 ClassCast- Exception 异常。要尽最大的努力消除这些警告。如果无法消除非受检警告,同时可以证明引起警告的代码是类型安全的,就可能在尽可能小的范围中,用@SupperessWarnings(unchecked)注解来禁止该警告。要用注释把禁止该警告的原因记录下来。 【25】列表优先于数组数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的;泛型是不可变且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好的混用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表 (List或者List) 代替数组 (Object[ ] 或者 E[ ] )。 123456789101112131415161718192021static <E> E reduce(List<E> list,Function<E> f,E initval) { List<E> snapshot; synchronized(list) { snapshot = new ArrayList<E>(list); } E result = initVal; for (E e:snapshot) { result = f.apply(result,e); } return result;} 【26】优先考虑泛型使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保他们不需要这种转换就可以使用。这通常意味着要把类做成泛型。只要时间允许,就要把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《EffectiveJava》(一)","slug":"EffectiveJava","date":"2019-11-23T07:25:42.000Z","updated":"2019-11-23T09:15:46.004Z","comments":true,"path":"2019/11/23/EffectiveJava/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/EffectiveJava/","excerpt":"一、基本原则:【1】模块的用户永远也不应该被模块的行为所迷惑【2】模块要尽可能小,但又不能太小【3】代码应该被重用,而不是被拷贝【4】模块之间的依赖性应该尽可能地降到最小【5】错误应该尽早被检测出来,最好是在编译时刻","text":"一、基本原则:【1】模块的用户永远也不应该被模块的行为所迷惑【2】模块要尽可能小,但又不能太小【3】代码应该被重用,而不是被拷贝【4】模块之间的依赖性应该尽可能地降到最小【5】错误应该尽早被检测出来,最好是在编译时刻 二、Java语言支持四种类型:接口(interface)、类(class)、数组(array)和基本类型(primitive) 前三种类型通常被称为引用类型(reference type),类实例和数组是对象(object),而基本类型的值则不是对象。 三、创建和销毁对象:【1】考虑用静态工厂方法代替构造器优点:a.静态工厂方法有名称,可以提示返回值,而构造器返回值是默认的 b.不必在每次调用静态工厂方法时都创建一个新的对象(单例模式) c.静态工厂方法可以返回元返回类型的任何子类型的对象 d.静态工厂方法在创建参数化类型实例的时候,使得代码变得更加简洁 实例: 一般定义: 1Map<String,List<String>> m = new HashMap<String,List<String>>(); 静态工厂方法: 12345public static <K,V> HashMap<K,V> newInstance(){ return new HashMap<K,V>();} 静态工厂方法定义: 1Map<String,List<String>> m = HashMap.newInstance(); 缺点: i.类如果不含有共有的或者受保护的构造器,就不能被子例化 ii.它们与其他的静态方法实际上没有任何的区别 常用方法: <1> valueOf() 类型转换方法 <2> getInstance() 获取实例,对于Singleton来说,该方法没有参数,并返回唯一实例 <3> newInstance() 获取新的不同实例 【2】遇到多个构造器参数时要考虑用构建器(Builder)如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与是用传统的重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBean更加安全。 实例: 123456789NutritionFacts cocaCola = new NutritionFacts.Builder(240,8) .calories(100) .sodium(35) .carbonhydrate(27) .build(); 【3】用私有构造器或者枚举类型强化Singleton属性实现方法: a.共有静态成员为final域 b.公有成员为静态工厂方法 c.包含单个元素的枚举类型 【4】通过私有构造器强化不可实例化的能力对于工具类,一般只包含静态方法和静态的域,这样的工具类(utility class)不希望被实例化,实例化对它没有任何意义。 企图将类做成抽象类来强制该类不可被实例化是行不通的,因为该类可以被子类化,并且该子类也可以被实例化。 将显示构造器声明为私有的就可以使类不能被实例化: 1234567891011public class UtilityClass{ //Suppress default constructor for noninstantiability private UtilityClass(){ throw new AssertionError(); }} 这样做的副作用:使一个类不能被子类化。 【5】避免创建不必要的对象例子: 123String s = new String(\"ABC\");//**DON‘T DO THIS**String s = \"ABC\";//**OK** tips:要优先使用基本数据类型而不是装箱基本类型,要当心无意识的自动装箱。 Long sum = 0L —-> Long sum = 0l 使用0L的时候,代表每调用一次sum就会新创建一个Long实例 通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非对象池中的对象是非常重量级的(例如数据库连接池、线程池)。 在提倡保护性拷贝的时候,因重用对象付出的代价要远远大于因创建重复对象而付出的代价,必要时如果没能实施保护性拷贝,会导致潜在的错误和安全漏洞;而不必要的创建对象则只会影响程序的风格和性能。 【6】消除过期的对象引用一般而言,只要类是自己管理内存,程序就应该警惕内存泄漏问题。 典型的例子就是 Stack 类,Stack 类自己管理内存,如果一个栈先是增长,然后再收缩,那么从栈中弹出来的对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,由于栈内维护着这些对象的过期引用,它们也不会被回收。 清空过期引用的例子: 123456789101112131415public Object pop(){ if(size == 0){ throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null;//消除过期对象引用 return result;} 【7】避免使用终结方法类中对象中封装的资源(例如文件或者线程)需要终止时,只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法即可。 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止。 12345678910111213Foo foo = new Foo(...);try{ //Do what must done with foo ...} finally { foo.terminate();//显式调用终止方法} 四、对于所有对象都通用的方法:【8】覆盖 equals 时请遵守通用约定Object类提供的equals方法:类的每个实例都只与它的自身相等 一般来说,内容相同不一定他们在内存中指向的地址也是相同的。而不同的对象在内存中若指向同一个地址,则他们的内容肯定是相同的(因为他们实际上就是同一个对象)。而==(两个等号)运算符与 equal函数就是运来比较这两块内容的。其中==运算符是用来比较内存中的地址是否相同,即比较它们的身份证号码是否相同。而equal函数则只比较他们的内容。如果他们的内容相同,即使身份证号码不相同(内存中的地址不同),这个函数也人们他们是相同的,会返回TRUE值。 例子: ◆String str1=new String(“welcome”); //创建一个对象,给利用单词welcome初始化 ◆String str2=new String(“welcome”); //创建一个对象,给利用单词welcome初始化 ◆String str3=str1; //创建一个对象,并利用对象str1的地址赋值 比较 str1==str2 返回 false 比较 str1==str3 返回 true 比较 str1.equals(str2) 返回 true Object 中的 equals 方法实现了等价关系,具有以下特性: (1)自反性 (2)对称性 (3)传递性 (4)一致性 (5)非空性 实现高质量的 equals 方法的诀窍: <1>使用 == 操作符检查“参数是否为这个对象的引用” <2>使用 instanceof 操作符检查“参数是否为正确的类型” <3>把参数转换成正确地类型 <4>对于该类中的每个“关键域”,检查参数中的域是否与该对象中对应的域匹配 <5>当你编写完成了 equals 方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的? 最后的忠告: ->覆盖 equals 时总要覆盖 hashCode ->不要企图让 equals 方法过于智能 ->不要将 equals 声明中的 Object 对象替换为其它的类型 【9】覆盖 equals 时总要覆盖 hashCode【10】始终要覆盖 toStringjava.lang.Object 类提供了一个原生的 toString 方法实现,返回的字符串为:类名称+@+hash-Code,例如 “PhoneNumber@163b91” ;为了使类用起来更加舒适,建议所有的子类都覆盖toString 方法。 在实际使用中,toString 方法应该返回对象中包含的所有值得关注的信息,无论你对返回的信息是否指定确切的格式,都应该在函数文档说明中明确地表明自己的意图;无论是否指定格式,都应该为 toString 返回值中包含的所有信息,提供一种编程式的访问途径。 例如:PhoneNumber类: 1234567891011121314151617181920212223242526272829/*** 以 “(XXX) YYY-ZZZZ” 的格式返回 PhoneNumber 实例*/@Override public String toString() { return String.format(\"(%03d) %03d-%04d,areaCode,prefix,lineNumber\");}public String getAreaCode() { return this.areaCode;}public String getPrefix() { return this.prefix;}public String getLineNumber() { return this.lineNumber;} 【11】谨慎地覆盖 clone记住一点:Cloneable接口具有许多的问题,其他的接口都不应该扩展这个接口,为了继承而设计的类也不应该实现这个接口。对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护 clone 方法,它的子类就不可能实现 Cloneable 接口。 【12】考虑实现 Comparable 接口通用约定:将对象与指定的对象进行比较,当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCa- stException异常。 五、类和接口:【13】使类和成员的可访问性最小化要区别设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其它实现细节。设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。 对于顶层的(非嵌套类)类和接口,只有两种可能的访问级别:包级私有的(package-private)和公有的(public)。如果你用 public 修饰符声明了顶层类或者接口,那它就是公有的;否则,它将是包级私有的。例如: 12345class Student { ...} 为包级私有的类,只有同一个包下的类才能够访问到,如果在另一个包内的类,就不能访问到。 12345public class Student{ ...} 为公有类,在不同的包内都可以访问到。 实例域决不能是公有的,以保证实例域的不可变性。 注意,长度非零的数组总是可变的,所以,类具有公有静态 final 数组域,或者返回这个域的访问方法,这几乎总是错误的。如果类具有这样的域或者访问方法,客户端就能够修改数组中的内容,可以使公有数组变成私有的,并增加一个公有的不可变列表来修正: 12345private static final Thing[ ] PRIVATE_VALUES = {...};public static final List<Thing> VALUES = Collection.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); 【14】在公有类中使用访问方法而非公有域1234567891011121314151617181920212223242526272829Class Point { private double x; private double y; public Point(double x,double y){ this.x = x; this.y = y; } public double getX(){ return x; } public double getY(){ return y; }...} 公有类永远都不应该暴露可变的域。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《设计原本》","slug":"设计原本","date":"2019-11-23T07:22:34.000Z","updated":"2019-11-23T09:19:49.765Z","comments":true,"path":"2019/11/23/设计原本/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E6%9C%AC/","excerpt":"【1】什么是设计?Dorothy Sayers(英国作家和戏剧家)说设计有三个阶段:概念构造的形成,在真实媒质上的实现,与真正用户的交互。 【2】新思想来自于将一门艺术中的领悟联系并应用到另一门艺术中,经理若干次这样的经历而有所悟,脑海里自然就孕育出了“新思想”。——培根","text":"【1】什么是设计?Dorothy Sayers(英国作家和戏剧家)说设计有三个阶段:概念构造的形成,在真实媒质上的实现,与真正用户的交互。 【2】新思想来自于将一门艺术中的领悟联系并应用到另一门艺术中,经理若干次这样的经历而有所悟,脑海里自然就孕育出了“新思想”。——培根 【3】给设计者的六点建议: a.专心研究以前设计者的工作,看看他们如何解决问题。 b.尝试弄明白他们为什么做出那样的设计决定,这是对你自己最有启发性的问题。 c.仔细研究以前设计者的风格。最好的方式是尝试用他们的一些风格勾画设计草图。 d.保存一本“草图本”,将您的想法、设计和局部设计记录下来,不论使用何种媒质 e.在开始设计时,写下您对用户和使用方式的假定。 f.设计、设计、设计! 【4】哪些领域的研究将有助于毕业生变成卓越的软件设计师? a.算法和数据结构是重要的基础课程。 b.计算机硬件架构。 c.应用领域,特别是商业数据处理、数据库技术和数据挖掘。 d.心理学,特别是直觉心理学,因为用户是最重要的。 【5】对于大多数的创作者来说,构思的不完整性和不一致性只有到了实现的时候才变得明显起来。因此,记录、试验和“解决”成为了理论家们的关键原则。 【6】设计中最困难的部分在于决定要设计什么,而设计师的主要任务乃是帮助客户发现他们想要的设计。 【7】快速原型是一种进行精准的需求配置的必要工具。不仅整个设计过程是迭代的,就连设计目标的设定过程也是迭代的。 【8】研习设计史的最有利的原因是去了解怎么样的设计方案是行不通的以及为什么这些设计方案行不通。(前世之事,后事之师?!) 【9】我们都是围绕着约束来做设计的,该过程要求对于设计空间中少有人问津的角落有着很多创新和探索的精神,这是设计之乐的部分所在,也是设计之难的大部分所在。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《架构之美》","slug":"架构之美","date":"2019-11-23T07:16:51.000Z","updated":"2019-11-23T09:19:37.558Z","comments":true,"path":"2019/11/23/架构之美/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/%E6%9E%B6%E6%9E%84%E4%B9%8B%E7%BE%8E/","excerpt":"【1】形式永远服从功能。 【2】好的架构的生成: <1>确实进行有意为之的前端设计。 <2>设计者的素质和经验。 <3>在开发过程中,保持清晰的设计观点。","text":"【1】形式永远服从功能。 【2】好的架构的生成: <1>确实进行有意为之的前端设计。 <2>设计者的素质和经验。 <3>在开发过程中,保持清晰的设计观点。 <4>授权团队负责软件的整体设计,而团队也承担起这一责任。 <5>不要害怕改变设计:没有什么是一成不变的。 <6>让合适的人加入到团队中,包括设计者、程序员和经理,确保开发团队的规模合适。 确保他们具有健康的工作关系,应为这些关系不可避免地影响代码的结构。 <7>在合适的时候做出设计决定,当你知道所有必要的信息时再做决定。延迟那些暂时不 能做出的决定。 <8>好的项目管理,以及合适的最后期限。 【3】新芯片的设计目标不是将一件事做得更快,而是同时做多件事。如果在芯片上运行的 多项任务实际上可以同时执行,那么在芯片层面上引入并发执行将带来更好的总体性能。 【4】关于数据传输:快重试与慢重试的交叉使用。快重试是指客户端发送文件,服务器接收 文件后返回状态码,客户端接收到的状态码如果不是OK则立即重新传输文件,最多重传3次; 慢重试是指如果3次重传返回的状态码依旧是FAIL,则将任务放在失败重传序列,序列将以20 分钟为间隔来进行新一轮的重传操作,直到文件传输完毕。当然在进行传输时需要设置超时时 间。 【5】面向资源的架构方法很优雅的实现了一些折中。一方面,对于传统的方法来说,这些方法 可能看起来有些奇怪,从而没有尝试过。如果人们关心自己的简历,就会希望停留在尝试过的、 真正在使用的方法。另一方面,对于那些研究过Web和它的基本组成模块的人来说,它很有意义, 代表人们设想和实现过的最大、最成功的网络软件架构。 【6】Facebook:数据隐私层;跨语言的进程间通信(IPC)系统Thrift;","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"《人月神话》","slug":"人月神话","date":"2019-11-23T07:10:52.000Z","updated":"2019-11-23T07:21:47.490Z","comments":true,"path":"2019/11/23/人月神话/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/23/%E4%BA%BA%E6%9C%88%E7%A5%9E%E8%AF%9D/","excerpt":"【1】由一个人来进行问题的分解,其他人给予他所需要的支持,以提高效率和生产力。 【2】系统的 体系结构(architecture) 指的是完整和详细的用户接口说明。 对于计算机,它是编程手册;对于编译器,它是语言手册; 对于控制程序,它是语言和函数调用手册;","text":"【1】由一个人来进行问题的分解,其他人给予他所需要的支持,以提高效率和生产力。 【2】系统的 体系结构(architecture) 指的是完整和详细的用户接口说明。 对于计算机,它是编程手册;对于编译器,它是语言手册; 对于控制程序,它是语言和函数调用手册; 对于整个系统,它是用户要完成自己全部工作所需参考的手册的集合。 【3】想要成功,结构师必须: a.牢记是开发人员承担创造性和发明性的实现责任, 所以结构师只能建议, 而不能支配; b.时刻准备着为所指定的说明建议 一种 实现的方法, 同样准备接受其他任何能达到目标的方法; c.对上述的建议保持低调和平静; d.准备放弃坚持所作的改进建议; 【4】一个可以开阔结构师眼界的准则是为每个小功能分配一个值:每次改进,功能 x 不超过 m字节的内存和 n微秒。这些值会在一开始作为决策的向导, 在物理实现期间充当指南和对所有人的警示。 【5】一个规格说明作者必须在仔细定义规定什么的同时,定义未规定什么。 【6】如同前面所示,形式化定义可以是一种设计实现。反之,设计实现也可以作为一种形式化定义的方法。当制造第一批兼容性的计算机时,我们使用的正是上述技术:新的机器同原有的机器一致。如果手册有一些模糊的地方?“问一问机器! ”——设计一段程序来得到其行为,新机器必须按照上述结果运行。 【7】认识到编程需要技术积累,需要开发很多公共单元构件。每个项目要有能用于队列、搜索和排序的例程或者宏库。对于每项功能,库至少应该有两个程序实现:运行速度较快和短小精炼的。 上述的公共库开发是一件重要的实现工作, 它可以与系统设计工作并行进行。 【8】由于缺乏空间而绞尽脑汁的编程人员,常常能通过从自己的代码中挣脱出来,回顾、分析实际情况,仔细思考程序的数据,最终获得非常好的结果。实际上,数据的表现形式 是编程的根本。 【9】不变只是愿望,变化才是永恒。——SWIFT 普遍的做法是,选择一种方法,试试看;如果失败了,没关系,再试试别的。不管怎么样,重要的是先去尝试。——富兰克林 D.罗斯福 【10】每个产品都应该有数字版本号,每个版本都应该有自己的日程表和冻结日期,在此之后的变更属于下一个版本的范畴。 【11】自顶向下的设计:开始是勾画出能得到主要结果的,但比较粗略的任务定义和大概的解决方案。然后,对该定义和方案进行细致的检查,以判断结果与期望之间的差距。同时,将上述步骤的解决方案,在更细的步骤中进行分解,每一项任务定义的精化变成了解决方案中算法的精化,后者还可能伴随着数据表达方式的精化。 【12】增量模式开发系统:首先系统应该能够运行,即使未完成任何有用功能,只能正确调用一系列伪子系统。接着,系统一点一点被充实,子系统轮流被开发,或者是在更低的层次调用程序、模块、子系统的占位符(伪程序)等。 【13】向软件系统增加必要的复杂性: a.层次化,通过分层的模块或者对象。 b.增量化,从而系统可以持续地运行。 【14】精炼、 充分和快速的程序。 往往是 战略性 突破的结果, 而不仅仅技巧上的提高。这种突破常常是一种新型算法。更普遍的是, 战略上突破常来自于数据或表的重新表达。 数据的表现形式是编程的根本。 【15】对于大多数项目,第一个开发的系统并不合用。它可能太慢、太大,而且难以使用,或者三者兼而有之。系统的丢弃和重新设计可以一步完成, 也可以一块块地实现。 这是个 必须完成的步骤。 【16】对于大型项目, 一个对里程碑报告进行维护的 计划和控制(Plan and Control )小组是非常可贵的。 【17】每一份发布的程序拷贝应该包括一些测试用例,其中一部分用于校验输入数据,一部分用于边界输入数据,另一部分用于无效的输入数据。 【18】增量开发模型:首先设计框架,再向框架内添加内容,保证在每一个设计阶段都有可运行的系统输出,从空的框架一步步到添加了各个功能的完整系统。 【19】设计类似一棵树的技巧是将那些变化可能性较小的设计决策放置在树的根部。这样的设计使得模块的重用最大化。更重要的是,可以延伸相同的策略,使它不但可以包括发布产品,而且还包括以增量开发策略创建的后续中间版本。这样,产品可以通过它的中间阶段,以最低限度的回溯代价增长。 【20】“从第一个里程碑开始构建”的微软流程和快速原型之间的差别是什么?功能。 第一个里程碑产品可能不包含足够的功能使任何人对它产生兴趣,而可发布产品和定义中的一样,在完整性上——配备了一系列实用的功能集,在质量上——它可以健壮地运行。 【21】人力(人)和时间(月)之间的平衡远不是线性关系,使用人月作为生产率的衡量标准实际是一个神话: a.第一次发布的成本最优进度时间,T = 2. 5(M M )1/ 3 。即,月单位的最优时间是估计工作量(人月)的立方根,估计工作量则由规 模估计和模型中的其他因子导出。最优人员配备曲线是由推导得出的。 b.当计划进度比最优进度长时, 成本曲线会缓慢攀升。 时间越充裕, 花的时间也越长。 c.当计划进度比最优进度短时,成本曲线急剧升高。 d.无论安排多少人手, 几乎没有任何项目能够在少于 3/ 4的最优时间内获得成功! 【22】对于项目的成功而言,项目人员的素质、人员的组织管理是比使用的工具或采用的技术方法更重要的因素。 【23】如果人们认同我在文中多处提到的观点——创造力来自于个人,而不是组织架构或者开发过程, 那么项目管理面对的中心问题是如何设计架构和流程, 来提高而不是压制主动性和创造力。 【24】通过权力委派,中心的权威实际上是得到了加强;从整体而言,组织机构实际上更加融洽和繁荣。 【25】“大多数成功的第 4 代语言是以选项和参数方式系统化某个应用领域的结果。 ”这些第 4 代语言最普遍的情况是带有查询语言、数据库以及通讯软件包的应用生成程序。 【26】软件工程的一些特殊问题: a.如何把一系列程序设计和构建成 系统。 b.如何把程序或者系统设计成健壮的、经过测试的、文档化的、可支持的产品。 c.如何维持对大量的 复杂性 的控制。 【27】软件工程这个复杂的行业需要:进行持续的发展;学习使用更大的要素来开发;新工具的最佳使用;经论证的管理方法的最佳应用;良好判断的自由发挥;以及能够使我们认识到自己不足和容易犯错的——上帝所赐予的谦卑。 【28】乍一看,用任何规则或者原理来约束创造性思维的想法是一种阻碍, 而不是帮助, 但实际情况中完全不是这样. 规范的思维实际上是促进而不是阻碍了灵感的产生。 【29】Brooks《 人月神话 》的核心论点:由于人力的划分,大型项目遭遇的管理问题与小型项目的不同;因此,产品的概念完整性很关键;取得这种统一性是很困难,但并不是不可能的。","categories":[{"name":"读书笔记","slug":"读书笔记","permalink":"https://www.smartonline.net.cn/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"book","slug":"book","permalink":"https://www.smartonline.net.cn/tags/book/"}]},{"title":"贪婪算法介绍","slug":"greedy","date":"2019-11-18T12:57:57.000Z","updated":"2019-11-20T08:46:36.065Z","comments":true,"path":"2019/11/18/greedy/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/greedy/","excerpt":"算法简介贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。 贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。","text":"算法简介贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。 贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。 贪婪算法并没有固定的算法解决框架,算法的关键是贪婪策略的选择,根据不同的问题选择不同的策略。 必须注意的是策略的选择必须具备无后效性,即某个状态的选择不会影响到之前的状态,只与当前状态有关,所以对采用的贪婪的策略一定要仔细分析其是否满足无后效性。 比如前边介绍的最短路径问题(广度优先、狄克斯特拉)都属于贪婪算法,只是在其问题策略的选择上,刚好可以得到最优解。 基本思路其基本的解题思路为: 1.建立数学模型来描述问题 2.把求解的问题分成若干个子问题 3.对每一子问题求解,得到子问题的局部最优解 4.把子问题对应的局部最优解合成原来整个问题的一个近似最优解 案例这边的案例来自”算法图解”一书 案例一区间调度问题: 假设有如下课程,希望尽可能多的将课程安排在一间教室里: 课程 开始时间 结束时间 美术 9AM 10AM 英语 9:30AM 10:30AM 数学 10AM 11AM 计算机 10:30AM 11:30AM 音乐 11AM 12PM 这个问题看似要思考很多,实际上算法很简单: 1.选择结束最早的课,便是要在这教室上课的第一节课 2.接下来,选择第一堂课结束后才开始的课,并且结束最早的课,这将是第二节在教室上的课。 重复这样做就能找出答案,这边的选择策略便是结束最早且和上一节课不冲突的课进行排序,因为每次都选择结束最早的,所以留给后面的时间也就越多,自然就能排下越多的课了。 每一节课的选择都是策略内的局部最优解(留给后面的时间最多),所以最终的结果也是近似最优解(这个案例上就是最优解)。 (该案例的代码实现,就是一个简单的时间遍历比较过程) 案例二背包问题:有一个背包,容量为35磅 , 现有如下物品 物品 重量 价格 吉他 15 1500 音响 30 3000 笔记本电脑 20 2000 显示器 29 2999 笔 1 200 要求达到的目标为装入的背包的总价值最大,并且重量不超出。 方便计算所以只有3个物品,实际情况可能是成千上万。 同上使用贪婪算法,因为要总价值最大,所以每次每次都装入最贵的,然后在装入下一个最贵的,选择结果如下: 选择: 音响 + 笔,总价值 3000 + 200 = 3200 并不是最优解: 吉他 + 笔记本电脑, 总价值 1500 + 2000 = 3500 当然选择策略有时候并不是很固定,可能是如下: (1)每次挑选价值最大的,并且最终重量不超出: 选择: 音响 + 笔,总价值 3000 + 200 = 3200 (2)每次挑选重量最大的,并且最终重量不超出(可能如果要求装入最大的重量才会优先考虑): 选择: 音响 + 笔,总价值 3000 + 200 = 3200 (3)每次挑选单位价值最大的(价格/重量),并且最终重量不超出: 选择: 笔+ 显示器,总价值 200 + 2999 = 3199 如上最终的结果并不是最优解,在这个案例中贪婪算法并无法得出最优解,只能得到近似最优解,也算是该算法的局限性之一。该类问题中需要得到最优解的话可以采取动态规划算法(后续更新,也可以关注我的公众号第一时间获取更新信息)。 案例三集合覆盖问题: 假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。 广播台 覆盖地区 K1 ID,NV,UT K2 WA,ID,MT K3 OR,NV,CA K4 NV,UT K5 CA,AZ … … 如何找出覆盖所有地区的广播台的集合呢,听起来容易,实现起来很复杂,使用穷举法实现: (1) 列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2ⁿ个【由组合及二项式定理得出:C(n,1)+C(n,2)+C(n,3)+…+C(n,n) = 2^n】 (2) 在这些集合中,选出覆盖全部地区的最小的集合,假设n不在,但是当n非常大的时候,假设每秒可以计算10个子集 广播台数量n 子集总数2ⁿ 需要的时间 5 32 3.2秒 10 1024 102.4秒 32 4294967296 13.6年 100 1.26*100³º 4x10²³年 目前并没有算法可以快速计算得到准备的值, 而使用贪婪算法,则可以得到非常接近的解,并且效率高: 选择策略上,因为需要覆盖全部地区的最小集合: (1) 选出一个广播台,即它覆盖了最多未覆盖的地区即便包含一些已覆盖的地区也没关系 (2) 重复第一步直到覆盖了全部的地区 这是一种近似算法(approximation algorithm,贪婪算法的一种)。在获取到精确的最优解需要的时间太长时,便可以使用近似算法,判断近似算法的优劣标准如下: 速度有多快 得到的近似解与最优解的接近程度 在本例中贪婪算法是个不错的选择,不仅运行速度快,本例运行时间O(n²),最坏的情况,假设n个广播台,每个广播台就覆盖1个地区,n个地区,总计需要查询n*n=O(n²),实现可查看后面的java代码实现 广播台数量n 子集总数2ⁿ 穷举需要时间 贪婪算法 5 32 3.2秒 2.5秒 10 32 102.4秒 10秒 32 32 13.6年 102.4秒 100 32 4x10²³年 1000秒 此时算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区,可能不是预期中的K2, K3,K4,K5(也许预期中的更便宜,更便于实施等等) NP完全问题案例四旅行商问题 假设有旅行商需要从下面三个城市的某一个城市出发,如何规划路线获取行程的最短路径。 存在3!(阶乘)=6种可能情况: A->B->C A->C->B B->A->C B->C->A C->A->B C->B->A 这边和之前求最短路径的算法(广度搜索、狄克斯特拉、贝尔曼-福特),最大的差别是没有固定源点(起点),,每一个节点都可能是源点,并且需要经过每一个节点,所以若穷举法则不得不找出每一种可能并进行比较。 当城市数量为n,则可能性为n!,假设每秒处理判断一个路线 数量n 总数n! 穷举需要时间 5 120 120秒 10 32 42天 而使用贪婪算法,随机选择从一个城市出发,比如A,每次选择从最近的还没去过的城市出发,则可以得到近似最优解。 第一次比较n-1个城市 第二次比较n-2个城市 … 第n-1次比较1个城市 第n次不存在需要比较的了个 0+1+2+3+..+(n-1) ≈ O(n²/2) 数量n 总数n! 穷举需要时间 贪婪需要时间 5 120 120秒 12.5秒 10 32 42天 50秒 类似上述集合覆盖问题、旅行商问题,都属于NP完全问题,在数学领域上并没有快速得到最优解的方案,贪婪算法是最适合处理这类问题的了。 如何判断是NP完全问题的:1.元素较少时,一般运行速度很快,但随着元素数量增多,速度会变得非常慢 2.涉及到需要计算比较”所有的组合”情况的通常是NP完全问题 3.无法分割成小问题,必须考虑各种可能的情况。这可能是NP完全问题 4.如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题 5.如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题 6.如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题 小结1.贪婪算法可以寻找局部最优解,并尝试与这种方式获得全局最优解 2.得到的可能是近似最优解,但也可能便是最优解(区间调度问题,最短路径问题(广度优先、狄克斯特拉)) 3.对于完全NP问题,目前并没有快速得到最优解的解决方案 4.面临NP完全问题,最佳的做法就是使用近似算法 5.贪婪算法(近似算法)在大部分情况下易于实现,并且效率不错 JAVA 实现贪婪算法需要根据具体问题,选择对应的策略来实现,所以这边只取集合覆盖问题做个示例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/** * 贪婪算法 - 集合覆盖问题 * @author Administrator * */ public class Greedy { public static void main(String[] args){ //初始化广播台信息 HashMap<String,HashSet<String>> broadcasts = new HashMap<String,HashSet<String>>(); broadcasts.put(\"K1\", new HashSet(Arrays.asList(new String[]{\"ID\",\"NV\",\"UT\"}))); broadcasts.put(\"K2\", new HashSet(Arrays.asList(new String[] {\"WA\",\"ID\",\"MT\"}))); broadcasts.put(\"K3\", new HashSet(Arrays.asList(new String[] {\"OR\",\"NV\",\"CA\"}))); broadcasts.put(\"K4\", new HashSet(Arrays.asList(new String[] {\"NV\",\"UT\"}))); broadcasts.put(\"K5\", new HashSet(Arrays.asList(new String[] {\"CA\",\"AZ\"}))); //需要覆盖的全部地区 HashSet<String> allAreas = new HashSet(Arrays.asList(new String[] {\"ID\",\"NV\",\"UT\",\"WA\",\"MT\",\"OR\",\"CA\",\"AZ\"})); //所选择的广播台列表 List<String> selects = new ArrayList<String>(); HashSet<String> tempSet = new HashSet<String>(); String maxKey = null; while(allAreas.size()!=0) { maxKey = null; for(String key : broadcasts.keySet()) { tempSet.clear(); HashSet<String> areas = broadcasts.get(key); tempSet.addAll(areas); //求出2个集合的交集,此时tempSet会被赋值为交集的内容,所以使用临时变量 tempSet.retainAll(allAreas); //如果该集合包含的地区数量比原本的集合多 if (tempSet.size()>0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) { maxKey = key; } } if (maxKey != null) { selects.add(maxKey); allAreas.removeAll(broadcasts.get(maxKey)); } } System.out.print(\"selects:\" + selects); } } 执行完main方法打印信息如下: 1selects:[K1, K2, K3, K5] 参考:https://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741375.html 原文链接:https://www.jianshu.com/p/fede80bad3f1","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"}]},{"title":"SHA(安全散列算法)简单实现","slug":"sha","date":"2019-11-18T12:42:00.000Z","updated":"2019-11-18T13:20:09.270Z","comments":true,"path":"2019/11/18/sha/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/sha/","excerpt":"摘要算法:SHA 及 Java 实现样例 SHA = 安全散列算法(Secure Hash Algorithm)。 SHA 与 MD5 类似,都是单向不可逆散列函数,用于计算消息摘要,生成消息数字签名(指纹)。","text":"摘要算法:SHA 及 Java 实现样例 SHA = 安全散列算法(Secure Hash Algorithm)。 SHA 与 MD5 类似,都是单向不可逆散列函数,用于计算消息摘要,生成消息数字签名(指纹)。 Algorithm 散列值长度(单位比特) SHA-1 160 SHA-224 224 SHA-256 256 SHA-384 384 SHA-512 512 Java 实现样例: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import java.security.MessageDigest; public class MySHA { public static void main(String[] args) throws Exception { // TODO Auto-generated method stub String msg = \"0123456789abcdef\"; MessageDigest sha = MessageDigest.getInstance(\"SHA\"); sha.update(msg.getBytes()); byte []shaBin = sha.digest(); printBytes(shaBin); MessageDigest sha1 = MessageDigest.getInstance(\"SHA-1\"); sha1.update(msg.getBytes()); printBytes(sha1Bin); MessageDigest sha224 = MessageDigest.getInstance(\"SHA-224\"); sha224.update(msg.getBytes()); byte []sha224Bin = sha224.digest(); printBytes(sha224Bin); MessageDigest sha256 = MessageDigest.getInstance(\"SHA-256\"); sha256.update(msg.getBytes()); byte []sha256Bin = sha256.digest(); printBytes(sha256Bin); MessageDigest sha384 = MessageDigest.getInstance(\"SHA-384\"); sha384.update(msg.getBytes()); byte []sha384Bin = sha384.digest(); printBytes(sha384Bin); MessageDigest sha512 = MessageDigest.getInstance(\"SHA-512\"); sha512.update(msg.getBytes()); byte []sha512Bin = sha512.digest(); printBytes(sha512Bin); } } /** * 十六进制打印字节数组 * @param b byte[] */ public static void printBytes(byte[] b) { for(int i=0;i<b.length;i++) { System.out.printf(\"%02X\", b[i]); } System.out.println(); } } 注:散列值都是按照十六进制大写字母编码表示 很多人肯定会出来反驳,加密简单的123456可以在某些解密网站直接解密出来。 在这样的情况下,我们可以尝试在字符串追加其他文字如yangzhangyin,实际如下: 123456789101112131415161718192021222324import java.math.BigInteger; import java.security.MessageDigest; public class SHAEncryption { public static byte[] encryptSHA(byte[] data, String shaN) throws Exception { MessageDigest sha = MessageDigest.getInstance(shaN); sha.update(data); return sha.digest(); } public static String encryptFlychordPwd(String str) { byte[] outputData = new byte[0]; try { outputData = encryptSHA((str+\"yangzhangyin\").getBytes(), \"SHA-256\"); return new BigInteger(1, outputData).toString(16); } catch (Exception e) { e.printStackTrace(); } return \"\"; } public static void main(String[] args) { //加密123456 System.out.println(ss.encryptFlychordPwd(\"123456\")); } } 这样就解决了简单密码被解密的问题啦。","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"},{"name":"java","slug":"java","permalink":"https://www.smartonline.net.cn/tags/java/"}]},{"title":"Diffie-Hellman(秘钥协商算法)介绍","slug":"diffie-hellman","date":"2019-11-18T12:35:06.000Z","updated":"2019-11-19T00:48:49.698Z","comments":true,"path":"2019/11/18/diffie-hellman/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/diffie-hellman/","excerpt":"一、概述 Diffie-Hellman密钥协商算法主要解决秘钥配送问题,本身并非用来加密用的;该算法其背后有对应数学理论做支撑,简单来讲就是构造一个复杂的计算难题,使得对该问题的求解在现实的时间内无法快速有效的求解(computationally infeasible )。","text":"一、概述 Diffie-Hellman密钥协商算法主要解决秘钥配送问题,本身并非用来加密用的;该算法其背后有对应数学理论做支撑,简单来讲就是构造一个复杂的计算难题,使得对该问题的求解在现实的时间内无法快速有效的求解(computationally infeasible )。 理解Diffie-Hellman密钥协商的原理并不困难,只需要一点数论方面的知识既可以理解,主要会用到简单的模算术运算、本原根、费马小定理、离散对数等基础数论的知识。在现代密码学中的基础数论知识梳理中已经对这些知识做了必要的总结。 二、从何而来 DH密钥协商算法在1976年在Whitfield Diffie和Martin Hellman两人合著的论文New Directions in Cryptography(Section Ⅲ PUBLIC KEY CRYPTOGRAPHY)中被作为一种公开秘钥分发系统(public key distribution system)被提出来。原文的叙述过程比较简单,但基本阐述了算法的原理以及其可行性。 在该论文中实际上提出了一些在当时很有创新性的思想。原论文重点讨论两个话题: (1)在公网通道上如何进行安全的秘钥分派。 (2)认证(可以细分为消息认证和用户认证)。 为了解决第一个问题,原文提出两种方法:公钥加密系统(public key cryptosystem)和秘钥分发系统(public key distribution system)。对于公钥加密系统,原文只是勾画了一种比较抽象的公钥加密系统的概念模型,重点是加解密采用不同的秘钥,并总结了该系统应该满足的一些特性,相当于是一种思想实验,并没有给出具体的算法实现途径,但这在当时应该来说已经足够吸引人。后来RSA三人组(Ron Rivest、Adi Shamir 和 Leonard Adleman)受此启发,经过许多轮失败的尝试后,于第二年在论文A Method for Obtaining Digital Signatures and Public-Key Cryptosystems中提出了切实可行且很具体的公钥加密算法–RSA公钥加密算法。而对于秘钥分发系统,就是本文的DH秘钥协商算法。 为了解决第二个问题,原文通过单向函数(one-way function)来解决,这就是单向认证的问题。另外作者还讨论了这些密码学问题之间的关联性以及如何相互转化。比如一个安全的密码系统(可以防御明文攻击)可以用来生成一个的单向函数、公钥加密系统可以用来作为单向认证、陷门密码系统可以用来生成一个公钥加密系统。数学难题的计算复杂度被当成一种保障密码学安全问题的有效工具被利用起来,这一重要思想贯穿现代密码学的许多加密算法。 三、算法流程及原理 按照惯例,以Alice和Bob这两个密码学中的网红为角色,述阐DH算法的流程。 假设Alice需要与Bob协商一个秘钥(秘钥本质上就是一个比特序列,从计算的角度看就是一个大数)。 1)首先Alice与Bob共享一个素数p以及该素数p的本原根g(geneator),当然这里有2⩽g⩽p−1。这两个数是可以不经过加密地由一方发送到另一方,至于谁发送给并不重要,其结果只要保证双方都得知p和g即可。 2)然后Alice产生一个私有的随机数A,满足1⩽A⩽p−1,然后计算gAmodp=Ya,将结果Ya通过公网发送给Bob;与此同时,Bob也产生一个私有的随机数B,满足1⩽B⩽p−1,计算gBmodp=Yb,将结果Yb通过公网发送给Alice。 3)此时Alice知道的信息有p,g,A,Ya,其中数字A是Alice私有的,只有她自己知道,别人不可能知道,其他三个信息都是别人有可能知道的;Bob知道的信息有p,g,B,Yb,其中数字B是Bob私有的,只有他自己知道,别人不可能知道,其他都是别人有可能知道的。 到目前为止,Alice和Bob之间的秘钥协商结束。 Alice通过计算Ka=(Yb)Amodp得到秘钥Ka,同理,Bob通过计算Kb=(Ya)Bmodp得到秘钥Kb,此时可以证明,必然满足Ka=Kb。因此双方经过协商后得到了相同的秘钥,达成秘钥协商的目的。 证明: 对于Alice有: Ka=(Yb)Amodp=(gBmodp)Amodp=gB×Amodp 对于Bob有: Kb=(Ya)Bmodp=(gAmodp)Bmodp=gA×Bmodp 可见,Alice和Bob生成秘钥时其实是进行相同的运算过程,因此必然有Ka=Kb。”相同的运算过程”是双方能够进行秘钥协商的本质原因,类似的利用椭圆曲线进行秘钥协商也是与之相同的原理。 更严密地考虑,A和B不应该选择p−1,也就是说只能在集合{1,2,3,…,p−2}中选择。这是因为如果选择p−1,那么由费马小定理可知,情况就退化成了gp−1≡1(modp)的情况,对秘钥协商的机密性构成威胁。 所以总结起来,整个流程串起来大概就是这样: 那么窃听者Eve能否破解秘钥呢?首先要知道Eve能够得知哪些信息,显然Eve能够窃听到的信息只能有p,g,Ya,Yb,现在的问题是Eve能够通过以上信息计算出Ka或者Kb吗?要计算Ka或者Kb需要知道A或者B。 以计算A为例,Eve能根据条件gAmodp=Ya计算出A吗?实际上当p是大质数的时候,这是相当困难的,这就是离散对数问题。实际上在论文发表的当时,计算该问题的最有效的算法的时间复杂度大约是O(p–√)。也正是求解该问题在计算上的困难程度保证了DH算法的安全性。如果能够找到对数时间复杂度的算法,那么该算法即容易被攻破。 四、一个实例 1)假设Alice和Bob共享的p和g分别是p=17,g=3,显然这里g=3是p=17的一个本原根,实际上3,5,6,7,10,11,12,14都是17的本原根。 2)然后Alice选定一个私有数字,假设A=15,计算Ya=315mod17=14348907mod17=6,将6发送给Bob;同时Bob也选定一个私有的数字,假设B=13,计算Ya=313mod17=1594323mod17=12,将12发送给Alice。 3)Alice计算秘钥Ka=1215mod17=2147483647mod17=8,Bob计算秘钥Kb=613mod17=2147483647mod17=8。双方经过协商后,8最终成为双方的协商的秘钥。 实际上,当指数和模数的位数都比较大的时候,存在一种快速计算幂取模的算法叫做“反复平方算法”,实现取来也比较简单,在算法导论中第三十一章有相应的解释。 五、存在的问题 是否DH秘钥协商算法就一定安全呢?应该说也不是,因为存在一种伪装者攻击(或者称为中间人攻击)能够对这种秘钥协商算法构成威胁。 假设秘钥协商过程中,在Alice和Bob中间有一个称为Mallory的主动攻击者,他能够截获Alice和Bob的消息并伪造假消息,考虑如下情况。 1)Alice和Bob已经共享一个素数p及其该素数p的本原根g,当然Mallory监听到报文也得知了这两个消息。 2)此时Alice计算Ya=gAmodp,然而在将Ya发送给Bob的过程中被Mallory拦截,Mallory自己选定一个随机数S,计算Ysb=gSmodp,然后将Ysb发送给了Bob。 3)同时Bob计算Yb=gBmodp,然而在将Yb发送给Alice的过程中被Mallory拦截,Mallory自己选定一个随机数T,计算Yta=gTmodp,然后将Yta发送给了Alice。 由于通讯消息被替换,Alice计算出的秘钥实际上是Alice和Mallory之间协商秘钥:Kam=gA×Tmodp;Bob计算出的秘钥实际上是Bob与Mallory之间协商的秘钥:Kbm=gB×Smodp。如果之后Alice和Bob用他们计算出的秘钥加密任何信息,Mallory截获之后都能够解密得到明文,而且Mallory完全可以伪装成Alice或者Bob给对方发消息。 六、References 1、New Directions in Cryptography 2、密码编码学与网络安全原理与实践 3、图解密码技术 原文出处:http://www.cnblogs.com/qcblog/p/9016704.html","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"}]},{"title":"Simhash算法介绍","slug":"simhash","date":"2019-11-18T12:20:56.000Z","updated":"2019-11-19T00:54:23.971Z","comments":true,"path":"2019/11/18/simhash/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/simhash/","excerpt":"Simhash的生成及存储 一、背景介绍 根据 Detecting Near-Duplicates for Web Crawling 论文中的介绍,在互联网中有很多网页的内容是一样的,但是它们的网页元素却不是完全相同的。每个域名下的网页总会有一些自己的东西,比如广告、导航栏、网站版权之类的东西,但是对于搜索引擎来讲,只有内容部分才是有意义的,虽然网页元素不同,但是对搜索结果没有任何影响,所以在判定内容是否重复的时候,应该忽视后面的部分。当新爬取的内容和数据库中的某个网页的内容一样的时候,就称其为Near-Duplicates(重复文章)。对于重复文章,不应再执行入库操作,这种操作的优点是(A)节省带宽、(B)节省磁盘、(C)减轻服务器负荷以及(D)去除相似文章噪点干扰,提升索引的质量。","text":"Simhash的生成及存储 一、背景介绍 根据 Detecting Near-Duplicates for Web Crawling 论文中的介绍,在互联网中有很多网页的内容是一样的,但是它们的网页元素却不是完全相同的。每个域名下的网页总会有一些自己的东西,比如广告、导航栏、网站版权之类的东西,但是对于搜索引擎来讲,只有内容部分才是有意义的,虽然网页元素不同,但是对搜索结果没有任何影响,所以在判定内容是否重复的时候,应该忽视后面的部分。当新爬取的内容和数据库中的某个网页的内容一样的时候,就称其为Near-Duplicates(重复文章)。对于重复文章,不应再执行入库操作,这种操作的优点是(A)节省带宽、(B)节省磁盘、(C)减轻服务器负荷以及(D)去除相似文章噪点干扰,提升索引的质量。 在现实中,一模一样的网页的概率是很小的,大部分的相似网页都会存在一些细节的变化,而如何进行这种判定就是一个本文要解决的一个问题。除了近似文章判定算法的难题,还有以下待解决的难点(按照80亿篇文章来考虑): 数据规模巨大,对于海量数据如何存储 查找速度,如何做到在毫秒级别返回检索结果 二、simhash介绍 simhash是由 Charikar 在2002年提出来的,它是一种能计算文档相似度的hash算法,google用它来进行海量文本去重工作。simhash属于局部敏感型(locality sensitive hash)的一种,其主要思想是降维,将高维的特征向量转化成一个f位的指纹(fingerprint),通过算出两个指纹的海明距离(hamming distince)来确定两篇文章的相似度,海明距离越小,相似度越低(根据 Detecting Near-Duplicates for Web Crawling 论文中所说),一般海明距离为3就代表两篇文章相同。 simhash也有其局限性,在处理小于500字的短文本时,simhash的表现并不是很好,所以在使用simhash前一定要注意这个细节。 三、simhash与hash算法的区别 传统Hash算法只负责将原始内容尽量均匀随机地映射为一个签名值,原理上仅相当于伪随机数产生算法。传统hash算法产生的两个签名,如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,所产生的签名也很可能差别很大,所以传统Hash是无法在签名的维度上来衡量原内容的相似度。而SimHash本身属于一种局部敏感哈希算法,它产生的hash签名在一定程度上可以表征原内容的相似度。 我们主要解决的是文本相似度计算,要比较的是两个文章是否相似,当然我们降维生成了hash签名也是用于这个目的。看到这里,估计大家就明白了,即使把文章中的字符串变成 01 串,我们使用的simhash算法也还是可以用于计算相似度,而传统的hash却不行。我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。 通过simhash计算结果为: 1000010010101101111111100000101011010001001111100001001011001011 1000010010101101011111100000101011010001001111100001101010001011 通过传统hash计算为: 0001000001100110100111011011110 1010010001111111110010110011101 大家可以看得出来,相似的文本只有部分 01 串变化了,而普通的hash却不能做到,这个就是局部敏感哈希的魅力。 四、simhash的生成 simhash的生成图解如下图: 为了更加通俗易懂,采用例子来详解simhash的生成规则。simhash的生成划分为五个步骤:分词->hash->加权->合并->降维 1:分词。首先,判断文本分词,形成这个文章的特征单词。然后,形成去掉噪音词的单词序列。最后,为每个分词加上权重。我们假设权重分为5个级别(1~5),比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。 2:hash。通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样,我们的字符串就变成了一串串数字,还记得文章开头说过的吗?要把文章变为数字计算,才能提高相似度计算性能,现在是降维过程进行时。 3:加权。在第2步骤hash生成结果后,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。 4:合并。把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。这里作为示例只算了两个单词的,真实的计算需要把所有单词的序列串累加。 5:降维。把第4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于或等于0 则记为 0。最后算出结果为:“1 0 1 0 1 1”。 整个过程的流程图为: 五、simhash分表存储策略 在线上查询算法中,首先建立多个指纹表:T1,T2,√…,Tt。每个指纹表 Ti 关联两个未知数:一个整型pi和一个在f bit-positions上的排列πi,Ti就是对已经存在的所有指纹进行排列πi得到的有序集合。对于一个指纹f和一个整数k,算法分两个步骤: 1 找到Ti中所有的前pi个bit-positions和πi(F)的前pi个bit-positions相同的指纹,假设为指纹集合F。 2 在F中的每一个指纹,比较其是否和πi(F)有的差异不超过k个。 分表存储原理 借鉴“hashmap算法找出可以hash的key值”。因为我们使用的simhash是局部敏感哈希,这个算法的特点是:只要相似的字符串,只有个别的位数是有差别变化的。这样,我们可以推断两个相似的文本,至少有16位的simhash是一样的。 分表存储设计 假设f = 64 ,k=3,并且我们有80亿 = 2^34个数的网页指纹,d=34,可以有下面四种设计方法 (f:指纹位数,k:海明距离,d:将文章数量转化成2的幂次方,d就是幂值) 1.20个表:将64bit分为11,11,11,11,10,10六个bit块。根据排列组合,如果想从这6个块中找3个作为leading bits的话(这样才能满足|pi-d|是个小整数),一共有C(6,3)=20种找法,所以需要20个表,每个表的前三块来自不同的三个块,那么pi就有11+11+11、11+ 11+10和11+10+10三种可能了。一次嗅探平均需要检索2^(34-31)=8个指纹。 2.16个表:先将64bit均分成4份,然后针对每份,将剩余的48bit再均分成四份,也就是16,12,12,12,12,12五部分,很明显这种组合的可能是4*4,而pi = 28。一次嗅探平均需要检索2^(34-28)=64个指纹。 3.10个表:将64bit分成 13,13,13,13,12 五个bit快。根据排列组合,需要从5块中找到2个作为leading bits,共有C(5,2)=10种找法,需要10张表,而pi=25或26。一次嗅探平均需要检索2^(34-25)=512个指纹。 4.4个表:同理 64 等分为4份,每份16bit,从四份中找出1个leading bits,共有C(4,1)=4种找法,pi=16,一次嗅探平均需要检索2^(34-16)=256K个指纹。 分表存储实现 存储: 1、将一个64位的simhash签名拆分成4个16位的二进制码。(图上红色的16位) 2、分别拿这4个16位二进制码查找当前对应位置上是否有元素。(放大后的16位) 3、对应位置没有元素,直接追加到链表上;对应位置有则直接追加到链表尾端。(图上的 S1 — SN) 查找: 1、将需要比较的simhash签名拆分成4个16位的二进制码。 2、分别拿着4个16位二进制码每一个去查找simhash集合对应位置上是否有元素。 3、如果有元素,则把链表拿出来顺序查找比较,直到simhash小于一定大小的值,整个过程完成。 原理: 借鉴“hashmap算法找出可以hash的key值”。因为我们使用的simhash是局部敏感哈希,这个算法的特点是:只要相似的字符串,只有个别的位数是有差别变化的。那这样我们可以推断两个相似的文本,至少有16位的simhash是一样的。具体选择16位、8位、4位,大家根据自己的数据测试选择,虽然比较的位数越小越精准,但是空间会变大。分为4个16位段的存储空间是单独simhash存储空间的4倍。之前算出5000w数据是 382 Mb,扩大4倍1.5G左右,还可以接受 最佳分表策略 根据 4.2节分表存储设计,给定 f,k 我们可以有很多种分表的方式,增加表的个数会减少检索时间,但是会增加内存的消耗,相反的,减少表的个数,会减少内存的压力,但是会增加检索时间。 根据google大量的实验,存在一个分表策略满足时间和空间的平衡点 τ=d-pi (pi计算看4.2章节,取最小pi) simhash存储实现(Go) 国外有一大神用go实现了d=3和6的实现,在他的基础上我实现了d到8的扩展,源码请看https://github.com/kricen/shstorage 参考文章 论文 Detecting Near-Duplicates for Web Crawling http://www.cnblogs.com/maybe2030/p/5203186.html 原文链接:https://kricen.github.io/2018/03/06/perday/simhash/","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"}]},{"title":"如何使用 Disruptor(三)","slug":"disruptor3","date":"2019-11-18T12:03:12.000Z","updated":"2019-11-19T00:50:59.661Z","comments":true,"path":"2019/11/18/disruptor3/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/disruptor3/","excerpt":"写入 Ringbuffer 本文的重点是:不要让 Ring 重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。 作者:Trisha 廖涵译 这是 Disruptor 全方位解析(end-to-end view)中缺少的一章。当心,本文非常长。但是为了让你能联系上下文阅读,我还是决定把它们写进一篇博客里。","text":"写入 Ringbuffer 本文的重点是:不要让 Ring 重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。 作者:Trisha 廖涵译 这是 Disruptor 全方位解析(end-to-end view)中缺少的一章。当心,本文非常长。但是为了让你能联系上下文阅读,我还是决定把它们写进一篇博客里。 本文的 重点 是:不要让 Ring 重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。 ProducerBarriers Disruptor 代码 给 消费者 提供了一些接口和辅助类,但是没有给写入 Ring Buffer 的 生产者 提供接口。这是因为除了你需要知道生产者之外,没有别人需要访问它。尽管如此,Ring Buffer 还是与消费端一样提供了一个 ProducerBarrier 对象,让生产者通过它来写入 Ring Buffer。 写入 Ring Buffer 的过程涉及到两阶段提交 (two-phase commit)。首先,你的生产者需要申请 buffer 里的下一个节点。然后,当生产者向节点写完数据,它将会调用 ProducerBarrier 的 commit 方法。 那么让我们首先来看看第一步。 “给我 Ring Buffer 里的下一个节点”,这句话听起来很简单。的确,从生产者角度来看它很简单:简单地调用 ProducerBarrier 的 nextEntry() 方法,这样会返回给你一个 Entry 对象,这个对象就是 Ring Buffer 的下一个节点。 ProducerBarrier 如何防止 Ring Buffer 重叠 在后台,由 ProducerBarrier 负责所有的交互细节来从 Ring Buffer 中找到下一个节点,然后才允许生产者向它写入数据。 (我不确定 闪闪发亮的新手写板 能否有助于提高我画图片的清晰度,但是它用起来很有意思)。 在这幅图中,我们假设只有一个生产者写入 Ring Buffer。过一会儿我们再处理多个生产者的复杂问题。 ConsumerTrackingProducerBarrier 对象拥有所有正在访问 Ring Buffer 的 消费者 列 表。这看起来有点儿奇怪-我从没有期望 ProducerBarrier 了解任何有关消费端那边的事情。但是等等,这是有原因的。因为我们不想与队列“混为一谈”(队列需要追踪队列的头和尾,它们有时候会指向相同的位 置),Disruptor 由消费者负责通知它们处理到了哪个序列号,而不是 Ring Buffer。所以,如果我们想确定我们没有让 Ring Buffer 重叠,需要检查所有的消费者们都读到了哪里。 在上图中,有一个 消费者 顺利的读到了最大序号 12(用红色/粉色高亮)。第二个消费者 有点儿落后——可能它在做 I/O 操作之类的——它停在序号 3。因此消费者 2 在赶上消费者 1 之前要跑完整个 Ring Buffer 一圈的距离。 现在生产者想要写入 Ring Buffer 中序号 3 占据的节点,因为它是 Ring Buffer 当前游标的下一个节点。但是 ProducerBarrier 明白现在不能写入,因为有一个消费者正在占用它。所以,ProducerBarrier 停下来自旋 (spins),等待,直到那个消费者离开。 申请下一个节点 现在可以想像消费者 2 已经处理完了一批节点,并且向前移动了它的序号。可能它挪到了序号 9(因为消费端的批处理方式,现实中我会预计它到达 12,但那样的话这个例子就不够有趣了)。 上图显示了当消费者 2 挪动到序号 9 时发生的情况。在这张图中我已经忽略了ConsumerBarrier,因为它没有参与这个场景。 ProducerBarier 会看到下一个节点——序号 3 那个已经可以用了。它会抢占这个节点上的 Entry(我还没有特别介绍 Entry 对象,基本上它是一个放写入到某个序号的 Ring Buffer 数据的桶),把下一个序号(13)更新成 Entry 的序号,然后把 Entry 返回给生产者。生产者可以接着往 Entry 里写入数据。 提交新的数据 两阶段提交的第二步是——对,提交。 绿色表示最近写入的 Entry,序号是 13 ——厄,抱歉,我也是红绿色盲。但是其他颜色甚至更糟糕。 当生产者结束向 Entry 写入数据后,它会要求 ProducerBarrier 提交。 ProducerBarrier 先等待 Ring Buffer 的游标追上当前的位置(对于单生产者这毫无意义-比如,我们已经知道游标到了 12 ,而且没有其他人正在写入 Ring Buffer)。然后 ProducerBarrier 更新 Ring Buffer 的游标到刚才写入的 Entry 序号-在我们这儿是 13。接下来,ProducerBarrier 会让消费者知道 buffer 中有新东西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 对象说-“喂,醒醒!有事情发生了!”(注意-不同的 WaitStrategy 实现以不同的方式来实现提醒,取决于它是否采用阻塞模式。) 现在消费者 1 可以读 Entry 13 的数据,消费者 2 可以读 Entry 13 以及前面的所有数据,然后它们都过得很 happy。 ProducerBarrier 上的批处理 有趣的是 Disruptor 可以同时在生产者和 消费者 两端实现批处理。还记得伴随着程序运行,消费者 2 最后达到了序号 9 吗?ProducerBarrier 可以在这里做一件很狡猾的事-它知道 Ring Buffer 的大小,也知道最慢的消费者位置。因此它能够发现当前有哪些节点是可用的。 如果 ProducerBarrier 知道 Ring Buffer 的游标指向 12,而最慢的消费者在 9 的位置,它就可以让生产者写入节点 3,4,5,6,7 和 8,中间不需要再次检查消费者的位置。 多个生产者的场景 到这里你也许会以为我讲完了,但其实还有一些细节。 在上面的图中我稍微撒了个谎。我暗示了 ProducerBarrier 拿到的序号直接来自 Ring Buffer 的游标。然而,如果你看过代码的话,你会发现它是通过 ClaimStrategy 获取的。我省略这个对象是为了简化示意图,在单个生产者的情况下它不是很重要。 在多个生产者的场景下,你还需要其他东西来追踪序号。这个序号是指当前可写入的序号。注意这和“向 Ring Buffer 的游标加 1”不一样-如果你有一个以上的生产者同时在向 Ring Buffer 写入,就有可能出现某些 Entry 正在被生产者写入但还没有提交的情况。 让我们复习一下如何申请写入节点。每个生产者都向 ClaimStrategy 申请下一个可用的节点。生产者 1 拿到序号 13,这和上面单个生产者的情况一样。生产者 2 拿到序号 14,尽管 Ring Buffer的当前游标仅仅指向 12。这是因为 ClaimSequence 不但负责分发序号,而且负责跟踪哪些序号已经被分配。 现在每个生产者都拥有自己的写入节点和一个崭新的序号。 我把生产者 1 和它的写入节点涂上绿色,把生产者 2 和它的写入节点涂上可疑的粉色-看起来像紫色。 现在假设生产者 1 还生活在童话里,因为某些原因没有来得及提交数据。生产者 2 已经准备好提交了,并且向 ProducerBarrier 发出了请求。 就像我们先前在 commit 示意图中看到的一样,ProducerBarrier 只有在 Ring Buffer 游标到达准备提交的节点的前一个节点时它才会提交。在当前情况下,游标必须先到达序号 13 我们才能提交节点 14 的数据。但是我们不能这样做,因为生产者 1 正盯着一些闪闪发光的东西,还没来得及提交。因此 ClaimStrategy 就停在那儿自旋 (spins), 直到 Ring Buffer 游标到达它应该在的位置。 现在生产者 1 从迷糊中清醒过来并且申请提交节点 13 的数据(生产者 1 发出的绿色箭头代表这个请求)。ProducerBarrier 让 ClaimStrategy 先等待 Ring Buffer 的游标到达序号 12,当然现在已经到了。因此 Ring Buffer 移动游标到 13,让 ProducerBarrier 戳一下 WaitStrategy 告诉所有人都知道 Ring Buffer 有更新了。现在 ProducerBarrier 可以完成生产者 2 的请求,让 Ring Buffer 移动游标到 14,并且通知所有人都知道。 你会看到,尽管生产者在不同的时间完成数据写入,但是 Ring Buffer 的内容顺序总是会遵循 nextEntry() 的初始调用顺序。也就是说,如果一个生产者在写入 Ring Buffer 的时候暂停了,只有当它解除暂停后,其他等待中的提交才会立即执行。 呼——。我终于设法讲完了这一切的内容并且一次也没有提到内存屏障(Memory Barrier)。 更新:最近的 RingBuffer 版本去掉了 Producer Barrier。如果在你看的代码里找不到 ProducerBarrier,那就假设当我讲“Producer Barrier”时,我的意思是“Ring Buffer”。 更新2:注意 Disruptor 2.0 版使用了与本文不一样的命名。如果你对类名感到困惑,请阅读我写的Disruptor 2.0更新摘要。 原文链接:http://ifeve.com/dissecting-the-disruptor-writing-to-the-ring-buffer/ 译文链接:http://ifeve.com/disruptor-writing-ringbuffer/","categories":[],"tags":[{"name":"concurrent","slug":"concurrent","permalink":"https://www.smartonline.net.cn/tags/concurrent/"}]},{"title":"如何使用Disruptor(二)","slug":"disruptor2","date":"2019-11-18T11:57:31.000Z","updated":"2019-11-19T00:49:59.458Z","comments":true,"path":"2019/11/18/disruptor2/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/disruptor2/","excerpt":"从Ringbuffer读取数据 从上一篇文章中我们都了解了什么是Ring Buffer以及它是如何的特别。但遗憾的是,我还没有讲述如何使用Disruptor向Ring Buffer写数据和从Ring Buffer中读取数据。 从上一篇文章中我们都了解了什么是Ring Buffer以及它是如何的特别。但遗憾的是,我还没有讲述如何使用Disruptor向Ring Buffer写数据和从Ring Buffer中读取数据。","text":"从Ringbuffer读取数据 从上一篇文章中我们都了解了什么是Ring Buffer以及它是如何的特别。但遗憾的是,我还没有讲述如何使用Disruptor向Ring Buffer写数据和从Ring Buffer中读取数据。 从上一篇文章中我们都了解了什么是Ring Buffer以及它是如何的特别。但遗憾的是,我还没有讲述如何使用Disruptor向Ring Buffer写数据和从Ring Buffer中读取数据。 ConsumerBarrier与消费者 这里我要稍微反过来介绍,因为总的来说读取数据这一过程比写数据要容易理解。假设通过一些“魔法”已经把数据写入到Ring Buffer了,怎样从Ring Buffer读出这些数据呢? (好,我开始后悔使用Paint/Gimp 了。尽管这是个购买绘图板的好借口,如果我继续写下去的话… UML界的权威们大概也在诅咒我的名字了。) 消费者(Consumer)是一个想从Ring Buffer里读取数据的线程,它可以访问ConsumerBarrier对象——这个对象由RingBuffer创建并且代表消费者与RingBuffer进行交互。就像Ring Buffer显然需要一个序号才能找到下一个可用节点一样,消费者也需要知道它将要处理的序号——每个消费者都需要找到下一个它要访问的序号。在上面的例子中,消费者处理完了Ring Buffer里序号8之前(包括8)的所有数据,那么它期待访问的下一个序号是9。 消费者可以调用ConsumerBarrier对象的waitFor()方法,传递它所需要的下一个序号. 1final long availableSeq = consumerBarrier.waitFor(nextSequence); ConsumerBarrier返回RingBuffer的最大可访问序号——在上面的例子中是12。ConsumerBarrier有一个WaitStrategy方法来决定它如何等待这个序号,我现在不会去描述它的细节,代码的注释里已经概括了每一种WaitStrategy的优点和缺点 。 接下来怎么做? 接下来,消费者会一直原地停留,等待更多数据被写入Ring Buffer。并且,一旦数据写入后消费者会收到通知——节点9,10,11和12 已写入。现在序号12到了,消费者可以让ConsumerBarrier去拿这些序号节点里的数据了。 拿到了数据后,消费者(Consumer)会更新自己的标识(cursor)。 你应该已经感觉得到,这样做是怎样有助于平缓延迟的峰值了——以前需要逐个节点地询问“我可以拿下一个数据吗?现在可以了么?现在呢?”,消费者(Consumer)现在只需要简单的说“当你拿到的数字比我这个要大的时候请告诉我”,函数返回值会告诉它有多少个新的节点可以读取数据了。因为这些新的节点的确已经写入了数据(Ring Buffer本身的序号已经更新),而且消费者对这些节点的唯一操作是读而不是写,因此访问不用加锁。这太好了,不仅代码实现起来可以更加安全和简单,而且不用加锁使得速度更快。 另一个好处是——你可以用多个消费者(Consumer)去读同一个RingBuffer ,不需要加锁,也不需要用另外的队列来协调不同的线程(消费者)。这样你可以在Disruptor的协调下实现真正的并发数据处理。 BatchConsumer代码是一个消费者的例子。如果你实现了BatchHandler, 你可以用BatchConsumer来完成上面我提到的复杂工作。它很容易对付那些需要成批处理的节点(例如上文中要处理的9-12节点)而不用单独地去读取每一个节点。 更新:注意Disruptor 2.0版本使用了与本文不一样的命名。如果你对类名感到困惑,请阅读我的变更总结。 原文链接:http://ifeve.com/dissecting-the-disruptor-how-do-i-read-from-the-ring-buffer/ 译文链接:http://ifeve.com/dissecting_the_disruptor_how_doi_read_from_the_ring_buffer/","categories":[],"tags":[{"name":"concurrent","slug":"concurrent","permalink":"https://www.smartonline.net.cn/tags/concurrent/"}]},{"title":"如何使用Disruptor(一)","slug":"disruptor1","date":"2019-11-18T11:35:08.000Z","updated":"2019-11-19T00:49:31.187Z","comments":true,"path":"2019/11/18/disruptor1/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/disruptor1/","excerpt":"Ringbuffer的特别之处 首先介绍ringbuffer。我对Disruptor的最初印象就是ringbuffer。但是后来我意识到尽管ringbuffer是整个模式(Disruptor)的核心,但是Disruptor对ringbuffer的访问控制策略才是真正的关键点所在。 作者:Trisha 寒桐译 最近,我们开源了LMAX Disruptor, 它是我们的交易系统吞吐量快(LMAX是一个新型的交易平台,","text":"Ringbuffer的特别之处 首先介绍ringbuffer。我对Disruptor的最初印象就是ringbuffer。但是后来我意识到尽管ringbuffer是整个模式(Disruptor)的核心,但是Disruptor对ringbuffer的访问控制策略才是真正的关键点所在。 作者:Trisha 寒桐译 最近,我们开源了LMAX Disruptor, 它是我们的交易系统吞吐量快(LMAX是一个新型的交易平台, 号称能够单线程每秒处理数百万的订单)的关键原因。为什么我们要将其开源?我们意识到对高性能编程领域的一些传统观点,有点不对劲。我们找到了一种更好、更快地在线程间共享数据的方法,如果不公开于业界共享的话,那未免太自私了。同时开源也让我 们觉得看起来更酷。 从这个站点,你可以下载到一篇解释什么是Disruptor及它为什么如此高性能的文档。这篇文档的编写过程,我并没有参与太多,只是简单地插入了一些标点符号和重组了一些我不懂的句子,但是非常高兴的是,我仍然从中提升了自己的写作水平。 我发现要把所有的事情一下子全部解释清楚还是有点困难的,所有我准备一部分一部分地解释它们,以适合我的NADD听众。 首先介绍ringbuffer。我对Disruptor的最初印象就是ringbuffer。但是后来我意识到尽管ringbuffer是整个模式(Disruptor)的核心,但是Disruptor对ringbuffer的访问控制策略才是真正的关键点所在。 ringbuffer到底是什么? 嗯,正如名字所说的一样,它是一个环(首尾相接的环),你可以把它用做在不同上下文(线程)间传递数据的buffer。 (好吧,这是我通过画图板手画的,我试着画草图,希望我的强迫症不会让我画完美的圆和直线) 基本来说,ringbuffer拥有一个序号,这个序号指向数组中下一个可用的元素。(校对注:如下图右边的图片表示序号,这个序号指向数组的索引4的位置。) 随着你不停地填充这个buffer(可能也会有相应的读取),这个序号会一直增长,直到绕过这个环。 要找到数组中当前序号指向的元素,可以通过mod操作: sequence mod array length = array index 以上面的ringbuffer为例(java的mod语法):12 % 10 = 2。很简单吧。 事实上,上图中的ringbuffer只有10个槽完全是个意外。如果槽的个数是2的N次方更有利于基于二进制的计算机进行计算。 (校对注:2的N次方换成二进制就是1000,100,10,1这样的数字, sequence & (array length-1) = array index,比如一共有8槽,3&(8-1)=3,HashMap就是用这个方式来定位数组元素的,这种方式比取模的速度更快。) 那又怎么样? 如果你看了维基百科里面的关于环形buffer的 词条,你就会发现,我们的实现方式,与其最大的区别在于:没有尾指针。我们只维护了一个指向下一个可用位置的序号。这种实现是经过深思熟虑的—我们选择用 环形buffer的最初原因就是想要提供可靠的消息传递。我们需要将已经被服务发送过的消息保存起来,这样当另外一个服务通过nak (校对注:拒绝应答信号)告诉我们没有成功收到消息时,我们能够重新发送给他们。 听起来,环形buffer非常适合这个场景。它维护了一个指向尾部的序号,当收到nak(校对注:拒绝应答信号)请求,可以重发从那一点到当前序号之间的所有消息: 我们实现的ring buffer和大家常用的队列之间的区别是,我们不删除buffer中的数据,也就是说这些数据一直存放在buffer中,直到新的数据覆盖他们。这就是 和维基百科版本相比,我们不需要尾指针的原因。ringbuffer本身并不控制是否需要重叠(决定是否重叠是生产者-消费者行为模式的一部分–如果你等 不急我写blog来说明它们,那么可以自行检出Disruptor项目)。 它为什么如此优秀? 之所以ringbuffer采用这种数据结构,是因为它在可靠消息传递方面有很好的性能。这就够了,不过它还有一些其他的优点。 首先,因为它是数组,所以要比链表快,而且有一个容易预测的访问模式。(译者注:数组内元素的内存地址的连续性存储的)。这是对CPU缓存友好的—也就是说,在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素。(校对注:因为只要一个元素被加载到缓存行,其他相邻的几个元素也会被加载进同一个缓存行) 其次,你可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。 缺少的部分 我并没有在本文中介绍如何避免ringbuffer产生重叠,以及如何对ringbuffer进行读写操作。你可能注意到了我将ringbuffer和链表那样的数据结构进行比较,因为我并认为链表是实际问题的标准答案。 当你将Disruptor和基于 队列之类的实现进行比较时,事情将变得很有趣。队列通常注重维护队列的头尾元素,添加和删除元素等。所有的这些我都没有在ringbuffer里提到,这 是因为ringbuffer不负责这些事情,我们把这些操作都移到了数据结构(ringbuffer)的外部 原文链接:http://ifeve.com/ringbuffer/ 译文链接:http://ifeve.com/dissecting-disruptor-whats-so-special/","categories":[],"tags":[{"name":"concurrent","slug":"concurrent","permalink":"https://www.smartonline.net.cn/tags/concurrent/"}]},{"title":"TF-IDF算法介绍","slug":"TF-IDF","date":"2019-11-18T11:14:08.000Z","updated":"2019-11-19T00:55:01.350Z","comments":true,"path":"2019/11/18/TF-IDF/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/TF-IDF/","excerpt":"TF-IDF是什么 TF-IDF是一种统计方法,用以评估一个词对于一篇文章或语料库中一篇文章的重要性。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。 TF-IDF的使用场景 TF-IDF加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。除了TF-IDF以外,因特网上的搜索引擎还会使用基于链接分析的评级方法,以确定文件在搜寻结果中出现的顺序。","text":"TF-IDF是什么 TF-IDF是一种统计方法,用以评估一个词对于一篇文章或语料库中一篇文章的重要性。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。 TF-IDF的使用场景 TF-IDF加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。除了TF-IDF以外,因特网上的搜索引擎还会使用基于链接分析的评级方法,以确定文件在搜寻结果中出现的顺序。 TF-IDF原理 TF(Term Frequency) 表示词频,即一个词在在一篇文章中出现的次数,但在实际应用时会有一个漏洞,就是篇幅长的文章给定词出现的次数会更多一点。因此我们需要对次数进行归一化,通常用给定词的次数除以文章的总词数。 这其中还有一个漏洞,就是 ”的“ ”是“ ”啊“ 等类似的词在文章中出现的此时是非常多的,但是这些大多都是没有意义词,对于判断文章的关键词几乎没有什么用处,我们称这些词为”停用词“,也就是说,在度量相关性的时候不应该考虑这些词的频率。 IDF(Inverse Document Frequency)逆文本频率指数,如果包含关键词w的文档越少,则说明关键词w具有很好的类别区分能力。某一关键词的IDF,可以用总的文章数量除以包含该关键词的文章的数量,然后对结果取对数得到 注:分母加1是为了避免没有包含关键词的文章时分母是0的情况 一个词预测主题的能力越强,权重就越大,反之,权重越小,因此一个词的TF-IDF就是: 实际应用 通常在新闻的分类,或者说文章的分类的时候我们会用到ID-IDF。如果让编辑来对新闻或者文章分类,他一定要先读懂文章,然后找出主题,最后根据主题的不同对文章进行分类。而让电脑对文章进行分类,就要求我们先把文字的文章变成一组可以计算的数字,然后通过算法来算出文章的相似性。 首先我们先来看怎么用一组数字(或者说一个向量)来表示一篇文章。对于一篇文章的所有实词(除去无意义的停用词),计算出他们的TF-IDF值,把这些值按照对应的实词在词汇表的位置依次排列,就得到了一个向量。比如,词汇表中有64000个词,其编号和词: 单词编号 汉字词 1 阿 2 啊 … … 789 服装 … … 64000 做作 在某一篇文章中,文章中的词的TF-IDF值对应为: 单词编号 TF-IDF 1 0 2 0.0034 … … 789 0.034 … … 64000 0.075 如果单词表的某个词在文章中没有出现,对应的值为零,这样我们就得到了一个64000维的向量,我们称为这篇文章的特征向量。然后每篇文章就可以用一个向量来表示,这样我们就可以计算文章之间的相似程度了。 向量的夹角是衡量两个向量相近程度的度量。因此,可以通过计算两篇文章的特征向量的夹角来判断两篇文章的主题的接近程度。那么我们就需要用余弦地理了。 ∠A的余弦值为: 如果将三角形的两边b和c看成是两个以A为起点的向量,那么上述公式等于: 其中,分母便是两个向量b和c的长度,分子表示两个向量的内积。假设文章X和文章Y对应的向量是 那么他们的夹角的余弦等于 由于向量中的每一个变量都是正数,所以余弦的取值在0到1之间。当两篇文章向量的余弦等于1时,这两个向量夹角为零,两篇文章完全相同;当夹角的余弦接近于1时两篇文章越相似,从而可以归成一类;夹角的余弦越小,夹角越大,两篇文章越不相关。 现在假定我们已知一些文章的特征向量,那么对于任何一个要被分类的文章,就很容易计算出它和各类文章的余弦相似性,并将其归入它该去的那一类中。 如果事先没有已知的文章的特征向量呢,可以用自底向上不断合并的方法。 计算所有文章之间凉凉的余弦相似性,把相似性大于一个阈值的合并成一小类 把每个小类中的所有文章作为一个整体,计算小类的特征向量,在计算小雷之间两两的余弦相似性,然后合并成一个大类 这样不断做下去,类别越来越少,而每个类越来越大。当某一类太大时,这一类里的文章的相似性就很小了,这时就要停止迭代过程了,然后完成分类。","categories":[],"tags":[{"name":"algorithm","slug":"algorithm","permalink":"https://www.smartonline.net.cn/tags/algorithm/"}]},{"title":"工具网站收藏","slug":"useful-website","date":"2019-11-18T08:28:03.000Z","updated":"2019-11-19T00:55:23.613Z","comments":true,"path":"2019/11/18/useful-website/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/useful-website/","excerpt":"Part1:前端工具 【1】字体图标生成网站:IconMoon:https://icomoon.io/ 【2】reset 和 globle CSS:https://meyerweb.com/eric/tools/css/reset/ 【3】阿里巴巴矢量图标库:https://www.iconfont.cn/","text":"Part1:前端工具 【1】字体图标生成网站:IconMoon:https://icomoon.io/ 【2】reset 和 globle CSS:https://meyerweb.com/eric/tools/css/reset/ 【3】阿里巴巴矢量图标库:https://www.iconfont.cn/ 【4】EChart:https://echarts.baidu.com/ 【5】iView:https://www.iviewui.com/ Part2:视频教程 【1】慕课网:https://www.imooc.com/ 【2】Siki学院:http://www.sikiedu.com/ 【3】Bilibili:https://www.bilibili.com/ Part3:实用网站 【1】国家哲学社会科学文献中心:http://www.ncpssd.org 【2】MSDN,我告诉你: https://msdn.itellyou.cn/","categories":[],"tags":[{"name":"tools","slug":"tools","permalink":"https://www.smartonline.net.cn/tags/tools/"}]},{"title":"树莓派镜像制作","slug":"raspberry-blog1","date":"2019-11-18T05:21:43.000Z","updated":"2019-11-19T00:51:45.808Z","comments":true,"path":"2019/11/18/raspberry-blog1/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/raspberry-blog1/","excerpt":"【1】树莓派原始镜像烧录。 【2】中文字库以及中文拼音输入法下载: 启动后,开启Terminal终端,出现提示符时输入: 1sudo apt-get install ttf-wqy-zenhei","text":"【1】树莓派原始镜像烧录。 【2】中文字库以及中文拼音输入法下载: 启动后,开启Terminal终端,出现提示符时输入: 1sudo apt-get install ttf-wqy-zenhei 将安装文泉驿的开源中文字体,在这里向文泉驿表示致敬,貌似它是唯一一个开源的中文字体库。郭嘉有钱建孔子学院,但是从来不会有钱搞一套比较完整的开源中文字库出来的。 中文是可以显示啦,输入呢?Linux下早就有啦,叫SCIM(Smart Common Input Method ),所以只要输入: 1sudo apt-get install scim-pinyin 就会安装拼音输入法,安装完成后,可以直接打入scim激活,下次启动是会自动启动的。快捷键也是Ctrl+空格。或者直接点击右下角图标选择。 接着运行: 1sudo raspi-config 然后选择change_locale,在Default locale for the system environment:中选择zh_CN.UTF-8。然后重启机器,就发现整个环境变成中文的了。 【3】安装 Pi4j: Installation Easy/Preferred (NOTE: This installation method requires that your RaspberryPi is connected to the Internet.) The simplest method to install Pi4J on your RaspberryPi is to execute the following command directly on your RaspberryPi. 1curl -s get.pi4j.com | sudo bash This method will download and launch an installation script that perform the following steps: adds the Pi4J APT repository to the local APT repositories downloads and installs the Pi4J GPG public key for signature validation invokes the ‘apt-get update’ command on the Pi4J APT repository to update the local package database invokes the ‘apt-get install pi4j’ command to perform the download and installation Offline/Manual If you prefer/need to install Pi4J on a RaspberryPi device without an Internet connection, the following instructions provide the steps necessary to install Pi4J without requiring an Internet connection. First, download a copy of the latest Pi4J Debian/Raspian installer package (.deb) file to your local computer. You can download the Pi4J Debian/Raspian installer package (.deb) using your web browser at the following URL: http://get.pi4j.com/download/pi4j-1.2-SNAPSHOT.deb Next, you will need to transfer the download installer package over to your RaspberryPi. You can use any method you prefer to transfer the file (USB, SCP, FTP, etc.) (NOTE: If you have a previous version of Pi4J installed, you will need to uninstall it first.) Once the installer package is available on your RaspberryPi, use the following command on the Pi to perform the installation: 1sudo dpkg -i pi4j-1.2-SNAPSHOT.deb Upgrade Easy/Preferred If you originally installed Pi4J using the ‘easy’ method, then Pi4J upgrades will be available anytime you perform a system update using ‘sudo apt-get update’ and ‘sudo update-get upgrade’. If you wish to force an upgrade of the Pi4J package only, you can do so by executing the following command: 1sudo apt-get install pi4j or pi4j --update Offline/Manual If you originally installed Pi4J using the ‘offline’ method, then you will need to manually uninstall the Pi4J package and download, transfer, and install the new version package using the ‘offline’ uninstall and installation methods described here on this page. Uninstall Easy/Preferred If you originally installed Pi4J using the ‘easy’ method, then you can uninstall Pi4J simply by executing the following command on your RaspberryPi. 1sudo apt-get remove pi4j or pi4j --uninstall Complete/Full Removal If you originally installed Pi4J using the ‘easy’ method and you want to remove all traces of Pi4J, including the Pi4J repository in the APT repositories list and the Pi4J GPG signature, then simply execute the following command on your RaspberryPi. 1curl -s get.pi4j.com/uninstall | sudo bash Offline/Manual If you originally installed Pi4J using the ‘offline’ method, then you will need to manually uninstall the Pi4J package by executing the following command on your Raspberry Pi: 1sudo dpkg -r pi4j Installed Location / Example Files This will install the Pi4J libraries and example source files to: 12/opt/pi4j/lib /opt/pi4j/examples When attempting to compile a Java program using the Pi4J libraries, make sure to include the Pi4J lib folder in the classpath: 1javac -classpath .:classes:/opt/pi4j/lib/'*' ... When attempting to start a Java program using the Pi4J libraries, make sure to include the Pi4J lib folder in the classpath: 1sudo java -classpath .:classes:/opt/pi4j/lib/'*' ... If you would like to explore the examples, you can compile all the examples with the following commands: 1/opt/pi4j/examples/build Pi4j官网:https://pi4j.com 【4】安装 JavaFX 插件包: As you can read here, the most recent JDK versions for ARM don’t include JavaFX. If you want to use JavaFX in your Raspberry Pi, the solution is adding the missing JavaFX SDK. If you install the recent Oracle’s JDK for ARM from here (select jdk-8u111-linux-arm32-vfp-hflt.tar.gz), then you will need to download the JavaFX SDK from Gluon’s site (select JavaFX Embedded SDK for armv6 hard float). Once you have the file, unzip it, and copy the folders to your JDK. Assuming you have downloaded armv6hf-sdk-8.60.8.zip to your Pi/Downloads folder, and you have unzip it to a folder armv6hf-sdk, like in the following picture: using the following commands will allow you moving from command line the files to the JDK required folders. You can use a graphic tool for this as well. 12345678cd Downloads sudo chown -R root:root armv6hf-sdk cd armv6hf-sdk sudo mv lib/javafx-mx.jar /opt/jdk1.8.0_111/lib/ cd rt/lib/ sudo mv j* /opt/jdk1.8.0_111/jre/lib/ sudo mv arm/* /opt/jdk1.8.0_111/jre/lib/arm/ sudo mv ext/* /opt/jdk1.8.0_111/jre/lib/ext/ After that you should be able to run Java/JavaFX programs. 参考链接:https://stackoverflow.com/questions/40481455/running-javafx-gui-on-the-raspberry-pi/40483500#40483500 【5】解决树莓派图形渲染问题: JavaFX glGetError 0x505 You can try increase the available raspberry pi video memory using the sudo raspi-config tool. try change to the 50/50 memory spit. 参考链接:https://www.raspberrypi.org/forums/viewtopic.php?f=81&t=60024#p448200 【6】树莓派播放视频: WebView and Media were never part of the JavaFX ARM distribution, but Gluon recently added it to the embedded SDK that can be downloaded from here and installed with a recent JDK for ARM, available here. Media requires a few extra steps as it depends in the native drivers that usually are not fully installed on a regular Jessie distribution. First install these drivers: 12sudo apt-get install gstreamer0.10-plugins-good sudo apt-get install gstreamer0.10-plugins-bad Now edit /etc/apt/sources.list and add at the end: 1deb http://ftp.uk.debian.org/debian/ wheezy main deb-src http://ftp.uk.debian.org/debian/ wheezy main Save the file (Ctrl+O, Ctrl+X). Finally update and install the drivers: 123sudo apt-get update sudo apt-get install gstreamer0.10-ffmpeg sudo apt-get install gstreamer0.10-alsa Now you can try to run again your JavaFX application. If you find again the same exception (MediaException: UNKNOWN), check if it shows this message: Error in GstPipelineFactory, notice the driver that is missing, and try to install it. 参考链接:https://stackoverflow.com/questions/42076680/play-a-video-using-javafx-on-raspberry-pi 【7】常用的设置树莓派自启动的方法: 这个方式不用修改 rc.local 文件。机制上类似于 Windows 的“开始”菜单中的“启动”菜单。方法如下: 在 /home/pi/.config 下创建一个文件夹,名称为 autostart,并在该文件夹下创建一个xxx.desktop文件(文件名以.desktop结尾,前面可以自定义),文件内容如下: 12345678910[Desktop Entry] Name=example Comment=My Python Program Exec=python /home/pi/example.py Icon=/home/pi/example.png Terminal=false MultipleArgs=false Type=Application Categories=Application;Development; StartupNotify=true 以上 Name、Comment、Icon 可以自定,分别表示这个启动项目的名称、备注以及显示的图标。Exec 表示调用的指令,和在终端输入运行脚本的指令格式一致。 参考链接:https://www.jianshu.com/p/1a160067d8fd 【8】用树莓派播放视频: 树莓派上可以播放 H264 和 mp4 等视频格式,1080p也没问题,因为这种格式的文件有硬件加速。 首先安装 安装 omxplayer ,这是一个命令行的播放器: 1sudo apt-get install omxplayer 然后就可以播放了,当然需要通过 HDMI 连接到显示器看: 1omxplayer -o hdmi /path/to/filename.mp4 -o hdmi 表示音频直接通过 HDMI 播放,播放时按左右箭头快进、按 q 退出。 更多命令行选项和播放时的控制快捷键请参考 omxplayer 的文档。 【9】树莓派安装 JDK: 首先是安装JDK 1sudo apt-get install oracle-java8-jdk 也可以在这个地方下载 修改环境变量,我用的版本是JDK8,arm版HFLT,代表arm架构硬件浮点运算,放在/usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt这个文件夹了 1sudo nano /etc/profile 1234567[cc lang=\"php\"]#set java environmentJAVA_HOME=/usr/lib/jvm/jdk-8-oracle-arm-vfp-hfltCLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/toolPATH=$JAVA_HOME/bin:$PATHexport JAVA_HOME CLASSPATH PATH[/cc] 接下来是重启树莓派,看看版本号: 1java -version 【10】树莓派去黑边: 在使用树莓派连接HDMI电脑显示器的时候,可能会出现屏幕显示不全,有黑边的情况。这时候需要调节分辨率以适应屏幕。 进入树莓派系统,输入以下指令设置config.txt文件: 1sudo vi /boot/config.txt 调节任何参数时,将#号去除即可生效 123# uncomment to force a specific HDMI mode (this will force VGA) hdmi_group=2 //将显示模式切换成DMT(显示器模式) hdmi_mode=82 //1920x1080 60Hz 1080p 如果显示器不是1080P。则可以参考注1参数修改 这时候就将显示设置成1080P的分辨率,但是是不带声音的,如果你的显示器支持HDMI声音输出或者自带音响,则将如下代码参数去除#号解锁强制获取声音。 12# uncomment to force a HDMI mode rather than DVI. This can make audio work in # DMT (computer monitor) modes hdmi_drive=2 //HDMI模式 更多设置参考官方配置文档:https://www.raspberrypi.org/documentation/configuration/config-txt.md 注1: DMT模式分辨率参数 hdmi_mode resolution frequency notes 1 640x350 85Hz 2 640x400 85Hz 3 720x400 85Hz 4 640x480 60Hz 5 640x480 72Hz 6 640x480 75Hz 7 640x480 85Hz 8 800x600 56Hz 9 800x600 60Hz 10 800x600 72Hz 11 800x600 75Hz 12 800x600 85Hz 13 800x600 120Hz 14 848x480 60Hz 15 1024x768 43Hz incompatible with the Raspberry Pi 16 1024x768 60Hz 17 1024x768 70Hz 18 1024x768 75Hz 19 1024x768 85Hz 20 1024x768 120Hz 21 1152x864 75Hz 22 1280x768 reduced blanking 23 1280x768 60Hz 24 1280x768 75Hz 25 1280x768 85Hz 26 1280x768 120Hz reduced blanking 27 1280x800 reduced blanking 28 1280x800 60Hz 29 1280x800 75Hz 30 1280x800 85Hz 31 1280x800 120Hz reduced blanking 32 1280x960 60Hz 33 1280x960 85Hz 34 1280x960 120Hz reduced blanking 35 1280x1024 60Hz 36 1280x1024 75Hz 37 1280x1024 85Hz 38 1280x1024 120Hz reduced blanking 39 1360x768 60Hz 40 1360x768 120Hz reduced blanking 41 1400x1050 reduced blanking 42 1400x1050 60Hz 43 1400x1050 75Hz 44 1400x1050 85Hz 45 1400x1050 120Hz reduced blanking 46 1440x900 reduced blanking 47 1440x900 60Hz 48 1440x900 75Hz 49 1440x900 85Hz 50 1440x900 120Hz reduced blanking 51 1600x1200 60Hz 52 1600x1200 65Hz 53 1600x1200 70Hz 54 1600x1200 75Hz 55 1600x1200 85Hz 56 1600x1200 120Hz reduced blanking 57 1680x1050 reduced blanking 58 1680x1050 60Hz 59 1680x1050 75Hz 60 1680x1050 85Hz 61 1680x1050 120Hz reduced blanking 62 1792x1344 60Hz 63 1792x1344 75Hz 64 1792x1344 120Hz reduced blanking 65 1856x1392 60Hz 66 1856x1392 75Hz 67 1856x1392 120Hz reduced blanking 68 1920x1200 reduced blanking 69 1920x1200 60Hz 70 1920x1200 75Hz 71 1920x1200 85Hz 72 1920x1200 120Hz reduced blanking 73 1920x1440 60Hz 74 1920x1440 75Hz 75 1920x1440 120Hz reduced blanking 76 2560x1600 reduced blanking 77 2560x1600 60Hz 78 2560x1600 75Hz 79 2560x1600 85Hz 80 2560x1600 120Hz reduced blanking 81 1366x768 60Hz 82 1920x1080 60Hz 1080p 83 1600x900 reduced blanking 84 2048x1152 reduced blanking 85 1280x720 60Hz 720p 86 1366x768 reduced blanking 【11】OmxPlayer 调节声音大小: to provide more precise information for playing through scripts, there are 3 ways to change sound volume in current version of omxplayer, and values are not so intuitive: on starting command line, param –vol YYY, double millibels, default 0, range [-6000:0] by stdin interface, sending +/- to omxplayer will increase/decrease volume for 300 dmbels with DBUS interface, cmd ‘set volume’, value double:XXX, default 1, range [0:1] xxx to yyy relation is: XXX = 10 ^ (YYY / 2000) … according to omxplayer.cpp source code, reverse formula would be: YYY = 2000 * (log XXX). so if we need: volume 1%, XXX=0.01 and YYY=-4000 (10^(-4000/2000)=10^-2=0.01 volume 10%, XXX=0.1 and YYY=-2000 (10^(-2000/2000)=10^-1=0.1 volume 50%, XXX=0.5 and YYY=-602 (10^(-602/2000))~=0.5 volume 100%, XXX=1 and YYY=0 (10^(0/2000)=10^0=1) volume 150%, XXX=1.5 and YYY=352 … (for boost test, normal values are <=100%) working bash script for dbus volume command: 123456export DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/omxplayerdbus.${USER:-root}) \\dbus-send --print-reply --session --reply-timeout=500 \\ --dest=org.mpris.MediaPlayer2.omxplayer \\ /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Set\\ string:\"org.mpris.MediaPlayer2.Player\" \\ string:\"Volume\" double:0.5 # <-- XXX=0.5 (50% sound volume) equals to volume parameter at startup: 1omxplayer --vol -602 mediaFileName.mp4 … both sets sound volume to 50%. 【12】树莓派设置不休眠的方法 树莓派长时间没人操作时,会自动进入休眠状态,这是因为长时间无操作触发linux的节电休眠机制。所以当树莓派运行后台程序,比如用树莓派看视频时,时间一长就会自动黑屏,树莓派自动进入休眠状态。 怎么设置树莓派不休眠,其实通过建立和设置内置文件就行了,很简单。 以下是防止树莓派休眠的设置步骤: 1、用管理员root账户登录树莓派,在文件夹/etc/profile.d/里面新建内置文件screen.sh。 2、编辑文件screen.sh,写入以下两行内容: 12xset dpms 0 0 0xset s off 保存文件。 3、重启树莓派,就能实现永久禁用树莓派休眠。 【13】树莓派中的GPU渲染内存设置 为了平衡树莓派CPU运行内存和GPU渲染内存,将GPU的MemorySplit设置成320M这个经验值(总内存1GB,GPU分得320M,则CPU持有704M)是一个不错的选择,设置方法如下: raspi-config>>Advanced Options>>Memory Split>>更改内存为320","categories":[],"tags":[{"name":"raspberry","slug":"raspberry","permalink":"https://www.smartonline.net.cn/tags/raspberry/"},{"name":"linux","slug":"linux","permalink":"https://www.smartonline.net.cn/tags/linux/"}]},{"title":"Hello World","slug":"hello-world","date":"2019-11-18T02:02:50.485Z","updated":"2019-11-18T07:27:27.737Z","comments":true,"path":"2019/11/18/hello-world/","link":"","permalink":"https://www.smartonline.net.cn/2019/11/18/hello-world/","excerpt":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","categories":[],"tags":[]}]}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。