首页 文章 精选 留言 我的

精选列表

搜索[基础搭建],共10000篇文章
优秀的个人博客,低调大师

SQL Serever学习9——基础查询语句

SQL语言概述 SQL是结构化查询语言(Structure Query Language),1974年提出,1979年被IBM实现,SQL语言已经成为关系型数据库的标准语言。 包括: DDL数据定义语言 语句有CREATE ,ALTER ,DROP,操作表,视图,触发器,存储过程 DML数据操作语言 语句有SELECT ,INSERT , UPDATE , DELETE,用于检索和操作数据 DCL数据控制语言 语句有GRANT , DENY , REVOKE,只有sysadmin,数据库创建者,拥有者,安全管理员有权利执行,用来设置或更改数据库用户或角色权限 流程控制 常用语句有BEGIN...END , IF...ELSE , WHILE , BREAK , GOTO , WAITFOR , RETURN等语 逻辑运算符 AND OR NOT ALL,所有表达式为true才为true ANY,表达式中一个为true则为true BETWEEN ,在某个范围内则为true EXISTS IN ,操作数为表达式列表中的一个则为true。 语句基本格式 SELECT * FROM 表名 WHERE 条件 GROUP BY 字段 HAVING 表达式 ORDER BY 字段 ASC| DESC 说明:GROUP BY子句后可以使用HAVING 短语,用来分组后筛选,HAVING 必须跟随ORDER BY子句使用。 默认情况,查询结构表的标题可以是表的字段名,也可以无标题,还可以使用AS 对字段标题进行修改 USE 销售管理 GO SELECT 商品名称,型号,销售价-进价 AS 差价,库存 FROM 商品表 GO 使用查询生成新表,或者临时表 USE 销售管理 GO SELECT 商品名称,型号,销售价-进价 AS 差价,库存 INTO 商品表附表 FROM 商品表 GO 结果 临时表的使用 临时表在本次服务器连接过程中有效,一旦服务器断开连接,临时表失效,并被删除。 USE 销售管理 GO SELECT 商品名称,型号,销售价-进价 AS 差价,库存 INTO #临时商品表 FROM 商品表 GO 快速生成数据表结构(空白哦) 因为WHERE 1=2不成立,所以就不会检索出符合条件的数据,生成的是一个没有数据的空白表 USE 销售管理 GO SELECT 商品名称,型号,销售价-进价 AS 差价,库存 INTO #商品表副本 FROM 商品表 WHERE 1=2 GO SELECT * FROM #商品表副本 SQL汇总查询 聚合函数 常用的聚合函数有6个: COUNT(*),统计所有记录个数 COUNT[ DISTINCT] 字段,统计字段中值的个数 SUM 字段,对指定字段(数值型)求和 AVG 字段,对指定字段(数值型)求平均值 MAX 字段,求一个字段最大值 MIN 字段,求一个字段最小值 分组查询语句 有时候统计每种商品销售总金额,需要对销售表中销售金额进行汇总,然后再进行操作,这就是分组查询。 GROUP BY子句实现, GROUP BY 字段 HAVING 分组后的筛选条件表达式 注意:BY 字段 按指定字段进行分组,字段值相同的记录放在一组,每一组汇总只有一条数据。 HAVING 的筛选是对经过分组后结果进行筛选,而不是对原始表筛选。 SELECT 子句后的字段列表,必须是聚合函数 ,或者是GROUP BY 子句中的字段。 demo.sql USE 销售管理 GO SELECT 品牌,COUNT(品牌) AS 数量 FROM 商品表 GROUP BY 品牌 --HAVING 品牌='A牌' 结果 注意这里的HAVING子句和WHERE的区别: HAVING可以有聚集函数,而WHERE子句不可以 HAVING作用于分组后的结果集,WHERE 子句作用于基本表 下面来一个小demo,用来查找某个属性的值出现最多的那个记录 原表 现在查找哪个品牌数量最多,并找出这个品牌的记录 demo USE 销售管理 GO --申明变量用来存储数量最多的品牌 DECLARE @ELE VARCHAR(20) SELECT @ELE=A.品牌 FROM (SELECT TOP 1 品牌,COUNT(品牌) AS 数量 FROM 商品表 GROUP BY 品牌 ORDER BY 数量 DESC) A --print @ele SELECT * FROM 商品表 WHERE 品牌=@ELE 结果集 汇总合计函数ROLLUP(在sqlserver2008叫做COMPUTE) 使用这个函数,需要最分组函数的最后添加with rollup,然后会在最后多一行。 分组 USE 销售管理 GO SELECT 品牌,COUNT(品牌) AS 数量 FROM 商品表 GROUP BY 品牌 使用ROLLUP汇总 USE 销售管理 GO SELECT 品牌,COUNT(品牌) AS 数量 FROM 商品表 GROUP BY 品牌 WITH ROLLUP 连接查询 就是多个表单的关联查询 INNER JOIN,内连接 LEFT JOIN,左连接,结果包含满足条件的行和左侧表的全部行,使用NULL值代替无法匹配的值 RIGHT JOIN,右连接 FULL JOIN,全连接,结果包含满足条件的行和2侧表的全部行 CROSS JOIN,交叉连接,结果包含2个表的所有行的组合,2个表的笛卡尔操作,用的不多 内连接范例 使用sqlserver语法 USE 销售管理 GO SELECT A.商品名称,A.品牌,A.销售价,B.类型名称 FROM 商品表 A,商品类型表 B WHERE A.类型=B.类型编号 使用ANSI语法 USE 销售管理 GO SELECT A.商品名称,A.品牌,A.销售价,B.类型名称 FROM 商品表 A INNER JOIN 商品类型表 B ON A.类型=B.类型编号 注意:<表名> A的意思是将某个表在这一次查询红命名为A,这样在整个查询中都可以使用A代替该表,简化操作。 子查询 子查询出现的形式: 多数情况出现在WHERE 子语句中 出现在外部查询的SELECT 子语句中 出现在外部查询的FROM 子句中,即把查询结果集看做另外一张表 使用比较运算符的子查询 /*查询一级买家信息*/ SELECT * FROM 买家表 WHERE 级别= (SELECT 级别编号 FROM 买家级别表 WHERE 级别名称='一级') 使用ALL ANY运算符的子查询 当子查询返回的是单列多值,使用ALL ANY和比较运算符构成特殊查询 >ANY,表示大于子查询结果的某个值,就是大于查询结果最小值 =ANY,等于查询结果的某个值,相当于IN <ANY,小于查询结果的最大值 >ALL,大于查询结果最大值 !=ALL,相当于NOT IN 比如查询那些台式电脑比笔记本电脑的进价还要贵 /*查询那些台式电脑比笔记本电脑的进价还要贵*/ SELECT * FROM 商品表 WHERE 商品名称='台式机' AND 进价>ANY (SELECT 进价 FROM 商品表 WHERE 商品名称='笔记本') 使用IN运算符的子查询 比如查询进价大于5000的商品销售情况 /*查询进价大于5000的商品销售情况*/ SELECT 商品编号,买家编号 FROM 销售表 WHERE 商品编号 IN (SELECT 商品编号 FROM 商品表 WHERE 进价>5000) 使用EXISTS运算符的子查询 用来判断子查询是否有结果返回,NOT EXISTS的作用刚好相反 比如查询至少有一次实际销售价比进价还低的商品信息 /*查询至少有一次实际销售价比进价还低的商品信息*/ SELECT * FROM 商品表 A WHERE EXISTS (SELECT * FROM 销售表 B WHERE A.商品编号=B.商品编号 AND B.实际销售价格<A.进价) 由于不需要子查询返回具体值,所以这种子查询的通常返回的列为*的格式 有个查询很难理解,记录如下 查询销售表每种商品(由商品编号区分)销售价格最贵的销售情况 分析:首先将商品种类分组 SELECT 商品编号,MAX(实际销售价格) FROM 销售表 GROUP BY 商品编号 这里还不能输出要求的信息,所以还要使用自连接(自己与自己的一个副本连接) 原表 经过筛选 /*查询销售表每种商品(由商品编号区分)销售价格最贵的销售情况*/ SELECT * FROM 商品表 A WHERE 销售价= (SELECT MAX(销售价) FROM 商品表 B WHERE A.品牌=B.品牌) ORDER BY 商品编号 数据库中数据的管理 插入数据INSERT 使用INSERT语句插入数据进数据表,有2种方式:插入单行数据(使用VALUES),插入多行数据(使用SELECT) 插入单行数据 /*插入单行数据*/ INSERT INTO 买家表(买家编号,买家名称,买家电话,级别) VALUES('M05','薛松','5362313','J02'); 当插入数据的数量和顺序和表中字段一一对应,可以省略字段名列表 /*插入单行数据*/ INSERT INTO 买家表 VALUES('M06','宋松','5362220','J02'); 插入多行数据 新建一张表,名为“高价销售表类”,结构与销售表相同,将销售表的实际销售价格>3000的记录插入该表。 /*插入多行数据*/ --建立一张空表 SELECT * INTO 高价销售表 FROM 销售表 WHERE 1=2 GO --插入多行数据 INSERT INTO 高价销售表(商品编号,买家编号,实际销售价格,销售日期,销售数量) SELECT 商品编号,买家编号,实际销售价格,销售日期,销售数量 FROM 销售表 WHERE 实际销售价格>3000 GO 为了建立一张空白表,查询条件WHERE 1=2永远不成立,这个是一个常用的方法。 将所有一级买家的信息存入新表“高级买家” /*插入多行数据*/ --创建新表 SELECT * INTO 高级买家 FROM 买家表 WHERE 1=2 GO --添加数据 INSERT INTO 高级买家 SELECT 买家表.* FROM 买家表,买家级别表 WHERE 买家表.级别=买家级别表.级别编号 AND 级别名称='一级' GO 由于查询过程使用了2个表了,所以SELECT 语句要声明,只要买家表的列 修改数据UPDATE 普通修改 因为与A品牌的合作有了新政策,所有A品牌商品进货价下调5% /*普通修改数据*/ UPDATE 商品表 SET 销售价=销售价*0.95 WHERE 品牌='A牌' 带子查询的修改 为了增加耗材商品的销售份额,公司决定将所有耗材商品销售价格下调5% /*子查询修改数据*/ UPDATE 商品表 SET 销售价=销售价*0.95 WHERE 商品表.类型= (SELECT 类型编号 FROM 商品类型表 WHERE 类型名称='耗材') 删除数据DELETE 删除普通数据 删除销售表4所有B牌的商品购买信息 /*删除数据*/ SELECT * INTO 销售表4 FROM 销售表 GO DELETE FROM 销售表4 WHERE 品牌='B牌' 删除子查询 删除销售表4中所有买家名称为“个人”的买家购买信息 /*删除数据*/ SELECT * INTO 销售表4 FROM 销售表 GO DELETE FROM 销售表4 WHERE 买家编号= (SELECT 买家编号 FROM 买家表 WHERE 买家名称='个人') 清空数据表 /*删除数据*/ SELECT * INTO 销售表4 FROM 销售表 GO TRUNCATE TABLE 销售表4 注意:TRUNCATE TABLE 和不带条件的DELETE最终效果都是清空表中所有数据,但是在执行上TRUNCATE TABLE 更高,速度更快,因为他不记录事务日志,会释放数据,索引占据的空间,删除的数据不可恢复。

优秀的个人博客,低调大师

(一)Java工程化--Maven基础

Maven 读作['mevən] 翻译成中文是"内行,专家" Maven是什么 包依赖的前世今生: 原始的jar包引用--> ant --> maven. 是一种项目管理工具 Maven优势: convertion over configuration 约定优于配置: 这个原则不仅适用于maven, 更是目前大多数框架遵循的原则,如mvc 简单 易于测试 构建简单 CI(持续集成) 插件丰富 下载和安装 下载: http://maven.apache.org/download.cgi 安装: 解压安装即可 环境变量配置: windows: 配置path MAVEN_HOME linux: 在 .bash_profile 文件中 运行参数: 定义环境变量MAVEN_OPS 配置settings.xml: settings.xml文件是个空模板,我们可以在这个文件进行一些自定义配置. 常用的配置如:repo存储目录 创建maven项目 项目结构: 遵循约定优于配置原则, 项目包的组织结构如下 pom.xml groupId 公司组织id artifactId 功能命名 version 版本号 packageing 打包方式,默认jar,可修改为maven-project,war dependancyManagement 最好之出现在父pom中,用于统一版本号,只做声明依赖,子模块pom中还需要引用,但不需要制定version. dependancy type 默认jar scope 指定哪个阶段适用,各阶段如下: compile 编译,打包,默认 如spring-core test 测试 如spring-test provided 编译 如servlet runtime 运行时 如JDBC驱动实现包 system 本地一些jar 依赖传递 依赖仲裁:(1)最短路径原则,根据依赖树就近取最接近的版本;(2)加载顺序原则;(3)exclusions 排除包 命令: mvn dependancy:tree Maven生命周期(三个过程) 区分术语:lifecycle/phase/goal 生命周期: clean 包含的phase: pre-clean --> clean --> post-clean default compile package install deploy ... site pre-site --> site -->post-site --> site-deploy A Build Lifecycle is Made Up of Phases 一个构建生命周期是有多个phase组成 A Build Phase is Made Up of Plugin Goals 一个构建phase是由多个插件目标goal构成

优秀的个人博客,低调大师

Python编程入门基础语法详解经典

一、基本概念 1.内置的变量类型: Python是有变量类型的,而且会强制检查变量类型。内置的变量类型有如下几种: #浮点 float_number=2.3 #复数 complex_number=1+5j #整型 integer_number=1 #list 序列 sample_list=[2,52,36,'abc'] #嵌套 sample_nest=[(2,4,6),{5:7,9:11,'key':[2,5]},6] #dictionary 字典 sample_dic={"key":value, 5:10} #tuple 只读的序列 sample_tuple=(3,9,"ab") 从上面的示例可以看出,python的变量无需声明,可直接给变量赋值。 2.字符串 python中声明一个字符串通常有三种方法,''、" "和''' ''',这三种方法在声明普通字符串时的效果是完全一样的,区别在于字符串本身中存在引号的情况,举例如下: word='good' sentence="hello world" paragraph='''good noon:"nice to meet you."''' python中单行注释采用#开头。 #!/usr/bin/python # First comment print "Hello, world!"; # second comment 上述内容输出:hello,Python!。注释可以在语句或表达式行末。 多行注释可采用三引号,比如: '''This is a comment. This is a comment, too. This is a comment, too. I said that already.''' 转义符 ''; 自然字符串, 通过在字符串前加r或R。 如 r"this is a line with " 则 会显示,并不是换行。 python允许处理unicode字符串,加前缀u或U, 如 u"this is an unicode string"。 3.Python标识符 在python里,标识符有字母、数字、下划线组成。 在python中,所有标识符可以包括英文、数字以及下划线(_),但不能以数字开头。 python中的标识符是区分大小写的。 以下划线开头的标识符是有特殊意义的。以单下划线开头(_foo)的代表不能直接访问的类属性,需通过类提供的接口进行访问,不能用"from xxx import *"而导入; 以双下划线开头的(__foo)代表类的私有成员;以双下划线开头和结尾的(__foo__)代表python里特殊方法专用的标识,如__init__()代表类的构造函数。 4.Python保留字符 下面的列表显示了在Python中的保留字。这些保留字不能用作常数或变数,或任何其他标识符名称。 所有Python的关键字只包含小写字母。 如果你在学习Python的过程中遇见了很多疑问和难题,可以加-q-u-n227 -435-450里面有软件视频资料免费 5.行和缩进 学习Python与其他语言最大的区别就是,Python的代码块不使用大括号({})来控制类,函数以及其他逻辑判断。python最具特色的就是用缩进来写模块。 缩进的空白数量是可变的,但是所有代码块语句必须包含相同的缩进空白数量,这个必须严格执行。如下所示: ifTrue: print"True" else: print"False" 上述,if与else的缩进空白数量相同。以下代码将会报错: ifTrue: print"Answer" print "True" else: print "Answer" print"False" python中分号;标识一个逻辑行的结束,但是实际中一般每个物理行只写一个逻辑行,可以避免使用分号。这样书写便于阅读。注意:不要混合使用空格和制表符来缩进,因为在跨越不同的平台时无法正常工作。 多个物理行中可以写一个逻辑行,使用行连接符,如下: s="peteris writing this article" 二、运算符与表达式 1. 运算符与其用法 2. 运算符优先级(从低到高) 三.控制流 1. 条件语句 1.1 if语句 例子, 执行if语句内的程序 a=input("a:") b=input("b:") if(a > b): print a, " > ", b if else语句: a=input("a:") b=input("b:") if(a > b): print a, " > ", b else: print a, " < ", b 1.2 if…elif…else语句 例子:根据输入的分数,输出分数等级: score=raw_input("score:") score=int(score) if(score >=90)and(score <=100): print"A" elif(score >=80)and(score < 90): print"B" elif(score >=60)and(score < 80): print "C" else: print "D" raw_input() 读取输入值。 1.3 if语句的嵌套 编写条件语句时,应该尽量避免使用嵌套语句。嵌套语句不便于阅读,而且可能会忽略一些可能性。 x=-1 y=99 if(x >=0): if(x > 0): #嵌套的if语句 y=1 else: y=0 else: y=-1 print "y =", y 1.4 实现switch语句的功能 python中没有switch特殊字,Python可以通过字典实现switch语句的功能。 实现方法分两步。首先,定义一个字典。字典是由键值对组成的集合。其次,调用字典的get()获取相应的表达式。 from__future__importdivision x=1 y=2 operator="/" result={ "+" : x+y, "-" : x-y, "*" : x*y, "/" : x/y } print result.get(operator) 输出为0.5; 另一种使用switch分支语句的方案是创建一个switch类,处理程序的流程。 a) 创建一个switch类,该类继承自Python的祖先类object。调用构造函数init( )初始化需要匹配的字符串,并需要定义两个成员变量value和fall。Value用于存放需要匹配的字符串,fall用于记录是否匹配成功,初始值为false,标识匹配不成功。如果匹配成功,程序往后执行。 b) 定义一个match( )方法,该方法用于用于匹配case子句。这里需要考虑三种情况:首先是匹配成功的情况,其次是匹配失败的默认case子句,最后是case子句中没有使用break中断的情况。 c) 重写__iter__( )方法,定义该方法后才能使switch类用于循环语句中。__iter__( )调用match( )方法进行匹配。通过yield保留字,使函数可以在循环中迭代。此外,调用StopIteration异常中断循环。 d) 编写调用代码,在for…in…循环中使用switch类。 #!/usr/bin/python # -*- coding: UTF-8 -*- classswitch(object): def__init__(self, value): # 初始化需要匹配的值value self.value=value self.fall=False # 如果匹配到的case语句中没有break,则fall为true。 def__iter__(self): yieldself.match # 调用match方法 返回一个生成器 raiseStopIteration # StopIteration 异常来判断for循环是否结束 defmatch(self,*args): # 模拟case子句的方法 ifself.fallornotargs: # 如果fall为true,则继续执行下面的case子句 # 或case子句没有匹配项,则流转到默认分支。 returnTrue elifself.valueinargs: # 匹配成功 self.fall=True returnTrue else: # 匹配失败 returnFalse operator="+" x=1 y=2 forcaseinswitch(operator): # switch只能用于for in循环中 ifcase('+'): print x+y break ifcase('-'): printx-y break ifcase('*'): printx*y break ifcase('/'): printx/y break ifcase(): # 默认分支 print"" 2.while...语句 只要在一个条件为真的情况下,while语句允许你重复执行一块语句。while语句是所谓 循环 语句的一个例子。while语句有一个可选的else从句。 whileTrue: pass else: pass #else语句可选,当while为False时,else语句被执行。 pass是空语句。 3.for 循环 foriinrange(0, 5): print i else: pass # 打印0到4 注:当for循环结束后执行else语句;range(a, b)返回一个序列,从a开始到b为止,但不包括b,range默认步长为1,可以指定步长,range(0,10,2); 四、函数 函数通过def定义。def关键字后跟函数的标识符名称,然后跟一对圆括号,括号之内可以包含一些变量名,该行以冒号结尾;接下来是一块语句,即函数体。 defsumOf(a, b): returna+b 4.1 局部变量 在函数内定义的变量与函数外具有相同名称的其他变量没有任何关系,即变量名称对于函数来说是局部的。这称为变量的作用域。global语句, 为定义在函数外的变量赋值时使用global语句。 deffunc(): globalx print "x is ", x x=1 x=3 func() printx 以上代码,输出的结果为: 3 1 4.2 默认参数 通过使用默认参数可以使函数的一些参数是‘可选的’。 defsay(msg, times=1): print msg*times say("peter") say("peter", 3) 注意:只有在形参表末尾的那些参数可以有默认参数值,即不能在声明函数形参的时候,先声明有默认值的形参而后声明没有默认值的形参,只是因为赋给形参的值是根据位置而赋值的。 4.3 关键参数 如果某个函数有很多参数,而现在只想指定其中的部分,那么可以通过命名为这些参数赋值(称为‘关键参数’)。 优点:不必担心参数的顺序,使函数变的更加简单;假设其他参数都有默认值,可以只给我们想要的那些参数赋值。 deffunc(a, b=2, c=3): print"a is %s, b is %s, c is %s"%(a, b, c) func(1) #输出a is 1, b is 2, c is 3 func(1, 5) #输出a is 1, b is 5, c is 3 func(1, c=10) #输出a is 1, b is 2, c is 10 func(c=20, a=30) #输出a is 30, b is 2, c is 20 4.3 return语句 return语句用来从一个函数返回,即跳出函数。可从函数返回一个值。 没有返回值的return语句等价于return None。None表示没有任何东西的特殊类型。

优秀的个人博客,低调大师

Java基础-Java的语法规范

1.Java中的语法大小写区分,举例:(int H;h=10)在声明变量时用大写,赋值却用的小写的他是不认的,还有关键字,大小写区分要不然不认的。 2.括号成对出现:举例写的时候将格式书写完整{}[](); 3.java中的语法符号都是半角符号:因为全角符号占2个字节 半角符号只占1个字节( ; ;)跟语法相关的都是半角符号。 4.Java跟C语言的语法很像:每句代码要有分号结束,因为Java模仿C语言; 5.命名规范;不能含有的字符很多 , 字母或下划线开头可以用数字结尾(变量名,类名,方法名,属性名)。 6.书写代码要有良好的代码书写格式手法(结构写完整,然后填充内容)会很少会发生语法上的错误。 7.备份:每天要学会备份,不备份存在隐患,尽量上传到服务器上去,上传到服务器上的好处,不存在丢除非服务区死掉。 代码越写路越窄:备份了的话可以回头看,回到以前看看,可以调整思路。 8.类的命名规范:里面如果有一个用public修饰的class那他的名称必须和文件名称一致。 一个文件文件可以申请多个class吗?:一个文件可以申请多个class文件 一个文件不能含有多个public class声明。 正确写法:a.java 文件名必须一致 public class a{ } class b{ } class c{ } class d{ } 上面编译以后会生成3个文件; a.class b.class c.class d.class 写的是写在过括号外面的。 一个文件最好声明一个class,不要写多个,多个容易出错,查找起来也麻烦,有可能会起冲突。class(有可能会重名,起冲突)

优秀的个人博客,低调大师

javascript基础修炼(3)—What's this(下)

开发者的javascript造诣取决于对【动态】和【异步】这两个词的理解水平。 这一期主要分析各种实际开发中各种复杂的this指向问题。 一. 严格模式 严格模式是ES5中添加的javascript的另一种运行模式,它可以禁止使用一些语法上不合理的部分,提高编译和运行速度,但语法要求也更为严格,使用use strict标记开启。 严格模式中this的默认指向不再为全局对象,而是默认指向undefined。这样限制的好处是在使用构造函数而忘记写new操作符时会报错,而不会把本来需要绑定在实例上的一堆属性全绑在window对象上,在许多没有正确地绑定this的场景中也会报错。 二. 函数和方法的嵌套与混用 词法定义并不影响this的指向 , 因为this是运行时确定指向的。 2.1 函数定义的嵌套 function outerFun(){ function innerFun(){ console.log('innerFun内部的this指向了:',this); } innerFun(); } outerFun(); 控制台输出的this指向全局对象。 2.2 对象属性的嵌套 当调用的函数在对象结构上的定义具有一定深度时,this指向这个方法所在的对象,而不是最外层的对象。 var IronMan = { realname:'Tony Stark', rank:'1', ability:{ total_types:100, fly:function(){ console.log('IronMan.ability.fly ,作为方法调用时this指向:',this); }, } } IronMan.ability.fly(); 控制台输出的this指向IronMan的ability属性所指向的对象,调用fly( )这个方法的对象是IronMan.ability所指向的对象,而不是IronMan所指向的对象。 this作为对象方法调用时,标识着这个方法是如何被找到的。IronMan这个标识符指向的对象信息并不能在运行时找到fly( )这个方法的位置,因为ability属性中只存了另一个对象的引用地址,而IronMan.ability对象的fly属性所记录的指向,才能让引擎在运行时找到这个匿名方法。 三. 引用转换 引用转换实际上并不会影响this的指向,因为它是词法性质的,发生在定义时,而this的指向是运行时确定的。只要遵循this指向的基本原则就不难理解。 3.1 标识符引用转换为对象方法引用 var originFun = function (){ console.log('originFun内部的this为:',this); } var ironMan = { attack:originFun }; ironMan.attack(); 这里的this指向其调用者,也就是ironMan引用的对象。 3.2 对象方法转换为标识符引用 var ironMan = { attack:function(){ console.log('对象方法中this指向了:',this); } } var originFun = ironMan.attack; originFun(); 这里的this指向全局对象,浏览器中也就是window对象。3.2中的示例被认为是javascript语言的bug,即this指向丢失的问题。同样的问题也可能在回调函数传参时发生,本文【第5章】将对这种情况进行详细说明。 四. 回调函数 javascript中的函数是可以被当做参数传递进另一个函数中的,也就有了回调函数这样一个概念。 4.1 this在回调函数中的表现 var IronMan = { attack:function(findEnemy){ findEnemy(); } } function findEnemy(){ console.log('已声明的函数被当做回调函数调用,this指向:',this); } var attackAction = { findEnemy:function(){ console.log('attackAction.findEnemy本当做回调函数调用时,this指向',this); }, isArmed:function(){ console.log('check whether the actor is Armed'); } } //1.直接传入匿名函数 IronMan.attack(function(){ console.log(this); }); //2.传入外部定义函数 IronMan.attack(findEnemy); //3.传入外部定义的对象方法 IronMan.attack(attackAction.findEnemy); 从控制台打印的结果来看,无论以哪种方式来传递回调函数,回调函数执行时的this都指向了全局变量。 4.2 原理 javascript中函数传参全部都是值传递,也就是说如果调用函数时传入一个原始类型,则会把这个值赋值给对应的形参;如果传入一个引用类型,则会把其中保存的内存指向的地址赋值给对应的形参。所以在函数内部操作一个值为引用类型的形参时,会影响到函数外部作用域,因为它们均指向内存中的同一个函数。详细可参考[深入理解javascript函数系列第二篇——函数参数]这篇博文。 理解了函数传参,就很容易理解回调函数中this为何指向全局了,回调函数对应的形参是一个引用类型的标识符,其中保存的地址直接指向这个函数在内存中的真实位置,那么通过执行这个标识符来调用函数就等同于this基本指向规则中的作为函数来调用的情况,其this指向全局对象也就不难理解了。 五. this指针丢失 在第三节和第四节中,通过原理分析就能够明白为何在一些特定的场合下this会指向全局对象,但是从语言的角度来看,却很难理解this为什么指向了全局对象,因为这个规则和语法的字面意思是有冲突的。 5.1 回调函数的字面语境 var name = 'HanMeiMei'; var liLei = { name:'liLei', introduce:function () { console.log('My name is ', this.name); } }; var liLeiSay = liLei.introduce; liLeiSay();//同第三节中的引用转换示例 setTimeout(liLei.introduce,2000);//同第四节中的回调函数示例 上面的代码从字面上看意义是很明确的,就是希望liLei立刻介绍一下自己,在2秒后再介绍一下他自己。但控制台输出的结果中,他却两次都说自己的名字是HanMeiMei。 5.2 this指针丢失 5.1中的示例,也称为this指针丢失问题,被认为是Javascript语言的设计失误,因为这种设计在字面语义上造成了混乱。 5.3 this指针修复 方式1-使用bind 为了使代码的字面语境和实际执行保持一致,需要通过显示指定this的方式对this的指向进行修复。常用的方法是使用bind( )生成一个确定了this指向的新函数,将上述示例改为如下方式即可修复this的指向: var liLeiSay = liLei.introduce.bind(liLei); setTimeout(liLei.introduce.bind(liLei),2000); bind( )的实现其实并不复杂,是闭包实现高阶函数的一个简单的实例,感兴趣的读者可以自行了解。 方式2-使用Proxy Proxy是ES6中才支持的方法。 //绑定This的函数 function fixThis (target) { const cache = new WeakMap(); //返回一个新的代理对象 return new Proxy(target, { get (target, key) { const value = Reflect.get(target, key); //如果要取的属性不是函数,则直接返回属性值 if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }); } const toggleButtonInstance = fitThis(new ToggleButton()); 两种修复this指向的思路其实很类似,第一种方式相当于为调用的方法创建了一个代理方法,第二种方式是为被访问的对象创建了一个代理对象。 六. this的透传 实际开发过程中,往往需要在更深层次的函数中获取外层this的指向。 常规的解决方案是:将外层函数的this赋值给一个局部变量,通会使用_this,that,self,_self等来作为变量名保存当前作用域中的this。由于在javascript中作用域链的存在,嵌套的内部函数可以调用外部函数的局部变量,标识符会去寻找距离作用域链末端最近的一个指向作为其值,示例如下: document.querySelector('#btn').onclick = function(){ //保存外部函数中的this var _this = this; _.each(dataSet, function(item, index){ //回调函数的this指向了全局,调用外部函数的this来操作DOM元素 _this.innerHTML += item; }); } 七. 事件监听 事件监听中this的指向情况其实是几种情况的集合,与代码如何编写有很大关系。 7.1 表现 1. 在html文件中使用事件监听相关的属性来触发方法 <button onclick="someFun()">点击按钮</button> <button onclick="someObj.someFun()">点击按钮</button> 如果以第一种方式触发,则函数中的this指向全局; 如果以第二种方式触发,则函数中的this指向someObj这个对象。 2. 在js文件中直接为属性赋值 //声明一个函数 function callFromHTML() { console.log('callFromHTML,this指向:',this); } //定义一个对象方法 var obj = { callFromObj:function () { console.log('callFromObj',this); } } //注册事件监听-方式1 document.querySelector('#btn').onclick = function (event) { console.log(this); } //注册事件监听-方式2 document.querySelector('#btn').onclick = callFromHTML; //注册事件监听-方式3 document.querySelector('#btn').onclick = obj.callFromObj; 以上三种注册的事件监听响应函数,其this均指向id="btn"的DOM元素。 3. 使用addEventListener方法注册响应函数 //低版本IE浏览器中需要使用另外的方法 document.querySelector('#btn').addEventListener('click',function(event){ console.log(this); }); //也可以将函数名或对象方法作为回调函数传入 document.querySelector('#btn').addEventListener('click',callFromHTML); document.querySelector('#btn').addEventListener('click',obj.callFromObj); 这种方式注册的响应函数,其this与场景2相同,均指向id="btn"的DOM元素。区别在于使用addEventListener方法添加的响应函数会依次执行,而采用场景2的方式时,只有最后一次赋值的函数会被调用。 7.2 基本原理 1. 通过标签属性注册 <button id="btn" onclick="callFromHTML()">点我</button> <script> function callFromHTML() { console.log(document.querySelector('#btn').onclick); } </script> 在html中绑定事件处理程序,然后当按钮点击时,在控制台打印出DOM对象的onclick属性,可以看到: 这种绑定方式其实是将监听方法包裹在另一个函数中去执行,相当于: document.querySelector('#btn').onclick = function(event){ callFromHTML(); } 这样上述的表现就不难理解了。 2. 通过元素对象属性注册 document在javascript中是一个对象,通过其暴露的查找方法返回的节点也是一个对象,那么方式二绑定的监听函数在运行时,实际上就是在执行指定节点的onclick方法,根据this指向的基本规则可知其函数体中的this应该指向调用对象,也就是onclick这个方法所在的节点对象。 3. 通过addEventListener方法注册 这种方式是在DOM2事件模型中扩展的,用于支持多个监听器绑定的场景。DOM2事件模型的描述中规定了通过这种方式添加的监听函数执行时的this指向所在的节点对象,不同内核的浏览器实现方式有区别。 7.3 使用建议 不同的使用方式实质上是伴随着DOM事件模型升级而发生改变的,现代浏览器对于以上几种模式都是支持的,只有需要兼容老版本浏览器时需要考虑对DOM事件模型的支持程度。开发中DOM2级事件模型中addEventListener()和removeEventListener()来管理事件监听函数是最为推荐的方法。 八. 异步函数 1. setTimeout( )和setInterval( ) 这里的情况相当于上文中的回调函数的情况。 2. 事件监听 详见第7章。 3. ajax请求 几乎没有遇到过。 4. Promise 这里的情况相当于上文中的回调函数的情况。 九. 箭头函数和this 箭头函数是ES6标准中支持的语法,它的诞生不仅仅是因为表达方式简洁,也是为了更好地支持函数式编程。箭头函数内部不绑定this,arguments,super,new.target,所以由于作用域链的机制,箭头函数的函数体中如果使用到this,则执行引擎会沿着作用域链去获取外层的this。 十. Nodejs中的this Nodejs是一种脱离浏览器环境的javascript运行环境,this的指向规则上与浏览器环境在全局对象的指向上存在一定差异。 1. 全局对象global Nodejs的运行环境并不是浏览器,所以程序里没有DOM和BOM对象,Nodejs中也存在全局作用域,用来定义一些不需要通过任何模块的加载即可使用的变量、函数或类,全局对象中多为一些系统级的信息或方法,例如获取当前模块的路径,操作进程,定时任务等等。 2. 文件级this指向 Nodejs是支持模块作用域的,每一个文件都是一个模块,可通过require( )的方式同步引入,通过module.exports来暴露接口供其他模块调用。在一个文件中最顶级的this指向当前这个文件模块对外暴露的接口对象,也就是module.exports指向的对象。示例: var IronMan = { name:'Tony Stark', attack: function(){ } } exports.IronMan = IronMan; console.log(this); 在控制台即可看到,this指向一个对象,对象中只有一个属性IronMan,属性值为文件中定义的IronMan这个对象。 3. 函数级this指向 this的基本规则中有一条—当作为函数调用时,函数中的this指向全局对象,这一条在nodejs中也是成立的,这里的this指向了全局对象(此处的全局对象Global对象是有别于模块级全局对象的)。 思考题— React组件中为什么要bind(this) 如果你尝试使用过React进行前端开发,一定见过下面这样的代码: //假想定义一个ToggleButton开关组件 class ToggleButton extends React.Component{ constructor(props){ super(props); this.state = {isToggleOn: true}; this.handleClick = this.handleClick.bind(this); this.handleChange = this.handleChange.bind(this); } handleClick(){ this.setState(prevState => ({ isToggleOn: !preveState.isToggleOn })); } handleChange(){ console.log(this.state.isToggleOn); } render(){ return( <button onClick={this.handleClick} onChange={this.handleChange}> {this.state.isToggleOn ? 'ON':'OFF'} </button> ) } } 思考题:构造方法中为什么要给所有的实例方法绑定this呢?(强烈建议读者先自己思考再看笔者分析) 1. 代码执行的细节 上例仅仅是一个组件类的定义,当在其他组件中调用或是使用ReactDOM.render( )方法将其渲染到界面上时会生成一个组件的实例,因为组件是可以复用的,面向对象的编程方式非常适合它的定位。根据this指向的基本规则就可以知道,这里的this最终会指向组件的实例。 组件实例生成的时候,构造器constructor会被执行,此处着重分析一下下面这行代码: this.handleClick = this.handleClick.bind(this); 此时的this指向新生成的实例,那么赋值语句右侧的表达式先查找this.handleClick( )这个方法,由对象的属性查找机制(沿原型链由近及远查找)可知此处会查找到原型方法this.handleClick( ),接着执行bind(this),此处的this指向新生成的实例,所以赋值语句右侧的表达式计算完成后,会生成一个指定了this的新方法,接着执行赋值操作,将新生成的函数赋值给实例的handleClick属性,由对象的赋值机制可知,此处的handleClick会直接作为实例属性生成。总结一下,上面的语句做了一件这样的事情: 把原型方法handleClick( )改变为实例方法handleClick( ),并且强制指定这个方法中的this指向当前的实例。 2. 绑定this的必要性 在组件上绑定事件监听器,是为了响应用户的交互动作,特定的交互动作触发事件时,监听函数中往往都需要操作组件某个状态的值,进而对用户的点击行为提供响应反馈,对开发者来说,这个函数触发的时候,就需要能够拿到这个组件专属的状态合集(例如在上面的开关组件ToggleButton例子中,它的内部状态属性state.isToggleOn的值就标记了这个按钮应该显示ON或者OFF),所以此处强制绑定监听器函数的this指向当前实例的也很容易理解。 React构造方法中的bind会将响应函数与这个组件Component进行绑定以确保在这个处理函数中使用this时可以时刻指向这一组件的实例。 3. 如果不绑定this 如果类定义中没有绑定this的指向,当用户的点击动作触发this.handleClick( )这个方法时,实际上执行的是原型方法,可这样看起来并没有什么影响,如果当前组件的构造器中初始化了state这个属性,那么原型方法执行时,this.state会直接获取实例的state属性,如果构造其中没有初始化state这个属性(比如React中的UI组件),说明组件没有自身状态,此时即使调用原型方法似乎也没什么影响。 事实上的确是这样,这里的bind(this)所希望提前规避的,就是第五章中的this指针丢失的问题。 例如使用解构赋值的方式获取某个属性方法时,就会造成引用转换丢失this的问题: const toggleButton = new ToggleButton(); import {handleClick} = toggleButton; 上例中解构赋值获取到的handleClick这个方法在执行时就会报错,Class的内部是强制运行在严格模式下的,此处的this在赋值中丢失了原有的指向,在运行时指向了undefined,而undefined是没有属性的。 另一个存在的限制,是没有绑定this的响应函数在异步运行时可能会出问题,当它作为回调函数被传入一个异步执行的方法时,同样会因为丢失了this的指向而引发错误。 如果没有强制指定组件实例方法的this,在将来的使用中就无法安心使用引用转换或作为回调函数传递这样的方式,对于后续使用和协作开发而言都是不方便的。 参考 [1]《javascript高级程序设计(第三版)》 [2]《深入理解javascript函数系列第二篇》https://www.cnblogs.com/xiaohuochai/p/5706289.html [3]《ES6-Class基本语法》https://www.cnblogs.com/ChenChunChang/p/8296350.html

优秀的个人博客,低调大师

Java 基础 之 do while 循环实例

http://www.verejava.com/?id=16992628432522 /** 连续录入学生姓名,输入“q”则系统退出 */ import java.util.Scanner; public class DoWhile2 { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("请连续录入学生姓名, 直到输入q退出系统"); String name = ""; do { name = in.nextLine(); } while (!name.equals("q")); System.out.println("退出系统"); } } /* while循环和do-while循环的区别? */ http://www.verejava.com/?id=16992628432522

优秀的个人博客,低调大师

Java 基础 之 关系运算符

http://www.verejava.com/?id=16992594990214 public class Operation3 { public static void main(String[] args) { //关系运算符 只有两种结果:真 true, 假 false /* 包括: 大于 >, 大于等于 >= 小于 <, 小于等于 <= 等于==, 不等于 != */ System.out.println(1>2); System.out.println(1>=1); System.out.println(1<2); System.out.println(1<=2); System.out.println(1==1); System.out.println(1!=2); System.out.println(1==1.0); System.out.println('A'==65); //true 不能等于1 ,false 不能等于 0; //System.out.println(true==1); } } http://www.verejava.com/?id=16992594990214

优秀的个人博客,低调大师

Java 基础 之 算数运算符

http://www.verejava.com/?id=16992579787012 /* 1. 算数运算 */ public class Operation { public static void main(String[] args) { //1. 算数运算 加+,减-,乘*,整除/,取模%,自减--,自增++ int a=1; int b=2; int c=3; int d=4; System.out.println(a+b); System.out.println(a-b); System.out.println(a*b); System.out.println(b/a); System.out.println(a/b); //取模就是整除后余下的数 System.out.println(c%b); System.out.println(d%b); System.out.println(4%-3); System.out.println(-4%-3); System.out.println(-4%3); System.out.println(d++); System.out.println(++d); System.out.println(d--); System.out.println(--d); /* 注意: 1. 整除的时候会自动切除小数部分 2. 取模的时候结果值的正负取决于分子的符号 3. 对于自增++在变量后面时是先输出,再加1, 如果++放在变量前面则是先加+再输出计算。 4. 对于自减在变量后面时是先输出再减1,如果放在变量后面是先减1再输出计算 */ } } http://www.verejava.com/?id=16992579787012

优秀的个人博客,低调大师

JavaScript 基础 --- (数据类型/循环/条件)

一、js 输出1.window.alert() 警告框 2.document.write() 写到 HTML 文档中 3.innerHTML 写到 HTML 元素 4.console.log() 写到浏览器的控制台 二、js 语句与注释 1. ; (var a = 1;) ;表示语句的结尾 2. 语句标识符:var / if / for 等 3.注释:单行 // 多行 /* */三、数据类型 1.未定义 (undefined) var x; //x 为 undefined(表示变量不含有值) 2.数字 (Number) var y = 5; //y 为数字 3.字符串 (String) var z = "John" // z为字符串 4.布尔 (Boolean) var a = true; //a 为 布尔类型 5.对象 (Object) var person ={firstname:"John",lastname:"Doe",id:5566}; 取值:console.log(person.firstname) console.log(person["firstname"]) 6.数组 (Array) 6-1. var cars = new Array(); //此时 cars 数组的长度为 0 cars[0] = "Saab"; //此时 cars 数组的长度为 1 ,js 数组是动态变化的,即第一个数赋值为 Saab car[1] = "Volvo"; //即第二个数赋值为 Volvo car[1] = "BMW"; //即第三个数赋值为 BMW 6-2. var cars = new Array("Saab","Volvo","bmw") ; 6-3. var cars = ["Saab"."Volvo","BMW"]; 7.空 (Null) var b = null //b为 null(可以用来清空变量) 四、js 变量 1.变量必须以字母开头,(不推荐 $ 和 _)区分大小写 五、js 函数 函数:是由事件驱动的或者当它被调用时执行的可重复使用的代码块 定义方法:1.function a(参数){ } 声明会前置 输出: hello world 输出: hello world 2. var a = function(参数){} 匿名函数 输出:这是一个匿名函数 输出: 1 2 3 (传递参数) return 输出: 六、js 比较与逻辑运算符 1. < 小于 > 大于 2.== 等于(5=="5") === 绝对等于(值和类型均相等 5===5) != 不等于 3.&& 与 || 或 !非 七、js 条件语句 if else if 输出: switch 输出:1 八、js 循环语句 for 输出: for in(循环遍历对象) 输出: while(指定条件为 true 时循环指定的代码块) 输出: break (跳出整个循环) 输出: continue(跳出本次循环) 输出:

优秀的个人博客,低调大师

Java基础之HashTable与ConcurrentHashMap解析

HashTable和HashMap的区别 在面试的过程中,经常会被问到HashTable和HashMap的区别,下面就这些区别做一个简单的总结。 1、继承的父类不同 Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类,但二者都实现了Map接口。 2、线程安全性不同 Hashtable 中的方法是Synchronized的,而HashMap中的方法在缺省情况下是非Synchronized的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。 总结一句话:Hashtable(1.0版本)不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 3、是否提供contains方法 HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。 4、key和value是否允许null值 Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。 5、遍历的内部实现方式不同 Hashtable、HashMap都使用了 Iterator。但由于历史原因,Hashtable还使用了Enumeration的方式 。 6,数组初始化和扩容方式不同 HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。具体扩容时,Hashtable将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 HashTable 由于HashTable的性能问题,在实际编程中HashTable并不是很常见,更多的是使用HashMap或ConcurrentHashMap。 简单来说,HashTable是一个线程安全的哈希表,它通过使用synchronized关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程环境中效率低下等问题。 HashTable存储模型 HashTable保存数据是和HashMap是相同的,使用的也是Entry对象。HashTable类继承自Dictionary类,实现了Map,Cloneable和java.io.Serializable三个接口,其UML图如下图所示。 HashTable的功能与与HashMap中的功能相同,主要有:put,get,remove和rehash等。 HashTable的主要方法的源码实现逻辑与HashMap中非常相似,有一点重大区别就是所有的操作都是通过synchronized锁保护的。也就是说,只有获得了对应的锁,才能进行后续的读写等操作。 下面就HashTable常见的方法给大家做一个简单的解析。 构造方法 HashTable的构造方法源码如下: public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; table = new Entry<?,?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } public Hashtable() { this(11, 0.75f); } 从构造函数中可以得到如下的信息:HashTable默认的初始化容量为11(与HashMap不同,HashMap是16),负载因子默认为0.75(与HashMap相同)。而正因为默认初始化容量的不同,同时也没有对容量做调整的策略,所以可以先推断出,HashTable使用的哈希函数跟HashMap是不一样的。 put put方法的主要逻辑如下: 先获取synchronized锁; put方法不允许null值,如果发现是null,则直接抛出异常; 计算key的哈希值和index; 遍历对应位置的链表,如果发现已经存在相同的hash和key,则更新value,并返回旧值; 如果不存在相同的key的Entry节点,则调用addEntry方法增加节点; addEntry方法中,如果需要则进行扩容,之后添加新节点到链表头部。 Put方法的源码如下: public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); //计算桶的位置 int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; //遍历桶中的元素,判断是否存在相同的key for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } //不存在相同的key,则把该key插入到桶中 addEntry(hash, key, value, index); return null; } 涉及的Entry对象的源码如下: private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; //哈希表的键值对个数达到了阈值,则进行扩容 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; //把新节点插入桶中(头插法) tab[index] = new Entry<>(hash, key, value, e); count++; } 从上面的源码可以看到,put方法一开始就会进行值的null值检测,同时,HashTable的put方法也是使用synchronized来修饰。你可以发现,在HashTable中,几乎所有的方法都使用了synchronized来保证线程安全。 get get方法的主要逻辑如下: 先获取synchronized锁; 计算key的哈希值和index; 在对应位置的链表中寻找具有相同hash和key的节点,返回节点的value; 如果遍历结束都没有找到节点,则返回null。 get函数的源码如下: public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); //通过哈希函数,计算出key对应的桶的位置 int index = (hash & 0x7FFFFFFF) % tab.length; //遍历该桶的所有元素,寻找该key for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; } 从上面的代码可以发现,get方法使用了synchronized来修饰,以保证线程的安全,并且它是通过链表的方式来处理冲突的。另外,我们还可以看见HashTable并没有像HashMap那样封装一个哈希函数,而是直接把哈希函数写在了方法中。 rehash扩容 rehash扩容方法主要逻辑如下:数组长度增加一倍(如果超过上限,则设置成上限值);更新哈希表的扩容门限值;遍历旧表中的节点,计算在新表中的index,插入到对应位置链表的头部。 rehash方法的源码如下: protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; //扩容扩为原来的两倍+1 int newCapacity = (oldCapacity << 1) + 1; //判断是否超过最大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; //计算下一次rehash的阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; //把旧哈希表的键值对重新哈希到新哈希表中去 for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } } HashTable的rehash方法相当于HashMap的resize方法。跟HashMap那种巧妙的rehash方式相比,HashTable的rehash过程需要对每个键值对都重新计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作。这也是HashTable比HashMap低效的原因之一。 remove remove方法主要逻辑如下: 先获取synchronized锁; 计算key的哈希值和index; 遍历对应位置的链表,寻找待删除节点,如果存在,用e表示待删除节点,pre表示前驱节点。如果不存在,返回null; 更新前驱节点的next,指向e的next。返回待删除节点的value值。 remove函数的源码如下: public synchronized V remove(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { modCount++; if (prev != null) { prev.next = e.next; } else { tab[index] = e.next; } count--; V oldValue = e.value; e.value = null; return oldValue; } } return null; } ConcurrentHashMap HashMap是我们平时开发过程中使用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。例如: final HashMap<String, String> map = new HashMap<String, String>(2); for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }).start(); } 但是解决方法也有很多,如Hashtable和Collections.synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能可想而知。此时,可以使用ConcurrentHashMap来解决。 JDK 1.7 ConcurrentHashMap实现 和HashMap不同,ConcurrentHashMap采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构。ConcurrentHashMap最核心的两个核心静态内部类包括:Segment和HashEntry。 理解ConcurrentHashMap需要注意如下几个概念: Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶; HashEntry 用来封装映射表的键 / 值对; 每个桶是由若干个 HashEntry 对象链接起来的链表。 一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,其数据结构如下: JDK1.8 ConcurrentHashMap实现 1.8的实现已经抛弃了Segment分段锁机制,而是采用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。而HashMap在1.8版本中也对存储结构进行了优化,采用数组+链表+红黑树的方式进行数据存储,红黑树可以有效的平衡二叉树,带来插入、查找性能上的提升。 ConcurrentHashMap在1.8版本的数据存储结构如下图: 初始化 只有在第一次执行put方法时才会调用initTable()初始化Node数组,该方法的源码如下: private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; } sizeCtl默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1,有且只有一个线程能够修改成功,其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。 关于具体的的一些put、get、table扩容等操作,大家可以自行搜索相关的资料。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册