• 关于 过程赋值有什么用 的搜索结果

回答

少年, 看过唐诗么? 有没有所有的字都认识, 但是放在一起就不知道是什么意思的感觉? 所谓单步调试都看懂,跟这个是类似的. 一行代码,无非赋值,运算, 比较,调用子过程,谁都看得懂, 关键是,为什么?为什么赋值?为什么运算?为什么比较?为什么调用子过程? 这个为什么, 是与具体的业务,或者说事实,东西相对应的. 你说你懂得原理, 但是写不出代码, 我比较不相信前半句. ######顶!######坐等大牛回答###### 先想后写,从小处着手积累信心 PS 我不是大牛,小牛也不是 ###### 这是个很抽象的问题,根据我的经验给出如下练习步骤 确认智商(> 80) 写代码之前将问题进行细化,写#字游戏这个过程可以分为以下几个阶段 设计阶段(确定用何种数据结构保存游戏中的数据) 涉及到哪些技术(画图,坐标计算) 游戏的逻辑(如何判断输赢,禁手) 按照以上步骤思考一下,那一步不会就去学习相应的知识. ######先背一些常会用到的代码段######背啊。没有谁编程不背的。背了就模仿别人。这才是王道啊。######你先把头像上传了在说.写不好程序和没有上传头像有很大关系的.######对于开始应用语言编程时,我也有这方面的困惑,总感觉需要站在巨人的肩膀上才可以!万事平地起,难度太大。######关键是你要对它有兴趣,他就是你的艺术品!######先写伪代码  再写真代码~~~用小块函数实现小功能  凑在一起就变成了大项目~~~!!日积月累是我们成为大牛的 不变真理~~~

kun坤 2020-06-10 09:34:55 0 浏览量 回答数 0

回答

今天浏览博客的时候看到这么一句话: python中变量名和对象是分离的;最开始的时候是看到这句话的时候没有反应过来。决定具体搞清楚一下python中变量与对象之间的细节。(其实我感觉应该说 引用和对象分离 更为贴切)   从最开始的变量开始思考:    在python中,如果要使用一个变量,不需要提前进行声明,只需要在用的时候,给这个变量赋值即可 (这个和C语言等静态类型语言不同,和python为动态类型有关)。    举第一个栗子:     a = 1    这是一个简单的赋值语句,整数 1 为一个对象,a 是一个引用,利用赋值语句,引用a指向了对象1;这边形象比喻一下:这个过程就相当于“放风筝”,变量a就是你手里面的“线”,python就跟那根“线”一样,通过引用来接触和拴住天空中的风筝——对象。    你可以通过python的内置函数 id() 来查看对象的身份(identity),这个所谓的身份其实就是 对象 的内存地址:     注:      python一切皆对象的理念,所以函数也是一个对象,因此可以使用 id() 函数的__doc__方法来查看这个函数的具体描述: 12 id.__doc__ "id(object) -> integer\n\nReturn the identity of an object. This is guaranteed to be unique among\nsimultaneously existing objects.       (Hint: it's the object's memory address.)"       第二个栗子:     a = 2     a = 'banana'    利用上面第一个栗子用到的 id()函数:     123456 a = 1id(a) 24834392 a = 'banana'id(a) 139990659655312    第一个语句中, 2是储存在内存中的一个整数对象,通过赋值 引用a 指向了 对象 1     第二个语句中,内存中建立了一个字符串对象‘banana’,通过赋值 将 引用a 指向了 ‘banana’,同时,对象1不在有引用指向它,它会被python的内存处理机制给当我垃圾回收,释放内存。    第三个栗子:     a = 3     b = 3    通过函数查看 变量a 和 变量b的引用情况:  123456 a = 3b = 3id(a) 10289448 id(b) 10289448  在这里可以看到 这俩个引用 指向了同一个 对象,这是为什么呢? 这个跟python的内存机制有关系,因为对于语言来说,频繁的进行对象的销毁和建立,特别浪费性能。所以在Python中,整数和短小的字符,Python都会缓存这些对象,以便重复使用。    第四个栗子:     1. a = 4     2. b = a(这里就是让引用b指向引用a指向的那个对象)     3. a = a + 2    通过函数查看引用情况:     当执行到第2步的时候,查看一下 a 和 b 的引用:       123456 a = 4b = aid(a) 36151568 id(b) 36151568    可以看到 a 和 b 都指向了 整数对象 4     接下来指向第3步: 12345 a = a+2id(a) 36151520 id(b) 36151568    可以看到 a 的引用改变了,但是 b 的引用未发生改变;a,b指向不同的对象; 第3句对 a 进行了重新赋值,让它指向了新的 对象6;即使是多个引用指向同一个对象,如果一个引用值发生变化,那么实际上是让这个引用指向一个新的引用,并不影响其他的引用的指向。从效果上看,就是各个引用各自独立,互不影响。    第五个栗子(这个栗子会涉及到 python中的 可变数据类型 和 不可变数据类型):    开始这个栗子之前,请记得注意到 第四个栗子的不同之处。      1. L1 = [1, 2, 3]      2. L2 = L1      3. L1[0] = 10    通过函数查看引用情况:      当执行第1步 和 第二步 的时候,查看一下 L1 和 L2 的引用情况: 123456 L1 = [1,2,3]L2 = L1id(L1) 139643051219496 id(L2) 139643051219496     此时 L1 和 L2 的引用相同,都是指向 [1,2,3]这个列表对象。      接下来,继续执行第3步: 1234567 L1[0] = 10id(L1) 139643051219496 id(L2) 139643051219496 L2 [10, 2, 3]     同样的跟第四个栗子那样,修改了其中一个对象的值,但是可以发现 结果 并不与 第四个栗子那样, 在本次实验中,L1 和 L2 的引用没有发生任何变化,但是 列表对象[1,2,3] 的值 变成了 [10,2,3](列表对象改变了)      在该情况下,我们不再对L1这一引用赋值,而是对L1所指向的表的元素赋值。结果是,L2也同时发生变化。      原因何在呢?因为L1,L2的指向没有发生变化,依然指向那个表。表实际上是包含了多个引用的对象(每个引用是一个元素,比如L1[0],L1[1]..., 每个引用指向一个对象,比如1,2,3), 。而L1[0] = 10这一赋值操作,并不是改变L1的指向,而是对L1[0], 也就是表对象的一部份(一个元素),进行操作,所以所有指向该对象的引用都受到影响。 (与之形成对比的是,我们之前的赋值操作都没有对对象自身发生作用,只是改变引用指向。)      列表可以通过引用其元素,改变对象自身(in-place change)。这种对象类型,称为可变数据对象(mutable object),词典也是这样的数据类型。      而像之前的数字和字符串,不能改变对象本身,只能改变引用的指向,称为不可变数据对象(immutable object)。      我们之前学的元组(tuple),尽管可以调用引用元素,但不可以赋值,因此不能改变对象自身,所以也算是immutable object.              is关键字:     当然,我们也可以要想知道是否指向同一个对象,我们可以使用 python的 is 关键词,is用于判断两个引用所指的对象是否相同。     就像上述第四个栗子 当进行到 第1步 和 第2步 的时候: 1234 a = 4 ……id(a) = 36151568b =a ……id(b) = 36151568a is b True    当进行到第3步的时候: 123 a = a + 2 ……id(a) = 36151520a is b ……id(b) = 36151568 False                   突然想到,对于python 的 深拷贝 和 浅拷贝 的理解,也是可以根据这个进行验证,可以通过第五个栗子进行辅助理解。        

xuning715 2019-12-02 01:10:27 0 浏览量 回答数 0

问题

JavaScript参数传递中值和引用的一种理解

云效平台 2019-12-01 21:19:51 2506 浏览量 回答数 1

新用户福利专场,云服务器ECS低至96.9元/年

新用户福利专场,云服务器ECS低至96.9元/年

问题

关于c语言中scanf()的“%c”格式控制的一个问题:报错

kun坤 2020-06-06 15:53:58 0 浏览量 回答数 1

回答

排查过程 pom中新加的各种依赖 添加 exclusion。 -> 不管用pom中添加各种 slf4j-log4j12 not-exist -> 不管用了解logback和slf4j的业务知识点断点排查 断点 LoggerFactory.getLogger 发现logger绑定逻辑,没什么卵用断点 logger.info 逻辑, 发现 logger对应的appender为空,发现对应的aai变量也为空找到对应的设置aai的地方在demo中,找到给给aai赋值的最上层,org.springframework.boot.logging.logback.LogbackLoggingSystem#configureByResourceUrl发现这里读取的 logback配置文件错误包含该配置文件的依赖为 hsf.app.spring查找包含该依赖的依赖,并exclude掉hsf.app.spring日志回复正常 能力沉淀 找到问题实在找不到原因可以debug 找到有问题的变量找到设置变量的地方 (可以通过demo来找)原先程序在设置变量的地方debug,观察各个变量是否符合预期

君浩 2020-03-31 00:21:06 0 浏览量 回答数 0

回答

一。zval、引用计数、变量分离、写时拷贝我们一步步来理解1、php语言特性PHP是脚本语言,所谓脚本语言,就是说PHP并不是独立运行的,要运行PHP代码需要PHP解析器,用户编写的PHP代码最终都会被PHP解析器解析执行PHP的执行是通过Zend engine(ZE, Zend引擎),ZE是用C编写的用户编写的PHP代码最终都会被翻译成PHP的虚拟机ZE的虚拟指令(OPCODES)来执行也就说最终会被翻译成一条条的指令既然这样,有什么结果和你预想的不一样,查看php源码是最直接最有效的 2、php变量的存储结构在PHP中,所有的变量都是用一个结构zval结构来保存的,在Zend/zend.h中可以看到zval的定义:zval结构包括:① value —— 值,是真正保存数据的关键部分,定义为一个联合体(union)② type —— 用来储存变量的类型 ③ is_ref —— 下面介绍④ refcount —— 下面介绍 声明一个变量$addr="北京";PHP内部都是使用zval来表示变量的,那对于上面的脚本,ZE是如何把addr和内部的zval结构联系起来的呢?变量都是有名字的(本例中变量名为addr)而zval中并没有相应的字段来体现变量名。PHP内部肯定有一个机制,来实现变量名到zval的映射在PHP中,所有的变量都会存储在一个数组中(确切的说是hash table)当你创建一个变量的时候,PHP会为这个变量分配一个zval,填入相应的信息,然后将这个变量的名字和指向这个zval的指针填入一个数组中。当你获取这个变量的时候,PHP会通过查找这个数组,取得对应的zval 注意:数组和对象这类复合类型在生成zval时,会为每个单元生成一个zval3、我们经常说每个变量都有一个内存地址,那这个zval和变量的内存地址,这俩有什么关系吗?定义一个变量会开辟一块内存,这块内存好比一个盒子,盒子里放了zval,zval里保存了变量的相关信息,需要开辟多大的内存,是由zval所占空间大小决定的zval是内存对象,垃圾回收的时候会把zval和内存地址(盒子)分别释放掉 4、引用计数、变量分离、写时拷贝zval中的refcount和is_ref还没有介绍,我们知道PHP是一个长时间运行的服务器端脚本。那么对于它来说,效率和资源占用率是一个很重要的衡量标准,也就是说,PHP必须尽量减少内存占用率。考虑下面这段代码:第一行代码创建了一个字符串变量,申请了一个大小为9字节的内存,保存了字符串“laruence”和一个NULL(0)的结尾第二行定义了一个新的字符串变量,并将变量var的值“复制”给这个新的变量第三行unset了变量var 这样的代码是很常见的,如果PHP对于每一个变量赋值都重新分配内存,copy数据的话,那么上面的这段代码就要申请18个字节的内存空间,为了申请新的内存,还需要cpu执行某些计算,这当然会加重cpu的负载而我们也很容易看出来,上面的代码其实根本没有必要申请两份空间,当第三句执行后,$var被释放了,我们刚才的设想(申请18个字节内存空间)突然变的很滑稽,这次复制显得好多余。如果早知道$var不用了,直接让$var_dup用$var的内存不就行了,还复制干嘛?如果你觉得9个字节没什么,那设想下如果$var是个10M的文件内容,或者20M,是不是我们的计算机资源消耗的有点冤枉呢?呵呵,PHP的开发者也看出来了: 刚才说了,PHP中的变量是用一个存储在symbol_table中的符号名,对应一个zval来实现的,比如对于上面的第一行代码,会在symbol_table中存储一个值“var”,对应的有一个指针指向一个zval结构,变量值“laruence”保存在这个zval中,所以不难想象,对于上面的代码来说,我们完全可以让“var”和“var_dup”对应的指针都指向同一个zval就可以了(额,鸟哥一会说hash table,一会说symbol_table,暂且理解为symbol_table是hash table的子集) PHP也是这样做的,这个时候就需要介绍一下zval结构中的refcount字段了refcount,引用计数,记录了当前的zval被引用的次数(这里的引用并不是真正的 & ,而是有几个变量指向它)比如对于代码:第一行,创建了一个整形变量,变量值是1。 此时保存整形1的这个zval的refcount为1第二行,创建了一个新的整形变量(通过赋值的方式),变量也指向刚才创建的zval,并将这个zval的refcount加1,此时这个zval的refcount为2所以,这个时候(通过值传递的方式赋值给别的变量),并没有产生新的zval,两个变量指向同一zval,通过一个计数器来共用zval及内存地址,以达到节省内存空间的目的当一个变量被第一次创建的时候,它对应的zval结构的refcount的值会被初始化为1,因为只有这一个变量在用它。但是当你把这个变量赋值给别的变量时,refcount属性便会加1变成2,因为现在有两个变量在用这个zval结构了 PHP提供了一个函数可以帮助我们了解这个过程debug_zval_dump输出:long(1) refcount(2)long(1) refcount(3)如果你奇怪 ,var的refcount应该是1啊?我们知道,对于简单变量,PHP是以传值的形式传参数的。也就是说,当执行debug_zval_dump($var)的时候,$var会以传值的方式传递给debug_zval_dump,也就是会导致var的refcount加1,所以只要能看到,当变量赋值给一个变量以后,能导致zval的refcount加1这个结果即可现在我们回头看上面的代码, 当执行了最后一行unset($var)以后,会发生什么呢?unset($var)的时候,它删除符号表里的$var的信息,准备清理它对应的zval及内存空间,这时它发现$var对应的zval结构的refcount值是2,也就是说,还有另外一个变量在一起用着这个zval,所以unset只需把这个zval的refcount减去1就行了上代码:输出:string(8) "laruence" refcount(2) 但是,对于下面的代码呢?很明显在这段代码执行以后,$var_dup的值应该还是“laruence”,那么这又是怎么实现的呢?这就是PHP的copy on write机制(简称COW):PHP在修改一个变量以前,会首先查看这个变量的refcount,如果refcount大于1,PHP就会执行一个分离的过程(在Zend引擎中,分离是破坏一个引用对的过程)对于上面的代码,当执行到第三行的时候,PHP发现$var想要改变,并且它指向的zval的refcount大于1,那么PHP就会复制一个新的zval出来,改变其值,将改变的变量指向新的zval(哪个变量指向新复制的zval其实已经无所谓了),并将原zval的refcount减1,并修改symbol_table里该变量的指针,使得$var和$var_dup分离(Separation)。这个机制就是所谓的copy on write(写时复制,这里的写包括普通变量的修改及数组对象里的增加、删除单元操作)如果了解了is_ref之后,上面说的并不严谨 上代码测试:输出:long(1) refcount(2)string(8) "laruence" refcount(2) 现在我们知道,当使用变量复制的时候 ,PHP内部并不是真正的复制,而是采用指向相同的zval结构来节约开销。那么,对于PHP中的引用,又是如何实现呢?这段代码结束以后,$var也会被间接的修改为1,这个过程称作(change on write:写时改变)那么ZE是怎么知道,这次的复制不需要Separation呢?这个时候就要用到zval中的is_ref字段了:对于上面的代码,当第二行执行以后,$var所代表的zval的refcount变为2,并且设置is_ref为1到第三行的时候,PHP先检查var_ref对应的zval的is_ref字段(is_ref 表示该zval是否被&引用,仅表示真或假,就像开关的开与关一样,zval的初始化情况下为0,即非引用),如果为1,则不分离,直接更改(否则需要执行刚刚提到的zval分离),更改共享的zval实际上也间接更改了$var的值,因为引擎想所有的引用变量都看到这一改变php源码做了这样一个判断,大体逻辑示意如下:如果这个zval中的if_ref为1(即被引用),或者该zval引用计数小于2任何一种方式:都不会进行分离 尽管已经存在写时复制和写时改变,但仍然还存在一些不能通过is_ref和refcount来解决的问题对于如下的代码,又会怎样呢?这里$var、$var_dup、$var_ref三个变量将共用一个zval结构(其实这是不可能的,一个zval不可能既被&,又被指向),有两个属于change-on-write组合($var和$var_ref),有两个属于copy-on-write组合($var和$var_dup),那is_ref和refcount该怎样工作,才能正确的处理好这段复杂的关系呢?答案是不可能!在这种情况下,变量的值必须分离成两份完全独立的存在当执行第二行代码的时候,和前面讲过的一样,$var_dup 和 $var 指向相同的zval, refcount为2当执行第三行的时候,PHP发现要操作的zval的refcount大于1,则PHP会执行Separation(也就是说php将一个zval的is_ref从0设为1 之前,当然此时refcount还没有增加,会看该zval的refcount,如果refcount>1,则会分离), 将$var_dup分离出去,并将$var和$var_ref做change on write关联。也就是,refcount=2, is_ref=1;所以内存会给变量var_dup 分配出一个新的zval,类型与值同 $var和$var_ref指向的zval一样,是新分配出来的,尽管他们拥有同样的值,但是必须通过两个zval来实现。试想一下,如果三者指向同一个zval的话,改边 $var_dup 的值,那么 $var和$var_ref 也会受到影响,这样就乱套了图解:下面的这段代码在内核中同样会产生歧义,所以需要强制复制!也就是说一个zval不会既被引用,又被指向,必须分离 基于这样的分析,我们就可以让debug_zval_dump出refcount为1的结果来:输出:string(8) "laruence" refcount(1) 为什么结果是refcount(1)呢debug_zval_dump()中参数是引用的话,refcount永远为1这两段代码在执行的时候是这样的逻辑:PHP先看变量指向的zval是否被引用,如果是引用,则不再产生新的zval甭管哪个变量引用了它,比如有个变量$a被引用了,$b=&$a,就算自己引用自己$a=&$a,$a所指向的zval都不会被复制,改变其中一个变量的值,另一个值也被改变(写时改变)如果is_ref为0且refcount大于1,改变其中一个变量时,复制新的zval(写时复制) 还有一个知识点需要了解下,就是PHP数组复制的机制复制一个数组,就是把一个数组赋值给一个变量便可。会把数组指针位置一同复制。这里面有两种情况:① 指针位置合法,这时直接复制,无影响② 原数组指针位置非法时(移出界),“新”数组指针会初始化(这里的新为什么要加引号?请看下文),而老的数组指针位置不变,还是false先看例子: 结果:!结果:出现这种情况好像不对?$arr2 难道不是新数组?新数组的数组指针应该重置了啊这里注意了:$arr2 = $arr1 ,在俩变量都没发生写操作时,他们其实引用的是同一个内存地址。在其中一个变量发生写操作后,内存地址会复制一份,发生改变的变量会去引用它,并把数组指针初始化。所以 $arr1 会去引用复制的内存地址,并将指针初始化二。.foreach循环时调用current等函数!结果: 56按照之前说的,foreach先赋值,再移动指针,再执行循环体,第一次结果为2可以理解为什么三次都是2呢?咋就这么2呢?因为current函数是按引用传递的函数 在zval笔记中说了,一个zval不能既被引用,又被指向所以,变量分离,重新拷贝一份数组专门用于current函数 当然,如果数组zval的is_ref为1,则不会拷贝数组了或者:结果:current是引用传参

杨冬芳 2019-12-02 02:26:33 0 浏览量 回答数 0

回答

引用来自“李崇”的答案 分开两个类来处理。 Entity:和数据库中字段一一对应,比如你的员工和部门,是多对一关系, 所以员工类里面有一个deptId即可。如果是多对多关系那就建立一个中间表的类。 操作Entity中的字段就相当于操作数据库的字段。 VO:用来做显示,和界面上的元素一一对应。 UserVO{   privateintid;     privateStringdeptId;        privateStringdeptName; } 差不多是这样,传入到Dao层的是Entity,传出的是VO,通过关联查询的SQL语句为VO赋值,传递回界面。 你那种做法可能是用惯了hibernate的做法,不知道能不能理解。 明白了!嗯,这样扩展性大大提高了!多谢啊! 最好的设计--去掉pojo和DAO 现在可能无法理解 慢慢就理解了您的意思是不要考虑这些东西,直接考虑代码与数据库的关系是吗?拼接SQL,然后直接使用DBHelper执行?如果使用pojo与Dao,就要使用Hibernate是吗?最纯粹的才是最简便的,可惜我的场景就是这样的,头疼! 分开两个类来处理。 Entity:和数据库中字段一一对应,比如你的员工和部门,是多对一关系, 所以员工类里面有一个deptId即可。如果是多对多关系那就建立一个中间表的类。 操作Entity中的字段就相当于操作数据库的字段。 VO:用来做显示,和界面上的元素一一对应。 UserVO{   privateintid;     privateStringdeptId;        privateStringdeptName; } 差不多是这样,传入到Dao层的是Entity,传出的是VO,通过关联查询的SQL语句为VO赋值,传递回界面。 你那种做法可能是用惯了hibernate的做法,不知道能不能理解。 谢谢你耐心的回答,我明白你的意思了,一个对应数据库,一个对应需求,不过我有个疑问。如果针对UserVO的业务比较复杂,也就是外键跨表字段比较多,是不是要在UserVO里面定义很多跨表的属性,有没有基于对象设计思想的解决方案啊!这样是不是扩展性有所欠缺,VO与页面绑死了!不知我说的对否! 看了看楼主的设计,感觉挺合理的啊-。-有一个问题,不知楼主能否帮忙解答 为什么 executeUpdate(StringBuildersql,Object...params)没有传入Connection对象,而 executeQuery(Connectionconn,StringBuildersql,Object...params)传入了呢? 这样设计的道理是什么? 这个很简单,1对多在多的一方设置一个外键字段,为了实现实体查询方便、添加一个List临时字段,查询的时候多的可以查询放入该List即可同理其他1对1多对多也一样,hibernate也是这样思想的嗯,你说的思路我明白,如果这样做,就要自己写一个类似Hibernate的懒加载机制,不然会死循环的 引用来自“Timco”的答案 看了看楼主的设计,感觉挺合理的啊-。-有一个问题,不知楼主能否帮忙解答 为什么 executeUpdate(StringBuildersql,Object...params)没有传入Connection对象,而 executeQuery(Connectionconn,StringBuildersql,Object...params)传入了呢? 这样设计的道理是什么? 明白,我现在数据库的dao设计都是采用了楼主这样的设计。不过executeUpdate也传人了,因为这样可以在dao层同一connection进行事物控制,禁用autoCommit,等事物完成再提交。是啊,你说的很对,所以后续扩展了这个DBHelper类,重载了几个方法,也加入了执行事务的方法,执行存储过程的方法等等。

爱吃鱼的程序员 2020-06-22 14:20:35 0 浏览量 回答数 0

问题

【精品问答】前端开发必懂之JS技术二百问

茶什i 2019-12-01 22:05:04 146 浏览量 回答数 0

问题

2018MySQL技术问答集锦,希望能给喜欢MySQL的同学一些帮助

技术小能手 2019-12-01 19:31:11 1856 浏览量 回答数 0

问题

迷你书下载 精彩片段: 恶名昭著的指针究竟是什么:报错

kun坤 2020-06-09 15:10:04 4 浏览量 回答数 1

回答

初识 MyBatis MyBatis 是第一个支持自定义 SQL、存储过程和高级映射的类持久框架。MyBatis 消除了大部分 JDBC 的样板代码、手动设置参数以及检索结果。MyBatis 能够支持简单的 XML 和注解配置规则。使 Map 接口和 POJO 类映射到数据库字段和记录。 MyBatis 的特点 那么 MyBatis 具有什么特点呢?或许我们可以从如下几个方面来描述 MyBatis 中的 SQL 语句和主要业务代码分离,我们一般会把 MyBatis 中的 SQL 语句统一放在 XML 配置文件中,便于统一维护。 解除 SQL 与程序代码的耦合,通过提供 DAO 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。SQL 和代码的分离,提高了可维护性。 MyBatis 比较简单和轻量 本身就很小且简单。没有任何第三方依赖,只要通过配置 jar 包,或者如果你使用 Maven 项目的话只需要配置 Maven 以来就可以。易于使用,通过文档和源代码,可以比较完全的掌握它的设计思路和实现。 屏蔽样板代码 MyBatis 回屏蔽原始的 JDBC 样板代码,让你把更多的精力专注于 SQL 的书写和属性-字段映射上。 编写原生 SQL,支持多表关联 MyBatis 最主要的特点就是你可以手动编写 SQL 语句,能够支持多表关联查询。 提供映射标签,支持对象与数据库的 ORM 字段关系映射 ORM 是什么?对象关系映射(Object Relational Mapping,简称ORM) ,是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。 提供 XML 标签,支持编写动态 SQL。 你可以使用 MyBatis XML 标签,起到 SQL 模版的效果,减少繁杂的 SQL 语句,便于维护。 MyBatis 整体架构 MyBatis 最上面是接口层,接口层就是开发人员在 Mapper 或者是 Dao 接口中的接口定义,是查询、新增、更新还是删除操作;中间层是数据处理层,主要是配置 Mapper -> XML 层级之间的参数映射,SQL 解析,SQL 执行,结果映射的过程。上述两种流程都由基础支持层来提供功能支撑,基础支持层包括连接管理,事务管理,配置加载,缓存处理等。 接口层 在不与Spring 集成的情况下,使用 MyBatis 执行数据库的操作主要如下: InputStream is = Resources.getResourceAsStream("myBatis-config.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); sqlSession = factory.openSession(); 其中的SqlSessionFactory,SqlSession是 MyBatis 接口的核心类,尤其是 SqlSession,这个接口是MyBatis 中最重要的接口,这个接口能够让你执行命令,获取映射,管理事务。 数据处理层 配置解析 在 Mybatis 初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configration 对象中。之后,根据该对象创建SqlSessionFactory 对象。待 Mybatis 初始化完成后,可以通过 SqlSessionFactory 创建 SqlSession 对象并开始数据库操作。 SQL 解析与 scripting 模块 Mybatis 实现的动态 SQL 语句,几乎可以编写出所有满足需要的 SQL。 Mybatis 中 scripting 模块会根据用户传入的参数,解析映射文件中定义的动态 SQL 节点,形成数据库能执行的SQL 语句。 SQL 执行 SQL 语句的执行涉及多个组件,包括 MyBatis 的四大核心,它们是: Executor、StatementHandler、ParameterHandler、ResultSetHandler。SQL 的执行过程可以用下面这幅图来表示 MyBatis 层级结构各个组件的介绍(这里只是简单介绍,具体介绍在后面): SqlSession: ,它是 MyBatis 核心 API,主要用来执行命令,获取映射,管理事务。接收开发人员提供 Statement Id 和参数。并返回操作结果。Executor :执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成以及查询缓存的维护。StatementHandler : 封装了JDBC Statement 操作,负责对 JDBC Statement 的操作,如设置参数、将Statement 结果集转换成 List 集合。ParameterHandler : 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。ResultSetHandler : 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。TypeHandler : 用于 Java 类型和 JDBC 类型之间的转换。MappedStatement : 动态 SQL 的封装SqlSource : 表示从 XML 文件或注释读取的映射语句的内容,它创建将从用户接收的输入参数传递给数据库的 SQL。Configuration: MyBatis 所有的配置信息都维持在 Configuration 对象之中。 基础支持层 反射模块 Mybatis 中的反射模块,对 Java 反射进行了很好的封装,提供了简易的 API,方便上层调用,并且对反射操作进行了一系列的优化,比如,缓存了类的 元数据(MetaClass)和对象的元数据(MetaObject),提高了反射操作的性能。 类型转换模块 Mybatis 的别名机制,能够简化配置文件,该机制是类型转换模块的主要功能之一。类型转换模块的另一个功能是实现 JDBC 类型与 Java 类型的转换。在 SQL 语句绑定参数时,会将数据由 Java 类型转换成 JDBC 类型;在映射结果集时,会将数据由 JDBC 类型转换成 Java 类型。 日志模块 在 Java 中,有很多优秀的日志框架,如 Log4j、Log4j2、slf4j 等。Mybatis 除了提供了详细的日志输出信息,还能够集成多种日志框架,其日志模块的主要功能就是集成第三方日志框架。 资源加载模块 该模块主要封装了类加载器,确定了类加载器的使用顺序,并提供了加载类文件和其它资源文件的功能。 解析器模块 该模块有两个主要功能:一个是封装了 XPath,为 Mybatis 初始化时解析 mybatis-config.xml配置文件以及映射配置文件提供支持;另一个为处理动态 SQL 语句中的占位符提供支持。 数据源模块 Mybatis 自身提供了相应的数据源实现,也提供了与第三方数据源集成的接口。数据源是开发中的常用组件之一,很多开源的数据源都提供了丰富的功能,如连接池、检测连接状态等,选择性能优秀的数据源组件,对于提供ORM 框架以及整个应用的性能都是非常重要的。 事务管理模块 一般地,Mybatis 与 Spring 框架集成,由 Spring 框架管理事务。但 Mybatis 自身对数据库事务进行了抽象,提供了相应的事务接口和简单实现。 缓存模块 Mybatis 中有一级缓存和二级缓存,这两级缓存都依赖于缓存模块中的实现。但是需要注意,这两级缓存与Mybatis 以及整个应用是运行在同一个 JVM 中的,共享同一块内存,如果这两级缓存中的数据量较大,则可能影响系统中其它功能,所以需要缓存大量数据时,优先考虑使用 Redis、Memcache 等缓存产品。 Binding 模块 在调用 SqlSession 相应方法执行数据库操作时,需要制定映射文件中定义的 SQL 节点,如果 SQL 中出现了拼写错误,那就只能在运行时才能发现。为了能尽早发现这种错误,Mybatis 通过 Binding 模块将用户自定义的Mapper 接口与映射文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。注意,在开发中,我们只是创建了 Mapper 接口,而并没有编写实现类,这是因为 Mybatis 自动为 Mapper 接口创建了动态代理对象。 MyBatis 核心组件 在认识了 MyBatis 并了解其基础架构之后,下面我们来看一下 MyBatis 的核心组件,就是这些组件实现了从 SQL 语句到映射到 JDBC 再到数据库字段之间的转换,执行 SQL 语句并输出结果集。首先来认识 MyBatis 的第一个核心组件 SqlSessionFactory 对于任何框架而言,在使用该框架之前都要经历过一系列的初始化流程,MyBatis 也不例外。MyBatis 的初始化流程如下 String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSessionFactory.openSession(); 上述流程中比较重要的一个对象就是SqlSessionFactory,SqlSessionFactory 是 MyBatis 框架中的一个接口,它主要负责的是 MyBatis 框架初始化操作 为开发人员提供SqlSession 对象 SqlSessionFactory 有两个实现类,一个是 SqlSessionManager 类,一个是 DefaultSqlSessionFactory 类 DefaultSqlSessionFactory : SqlSessionFactory 的默认实现类,是真正生产会话的工厂类,这个类的实例的生命周期是全局的,它只会在首次调用时生成一个实例(单例模式),就一直存在直到服务器关闭。 SqlSessionManager : 已被废弃,原因大概是: SqlSessionManager 中需要维护一个自己的线程池,而使用MyBatis 更多的是要与 Spring 进行集成,并不会单独使用,所以维护自己的 ThreadLocal 并没有什么意义,所以 SqlSessionManager 已经不再使用。 ####SqlSessionFactory 的执行流程 下面来对 SqlSessionFactory 的执行流程来做一个分析 首先第一步是 SqlSessionFactory 的创建 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 1 从这行代码入手,首先创建了一个 SqlSessionFactoryBuilder 工厂,这是一个建造者模式的设计思想,由 builder 建造者来创建 SqlSessionFactory 工厂 然后调用 SqlSessionFactoryBuilder 中的 build 方法传递一个InputStream 输入流,Inputstream 输入流中就是你传过来的配置文件 mybatis-config.xml,SqlSessionFactoryBuilder 根据传入的 InputStream 输入流和environment、properties属性创建一个XMLConfigBuilder对象。SqlSessionFactoryBuilder 对象调用XMLConfigBuilder 的parse()方法,流程如下。 XMLConfigBuilder 会解析/configuration标签,configuration 是 MyBatis 中最重要的一个标签,下面流程会介绍 Configuration 标签。 MyBatis 默认使用 XPath 来解析标签,关于 XPath 的使用,参见 https://www.w3school.com.cn/xpath/index.asp 在 parseConfiguration 方法中,会对各个在 /configuration 中的标签进行解析 重要配置 说一下这些标签都是什么意思吧 properties,外部属性,这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件中配置,亦可通过 properties 元素的子元素来传递。 <properties> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="root" /> </properties> 一般用来给 environment 标签中的 dataSource 赋值 <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${driver}" /> <property name="url" value="${url}" /> <property name="username" value="${username}" /> <property name="password" value="${password}" /> </dataSource> </environment> 还可以通过外部属性进行配置,但是我们这篇文章以原理为主,不会介绍太多应用层面的操作。 settings ,MyBatis 中极其重要的配置,它们会改变 MyBatis 的运行时行为。 settings 中配置有很多,具体可以参考 https://mybatis.org/mybatis-3/zh/configuration.html#settings 详细了解。这里介绍几个平常使用过程中比较重要的配置 一般使用如下配置 <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> </settings> typeAliases,类型别名,类型别名是为 Java 类型设置的一个名字。 它只和 XML 配置有关。 <typeAliases> <typeAlias alias="Blog" type="domain.blog.Blog"/> </typeAliases> 当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。 typeHandlers,类型处理器,无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。 在 org.apache.ibatis.type 包下有很多已经实现好的 TypeHandler,可以参考如下 你可以重写类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很方便的类 org.apache.ibatis.type.BaseTypeHandler, 然后可以选择性地将它映射到一个 JDBC 类型。 objectFactory,对象工厂,MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。 public class ExampleObjectFactory extends DefaultObjectFactory { public Object create(Class type) { return super.create(type); } public Object create(Class type, List constructorArgTypes, List constructorArgs) { return super.create(type, constructorArgTypes, constructorArgs); } public void setProperties(Properties properties) { super.setProperties(properties); } public boolean isCollection(Class type) { return Collection.class.isAssignableFrom(type); } } 然后需要在 XML 中配置此对象工厂 <objectFactory type="org.mybatis.example.ExampleObjectFactory"> <property name="someProperty" value="100"/> </objectFactory> plugins,插件开发,插件开发是 MyBatis 设计人员给开发人员留给自行开发的接口,MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。MyBatis 允许使用插件来拦截的方法调用包括:Executor、ParameterHandler、ResultSetHandler、StatementHandler 接口,这几个接口也是 MyBatis 中非常重要的接口,我们下面会详细介绍这几个接口。 environments,MyBatis 环境配置,MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中 使用相同的 SQL 映射。 这里注意一点,虽然 environments 可以指定多个环境,但是 SqlSessionFactory 只能有一个,为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties); databaseIdProvider ,数据库厂商标示,MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 <databaseIdProvider type="DB_VENDOR"> <property name="SQL Server" value="sqlserver"/> <property name="DB2" value="db2"/> <property name="Oracle" value="oracle" /> </databaseIdProvider> mappers,映射器,这是告诉 MyBatis 去哪里找到这些 SQL 语句,mappers 映射配置有四种方式 上面的一个个属性都对应着一个解析方法,都是使用 XPath 把标签进行解析,解析完成后返回一个 DefaultSqlSessionFactory 对象,它是 SqlSessionFactory 的默认实现类。这就是 SqlSessionFactoryBuilder 的初始化流程,通过流程我们可以看到,初始化流程就是对一个个 /configuration 标签下子标签的解析过程。 SqlSession 在 MyBatis 初始化流程结束,也就是 SqlSessionFactoryBuilder -> SqlSessionFactory 的获取流程后,我们就可以通过 SqlSessionFactory 对象得到 SqlSession 然后执行 SQL 语句了。具体来看一下这个过程‘ 在 SqlSessionFactory.openSession 过程中我们可以看到,会调用到 DefaultSqlSessionFactory 中的 openSessionFromDataSource 方法,这个方法主要创建了两个与我们分析执行流程重要的对象,一个是 Executor 执行器对象,一个是 SqlSession 对象。执行器我们下面会说,现在来说一下 SqlSession 对象 SqlSession 对象是 MyBatis 中最重要的一个对象,这个接口能够让你执行命令,获取映射,管理事务。SqlSession 中定义了一系列模版方法,让你能够执行简单的 CRUD 操作,也可以通过 getMapper 获取 Mapper 层,执行自定义 SQL 语句,因为 SqlSession 在执行 SQL 语句之前是需要先开启一个会话,涉及到事务操作,所以还会有 commit、 rollback、close 等方法。这也是模版设计模式的一种应用。 MapperProxy MapperProxy 是 Mapper 映射 SQL 语句的关键对象,我们写的 Dao 层或者 Mapper 层都是通过 MapperProxy 来和对应的 SQL 语句进行绑定的。下面我们就来解释一下绑定过程 这就是 MyBatis 的核心绑定流程,我们可以看到 SqlSession 首先调用 getMapper 方法,我们刚才说到 SqlSession 是大哥级别的人物,只定义标准(有一句话是怎么说的来着,一流的企业做标准,二流的企业做品牌,三流的企业做产品)。 SqlSession 不愿意做的事情交给 Configuration 这个手下去做,但是 Configuration 也是有小弟的,它不愿意做的事情直接甩给小弟去做,这个小弟是谁呢?它就是 MapperRegistry,马上就到核心部分了。MapperRegistry 相当于项目经理,项目经理只从大面上把握项目进度,不需要知道手下的小弟是如何工作的,把任务完成了就好。最终真正干活的还是 MapperProxyFactory。看到这段代码 Proxy.newProxyInstance ,你是不是有一种恍然大悟的感觉,如果你没有的话,建议查阅一下动态代理的文章,这里推荐一篇 (https://www.jianshu.com/p/95970b089360) 也就是说,MyBatis 中 Mapper 和 SQL 语句的绑定正是通过动态代理来完成的。 通过动态代理,我们就可以方便的在 Dao 层或者 Mapper 层定义接口,实现自定义的增删改查操作了。那么具体的执行过程是怎么样呢?上面只是绑定过程,别着急,下面就来探讨一下 SQL 语句的执行过程。 MapperProxyFactory 会生成代理对象,这个对象就是 MapperProxy,最终会调用到 mapperMethod.execute 方法,execute 方法比较长,其实逻辑比较简单,就是判断是 插入、更新、删除 还是 查询 语句,其中如果是查询的话,还会判断返回值的类型,我们可以点进去看一下都是怎么设计的。 很多代码其实可以忽略,只看我标出来的重点就好了,我们可以看到,不管你前面经过多少道关卡处理,最终都逃不过 SqlSession 这个老大制定的标准。 我们以 selectList 为例,来看一下下面的执行过程。 这是 DefaultSqlSession 中 selectList 的代码,我们可以看到出现了 executor,这是什么呢?我们下面来解释。 Executor 还记得我们之前的流程中提到了 Executor(执行器) 这个概念吗?我们来回顾一下它第一次出现的位置。 由 Configuration 对象创建了一个 Executor 对象,这个 Executor 是干嘛的呢?下面我们就来认识一下 Executor 的继承结构 每一个 SqlSession 都会拥有一个 Executor 对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为 JDBC 中 Statement 的封装版。 也可以理解为 SQL 的执行引擎,要干活总得有一个发起人吧,可以把 Executor 理解为发起人的角色。 首先先从 Executor 的继承体系来认识一下 如上图所示,位于继承体系最顶层的是 Executor 执行器,它有两个实现类,分别是BaseExecutor和 CachingExecutor。 BaseExecutor 是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配 的体现,是Executor 的默认实现,实现了大部分 Executor 接口定义的功能,降低了接口实现的难度。BaseExecutor 的子类有三个,分别是 SimpleExecutor、ReuseExecutor 和 BatchExecutor。 SimpleExecutor : 简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象) ReuseExecutor : 可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。因为每一个 SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的 Statement作用域是同一个 SqlSession。 BatchExecutor : 批处理执行器,用于将多个 SQL 一次性输出到数据库 CachingExecutor: 缓存执行器,先从缓存中查询结果,如果存在就返回之前的结果;如果不存在,再委托给Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器。 Executor 的创建和选择 我们上面提到 Executor 是由 Configuration 创建的,Configuration 会根据执行器的类型创建,如下 这一步就是执行器的创建过程,根据传入的 ExecutorType 类型来判断是哪种执行器,如果不指定 ExecutorType ,默认创建的是简单执行器。它的赋值可以通过两个地方进行赋值: 可以通过 标签来设置当前工程中所有的 SqlSession 对象使用默认的 Executor <settings> <!--取值范围 SIMPLE, REUSE, BATCH --> <setting name="defaultExecutorType" value="SIMPLE"/> </settings> 另外一种直接通过Java对方法赋值的方式 session = factory.openSession(ExecutorType.BATCH); Executor 的具体执行过程 Executor 中的大部分方法的调用链其实是差不多的,下面是深入源码分析执行过程,如果你没有时间或者暂时不想深入研究的话,给你下面的执行流程图作为参考。 我们紧跟着上面的 selectList 继续分析,它会调用到 executor.query 方法。 当有一个查询请求访问的时候,首先会经过 Executor 的实现类 CachingExecutor ,先从缓存中查询 SQL 是否是第一次执行,如果是第一次执行的话,那么就直接执行 SQL 语句,并创建缓存,如果第二次访问相同的 SQL 语句的话,那么就会直接从缓存中提取。 上面这段代码是从 selectList -> 从缓存中 query 的具体过程。可能你看到这里有些觉得类都是什么东西,我想鼓励你一下,把握重点,不用每段代码都看,从找到 SQL 的调用链路,其他代码想看的时候在看,看源码就是很容易发蒙,容易烦躁,但是切记一点,把握重点。 上面代码会判断缓存中是否有这条 SQL 语句的执行结果,如果没有的话,就再重新创建 Executor 执行器执行 SQL 语句,注意, list = doQuery 是真正执行 SQL 语句的过程,这个过程中会创建我们上面提到的三种执行器,这里我们使用的是简单执行器。 到这里,执行器所做的工作就完事了,Executor 会把后续的工作交给 StatementHandler 继续执行。下面我们来认识一下 StatementHandler 上面代码会判断缓存中是否有这条 SQL 语句的执行结果,如果没有的话,就再重新创建 Executor 执行器执行 SQL 语句,注意, list = doQuery 是真正执行 SQL 语句的过程,这个过程中会创建我们上面提到的三种执行器,这里我们使用的是简单执行器。 到这里,执行器所做的工作就完事了,Executor 会把后续的工作交给 StatementHandler 继续执行。下面我们来认识一下 StatementHandler StatementHandler 的继承结构 有没有感觉和 Executor 的继承体系很相似呢?最顶级接口是四大组件对象,分别有两个实现类 BaseStatementHandler 和 RoutingStatementHandler,BaseStatementHandler 有三个实现类, 他们分别是 SimpleStatementHandler、PreparedStatementHandler 和 CallableStatementHandler。 RoutingStatementHandler : RoutingStatementHandler 并没有对 Statement 对象进行使用,只是根据StatementType 来创建一个代理,代理的就是对应Handler的三种实现类。在MyBatis工作时,使用的StatementHandler 接口对象实际上就是 RoutingStatementHandler 对象。 BaseStatementHandler : 是 StatementHandler 接口的另一个实现类,它本身是一个抽象类,用于简化StatementHandler 接口实现的难度,属于适配器设计模式体现,它主要有三个实现类 SimpleStatementHandler: 管理 Statement 对象并向数据库中推送不需要预编译的SQL语句。PreparedStatementHandler: 管理 Statement 对象并向数据中推送需要预编译的SQL语句。CallableStatementHandler:管理 Statement 对象并调用数据库中的存储过程。 StatementHandler 的创建和源码分析 我们继续来分析上面 query 的调用链路,StatementHandler 的创建过程如下 MyBatis 会根据 SQL 语句的类型进行对应 StatementHandler 的创建。我们以预处理 StatementHandler 为例来讲解一下 执行器不仅掌管着 StatementHandler 的创建,还掌管着创建 Statement 对象,设置参数等,在创建完 PreparedStatement 之后,我们需要对参数进行处理了。 如 如果用一副图来表示一下这个执行流程的话我想是这样 这里我们先暂停一下,来认识一下第三个核心组件 ParameterHandler ParameterHandler - ParameterHandler 介绍 ParameterHandler 相比于其他的组件就简单很多了,ParameterHandler 译为参数处理器,负责为 PreparedStatement 的 sql 语句参数动态赋值,这个接口很简单只有两个方法 ParameterHandler 只有一个实现类 DefaultParameterHandler , 它实现了这两个方法。 getParameterObject: 用于读取参数setParameters: 用于对 PreparedStatement 的参数赋值ParameterHandler 的解析过程 上面我们讨论过了 ParameterHandler 的创建过程,下面我们继续上面 parameterSize 流程 这就是具体参数的解析过程了,下面我们来描述一下 下面用一个流程图表示一下 ParameterHandler 的解析过程,以简单执行器为例 我们在完成 ParameterHandler 对 SQL 参数的预处理后,回到 SimpleExecutor 中的 doQuery 方法 上面又引出来了一个重要的组件那就是 ResultSetHandler,下面我们来认识一下这个组件 ResultSetHandler - ResultSetHandler 简介 ResultSetHandler 也是一个非常简单的接口 ResultSetHandler 是一个接口,它只有一个默认的实现类,像是 ParameterHandler 一样,它的默认实现类是DefaultResultSetHandler ResultSetHandler 解析过程 MyBatis 只有一个默认的实现类就是 DefaultResultSetHandler,DefaultResultSetHandler 主要负责处理两件事 处理 Statement 执行后产生的结果集,生成结果列表 处理存储过程执行后的输出参数 按照 Mapper 文件中配置的 ResultType 或 ResultMap 来封装成对应的对象,最后将封装的对象返回即可。 其中涉及的主要对象有: ResultSetWrapper : 结果集的包装器,主要针对结果集进行的一层包装,它的主要属性有 ResultSet : Java JDBC ResultSet 接口表示数据库查询的结果。 有关查询的文本显示了如何将查询结果作为java.sql.ResultSet 返回。 然后迭代此ResultSet以检查结果。 TypeHandlerRegistry: 类型注册器,TypeHandlerRegistry 在初始化的时候会把所有的 Java类型和类型转换器进行注册。 ColumnNames: 字段的名称,也就是查询操作需要返回的字段名称 ClassNames: 字段的类型名称,也就是 ColumnNames 每个字段名称的类型 JdbcTypes: JDBC 的类型,也就是 java.sql.Types 类型 ResultMap: 负责处理更复杂的映射关系 在 DefaultResultSetHandler 中处理完结果映射,并把上述结构返回给调用的客户端,从而执行完成一条完整的SQL语句。 内容转载自:CSDN博主:cxuann 原文链接:https://blog.csdn.net/qq_36894974/article/details/104132876?depth_1-utm_source=distribute.pc_feed.none-task&request_id=&utm_source=distribute.pc_feed.none-task

问问小秘 2020-03-05 15:44:27 0 浏览量 回答数 0

问题

【Java学习全家桶】1460道Java热门问题,阿里百位技术专家答疑解惑

管理贝贝 2019-12-01 20:07:15 27612 浏览量 回答数 19

回答

" (T[]) Arrays.copyOf(elementData, size, a.getClass()); 强转可以成功是因为数组的类型本身就为a.getClass()。 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } 通过Array.newInstance(newType.getComponentType(), newLength)创建的(目标类型)数组对象。 String[] y = (String[]) x.toArray()无法强制转换是因为array的数据类型为Object[]而不是String[]。###### String 是 Object 的子类,但是 String[] 不是 Object[] 的子类,所以对于实际类型是 String 的 Object 引用是可以强转成 String 的。但是 Object[] 怎么都不能强转成 String[],只能采用个个赋值的方式,把里面的引用挨个强转再拷贝过来(当然可以用 Arrays.copyOf() 来做这个事情。###### 把子類的值轉回子類才是合法的 把Object[]的值cast成String[], 這個行為和(Integer)(new Object())同類###### ArrayList的泛型E不是必选的,当未指定泛型的时候,例如: ArrayList list = new ArrayList(); list.add("test"); list.add(123); list.add(11.22); 这种情况第二种方法是不适用的,也没办法做强制类型转换###### 提示:关键还是要看清楚源码List<Integer> list = new ArrayList<Integer>();list.add(1);1.先分析list.toArray();Integer[] newArray = (Integer[])list.toArray();//会报错list.toArray(new Integer[1]);//不会报错这个方法其实调用的是下面这个方法Arrays.copyOf(elementData, size); 而这里的elementDate是什么?ArrayList,大家应该知道ArrayList底层是数组实现的。那么问题来了,这个底层的数组是什么,里面的元素是什么类型?看一眼源码就知道,这个数组是Object[],为啥是这个啊,不是Integer[]?因为泛型会擦除啊,所以底层的数组里面的元素是Object类型,而不是Integer,只有当取出元素的时候才会强转成Integer,具体了解泛型去吧。接着Arrays.copyOf(elementData, size);这个方法会调用如下方法Arrays.copyOf(original, newLength, original.getClass());最重要的一点来了,这个方法里面有个三目运算T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); 这个三目运算意思是说,如果你给我的那个数组,是Object[]的,那么我的T[]就是Object[]的,实际就是T[] copy = new Object[1];如果你给我的不是Objcet[].class的,那么我就newInstance一个实际类型的T[],比如上面的list.toArray(new Integer[1]);实际就是:T[] copy = new Integer[1];答案已经有了啊,很明显的:T[] copy = new Object[1]; copy不能强制转换为 Integer[]; 为什么?因为如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的。T[] copy = new Integer[1]; copy能强制转换为 Integer[]; 为什么?因为如果父类引用的对象是指向子类的对象,那么在向下转型的过程中是安全的。不理解的兄弟们可以去看看java泛型的原理" ![image.png](https://ucc.alicdn.com/pic/developer-ecology/388561be30914660a2cbd3d1d239600e.png)

因为相信,所以看见。 2020-05-26 09:48:39 0 浏览量 回答数 0

问题

【精品问答】python技术1000问(1)

问问小秘 2019-12-01 21:57:48 448858 浏览量 回答数 12

回答

1.字符串转义序列转义字符 描述(在行尾时) 续行符\ 反斜杠符号' 单引号" 双引号a 响铃b 退格(Backspace)e 转义000 空n 换行v 纵向制表符t 横向制表符r 回车f 换页oyy 八进制数yy代表的字符,例如:o12代表换行xyy 十进制数yy代表的字符,例如:x0a代表换行other 其它的字符以普通格式输出 2.字符串格式化 3.操作符 一、算术运算符 注意: 双斜杠 // 除法总是向下取整。 从符点数到整数的转换可能会舍入也可能截断,建议使用math.floor()和math.ceil()明确定义的转换。 Python定义pow(0, 0)和0 ** 0等于1。 二、比较运算符 运算符 描述< 小于<= 小于或等于 大于= 大于或等于== 等于 != 不等于is 判断两个标识符是不是引用自一个对象is not 判断两个标识符是不是引用自不同对象注意: 八个比较运算符优先级相同。 Python允许x < y <= z这样的链式比较,它相当于x < y and y <= z。 复数不能进行大小比较,只能比较是否相等。 三、逻辑运算符 运算符 描述 备注x or y if x is false, then y, elsex x andy if x is false, then x, elsey not x if x is false, then True,elseFalse 注意: or是个短路运算符,它只有在第一个运算数为False时才会计算第二个运算数的值。 and也是个短路运算符,它只有在第一个运算数为True时才会计算第二个运算数的值。 not的优先级比其他类型的运算符低,所以not a == b相当于not (a == b),而 a == not b是错误的。 四、位运算符 运算符 描述 备注x | y 按位或运算符 x ^ y 按位异或运算符 x & y 按位与运算符 x << n 左移动运算符 x >> n 右移动运算符 ~x 按位取反运算符 五、赋值运算符 复合赋值运算符与算术运算符是一一对应的: 六、成员运算符 Python提供了成员运算符,测试一个元素是否在一个序列(Sequence)中。 运算符 描述in 如果在指定的序列中找到值返回True,否则返回False。not in 如果在指定的序列中没有找到值返回True,否则返回False。 4.关键字总结 Python中的关键字包括如下: and del from not while as elif global or with assert else if pass yield break except import print class exec in raise continue finally is return def for lambda try你想看看有哪些关键字?OK,打开一个终端,就像这样~ long@zhouyl:~$ pythonPython 2.7.3 (default, Jan 2 2013, 16:53:07) [GCC 4.7.2] on linux2Type "help", "copyright", "credits" or "license" for more information. import keywordkeyword.kwlist ['and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield'] ============================== 华丽的 正文分隔符 ======================================== 看到这些关键字你还能记得多少?你不妨自己一个一个对照想想它的用法,下面是我总结的,我根据前面的学习笔记将上述关键字分为以下几类: 1.判断、循环 对于Python的循环及判断主要包括这些关键字: if elif else for while break continue and or is not in 这几个关键字在前面介绍 if 语法、while语法、for语法以及and...or语法中已有介绍,下面再一笔带过: 1.1 if 语法 if语法与C语言、shell脚本之下的非常类似,最大的区别就是冒号以及严格的缩进,当然这两点也是Python区别于其他语言的地方: if condition1: do something elif condition2: do another thing else: also do something 1.2 while 语法 Python的while语法区别于C、shell下的while除了冒号及缩进之外,还有一点就是while可以携带一个可选的else语句: while condition: do something else: do something 注:else语句是可选的,但是使用while语句时一定要注意判断语句可以跳出! 1.3 for 语法 与while类似,Python的for循环也包括一个可选的else语句(跳出for循环时执行,但是如果是从break语句跳出则不执行else语句块中的代码!),而且for 加上 关键字in就组成了最常见的列表解析用法(以后会写个专门的博客)。 下面是for的一般用法: for i in range(1,10,2): do something if condition: break else: do something for的列表解析用法: for items in list: print items 1.4 and...or 语法 Python的and/or操作与其他语言不同的是它的返回值是参与判断的两个值之一,所以我们可以通过这个特性来实现Python下的 a ? b : c ! 有C语言基础的知道 “ a ? b : c ! ” 语法是判断 a,如果正确则执行b,否则执行 c! 而Python下我们可以这么用:“ a and b or c ”(此方法中必须保证b必须是True值),python自左向右执行此句,先判断a and b :如果a是True值,a and b语句仍需要执行b,而此时b是True值!所以a and b的值是b,而此时a and b or c就变成了b or c,因b是True值,所以b or c的结果也是b;如果a是False值,a and b语句的结果就是a,此时 a and b or c就转化为a or c,因为此时a是 False值,所以不管c是True 还是Flase,a or c的结果就是c!!!捋通逻辑的话,a and b or c 是不是就是Python下的a ? b : c ! 用法? 1.5 is ,not is 和 is not 是Python下判断同一性的关键字,通常用来判断 是 True 、False或者None(Python下的NULL)! 比如 if alue is True : ... (不记得本节的童鞋罚复习:python 学习笔记 2 -- 判断语句) 2.函数、模块、类 对于Python的函数及模块主要包括这些关键字: from import as def pass lambda return class 那么你还能记得它们么?下面简单介绍一下: 2.1 模块 Python的编程通常大量使用标准库中的模块,使用方法就是使用import 、from以及as 关键字。 比如: import sys # 导入sys模块 from sys import argv # 从sys模块中导入argv ,这个在前面介绍脚本传参数时使用到 import cPickle as p # 将cPickle模块导入并在此将它简单命名为p,此后直接可以使用p替代cPickle模块原名,这个在介绍文件输入输出时的存储器中使用到 2.2 函数 Python中定义函数时使用到def关键字,如果你当前不想写入真实的函数操作,可以使用pass关键字指代不做任何操作: def JustAFunction: pass 当然,在需要给函数返回值时就用到了return关键字,这里简单提一下Python下的函数返回值可以是多个(接收返回值时用相应数量的变量接收!)! 此外Python下有个神奇的Lambda函数,它允许你定义单行的最小函数,这是从Lisp中借用来的,可以用在任何需要函数的地方。比如: g = lambda x : x*2 # 定义一个Lambda函数用来计算参数的2倍并返回! print g(2) # 使用时使用lambda函数返回的变量作为这个函数的函数名,括号中带入相应参数即可! (不记得本节的童鞋罚复习:python 学习笔记 4 -- 函数篇) 3.异常 对于Python的异常主要包括这些关键字: try except finally raise 异常这一节还是比较简单的,将可能出现的异常放在 try: 后面的语句块中,使用except关键字捕获一定的异常并在接下来的语句块中做相应操作,而finally中接的是无论出现什么异常总在执行最后做finally: 后面的语句块(比如关闭文件等必要的操作!) raise关键字是在一定的情况下引发异常,通常结合自定义的异常类型使用。 (不记得本节的童鞋罚复习:python 学习笔记 6 -- 异常处理) 4.其他 上面的三类过后,还剩下这些关键字: print del global with assert yield exec 首先print 在前面的笔记或者任何地方你都能见到,所以还是比较熟悉的,此处就不多介绍了!del 关键字在前面的笔记中已有所涉及,比如删除列表中的某项,我们使用 “ del mylist[0] ” 可能这些剩下来的关键字你比较陌生,所以下面来介绍一下: 4.1.global 关键字 当你在函数定义内声明变量的时候,它们与函数外具有相同名称的其他变量没有任何关系,即变量名称对于函数来说是 局部 的。这称为变量的 作用域 。所有变量的作用域是它们被定义的块,从它们的名称被定义的那点开始。 eg. ? 1 2 3 4 5 6 7 8 9 10 11 !/usr/bin/python Filename: func_local.py def func(x): print'x is', x x = 2 print'Changed local x to', x x = 50 func(x) print'x is still', x 运行的结果是这样的:? 1 2 3 4 $ python func_local.py x is 50 # 运行func函数时,先打印x的值,此时带的值是作为参数带入的外部定义的50,所以能正常打印 x=50 Changed local x to 2 # 在func函数中将x赋2,并打印 x is still 50 # 运行完func函数,打印x的值,此时x的值仍然是之前赋给的50,而不是func函数中修改过的2,因为在函数中修改的只是函数内的局部变量 那么为什么我们要在这提到局部变量呢?bingo,聪明的你一下就猜到这个global就是用来定义全局变量的。也就是说如果你想要为一个在函数外定义的变量赋值,那么你就得告诉Python这个变量名不是局部的,而是 全局 的。我们使用global语句完成这一功能。没有global语句,是不可能为定义在函数外的变量赋值的。eg.? 1 2 3 4 5 6 7 8 9 10 11 12 !/usr/bin/python Filename: func_global.py def func(): global x print'x is', x x = 2 print'Changed local x to', x x = 50 func() print'Value of x is', x 运行的结果是这样的:? 1 2 3 4 $ python func_global.py x is 50 Changed global x to 2 Value of x is 2 # global语句被用来声明x是全局的——因此,当我们在函数内把值赋给x的时候,这个变化也反映在我们在主块中使用x的值的时候。 你可以使用同一个global语句指定多个全局变量。例如global x, y, z。 4.2.with 关键字 有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python的with语句提供了一种非常方便的处理方式。一个很好的例子是文件处理,你需要获取一个文件句柄,从文件中读取数据,然后关闭文件句柄。如果不用with语句,打开一个文件并读文件的代码如下:? 1 2 3 file = open("/tmp/foo.txt") data = file.read() file.close() 当然这样直接打开有两个问题:一是可能忘记关闭文件句柄;二是文件读取数据发生异常,没有进行任何处理。下面是添加上异常处理的版本:? 1 2 3 4 5 file = open("/tmp/foo.txt") try: data = file.read() finally: file.close() 虽然这段代码运行良好,但是太冗余了。这时候就是with一展身手的时候了。除了有更优雅的语法,with还可以很好的处理上下文环境产生的异常。下面是with版本的代码:? 1 2 with open("/tmp/foo.txt") as file: data = file.read() 这看起来充满魔法,但不仅仅是魔法,Python对with的处理还很聪明。基本思想是with所求值的对象必须有一个__enter__()方法,一个__exit__()方法。with语句的执行逻辑如下:紧跟with后面的语句被求值后,返回对象的__enter__()方法被调用,这个方法的返回值将被赋值给as后面的变量。当with后面的代码块全部被执行完之后,将调用前面返回对象的__exit__()方法。 下面例子可以具体说明with如何工作:? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 !/usr/bin/python with_example01.py classSample: def __enter__(self): print"In __enter__()" return"Foo" def __exit__(self, type, value, trace): print"In __exit__()" def get_sample(): returnSample() with get_sample() as sample: print"sample:", sample 运行代码,输出如下? 1 2 3 4 $python with_example01.py In __enter__() # __enter__()方法被执行 sample: Foo # __enter__()方法返回的值 - 这个例子中是"Foo",赋值给变量'sample',执行代码块,打印变量"sample"的值为"Foo" In __exit__() # __exit__()方法被调用 4.3.assert 关键字 assert语句是一种插入调试断点到程序的一种便捷的方式。assert语句用来声明某个条件是真的,当assert语句失败的时候,会引发一AssertionError,所以结合try...except我们就可以处理这样的异常。 mylist # 此时mylist是有三个元素的列表['a', 'b', 'c']assert len(mylist) is not None # 用assert判断列表不为空,正确无返回assert len(mylist) is None # 用assert判断列表为空 Traceback (most recent call last): File "", line 1, in AssertionError # 引发AssertionError异常 4.4.yield 关键字 我们先看一个示例:? 1 2 3 4 5 6 7 8 def fab(max): n, a, b = 0,0,1 whilen < max: yield b # print b a, b = b, a + b n = n + 1 ''' 使用这个函数:? 1 2 3 4 5 6 7 8 forn in fab(5): ... print n ... 1 1 2 3 5 简单地讲,yield 的作用就是把一个函数变成一个 generator(生成器),带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable(可迭代的)对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 f = fab(5) f.next() 1 f.next() 1 f.next() 2 f.next() 3 f.next() 5 f.next() Traceback (most recent call last): File"", line 1, in StopIteration 当函数执行结束时,generator 自动抛出 StopIteration 异常,表示迭代完成。在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。 我们可以得出以下结论:一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。 yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。 注:如果看完此段你还未明白yield,没问题,因为yield是初学者的一个难点,那么你下一步需要做的就是……看一看下面参考资料中给的关于yield的博文! 4.5.exec 关键字 官方文档对于exec的解释: "This statement supports dynamic execution of Python code."也就是说使用exec可以动态执行Python代码(也可以是文件)。? 1 2 3 4 5 6 7 8 9 10 11 12 13 longer = "print "Hello World ,my name is longer"" # 比如说我们定义了一个字符串 longer 'print "Hello World ,my name is longer"' exec(longer) # 使用exec 动态执行字符串中的代码 Hello World ,my name is longer exec(sayhi) # 使用exec直接打开文件名(指定sayhi,sayhi.py以及"sayhi.py"都会报一定的错,但是我觉得直接带sayhi报错非常典型) Traceback (most recent call last): File"", line 1, in TypeError: exec: arg 1must be a string, file, or code object # python IDE报错,提示exec的第一个参 数必须是一个字符串、文件或者一个代码对象 f = file("sayhi.py") # 使用file打开sayhi.py并创建f实例 exec(f) # 使用exec直接运行文件描述符f,运行正常!! Hi,thisis [''] script 上述给的例子比较简单,注意例子中exec语句的用法和eval_r(), execfile()是不一样的. exec是一个关键字(要不然我怎么会在这里介绍呢~~~), 而eval_r()和execfile()则是内建函数。更多关于exec的使用请详看引用资料或者Google之 在需要在字符中使用特殊字符时,python用反斜杠()转义字符。 原始字符串 有时我们并不想让转义字符生效,我们只想显示字符串原来的意思,这就要用r和R来定义原始字符串。如: print r’tr’ 实际输出为“tr”。 转义字符 描述 (在行尾时) 续行符 反斜杠符号 ’ 单引号 ” 双引号 a 响铃 b 退格(Backspace) e 转义 000 空 n 换行 v 纵向制表符 t 横向制表符 r 回车 f 换页 oyy 八进制数yy代表的字符,例如:o12代表换行 xyy 十进制数yy代表的字符,例如:x0a代表换行 other 其它的字符以普通格式输出

xuning715 2019-12-02 01:10:21 0 浏览量 回答数 0

回答

1、了解视频面试的有效交流成分 面试者可先试演盯着摄像头说话,让对方有一种面谈的感觉;增加一些无伤大雅的微动作,比如点头赞同对方;以及找到自己最适合视频说话的语调和语速,这些将会缩小与面试官的距离感。 2、熟悉面试平台的操作流程 可以使用一下自己常用的招聘APP,查找一下平台视频面试流程的详细说明 3、做好个人面试前的准备 天下大事,必作于细。除了对视频面试和面试平台的了解,个人的准备也是事关重要的。 - [1]个人的形象准备。 虽然是线上的视频面试,但还是可以看到彼此,我们都需要做好准备。比如面试官在国外的下午进行视频面试,国内刚好是晚上,如果此时一身家居服的你与面试官视频,对方难以感受到尊重。所以,无论任何时间点,符合面试的正式服装并且穿戴整齐,才能将专业度传递给面试官。 - [2]室内场所的选择。 选择一个安静的没有干扰的地方,视频区域整洁没有多余的杂物;灯光明亮,避免人像曝光,面试官可清晰看到你;确保坐的椅子舒适,利于自己在面试过程中精神保持专注。 - [3]个人设备和网络。 确认手机电量充足,对应的相机和麦克风功能可以正常使用;关闭任何会发出提示音的设备,避免面试中收到干扰;测试设备和网络是否能正常使用,减少面试中出现断网等低级错误。疫情未止,但这不会成为找工作面试的阻碍,在疫情期间做好面试的充足准备,提高线上面试的重视度,即便现场出现突发状况,镇静并且及时与对方沟通,商量解决方案,一切都能迎刃而解。总之,只要做好十足的准备,确保一切都是最佳状态,即便从未经历过视频面试的你,也能脱颖而出。 面试某技术岗位,事先练习面试题 比如Python,小编为大家精心准备了以下面试题 1.Python是如何进行内存管理的? 答:从三个方面来说,一对象的引用计数机制,二垃圾回收机制,三内存池机制 一、对象的引用计数机制 Python内部使用引用计数,来保持追踪内存中的对象,所有对象都有引用计数。 引用计数增加的情况: - 1,一个对象分配一个新名称 - 2,将其放入一个容器中(如列表、元组或字典) 引用计数减少的情况: - 1,使用del语句对对象别名显示的销毁 - 2,引用超出作用域或被重新赋值 sys.getrefcount( )函数可以获得对象的当前引用计数 多数情况下,引用计数比你猜测得要大得多。对于不可变数据(如数字和字符串),解释器会在程序的不同部分共享内存,以便节约内存。 二、垃圾回收 - 1,当一个对象的引用计数归零时,它将被垃圾收集机制处理掉。 - 2,当两个对象a和b相互引用时,del语句可以减少a和b的引用计数,并销毁用于引用底层对象的名称。然而由于每个对象都包含一个对其他对象的应用,因此引用计数不会归零,对象也不会销毁。(从而导致内存泄露)。为解决这一问题,解释器会定期执行一个循环检测器,搜索不可访问对象的循环并删除它们。 三、内存池机制 Python提供了对内存的垃圾收集机制,但是它将不用的内存放到内存池而不是返回给操作系统。 - 1,Pymalloc机制。为了加速Python的执行效率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。 - 2,Python中所有小于256个字节的对象都使用pymalloc实现的分配器,而大的对象则使用系统的malloc。 - 3,对于Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。 2.什么是lambda函数?它有什么好处? 答:lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数 lambda函数:首要用途是指点短小的回调函数 lambda [arguments]:expression a=lambdax,y:x+y a(3,11) 3.Python里面如何实现tuple和list的转换? 答:直接使用tuple和list函数就行了,type()可以判断对象的类型 4.请写出一段Python代码实现删除一个list里面的重复元素 答: - 1,使用set函数,set(list) - 2,使用字典函数, a=[1,2,4,2,4,5,6,5,7,8,9,0] b={} b=b.fromkeys(a) c=list(b.keys()) c 5.编程用sort进行排序,然后从最后一个元素开始判断 a=[1,2,4,2,4,5,7,10,5,5,7,8,9,0,3] a.sort() last=a[-1] for i inrange(len(a)-2,-1,-1): if last==a[i]: del a[i] else:last=a[i] print(a) 6.Python里面如何拷贝一个对象?(赋值,浅拷贝,深拷贝的区别) 答:赋值(=),就是创建了对象的一个新的引用,修改其中任意一个变量都会影响到另一个。 浅拷贝:创建一个新的对象,但它包含的是对原始对象中包含项的引用(如果用引用的方式修改其中一个对象,另外一个也会修改改变){1,完全切片方法;2,工厂函数,如list();3,copy模块的copy()函数} 深拷贝:创建一个新的对象,并且递归的复制它所包含的对象(修改其中一个,另外一个不会改变){copy模块的deep.deepcopy()函数} 7.介绍一下except的用法和作用? 答:try…except…except…[else…][finally…] 执行try下的语句,如果引发异常,则执行过程会跳到except语句。对每个except分支顺序尝试执行,如果引发的异常与except中的异常组匹配,执行相应的语句。如果所有的except都不匹配,则异常会传递到下一个调用本代码的最高层try代码中。 try下的语句正常执行,则执行else块代码。如果发生异常,就不会执行 如果存在finally语句,最后总是会执行。 8.Python中pass语句的作用是什么? 答:pass语句不会执行任何操作,一般作为占位符或者创建占位程序,whileFalse:pass 9.介绍一下Python下range()函数的用法? 答:列出一组数据,经常用在for in range()循环中 10.如何用Python来进行查询和替换一个文本字符串? 答:可以使用re模块中的sub()函数或者subn()函数来进行查询和替换, 格式:sub(replacement, string[,count=0])(replacement是被替换成的文本,string是需要被替换的文本,count是一个可选参数,指最大被替换的数量) import re p=re.compile(‘blue|white|red’) print(p.sub(‘colour’,'blue socks and red shoes’)) colour socks and colourshoes print(p.sub(‘colour’,'blue socks and red shoes’,count=1)) colour socks and redshoes subn()方法执行的效果跟sub()一样,不过它会返回一个二维数组,包括替换后的新的字符串和总共替换的数量 11.Python里面match()和search()的区别? 答:re模块中match(pattern,string[,flags]),检查string的开头是否与pattern匹配。 re模块中research(pattern,string[,flags]),在string搜索pattern的第一个匹配值。 print(re.match(‘super’, ‘superstition’).span()) (0, 5) print(re.match(‘super’, ‘insuperable’)) None print(re.search(‘super’, ‘superstition’).span()) (0, 5) print(re.search(‘super’, ‘insuperable’).span()) (2, 7) 12.用Python匹配HTML tag的时候,<.>和<.?>有什么区别? 答:术语叫贪婪匹配( <.> )和非贪婪匹配(<.?> ) 例如: test <.> : test <.?> : 13.Python里面如何生成随机数? 答:random模块 随机整数:random.randint(a,b):返回随机整数x,a<=x<=b random.randrange(start,stop,[,step]):返回一个范围在(start,stop,step)之间的随机整数,不包括结束值。 随机实数:random.random( ):返回0到1之间的浮点数 random.uniform(a,b):返回指定范围内的浮点数。 14.有没有一个工具可以帮助查找python的bug和进行静态的代码分析? 答:PyChecker是一个python代码的静态分析工具,它可以帮助查找python代码的bug, 会对代码的复杂度和格式提出警告 Pylint是另外一个工具可以进行codingstandard检查 15.如何在一个function里面设置一个全局的变量? 答:解决方法是在function的开始插入一个global声明: def f() global x 16.单引号,双引号,三引号的区别 答:单引号和双引号是等效的,如果要换行,需要符号(),三引号则可以直接换行,并且可以包含注释 如果要表示Let’s go 这个字符串 单引号:s4 = ‘Let\’s go’ 双引号:s5 = “Let’s go” s6 = ‘I realy like“python”!’ 这就是单引号和双引号都可以表示字符串的原因了 最后小编祝福大家能在2020年找到心仪的工作哈

剑曼红尘 2020-03-12 16:06:50 0 浏览量 回答数 0

回答

HashMap HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有 不同 其实1.7一个很明显需要优化的地方就是: 当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效 率就会越来越低;时间复杂度为 O(N)。 因此 1.8 中重点优化了这个查询效率。 1.8 HashMap 结构图 JDK 1.8 对 HashMap 进行了修改: 最大的不同就是利用了红黑树,其由数组+链表+红黑树组成。 JDK 1.7 中,查找元素时,根据 hash 值能够快速定位到数组的具体下标, 但之后需要顺着链表依次比较才能查找到需要的元素,时间复杂度取决于链 表的长度,为 O(N)。 为了降低这部分的开销,在 JDK 1.8 中,当链表中的元素超过 8 个以后,会 将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。 JDK 1.8 使用 Node(1.7 为 Entry) 作为链表的数据结点,仍然包含 key, value,hash 和 next 四个属性。 红黑树的情况使用的是 TreeNode。 根据数组元素中,第一个结点数据类型是 Node 还是 TreeNode 可以判断该位 置下是链表还是红黑树。 核心成员变量于 1.7 类似,增加了核心变量,如下表。 属性说明TREEIFY_THRESHOLD用于判断是否需要将链表转换为红黑树的阈值,默认 为 8。 put步骤: 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始 化)。 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进 行赋值及返回。 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的 后面(形成链表)。 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。 如果在遍历过程中找到 key 相同时直接退出遍历。 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。 后判断是否需要进行扩容. get 方法看起来就要简单许多了。 首先将 key hash 之后取得所定位的桶。 如果桶为空则直接返回 null 。 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是 就直接返回 value。 如果第一个不匹配,则判断它的下一个是红黑树还是链表。 红黑树就按照树的查找方式返回值。 不然就按照链表的方式遍历匹配返回值。 从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之 后查询效率直接提高到了 O(logn)。 但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。 但是为什么呢?简单分析下。 看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并 发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。 如下图: HashTable HashTable 容器使用 synchronized来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效 率非常低下。 当一个线程访问 HashTable 的同步方法时,其他线程访问 HashTable 的同步方 法可能会进入阻塞或轮询状态。 HashTable 容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有 访问它的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容 器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就 不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap(JDK 1.7) 使用的 锁分段技术。 ConcurrentHashMap 将数据分成一段一段的存储,然后给每一段数据配一把 锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他 线程访问。 有些方法需要跨段,比如 size() 和 containsValue(),它们可能需要锁定整个表 而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所 有段的锁。 按顺序 很重要,否则极有可能出现死锁,在 ConcurrentHashMap 内部,段数 组是 final 的,并且其成员变量实际也是 final 的,但是,仅仅是将数组声明为 final 的并不保证数组成员也是 final 的,需要实现上的保证。这可以确保不会 出现死锁,因为获得锁的顺序是固定的。 HashTable 的迭代器是强一致性的,而 ConcurrentHashMap 是弱一致的。 ConcurrentHashMap 的 get,clear,iterator 方法都是弱一致性的。 初识ConcurrentHashMap Concurrent翻译过来是并发的意思,字面理解它的作用是处理并发情况的 HashMap。 通过前面的学习,我们知道多线程并发下 HashMap 是不安全的(如死循环),更普遍 的是多线程并发下,由于堆内存对于各个线程是共享的,而 HashMap 的 put 方法 不是原子操作,假设Thread1先 put 值,然后 sleep 2秒(也可以是系统时间片切换失 去执行权),在这2秒内值被Thread2改了,Thread1“醒来”再 get 的时候发现已经不 是原来的值了,这就容易出问题。 那么如何避免这种多线程出错的情况呢? 常规思路就是给 HashMap 的 put 方法加锁(synchronized),保证同一个时刻只允 许一个线程拥有对 hashmap 有写的操作权限即可。然而假如线程1中操作耗时,其 他需要操作该 hashmap 的线程就需要在门口排队半天,严重影响用户体验, HashTable 就是这样子做的。 举个生活中的例子,很多银行除了存取钱,还支持存取贵重物品,贵重物品都放在 保险箱里,把 HashMap 和 HashTable 比作银行,结构: 把线程比作人,对应的情况如下: 多线程下用 HashMap 不确定性太高,有破产的风险,不能选;用 HashTable 不会 破产,但是用户体验不太好,那么怎样才能做到多人存取既不影响他人存值,又不 用排队呢? 有人提议搞个「银行者联盟」,多开几个像HashTable 这种「带锁」的银行就好 了,有多少人办理业务,就开多少个银行,一对一服务,这个区都是大老板,开银 行的成本都是小钱,于是「银行者联盟」成立了。 接下来的情况是这样的:比如用户A和用户B一起去银行存各自的项链,这个「银行 者联盟」操作后,然后对用户A说,1号银行现在没人你可以去那存,不用排队,然 后用户A就去1号银行存项链,1号银行把用户A接进门,马上拉闸,然后把用户A的 项链放在第x行第x个保险箱,等用户A办妥离开后,再开闸;对于用户B同理。此时 不管用户A和用户B在各自银行里面待多久都不会影响到彼此,不用担心自己的项链 被人偷换了。这就是ConcurrentHashMap的设计思路,用一个图来理解 从上图可以看出,此时锁的是对应的单个银行,而不是整个「银行者联盟」。分析 下这种设计的特点: 多个银行组成的「银行者联盟」 当有人来办理业务时,「银行者联盟」需要确定这个人去哪个银行 当此人去到指定银行办理业务后,该银行上锁,其他人不能同时执行修改操作,直 到此人离开后解锁. ConcurrentHashMap源码解析 ConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。 先来看看 1.7 的实现,下面是结构图: 如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组 加链表。主要是通过分段锁实现的。 关于分段锁 段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段, 当每个Segment越来越大时,锁的粒度就变得有些大了。 分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步 synchronized是有优势的。 缺点在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争 同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当 某个段很大时,分段锁的性能会下降。 1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存 在 HashMap 在 1.7 版本中的问题。 那就是查询遍历链表效率太低。 因此 1.8 做了一些数据结构上的调整。 首先来看下底层的组成结构: 其实和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,也是会转换成红 黑树的,大体结构也是一样的。 那么 JDK 1.8 ConcurrentHashMap 到底是如何实现线程安全的? 答案:其中抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证 并发安全性。(cas:比较并替换) **① 基本组成 ** 抛弃了 JDK 1.7 中原有的 Segment 分段锁,而采用了 CAS + synchronized 来 保证并发安全性。 将JDK 1.7 中存放数据的 HashEntry 改为 Node,但作用是相同的。、 我们来看看 ConcurrentHashMap 的几个重要属性. 重要组成元素 Node:链表中的元素为 Node 对象。他是链表上的一个节点,内部存储了 key、 value 值,以及他的下一 个节点的引用。这样一系列的 Node 就串成一串,组成一 个链表。 ForwardingNode:当进行扩容时,要把链表迁移到新的哈希表,在做这个操作 时,会在把数组中的头节点替换为 ForwardingNode 对象。ForwardingNode 中不 保存 key 和 value,只保存了扩容后哈希表 (nextTable)的引用。此时查找相应 node 时,需要去 nextTable 中查找。 TreeBin:当链表转为红黑树后,数组中保存的引用为 TreeBin,TreeBin 内部不保 存 key/value,他保存了 TreeNode 的 list 以及红黑树 root。 TreeNode:红黑树的节点。 **② put 方法过程 ** 存储结构定义了容器的 “形状”,那容器内的东西按照什么规则来放呢?换句话讲, 某个 key 是按 照什么逻辑放入容器的对应位置呢? 我们假设要存入的 key 为对象 x,这个过程如下 : 1、通过对象 x 的 hashCode () 方法获取其 hashCode; 2、将 hashCode 映射到数组的某个位置上; 3、把该元素存储到该位置的链表中。 put 方法用来把一个键值对存储到 map 中。代码如下: 实际调用的是 putVal 方 法,第三个参数传入 false,控制 key 存在时覆盖原来的值。 请先看完代码注释,有个大致的了解,然后我们更加详细的学习一下: 判断存储的 key、value 是否为空,若为空,则抛出异常,否则,进入步骤 2。 计算 key 的 hash 值,随后进入自旋,该自旋可以确保成功插入数据,若 table 表为空或者长度为 0,则初始化 table 表,否则,进入步骤 3。 根据 key 的 hash 值取出 table 表中的结点元素,若取出的结点为空(该桶为 空),则使用 CAS 将 key、value、hash 值生成的结点放入桶中。否则,进入 步骤 4。 若该结点的的 hash 值为 MOVED(-1),则对该桶中的结点进行转移,否则, 进入步骤 5。 5 . 对桶中的第一个结点(即 table 表中的结点)进行加锁,对该桶进行遍历,桶中 的结点的 hash 值与 key 值与给定的 hash 值和 key 值相等,则根据标识选择是 否进行更新操作(用给定的 value 值替换该结点的 value 值),若遍历完桶仍 没有找到 hash 值与 key 值和指定的 hash 值与 key 值相等的结点,则直接新生 一个结点并赋值为之前后一个结点的下一个结点。进入步骤 6。 若 binCount 值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储, 后,增加 binCount 的值。 如果桶中的第一个元素的 hash 值大于 0,说明是链表结构,则对链表插入或者 更新。 如果桶中的第一个元素是 TreeBin,说明是红黑树结构,则按照红黑树的方式进 行插入或者更新。 在锁的保护下,插入或者更新完毕后,如果是链表结构,需要判断链表中元素 的数量是否超过 8(默认),一旦超过,就需要考虑进行数组扩容,或者是链表 转红黑树。 扩容 什么时候会扩容? 使用put()添加元素时会调用addCount(),内部检查sizeCtl看是否需要扩容。 tryPresize()被调用,此方法被调用有两个调用点: 链表转红黑树(put()时检查)时如果table容量小于64(MIN_TREEIFY_CAPACITY),则会 触发扩容。 调用putAll()之类一次性加入大量元素,会触发扩容。 addCount() addCount()与tryPresize()实现很相似,我们先以addCount()分析下扩容逻辑: **1.链表转红黑树 ** 首先我们要理解为什么 Map 需要扩容,这是因为我们采用哈希表存储数据,当固定 大小的哈希表存 储数据越来越多时,链表长度会越来越长,这会造成 put 和 get 的 性能下降。此时我们希望哈希表中多一些桶位,预防链表继续堆积的更长。 ConcurrentHashMap 有链表转红黑树的操作,以提高查找的速度,红黑树时间复 杂度为 O (logn),而链表是 O (n/2),因此只在 O (logn)<O (n/2) 时才会进行转换, 也就是以 8 作为分界点。 接下来我们分析 treeifyBin 方法代码,这个代码中会选择是把此时保存数据所在的 链表转为红黑树,还是对整个哈希表扩容。 treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容。 构造完TreeBin这个空节点之后,就开始构造红黑树,首先是第一个节点,左右 子节点设置为空,作为红黑树的root节点,设置为黑色,父节点为空。 然后在每次添加完一个节点之后,都会调用balanceInsertion方法来维持这是一 个红黑树的属性和平衡性。红黑树所有操作的复杂度都是O(logn),所以当元素量比 较大的时候,效率也很高。 **数组扩容 ** 我们大致了解了 ConcurrentHashMap 的存储结构,那么我们思考一个问题,当数 组中保存的链表越来越多,那么再存储进来的元素大概率会插入到现有的链表中, 而不是使用数组中剩下的空位。 这样会造成数组中保存的链表越来越长,由此导致 哈希表查找速度下降,从 O (1) 慢慢趋近于链表 的时间复杂度 O (n/2),这显然违背 了哈希表的初衷。 所以 ConcurrentHashMap 会做一个操作, 称为扩容。也就是把数组长度变大,增 加更多的空位出来,终目的就是预防链表过长,这样查找的时间复杂度才会趋向于 O (1)。扩容的操作并不会在数组没有空位时才进行,因为在桶位快满时, 新保存元 素更大的概率会命中已经使用的位置,那么可能后几个桶位很难被使用,而链表却 越来 越长了。ConcurrentHashMap 会在更合适的时机进行扩容,通常是在数组中 75% 的位置被使用 时。 其实以上内容和 HashMap 类似,ConcurrentHashMap 此外提供了线程安全的保 证,它主要是通 过 CAS 和 Synchronized 关键字来实现,我们在源码分析中再详细 来看。 我们做一下总结: 1、ConcurrentHashMap 采用数组 + 链表 + 红黑树的存储结构; 2、存入的 Key 值通过自己的 hashCode 映射到数组的相应位置; 3、ConcurrentHashMap 为保障查询效率,在特定的时候会对数据增加长度,这个 操作叫做扩容; 4、当链表长度增加到 8 时,可能会触发链表转为红黑树(数组长度如果小于 64, 优先扩容,具体 看后面源码分析)。 接下来,我们的源码分析就从 ConcurrentHashMap 的构成、保存元素、哈希算 法、扩容、查找数 据这几个方面来进行 扩容后数组容量为原来的 2 倍。 **数据迁移( 扩容时的线程安全) ** ConcurrentHashMap 的扩容时机和 HashMap 相同,都是在 put 方法的后一步 检查是否需要扩容,如果需要则进行扩容,但两者扩容的过程完全不同, ConcurrentHashMap 扩容的方法叫做 transfer,从 put 方法的 addCount 方法进 去,就能找到 transfer 方法,transfer 方法的主要思路是: 首先需要把老数组的值全部拷贝到扩容之后的新数组上,先从数组的队尾开始 拷贝; 拷贝数组的槽点时,先把原数组槽点锁住,保证原数组槽点不能操作,成功拷 贝到新数组时,把 原数组槽点赋值为转移节点; 这时如果有新数据正好需要 put 到此槽点时,发现槽点为转移节点,就会一直 等待,所以在扩容完成之前,该槽点对应的数据是不会发生变化的; 从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组中的节点设置成转移 节点; 直到所有数组数据都拷贝到新数组时,直接把新数组整个赋值给数组容器,拷 贝完成 putTreeVal()与此方法遍历方式类似不再介绍。  ④ get 方法过程 ConcurrentHashMap 读的话,就比较简单,先获取数组的下标,然后通过判断数 组下标的 key 是 否和我们的 key 相等,相等的话直接返回,如果下标的槽点是链表 或红黑树的话,分别调用相应的 查找数据的方法,整体思路和 HashMap 很像,源 码如下: 计算 hash 值。 根据 hash 值找到数组对应位置: (n – 1) & h。 根据该位置处结点性质进行相应查找。 如果该位置为 null,那么直接返回 null。 如果该位置处的结点刚好就是需要的,返回该结点的值即可。 如果该位置结点的 hash 值小于 0,说明正在扩容,或者是红黑树。 如果以上 3 条都不满足,那就是链表,进行遍历比对即可。 ** 初始化数组 ** 数组初始化时,首先通过自旋来保证一定可以初始化成功,然后通过 CAS 设置 SIZECTL 变量的值,来保证同一时刻只能有一个线程对数组进行初始化,CAS 成功 之后,还会再次判断当前数组是否已经初始化完成,如果已经初始化完成,就不会 再次初始化,通过自旋 + CAS + 双重 check 等 手段保证了数组初始化时的线程安 全,源码如下: 里面有个关键的值 sizeCtl,这个值有多个含义。 1、-1 代表有线程正在创建 table; 2、-N 代表有 N-1 个线程正在复制 table; 3、在 table 被初始化前,代表 根据构造函数传入的值计算出的应被初始化的大小; 4、在 table 被初始化后,则被 设置为 table 大小 的 75%,代表 table 的容量(数组容量)。 initTable 中使用到 1 和 4,2 和 3 在其它方法中会有使用。下面我们可以先看下 ConcurrentHashMap 的构造方法,里面会使用上面的 3 最后来回顾总结下HashMap和ConcurrentHashMap对比 ConcurrentHashMap 和 HashMap 两者的相同之处: 1.数组、链表结构几乎相同,所以底层对数据结构的操作思路是相同的(只是思路 相同,底层实现 不同); 2.都实现了 Map 接口,继承了 AbstractMap 抽象类,所以大多数的方法也都是相 同的, HashMap 有的方法,ConcurrentHashMap 几乎都有,所以当我们需要从 HashMap 切换到 ConcurrentHashMap 时,无需关心两者之间的兼容问题 不同点: 1.红黑树结构略有不同,HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属 性,还维护着红黑树的结构,比如说查找,新增等等; ConcurrentHashMap 中红黑树被拆分成 两块,TreeNode 仅仅维护的属性和查找 功能,新增了 TreeBin,来维护红黑树结构,并负责根 节点的加锁和解锁; 2.新增 ForwardingNode (转移)节点,扩容的时候会使用到,通过使用该节点, 来保证扩容时的线程安全。

剑曼红尘 2020-03-25 11:21:44 0 浏览量 回答数 0

回答

核心是利用ES5的Object.defineProperty,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。 Object.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 Object.defineProperty( obj, // 定义属性的对象 prop, // 要定义或修改的属性的名称 descriptor, // 将被定义或修改属性的描述符【核心】 observe的功能就是用来监测数据的变化。实现方式是给非VNode的对象类型数据添加一个Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个Observer对象实例。 Observer是一个类,它的作用是给对象属性添加getter和setter,用于 依赖收集 和 派发更新 依赖收集getter(重点关注以下两点) *const dep = new Dep() // 实例化一个Dep实例*在get函数中通过dep.depend做依赖收集 Dep是一个Class,它定义了一些属性和方法,它有一个静态属性target,这是一个全局唯一Watcher【同一时间内只能有一个全局的Watcher被计算】。Dep实际上就是对Watcher的一种管理,Dep脱离Watcher单独存在是没有意义的。Watcher和Dep就是典型的观察者设计模式。 Watcher是一个Class,在它的构造函数中定义了一些和Dep相关的属性: this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() 收集过程:当我们实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行他的this.get()方法,进入get函数把Dep.target赋值为当前渲染watcher并压栈(为了恢复用)。接着执行vm._render()方法,生成渲染VNode,并且在这个过程对vm上的数据访问,这个时候就触发数据对象的getter(在此期间执行Dep.target.addDep(this)方法,将watcher订阅到这个数据持有的dep的subs中,为后续数据变化时通知到哪些subs做准备)。然后递归遍历添加所有子项的getter。 Watcher在构造函数中初始化两个Dep实例数组。newDeps代表新添加的Dep实例数组,deps代表上一次添加的Dep实例数组。 依赖清空:在执行清空依赖(cleanupDeps)函数时,会首先遍历deps,移除对dep的订阅,然后把newDepsIds和depIds交换,newDeps和deps交换,并把newDepIds和newDeps清空。考虑场景,在条件渲染时,及时对不需要渲染数据的订阅移除,减少性能浪费。 考虑到Vue是数据驱动的,所以每次数据变化都会重写Render,那么vm._render()方法会再次执行,并再次触发数据 收集依赖的目的是为了当这些响应式数据发生变化,触发它们的setter的时候,能知道应该通知哪些订阅者去做相应的逻辑处理【派发更新】 派发更新setter(重点关注以下两点) *childOb = !shallow && observe(newVal) // 如果shallow为false的情况,会对新设置的值变成一个响应式对象*dep.notify() // 通知所有订阅者 派发过程:当我们组件中对响应的数据做了修改,就会触发setter的逻辑,最后调用dep.notify()方法,它是Dep的一个实例方法。具体做法是遍历依赖收集中建立的subs,也就是Watcher的实例数组【subs数组在依赖收集getter中被添加,期间通过一些逻辑处理判断保证同一数据不会被添加多次】,然后调用每一个watcher的update方法。 update函数中有个queueWatcher(this)方法引入了队列的概念,是vue在做派发更新时优化的一个点,它并不会每次数据改变都会触发watcher回调,而是把这些watcher先添加到一个队列中,然后在nextTick后执行watcher的run函数 队列排序保证: 组件的更新由父到子。父组件创建早于子组件,watcher的创建也是用户自定义watcher要早于渲染watcher执行,因为用户自定义watcher是在渲染watcher前创建的如果一个组件在父组件watcher执行期间被销毁,那么它对应的watcher执行都可以被跳过,所以父组件的watcher应该先执行。 队列遍历:排序完成后,对队列进行遍历,拿到对应的watcher,执行watcher.run()。 run函数解析:先通过this.get()得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行的时候会把第一个参数和第二个参数传入新值value和旧值oldValue,这就是当我们自己添加watcher时候可以在参数中取到新旧值的来源。对应渲染watcher而言,在执行this.get()方法求值的时候,会执行getter方法。因此在我们修改组件相关数据时候,会触发组件重新渲染,接着重新执行patch的过程 手写一个数据绑定: <input id="input" type="text" /> <div id="text"></div> let input = document.getElementById("input"); let text = document.getElementById("text"); let data = { value: "" }; Object.defineProperty(data, "value", { set: function(val) { text.innerHTML = val; input.value = val; }, get: function() { return input.value; } }); input.onkeyup = function(e) { data.value = e.target.value; };

九旬 2020-05-24 11:24:58 0 浏览量 回答数 0

回答

本文主要介绍Java中的自动拆箱与自动装箱的有关知识。 基本数据类型 基本类型,或者叫做内置类型,是Java中不同于类(Class)的特殊类型。它们是我们编程中使用最频繁的类型。 Java是一种强类型语言,第一次申明变量必须说明数据类型,第一次变量赋值称为变量的初始化。 Java基本类型共有八种,基本类型可以分为三类: 字符类型char 布尔类型boolean 数值类型byte、short、int、long、float、double。 数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double。 Java中的数值类型不存在无符号的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变。 实际上,Java中还存在另外一种基本类型void,它也有对应的包装类 java.lang.Void,不过我们无法直接对它们进行操作。 基本数据类型有什么好处 我们都知道在Java语言中,new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象;所以,对象本身来说是比较消耗资源的。 对于经常用到的类型,如int等,如果我们每次使用这种变量的时候都需要new一个Java对象的话,就会比较笨重。所以,和C++一样,Java提供了基本数据类型,这种数据的变量不需要使用new创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。 整型的取值范围 Java中的整型主要包含byte、short、int和long这四种,表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。 先来个简答的科普,1字节=8位(bit)。java中的整型属于有符号数。 先来看计算中8bit可以表示的数字: 最小值:10000000 (-128)(-2^7) 最大值:01111111(127)(2^7-1) 整型的这几个类型中, byte:byte用1个字节来存储,范围为-128(-2^7)到127(2^7-1),在变量初始化的时候,byte类型的默认值为0。 short:short用2个字节存储,范围为-32,768 (-2^15)到32,767 (2^15-1),在变量初始化的时候,short类型的默认值为0,一般情况下,因为Java本身转型的原因,可以直接写为0。 int:int用4个字节存储,范围为-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1),在变量初始化的时候,int类型的默认值为0。 long:long用8个字节存储,范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在变量初始化的时候,long类型的默认值为0L或0l,也可直接写为0。 超出范围怎么办 上面说过了,整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致超出表示范围,即溢出。如以下代码: int i = Integer.MAX_VALUE; int j = Integer.MAX_VALUE; int k = i + j; System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")"); 输出结果:i (2147483647) + j (2147483647) = k (-2) **这就是发生了溢出,溢出的时候并不会抛异常,也没有任何提示。**所以,在程序中,使用同类型的数据进行运算的时候,一定要注意数据溢出的问题。 包装类型 Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。 包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示 基本数据类型包装类byteBytebooleanBooleanshortShortcharCharacterintIntegerlongLongfloatFloatdoubleDouble 在这八个类名中,除了Integer和Character类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。 为什么需要包装类 很多人会有疑问,既然Java中为了提高效率,提供了八种基本数据类型,为什么还要提供包装类呢? 这个问题,其实前面已经有了答案,因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。 为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。 拆箱与装箱 那么,有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。比如把一个基本数据类型的int转换成一个包装类型的Integer对象。 我们认为包装类是对基本类型的包装,所以,把基本数据类型转换成包装类的过程就是打包装,英文对应于boxing,中文翻译为装箱。 反之,把包装类转换成基本数据类型的过程就是拆包装,英文对应于unboxing,中文翻译为拆箱。 在Java SE5之前,要进行装箱,可以通过以下代码: Integer i = new Integer(10); 自动拆箱与自动装箱 在Java SE5中,为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。 自动装箱: 就是将基本数据类型自动转换成对应的包装类。 自动拆箱:就是将包装类自动转换成对应的基本数据类型。 Integer i =10; //自动装箱 int b= i; //自动拆箱 Integer i=10 可以替代 Integer i = new Integer(10);,这就是因为Java帮我们提供了自动装箱的功能,不需要开发者手动去new一个Integer对象。 自动装箱与自动拆箱的实现原理 既然Java提供了自动拆装箱的能力,那么,我们就来看一下,到底是什么原理,Java是如何实现的自动拆装箱功能。 我们有以下自动拆装箱的代码: public static void main(String[]args){ Integer integer=1; //装箱 int i=integer; //拆箱 } 对以上代码进行反编译后可以得到以下代码: public static void main(String[]args){ Integer integer=Integer.valueOf(1); int i=integer.intValue(); } 从上面反编译后的代码可以看出,int的自动装箱都是通过Integer.valueOf()方法来实现的,Integer的自动拆箱都是通过integer.intValue来实现的。如果读者感兴趣,可以试着将八种类型都反编译一遍 ,你会发现以下规律: 自动装箱都是通过包装类的valueOf()方法来实现的.自动拆箱都是通过包装类对象的xxxValue()来实现的。 哪些地方会自动拆装箱 我们了解过原理之后,在来看一下,什么情况下,Java会帮我们进行自动拆装箱。前面提到的变量的初始化和赋值的场景就不介绍了,那是最简单的也最容易理解的。 我们主要来看一下,那些可能被忽略的场景。 场景一、将基本数据类型放入集合类 我们知道,Java中的集合类只能接收对象类型,那么以下代码为什么会不报错呢? List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i ++){ li.add(i); } 将上面代码进行反编译,可以得到以下代码: List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i += 2){ li.add(Integer.valueOf(i)); } 以上,我们可以得出结论,当我们把基本数据类型放入集合类中的时候,会进行自动装箱。 场景二、包装类型和基本类型的大小比较 有没有人想过,当我们对Integer对象与基本类型进行大小比较的时候,实际上比较的是什么内容呢?看以下代码: Integer a=1; System.out.println(a==1?"等于":"不等于"); Boolean bool=false; System.out.println(bool?"真":"假"); 对以上代码进行反编译,得到以下代码: Integer a=1; System.out.println(a.intValue()==1?"等于":"不等于"); Boolean bool=false; System.out.println(bool.booleanValue?"真":"假"); 可以看到,包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。 场景三、包装类型的运算 有没有人想过,当我们对Integer对象进行四则运算的时候,是如何进行的呢?看以下代码: Integer i = 10; Integer j = 20; System.out.println(i+j); 反编译后代码如下: Integer i = Integer.valueOf(10); Integer j = Integer.valueOf(20); System.out.println(i.intValue() + j.intValue()); 我们发现,两个包装类型之间的运算,会被自动拆箱成基本类型进行。 场景四、三目运算符的使用 这是很多人不知道的一个场景,作者也是一次线上的血淋淋的Bug发生后才了解到的一种案例。看一个简单的三目运算符的代码: boolean flag = true; Integer i = 0; int j = 1; int k = flag ? i : j; 很多人不知道,其实在int k = flag ? i : j;这一行,会发生自动拆箱。反编译后代码如下: boolean flag = true; Integer i = Integer.valueOf(0); int j = 1; int k = flag ? i.intValue() : j; System.out.println(k); 这其实是三目运算符的语法规范。当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。 因为例子中,flag ? i : j;片段中,第二段的i是一个包装类型的对象,而第三段的j是一个基本类型,所以会对包装类进行自动拆箱。如果这个时候i的值为null,那么就会发生NPE。(自动拆箱导致空指针异常) 场景五、函数参数与返回值 这个比较容易理解,直接上代码了: //自动拆箱 public int getNum1(Integer num) { return num; } //自动装箱 public Integer getNum2(int num) { return num; } 自动拆装箱与缓存 Java SE的自动拆装箱还提供了一个和缓存有关的功能,我们先来看以下代码,猜测一下输出结果: public static void main(String... strings) { Integer integer1 = 3; Integer integer2 = 3; if (integer1 == integer2) System.out.println("integer1 == integer2"); else System.out.println("integer1 != integer2"); Integer integer3 = 300; Integer integer4 = 300; if (integer3 == integer4) System.out.println("integer3 == integer4"); else System.out.println("integer3 != integer4"); } 我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。在Java中,==比较的是对象应用,而equals比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。 上面这段代码真正的输出结果: integer1 == integer2 integer3 != integer4 原因就和Integer中的缓存机制有关。在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。 适用于整数值区间-128 至 +127。 只适用于自动装箱。使用构造函数创建对象不适用。 具体的代码实现可以阅读Java中整型的缓存机制一文,这里不再阐述。 我们只需要知道,当需要进行自动装箱时,如果数字在-128至127之间时,会直接使用缓存中的对象,而不是重新创建一个对象。 其中的javadoc详细的说明了缓存支持-128到127之间的自动装箱过程。最大值127可以通过-XX:AutoBoxCacheMax=size修改。 实际上这个功能在Java 5中引入的时候,范围是固定的-128 至 +127。后来在Java 6中,可以通过java.lang.Integer.IntegerCache.high设置最大值。 这使我们可以根据应用程序的实际情况灵活地调整来提高性能。到底是什么原因选择这个-128到127范围呢?因为这个范围的数字是最被广泛使用的。 在程序中,第一次使用Integer的时候也需要一定的额外时间来初始化这个缓存。 在Boxing Conversion部分的Java语言规范(JLS)规定如下: 如果一个变量p的值是: -128至127之间的整数(§3.10.1) true 和 false的布尔值 (§3.10.3) ‘\u0000’至 ‘\u007f’之间的字符(§3.10.4) 范围内的时,将p包装成a和b两个对象时,可以直接使用a==b判断a和b的值是否相等。 自动拆装箱带来的问题 当然,自动拆装箱是一个很好的功能,大大节省了开发人员的精力,不再需要关心到底什么时候需要拆装箱。但是,他也会引入一些问题。 包装对象的数值比较,不能简单的使用==,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals比较。 前面提到,有些场景会进行自动拆装箱,同时也说过,由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出NPE。 如果一个for循环中有大量拆装箱操作,会浪费很多资源。 参考资料 Java的自动拆装箱

montos 2020-06-01 21:24:01 0 浏览量 回答数 0

回答

Java内存模型 按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 JVM主要管理两种类型内存:堆和非堆,堆内存(Heap Memory)是在 Java 虚拟机启动时创建,非堆内存(Non-heap Memory)是在JVM堆之外的内存。 堆是Java代码可及的内存,留给开发人员使用的;非堆是JVM留给自己用的,包含方法区、JVM内部处理或优化所需的内存(如 JIT Compiler,Just-in-time Compiler,即时编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码。 JVM 内存包含如下几个部分: 堆内存(Heap Memory): 存放Java对象 非堆内存(Non-Heap Memory): 存放类加载信息和其它meta-data 其它(Other): 存放JVM 自身代码等 Java内存分配 Java的内存管理实际上就是变量和对象的管理,其中包括对象的分配和释放。 JVM内存申请过程如下: JVM 会试图为相关Java对象在Eden中初始化一块内存区域 当Eden空间足够时,内存申请结束;否则到下一步 JVM 试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区 Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区 当OLD区空间不够时,JVM 会在OLD区进行完全的垃圾收集(0级) 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory”错误 GC基本原理 GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。 编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显式操作方法。所以,Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。 对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为”不可达的”.GC将负责回收所有”不可达”对象的内存空间。 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”.当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。 为了保证 GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。 GC分代划分 JVM内存模型中Heap区分两大块,一块是 Young Generation,另一块是Old Generation 在Young Generation中,有一个叫Eden Space的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from、to),它们的大小总是一样,它们用来存放每次垃圾回收后存活下来的对象。 在Old Generation中,主要存放应用程序中生命周期长的内存对象。 在Young Generation块中,垃圾回收一般用Copying的算法,速度快。每次GC的时候,存活下来的对象首先由Eden拷贝到某个SurvivorSpace,当Survivor Space空间满了后,剩下的live对象就被直接拷贝到OldGeneration中去。因此,每次GC后,Eden内存块会被清空。 在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求。 垃圾回收分多级,0级为全部(Full)的垃圾回收,会回收OLD段中的垃圾;1级或以上为部分垃圾回收,只会回收Young中的垃圾,内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。增量式GC 增量式GC(Incremental GC),是GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU。 当GC进程运行时,应用程序停止运行。当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低. 增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。 Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。 HotSpot JVM增量式GC的实现是采用Train GC算法,它的基本想法就是:将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。 详解函数finalize 更多内容: https://chenhx.blog.csdn.net/article/details/83957456 https://chenhx.blog.csdn.net/article/details/84294481

谙忆 2019-12-02 03:08:20 0 浏览量 回答数 0

回答

Python 的 Decorator在使用上和Java/C#的Annotation很相似,就是在方法名前面加一个@XXX注解来为这个方法装饰一些东西。但是,Java/C#的Annotation也很让人望而却步,太TMD的复杂了,你要玩它,你需要了解一堆Annotation的类库文档,让人感觉就是在学另外一门语言。 而Python使用了一种相对于Decorator Pattern和Annotation来说非常优雅的方法,这种方法不需要你去掌握什么复杂的OO模型或是Annotation的各种类库规定,完全就是语言层面的玩法:一种函数式编程的技巧。如果你看过本站的《函数式编程》,你一定会为函数式编程的那种“描述你想干什么,而不是描述你要怎么去实现”的编程方式感到畅快。(如果你不了解函数式编程,那在读本文之前,还请你移步去看看《函数式编程》) 好了,我们先来点感性认识,看一个Python修饰器的Hello World的代码。 Hello World 下面是代码:文件名:hello.py def hello(fn): def wrapper(): print "hello, %s" % fn.__name__ fn() print "goodby, %s" % fn.__name__ return wrapper @hellodef foo(): print "i am foo" foo() 当你运行代码,你会看到如下输出: [chenaho@chenhao-air]$ python hello.pyhello, fooi am foogoodby, foo 你可以看到如下的东西: 1)函数foo前面有个@hello的“注解”,hello就是我们前面定义的函数hello 2)在hello函数中,其需要一个fn的参数(这就用来做回调的函数) 3)hello函数中返回了一个inner函数wrapper,这个wrapper函数回调了传进来的fn,并在回调前后加了两条语句。 Decorator 的本质 对于Python的这个@注解语法糖- Syntactic Sugar 来说,当你在用某个@decorator来修饰某个函数func时,如下所示: @decoratordef func(): pass 其解释器会解释成下面这样的语句: func = decorator(func) 尼玛,这不就是把一个函数当参数传到另一个函数中,然后再回调吗?是的,但是,我们需要注意,那里还有一个赋值语句,把decorator这个函数的返回值赋值回了原来的func。 根据《函数式编程》中的first class functions中的定义的,你可以把函数当成变量来使用,所以,decorator必需得返回了一个函数出来给func,这就是所谓的higher order function 高阶函数,不然,后面当func()调用的时候就会出错。 就我们上面那个hello.py里的例子来说, @hellodef foo(): print "i am foo" 被解释成了: foo = hello(foo) 是的,这是一条语句,而且还被执行了。你如果不信的话,你可以写这样的程序来试试看: def fuck(fn): print "fuck %s!" % fn.__name__[::-1].upper() @fuckdef wfg(): pass 没了,就上面这段代码,没有调用wfg()的语句,你会发现, fuck函数被调用了,而且还很NB地输出了我们每个人的心声! 再回到我们hello.py的那个例子,我们可以看到,hello(foo)返回了wrapper()函数,所以,foo其实变成了wrapper的一个变量,而后面的foo()执行其实变成了wrapper()。 知道这点本质,当你看到有多个decorator或是带参数的decorator,你也就不会害怕了。 比如:多个decorator @decorator_one@decorator_twodef func(): pass 相当于: func = decorator_one(decorator_two(func)) 比如:带参数的decorator: @decorator(arg1, arg2)def func(): pass 相当于: func = decorator(arg1,arg2)(func) 这意味着decorator(arg1, arg2)这个函数需要返回一个“真正的decorator”。 带参数及多个Decrorator 我们来看一个有点意义的例子:html.py def makeHtmlTag(tag, args, *kwds): def real_decorator(fn): css_class = " class='{0}'".format(kwds["css_class"]) if "css_class" in kwds else "" def wrapped(*args, **kwds): return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">" return wrapped return real_decorator @makeHtmlTag(tag="b", css_class="bold_css")@makeHtmlTag(tag="i", css_class="italic_css")def hello(): return "hello world" print hello() 输出: hello world 在上面这个例子中,我们可以看到:makeHtmlTag有两个参数。所以,为了让 hello = makeHtmlTag(arg1, arg2)(hello) 成功,makeHtmlTag 必需返回一个decorator(这就是为什么我们在makeHtmlTag中加入了real_decorator()的原因),这样一来,我们就可以进入到 decorator 的逻辑中去了—— decorator得返回一个wrapper,wrapper里回调hello。看似那个makeHtmlTag() 写得层层叠叠,但是,已经了解了本质的我们觉得写得很自然。 你看,Python的Decorator就是这么简单,没有什么复杂的东西,你也不需要了解过多的东西,使用起来就是那么自然、体贴、干爽、透气,独有的速效凹道和完美的吸收轨迹,让你再也不用为每个月的那几天感到焦虑和不安,再加上贴心的护翼设计,量多也不用当心。对不起,我调皮了。 什么,你觉得上面那个带参数的Decorator的函数嵌套太多了,你受不了。好吧,没事,我们看看下面的方法。 class式的 Decorator 首先,先得说一下,decorator的class方式,还是看个示例: class myDecorator(object): def __init__(self, fn): print "inside myDecorator.__init__()" self.fn = fn def __call__(self): self.fn() print "inside myDecorator.__call__()" @myDecoratordef aFunction(): print "inside aFunction()" print "Finished decorating aFunction()" aFunction() 输出: inside myDecorator.__init__() Finished decorating aFunction() inside aFunction() inside myDecorator.__call__() 上面这个示例展示了,用类的方式声明一个decorator。我们可以看到这个类中有两个成员:1)一个是__init__(),这个方法是在我们给某个函数decorator时被调用,所以,需要有一个fn的参数,也就是被decorator的函数。2)一个是__call__(),这个方法是在我们调用被decorator函数时被调用的。上面输出可以看到整个程序的执行顺序。 这看上去要比“函数式”的方式更易读一些。 下面,我们来看看用类的方式来重写上面的html.py的代码:html.py class makeHtmlTagClass(object): def __init__(self, tag, css_class=""): self._tag = tag self._css_class = " class='{0}'".format(css_class) if css_class !="" else "" def __call__(self, fn): def wrapped(*args, **kwargs): return "<" + self._tag + self._css_class+">" + fn(*args, **kwargs) + "</" + self._tag + ">" return wrapped @makeHtmlTagClass(tag="b", css_class="bold_css")@makeHtmlTagClass(tag="i", css_class="italic_css")def hello(name): return "Hello, {}".format(name) print hello("Hao Chen") 上面这段代码中,我们需要注意这几点:1)如果decorator有参数的话,__init__() 成员就不能传入fn了,而fn是在__call__的时候传入的。2)这段代码还展示了 wrapped(args, *kwargs) 这种方式来传递被decorator函数的参数。(其中:args是一个参数列表,kwargs是参数dict,具体的细节,请参考Python的文档或是StackOverflow的这个问题,这里就不展开了) 用Decorator设置函数的调用参数 你有三种方法可以干这个事: 第一种,通过 **kwargs,这种方法decorator会在kwargs中注入参数。 def decorate_A(function): def wrap_function(*args, **kwargs): kwargs['str'] = 'Hello!' return function(*args, **kwargs) return wrap_function @decorate_Adef print_message_A(args, *kwargs): print(kwargs['str']) print_message_A() 第二种,约定好参数,直接修改参数 def decorate_B(function): def wrap_function(*args, **kwargs): str = 'Hello!' return function(str, *args, **kwargs) return wrap_function @decorate_Bdef print_message_B(str, args, *kwargs): print(str) print_message_B() 第三种,通过 *args 注入 def decorate_C(function): def wrap_function(*args, **kwargs): str = 'Hello!' #args.insert(1, str) args = args +(str,) return function(*args, **kwargs) return wrap_function class Printer: @decorate_C def print_message(self, str, *args, **kwargs): print(str) p = Printer()p.print_message() Decorator的副作用 到这里,我相信你应该了解了整个Python的decorator的原理了。 相信你也会发现,被decorator的函数其实已经是另外一个函数了,对于最前面那个hello.py的例子来说,如果你查询一下foo.__name__的话,你会发现其输出的是“wrapper”,而不是我们期望的“foo”,这会给我们的程序埋一些坑。所以,Python的functool包中提供了一个叫wrap的decorator来消除这样的副作用。下面是我们新版本的hello.py。文件名:hello.py from functools import wrapsdef hello(fn): @wraps(fn) def wrapper(): print "hello, %s" % fn.__name__ fn() print "goodby, %s" % fn.__name__ return wrapper @hellodef foo(): '''foo help doc''' print "i am foo" pass foo()print foo.__name__ #输出 fooprint foo.__doc__ #输出 foo help doc 当然,即使是你用了functools的wraps,也不能完全消除这样的副作用。 来看下面这个示例: from inspect import getmembers, getargspecfrom functools import wraps def wraps_decorator(f): @wraps(f) def wraps_wrapper(*args, **kwargs): return f(*args, **kwargs) return wraps_wrapper class SomeClass(object): @wraps_decorator def method(self, x, y): pass obj = SomeClass()for name, func in getmembers(obj, predicate=inspect.ismethod): print "Member Name: %s" % name print "Func Name: %s" % func.func_name print "Args: %s" % getargspec(func)[0] 输出: Member Name: method Func Name: method Args: [] 你会发现,即使是你你用了functools的wraps,你在用getargspec时,参数也不见了。 要修正这一问,我们还得用Python的反射来解决,下面是相关的代码: def get_true_argspec(method): argspec = inspect.getargspec(method) args = argspec[0] if args and args[0] == 'self': return argspec if hasattr(method, '__func__'): method = method.__func__ if not hasattr(method, 'func_closure') or method.func_closure is None: raise Exception("No closure for method.") method = method.func_closure[0].cell_contents return get_true_argspec(method) 当然,我相信大多数人的程序都不会去getargspec。所以,用functools的wraps应该够用了。 一些decorator的示例 好了,现在我们来看一下各种decorator的例子: 给函数调用做缓存 这个例实在是太经典了,整个网上都用这个例子做decorator的经典范例,因为太经典了,所以,我这篇文章也不能免俗。 from functools import wrapsdef memo(fn): cache = {} miss = object() @wraps(fn) def wrapper(*args): result = cache.get(args, miss) if result is miss: result = fn(*args) cache[args] = result return result return wrapper @memodef fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2) 上面这个例子中,是一个斐波拉契数例的递归算法。我们知道,这个递归是相当没有效率的,因为会重复调用。比如:我们要计算fib(5),于是其分解成fib(4) + fib(3),而fib(4)分解成fib(3)+fib(2),fib(3)又分解成fib(2)+fib(1)…… 你可看到,基本上来说,fib(3), fib(2), fib(1)在整个递归过程中被调用了两次。 而我们用decorator,在调用函数前查询一下缓存,如果没有才调用了,有了就从缓存中返回值。一下子,这个递归从二叉树式的递归成了线性的递归。 Profiler的例子 这个例子没什么高深的,就是实用一些。 import cProfile, pstats, StringIO def profiler(func): def wrapper(*args, **kwargs): datafn = func.__name__ + ".profile" # Name the data file prof = cProfile.Profile() retval = prof.runcall(func, *args, **kwargs) #prof.dump_stats(datafn) s = StringIO.StringIO() sortby = 'cumulative' ps = pstats.Stats(prof, stream=s).sort_stats(sortby) ps.print_stats() print s.getvalue() return retval return wrapper 注册回调函数 下面这个示例展示了通过URL的路由来调用相关注册的函数示例: class MyApp(): def __init__(self): self.func_map = {} def register(self, name): def func_wrapper(func): self.func_map[name] = func return func return func_wrapper def call_method(self, name=None): func = self.func_map.get(name, None) if func is None: raise Exception("No function registered against - " + str(name)) return func() app = MyApp() @app.register('/')def main_page_func(): return "This is the main page." @app.register('/next_page')def next_page_func(): return "This is the next page." print app.call_method('/')print app.call_method('/next_page') 注意:1)上面这个示例中,用类的实例来做decorator。2)decorator类中没有__call__(),但是wrapper返回了原函数。所以,原函数没有发生任何变化。 给函数打日志 下面这个示例演示了一个logger的decorator,这个decorator输出了函数名,参数,返回值,和运行时间。 from functools import wrapsdef logger(fn): @wraps(fn) def wrapper(*args, **kwargs): ts = time.time() result = fn(*args, **kwargs) te = time.time() print "function = {0}".format(fn.__name__) print " arguments = {0} {1}".format(args, kwargs) print " return = {0}".format(result) print " time = %.6f sec" % (te-ts) return result return wrapper @loggerdef multipy(x, y): return x * y @loggerdef sum_num(n): s = 0 for i in xrange(n+1): s += i return s print multipy(2, 10)print sum_num(100)print sum_num(10000000) 上面那个打日志还是有点粗糙,让我们看一个更好一点的(带log level参数的): import inspectdef get_line_number(): return inspect.currentframe().f_back.f_back.f_lineno def logger(loglevel): def log_decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): ts = time.time() result = fn(*args, **kwargs) te = time.time() print "function = " + fn.__name__, print " arguments = {0} {1}".format(args, kwargs) print " return = {0}".format(result) print " time = %.6f sec" % (te-ts) if (loglevel == 'debug'): print " called_from_line : " + str(get_line_number()) return result return wrapper return log_decorator 但是,上面这个带log level参数的有两具不好的地方,1) loglevel不是debug的时候,还是要计算函数调用的时间。2) 不同level的要写在一起,不易读。 我们再接着改进: import inspect def advance_logger(loglevel): def get_line_number(): return inspect.currentframe().f_back.f_back.f_lineno def _basic_log(fn, result, *args, **kwargs): print "function = " + fn.__name__, print " arguments = {0} {1}".format(args, kwargs) print " return = {0}".format(result) def info_log_decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): result = fn(*args, **kwargs) _basic_log(fn, result, args, kwargs) return wrapper def debug_log_decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): ts = time.time() result = fn(*args, **kwargs) te = time.time() _basic_log(fn, result, args, kwargs) print " time = %.6f sec" % (te-ts) print " called_from_line : " + str(get_line_number()) return wrapper if loglevel is "debug": return debug_log_decorator else: return info_log_decorator 你可以看到两点,1)我们分了两个log level,一个是info的,一个是debug的,然后我们在外尾根据不同的参数返回不同的decorator。2)我们把info和debug中的相同的代码抽到了一个叫_basic_log的函数里,DRY原则。 一个MySQL的Decorator 下面这个decorator是我在工作中用到的代码,我简化了一下,把DB连接池的代码去掉了,这样能简单点,方便阅读。 import umysqlfrom functools import wraps class Configuraion: def __init__(self, env): if env == "Prod": self.host = "coolshell.cn" self.port = 3306 self.db = "coolshell" self.user = "coolshell" self.passwd = "fuckgfw" elif env == "Test": self.host = 'localhost' self.port = 3300 self.user = 'coolshell' self.db = 'coolshell' self.passwd = 'fuckgfw' def mysql(sql): _conf = Configuraion(env="Prod") def on_sql_error(err): print err sys.exit(-1) def handle_sql_result(rs): if rs.rows > 0: fieldnames = [f[0] for f in rs.fields] return [dict(zip(fieldnames, r)) for r in rs.rows] else: return [] def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): mysqlconn = umysql.Connection() mysqlconn.settimeout(5) mysqlconn.connect(_conf.host, _conf.port, _conf.user, _conf.passwd, _conf.db, True, 'utf8') try: rs = mysqlconn.query(sql, {}) except umysql.Error as e: on_sql_error(e) data = handle_sql_result(rs) kwargs["data"] = data result = fn(*args, **kwargs) mysqlconn.close() return result return wrapper return decorator @mysql(sql = "select * from coolshell" )def get_coolshell(data): ... ... ... .. 线程异步 下面量个非常简单的异步执行的decorator,注意,异步处理并不简单,下面只是一个示例。 from threading import Threadfrom functools import wraps def async(func): @wraps(func) def async_func(*args, **kwargs): func_hl = Thread(target = func, args = args, kwargs = kwargs) func_hl.start() return func_hl return async_func if name == '__main__': from time import sleep @async def print_somedata(): print 'starting print_somedata' sleep(2) print 'print_somedata: 2 sec passed' sleep(2) print 'print_somedata: 2 sec passed' sleep(2) print 'finished print_somedata' def main(): print_somedata() print 'back in main' print_somedata() print 'back in main' main() 其它 关于更多的示例,你可以参看: Python Decorator Library来源:网络

51干警网 2019-12-02 01:10:47 0 浏览量 回答数 0

问题

Windows下eclipse的 C++环境配置:报错

kun坤 2020-06-06 19:54:50 0 浏览量 回答数 1

问题

Windows下eclipse的 C++环境配置 403.10 禁止访问:配置无效 

kun坤 2020-05-27 14:15:36 6 浏览量 回答数 1

问题

【今日算法】4月22日-删除排序数组中的重复项

游客ih62co2qqq5ww 2020-04-23 07:20:29 27 浏览量 回答数 1

回答

如何掌握牢靠Go语言的容器? 容器相对来说更偏重细节一些,如果想掌握的更牢靠的话呢,还是要多看一下代码,重点给大家几个提示 Go语言的并发初步有哪两个特别重要的特点? **GO语言的协程并发操作或者说协程的资源池,其调度策略有两个: ** 1、没有优先级,没有API能设置优先级,正是因为它一切都是靠Go语言自身的一个调度器来听调度,才能保证它的高效率,这点非常重要。 2、调度的策略是可抢占的,假如说一个任务它长时间的占用CPU,那么它是有可能被购入天的这个调度器给其抢占过来,让其其的任务来做运行,这是两个最重要的特点。 GO语言调度的单元goroutine的应用场景是什么? 使用JAVA或者C编写网络程序时,一个线程来处理一个http请求, 但是对于资源的利用率不高。而Go语言实现了轻量级线程的机制,GO语言在底层封装了所有的系统调用,自己实现了一个调度器,这种设计在操作系统的代码中非常多见。比如现代的操作系统基本都会封装一个软件的Timer,同时可以提供上万个软Timer同时工作,而这只是基于数量很少的硬件timer实现的,而GO语言中的并发也是如此,他是基于线程的调度池,这种调度的单元在Go语言中被称为goroutine。 GO语言与其它并发模型最大的区别是什么? 宏观GO语言与其它并发模型最大的不同,就是其推荐使用通信的这种方式来替代共享内存。当资源需要在goroutine之间进行共享的时候,实际上就是这个资源,或者说这个信息通过通道在goroutine之间进行通信的过程。因为这个锁,一般来说都是用在这个共享内存当中的,因为如果说大家阅读GO语言的相关代码,就可以看到这个channel,它实际上是基于锁来保证并发安全。 然而,这也不代表GO语言当中只能使用channel来进行一些操作,其也具备锁这方面的知识。因为现实当中,这个锁还是有一定它现实的意义和现实的要求,因为这个锁它最关键的一个意义就是它能保证资源能在并发的操作当中有一个合理的调度情况和调度策略。其中跟这个最重要,或者说最关联性最强的一个概念就是原子操作。 GO语言中的原子操作具体实现过程是怎样的? 对于原子操作,在其逻辑下,按照它书面的定义上来讲,是指不会被调度器打断的操作。对原子操作实际上就是不存在中间状态的一种操作,要不就全成功,要不全失败,这个在我们在用并发方式来调动某任务,或者说来设计某种并发系统的情况下,这种名字操作我发现是非常重要的设计理念之一。 并发与并行具体概念及实际区分是怎样的? 有一个比较重要的一个概念,就是并发与并行,其实并发与并行,它实际上具体的含义是不一样的,并发实际上是把任务在不同的时间点交给同样一个处理器来进行处理,在同一个时间点,任务不会同时进行,只是任务感觉自己正在执行,因为其那会儿可能正在堵塞状态或者说是就绪状态,其不知道自己被暂停了,以为已经被调度走了,可能自己没有感知,但是实际上CPU所有权已经不在这个任务身上了。 并行比并发更高级一些,它实际上是把每个任务都交给独立的处理器去进行完成,但同一时间点,任务在一定程度上实际上是同时在执行的。一般来说,并发的性能是要比并行更重要一些,在1.5版本之前,我们需要人工去设置GO调度器最多能运行在多少个CPU上,但是在最新的GO版本当中,已经不需要这个相关的操作。 详细介绍一下并发程序中的竞争态? 并发系统设计最初始的这一个概念就是并发程序设计当中一个竞合的概念,或者也叫竞争态。假如说我要记录一个文件的阅读量,但是这个文件或者说这个网页,可能它的阅读渠道有非常多,有可能通过引擎通过微信通过APP等等这些渠道,这些渠道的话呢,它的阅读也都是并发的,这就会涉及到同样一个变量,被多个协程的所共同访问的情况。具体代码如下: 对于GO语言并发体系中的主推的通信机制是什么? channel是GO语言并发体系中的主推的通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。 GO语言当中,它实际上是大家协同的机制,通过这种方式让几个goroutine之间做达到一个协调的效果,那么每个goroutine当中,实际上channel都是一个特殊的类型,它实际上是可以发送数据。比如现在想发送一个int类型的数据,那么channel就要定义一个发送int数据的一个管道。 那么GO语言当中,提倡使用通讯的方式来代替共享内存的方式来做goroutine,或者说并发之间的一个协同。channel如果我们后续阅读它的代码就会知道,它是保证协程安全,并且它遵循这个先入先出的原则来让这个储蓄方读取获得数据,而且它能保证顺序,正是这两个特性,可以让这个channel替代共享内存,因为它的如果顺序有所改变的话,它实际上也是有会有问题。 详细介绍GO语言中关于通道的声明涉及哪些方面? 1.经典方式声明 通过使用chan类型,其声明方式如下: var name chan type 其中type表示通道内的数据类型;name:通道的变量名称,不过这样创建的通道只是空值 nil,一般来说都是通道都是通过make函数创建的。 2.make方式 make函数可以创建通道格式如下: name := make(chan type) 3.创建带有缓冲的通道 后面会讲到缓冲通道的概念,这里先说他的定义方式 name := make(chan type, size) 其中type表示通道内的数据类型;name:通道的变量名称,size代表缓冲的长度。 具体介绍通道数据收发的详细过程有哪些? 通道的数据发送 通道当中发送数据的操作服务是这样的这样的一个大于号加上一个减号。 chan <- value 注意,如果是发送给一个没有缓冲的一个通道。假如说数据没有被接收的话,那么这个发送操作将持续被注册,也就是说就是channel这个语句就直接被注册到这,假如说没有任何的协程去读到他或者其他语句去读到这个产品,那么这个语句就被注册掉了。但GO语言是能发现的,如果其一直在堵塞的话,那实际上就造成死锁,GO语言的编译器实际上能发现的有点错误。 假如说,首先创建一个int型的通道,然后直接尝试发送一个数据给它,编译会报错,然后呢,数据的这个数据的接收的话,实际上就是把这个点号的位置跟那个大于号的位置做了一个调换。其实把这个双方的位置做了一个调换之后,是实际上就是都做了一个允许的操作。这其中的话呢,还有一种比较特殊的一个读取操作是其可以忽略到接收到的数据,因为不管管道中发出的数据,如果没读的话就堵塞到这,那么如果你觉得这个语句你也不需要,那么你可以把那个变量给它忽略掉。 2.通道的数据接收 通道接收数据的操作符也是<-,具体有以下几种方式 - 1) 阻塞接收数据 阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下: data := <-ch 执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。 如需要忽略接收的数据,则将data变量省略,具体格式如下: <-ch - 2) 非阻塞接收数据 使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下: data, ok := <-ch 非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。一般只配合select语句配合定时器做超时检测时使用。 关于通道数据收发有哪些需要注意的事项? 通道数据在进行输入收发的时候,必须要在两个不同的goroutine当中进行,因在同一个goroutine当中,收发的这些语句实际上都是堵塞的,你可能在同一个goroutine当中,它的这个函数已经在那边阻塞住了,或者说程序已经在那边阻塞住了,它已经停在那了,你后面有一句你能执行不到,所以说通道的收发必须在两个不同的goroutine之间来进行,在同一个goroutine之间的这个收发操作的话,实际上是没有意义的。 接收将持续堵塞,直到发送方发送出去,如果接收方接收,然后通道中没有发送方数据时,接收方也会发送,直到发送方到发送数据为止。就是刚才说的这个一体两面,这个发送方假如说没有人读的话,发送方会堵塞,假如说没有人写的话,那么接收方也会发生堵塞,这两边实际上都会有一个堵塞的情况。那么这个通道的收发的话呢,一般来说一次只能收一一个元素,假如说这个是一个有缓冲的一个通道,我通过一次不操作的话,实际上也只不过读出一个元素。不能把它一些缓冲区所有元素都读出来。 聊一下生产者消费者模式具体内容有哪些? 介绍一下生产者消费者模式,从GO语言的这个并发模型来看,也就是说假如说咱们站在一个比较高的一个高度来看,其实利用channel的确能达到共享内存的目的。这个channel的性质与在读写状态且保证顺序的共享内存并无不同。甚至我们可以说这个是基于消息队列的封装程度可以比共享内存来的更安全,所以说呢,这个在这个GO语言当中,或者说在GO语言的这个设计风格当中的话呢,其这个生产者消费者模式实现起来会相对来说比较简单一些。我们先介绍一下什么是生产者消费者。 就这个这这张图当中的话呢,就是一个典型的那种消费的问题, 就是说我是生产者的话我会生产一些产品,然后放到这个仓库当中,消费者的话会从那个仓库当中去取商品,这个可以说是消息队列,还有包括卡夫卡那些比较经典的相应队列当中,都会用到的这么一个设计模式,或者说其们从本质上来说的话,都是基于这样一个设计模式,交易的生产者是谁?消费者是谁?这个消息队列的话是。这个生产者消费者模式的话呢,实际上也成为有缓冲有限缓冲问题,它是一个并发的一个经典的案例,因为我们知道这个商品仓库的库房大小是有限的,也就是说生产者不能无限的去生产商品,一旦这个库房爆掉的话,它是它是必须要中止自己的生产,消费者也是不能无限地获取消息。 假如仓库是空的话,那这个消费者的这个相关的情况也需要被阻塞。那么怎么在这个生产者跟消费者之间保证商品不丢失。这就是生产者与消费者之间最核心的内容。先来看一下这个Java当中生产者消费者的这种实现到底是什么样的。这个可以说是一个最经典的这么样一个实现。这个Java当中是没有channel,那么它只能通过什么呢,只能通过信号量和一个一个log,也就是说一个忽视服务态度,这两个这两个配合信号量和所配合才能共同完成,这样一个生产者消费者这么一个相关的工作。 GO语言并发实战详细过程梳理 在现在这个远程办公的这一个大的背景下,积累了大量重复的文件,因为很可能大家都不断的在不同的群里发相同的文件,发相同的这个报表,以及一些相同的视频等等这些需要学习的材料,那么怎么把这些文件都找出来,然后把这些相同文件都给删掉了,这实际上是并发课的一个实践的一个内容,因为这个创业型的这个方案的话,它的代码相对来说比较长。 如何使用GO语言清理PC机中的文件,详细代码及注释如下: package main import ( // "fmt" // fmt 包使用函数实现 I/O 格式化(类似于 C 的 printf 和 scanf 的函数), 格式化参数源自C,但更简单 "io/ioutil" //"sync" //"time" ) func PrintRepreatFile(path string, fileNameSizeMap map[string]int64, exFileList []string) { fs, _ := ioutil.ReadDir(path) for _, file := range fs { if file.IsDir() { PrintRepreatFile(path+"/"+file.Name(), fileNameSizeMap, exFileList)//遍历整个文件系统,如果是目录则递归调用 } else { if file.Size() > 1000000 {//设定文件清理阈值,如果大于一定大小再进行清理 fileSize := fileNameSizeMap[file.Name()]//通过查哈希表的方式来确定,有无重名且大小相同的文件。 if fileSize == file.Size() { fmt.Println(path + "/" + file.Name())//如果有则打印出来 exFileList = append(exFileList, path+file.Name())//将结果记入切片当中 } else { fileNameSizeMap[file.Name()] = file.Size() } } } } } func main() { //方式一 fileNameSizeMap := make(map[string]int64, 10000) exFileList := make([]string, 100, 1000) PrintRepreatFile("E:/test", fileNameSizeMap, exFileList) } 这个程序在GO语言的环境下可以直接运行使用,其中有几个知识点,也是咱们前文提到过的,首先是切片的大小一定要设定的相对合适一些,如果容量不够大造成频繁扩容非常浪费资源。二是哈希表也就是map没有并发安全的属于,在我们这个未引入并发的程序中可以使用,如果有并发操作,那么map不再适用了。 可能很多人被GO语言的在并发性能所吸引入坑的,GO语言之父也就是UNIX之父Ken Thompson明显给出了很多建议,根据笔者在操作系统方面的相关经验来看,GO语言设计中经常参考UNIX内核的设计思路。比如硬定时器的数量有限,无法满足系统实际运行需要,所以在内核代码中就会看到基于硬件定时器的软件定时器的方案,而软件定时器的数量可以比硬件定时器多几百倍。 这样的理念明显融合到了 goroutine之中,由于其它编程语言往往直接通过系统级别的线程来实现并发功能,但是这样的方式往往会是大马拉小车,造成系统资源的浪费。因此GO语言封装了所有的系统操作,实现了更加轻量级的协程-goroutine。只要使用关键字(go)就可以启动协程,对比C++、JAVA的多线程并发模型,GO的协程更简单明了。 当然协程之间的消息通信与并发控制也是非常重要的一环。在GO语言借鉴了Message Queue的消息队列机制替代共享内存的方式进行协程间通信,其中管道channel作为基本的数据类型,保证并发时的操作安全。而且管道的引入还带来很多实践中非常实用的功能,比如可以方便实现生产者、消费者等并发设计模式,而这些设计模式在其它使用共享存内存的并发模型中实现起相关功能来非常的繁锁。 在GO语言中在调用函数前加入go 关键字,就能启动一个协程,也就是一个并发,但是我们上面的程序如果把调用方式改为: go PrintRepreatFile("E:/test", fileNameSizeMap, exFileList) 你会发现程序会直接退出,什么都没做,所以GO语言的并发对于初学者来说还是有一定门槛的,比如上例中如果想设计成一个并行的程序,如何让多个协程共同来帮忙找出重复的文件其实还是要费一番周折的。

剑曼红尘 2020-04-13 11:06:46 0 浏览量 回答数 0

问题

对症下药:Tomcat停机过程分析与线程处理方法

驻云科技 2019-12-01 21:36:46 4001 浏览量 回答数 0

回答

1 写出下面代码输出内容。 package main import (    "fmt" ) funcmain() {     defer_call() } funcdefer_call() {     deferfunc() {fmt.Println("打印前")}()     deferfunc() {fmt.Println("打印中")}()     deferfunc() {fmt.Println("打印后")}()     panic("触发异常") } 考点:defer执行顺序 解答: defer 是后进先出。 panic 需要等defer 结束后才会向上传递。 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。 打印后 打印中 打印前 panic: 触发异常 2 以下代码有什么问题,说明原因。 type student struct {     Name string     Age  int } funcpase_student() {     m := make(map[string]*student)     stus := []student{         {Name: "zhou",Age: 24},         {Name: "li",Age: 23},         {Name: "wang",Age: 22},     }    for _,stu := range stus {         m[stu.Name] =&stu     } } 考点:foreach 解答: 这样的写法初学者经常会遇到的,很危险! 与Java的foreach一样,都是使用副本的方式。所以m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝。 就像想修改切片元素的属性: for _, stu := rangestus {     stu.Age = stu.Age+10} 也是不可行的。 大家可以试试打印出来: func pase_student() {     m := make(map[string]*student)     stus := []student{         {Name: "zhou",Age: 24},         {Name: "li",Age: 23},         {Name: "wang",Age: 22},     }         // 错误写法     for _,stu := range stus {         m[stu.Name] =&stu     }          fork,v:=range m{               println(k,"=>",v.Name)     }           // 正确     for i:=0;i<len(stus);i++ {        m[stus[i].Name] = &stus[i]     }          fork,v:=range m{                println(k,"=>",v.Name)     } } 3 下面的代码会输出什么,并说明原因 func main() {     runtime.GOMAXPROCS(1)     wg := sync.WaitGroup{}     wg.Add(20)   for i := 0; i < 10; i++ {                  gofunc() {            fmt.Println("A: ", i)            wg.Done()         }()     }             for i:= 0; i < 10; i++ {                    gofunc(i int) {            fmt.Println("B: ", i)            wg.Done()         }(i)     }     wg.Wait() } 考点:go执行的随机性和闭包 解答: 谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是A:均为输出10,B:从0~9输出(顺序不定)。 第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。 故go func执行时,i的值始终是10。 第二个go func中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。 4 下面代码会输出什么? type People struct{}func (p People)ShowA() {     fmt.Println("showA")     p.ShowB() } func(pPeople)ShowB() {     fmt.Println("showB") } typeTeacher struct {     People } func(t*Teacher)ShowB() {     fmt.Println("teachershowB") } funcmain() {     t := Teacher{}     t.ShowA() } 考点:go的组合继承 解答: 这是Golang的组合模式,可以实现OOP的继承。 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。 showAshowB 5 下面代码会触发异常吗?请详细说明 func main() {     runtime.GOMAXPROCS(1)     int_chan := make(chanint, 1)     string_chan := make(chanstring, 1)     int_chan <- 1     string_chan <- "hello"     select {                case value := <-int_chan:        fmt.Println(value)           casevalue := <-string_chan:                   panic(value)     } } 考点:select随机性 解答: select会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则: select 中只要有一个case能return,则立刻执行。 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。 如果没有一个case能return则可以执行”default”块。 6 下面代码输出什么? funccalc(indexstring, a, bint) int {     ret := a+ b     fmt.Println(index,a, b, ret)     return ret } funcmain() {          a := 1     b := 2     defer calc("1", a,calc("10", a, b))    a = 0     defer calc("2", a,calc("20", a, b))    b = 1 } 考点:defer执行顺序 解答: 这道题类似第1题 需要注意到defer执行顺序和值传递 index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用 calc("10",1,2)==>10,1,2,3 执行index:2时,与之前一样,需要先调用calc("20",0,2)==>20,0,2,2 执行到b=1时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2最后执行index:1==>calc("1",1,3)==>1,1,3,4 10 1 2 320 0 2 22 0 2 21 1 3 4 7 请写出以下输入内容 funcmain() {            s := make([]int,5)     s = append(s,1, 2, 3)     fmt.Println(s) } 考点:make默认值和append 解答: make初始化是由默认值的哦,此处默认值为0 [00000123] 大家试试改为: s := make([]int, 0) s = append(s, 1, 2, 3) fmt.Println(s)//[1 2 3] 8 下面的代码有什么问题? type UserAges struct {     ages map[string]int     sync.Mutex } func(uaUserAges)Add(name string, age int) {     ua.Lock()          deferua.Unlock()     ua.ages[name] = age } func(uaUserAges)Get(name string)int {           ifage, ok := ua.ages[name]; ok {                  return age     }         return-1 } 考点:map线程安全 解答: 可能会出现 fatal error: concurrent mapreadandmapwrite. 修改一下看看效果 func (ua *UserAges)Get(namestring)int {     ua.Lock()          deferua.Unlock()          ifage, ok := ua.ages[name]; ok {                   return age     }            return-1 } 9.   下面的迭代会有什么问题? func (set *threadSafeSet)Iter()<-chaninterface{} {     ch := make(chaninterface{})                  gofunc() {         set.RLock()                for elem := range set.s {            ch <- elem         }                   close(ch)         set.RUnlock()     }()      return ch } 考点:chan缓存池 解答: 看到这道题,我也在猜想出题者的意图在哪里。 chan?sync.RWMutex?go?chan缓存池?迭代? 所以只能再读一次题目,就从迭代入手看看。 既然是迭代就会要求set.s全部可以遍历一次。但是chan是为缓存的,那就代表这写入一次就会阻塞。 我们把代码恢复为可以运行的方式,看看效果 package main import (          "sync"     "fmt")//下面的迭代会有什么问题?type threadSafeSet struct {     sync.RWMutex     s []interface{} } func(set*threadSafeSet)Iter() <-chaninterface{} {     //ch := make(chan interface{}) // 解除注释看看!     ch := make(chaninterface{},len(set.s))    gofunc() {         set.RLock()        forelem,value := range set.s {            ch <- elem             println("Iter:",elem,value)         }       close(ch)         set.RUnlock()     }()     return ch } funcmain() {     th:=threadSafeSet{         s:[]interface{}{"1","2"},     }     v:=<-th.Iter()     fmt.Sprintf("%s%v","ch",v) } 10 以下代码能编译过去吗?为什么? package main import (   "fmt") typePeople interface {     Speak(string) string } typeStduent struct{} func(stu*Stduent)Speak(think string)(talk string) {     ifthink == "bitch" {         talk = "Youare a good boy"     } else {         talk = "hi"     }     return } funcmain() {     var peoPeople = Stduent{}     think := "bitch"    fmt.Println(peo.Speak(think)) } 考点:golang的方法集 解答: 编译不通过! 做错了!?说明你对golang的方法集还有一些疑问。 一句话:golang的方法集仅仅影响接口实现和方法表达式转化,与通过实例或者指针调用方法无关。 11 以下代码打印出来什么内容,说出为什么。 package main import (   "fmt") typePeople interface {     Show() } typeStudent struct{} func(stuStudent)Show() { } funclive()People {     var stuStudent     return stu } funcmain() {   if live() == nil {         fmt.Println("AAAAAAA")     } else {         fmt.Println("BBBBBBB")     } } 考点:interface内部结构 解答: 很经典的题! 这个考点是很多人忽略的interface内部结构。 go中的接口分为两种一种是空的接口类似这样: varininterface{} 另一种如题目: type People interface {     Show() } 他们的底层结构如下: type eface struct {      //空接口     _type _type        //类型信息     data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void)} typeiface struct {      //带有方法的接口     tab  itab          //存储type信息还有结构实现方法的集合     data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void)} type_type struct {     size       uintptr //类型大小     ptrdata    uintptr //前缀持有所有指针的内存大小     hash       uint32  //数据hash值     tflag     tflag     align      uint8   //对齐     fieldalign uint8   //嵌入结构体时的对齐     kind       uint8   //kind 有些枚举值kind等于0是无效的     alg       *typeAlg //函数指针数组,类型实现的所有方法     gcdata    *byte   str       nameOff     ptrToThis typeOff }type itab struct {     inter  *interfacetype //接口类型     _type  *_type         //结构类型     link   *itab     bad    int32     inhash int32     fun    [1]uintptr     //可变大小方法集合} 可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口 结果: BBBBBBB 12.是否可以编译通过?如果通过,输出什么? func main() {     i := GetValue() switch i.(type) {          caseint:                println("int")            casestring:                println("string")            caseinterface{}:                println("interface")            default:                 println("unknown")     } } funcGetValue()int {    return1 } 解析 考点:type 编译失败,因为type只能使用在interface 13.下面函数有什么问题? func funcMui(x,y int)(sum int,error){     returnx+y,nil } 解析 考点:函数返回值命名 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。 14.是否可以编译通过?如果通过,输出什么? package mainfunc main() {    println(DeferFunc1(1)) println(DeferFunc2(1)) println(DeferFunc3(1)) }func DeferFunc1(i int)(t int) {     t = i   deferfunc() {         t += 3     }() return t } funcDeferFunc2(i int)int {     t := i  deferfunc() {         t += 3     }() return t } funcDeferFunc3(i int)(t int) {   deferfunc() {         t += i     }() return2} 解析 考点:defer和函数返回值 需要明确一点是defer需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数 DeferFunc1有函数返回值t作用域为整个函数,在return之前defer会被执行,所以t会被修改,返回4; DeferFunc2函数中t的作用域为函数,返回1;DeferFunc3返回3 15.是否可以编译通过?如果通过,输出什么? funcmain() {    list := new([]int)     list = append(list,1)     fmt.Println(list) } 解析 考点:new list:=make([]int,0) 16.是否可以编译通过?如果通过,输出什么? package mainimport "fmt"funcmain() {     s1 := []int{1, 2, 3}     s2 := []int{4, 5}     s1 = append(s1,s2)     fmt.Println(s1) } 解析 考点:append append切片时候别漏了'…' 17.是否可以编译通过?如果通过,输出什么? func main() {     sn1 := struct {         age  int         name string     }{age: 11,name: "qq"}     sn2 := struct {         age  int         name string     }{age: 11,name: "qq"}  if sn1== sn2 {         fmt.Println("sn1== sn2")     }     sm1 := struct {         age int         m   map[string]string     }{age: 11, m:map[string]string{"a": "1"}}     sm2 := struct {         age int         m   map[string]string     }{age: 11, m:map[string]string{"a": "1"}}             if sm1 == sm2 {         fmt.Println("sm1== sm2")     } } 解析 考点:结构体比较 进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。 sn3:= struct {     name string     age  int } {age:11,name:"qq"} sn3与sn1就不是相同的结构体了,不能比较。 还有一点需要注意的是结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice。 如果该结构属性都是可以比较的,那么就可以使用“==”进行比较操作。 可以使用reflect.DeepEqual进行比较 if reflect.DeepEqual(sn1, sm) {     fmt.Println("sn1==sm") }else {     fmt.Println("sn1!=sm") } 所以编译不通过: invalid operation: sm1 == sm2 18.是否可以编译通过?如果通过,输出什么? func Foo(x interface{}) {    if x== nil {         fmt.Println("emptyinterface")                 return     }     fmt.Println("non-emptyinterface") }        funcmain() {           var x *int = nil     Foo(x) } 解析 考点:interface内部结构 non-emptyinterface 19.是否可以编译通过?如果通过,输出什么? func GetValue(m map[int]string, id int)(string, bool) {              if _,exist := m[id]; exist {                    return"存在数据", true     }            returnnil, false}funcmain() {     intmap:=map[int]string{    1:"a",        2:"bb",        3:"ccc",     }     v,err:=GetValue(intmap,3)     fmt.Println(v,err) } 解析 考点:函数返回值类型 nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。报:cannot use nil as type string in return argument. 20.是否可以编译通过?如果通过,输出什么? const (     x = iota     y     z = "zz"     k     p = iota) funcmain()  {     fmt.Println(x,y,z,k,p) } 解析 考点:iota 结果: 0 1 zz zz 4 21.编译执行下面代码会出现什么? package mainvar(     size :=1024     max_size = size*2) funcmain() {     println(size,max_size) } 解析 考点:变量简短模式 变量简短模式限制: 定义变量同时显式初始化 不能提供数据类型 只能在函数内部使用 结果: syntaxerror: unexpected := 22.下面函数有什么问题? package main const cl = 100 var bl   = 123 funcmain() {     println(&bl,bl)    println(&cl,cl) } 解析 考点:常量 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用, cannot take the address of cl 23.编译执行下面代码会出现什么? package main funcmain() {     for i:=0;i<10;i++  {     loop:        println(i)     }    gotoloop } 解析 考点:goto goto不能跳转到其他函数或者内层代码 goto loop jumps intoblock starting at 24.编译执行下面代码会出现什么? package main import"fmt" funcmain() {      typeMyInt1 int      typeMyInt2 = int     var i int =9     var i1MyInt1 = i     var i2MyInt2 = i     fmt.Println(i1,i2) } 解析 考点:**Go 1.9 新特性 Type Alias ** 基于一个类型创建一个新类型,称之为defintion;基于一个类型创建一个别名,称之为alias。 MyInt1为称之为defintion,虽然底层类型为int类型,但是不能直接赋值,需要强转; MyInt2称之为alias,可以直接赋值。 结果: cannot use i (typeint) astype MyInt1 in assignment 25.编译执行下面代码会出现什么? package main import"fmt" typeUser struct { } typeMyUser1 User typeMyUser2 = User func(iMyUser1)m1(){     fmt.Println("MyUser1.m1") } func(iUser)m2(){     fmt.Println("User.m2") } funcmain() {     var i1MyUser1     var i2MyUser2     i1.m1()     i2.m2() } 解析 考点:**Go 1.9 新特性 Type Alias ** 因为MyUser2完全等价于User,所以具有其所有的方法,并且其中一个新增了方法,另外一个也会有。 但是 i1.m2() 是不能执行的,因为MyUser1没有定义该方法。 结果: MyUser1.m1User.m2 26.编译执行下面代码会出现什么? package main import"fmt" type T1 struct { } func(tT1)m1(){     fmt.Println("T1.m1") } type T2= T1 typeMyStruct struct {     T1     T2 } funcmain() {     my:=MyStruct{}     my.m1() } 解析 考点:**Go 1.9 新特性 Type Alias ** 是不能正常编译的,异常: ambiguousselectormy.m1 结果不限于方法,字段也也一样;也不限于type alias,type defintion也是一样的,只要有重复的方法、字段,就会有这种提示,因为不知道该选择哪个。 改为: my.T1.m1() my.T2.m1() type alias的定义,本质上是一样的类型,只是起了一个别名,源类型怎么用,别名类型也怎么用,保留源类型的所有方法、字段等。 27.编译执行下面代码会出现什么? package main import (           "errors"     "fmt") varErrDidNotWork = errors.New("did not work") funcDoTheThing(reallyDoItbool)(errerror) {     ifreallyDoIt {         result, err:= tryTheThing()         if err!= nil || result != "it worked" {            err = ErrDidNotWork         }     }    return err } functryTheThing()(string,error) {     return"",ErrDidNotWork } funcmain() {     fmt.Println(DoTheThing(true))     fmt.Println(DoTheThing(false)) } 解析 考点:变量作用域 因为 if 语句块内的 err 变量会遮罩函数作用域内的 err 变量,结果: 改为: func DoTheThing(reallyDoIt bool)(errerror) {     varresult string     ifreallyDoIt {         result, err =tryTheThing()         if err!= nil || result != "it worked" {            err = ErrDidNotWork         }     }    return err } 28.编译执行下面代码会出现什么? package main functest() []func() {     varfuns []func()     fori:=0;i<2;i++  {         funs = append(funs,func() {                       println(&i,i)         })     }    returnfuns } funcmain(){     funs:=test()            for_,f:=range funs{         f()     } } 解析 考点:闭包延迟求值 for循环复用局部变量i,每一次放入匿名函数的应用都是想一个变量。 结果: 0xc042046000 2 0xc042046000 2 如果想不一样可以改为: func test() []func()  {     varfuns []func()     fori:=0;i<2;i++  {         x:=i         funs = append(funs,func() {            println(&x,x)         })     }    returnfuns } 29.编译执行下面代码会出现什么? package main functest(x int)(func(),func()) {     returnfunc() {        println(x)     x+=10     }, func() {              println(x)     } } funcmain() {     a,b:=test(100)     a()     b() } 解析 考点:闭包引用相同变量* 结果: 100 110 30. 编译执行下面代码会出现什么? package main im port (   "fmt"     "reflect") funcmain1() {     deferfunc() {      iferr:=recover();err!=nil{           fmt.Println(err)        }else {           fmt.Println("fatal")        }     }()     deferfunc() {        panic("deferpanic")     }()     panic("panic") } funcmain() {     deferfunc() {        iferr:=recover();err!=nil{            fmt.Println("++++")            f:=err.(func()string)             fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())         }else {            fmt.Println("fatal")         }     }()     deferfunc() {        panic(func()string {            return "defer panic"         })     }()     panic("panic") } 解析 考点:panic仅有最后一个可以被revover捕获 触发panic("panic")后顺序执行defer,但是defer中还有一个panic,所以覆盖了之前的panic("panic") 原文链接:https://blog.csdn.net/itcastcpp/article/details/80462619

剑曼红尘 2020-03-09 10:46:30 0 浏览量 回答数 0

问题

如何定位Android NDK开发中遇到的错误?报错

爱吃鱼的程序员 2020-06-14 18:55:10 0 浏览量 回答数 1

问题

使用IBPP在C++中操作FireBird/Interbase数据库:报错

kun坤 2020-06-06 13:49:18 0 浏览量 回答数 1

回答

break; 11 case "0002,0013"://文件生成程序的标题 12 return "SH"; 13 break; 14 case "0008,0005"://文本编码 15 return "CS"; 16 break; 17 case "0008,0008": 18 return "CS"; 19 break; 20 case "0008,1032"://成像时间 21 return "SQ"; 22 break; 23 case "0008,1111": 24 return "SQ"; 25 break; 26 case "0008,0020"://检查日期 27 return "DA"; 28 break; 29 case "0008,0060"://成像仪器 30 return "CS"; 31 break; 32 case "0008,0070"://成像仪厂商 33 return "LO"; 34 break; 35 case "0008,0080": 36 return "LO"; 37 break; 38 case "0010,0010"://病人姓名 39 return "PN"; 40 break; 41 case "0010,0020"://病人id 42 return "LO"; 43 break; 44 case "0010,0030"://病人生日 45 return "DA"; 46 break; 47 case "0018,0060"://电压 48 return "DS"; 49 break; 50 case "0018,1030"://协议名 51 return "LO"; 52 break; 53 case "0018,1151": 54 return "IS"; 55 break; 56 case "0020,0010"://检查ID 57 return "SH"; 58 break; 59 case "0020,0011"://序列 60 return "IS"; 61 break; 62 case "0020,0012"://成像编号 63 return "IS"; 64 break; 65 case "0020,0013"://影像编号 66 return "IS"; 67 break; 68 case "0028,0002"://像素采样1为灰度3为彩色 69 return "US"; 70 break; 71 case "0028,0004"://图像模式MONOCHROME2为灰度 72 return "CS"; 73 break; 74 case "0028,0010"://row高 75 return "US"; 76 break; 77 case "0028,0011"://col宽 78 return "US"; 79 break; 80 case "0028,0100"://单个采样数据长度 81 return "US"; 82 break; 83 case "0028,0101"://实际长度 84 return "US"; 85 break; 86 case "0028,0102"://采样最大值 87 return "US"; 88 break; 89 case "0028,1050"://窗位 90 return "DS"; 91 break; 92 case "0028,1051"://窗宽 93 return "DS"; 94 break; 95 case "0028,1052": 96 return "DS"; 97 break; 98 case "0028,1053": 99 return "DS"; 100 break; 101 case "0040,0008"://文件夹标签 102 return "SQ"; 103 break; 104 case "0040,0260"://文件夹标签 105 return "SQ"; 106 break; 107 case "0040,0275"://文件夹标签 108 return "SQ"; 109 break; 110 case "7fe0,0010"://像素数据开始处 111 return "OW"; 112 break; 113 default: 114 return "UN"; 115 break; 116 } 117 } 复制代码 最关键的两个tag: 0002,0010 普通tag的读取方式 little字节序还是big字节序 隐式VR还是显示VR。由它的值决定 复制代码 1 switch (VFStr) 2 { 3 case "1.2.840.10008.1.2.10"://显示little 4 isLitteEndian = true; 5 isExplicitVR = true; 6 break; 7 case "1.2.840.10008.1.2.20"://显示big 8 isLitteEndian = false; 9 isExplicitVR = true; 10 break; 11 case "1.2.840.10008.1.20"://隐式little 12 isLitteEndian = true; 13 isExplicitVR = false; 14 break; 15 default: 16 break; 17 } 复制代码 7fe0,0010 像素数据开始处 整理 根据以上的分析相信解析一个dicom格式文件的过程已经很清晰了吧 第一步:跳过128字节导言部分,并读取"DICM"4个字符 以确认是dicom格式文件 第二步:读取第一部分 也就是非常重要的文件元dataElement 。读取所有0002开头的tag 并根据0002,0010的值确定传输语法。文件元tag部分的数据元素都是以显示VR的方式表示的 读取它的值 也就是字节码处理 别告诉我说你不会字节码处理哈。传输语法 说得那么官方,你就忽悠吧 其实就确定两个东西而已 1字节序 这个基本上都是little字节序。举个例子吧十进制数 35280 用十六进制表示是0xff00 但是存储到文件中你用十六进制编辑器打开你看到的是这个样子00ff 这就是little字节序。平常我们用的x86PC在windows下都是little字节序 包括AMD的CPU。别太较真 较真的话这个问题又可以写篇博客了。 2确定从0002以后的dataElement的VR是显示还是隐式。说来说去0002,0010的值就 那么固定几个 并且只能是那么几个 这些都在那个北美放射学会定义的dicom标准的第六章 有说明 : 1.2.840.10008.1.2 Implicit VR Little Endian: Default Transfer Syntax for DICOM Transfer Syntax 1.2.840.10008.1.2.1 Explicit VR Little Endian Transfer Syntax 1.2.840.10008.1.2.2 Explicit VR Big Endian Transfer Syntax 上面的那段代码其实就是这个表格的实现,讲到这里你会觉得多么的坑爹啊 是的dicom面向对象的破概念非常烦的。 第三步:读取普通tag 直到搜寻到7fe0,0010 这个最巨体的存储图像数据的 dataElement 它一个顶别人几十个 上百个。我们在前一步已经把VR是显示还是隐式确定 通过前面的图 ,也就是字节码处理而已无任何压力。显示情况下根据VR 和Len 确定数据类型 跟数据长度直接读取就可以了。隐式情况下这破玩艺儿有点烦,只能根据tag 字典确定它是什么VR再才能读取。关于这个字典也在dicom标准的第六章。上面倒数第二段代码已经把重要的字典都列了出来。 第四步:读取灰度像素数据并调窗 以GDI的方式显示出来。 说实话开始我还以为dicom这种号称医学什么影像的专家制定出来的标准 读取像素数据应该有难度吧 结果没想到这么的傻瓜。直接按像素从左到右从上到下 一行行依次扫描。两个字节表示1个像素普通Dicom格式存储的是16位的灰度图像,其实有效数据只有12位,除去0 所以最高值是2047。比如CT值 从-1000到+1000,空气的密度为-1000 水的密度为0 金属的密度为+1000 总共的值为2000 调窗技术: 即把12级灰度的数据 通过调节窗宽窗位并让他在RGB模式下显示出来。还技术呢 说实话这个也是没什么技术含量的所谓的技术,两句代码给你整明白。 调节窗宽窗位到底什么意思,12位的数据那么它总共有2047个等级的灰度 没有显示设备可以体现两千多级的明暗度 就算有我们肉眼也无法分辨更无法诊断。我们要诊断是要提取关键密度值的数据 在医院放射科呆久了你一定经常听医生讲什么骨窗 肺窗 之类的词儿,这就是指的这个“窗”。比如有病人骨折了打了钢板我们想看金属部分来诊断 那么我们应该抓取CT值从800到1000 密度的像素 也就是灰度值 然后把它放到RGB模式下显示,低于800的不论值大小都显示黑色 高于1000的不论值大小都显示白色。 通过以上例子那么这个范围1000-800=200 这个200表示窗宽,800+(200/2)这个表示窗位 一句话,从2047个等级的灰度里选取一个范围放到0~255的灰度环境里显示。 怎样把12位灰度影射到8位灰度显示出来呢,还怎么显示 上面方法都给说明了基本上算半成品了。联想到角度制弧度制,设要求的8位灰度值为x 已知的12位灰度值为y那么:x/255=y/2047 那么x=255y/2047 原理不多讲 等比中项十字相乘法 这个是初中的知识哈。初中没读过的童鞋飘过。。。 原理过程讲完了 代码走起 复制代码 1 class DicomHandler 2 { 3 string fileName = ""; 4 Dictionary tags = new Dictionary();//dicom文件中的标签 5 BinaryReader dicomFile;//dicom文件流 6 7 //文件元信息 8 public Bitmap gdiImg;//转换后的gdi图像 9 UInt32 fileHeadLen;//文件头长度 10 long fileHeadOffset;//文件数据开始位置 11 UInt32 pixDatalen;//像素数据长度 12 long pixDataOffset = 0;//像素数据开始位置 13 bool isLitteEndian = true;//是否小字节序(小端在前 、大端在前) 14 bool isExplicitVR = true;//有无VR 15 16 //像素信息 17 int colors;//颜色数 RGB为3 黑白为1 18 public int windowWith = 2048, windowCenter = 2048 / 2;//窗宽窗位 19 int rows, cols; 20 public void readAndShow(TextBox textBox1) 21 { 22 if (fileName == string.Empty) 23 return; 24 dicomFile = new BinaryReader(File.OpenRead(fileName)); 25 26 //跳过128字节导言部分 27 dicomFile.BaseStream.Seek(128, SeekOrigin.Begin); 28 29 if (new string(dicomFile.ReadChars(4)) != "DICM") 30 { 31 MessageBox.Show("没有dicom标识头,文件格式错误"); 32 return; 33 } 34 35 36 tagRead(); 37 38 IDictionaryEnumerator enor = tags.GetEnumerator(); 39 while (enor.MoveNext()) 40 { 41 if (enor.Key.ToString().Length > 9) 42 { 43 textBox1.Text += enor.Key.ToString() + "rn"; 44 textBox1.Text += enor.Value.ToString().Replace('0', ' '); 45 } 46 else 47 textBox1.Text += enor.Key.ToString() + enor.Value.ToString().Replace('0', ' ') + "rn"; 48 } 49 dicomFile.Close(); 50 } 51 public DicomHandler(string _filename) 52 { 53 fileName = _filename; 54 } 55 56 public void saveAs(string filename) 57 { 58 switch (filename.Substring(filename.LastIndexOf('.'))) 59 { 60 case ".jpg": 61 gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg); 62 break; 63 case ".bmp": 64 gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp); 65 break; 66 case ".png": 67 gdiImg.Save(filename, System.Drawing.Imaging.ImageFormat.Png); 68 break; 69 default: 70 break; 71 } 72 } 73 public bool getImg( )//获取图像 在图像数据偏移量已经确定的情况下 74 { 75 if (fileName == string.Empty) 76 return false; 77 78 int dataLen, validLen;//数据长度 有效位 79 int imgNum;//帧数 80 81 rows = int.Parse(tags["0028,0010"].Substring(5)); 82 cols = int.Parse(tags["0028,0011"].Substring(5)); 83 84 colors = int.Parse(tags["0028,0002"].Substring(5)); 85 dataLen = int.Parse(tags["0028,0100"].Substring(5)); 86 validLen = int.Parse(tags["0028,0101"].Substring(5)); 87 88 gdiImg = new Bitmap(cols, rows); 89 90 BinaryReader dicomFile = new BinaryReader(File.OpenRead(fileName)); 91 92 dicomFile.BaseStream.Seek(pixDataOffset, SeekOrigin.Begin); 93 94 long reads = 0; 95 for (int i = 0; i < gdiImg.Height; i++) 96 { 97 for (int j = 0; j < gdiImg.Width; j++) 98 { 99 if (reads >= pixDatalen) 100 break; 101 byte[] pixData = dicomFile.ReadBytes(dataLen / 8 * colors); 102 reads += pixData.Length; 103 104 Color c = Color.Empty; 105 if (colors == 1) 106 { 107 int grayGDI; 108 109 double gray = BitConverter.ToUInt16(pixData, 0); 110 //调窗代码,就这么几句而已 111 //1先确定窗口范围 2映射到8位灰度 112 int grayStart = (windowCenter - windowWith / 2); 113 int grayEnd = (windowCenter + windowWith / 2); 114 115 if (gray < grayStart) 116 grayGDI = 0; 117 else if (gray > grayEnd) 118 grayGDI = 255; 119 else 120 { 121 grayGDI = (int)((gray - grayStart) * 255 / windowWith); 122 } 123 124 if (grayGDI > 255) 125 grayGDI = 255; 126 else if (grayGDI < 0) 127 grayGDI = 0; 128 c = Color.FromArgb(grayGDI, grayGDI, grayGDI); 129 } 130 else if (colors == 3) 131 { 132 c = Color.FromArgb(pixData[0], pixData[1], pixData[2]); 133 } 134 135 gdiImg.SetPixel(j, i, c); 136 } 137 } 138 139 dicomFile.Close(); 140 return true; 141 } 142 void tagRead()//不断读取所有tag 及其值 直到碰到图像数据 (7fe0 0010 ) 143 { 144 bool enDir = false; 145 int leve = 0; 146 StringBuilder folderData = new StringBuilder();//该死的文件夹标签 147 string folderTag = ""; 148 while (dicomFile.BaseStream.Position + 6 < dicomFile.BaseStream.Length) 149 { 150 //读取tag 151 string tag = dicomFile.ReadUInt16().ToString("x4") + "," + 152 dicomFile.ReadUInt16().ToString("x4"); 153 154 string VR = string.Empty; 155 UInt32 Len = 0; 156 //读取VR跟Len 157 //对OB OW SQ 要做特殊处理 先置两个字节0 然后4字节值长度 158 //------------------------------------------------------这些都是在读取VR一步被阻断的情况 159 if (tag.Substring(0, 4) == "0002")//文件头 特殊情况 160 { 161 VR = new string(dicomFile.ReadChars(2)); 162 163 if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN") 164 { 165 dicomFile.BaseStream.Seek(2, SeekOrigin.Current); 166 Len = dicomFile.ReadUInt32(); 167 } 168 else 169 Len = dicomFile.ReadUInt16(); 170 } 171 else if (tag == "fffe,e000" || tag == "fffe,e00d" || tag == "fffe,e0dd")//文件夹标签 172 { 173 VR = "**"; 174 Len = dicomFile.ReadUInt32(); 175 } 176 else if (isExplicitVR == true)//有无VR的情况 177 { 178 VR = new string(dicomFile.ReadChars(2)); 179 180 if (VR == "OB" || VR == "OW" || VR == "SQ" || VR == "OF" || VR == "UT" || VR == "UN") 181 { 182 dicomFile.BaseStream.Seek(2, SeekOrigin.Current); 183 Len = dicomFile.ReadUInt32(); 184 } 185 else 186 Len = dicomFile.ReadUInt16(); 187 } 188 else if (isExplicitVR == false) 189 { 190 VR = getVR(tag);//无显示VR时根据tag一个一个去找 真烦啊。 191 Len = dicomFile.ReadUInt32(); 192 } 193 //判断是否应该读取VF 以何种方式读取VF 194 //-------------------------------------------------------这些都是在读取VF一步被阻断的情况 195 byte[] VF = { 0x00 }; 196 197 if (tag == "7fe0,0010")//图像数据开始了 198 { 199 pixDatalen = Len; 200 pixDataOffset = dicomFile.BaseStream.Position; 201 dicomFile.BaseStream.Seek(Len, SeekOrigin.Current); 202 VR = "UL"; 203 VF = BitConverter.GetBytes(Len); 204 } 205 else if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue))//靠 遇到文件夹开始标签了 206 { 207 if (enDir == false) 208 { 209 enDir = true; 210 folderData.Remove(0, folderData.Length); 211 folderTag = tag; 212 } 213 else 214 { 215 leve++;//VF不赋值 216 } 217 } 218 else if ((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue))//文件夹结束标签 219 { 220 if (enDir == true) 221 { 222 enDir = false; 223 } 224 else 225 { 226 leve--; 227 } 228 } 229 else 230 VF = dicomFile.ReadBytes((int)Len); 231 232 string VFStr; 233 234 VFStr = getVF(VR, VF); 235 236 //----------------------------------------------------------------针对特殊的tag的值的处理 237 //特别针对文件头信息处理 238 if (tag == "0002,0000") 239 { 240 fileHeadLen = Len; 241 fileHeadOffset = dicomFile.BaseStream.Position; 242 } 243 else if (tag == "0002,0010")//传输语法 关系到后面的数据读取 244 { 245 switch (VFStr) 246 { 247 case "1.2.840.10008.1.2.10"://显示little 248 isLitteEndian = true; 249 isExplicitVR = true; 250 break; 251 case "1.2.840.10008.1.2.20"://显示big 252 isLitteEndian = false; 253 isExplicitVR = true; 254 break; 255 case "1.2.840.10008.1.20"://隐式little 256 isLitteEndian = true; 257 isExplicitVR = false; 258 break; 259 default: 260 break; 261 } 262 } 263 for (int i = 1; i <= leve; i++) 264 tag = "--" + tag; 265 //------------------------------------数据搜集代码 266 if ((VR == "SQ" && Len == UInt32.MaxValue) || (tag == "fffe,e000" && Len == UInt32.MaxValue) || leve > 0)//文件夹标签代码 267 { 268 folderData.AppendLine(tag + "(" + VR + "):" + VFStr); 269 } 270 else if (((tag == "fffe,e00d" && Len == UInt32.MinValue) || (tag == "fffe,e0dd" && Len == UInt32.MinValue)) && leve == 0)//文件夹结束标签 271 { 272 folderData.AppendLine(tag + "(" + VR + "):" + VFStr); 273 tags.Add(folderTag + "SQ", folderData.ToString()); 274 } 275 else 276 tags.Add(tag, "(" + VR + "):" + VFStr); 277 } 278 } 279 } 复制代码 好了收工。 测试下成果 复制代码 1 if (openFileDialog1.ShowDialog() != DialogResult.OK) 2 return; 3 4 string fileName = openFileDialog1.FileName; 5 6 handler = new DicomHandler(fileName); 7 8 handler.readAndShow(textBox1); 9 10 this.Text = "DicomViewer-" + openFileDialog1.FileName; 11 12 13 backgroundWorker1.RunWorkerAsync(); 复制代码 这里处理gdi位图的时候直接用的setPix 处理速度比较慢所以用了backgroundWorker,实际应用中请使用内存缓冲跟指针的方式 否则效率低了是得不到客户的认可的哦,gdi位图操作可使用lockBits加指针的方式 ,12位的灰度像素数据可以第一次读取后缓存到内存中 以方便后面调窗的快速读取 优化这点代码也不难哈 对指针什么的熟点就行了,前几章都有。 这是ezDicom 经过公认测试的软件 我们来跟他对比一下,打开 调窗测试,我们注意到两个东西 在没有窗宽窗位时 默认窗宽是2047+1即2048 窗位是2048/2即1024 直观的感受是调窗宽像在调图像对比度 ,调窗位像在调图像亮度。 窗宽为255的时候图像是最瑞丽的 因为255其实就是8位图像的默认窗宽。 注意窗位那里有小小区别,ez窗位显示的是根据1024那里为0开始偏移 而我的程序是根据窗宽中间值没有偏移 没有偏移的情况稍微符合逻辑点吧。 但是可以看到原理是一样的 结果是一样的。

爵霸 2019-12-02 02:13:35 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 云栖号物联网 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站 云栖号弹性计算 阿里云云栖号 云栖号案例 云栖号直播