首页 / 资讯中心 / 文章详情

Java 转 go 学习 - map

Java 转 go 学习 - map ★ FEATURED ARTICLE
文章目录 1. map 基础1.1 创建1.2 增删改查1.3 遍历 map1.4 存在性检查 2. map 进阶用法2.1 零值 map2.2 值类型和指针类型2.3 函数传递2.4 map 类型的切片 3. 小结本系列文章Java 转 go 学习 - 项目管理Java 转 go 学习 - 基本语法Java 转 go 学习 - 类型转换Java 转 go 学习 - 流程控制结构Java 转 go 学习 - 数组和切片上面几篇文章学习了数组和切片的基本概念和用法这篇文章学习 map 的基础用法类似 Java 的 Map。 1. map 基础1.1 创建go 中的map无序的键值对(Key-Value)集合是引用类型也就是说函数传递时会作为引用传递同样的 go 中 map 的属性和 java 的一样下面先来看下 map 的初始化由下面几种方式。首先通过make创建这也是比较推荐的方式下面是创建的例子functest1(){// 创建空 map, 不指定初始容量m1:make(map[string]int)// 指定初始容量m2:make(map[string]int,100)fmt.Println(m1)// map[]fmt.Println(m2)// map[]}然后可以通过字面量创建也就是说可以创建的时候顺便初始化一些key-value下面是例子functest2(){// 字面量初始化m1:map[string]int{a:1,b:2,c:3,}// 空的 map 字面量m2:map[string]int{}fmt.Println(m1)// map[a:1 b:2 c:3]fmt.Println(m2)// map[]}接下来是第三种可以先声明再初始化声明的时候map nil声明完成之后必须要初始化之后才能使用下面是例子functest3(){// var 声明的 map 是 nil, 需要初始化之后才能用varm1map[string]intvarm2map[string]intvarm3map[string]int// 初始化m1make(map[string]int)m2map[string]int{}m3map[string]int{a:1,b:2}fmt.Println(m1)// map[]fmt.Println(m2)// map[]fmt.Println(m3)// map[a:1 b:2]}当然上一篇文章我们通过new创建了切片这里也可以通过new创建map但是要注意new分配的引用对象并没有初始化相当于声明一个没有初始化的变量然后返回地址下面可以看下例子functest4(){m1:new(map[string]int)// 报错 panic: assignment to entry in nil map// (*m1)[key] 1// 初始化*m1make(map[string]int)(*m1)[a]1(*m1)[b]2(*m1)[c]3fmt.Println(*m1)// map[a:1 b:2 c:3]}1.2 增删改查同理map 也可以进行增删改查要注意的是查询的时候有两个返回值value, exist如果我们选择用一个变量去接收那么这个变量就是value如果用两个变量第二个变量接受到的就是exist也就是key-value是否存在最后删除需要通过函数delete完成。下面来看下这几个例子functest5(){scores:make(map[string]int)// 增加scores[a]1scores[b]2scores[c]3fmt.Println(scores)// map[a:1 b:2 c:3]// 修改scores[c]3// 查询n1,exist:scores[a]fmt.Println(n1,exist)// 1 true// 查询不存在的值n2,exist:scores[d]fmt.Println(n2,exist)// 0 false// 删除delete(scores,a)fmt.Println(scores)// map[b:2 c:3]// 删除不存在的 key, 不会 panicdelete(scores,f)fmt.Println(scores)// map[b:2 c:3]// 查询 map 长度fmt.Println(map 长度: ,len(scores))// map 长度: 2}1.3 遍历 map还是通过 range 遍历 map同样的可以遍历key-value也可以单独遍历key或者单独遍历value。functest6(){m:map[string]int{apple:5,banana:3,orange:8,}// 遍历 key - valueforkey,value:rangem{fmt.Printf(%s - %d\n,key,value)// apple - 5// banana - 3// orange - 8}// 只遍历 keyforkey:rangem{fmt.Printf(%s\n,key)// apple// banana// orange}// 只遍历 valuefor_,value:rangem{fmt.Printf(%d\n,value)// 8// 5// 3}}map 的遍历是无序遍历也就是不按照添加的顺序遍历这点跟 Java 是一样的那么如果要有序遍历就需要先获取所有 key然后排序最后通过 key 访问 value 来实现有序遍历下面可以先看下遍历的例子但是要注意这里的无序是说每一次遍历顺序都不固定。functest7(){m:map[string]int{apple:5,banana:3,orange:8,}// 多次运行输出顺序不同fori:0;i3;i{fmt.Println(第,i,次遍历:)fork,v:rangem{fmt.Printf(%s:%d ,k,v)}fmt.Println()}// 第 0 次遍历:// orange:8 apple:5 banana:3// 第 1 次遍历:// apple:5 banana:3 orange:8// 第 2 次遍历:// banana:3 orange:8 apple:5}最后再来看下有序遍历functest8(){m:map[int]string{1:one,2:two,3:three,4:four,5:five,}// 注意这里不要用 make([]int, len(m)) 去初始化, 因为这样初始化就相当于创建了一个初始长度为 5, 且 [0, 4] 全是 0 的切片, 下面// append 的时候 key 追加的下标就会是 [5, 9], 后面遍历 keys 就会有 10 个 keykeys:make([]int,0,len(m))forkey:rangem{keysappend(keys,key)}// 排序sort.Ints(keys)forindex,value:rangekeys{fmt.Printf(index: %d, key: %d, value: %s\n,index,value,m[value])// index: 0, key: 1, value: one// index: 1, key: 2, value: two// index: 2, key: 3, value: three// index: 3, key: 4, value: four// index: 4, key: 5, value: five}}1.4 存在性检查存在性就是检查一个 key 是否存在这一点和 Java 有点不一样大家都知道 Java 的 Map 不能存储基本数据类型也就是说如果获取不到 key返回的就是 null当然你也可以自己往里面添加 null比如下面的例子public class Main{public static voidmain(String[]args){HashMapInteger,IntegermapnewHashMap();map.put(123,null);System.out.println(map.get(123)null);}}一般来说为了减少存储Java 中如果是 null 的 value 都会把 key 给删掉所以判断的时候通过 value null 来判断一个 key 是否存在也是没问题的但是 go 就不一样了go 没有包装类map 里面可以存储基本数据类型换句话说 map 的 value 可以存储任何类型。而 map 在获取一个不存在的 key 会返回 value 的零值比如int 就是 0如果我们不判断存在性就有可能存在下面问题functest9(){m:map[string]int{score:0,}v:m[score]v2:m[unknown]fmt.Println(v,v2)// 0 0}这种情况下我们没办法分辨score 和 unknown 这两个 key 到底是存在还是不存在所以一般来说都会结合两个参数如下面的写法functest10(){m:map[string]int{score:0,}v,exist1:m[score]v2,exist2:m[unknown]fmt.Println(v,exist1)// 0 truefmt.Println(v2,exist2)// 0 false}这样就能通过exist1和exist2来判断一个key到底是否存在了一个比较好的写法如下functest10(){m:map[string]int{score:0,}// 1. 关心 key-valueifval,ok:m[score];ok{fmt.Println(存在: ,val)}else{fmt.Println(不存在 score 这个 key)}// 存在: 0// 2. 只关心 key 是否存在if_,ok:m[unknown];!ok{fmt.Println(不存在 unknown 这个 key)}else{fmt.Println(存在 unknown 这个 key)}// 不存在 unknown 这个 key// 3. 不存在的时候给默认值val,ok:m[unknown]if!ok{val1}fmt.Println(val)// 1} 2. map 进阶用法2.1 零值 map假设现在我们声明了一个 map 但是不初始化这种情况下声明出来的map nil会返回 true但是注意如果此时通过这个 map 获取某个 key这种情况下会获取到 value 的零值比如 value 是 int 类型那么获取到的就是 0并不会panic但是如果此时往里面写入数据就会panic了比如下面的例子functest11(){// 声明但不初始化varm1map[string]intfmt.Println(m1nil)// truefmt.Println(m1[score])// 0}此时我们往声明的m1里面获取数据是不会panic的但是如果像下面这种写入数据就不行了。functest11(){// 声明但不初始化varm1map[string]int// panic: assignment to entry in nil mapm1[score]1}比较特殊的是删除一个 key 不管是nil map还是empty map都是没问题的。functest11(){// 声明但不初始化varm1map[string]int// 初始化为空 mapm2:make(map[string]int)fmt.Println(m1nil)// truefmt.Println(m1[score])// 0// 空 map 可以写入m2[score]1fmt.Println(m2[score])// 1// 都没问题, 不会报错delete(m1,score)delete(m2,score)}⚠️ 下面要注意在写代码的时候结构体中的 Map 初始化是nil此时不能直接往里面添加或者修改数据需要初始化。functest12(){cfg:Config{}fmt.Println(cfg.Settingsnil)// true// cfg.Settings[key] value// panic: assignment to entry in nil map// 1. 初始化的时候就创建 mapcfgConfig{Settings:make(map[string]string),}cfg.Settings[key]valuefmt.Println(cfg.Settings)// map[key:value]// 2. 使用构造函数来创建结构体cfgNewConfig()cfg.Settings[key]valuefmt.Println(cfg.Settings)// map[key:value]}2.2 值类型和指针类型这里是说 map 存储的 value 什么时候存值类型什么时候存指针类型一般来说存值就意味着通过 map 去获取的时候获取的是副本此时修改这个 value 不会影响到 map 里面存储的原有的 map那如果存的是指针类型那么这时候修改 value 会影响原来 map 里面的值。functest13(){// 1. value 存的是值类型stuMapVal:make(map[int]Student)stu1:Student{Name:张三,Age:18}// 存到 map 中, 但是此时存的是值的拷贝stuMapVal[1]stu1// 修改 stu1stu1.Name李四// 输出 map 里面的存储值fmt.Println(stuMapVal)// map[1:{张三 18}]// 2. value 存的是指针类型stuMapPtr:make(map[int]*Student)stu2:Student{Name:李四,Age:19}stuMapPtr[1]stu2// 存入指针, 指向 stu2 的内存地址// 修改 stu2stu2.Age21fmt.Println(stuMapPtr[1].Age)// 21fmt.Println((stuMapPtr[1].Age),(stu2.Age))// 0xc0000080d0 0xc0000080d0}可以看到 stu2 的 Age 和 stuMapPtr 里面的 Age 地址是一样的也就是说存指针类型的情况下可以直接对原始结构体里面的值进行修改。既然有了这两种类型下面就是这两种存储方式的区别维度存储「值」存储「指针」内存占用存储完整数据拷贝占用更大尤其结构体大时仅存储内存地址8 字节 / 64 位系统占用小修改行为修改原变量不影响 map需重新赋值 map修改原变量 / 通过指针直接改都影响 map性能存入 / 取出时需拷贝数据结构体越大越慢仅拷贝指针性能稳定无数据拷贝空值 / 零值处理直接存储类型零值如 Student {}可能存储 nil 指针需额外判空既然有了上面的比较什么时候存值什么时候存指针也可以得出基础数据类型如果是基础数据类型比如 int/bool/string 等这种要么本身的内存大小比 8 字节小要么就是不需要修改这种情况下存指针没啥意义直接存值就行。避免误修改如果不想map里面的数据被修改这时候就存值类型就行比如像上面示例里面的结构体就按照第一种方式存通过stuMapVal[1]获取到的 value 也是复制之后的值修改不会对原来 map 里面的属于有影响。小结构体占用内存 ≤ 指针大小比如结构体只有 1-2 个字段总大小≤ 8字节存值和存指针性能几乎无差别存值更加简单不需要考虑nil的问题。反过来需存储nil表示不存在这种情况下就存指针因为如果存值那么如果不存在获取到的就是零值没办法区分这个 key 对应的 value 到底是零值还是说就是不存在。2.3 函数传递funcmodifyMap(mmap[string]int){fmt.Printf(%p\n,m)// 0xc000058038m[b]2}functest14(){m:map[string]int{a:1}fmt.Printf(%p\n,m)// 0xc000058048modifyMap(m)fmt.Println(m)// map[a:1 b:2]}上面的例子说明了 map 作为引用传递函数里面修改 map 里面的内容原来 map 里面的内容也会改变modifyMap接收map的时候在这个方法的栈里面为m分配了一个新的内存空间整体表现来看就是将指针复制了一份这两个地址就是两个变量各自在栈中的地址但是两个指针指向堆内存里面的地址是同一个。2.4 map 类型的切片假设我们想获取一个 map 类型的切片就需要用两次make函数第一次make分配切片第二次make是分配切片里面的每个map如下面例子。functest15(){// 第一次 make 创建切片items:make([]map[string]int,5)fori:rangeitems{// 第二次 make 创建 mapitems[i]make(map[string]int,10)items[i][idx]i}fori:rangeitems{fmt.Println(items[i])}// map[idx:0]// map[idx:1]// map[idx:2]// map[idx:3]// map[idx:4]} 3. 小结好了这篇文章就先学到这里这里是关于 map 的基础用法后面有一些进阶的用法比如线程安全的 map 就到后面再看下一篇文章就来看下函数的内容。如有错误欢迎指出
阅读完成 · 觉得有帮助?
咨询建站