首頁 Java java教程 Java集合框架之LinkedHashMap源碼分析詳解

Java集合框架之LinkedHashMap源碼分析詳解

Sep 26, 2017 am 09:37 AM
java linkedhashmap 原始碼

這篇文章主要介紹了Java集合框架源碼分析之LinkedHashMap詳解,內容包括了linkedhashmap的簡介和源碼剖析以及關於LinkedHashMap的源碼總結,內容豐富,需要的朋友可以參考下。

LinkedHashMap簡介

LinkedHashMap是HashMap的子類,與HashMap有著同樣的儲存結構,但它加入了一個雙向鍊錶的頭結點,將所有put到LinkedHashmap的節點一一串成了雙向循環鍊錶,因此它保留了節點插入的順序,可以讓節點的輸出順序與輸入順序相同。

LinkedHashMap可以用來實作LRU演算法(這會在下面的原始碼中進行分析)。

LinkedHashMap同樣是非執行緒安全的,只在單執行緒環境下使用。

LinkedHashMap原始碼剖析

#LinkedHashMap原始碼如下(加入了詳細的註解):


#
package java.util; 
import java.io.*; 
public class LinkedHashMap<K,V> 
  extends HashMap<K,V> 
  implements Map<K,V> 
{ 
  private static final long serialVersionUID = 3801124242820219131L; 
  //双向循环链表的头结点,整个LinkedHashMap中只有一个header, 
  //它将哈希表中所有的Entry贯穿起来,header中不保存key-value对,只保存前后节点的引用 
  private transient Entry<K,V> header; 
  //双向链表中元素排序规则的标志位。 
  //accessOrder为false,表示按插入顺序排序 
  //accessOrder为true,表示按访问顺序排序 
  private final boolean accessOrder; 
  //调用HashMap的构造方法来构造底层的数组 
  public LinkedHashMap(int initialCapacity, float loadFactor) { 
    super(initialCapacity, loadFactor); 
    accessOrder = false;  //链表中的元素默认按照插入顺序排序 
  } 
  //加载因子取默认的0.75f 
  public LinkedHashMap(int initialCapacity) { 
    super(initialCapacity); 
    accessOrder = false; 
  } 
  //加载因子取默认的0.75f,容量取默认的16 
  public LinkedHashMap() { 
    super(); 
    accessOrder = false; 
  } 
  //含有子Map的构造方法,同样调用HashMap的对应的构造方法 
  public LinkedHashMap(Map<? extends K, ? extends V> m) { 
    super(m); 
    accessOrder = false; 
  } 
  //该构造方法可以指定链表中的元素排序的规则 
  public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) { 
    super(initialCapacity, loadFactor); 
    this.accessOrder = accessOrder; 
  } 
  //覆写父类的init()方法(HashMap中的init方法为空), 
  //该方法在父类的构造方法和Clone、readObject中在插入元素前被调用, 
  //初始化一个空的双向循环链表,头结点中不保存数据,头结点的下一个节点才开始保存数据。 
  void init() { 
    header = new Entry<K,V>(-1, null, null, null); 
    header.before = header.after = header; 
  } 
  //覆写HashMap中的transfer方法,它在父类的resize方法中被调用, 
  //扩容后,将key-value对重新映射到新的newTable中 
  //覆写该方法的目的是为了提高复制的效率, 
  //这里充分利用双向循环链表的特点进行迭代,不用对底层的数组进行for循环。 
  void transfer(HashMap.Entry[] newTable) { 
    int newCapacity = newTable.length; 
    for (Entry<K,V> e = header.after; e != header; e = e.after) { 
      int index = indexFor(e.hash, newCapacity); 
      e.next = newTable[index]; 
      newTable[index] = e; 
    } 
  } 
  //覆写HashMap中的containsValue方法, 
  //覆写该方法的目的同样是为了提高查询的效率, 
  //利用双向循环链表的特点进行查询,少了对数组的外层for循环 
  public boolean containsValue(Object value) { 
    // Overridden to take advantage of faster iterator 
    if (value==null) { 
      for (Entry e = header.after; e != header; e = e.after) 
        if (e.value==null) 
          return true; 
    } else { 
      for (Entry e = header.after; e != header; e = e.after) 
        if (value.equals(e.value)) 
          return true; 
    } 
    return false; 
  } 
  //覆写HashMap中的get方法,通过getEntry方法获取Entry对象。 
  //注意这里的recordAccess方法, 
  //如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做, 
  //如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。 
  public V get(Object key) { 
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null) 
      return null; 
    e.recordAccess(this); 
    return e.value; 
  } 
  //清空HashMap,并将双向链表还原为只有头结点的空链表 
  public void clear() { 
    super.clear(); 
    header.before = header.after = header; 
  } 
  //Enty的数据结构,多了两个指向前后节点的引用 
  private static class Entry<K,V> extends HashMap.Entry<K,V> { 
    // These fields comprise the doubly linked list used for iteration. 
    Entry<K,V> before, after; 
    //调用父类的构造方法 
    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { 
      super(hash, key, value, next); 
    } 
    //双向循环链表中,删除当前的Entry 
    private void remove() { 
      before.after = after; 
      after.before = before; 
    } 
    //双向循环立链表中,将当前的Entry插入到existingEntry的前面 
    private void addBefore(Entry<K,V> existingEntry) { 
      after = existingEntry; 
      before = existingEntry.before; 
      before.after = this; 
      after.before = this; 
    } 
    //覆写HashMap中的recordAccess方法(HashMap中该方法为空), 
    //当调用父类的put方法,在发现插入的key已经存在时,会调用该方法, 
    //调用LinkedHashmap覆写的get方法时,也会调用到该方法, 
    //该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部, 
    //accessOrder为true时,get方法会调用recordAccess方法 
    //put方法在覆盖key-value对时也会调用recordAccess方法 
    //它们导致Entry最近使用,因此将其移到双向链表的末尾 
    void recordAccess(HashMap<K,V> m) { 
      LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; 
      //如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部, 
      //如果是按照插入的先后顺序排序,则不做任何事情。 
      if (lm.accessOrder) { 
        lm.modCount++; 
        //移除当前访问的Entry 
        remove(); 
        //将当前访问的Entry插入到链表的尾部 
        addBefore(lm.header); 
      } 
    } 
    void recordRemoval(HashMap<K,V> m) { 
      remove(); 
    } 
  } 
  //迭代器 
  private abstract class LinkedHashIterator<T> implements Iterator<T> { 
  Entry<K,V> nextEntry  = header.after; 
  Entry<K,V> lastReturned = null; 
  /** 
   * The modCount value that the iterator believes that the backing 
   * List should have. If this expectation is violated, the iterator 
   * has detected concurrent modification. 
   */ 
  int expectedModCount = modCount; 
  public boolean hasNext() { 
      return nextEntry != header; 
  } 
  public void remove() { 
    if (lastReturned == null) 
    throw new IllegalStateException(); 
    if (modCount != expectedModCount) 
    throw new ConcurrentModificationException(); 
      LinkedHashMap.this.remove(lastReturned.key); 
      lastReturned = null; 
      expectedModCount = modCount; 
  } 
  //从head的下一个节点开始迭代 
  Entry<K,V> nextEntry() { 
    if (modCount != expectedModCount) 
    throw new ConcurrentModificationException(); 
      if (nextEntry == header) 
        throw new NoSuchElementException(); 
      Entry<K,V> e = lastReturned = nextEntry; 
      nextEntry = e.after; 
      return e; 
  } 
  } 
  //key迭代器 
  private class KeyIterator extends LinkedHashIterator<K> { 
  public K next() { return nextEntry().getKey(); } 
  } 
  //value迭代器 
  private class ValueIterator extends LinkedHashIterator<V> { 
  public V next() { return nextEntry().value; } 
  } 
  //Entry迭代器 
  private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> { 
  public Map.Entry<K,V> next() { return nextEntry(); } 
  } 
  // These Overrides alter the behavior of superclass view iterator() methods 
  Iterator<K> newKeyIterator()  { return new KeyIterator();  } 
  Iterator<V> newValueIterator() { return new ValueIterator(); } 
  Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); } 
  //覆写HashMap中的addEntry方法,LinkedHashmap并没有覆写HashMap中的put方法, 
  //而是覆写了put方法所调用的addEntry方法和recordAccess方法, 
  //put方法在插入的key已存在的情况下,会调用recordAccess方法, 
  //在插入的key不存在的情况下,要调用addEntry插入新的Entry 
  void addEntry(int hash, K key, V value, int bucketIndex) { 
    //创建新的Entry,并插入到LinkedHashMap中 
    createEntry(hash, key, value, bucketIndex); 
    //双向链表的第一个有效节点(header后的那个节点)为近期最少使用的节点 
    Entry<K,V> eldest = header.after; 
    //如果有必要,则删除掉该近期最少使用的节点, 
    //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。 
    if (removeEldestEntry(eldest)) { 
      removeEntryForKey(eldest.key); 
    } else { 
      //扩容到原来的2倍 
      if (size >= threshold) 
        resize(2 * table.length); 
    } 
  } 
  void createEntry(int hash, K key, V value, int bucketIndex) { 
    //创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同 
    HashMap.Entry<K,V> old = table[bucketIndex]; 
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old); 
    table[bucketIndex] = e; 
    //每次插入Entry时,都将其移到双向链表的尾部, 
    //这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素, 
    //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现 
    e.addBefore(header); 
    size++; 
  } 
  //该方法是用来被覆写的,一般如果用LinkedHashmap实现LRU算法,就要覆写该方法, 
  //比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中put 
  //Entry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。 
  protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { 
    return false; 
  } 
}
登入後複製

總結

關於LinkedHashMap的源碼,給出以下幾點比較重要的總結:

1、從源碼可以看出,LinkedHashMap中加入了一個head頭結點,將所有插入到該LinkedHashMap中的Entry按照插入的先後順序依序加入以head為頭結點的雙向循環鍊錶的尾部。

1、實際上就是HashMap和LinkedList兩個集合類別的儲存結構的結合。在LinkedHashMapMap中,所有put進來的Entry都保存在哈希表中,但它又額外定義了一個以head為頭結點的空的雙向循環鍊錶,每次put進來Entry,除了將其保存到對哈希表中對應的位置上外,還要將其插入到雙向循環鍊錶的尾部。

2、LinkedHashMap由於繼承自HashMap,因此它具有HashMap的所有特性,同樣允許key和value為null。

3、注意源碼中的accessOrder標誌位,當它false時,表示雙向鍊錶中的元素按照Entry插入LinkedHashMap到中的先後順序排序,即每次put到LinkedHashMap中的Entry都放在雙向鍊錶的尾部,這樣遍歷雙向鍊錶時,Entry的輸出順序便和插入的順序一致,這也是預設的雙向鍊錶的存儲順序;當它為true時,表示雙向鍊錶中的元素按照訪問的先後順序排列,可以看到,雖然Entry插入鍊錶的順序依然是按照其put到LinkedHashMap中的順序,但put和get方法均有調用recordAccess方法(put方法在key相同,覆蓋原有的Entry的情況下呼叫recordAccess方法),該方法判斷accessOrder是否為true,如果是,則將當前訪問的Entry(put進來的Entry或get出來的Entry)移到雙向鍊錶的尾部(key不相同時,put新Entry時,會呼叫addEntry,它會呼叫creatEntry,該方法同樣將新插入的元素放入到雙向鍊錶的尾部,既符合插入的先後順序,又符合訪問的先後順序,因為這時該Entry也被訪問了),否則,什麼都不做。

4、注意建構方法,前四個建構方法都將accessOrder設為false,說明預設是按照插入順序排序的,而第五個建構方法可以自訂傳入的accessOrder的值,因此可以指定雙向循環鍊錶中元素的排序規則,一般要用LinkedHashMap實作LRU演算法,就要用該構造方法,將accessOrder置為true。

5、LinkedHashMap並沒有覆寫HashMap中的put方法,而是覆寫了put方法中呼叫的addEntry方法和recordAccess方法,我們回過頭來再看下HashMap的put方法:


// 将“key-value”添加到HashMap中   
public V put(K key, V value) {   
  // 若“key为null”,则将该键值对添加到table[0]中。   
  if (key == null)   
    return putForNullKey(value);   
  // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。   
  int hash = hash(key.hashCode());   
  int i = indexFor(hash, table.length);   
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
    Object k;   
    // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!   
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
      V oldValue = e.value;   
      e.value = value;   
      e.recordAccess(this);   
      return oldValue;   
    }   
  }   
  // 若“该key”对应的键值对不存在,则将“key-value”添加到table中   
  modCount++;  
  //将key-value添加到table[i]处  
  addEntry(hash, key, value, i);   
  return null;   
}
登入後複製

當要put進來的Entry的key在雜湊表中已經在存在時,會呼叫recordAccess方法,當該key不存在時,則會呼叫addEntry方法將新的Entry插入到對應槽的單鍊錶的頭部。

我們先來看recordAccess方法:


#
//覆写HashMap中的recordAccess方法(HashMap中该方法为空), 
//当调用父类的put方法,在发现插入的key已经存在时,会调用该方法, 
//调用LinkedHashmap覆写的get方法时,也会调用到该方法, 
//该方法提供了LRU算法的实现,它将最近使用的Entry放到双向循环链表的尾部, 
//accessOrder为true时,get方法会调用recordAccess方法 
//put方法在覆盖key-value对时也会调用recordAccess方法 
//它们导致Entry最近使用,因此将其移到双向链表的末尾 
   void recordAccess(HashMap<K,V> m) { 
     LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; 
  //如果链表中元素按照访问顺序排序,则将当前访问的Entry移到双向循环链表的尾部, 
  //如果是按照插入的先后顺序排序,则不做任何事情。 
     if (lm.accessOrder) { 
       lm.modCount++; 
    //移除当前访问的Entry 
       remove(); 
    //将当前访问的Entry插入到链表的尾部 
       addBefore(lm.header); 
     } 
   }
登入後複製

該方法會判斷accessOrder是否為true,如果為true ,它會將目前存取的Entry(這裡指put進來的Entry)移到雙向循環鍊錶的尾部,從而實現雙向鍊錶中的元素按照訪問順序來排序(最近訪問的Entry放到鍊錶的最後,這樣多次下來,前面就是最近沒有被存取的元素,在實作、LRU演算法時,當雙向鍊錶中的節點數達到最大值時,將前面的元素刪去即可,因為前面的元素是最近最少使用的) ,否則什麼都不做。

再來看addEntry方法:


#
//覆写HashMap中的addEntry方法,LinkedHashmap并没有覆写HashMap中的put方法, 
//而是覆写了put方法所调用的addEntry方法和recordAccess方法, 
//put方法在插入的key已存在的情况下,会调用recordAccess方法, 
//在插入的key不存在的情况下,要调用addEntry插入新的Entry 
  void addEntry(int hash, K key, V value, int bucketIndex) { 
  //创建新的Entry,并插入到LinkedHashMap中 
    createEntry(hash, key, value, bucketIndex); 
    //双向链表的第一个有效节点(header后的那个节点)为近期最少使用的节点 
    Entry<K,V> eldest = header.after; 
  //如果有必要,则删除掉该近期最少使用的节点, 
  //这要看对removeEldestEntry的覆写,由于默认为false,因此默认是不做任何处理的。 
    if (removeEldestEntry(eldest)) { 
      removeEntryForKey(eldest.key); 
    } else { 
    //扩容到原来的2倍 
      if (size >= threshold) 
        resize(2 * table.length); 
    } 
  } 
  void createEntry(int hash, K key, V value, int bucketIndex) { 
  //创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同 
    HashMap.Entry<K,V> old = table[bucketIndex]; 
  Entry<K,V> e = new Entry<K,V>(hash, key, value, old); 
    table[bucketIndex] = e; 
  //每次插入Entry时,都将其移到双向链表的尾部, 
  //这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素, 
  //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现 
    e.addBefore(header); 
    size++; 
  }
登入後複製

同樣是將新的Entry插入到table中對應槽所對應單鍊錶的頭結點中,但可以看出,在createEntry中,同樣把新put進來的Entry插入到了雙向鍊錶的尾部,從插入順序的層面來說,新的Entry插入到雙向鍊錶的尾部,可以實現按照插入的先後順序來迭代Entry,而從訪問順序的層面來說,新put進來的Entry又是最近訪問的Entry,也應該將其放在雙向鍊錶的尾部。

上面還有個removeEldestEntry方法,該方法如下:


#
 //该方法是用来被覆写的,一般如果用LinkedHashmap实现LRU算法,就要覆写该方法, 
  //比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中put 
  //Entry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)。 
  protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { 
    return false; 
  } 
}
登入後複製

该方法默认返回false,我们一般在用LinkedHashMap实现LRU算法时,要覆写该方法,一般的实现是,当设定的内存(这里指节点个数)达到最大值时,返回true,这样put新的Entry(该Entry的key在哈希表中没有已经存在)时,就会调用removeEntryForKey方法,将最近最少使用的节点删除(head后面的那个节点,实际上是最近没有使用)。

6、LinkedHashMap覆写了HashMap的get方法:


//覆写HashMap中的get方法,通过getEntry方法获取Entry对象。 
//注意这里的recordAccess方法, 
//如果链表中元素的排序规则是按照插入的先后顺序排序的话,该方法什么也不做, 
//如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。 
  public V get(Object key) { 
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null) 
      return null; 
    e.recordAccess(this); 
    return e.value; 
  }
登入後複製

先取得Entry,如果不为null,一样调用recordAccess方法,上面已经说得很清楚,这里不在多解释了。

7、最后说说LinkedHashMap是如何实现LRU的。

首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。

结束语

以上是Java集合框架之LinkedHashMap源碼分析詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java 中的完美數 Java 中的完美數 Aug 30, 2024 pm 04:28 PM

Java 完美數指南。這裡我們討論定義,如何在 Java 中檢查完美數?

Java 中的隨機數產生器 Java 中的隨機數產生器 Aug 30, 2024 pm 04:27 PM

Java 隨機數產生器指南。在這裡,我們透過範例討論 Java 中的函數,並透過範例討論兩個不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。這裡我們透過範例討論簡介、如何使用 weka java、平台類型和優點。

Java 中的史密斯數 Java 中的史密斯數 Aug 30, 2024 pm 04:28 PM

Java 史密斯數指南。這裡我們討論定義,如何在Java中檢查史密斯號?帶有程式碼實現的範例。

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

Java 中的時間戳至今 Java 中的時間戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的時間戳記到日期指南。這裡我們也結合範例討論了介紹以及如何在java中將時間戳記轉換為日期。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

See all articles