• 关于

    对象在内存中存在形式、函数传递对象方式

    的搜索结果

回答

一。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

回答

python 读写、创建 文件的方法: python中对文件、文件夹(文件操作函数)的操作需要涉及到os模块和shutil模块。 得到当前工作目录,即当前Python脚本工作的目录路径: os.getcwd() 返回指定目录下的所有文件和目录名:os.listdir() 函数用来删除一个文件:os.remove() 删除多个目录:os.removedirs(r“c:python”) 检验给出的路径是否是一个文件:os.path.isfile() 检验给出的路径是否是一个目录:os.path.isdir() 判断是否是绝对路径:os.path.isabs() 检验给出的路径是否真地存:os.path.exists() 返回一个路径的目录名和文件名:os.path.split() eg os.path.split(‘/home/swaroop/byte/code/poem.txt’) 结果:(‘/home/swaroop/byte/code’, ‘poem.txt’) 分离扩展名:os.path.splitext() 获取路径名:os.path.dirname() 获取文件名:os.path.basename() 运行shell命令: os.system() 读取和设置环境变量:os.getenv() 与os.putenv() 给出当前平台使用的行终止符:os.linesep Windows使用’rn’,Linux使用’n’而Mac使用’r’ 指示你正在使用的平台:os.name 对于Windows,它是’nt’,而对于Linux/Unix用户,它是’posix’ 重命名:os.rename(old, new) 创建多级目录:os.makedirs(r“c:pythontest”) 创建单个目录:os.mkdir(“test”) 获取文件属性:os.stat(file) 修改文件权限与时间戳:os.chmod(file) 终止当前进程:os.exit() 获取文件大小:os.path.getsize(filename) 文件操作: os.mknod(“test.txt”) 创建空文件 fp = open(“test.txt”,w) 直接打开一个文件,如果文件不存在则创建文件 关于open 模式: w 以写方式打开, a 以追加模式打开 (从 EOF 开始, 必要时创建新文件) r+ 以读写模式打开 w+ 以读写模式打开 (参见 w ) a+ 以读写模式打开 (参见 a ) rb 以二进制读模式打开 wb 以二进制写模式打开 (参见 w ) ab 以二进制追加模式打开 (参见 a ) rb+ 以二进制读写模式打开 (参见 r+ ) wb+ 以二进制读写模式打开 (参见 w+ ) ab+ 以二进制读写模式打开 (参见 a+ ) fp.read([size]) #size为读取的长度,以byte为单位 fp.readline([size]) #读一行,如果定义了size,有可能返回的只是一行的一部分 fp.readlines([size]) #把文件每一行作为一个list的一个成员,并返回这个list。其实它的内部是通过循环调用readline()来实现的。如果提供size参数,size是表示读取内容的总长,也就是说可能只读到文件的一部分。 fp.write(str) #把str写到文件中,write()并不会在str后加上一个换行符 fp.writelines(seq) #把seq的内容全部写到文件中(多行一次性写入)。这个函数也只是忠实地写入,不会在每行后面加上任何东西。 fp.close() #关闭文件。python会在一个文件不用后自动关闭文件,不过这一功能没有保证,最好还是养成自己关闭的习惯。 如果一个文件在关闭后还对其进行操作会产生ValueError fp.flush() #把缓冲区的内容写入硬盘 fp.fileno() #返回一个长整型的”文件标签“ fp.isatty() #文件是否是一个终端设备文件(unix系统中的) fp.tell() #返回文件操作标记的当前位置,以文件的开头为原点 fp.next() #返回下一行,并将文件操作标记位移到下一行。把一个file用于for … in file这样的语句时,就是调用next()函数来实现遍历的。 fp.seek(offset[,whence]) #将文件打操作标记移到offset的位置。这个offset一般是相对于文件的开头来计算的,一般为正数。但如果提供了whence参数就不一定了,whence可以为0表示从头开始计算,1表示以当前位置为原点计算。2表示以文件末尾为原点进行计算。需要注意,如果文件以a或a+的模式打开,每次进行写操作时,文件操作标记会自动返回到文件末尾。 fp.truncate([size]) #把文件裁成规定的大小,默认的是裁到当前文件操作标记的位置。如果size比文件的大小还要大,依据系统的不同可能是不改变文件,也可能是用0把文件补到相应的大小,也可能是以一些随机的内容加上去。 目录操作: os.mkdir(“file”) 创建目录 复制文件: shutil.copyfile(“oldfile”,”newfile”) oldfile和newfile都只能是文件 shutil.copy(“oldfile”,”newfile”) oldfile只能是文件夹,newfile可以是文件,也可以是目标目录 复制文件夹: shutil.copytree(“olddir”,”newdir”) olddir和newdir都只能是目录,且newdir必须不存在 重命名文件(目录) os.rename(“oldname”,”newname”) 文件或目录都是使用这条命令 移动文件(目录) shutil.move(“oldpos”,”newpos”) 删除文件 os.remove(“file”) 删除目录 os.rmdir(“dir”)只能删除空目录 shutil.rmtree(“dir”) 空目录、有内容的目录都可以删 转换目录 os.chdir(“path”) 换路径 Python读写文件 1.open 使用open打开文件后一定要记得调用文件对象的close()方法。比如可以用try/finally语句来确保最后能关闭文件。 file_object = open(‘thefile.txt’) try: all_the_text = file_object.read( ) finally: file_object.close( ) 注:不能把open语句放在try块里,因为当打开文件出现异常时,文件对象file_object无法执行close()方法。 2.读文件 读文本文件 input = open(‘data’, ‘r’) 第二个参数默认为r input = open(‘data’) 读二进制文件 input = open(‘data’, ‘rb’) 读取所有内容 file_object = open(‘thefile.txt’) try: all_the_text = file_object.read( ) finally: file_object.close( ) 读固定字节 file_object = open(‘abinfile’, ‘rb’) try: while True: chunk = file_object.read(100) if not chunk: break do_something_with(chunk) finally: file_object.close( ) 读每行 list_of_all_the_lines = file_object.readlines( ) 如果文件是文本文件,还可以直接遍历文件对象获取每行: for line in file_object: process line 3.写文件 写文本文件 output = open(‘data’, ‘w’) 写二进制文件 output = open(‘data’, ‘wb’) 追加写文件 output = open(‘data’, ‘w+’) 写数据 file_object = open(‘thefile.txt’, ‘w’) file_object.write(all_the_text) file_object.close( ) 写入多行 file_object.writelines(list_of_text_strings) 注意,调用writelines写入多行在性能上会比使用write一次性写入要高。 在处理日志文件的时候,常常会遇到这样的情况:日志文件巨大,不可能一次性把整个文件读入到内存中进行处理,例如需要在一台物理内存为 2GB 的机器上处理一个 2GB 的日志文件,我们可能希望每次只处理其中 200MB 的内容。 在 Python 中,内置的 File 对象直接提供了一个 readlines(sizehint) 函数来完成这样的事情。以下面的代码为例: file = open(‘test.log’, ‘r’)sizehint = 209715200 # 200Mposition = 0lines = file.readlines(sizehint)while not file.tell() - position < 0: position = file.tell() lines = file.readlines(sizehint) 每次调用 readlines(sizehint) 函数,会返回大约 200MB 的数据,而且所返回的必然都是完整的行数据,大多数情况下,返回的数据的字节数会稍微比 sizehint 指定的值大一点(除最后一次调用 readlines(sizehint) 函数的时候)。通常情况下,Python 会自动将用户指定的 sizehint 的值调整成内部缓存大小的整数倍。 file在python是一个特殊的类型,它用于在python程序中对外部的文件进行操作。在python中一切都是对象,file也不例外,file有file的方法和属性。下面先来看如何创建一个file对象: file(name[, mode[, buffering]]) file()函数用于创建一个file对象,它有一个别名叫open(),可能更形象一些,它们是内置函数。来看看它的参数。它参数都是以字符串的形式传递的。name是文件的名字。 mode是打开的模式,可选的值为r w a U,分别代表读(默认) 写 添加支持各种换行符的模式。用w或a模式打开文件的话,如果文件不存在,那么就自动创建。此外,用w模式打开一个已经存在的文件时,原有文件的内容会被清空,因为一开始文件的操作的标记是在文件的开头的,这时候进行写操作,无疑会把原有的内容给抹掉。由于历史的原因,换行符在不同的系统中有不同模式,比如在 unix中是一个n,而在windows中是‘rn’,用U模式打开文件,就是支持所有的换行模式,也就说‘r’ ‘n’ ‘rn’都可表示换行,会有一个tuple用来存贮这个文件中用到过的换行符。不过,虽说换行有多种模式,读到python中统一用n代替。在模式字符的后面,还可以加上+ b t这两种标识,分别表示可以对文件同时进行读写操作和用二进制模式、文本模式(默认)打开文件。 buffering如果为0表示不进行缓冲;如果为1表示进行“行缓冲“;如果是一个大于1的数表示缓冲区的大小,应该是以字节为单位的。 file对象有自己的属性和方法。先来看看file的属性。 closed #标记文件是否已经关闭,由close()改写 encoding #文件编码 mode #打开模式 name #文件名 newlines #文件中用到的换行模式,是一个tuple softspace #boolean型,一般为0,据说用于print file的读写方法: F.read([size]) #size为读取的长度,以byte为单位 F.readline([size]) 读一行,如果定义了size,有可能返回的只是一行的一部分 F.readlines([size]) 把文件每一行作为一个list的一个成员,并返回这个list。其实它的内部是通过循环调用readline()来实现的。如果提供size参数,size是表示读取内容的总长,也就是说可能只读到文件的一部分。 F.write(str) 把str写到文件中,write()并不会在str后加上一个换行符 F.writelines(seq) 把seq的内容全部写到文件中。这个函数也只是忠实地写入,不会在每行后面加上任何东西。 file的其他方法: F.close() 关闭文件。python会在一个文件不用后自动关闭文件,不过这一功能没有保证,最好还是养成自己关闭的习惯。如果一个文件在关闭后还对其进行操作会产生ValueError F.flush() 把缓冲区的内容写入硬盘 F.fileno() 返回一个长整型的”文件标签“ F.isatty() 文件是否是一个终端设备文件(unix系统中的) F.tell() 返回文件操作标记的当前位置,以文件的开头为原点 F.next() 返回下一行,并将文件操作标记位移到下一行。把一个file用于for … in file这样的语句时,就是调用next()函数来实现遍历的。 F.seek(offset[,whence]) 将文件打操作标记移到offset的位置。这个offset一般是相对于文件的开头来计算的,一般为正数。但如果提供了whence参数就不一定了,whence可以为0表示从头开始计算,1表示以当前位置为原点计算。2表示以文件末尾为原点进行计算。需要注意,如果文件以a或a+的模式打开,每次进行写操作时,文件操作标记会自动返回到文件末尾。 F.truncate([size]) 把文件裁成规定的大小,默认的是裁到当前文件操作标记的位置。如果size比文件的大小还要大,依据系统的不同可能是不改变文件,也可能是用0把文件补到相应的大小,也可能是以一些随机的内容加上去。

元芳啊 2019-12-02 01:04:30 0 浏览量 回答数 0

回答

浅谈Flutter框架原理及其生态圈 Flutter的锋芒 跨平台高性能的渲染引擎逐渐成为移动端、大前端领域的一个热点,作为其中的明星框架Flutter,经过近几年来的迅速发展,由极大的可能成为下一代跨端终端解决方案。自从2017 年 5 月,谷歌公司发布的了 Alpha 版本的 Flutter; 2018 年底 Flutter Live 发布的 1.0 版本;2019年7月发布1.5版本,截止今日(2020年2月)已经发布了v1.14.6 Beta版本。 在Flutter诞生之前,已经有许多跨平台UI框架的方案如Cordova、ReactNative、weex、uni-app、Hippy等,常见的需要处理兼容的终端平台也包括android、ios、web、Iot等,但是在大前端的浪潮下,对于企业和开发者来说开发效率和使用体验都十分重要,传统的做法莫过于分不同的团队开发不同的终端项目,如果还要继续向其他平台,拓展的话,我们需要付出的成本和时间将成倍增长。正因为如此,在这样的背景下,Flutter等跨端框架的兴起,从本质上讲,帮助开发者增加业务代码的复用率,减少因为要适配多个平台带来的工作量,从而降低开发成本、提高开发效率。 纵观已有的跨端方案,可以分为三类:Web 容器、泛 Web 容器、自绘引擎框架。 基于web容器即基于浏览器的跨平台也做得越来越好,自然管线也越来越短,与native的一些技术手段来实现性能上的相互补充。比如Egret、Cocos、Laya这些游戏引擎,它们在跨平台方面的做法多以Typescript编写,在iOS和安卓平台的各种浏览器中轻松的运行HTML5游戏,并在不同平台浏览器里提供近乎一致的用户体验,比如Egret还会提供高效的 JS-C Binding 编译机制,以满足游戏编译为原生格式的需求,不过大多数HTML游戏引擎也属于web容器这个范畴内。web容器框架也有一个明显的致命(在对体验&性能有较高要求的情况下)的缺点,那就是WebView的渲染效率和JavaScript执行性能太差。再加上Android各个系统版本和设备厂商的定制,很难保证所在所有设备上都能提供一致的体验。 泛 Web 容器框架比如ReactNative和Weex,即上层通过面向前端友好的UI,下层通过native的渲染形式,虽然同样使用类HTML+JS的UI构建逻辑,但是最终会生成对应的自定义原生控件,以充分利用原生控件相对于WebView的较高的绘制效率,同时H5与native相互补充来达到更好的用户体验,这也是一种很好的解决方案。缺陷也很明显,随着系统版本变化和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现,这样框架的跨平台特性就会大打折扣。 自绘引擎框架这里专指Flutter框架,从底层就承担跨端的任务和渲染方式,从目前来看,从技术的实现和方案的成熟度、产品的性能方面比较,Flutter有很大可能成为下一代主流跨平台框架。 Flutter与其他跨端框架的不同点之一就是自带渲染引擎,Flutter渲染引擎依靠跨平台的Skia图形库来实现,Skia引擎会将使用Dart语言构建的抽象的视图结构数据加工成GPU数据,交由 OpenGL 最终提供给 GPU 渲染,至此完成渲染闭环,因此可以在最大程度上保证一款应用在不同平台、不同设备上的体验一致性。 而开发语言选用的是同时支持 JIT和 AOT的 Dart语言,Dart本身提供了三种运行方式,应对web环境,用Dart2js编译成JavaScript代码,运行在常规浏览器中;使用DartVM直接在命令行中运行Dart代码;AOT方式编译成机器码,例如Flutter App框架。而且Dart 避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错,不仅保证了开发效率,代码性能和用户体验也更卓越。因此,Flutter在各类跨平台移动开发方案中脱颖而出。同时在去年2019的Google IO大会上,备受关注的Fuchsia系统虽然并没有发布,但是宣布了 Flutter除了支持开发 Android 和 iOS 程序之外,现在还支持开发Web程序了,在 I/O 大会上,谷歌发布了 Web 版 Flutter 的首个技术预览版,宣布 Flutter 将为包括 Google Home Hub 在内的 Google Smart Display 平台提供技术支持,并迈出利用 Chrome 操作系统支持桌面级应用的第一步。 很多JS开发者会思考Google Flutter团队至于为啥选择Dart而不是JS,其实Google 公司给出的原因很简单也很直接:Dart 语言开发组就在隔壁,对于 Flutter 需要的一些语言新特性,能够快速在语法层面落地实现;而如果选择了 JavaScript,就必须经过各种委员会(TC39等)和浏览器提供商漫长的决议。 Flutter绘制原理 在计算机系统中,图像的显示需要 CPU、GPU 和显示器一起配合完成:CPU 负责图像数据计算,GPU 负责图像数据渲染,而显示器则负责最终图像显示。 CPU 把计算好的、需要显示的内容交给 GPU,由 GPU 完成渲染后放入帧缓冲区,随后视频控制器根据垂直同步信号(VSync)以每秒 60 次的速度,从帧缓冲区读取帧数据交由显示器完成图像显示。 操作系统在呈现图像时遵循了这种机制,而 Flutter 作为跨平台开发框架也采用了这种底层方案。下面有一张更为详尽的示意图来解释 Flutter 的绘制原理。可以看到,Flutter 关注如何尽可能快地在两个硬件时钟的 VSync 信号之间计算并合成视图数据,然后通过 Skia 交给 GPU 渲染:UI 线程使用 Dart 来构建视图结构数据,这些数据会在 GPU 线程进行图层合成,随后交给 Skia 引擎加工成 GPU 数据,而这些数据会通过 OpenGL 最终提供给 GPU 渲染。 Skia原理 Skia 是一款用由C++ 开发的2D 图像绘制引擎。在2005 年被 Google 公司收购后被广泛应用在 Android和其他等核心产品上,Skia 目前是Android 官方的图像渲染引擎,因此 Flutter Android SDK 无需内嵌 Skia 引擎就可以获得天然的 Skia 支持;而对于 iOS 平台来说,由于 Skia 是跨平台的,因此它作为 Flutter iOS 渲染引擎被嵌入到 Flutter 的 iOS SDK 中,替代了 iOS 闭源的 Core Graphics/Core Animation/Core Text,这也正是 Flutter iOS SDK 打包的 App 包体积比 Android 要大一些的原因。 底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。 Flutter架构 Framework底层是Flutter引擎,引擎主要负责图形绘制(Skia)、文字排版(libtxt)和提供Dart运行时,引擎全部使用C++实现,Framework层使我们可以用Dart语言调用引擎的强大能力。Flutter 架构采用分层设计,从下到上分为三层,依次为:Embedder、Engine、Framework。 Embedder 是操作系统适配层,实现了渲染 Surface 设置,线程设置,以及平台插件等平台相关特性的适配。从这里我们可以看到,Flutter 平台相关特性并不多,这就使得从框架层面保持跨端一致性的成本相对较低。 Engine 层主要包含 Skia、Dart 和 Text,实现了 Flutter 的渲染引擎、文字排版、事件处理和 Dart 运行时等功能。Skia 和 Text 为上层接口提供了调用底层渲染和排版的能力,Dart 则为 Flutter 提供了运行时调用 Dart 和渲染引擎的能力。而 Engine 层的作用,则是将它们组合起来,从它们生成的数据中实现视图渲染。 Framework 层则是一个用 Dart 实现的 UI SDK,包含了动画、图形绘制和手势识别等功能。为了在绘制控件等固定样式的图形时提供更直观、更方便的接口,Flutter 还基于这些基础能力,根据 Material 和 Cupertino 两种视觉设计风格封装了一套 UI 组件库,开发者可以直接使用这些组件库。 Flutter运行流程 页面中的各界面元素(Widget)以树的形式组织,即控件树。Flutter 通过控件树中的每个控件创建不同类型的渲染对象,组成渲染对象树。在Flutter界面渲染过程分为三个阶段:布局、绘制、合成,布局和绘制在Flutter框架中完成,合成则交由引擎负责。 Flutter 采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,最终完成布局过程。这里只需要注意一点,无论布局还是绘制,都是父子间的遍历关系:父Widget的布局需要依赖子Widget的布局结果;而绘制则反过来(子Widget需要盖在父Widget上),布局是后续遍历,绘制是前序遍历,他们都是深度优先遍历。 Flutter生命周期 可以看到,Flutter中State 的生命周期可以分为 3 个阶段:创建(插入视图树)、更新(在视图树中存在)、销毁(从视图树中移除)。接下来,我们一起看看每一个阶段的具体流程。 第一步创建 State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染。构造方法是 State 生命周期的起点,Flutter 会通过调用StatefulWidget.createState() 来创建一个 State。我们可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据。这些配置数据,决定了 Widget 最初的呈现效果。 initState,会在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作,比如为状态变量设定默认值。 didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。 build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。 第二步更新 Widget 的状态更新,主要由个方法触发:setState、didchangeDependencies、didUpdateWidget。 setState:我们最熟悉的方法之一。当状态数据发生变化时,我们总是通过调用这个方法告诉 Flutter:“我这儿的数据变啦,请使用更新后的数据重建 UI!” didChangeDependencies:State 对象的依赖关系发生变化后,Flutter 会回调这个方法,随后触发组件构建。哪些情况下 State 对象的依赖关系会发生变化呢?典型的场景是,系统语言 Locale 或应用主题改变时,系统会通知 State 执行 didChangeDependencies 回调方法。 didUpdateWidget:当 Widget 的配置发生变化时,比如,父 Widget 触发重建(即父 Widget 的状态发生变化时),热重载时,系统会调用这个函数。一旦这三个方法被调用,Flutter 随后就会销毁老 Widget,并调用 build 方法重建 Widget。 第三步销毁 比如组件被移除,或是页面销毁的时候,系统会调用 deactivate 和 dispose 这两个方法,来移除或销毁组件。 Flutter生态圈及其常用框架 一项技术一个框架是否流行,最直观的体现就是它的生态圈是否活跃,下面列举了一些Flutter开发中常用的库工具。 参考文献 1、[Flutter原理与实践](https://tech.meituan.com/2018/08/09/waimai-flutter-practice.html) 少杰 2、[Flutter框架技术概览](https://flutter.dev/docs/resources/technical-overview) 3、[Flutter中文官网](https://pub.dartlang.org/flutter/) 4、[Flutter插件仓库](https://pub.dev/flutter/packages)

罗思雨 2020-02-27 11:47:50 0 浏览量 回答数 0

阿里云爆款特惠专场,精选爆款产品低至0.95折!

爆款ECS云服务器8.1元/月起,云数据库低至1.5折,限时抢购!

问题

Spark,一种快速数据分析替代方案:报错

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

问题

【精品问答】Python二级考试题库

珍宝珠 2019-12-01 22:03:38 1146 浏览量 回答数 2

回答

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? 遍历方式有以下几种: for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持 Random Access,如LinkedList。 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 说一下 ArrayList 的优缺点 ArrayList的优点如下: ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。ArrayList 在顺序添加一个元素的时候非常方便。 ArrayList 的缺点如下: 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。插入元素的时候,也需要做一次元素复制操作,缺点同上。 ArrayList 比较适合顺序添加、随机访问的场景。 如何实现数组和 List 之间的转换? 数组转 List:使用 Arrays. asList(array) 进行转换。List 转数组:使用 List 自带的 toArray() 方法。 代码示例: ArrayList 和 LinkedList 的区别是什么? 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 补充:数据结构基础之双向链表 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 ArrayList 和 Vector 的区别是什么? 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。性能:ArrayList 在性能方面要优于 Vector。扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。 LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。 多线程场景下如何使用 ArrayList? ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: 为什么 ArrayList 的 elementData 加上 transient 修饰? ArrayList 中的数组定义如下: private transient Object[] elementData; 再看一下 ArrayList 的定义: public class ArrayList extends AbstractList implements List<E>, RandomAccess, Cloneable, java.io.Serializable 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。 List 和 Set 的区别 List , Set 都是继承自Collection 接口 List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。 Set和List对比 Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变 Set接口 说一下 HashSet 的实现原理? HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 HashSet如何检查重复?HashSet是如何保证数据不可重复的? 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 HashSet 中的add ()方法会使用HashMap 的put()方法。 HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 以下是HashSet 部分源码: hashCode()与equals()的相关规定: 如果两个对象相等,则hashcode一定也是相同的 两个对象相等,对两个equals方法返回true 两个对象有相同的hashcode值,它们也不一定是相等的 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 ** ==与equals的区别** ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同 HashSet与HashMap的区别 Queue BlockingQueue是什么? Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。 在 Queue 中 poll()和 remove()有什么区别? 相同点:都是返回第一个元素,并在队列中删除返回的对象。 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。 代码示例: Queue queue = new LinkedList (); queue. offer("string"); // add System. out. println(queue. poll()); System. out. println(queue. remove()); System. out. println(queue. size()); Map接口 说一下 HashMap 的实现原理? HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 HashMap 基于 Hash 算法实现的 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。 JDK1.8之前 JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 JDK1.8之后 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 JDK1.7 VS JDK1.8 比较 JDK1.8主要解决或优化了一下问题: resize 扩容优化引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 HashMap的put方法的具体流程? 当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 putVal方法执行流程图 ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 HashMap的扩容操作是怎么实现的? ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; ②.每次扩展的时候,都是扩展2倍; ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 HashMap是怎么解决哈希冲突的? 答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行; 什么是哈希? Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。 什么是哈希冲突? 当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。 HashMap的数据结构 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突: 这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化 hash()函数 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) } 这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动); JDK1.8新增红黑树 通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn); 总结 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: 使用链地址法(使用散列表)来链接拥有相同hash值的数据;使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;引入红黑树进一步降低遍历的时间复杂度,使得遍历更快; **能否使用任何类作为 Map 的 key? **可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 为什么HashMap中String、Integer这样的包装类适合作为K? 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; 如果使用Object作为HashMap的Key,应该怎么办呢? 答:重写hashCode()和equals()方法 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性; HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标 答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; 那怎么解决呢? HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; HashMap 的长度为什么是2的幂次方 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 这个算法应该如何设计呢? 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 那为什么是两次扰动呢? 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; HashMap 与 HashTable 有什么区别? 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 如何决定使用 HashMap 还是 TreeMap? 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 HashMap 和 ConcurrentHashMap 的区别 ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 ConcurrentHashMap 和 Hashtable 的区别? ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 两者的对比图: HashTable: JDK1.7的ConcurrentHashMap: JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? JDK1.7 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 JDK1.8 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 结构如下: 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; 辅助工具类 Array 和 ArrayList 有何区别? Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。 如何实现 Array 和 List 之间的转换? Array 转 List: Arrays. asList(array) ;List 转 Array:List 的 toArray() 方法。 comparable 和 comparator的区别? comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). 方法如何比较元素? TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 Collections 工具类的 sort 方法有两种重载的形式, 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

剑曼红尘 2020-03-24 14:41:57 0 浏览量 回答数 0

回答

1.   【初级】下面属于关键字的是() A. func B. def C. struct D. class 参考答案:AC   2.   【初级】定义一个包内全局字符串变量,下面语法正确的是() A. var str string B. str := "" C. str = "" D. var str = "" 参考答案:AD   3.   【初级】通过指针变量 p 访问其成员变量 name,下面语法正确的是() A. p.name B. (*p).name C. (&p).name D. p->name 参考答案:AB   4.   【初级】关于接口和类的说法,下面说法正确的是() A. 一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口 B. 实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理 C. 类实现接口时,需要导入接口所在的包 D. 接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口 参考答案:ABD   5.   【初级】关于字符串连接,下面语法正确的是() A. str := ‘abc’ + ‘123’ B. str := "abc" + "123" C. str := '123' + "abc" D. fmt.Sprintf("abc%d", 123) 参考答案:BD   6.   【初级】关于协程,下面说法正确是() A. 协程和线程都可以实现程序的并发执行 B. 线程比协程更轻量级 C. 协程不存在死锁问题 D. 通过channel来进行协程间的通信 参考答案:AD   7.   【中级】关于init函数,下面说法正确的是() A. 一个包中,可以包含多个init函数 B. 程序编译时,先执行导入包的init函数,再执行本包内的init函数 C. main包中,不能有init函数 D. init函数可以被其他函数调用 参考答案:AB   8.   【初级】关于循环语句,下面说法正确的有() A. 循环语句既支持for关键字,也支持while和do-while B. 关键字for的基本使用方法与C/C++中没有任何差异 C. for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环 D. for循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量  参考答案:CD   9.   【中级】对于函数定义: func add(args ...int) int {  sum :=0  for _,arg := range args {     sum += arg  }  returnsum } 下面对add函数调用正确的是() A. add(1, 2) B. add(1, 3, 7) C. add([]int{1, 2}) D. add([]int{1, 3, 7}...) 参考答案:ABD   【初级】关于类型转化,下面语法正确的是() A. type MyInt int var i int = 1 var jMyInt = i B. type MyIntint var i int= 1 var jMyInt = (MyInt)i C. type MyIntint var i int= 1 var jMyInt = MyInt(i) D. type MyIntint var i int= 1 var jMyInt = i.(MyInt) 参考答案:C   【初级】关于局部变量的初始化,下面正确的使用方式是() A. var i int = 10 B. var i = 10 C. i := 10 D. i = 10 参考答案:ABC   【初级】关于const常量定义,下面正确的使用方式是() A. const Pi float64 = 3.14159265358979323846 const zero= 0.0 B. const ( size int64= 1024 eof = -1 ) C. const ( ERR_ELEM_EXISTerror = errors.New("element already exists") ERR_ELEM_NT_EXISTerror = errors.New("element not exists") ) D. const u, vfloat32 = 0, 3 const a,b, c = 3, 4, "foo" 参考答案:ABD   【初级】关于布尔变量b的赋值,下面错误的用法是() A. b = true B. b = 1 C. b = bool(1) D. b = (1 == 2) 参考答案:BC   【中级】下面的程序的运行结果是() func main() {   if (true) {    defer fmt.Printf("1") } else {    defer fmt.Printf("2") } fmt.Printf("3") } A. 321 B. 32 C. 31 D. 13 参考答案:C   【初级】关于switch语句,下面说法正确的有() A. 条件表达式必须为常量或者整数 B. 单个case中,可以出现多个结果选项 C. 需要用break来明确退出一个case D. 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case 参考答案:BD   【中级】 golang中没有隐藏的this指针,这句话的含义是() A. 方法施加的对象显式传递,没有被隐藏起来 B. golang沿袭了传统面向对象编程中的诸多概念,比如继承、虚函数和构造函数 C. golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达 D. 方法施加的对象不需要非得是指针,也不用非得叫this 参考答案:ACD   【中级】 golang中的引用类型包括() A. 数组切片 B. map C. channel D. interface 参考答案:ABCD   【中级】 golang中的指针运算包括() A. 可以对指针进行自增或自减运算 B. 可以通过“&”取指针的地址 C. 可以通过“*”取指针指向的数据 D. 可以对指针进行下标运算 参考答案:BC   【初级】关于main函数(可执行程序的执行起点),下面说法正确的是() A. main函数不能带参数 B. main函数不能定义返回值 C. main函数所在的包必须为main包 D. main函数中可以使用flag包来获取和解析命令行参数 参考答案:ABCD   【中级】下面赋值正确的是() A. var x = nil B. var x interface{} = nil C. var x string = nil D. var x error = nil 参考答案:BD   【中级】关于整型切片的初始化,下面正确的是() A. s := make([]int) B. s := make([]int, 0) C. s := make([]int, 5, 10) D. s := []int{1, 2, 3, 4, 5} 参考答案:BCD   【中级】从切片中删除一个元素,下面的算法实现正确的是() A. func (s *Slice)Remove(value interface{})error { for i, v := range *s {    if isEqual(value, v) {        if i== len(*s) - 1 {            *s = (*s)[:i]        }else {            *s = append((*s)[:i],(*s)[i + 2:]...)        }        return nil    } } return ERR_ELEM_NT_EXIST } B. func (s*Slice)Remove(value interface{}) error { for i, v:= range *s {     if isEqual(value, v) {         *s =append((*s)[:i],(*s)[i + 1:])         return nil     } } returnERR_ELEM_NT_EXIST } C. func (s*Slice)Remove(value interface{}) error { for i, v:= range *s {     if isEqual(value, v) {         delete(*s, v)         return nil     } } returnERR_ELEM_NT_EXIST } D. func (s*Slice)Remove(value interface{}) error { for i, v:= range *s {     if isEqual(value, v) {         *s =append((*s)[:i],(*s)[i + 1:]...)         return nil     } } returnERR_ELEM_NT_EXIST } 参考答案:D   【初级】对于局部变量整型切片x的赋值,下面定义正确的是() A. x := []int{ 1, 2, 3, 4, 5, 6, } B. x :=[]int{ 1, 2, 3, 4, 5, 6 } C. x :=[]int{ 1, 2, 3, 4, 5, 6} D. x :=[]int{1, 2, 3, 4, 5, 6,} 参考答案:ACD   【初级】关于变量的自增和自减操作,下面语句正确的是() A. i := 1 i++ B. i := 1 j = i++ C. i := 1 ++i D. i := 1 i-- 参考答案:AD   【中级】关于函数声明,下面语法错误的是() A. func f(a, b int) (value int, err error) B. func f(a int, b int) (value int, err error) C. func f(a, b int) (value int, error) D. func f(a int, b int) (int, int, error) 参考答案:C   【中级】如果Add函数的调用代码为: func main() { var a Integer = 1 var b Integer = 2 var i interface{} = &a sum := i.(*Integer).Add(b) fmt.Println(sum) } 则Add函数定义正确的是() A. typeInteger int func (aInteger) Add(b Integer) Integer {  return a + b } B. typeInteger int func (aInteger) Add(b *Integer) Integer {  return a + *b } C. typeInteger int func (a*Integer) Add(b Integer) Integer {  return *a + b } D. typeInteger int func (a*Integer) Add(b *Integer) Integer {  return *a + *b } 参考答案:AC   【中级】如果Add函数的调用代码为: func main() { var a Integer = 1 var b Integer = 2 var i interface{} = a sum := i.(Integer).Add(b) fmt.Println(sum) } 则Add函数定义正确的是() A. typeInteger int func (a Integer)Add(b Integer) Integer {  return a + b } B. typeInteger int func (aInteger) Add(b *Integer) Integer {  return a + *b } C. typeInteger int func (a*Integer) Add(b Integer) Integer {  return *a + b } D. typeInteger int func (a*Integer) Add(b *Integer) Integer {  return *a + *b } 参考答案:A   【中级】关于GetPodAction定义,下面赋值正确的是() type Fragment interface { Exec(transInfo *TransInfo) error } type GetPodAction struct { } func (g GetPodAction) Exec(transInfo*TransInfo) error { ... return nil } A. var fragment Fragment =new(GetPodAction) B. var fragment Fragment = GetPodAction C. var fragment Fragment = &GetPodAction{} D. var fragment Fragment = GetPodAction{} 参考答案:ACD   【中级】关于GoMock,下面说法正确的是() A. GoMock可以对interface打桩 B. GoMock可以对类的成员函数打桩 C. GoMock可以对函数打桩 D. GoMock打桩后的依赖注入可以通过GoStub完成 参考答案:AD   【中级】关于接口,下面说法正确的是() A. 只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等价的,可以相互赋值 B. 如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A C. 接口查询是否成功,要在运行期才能够确定 D. 接口赋值是否可行,要在运行期才能够确定 参考答案:ABC   【初级】关于channel,下面语法正确的是() A. var ch chan int B. ch := make(chan int) C. <- ch D. ch <- 参考答案:ABC   【初级】关于同步锁,下面说法正确的是() A. 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex B. RWMutex在读锁占用的情况下,会阻止写,但不阻止读 C. RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占 D. Lock()操作需要保证有Unlock()或RUnlock()调用与之对应 参考答案:ABC   【中级】 golang中大多数数据类型都可以转化为有效的JSON文本,下面几种类型除外() A. 指针 B. channel C. complex D. 函数 参考答案:BCD   【中级】关于go vendor,下面说法正确的是() A. 基本思路是将引用的外部包的源代码放在当前工程的vendor目录下面 B. 编译go代码会优先从vendor目录先寻找依赖包 C. 可以指定引用某个特定版本的外部包 D. 有了vendor目录后,打包当前的工程代码到其他机器的$GOPATH/src下都可以通过编译 参考答案:ABD   【初级】 flag是bool型变量,下面if表达式符合编码规范的是() A. if flag == 1 B. if flag C. if flag == false D. if !flag 参考答案:BD   【初级】 value是整型变量,下面if表达式符合编码规范的是() A. if value == 0 B. if value C. if value != 0 D. if !value 参考答案:AC   【中级】关于函数返回值的错误设计,下面说法正确的是() A. 如果失败原因只有一个,则返回bool B. 如果失败原因超过一个,则返回error C. 如果没有失败原因,则不返回bool或error D. 如果重试几次可以避免失败,则不要立即返回bool或error 参考答案:ABCD   【中级】关于异常设计,下面说法正确的是() A. 在程序开发阶段,坚持速错,让程序异常崩溃 B. 在程序部署后,应恢复异常避免程序终止 C. 一切皆错误,不用进行异常设计 D. 对于不应该出现的分支,使用异常处理 参考答案:ABD   【中级】关于slice或map操作,下面正确的是() A. var s []int s =append(s,1) B. var mmap[string]int m["one"]= 1 C. var s[]int s =make([]int, 0) s =append(s,1) D. var mmap[string]int m =make(map[string]int) m["one"]= 1 参考答案:ACD   【中级】关于channel的特性,下面说法正确的是() A. 给一个 nil channel 发送数据,造成永远阻塞 B. 从一个 nil channel 接收数据,造成永远阻塞 C. 给一个已经关闭的 channel 发送数据,引起 panic D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值 参考答案:ABCD   【中级】关于无缓冲和有冲突的channel,下面说法正确的是() A. 无缓冲的channel是默认的缓冲为1的channel B. 无缓冲的channel和有缓冲的channel都是同步的 C. 无缓冲的channel和有缓冲的channel都是非同步的 D. 无缓冲的channel是同步的,而有缓冲的channel是非同步的 参考答案:D   【中级】关于异常的触发,下面说法正确的是() A. 空指针解析 B. 下标越界 C. 除数为0 D. 调用panic函数 参考答案:ABCD   【中级】关于cap函数的适用类型,下面说法正确的是() A. array B. slice C. map D. channel 参考答案:ABD   【中级】关于beego框架,下面说法正确的是() A. beego是一个golang实现的轻量级HTTP框架 B. beego可以通过注释路由、正则路由等多种方式完成url路由注入 C. 可以使用bee new工具生成空工程,然后使用bee run命令自动热编译 D. beego框架只提供了对url路由的处理,而对于MVC架构中的数据库部分未提供框架支持 参考答案:ABC   【中级】关于goconvey,下面说法正确的是() A. goconvey是一个支持golang的单元测试框架 B. goconvey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面 C. goconvey提供了丰富的断言简化测试用例的编写 D. goconvey无法与go test集成 参考答案:ABC   【中级】关于go vet,下面说法正确的是() A. go vet是golang自带工具go tool vet的封装 B. 当执行go vet database时,可以对database所在目录下的所有子文件夹进行递归检测 C. go vet可以使用绝对路径、相对路径或相对GOPATH的路径指定待检测的包 D. go vet可以检测出死代码 参考答案:ACD   100.             【中级】关于map,下面说法正确的是() A. map反序列化时json.unmarshal的入参必须为map的地址 B. 在函数调用中传递map,则子函数中对map元素的增加不会导致父函数中map的修改 C. 在函数调用中传递map,则子函数中对map元素的修改不会导致父函数中map的修改 D. 不能使用内置函数delete删除map的元素 参考答案:A 101.             【中级】关于GoStub,下面说法正确的是() A. GoStub可以对全局变量打桩 B. GoStub可以对函数打桩 C. GoStub可以对类的成员方法打桩 D. GoStub可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为 参考答案:ABD   102.             【初级】关于select机制,下面说法正确的是() A. select机制用来处理异步IO问题 B. select机制最大的一条限制就是每个case语句里必须是一个IO操作 C. golang在语言级别支持select关键字 D. select关键字的用法与switch语句非常类似,后面要带判断条件 参考答案:ABC   103.             【初级】关于内存泄露,下面说法正确的是() A. golang有自动垃圾回收,不存在内存泄露 B. golang中检测内存泄露主要依靠的是pprof包 C. 内存泄露可以在编译阶段发现 D. 应定期使用浏览器来查看系统的实时内存信息,及时发现内存泄露问题 参考答案:BD   ———————————————— 原文链接:https://blog.csdn.net/itcastcpp/article/details/80462619 ————————————————

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

问题

Java技术1000问(3)【精品问答】

问问小秘 2020-06-02 14:27:10 42 浏览量 回答数 1

回答

Go 的优势在于能够将简单的和经过验证的想法结合起来,同时避免了其他语言中出现的许多问题。本文概述了 Go 背后的一些设计原则和工程智慧,作者认为,Go 语言具备的所有这些优点,将共同推动其成为接替 Java 并主导下一代大型软件开发平台的最有力的编程语言候选。很多优秀的编程语言只是在个别领域比较强大,如果将所有因素都纳入考虑,没有其他语言能够像 Go 语言一样“全面开花”,在大型软件工程方面,尤为如此。 基于现实经验 Go 是由经验丰富的软件行业老手一手创建的,长期以来,他们对现有语言的各种缺点有过切身体会的痛苦经历。几十年前,Rob Pike 和 Ken Thompson 在 Unix、C 和 Unicode 的发明中起到了重要作用。Robert Griensemer 在为 JavaScript 和 Java 开发 V8 和 HotSpot 虚拟机之后,在编译器和垃圾收集方面拥有数十年的经验。有太多次,他们不得不等待 Google 规模的 C++/Java 代码库进行编译。于是,他们开始着手创建新的编程语言,将他们半个世纪以来的编写代码所学到的一切经验包含进去。 专注于大型工程 小型工程项目几乎可以用任何编程语言来成功构建。当成千上万的开发人员在数十年的持续时间压力下,在包含数千万行代码的大型代码库上进行协作时,就会发生真正令人痛苦的问题。这样会导致一些问题,如下: 较长的编译时间导致中断开发。代码库由几个人 / 团队 / 部门 / 公司所拥有,混合了不同的编程风格。公司雇佣了数千名工程师、架构师、测试人员、运营专家、审计员、实习生等,他们需要了解代码库,但也具备广泛的编码经验。依赖于许多外部库或运行时,其中一些不再以原始形式存在。在代码库的生命周期中,每行代码平均被重写 10 次,被弄得千疮百痍,而且还会发生技术偏差。文档不完整。 Go 注重减轻这些大型工程的难题,有时会以使小型工程变得更麻烦为代价,例如,代码中到处都需要几行额外的代码行。 注重可维护性 Go 强调尽可能多地将工作转给自动化的代码维护工具中。Go 工具链提供了最常用的功能,如格式化代码和导入、查找符号的定义和用法、简单的重构以及代码异味的识别。由于标准化的代码格式和单一的惯用方式,机器生成的代码更改看起来非常接近 Go 中人为生成的更改并使用类似的模式,从而允许人机之间更加无缝地协作。 保持简单明了 初级程序员为简单的问题创建简单的解决方案。高级程序员为复杂的问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。 ——Charles Connell 让很多人惊讶的一点是,Go 居然不包含他们喜欢的其他语言的概念。Go 确实是一种非常小巧而简单的语言,只包含正交和经过验证的概念的最小选择。这鼓励开发人员用最少的认知开销来编写尽可能简单的代码,以便许多其他人可以理解并使用它。 使事情清晰明了 良好的代码总是显而易见的,避免了那些小聪明、难以理解的语言特性、诡异的控制流和兜圈子。 许多语言都致力提高编写代码的效率。然而,在其生命周期中,人们阅读代码的时间却远远超过最初编写代码所需的时间(100 倍)。例如,审查、理解、调试、更改、重构或重用代码。在查看代码时,往往只能看到并理解其中的一小部分,通常不会有完整的代码库概述。为了解释这一点,Go 将所有内容都明确出来。 错误处理就是一个例子。让异常在各个点中断代码并在调用链上冒泡会更容易。Go 需要手动处理和返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理编写起来更加繁琐,但是也更容易理解。 简单易学 Go 是如此的小巧而简单,以至于人们可以在短短几天内就能研究通整个语言及其基本概念。根据我们的经验,培训用不了一个星期(相比于掌握其他语言需要几个月),初学者就能够理解 Go 专家编写的代码,并为之做出贡献。为了方便吸引更多的用户,Go 网站提供了所有必要的教程和深入研究的文章。这些教程在浏览器中运行,允许人们在将 Go 安装到本地计算机上之前就能够学习和使用 Go。 解决之道 Go 强调的是团队之间的合作,而不是个人的自我表达。 在 Go(和 Python)中,所有的语言特性都是相互正交和互补的,通常有一种方法可以做一些事情。如果你想让 10 个 Python 或 Go 程序员来解决同一个问题,你将会得到 10 个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更自在。在查看其他人的代码时,国骂会更少,而且人们的工作可以更好地融合在一起,从而形成了一致的整体,人人都为之感到自豪,并乐于工作。这还避免了大型工程的问题,如: 开发人员认为良好的工作代码很“混乱”,并要求在开始工作之前进行重写,因为他们的思维方式与原作者不同。 不同的团队成员使用不同的语言子集来编写相同代码库的部分内容。 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/e64418f1455d46aaacfdd03fa949f16d.png) 简单、内置的并发性 Go 专为现代多核硬件设计。 目前使用的大多数编程语言(Java、JavaScript、Python、Ruby、C、C++)都是 20 世纪 80 年代到 21 世纪初设计的,当时大多数 CPU 只有一个计算内核。这就是为什么它们本质上是单线程的,并将并行化视为边缘情况的马后炮。通过现成和同步点之类的附加组件来实现,而这些附加组件既麻烦又难以正确使用。第三方库虽然提供了更简单的并发形式,如 Actor 模型,但是总有多个可用选项,结果导致了语言生态系统的碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能高效运行。Go 是在多核处理器时代编写的,并且在语言中内置了简单、高级的 CSP 风格并发性。 面向计算的语言原语 就深层而言,计算机系统接收数据,对其进行处理(通常要经过几个步骤),然后输出结果数据。例如,Web 服务器从客户端接收 HTTP 请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就将接收到的数据转换成 HTML 或 JSON 并将其输出给调用者。Go 的内置语言原语直接支持这种范例: 结构表示数据 读和写代表流式 IO 函数过程数据 goroutines 提供(几乎无限的)并发性 在并行处理步骤之间传输管道数据 因为所有的计算原语都是由语言以直接形式提供的,因此 Go 源代码更直接地表达了服务器执行的操作。 OO — 好的部分 更改基类中的某些内容的副作用 面向对象非常有用。过去几十年来,面向对象的使用富有成效,并让我们了解了它的哪些部分比其他部分能够更好地扩展。Go 在面向对象方面采用了一种全新的方法,并记住了这些知识。它保留了好的部分,如封装、消息传递等。Go 还避免了继承,因为它现在被认为是有害的,并为组合提供了一流的支持。 现代标准库 目前使用的许多编程语言(Java、JavaScript、Python、Ruby)都是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库只提供了相对通用的网络支持,而这些网络并没有针对现代互联网进行优化。Go 是十年前创建的,当时互联网已全面发展。Go 的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这就避免了第三方库的常见问题: 碎片化:总是有多个选项实现相同的功能。 膨胀:库常常实现的不仅仅是它们的用途。 依赖地狱:库通常依赖于特定版本的其他库。 未知质量:第三方代码的质量和安全性可能存在问题。 未知支持:第三方库的开发可能随时停止支持。 意外更改:第三方库通常不像标准库那样严格地进行版本控制。 关于这方面更多的信息请参考 Russ Cox 提供的资料 标准化格式 Gofmt 的风格没有人会去喜欢,但人人都会喜欢 gofmt。 ——Rob Pike Gofmt 是一种以标准化方式来格式化 Go 代码的程序。它不是最漂亮的格式化方式,但却是最简单、最不令人生厌的格式化方式。标准化的源代码格式具有惊人的积极影响: 集中讨论重要主题: 它消除了围绕制表符和空格、缩进深度、行长、空行、花括号的位置等一系列争论。 开发人员在彼此的代码库中感觉很自在, 因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式进行格式化代码,但如果其他人按照自己喜欢的方式格式化了代码,这么做很招人烦。 自动代码更改并不会打乱手写代码的格式,例如引入了意外的空白更改。 许多其他语言社区现在正在开发类似 gofmt 的东西。当作为第三方解决方案构建时,通常会有几个相互竞争的格式标准。例如,JavaScript 提供了 Prettier 和 StandardJS。这两者都可以用,也可以只使用其中的一个。但许多 JS 项目并没有采用它们,因为这是一个额外的决策。Go 的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。 快速编译 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/8a76f3f07f484266af42781d9e7b8692.png) 对于大型代码库来说,它们长时间的编译是促使 Go 诞生的原因。Google 主要使用的是 C++ 和 Java,与 Haskell、Scala 或 Rust 等更复杂的语言相比,它们的编译速度相对较快。尽管如此,当编译大型代码库时,即使是少量的缓慢也会加剧编译的延迟,从而激怒开发人员,并干扰流程。Go 的设计初衷是为了提高编译效率,因此它的编译器速度非常快,几乎没有编译延迟的现象。这给 Go 开发人员提供了与脚本类语言类似的即时反馈,还有静态类型检查的额外好处。 交叉编译 由于语言运行时非常简单,因此它被移植到许多平台,如 macOS、Linux、Windows、BSD、ARM 等。Go 可以开箱即用地为所有这些平台编译二进制文件。这使得从一台机器进行部署变得很容易。 快速执行 Go 的运行速度接近于 C。与 JITed 语言(Java、JavaScript、Python 等)不同,Go 二进制文件不需要启动或预热的时间,因为它们是作为编译和完全优化的本地代码的形式发布的。Go 的垃圾收集器仅引入微秒量级的可忽略的停顿。除了快速的单核性能外,Go 还可以轻松利用所有的 CPU 内核。 内存占用小 像 JVM、Python 或 Node 这样的运行时不仅仅在运行时加载程序代码,每次运行程序时,它们还会加载大型且高度复杂的基础架构,以进行编译和优化程序。如此一来,它们的启动时间就变慢了,并且还占用了大量内存(数百兆字节)。而 Go 进程的开销更小,因为它们已经完全编译和优化,只需运行即可。Go 还以非常节省内存的方式来存储数据。在内存有限且昂贵的云环境中,以及在开发过程中,这一点非常重要。我们希望在一台机器上能够快速启动整个堆栈,同时将内存留给其他软件。 部署规模小 Go 的二进制文件大小非常简洁。Go 应用程序的 Docker 镜像通常比用 Java 或 Node 编写的等效镜像要小 10 倍,这是因为它无需包含编译器、JIT,以及更少的运行时基础架构的原因。这些特点,在部署大型应用程序时很重要。想象一下,如果要将一个简单的应用程序部署到 100 个生产服务器上会怎么样?如果使用 Node/JVM 时,我们的 Docker 注册表就必须提供 100 个 docker 镜像,每个镜像 200MB,那么一共就需要 20GB。要完成这些部署就需要一些时间。想象一下,如果我们想每天部署 100 次的话,如果使用 Go 服务,那么 Docker 注册表只需提供 10 个 docker 镜像,每个镜像只有 20MB,共只需 2GB 即可。大型 Go 应用程序可以更快、更频繁地部署,从而使得重要更新能够更快地部署到生产环境中。 独立部署 Go 应用程序部署为一个包含所有依赖项的单个可执行文件,并无需安装特定版本的 JVM、Node 或 Python 运行时;也不必将库下载到生产服务器上,更无须对运行 Go 二进制文件的机器进行任何更改。甚至也不需要讲 Go 二进制文件包装到 Docker 来共享他们。你需要做的是,只是将 Go 二进制文件放到服务器上,它就会在那里运行,而不用关心服务器运行的是什么。前面所提到的那些,唯一的例外是使用net和os/user包时针对对glibc的动态链接。 供应依赖关系 Go 有意识避免使用第三方库的中央存储库。Go 应用程序直接链接到相应的 Git 存储库,并将所有相关代码下载(供应)到自己的代码库中。这样做有很多好处: 在使用第三方代码之前,我们可以对其进行审查、分析和测试。该代码就和我们自己的代码一样,是我们应用程序的一部分,应该遵循相同的质量、安全性和可靠性标准。 无需永久访问存储依赖项的各个位置。从任何地方(包括私有 Git repos)获取第三方库,你就能永久拥有它们。 经过验收后,编译代码库无需进一步下载依赖项。 若互联网某处的代码存储库突然提供不同的代码,这也并不足为奇。 即使软件包存储库速度变慢,或托管包不复存在,部署也不会因此中断。 兼容性保证 Go 团队承诺现有的程序将会继续适用于新一代语言。这使得将大型项目升级到最新版本的编译器会非常容易,并且可从它们带来的许多性能和安全性改进中获益。同时,由于 Go 二进制文件包含了它们需要的所有依赖项,因此可以在同一服务器上并行运行使用不同版本的 Go 编译器编译的二进制文件,而无需进行复杂的多个版本的运行时设置或虚拟化。 文档 在大型工程中,文档对于使软件可访问性和可维护性非常重要。与其他特性类似,Go 中的文档简单实用: 由于它是嵌入到源代码中的,因此两者可以同时维护。 它不需要特殊的语法,文档只是普通的源代码注释。 可运行单元测试通常是最好的文档形式。因此 Go 要求将它们嵌入到文档中。 所有的文档实用程序都内置在工具链中,因此每个人都使用它们。 Go linter 需要导出元素的文档,以防止“文档债务”的积累。 商业支持的开源 当商业实体在开放式环境下开发时,那么一些最流行的、经过彻底设计的软件就会出现。这种设置结合了商业软件开发的优势——一致性和精细化,使系统更为健壮、可靠、高效,并具有开放式开发的优势,如来自许多行业的广泛支持,多个大型实体和许多用户的支持,以及即使商业支持停止的长期支持。Go 就是这样发展起来的。 缺点 当然,Go 也并非完美无缺,每种技术选择都是有利有弊。在决定选择 Go 之前,有几个方面需要进行考虑考虑。 未成熟 虽然 Go 的标准库在支持许多新概念(如 HTTP 2 Server push 等)方面处于行业领先地位,但与 JVM 生态系统中的第三方库相比,用于外部 API 的第三方 Go 库可能不那么成熟。 即将到来的改进 由于清楚几乎不可能改变现有的语言元素,Go 团队非常谨慎,只在新特性完全开发出来后才添加新特性。在经历了 10 年的有意稳定阶段之后,Go 团队正在谋划对语言进行一系列更大的改进,作为 Go 2.0 之旅的一部分。 无硬实时 虽然 Go 的垃圾收集器只引入了非常短暂的停顿,但支持硬实时需要没有垃圾收集的技术,例如 Rust。 结语 本文详细介绍了 Go 语言的一些优秀的设计准则,虽然有的准则的好处平常看起来没有那么明显。但当代码库和团队规模增长几个数量级时,这些准则可能会使大型工程项目免于许多痛苦。总的来说,正是这些设计准则让 Go 语言成为了除 Java 之外的编程语言里,用于大型软件开发项目的绝佳选择。

有只黑白猫 2020-01-07 14:11:38 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 企业信息查询 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站 阿里云双十一主会场 阿里云双十一新人会场 1024程序员加油包 阿里云双十一拼团会场 场景化解决方案 阿里云双十一直播大厅