网站首页 > 教程文章 正文
12. 深拷贝与浅拷贝
拷贝就是就是将现有的一个对象的属性都保存给另一个对象,由于引用数据类型是引用传递的,直接用=将原对象赋给拷贝对象,只是在栈中新建了一个引用指向堆中同一个对象,并没有实现拷贝。拷贝分为浅拷贝与深拷贝。
浅拷贝:浅拷贝一个对象,是将该对象中基本类型的变量进行拷贝,而对于引用类型,浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。实现:实现Cloneable接口,并重写clone()方法。
深拷贝:深拷贝会将所有的成员变量都复制,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正意义上的拷贝。实现方式主要有:
·手动赋值,代码简单,但是啰嗦
·通过序列化与反序列化实现,需要源对象以及所有成员对象类型都实现Serializable()接口,通过序列化,将源对象的信息以另外一种形式存放在了堆外。然后将堆外的这份信息通过反序列化的方式再放回到堆中,就创建了一个新的对象,也就是目标对象
·在浅拷贝的基础上,对所有成员对象类型实现Cloneable接口,并在拷贝时,分别调用每一个成员对象的clone()方法
13. 常用的容器
注意:常见的Vector,Hashtable,ConcurrentHashMap,stack是线程安全的
14. List,Set和Map的区别
·List:实现Collection接口
允许有重复元素
可以插入多个null元素
是有序容器,保持了每个元素的插入顺序
·Set:实现Collection接口
不允许有重复元素
只允许一个null元素
是无序容器
·Map:不是Collection接口的子接口或实现类
每个元素都有两个对象,分别是键与值,其中键不能重复,值可以重复
值可以有多个null值,键只能有一个null
Map接口的实现类中,HashMap是无序的,LinkedHashMap是有序的
15. HashMap底层实现数据结构
HashMap底层上数据结构实现是数组+链表,Java8之后是数组+链表+红黑树。
大概过程是,HashMap会初始化一个数组用于存储每一个key-value,当传入key-value时,HashMap根据key.hashcode()计算出的hash值来决定键值对存放在数组的哪个位置,当两个不同的key值计算出相同的hash值时,为hash冲突,在Java8之前,相同hash值的元素会以链表的形式存放在数组的同一个位置,Java8之后,一条链表上元素超过8个,链表就会转换成红黑树。
先看源码:初始化时
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始化数组长度为16
HashMap桶数组初始化默认长度是16,这里规范要求必须是2的幂次,至于为什么是16,这就与索引的计算有关了,可以往后看
static final float DEFAULT_LOAD_FACTOR = 0.75f;//初始化负载因子为0.75f
因为默认数组长度为16,当放入的元素太多时,就会导致hash冲突出现多次,链表越来越长,为了解决这个问题,HashMap引入负载因子,就是当数组数据插入超过一个阈值时,数组就会触发扩容,默认为0.75f,也就是数组默认长度下,当你存进第16*0.75+1个元素时,数组就会触发扩容,直接扩容为原来两倍长度。扩容的具体过程我们也后面再讲^_^
static final int TREEIFY_THRESHOLD = 8;//默认链表转数组条件为超过8个元素
static class Node<K,V> implements Map.Entry<K,V> {
//这里定义的是数组中保存的对象类型
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap中每一个键值对都以Node的形式保存在数组中,里面不但包括了key和value还有key计算出的hash值和指向链表下一个元素的指针
接下来我们看我们用的最多的get和put方法是怎么实现的:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
可以看出来,get方法就是直接计算key的hash值,调用getNode方法,getNode就会遍历这个链表或者从红黑树直接取出我们要的数据
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法也是类似,这里我们看一下putVal方法中索引的计算方式
Index = (n - 1) & hash
n是数组长度,默认16,索引是hash值和数组长度减1做与运算,也就是说n默认16的情况下,n-1的二进制表示就是1111,和hash做与运算后就是取hash值的后四位,这样做的目的是为了使hash出的索引能均匀分布在数组中,减少hash冲突。
即使平均分布了,当插入的元素数量增加时,还是会不可避免的出现大量hash冲突,所以上面说到HashMap有扩容机制,我们再讲讲扩容机制,HashMap的扩容过程主要分两步:
·第一步:扩容,创建一个新的桶数组,长度为原来的两倍
·第二步:ReHash:遍历原来的数组,将每一个元素重新计算其hash值,再放到新数组中
特别需要注意的是:在Java7之前,put进新元素进链表和ReHash构造链表时都采用头插法,也就是同一位置上的新数据总是插入在链表的头部,这样会在Rehash后,改变链表顺序,
而在Java7之后,采用尾插法,即同一位置上的新元素是插入在链表尾部。这样就不会改变链表顺序了,这样的好处就是解决了多线程扩容时产生的链表循环问题:
多线程使用头插法时,假设线程1,2都对同一个HashMap进行扩容,线程1执行到扩容,并ReHash了一个元素时,此时线程1被挂起,但此时A.next依然是B,
线程2进行ReHash后,B会插入链表头部,B.next又会指向A,就会造成链表循环,所以这也是HashMap线程不安全的原因之一。
而使用尾插法,在扩容时会保持链表原来的顺序,就不会出现链表循环问题。但也不是说Java7之后的HashMap就是线程安全的了,因为还是没有解决丢失更新的问题。不过也没有关系,在多线程情况下我们还可以使用线程安全的Hashtable和ConcurrentHashMap。
丢失更新:当线程1插入一个Map<key1,value1>,然后线程1挂起,这时线程2插入一个Map<key1,value2>,对于同一个key,value1就被覆盖为value2,现在线程1再获取key1的值就不是value1了,这就是丢失更新。
猜你喜欢
- 2025-04-27 Java程序员,一周Python入门:数组,元组,集合,集合,字典
- 2025-04-27 redis Scan 踩坑记 key的模糊匹配
- 2025-04-27 Java开发面试官终结者!HashMap高频面试题总结,务必拿下
- 2025-04-27 内存溢出OutOfMemoryError科普系列一
- 2025-04-27 关于API接口的签名和权鉴,你知道多少?
- 2025-04-27 LinkedHashMap源码分析及LRU实现原理
- 2025-04-27 揭秘HashMap扩容机制:为何应用变慢,如何彻底解决问题?
- 2025-04-27 Java 面试笔记之 HashMap 和 ConcurrentHashMap
- 2025-04-27 架构篇-一分钟掌握可扩展架构
- 2025-04-27 我是如何做列表页的
- 最近发表
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- md5 sha1 (32)
- mybatis plus page (35)
- semaphore 使用详解 (32)
- update from 语句 (32)
- vue @scroll (38)
- 堆栈区别 (33)
- 在线子域名爆破 (32)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)