【学习笔记】JDK源码学习之HashMap(附带面试题)

news2025/2/4 8:47:08

最近发布 AND 其他好文 :地址

什么是 HashMap ?它和 Map 之间又有什么样的关系呢?我们是否能拿它在并发中使用呢?

带着以上问题,让我们一同走入这让人着迷的 HashMap

1、什么是HashMap?

HashMap 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

数据结构:

JDK1.7 版本时,HashMap 使用的是 链表加数组 来实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GjOf7Yfj-1671503629125)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20221212164853732.png)]

而在 1.8 之后 HashMap 则使用的是 数组+链表+红黑树 来实现底层结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1aRkWyCj-1671503629126)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20221212165257367.png)]

PS:本篇文以 JDK1.8 为主,如果没有提及其他版本则就是以1.8版本来。

HashMap 的继承图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U38jebC7-1671503629126)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20221212171306075.png)]

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  ...
}

通过 继承图和源码 我们能知道 HashMap 继承了 AbstractMap 类,实现了 Map<K,V>, Cloneable, Serializable 接口。

HashMap 有了以下功能:

  • 具有Map的kv功能。
  • 可以进行深浅拷贝。
  • 能实现序列化功能。

2、HashMap常用的变量、构造函数和方法

2.1 常用的变量

HashMap 源码中,比较重要的常用变量,主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点。普通链表中每一个节点都会保存自身的hash、key、value、以及下个节点。

//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
//为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。
//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
//存放所有Node节点的数组
transient Node<K,V>[] table;
//存放所有的键值对
transient Set<Map.Entry<K,V>> entrySet;
//map中的实际键值对个数,即数组中元素个数
transient int size;
//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
transient int modCount;
//数组扩容阈值;当实际大小(容量*填充比)超过临界值时,会进行扩容 
int threshold;
//加载因子
final float loadFactor;					
 
//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
	//hash存放的是当前key的hashcode,put和get的时候都需要用到它来确定元素在数组中的位置
	final int hash;
	final K key;
	V value;
	//指向单链表的下一个节点
	Node<K,V> next;
 
	Node(int hash, K key, V value, Node<K,V> next) {
		this.hash = hash;
		this.key = key;
		this.value = value;
		this.next = next;
	}
}
 
//转化为红黑树的节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	//当前节点的父节点
	TreeNode<K,V> parent;  
	//左孩子节点
	TreeNode<K,V> left;
	//右孩子节点
	TreeNode<K,V> right;
	//指向前一个节点
	TreeNode<K,V> prev;    // needed to unlink next upon deletion
	//当前节点是红色或者黑色的标识
	boolean red;
	TreeNode(int hash, K key, V val, Node<K,V> next) {
		super(hash, key, val, next);
	}
}	

2.2 HashMap中的构造方法

HashMap 中有四种:

  • 默认无参构造,指定一个默认的加载因子
  • 可指定容量的有参构造,但是需要注意当前指定的容量并不一定就是实际的容量
  • 传入初始容量和加载因子
  • 可传入一个已有的map

2.2.1 HashMap()

源码:

//默认无参构造,指定一个默认的加载因子
public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

没有穿参数,默认指定一个加载因子—— DEFAULT_LOAD_FACTOR(0.75f)

2.2.2 HashMap(int initialCapacity)

源码:

//可指定容量的有参构造,但是需要注意当前指定的容量并不一定就是实际的容量,下面会说
public HashMap(int initialCapacity) {
	//同样使用默认加载因子
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

2.2.3 HashMap(int initialCapacity, float loadFactor)

public HashMap(int initialCapacity, float loadFactor) {
  // 
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  // 判断索引是否超过HashMap的最大值
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
  // 判断负载因子是否合法
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
		throw new IllegalArgumentException("Illegal load factor: " +
										   loadFactor);
	this.loadFactor = loadFactor;
	//把指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
	//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
  // 接着向下看嗷
	this.threshold = tableSizeFor(initialCapacity);
}

2.2.4 HashMap(Map<? extends K, ? extends V> m)

源码:

//可传入一个已有的map
public HashMap(Map<? extends K, ? extends V> m) {
  // 同样适用默认的负载因子
	this.loadFactor = DEFAULT_LOAD_FACTOR;
	putMapEntries(m, false);
}

2.2.5 tableSizeFor()

源码:

static final int tableSizeFor(int cap) {
	int n = cap - 1;
	n |= n >>> 1;
	n |= n >>> 2;
	n |= n >>> 4;
	n |= n >>> 8;
	n |= n >>> 16;
	return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

以传入参数为14 来举例,计算这个过程。

首先,14传进去之后先减1,n此时为13。然后是一系列的无符号右移运算。

//13的二进制
0000 0000 0000 0000 0000 0000 0000 1101 
//无右移1位,高位补0
0000 0000 0000 0000 0000 0000 0000 0110 
//然后把它和原来的13做或运算得到,此时的n值
0000 0000 0000 0000 0000 0000 0000 1111 
//再以上边的值,右移2位
0000 0000 0000 0000 0000 0000 0000 0011
//然后和第一次或运算之后的 n 值再做或运算,此时得到的n值
0000 0000 0000 0000 0000 0000 0000 1111
...
//发现,再执行右移 4,8,16位,同样n的值不变
//当n小于0时,返回1,否则判断是否大于最大容量,是的话返回最大容量,否则返回 n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
//很明显我们这里返回的是 n+1 的值,
0000 0000 0000 0000 0000 0000 0000 1111
+                                     1
0000 0000 0000 0000 0000 0000 0001 0000

将它转为十进制,就是 2^4 = 16 。发现一个规律,以上的右移运算,最终会把最低位的值都转化为 1111 这样的结构,然后再加1,就是1 0000 这样的结构,它一定是 2的n次幂。因此,这个方法返回的就是大于当前传入值的最小(最接近当前值)的一个2的n次幂的值。

2.3 HashMap中常用的方法

2.3.1 put(K key, V value)

源码:

//put方法,会先调用一个hash()方法,得到当前key的一个hash值,
//用于确定当前key应该存放在数组的哪个下标位置
//这里的 hash方法,姑且先认为是key.hashCode(),其实不是的,下面会细讲
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}
 
//把hash值和当前的key,value传入进来
//这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此传入false
//evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	//判断table是否为空,如果空的话,会先调用resize扩容(resize中会判断是否进行初始化)
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	//根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,
	//若没有,则把key、value包装成Node节点,直接添加到此位置。
	// i =(n - 1) & hash是计算下标位置的
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else { 
		//如果当前位置已经有元素了,分为三种情况。
		Node<K,V> e; K k;
      
		//1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,
		//则把p赋值给e,跳转到①处,后续需要做值的覆盖处理
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		//2.如果当前是红黑树结构,则把它加入到红黑树 
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
		//3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					//如果头结点的下一个节点为空,则插入新节点
					p.next = newNode(hash, key, value, null);
					//如果在插入的过程中,链表长度超过了8,则转化为红黑树
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					//插入成功之后,跳出循环,跳转到①处
					break;
				}
				//若在链表中找到了相同key的话,直接退出循环,跳转到①处
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		//①
		//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			//用新值替换旧值,并返回旧值。
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			//看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现,
			//只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能
			// Callbacks to allow LinkedHashMap post-actions
			//void afterNodeAccess(Node<K,V> p) { }
			afterNodeAccess(e);
			return oldValue;
		}
	}
	//fail-fast机制
	++modCount;
	//判断是否需要进行扩容
	if (++size > threshold)
		resize();
	//同样的空实现
	afterNodeInsertion(evict);
	return null;
}

map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。

  1. 如果此位置上的数据为空,此时的key1-value1添加成功。----情况1

  2. 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

    2.1 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key-value1添加成功。----情况2

    2.2 如果key1的哈希值和已经存在的某一个数据(key-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法

    ​ 2.2.1 如果equals()返回false:此时key1-value1添加成功。----情况3
    ​ 2.2.2 如果equals()返回true:使用value1替换value2.(修改作用的体现)

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

2.3.2 resize()

源码:

final Node<K,V>[] resize() {
	//旧数组
	Node<K,V>[] oldTab = table;
	//旧数组的容量
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	//旧数组的扩容阈值,注意,这里取的是当前对象的threshold值,下边的第2种情况会用到。
	int oldThr = threshold;
	//初始化新数组的容量和阈值,分三种情况讨论。
	int newCap, newThr = 0;
	//1.当旧数组的容量大于0时,说明在这之前肯定调用过resize扩容过一次,才会导致旧容量不为0。
	//需要注意的是,它返回的值是赋给了threshold,而不是capacity。
	//在这之前,没有在任何地方看到过,给capacity赋初始值。
	if (oldCap > 0) {
		//容量达到了最大值
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		//新数组的容量和阈值都扩大原来的2倍
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	//2.到这里,说明oldCap <=0,并且oldThr(threshold) > 0,这就是map初始化的时候,第一次调用resize的情况
	//而oldThr的值等于threshold,此时的threshold是通过tableSizeFor方法得到的一个2的n次幂的值(我们以16为例)。
	//因此,需要把oldThr的值,也就是threshold,赋值给新数组的容量newCap,以保证数组的容量是2的n次幂。
	//得出结论,当map第一次put元素时,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂)
	//但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别急,这个会在③处理。
	else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr;
	//3.到这里,说明oldCap和oldThr都是小于等于0的。也说明map是通过默认无参构造来创建的,
	//于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	//③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0,
	//因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	//赋予threshold正确的值,表示数组下次需要扩容的阈值(此时就把原来的16修正为12)。
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		//可以发现,在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。这是为了延迟加载,提高效率。
		Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	//如果原来的数组不为空,那么就需要把原来数组中的元素重新分配到新的数组中
	//如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。
	if (oldTab != null) {
		//遍历旧数组
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			//取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				//1.如果当前元素的下一个元素为空,则说明此处只有一个元素
				//则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。
				if (e.next == null)
					newTab[e.hash & (newCap - 1)] = e;
				//2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表
				else if (e instanceof TreeNode)
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				//3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并
				//判断当前位置的链表是否需要移动到新的位置
				else { // preserve order
					// loHead 和 loTail 分别代表链表旧位置的头尾节点
					Node<K,V> loHead = null, loTail = null;
					// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						//如果当前元素的hash值和oldCap做与运算为0,则原位置不变
						if ((e.hash & oldCap) == 0) {
							if (loTail == null)
								loHead = e;
							else
								loTail.next = e;
							loTail = e;
						}
						//否则,需要移动到新的位置
						else {
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
					//原位置不变的一条链表,数组下标不变
					if (loTail != null) {
						loTail.next = null;
						newTab[j] = loHead;
					}
					//移动到新位置的一条链表,数组下标为原下标加上旧数组的容量
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}



过程:

分为两步:

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

为什么要重新Hash?直接复制过去不可以吗?

是因为长度扩大以后,Hash的规则也随之改变。

Hash的公式—> index = HashCode(Key) & (Length - 1)

原来长度 (Length) 是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。

扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,假设当前tableSize是16的话16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2

2.3.3 get(Object key)

源码:

public V get(Object key) {
	Node<K,V> e;
	//如果节点为空,则返回null,否则返回节点的value。说明,hashMap是支持value为null的。
	//因此,就明白了,为什么hashMap支持Key和value都为null
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}
 
final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	//首先要确保数组不能为空,然后取到当前hash值计算出来的下标位置的第一个元素
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		//若hash值和key都相等,则说明要找的就是第一个元素,直接返回
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		//如果不是的话,就遍历当前链表(或红黑树)
		if ((e = first.next) != null) {
			//如果是红黑树结构,则找到当前key所在的节点位置
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			//如果是普通链表,则向后遍历查找,直到找到或者遍历到链表末尾为止。
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	//否则,说明没有找到,返回null
	return null;
}

HashMap常见面试题

jdk8中HashMap为什么要引入红黑树?

为啥重写equals方法的时候需要重写hashCode方法?用HashMap举个例子?

关于映射关系的key是否可以修改?

扩容后的新table数组,那老数组中的这个数据怎么迁移呢?

新的Entry节点,是怎么插入链表的? 为啥改为尾部插入呢?

头插是JDK1.7的那1.8的尾插是怎么样的呢?那是不是意味着Java8就可以把HashMap用在多线程中呢?

哈希冲突

为什么jdk1.7的HashMap链表会形成死循环?

HashMap是线程不安全的,如何处理其在线程安全的场景?

HashMap的底层数据结构?

HashMap的存取原理?

Java7和Java8的区别?

为啥会线程不安全?

有什么线程安全的类代替么?

默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

HashMap的扩容方式?负载因子是多少?为什是这么多?

HashMap的主要参数都有哪些?

HashMap是怎么处理hash碰撞的?

hash的计算规则?

谈谈对HashMap中put\get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?

参考文章

https://blog.csdn.net/weixin_49329785/article/details/122902494

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/103500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DOM算法系列005-获取给定节点所属的window对象

UID: 20221220091217 aliases: tags: source: cssclass: created: 2022-12-20 我们知道&#xff0c;在一个HTML页面中&#xff0c;可以包含很多个框架或框架集&#xff0c;那么&#xff0c;当给定一个节点&#xff0c;我们如何获取这个节点所属的window对象呢&#xff1f; 首先…

【填坑向】Linux获取Java程序resource下的文件路径(这次的坑好像没填住……

​╮(&#xffe3;▽&#xffe3;"")╭没错&#xff0c;就是那个传说中的FileNotFoundException&#xff0c;文件找不到了 一开始&#xff0c;特别单纯的俺是把文件直接放在项目根目录的&#xff0c;获取文件地址的位置就只写了一个文件名&#xff08;相对路径&…

Nature子刊:生命的最初几个小时内可快速学习音位辨别—基于fNIRS研究

摘要 人类新生儿可以区分音素&#xff0c;但这种能力背后的神经机制尚不清楚。新生儿的大脑可以学会区分自然&#xff08;前&#xff09;元音和逆向&#xff08;后&#xff09;元音&#xff08;在子宫中学习不到的&#xff09;。利用功能近红外光谱&#xff0c;我们检测了出生…

工控CTF之协议分析2——MMS

协议分析 流量分析 主要以工控流量和恶意流量为主&#xff0c;难度较低的题目主要考察Wireshark使用和找规律&#xff0c;难度较高的题目主要考察协议定义和特征 简单只能简单得干篇一律&#xff0c;难可以难得五花八门 常见的工控协议有&#xff1a;Modbus、MMS、MQTT、CoA…

关于3D可视化的几个知识点

3D可视化&#xff0c;运用三维仿真技术实现信息感知、智能调度以及主动式运营维护&#xff0c;把复杂抽象的数据信息&#xff0c;以合适的人们视觉元素及视角用3D立体方式进行一比一还原呈现&#xff01; 近些年&#xff0c;将繁琐的信息通过可视化的图表视觉化展现出来的应用场…

惠普Elite蜻笔记本系统损坏怎么U盘重装教学

惠普Elite蜻笔记本系统损坏怎么U盘重装教学&#xff0c;有用户使用的惠普Elite蜻笔记本系统受到了其他恶意程序的损坏&#xff0c;导致无法正常的开启使用。所以想要去进行电脑系统的重装。那么如何U盘重装电脑系统&#xff0c;一起来看看详细的重装步骤吧。 准备工作&#xff…

有哪些实用的软件开发项目管理工具?

给大家介绍下&#xff0c;目前最新的免费项目管理工具&#xff0c;希望对大家有所帮助。 1、 Co-Project智能项目管理工具 智能项目管理平台&#xff0c;是CoCode旗下的软件开发全生命周期的项目管理平台&#xff0c;涵盖了从需求、计划、设计、开发、测试再到交付的整个项目周…

青少年关联网络功能地形的性别差异

我们发现了个性化联想网络功能地形的规范性性别差异&#xff0c;包括腹侧注意、默认模式和额顶网络。此外&#xff0c;染色体富集分析显示&#xff0c;功能地形多变量模式的性别差异在空间上与x连锁基因的表达以及星形细胞和兴奋性神经元细胞类型的特征相耦合。这些结果突出了性…

实景三维模型色彩差异大,用模方与DasViewer直接对模型调色

在实际项目生产过程中&#xff0c;由于受到采集天气、设备、时间、季节、航高等各类因素的影响&#xff0c;重建后的三维模型尤其是大面积模型中&#xff0c;纹理不可避免的出现明暗不一、颜色不均匀的现象&#xff0c;导致后期处理时间与成本增高&#xff0c;影响项目工期。 三…

Apache Flink

目录 处理无边界和有边界数据 数据可以作为无边界流或有边界流处理 随时随地部署应用程序 以任何规模运行应用程序 利用内存性能 Apache Flink 起源于一个叫作 Stratosphere 的项目&#xff0c;它是由 3 所地处柏林的大学和欧洲其他一些大学共同进行的研究项目&#xff0c…

[附源码]计算机毕业设计Node.js湖南旅游景点网站(程序+LW)

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

Java码客过去的一周年

入坑伊始 2021年12月20日 我本身就是一名计算机科学与技术的大学生&#xff0c;应该也算是行内的科班出身&#xff0c;但是在2021年12月20日之前&#xff0c;也就是离19届大学生考研差不多一年的时间&#xff0c;那时候有一周的SSM网站编程实训课程&#xff0c;惊讶的是虽然学…

别再用 JWT 作为 Session 系统了,问题重重,后果很危险!

SON Web Tokens&#xff0c;又称 JWT。本文将详解&#xff1a;为何 JWT 不适合存储 Session&#xff0c;以及 JWT 引发的安全隐患。望各位对JWT有更深的理解&#xff01; 十分不幸&#xff0c;我发现越来越多的人开始推荐使用 JWT 管理网站的用户会话&#xff08;Session&…

从头到尾Spring概念,家族,核心技术使用,事务这一篇就够了!!!

目录 1.了解Spring家族 家族成员 Spring常用项目介绍 2.Spring Framework体系结构 概述 体系图 3.Spring框架提供的核心技术 IOC控制反转 1.IOC与Spring的关系 2.什么是Bean DI 依赖注入 IoC/DI技术实现 1.基于XML的实现 2.基于注解的实现 3.基于java的实现 AO…

【案例实践】InVEST实践与进阶及在生态系统服务供需、固碳、城市热岛、论文写作中的应用

【查看原文】InVEST实践与进阶及在生态系统服务供需、固碳、城市热岛、论文写作等实际项目中的具体应用 【专家】&#xff1a; 白老师&#xff08;研究员&#xff09;&#xff1a;长期从事生态系统结构-格局-过程-功能-服务的变化与响应关系等研究工作&#xff1b;重点围绕生…

使用 .NET MAUI 开发 ChatGPT 客户端

最近 chatgpt 很火&#xff0c;由于网页版本限制了 ip&#xff0c;还得必须开代理&#xff0c;用起来比较麻烦&#xff0c;所以我尝试用 maui 开发一个聊天小应用&#xff0c;结合 chatgpt 的开放 api 来实现&#xff08;很多客户端使用网页版本接口用 cookie 的方式&#xff0…

含辞未吐,声若幽兰,史上最强免费人工智能AI语音合成TTS服务微软Azure(Python3.10接入)

所谓文无第一&#xff0c;武无第二&#xff0c;云原生人工智能技术目前呈现三足鼎立的态势&#xff0c;微软&#xff0c;谷歌以及亚马逊三大巨头各擅胜场&#xff0c;不分伯仲&#xff0c;但目前微软Azure平台不仅仅只是一个PaaS平台&#xff0c;相比AWS&#xff0c;以及GAE&am…

【云原生】k8s的pod基础

内容预知 1.pod的相关知识 1.1 pod的基础概念 1.2 k8s中pod的两种使用方式 1.3 pod 容器的常规使用流程 1.4 k8s中pod结构设计的巧妙用意 通常把Pod分为两类 2. 容器的分类 2.1 pause基础容器&#xff08;infrastructure container&#xff09; &#xff08;1&#xf…

安装ZIMG 图片服务器

简介&#xff1a;zimg是图像存储和处理服务器。您可以使用URL参数从zimg获取压缩和缩放的图像。 zimg的并发I / O&#xff0c;分布式存储和时间处理能力非常出色。 您不再需要在图像服务器中使用nginx。在基准测试中&#xff0c;zimg可以在高并发级别上处理每秒3000图像下载任务…

【性能篇】29 # 怎么给Canvas绘制加速?

说明 【跟月影学可视化】学习笔记。 方法一&#xff1a;优化 Canvas 指令 例子&#xff1a;实现一些位置随机的多边形&#xff0c;并且不断刷新这些图形的形状和位置 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…