linkedhashmap 顺序_LinkedHashMap 源码详细分析(JDK1.8)

a60e11084ba36b2de822c28fc11efba7.png

1. 概述

LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。关于 HashMap 的源码分析,本文并不打算展开讲了。大家可以参考我之前的一篇文章“HashMap 源码详细分析(JDK1.8)”。在那篇文章中,我配了十多张图帮助大家学习 HashMap 源码。

本篇文章的结构与我之前两篇关于 Java 集合类(集合框架)的源码分析文章不同,本文将不再分析集合类的基本操作(查找、遍历、插入、删除),而是把重点放在双向链表的维护上。包括链表的建立过程,删除节点的过程,以及访问顺序维护的过程等。好了,接下里开始分析吧。

2. 原理

上一章说了 LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成,结构示意图大致如下:

364f4c23b5ec39d39882fd49df59c0e1.png

LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。其结构可能如下图:

3eb29a46d405aead0c1815d234b726ca.png

上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。

上面的结构并不是很难理解,虽然引入了红黑树,导致结构看起来略为复杂了一些。但大家完全可以忽略红黑树,而只关注链表结构本身。好了,接下来进入细节分析吧。

3. 源码分析

3.1 Entry 的继承体系

在对核心内容展开分析之前,这里先插队分析一下键值对节点的继承体系。先来看看继承体系结构图:

61f4b8d8fe8aad471c61c6379361a5dc.png

上面的继承体系乍一看还是有点复杂的,同时也有点让人迷惑。HashMap 的内部类 TreeNode 不继承它的了一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry。这里这样做是有一定原因的,这里先不说。先来简单说明一下上面的继承体系。LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。同时,TreeNode 继承 LinkedHashMap 的内部类 Entry 后,就具备了和其他 Entry 一起组成链表的能力。但是这里需要大家考虑一个问题。当我们使用 HashMap 时,TreeNode 并不需要具备组成链表能力。如果继承 LinkedHashMap 内部类 Entry ,TreeNode 就多了两个用不到的引用,这样做不是会浪费空间吗?简单说明一下这个问题(水平有限,不保证完全正确),这里这么做确实会浪费空间,但与 TreeNode 通过继承获取的组成链表的能力相比,这点浪费是值得的。在 HashMap 的设计思路注释中,有这样一段话:

Because TreeNodes are about twice the size of regular nodes, we

use them only when bins contain enough nodes to warrant use

(see TREEIFY_THRESHOLD). And when they become too small (due to

removal or resizing) they are converted back to plain bins. In

usages with well-distributed user hashCodes, tree bins are

rarely used.

大致的意思是 TreeNode 对象的大小约是普通 Node 对象的2倍,我们仅在桶(bin)中包含足够多的节点时再使用。当桶中的节点数量变少时(取决于删除和扩容),TreeNode 会被转成 Node。当用户实现的 hashCode 方法具有良好分布性时,树类型的桶将会很少被使用。

通过上面的注释,我们可以了解到。一般情况下,只要 hashCode 的实现不糟糕,Node 组成的链表很少会被转成由 TreeNode 组成的红黑树。也就是说 TreeNode 使用的并不多,浪费那点空间是可接受的。假如 TreeNode 机制继承自 Node 类,那么它要想具备组成链表的能力,就需要 Node 去继承 LinkedHashMap 的内部类 Entry。这个时候就得不偿失了,浪费很多空间去获取不一定用得到的能力。

说到这里,大家应该能明白节点类型的继承体系了。这里单独拿出来说一下,为下面的分析做铺垫。叙述略为啰嗦,见谅。

3.2 链表的建立过程

链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。

Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?在展开说明之前,我们先看一下 LinkedHashMap 插入操作相关的代码:

// HashMap 中实现public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}// HashMap 中实现final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) {...} // 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用 if ((p = tab[i = (n - 1) & hash]) == null) {...} else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) {...} else { // 遍历链表,并统计链表长度 for (int binCount = 0; ; ++binCount) { // 未在单链表中找到要插入的节点,将新节点接在单链表的后面 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) {...} break; } // 插入的节点已经存在于单链表中 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) {...} afterNodeAccess(e); // 回调方法,后续说明 return oldValue; } } ++modCount; if (++size > threshold) {...} afterNodeInsertion(evict); // 回调方法,后续说明 return null;}// HashMap 中实现Node newNode(int hash, K key, V value, Node next) { return new Node<>(hash, key, value, next);}// LinkedHashMap 中覆写Node newNode(int hash, K key, V value, Node e) { LinkedHashMap.Entry p = new LinkedHashMap.Entry(hash, key, value, e); // 将 Entry 接在双向链表的尾部 linkNodeLast(p); return p;}// LinkedHashMap 中实现private void linkNodeLast(LinkedHashMap.Entry p) { LinkedHashMap.Entry last = tail; tail = p; // last 为 null,表明链表还未建立 if (last == null) head = p; else { // 将新节点 p 接在链表尾部 p.before = last; last.after = p; }}

上面就是 LinkedHashMap 插入相关的源码,这里省略了部分非关键的代码。我根据上面的代码,可以知道 LinkedHashMap 插入操作的调用过程。如下:

219873a7822853f7f4069aaeaed2df79.png

我把 newNode 方法红色背景标注了出来,这一步比较关键。LinkedHashMap 覆写了该方法。在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。双向链表建立之后,我们就可以按照插入顺序去遍历 LinkedHashMap,大家可以自己写点测试代码验证一下插入顺序。

以上就是 LinkedHashMap 维护插入顺序的相关分析。本节的最后,再额外补充一些东西。大家如果仔细看上面的代码的话,会发现有两个以after开头方法,在上文中没有被提及。在 JDK 1.8 HashMap 的源码中,相关的方法有3个:

// Callbacks to allow LinkedHashMap post-actionsvoid afterNodeAccess(Node p) { }void afterNodeInsertion(boolean evict) { }void afterNodeRemoval(Node p) { }

根据这三个方法的注释可以看出,这些方法的用途是在增删查等操作后,通过回调的方式,让 LinkedHashMap 有机会做一些后置操作。上述三个方法的具体实现在 LinkedHashMap 中,本节先不分析这些实现,相关分析会在后续章节中进行。

3.3 链表节点的删除过程

与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现。在删除节点时,父类的删除逻辑并不会修复 LinkedHashMap 所维护的双向链表,这不是它的职责。那么删除及节点后,被删除的节点该如何从双链表中移除呢?当然,办法还算是有的。上一节最后提到 HashMap 中三个回调方法运行 LinkedHashMap 对一些操作做出响应。所以,在删除及节点后,回调方法 afterNodeRemoval 会被调用。LinkedHashMap 覆写该方法,并在该方法中完成了移除被删除节点的操作。相关源码如下:

// HashMap 中实现public V remove(Object key) { Node e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;}// HashMap 中实现final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) {...} else { // 遍历单链表,寻找要删除的节点,并赋值给 node 变量 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) {...} // 将要删除的节点从单链表中移除 else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); // 调用删除回调方法进行后续操作 return node; } } return null;}// LinkedHashMap 中覆写void afterNodeRemoval(Node e) { // unlink LinkedHashMap.Entry p = (LinkedHashMap.Entry)e, b = p.before, a = p.after; // 将 p 节点的前驱后后继引用置空 p.before = p.after = null; // b 为 null,表明 p 是头节点 if (b == null) head = a; else b.after = a; // a 为 null,表明 p 是尾节点 if (a == null) tail = b; else a.before = b;}

删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

举个例子说明一下,假如我们要删除下图键值为 3 的节点。

239bbd7c0f14c7a7fc3cdac47f3115b0.png

根据 hash 定位到该节点属于3号桶,然后在对3号桶保存的单链表进行遍历。找到要删除的节点后,先从单链表中移除该节点。如下:

54e99dbc41d7b67c0122e4ea6d41c19c.png

然后再双向链表中移除该节点:

a46f6476099d1ce483dfddcd19734960.png

删除及相关修复过程并不复杂,结合上面的图片,大家应该很容易就能理解,这里就不多说了。

3.4 访问顺序的维护过程

前面说了插入顺序的实现,本节来讲讲访问顺序。默认情况下,LinkedHashMap 是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。访问顺序的原理上并不复杂,当我们调用get/getOrDefault/replace等方法时,只需要将这些方法访问的节点移动到链表的尾部即可。相应的源码如下:

// LinkedHashMap 中覆写public V get(Object key) { Node e; if ((e = getNode(hash(key), key)) == null) return null; // 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后 if (accessOrder) afterNodeAccess(e); return e.value;}// LinkedHashMap 中覆写void afterNodeAccess(Node e) { // move node to last LinkedHashMap.Entry last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry p = (LinkedHashMap.Entry)e, b = p.before, a = p.after; p.after = null; // 如果 b 为 null,表明 p 为头节点 if (b == null) head = a; else b.after = a; if (a != null) a.before = b; /* * 这里存疑,父条件分支已经确保节点 e 不会是尾节点, * 那么 e.after 必然不会为 null,不知道 else 分支有什么作用 */ else last = b; if (last == null) head = p; else { // 将 p 接在链表的最后 p.before = last; last.after = p; } tail = p; ++modCount; }}

上面就是访问顺序的实现代码,并不复杂。下面举例演示一下,帮助大家理解。假设我们访问下图键值为3的节点,访问前结构为:

4e7544bf101d74a8580746b0a3799ee0.png

访问后,键值为3的节点将会被移动到双向链表的最后位置,其前驱和后继也会跟着更新。访问后的结构如下:

f1eca04c2396de3aa21a2620aa67fe80.png

3.5 基于 LinkedHashMap 实现缓存

前面介绍了 LinkedHashMap 是如何维护插入和访问顺序的,大家对 LinkedHashMap 的原理应该有了一定的认识。本节我们来写一些代码实践一下,这里通过继承 LinkedHashMap 实现了一个简单的 LRU 策略的缓存。在写代码之前,先介绍一下前置知识。

在3.2节分析链表建立过程时,我故意忽略了部分源码分析。本节就把忽略的部分补上,先看源码吧:

void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry first; // 根据条件判断是否移除最近最少被访问的节点 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); }}// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存protected boolean removeEldestEntry(Map.Entry eldest) { return false;}

上面的源码的核心逻辑在一般情况下都不会被执行,所以之前并没有进行分析。上面的代码做的事情比较简单,就是通过一些条件,判断是否移除最近最少被访问的节点。看到这里,大家应该知道上面两个方法的用途了。当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。本节所实现的缓存是基于判断节点数量是否超限的策略。在构造缓存对象时,传入最大节点数。当插入的节点数超过最大节点数时,移除最近最少被访问的节点。实现代码如下:

public class SimpleCache extends LinkedHashMap { private static final int MAX_NODE_NUM = 100; private int limit; public SimpleCache() { this(MAX_NODE_NUM); } public SimpleCache(int limit) { super(limit, 0.75f, true); this.limit = limit; } public V save(K key, V val) { return put(key, val); } public V getOne(K key) { return get(key); } public boolean exists(K key) { return containsKey(key); } /** * 判断节点数是否超限 * @param eldest * @return 超限返回 true,否则返回 false */ @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > limit; }}

测试代码如下:

public class SimpleCacheTest { @Test public void test() throws Exception { SimpleCache cache = new SimpleCache<>(3); for (int i = 0; i < 10; i++) { cache.save(i, i * i); } System.out.println("插入10个键值对后,缓存内容:"); System.out.println(cache + ""); System.out.println("访问键值为7的节点后,缓存内容:"); cache.getOne(7); System.out.println(cache + ""); System.out.println("插入键值为1的键值对后,缓存内容:"); cache.save(1, 1); System.out.println(cache); }}

测试结果如下:

5dd221fde0019c37c6a0313fff82bce9.png

在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。

本节作为对前面内的补充,简单介绍了 LinkedHashMap 在其他方面的应用。本节内容及相关代码并不难理解,这里就不在赘述了。

4. 总结

本文从 LinkedHashMap 维护双向链表的角度对 LinkedHashMap 的源码进行了分析,并在文章的结尾基于 LinkedHashMap 实现了一个简单的 Cache。在日常开发中,LinkedHashMap 的使用频率虽不及 HashMap,但它也个重要的实现。在 Java 集合框架中,HashMap、LinkedHashMap 和 TreeMap 三个映射类基于不同的数据结构,并实现了不同的功能。HashMap 底层基于拉链式的散列结构,并在 JDK 1.8 中引入红黑树优化过长链表的问题。基于这样结构,HashMap 可提供高效的增删改查操作。LinkedHashMap 在其之上,通过维护一条双向链表,实现了散列数据结构的有序遍历。TreeMap 底层基于红黑树实现,利用红黑树的性质,实现了键值对排序功能。我在前面几篇文章中,对 HashMap 和 TreeMap 以及他们均使用到的红黑树进行了详细的分析,有兴趣的朋友可以去看看。

到此,本篇文章就写完了,感谢大家的阅读!

附录:映射类文章列表

  • 红黑树详细分析
  • TreeMap源码分析
  • HashMap 源码详细分析(JDK1.8)

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

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

相关文章

testNG入门详解

TestNG 的注释: DataProvider ExpectedExceptions Factory Test Parameters <suite name"ParametersTest"><test name"Regression1"><classes><class name"com.example.ParameterSample" /><class name"com.exa…

尼康相机报错err_数码相机遇到这12种错误,自己动手就能解决,再不用找人维修...

如果您资深摄影师或者专业摄影爱好者&#xff0c;那么您必须熟悉下面提到的数码相机所出现的问题和错误。如果您没有遇到任何问题&#xff0c;要么您是初学者&#xff0c;要么您已经很少拍摄了。下面列出的常见相机问题及其解决方案&#xff0c;会为您在使用数码相机的过程中提…

mysql 慢查询过多_MySQL 慢查询优化

为什么查询速度会慢1.慢是指一个查询的响应时间长。一个查询的过程&#xff1a;客户端发送一条查询给服务器服务器端先检查查询缓存&#xff0c;如果命中了缓存&#xff0c;则立可返回存储在缓存中的结果。否则进入下一个阶段服务器端进行SQL解析、预处理&#xff0c;再由优化器…

android beta项目官方页面,安卓7.0开发者预览版如何安装?Android Beta项目正式上线...

谷歌现在越来越不按常理出牌了&#xff0c;今天早些时候&#xff0c;他们已经提前秀出了Android 7.0。从最新亮相的Android N开发者预览版来看&#xff0c;谷歌进行了一些调整&#xff0c;但更重要的是&#xff0c;增加了一些新的功能&#xff0c;比如分屏、新的通知控制等。那…

iOS-模糊查询

http://blog.csdn.net/qq_33701006/article/details/51836914 版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 目录(?)[] 前言: 为了巩固FMDB,就来找个简单的Demo学习一下。不好找工作啊&#xff0c;就学习吧&#xff0c;没应聘的消遣吧。 简单介…

钱币掉落动画android,mpvue实现小程序签到金币掉落动画(api实现)

这里使用小程序自带的api来实现&#xff0c;用小程序来写动画的恶心点在于&#xff0c;没有帧&#xff0c;只能用setimeout 来作为帧来使用&#xff0c;下面是实现代码&#xff0c; 下面是简单用div代替了图片&#xff0c;需要什么图片&#xff0c;可以自行替换相应的div即可需…

前端学习(2197):__WEBPACK_IMPORTED_MODULE_1_vuex__.a.store is not a constructor

在使用vuex过程中&#xff0c;发现报错 typeError:__WEBPACK_IMPORTED_MODULE_1_vuex__.a.store is not a constructor 经查找发现是实例化时 .store用的小写造成的,如下 new Vuex.store({state:{},mutations:{},actions:{},modules:{} }) 实际应为大写!&#xff08;居然有和…

android特殊代码,安卓手机输入这些特殊代码,电池状态查得清清楚楚!

原标题&#xff1a;安卓手机输入这些特殊代码&#xff0c;电池状态查得清清楚楚&#xff01;智能手机在很大程度上方便了我们的生活&#xff0c;但是我们也逐渐依赖上了手机&#xff0c;想更了解自己的手机&#xff0c;知道自己到底在手机哪些地方花费了多少时间吗&#xff0c;…

android 三星 白色,时尚实用都拥有 白色Android手机盘点

唯美大气&#xff1a;三星I9000三星I9000的高人气不用多说&#xff0c;许多人在看过了黑色之后也等待着白色版本的上市。而在上周该机的白色版本也终于到来&#xff0c;赶在圣诞节之前为我们提供了多一种的白色Android机型选择。从图片中可以看出I9000机身正面依旧为黑色&#…

见微知著(一):解析ctf中的pwn--Fast bin里的UAF

在网上关于ctf pwn的入门资料和writeup还是不少的&#xff0c;但是一些过渡的相关知识就比较少了&#xff0c;大部分赛棍都是在不断刷题中总结和进阶的。所以我觉得可以把学习过程中的遇到的一些问题和技巧总结成文&#xff0c;供大家参考和一起交流。当然&#xff0c;也不想搞…

火狐插件 打开html 死机,火狐flash插件崩溃(Firefox火狐Flash插件卡死问题完美解决方法)...

火狐flash插件崩溃(Firefox火狐Flash插件卡死问题完美解决方法)&#xff0c;哪吒游戏网给大家带来详细的火狐flash插件崩溃(Firefox火狐Flash插件卡死问题完美解决方法)介绍&#xff0c;大家可以阅读一下&#xff0c;希望这篇火狐flash插件崩溃(Firefox火狐Flash插件卡死问题完…

uuid表示时间的部分_技术译文 | UUID 很火但性能不佳?今天我们细聊一聊

作者&#xff1a;Yves Trudeau Yves 是 Percona 的首席架构师&#xff0c;专门研究分布式技术&#xff0c;例如 MySQL Cluster&#xff0c;Pacemaker 和 XtraDB cluster。他以前是 MySQL 和 Sun 的高级顾问。拥有实验物理学博士学位。原文链接&#xff1a;https://www.percona.…

西电计算机科学院实践中心,计算机基础教学实验中心

一、总体情况计算机基础教学实验中心隶属于计算机网络与信息安全国家级实验教学示范中心&#xff0c;承担着全校本科生的计算机基础教学和实验任务&#xff0c;是学校对外的重要窗口。中心总面积4200平方米&#xff0c;固定资产总价值接近1500万元&#xff0c;仪器设备共3907台…

jquery通过attr取html里自定义属性原来这么方便啊

<script type"text/javascript"> function fangGouWuChe(obj) { //放入购物车 var sMat $(obj).parent().parent().parent().parent().attr("material"); var sPrice $(obj).parent().parent().find(em[class"sale-price"]).text(); …

千牛通知栏常驻是什么意思_店铺运营|内贸1688 店铺真正的权重是什么?

想必大家都听说过店铺权重/单品权重/客户标签 这些名词。那到底是怎么操作的呢&#xff1f; 以下是简单的大纲&#xff1a;店铺权重的拆解拆分里面的小指标&#xff0c;看看我们有没有操作的空间单品的权重拆分里面的小指标&#xff0c;有哪些因素是我们能够控制的&#xff1f;…

charts引入icon图片_v-charts 踩坑之路

最近要做一个大屏 没有使用echarts 使用了更适合vue封装的v-charts组件库&#xff0c;第一次使用 期间踩了不少坑&#xff0c;记录下来和大家分享一下。废话不多说 开始搞起来&#xff01;一、安装 引入什么的大家自行百度 百度一下&#xff0c;你就知道​www.baidu.com二、2.1…

pla3d打印材料密度_模具粉必看!总有一款粉末能解决您的问题-毅速3D打印研制...

金属3D打印最常见的形式是粉末床熔融。这类工艺使用热源&#xff08;SLM工艺使用激光&#xff0c;EBM工艺使用电子束&#xff09;逐点将粉末颗粒熔融在一起&#xff0c;逐层加工至物件完成。在金属3D打印过程中&#xff0c;可能存在很多设备操作者试图避免的问题&#xff0c;包…