?面試官?:
“HashMap 是 Java 中最常用的數據結構之一,你能說說它的底層實現嗎?比如哈希沖突是怎么解決的?”
?你?(結合源碼與優化場景):
“好的,HashMap 底層是數組+鏈表/紅黑樹的結構。比如我們項目里的用戶標簽系統,用 HashMap 存儲用戶ID和標簽數據,當兩個用戶的哈希值沖突時,會以鏈表形式掛在同一個數組位置。
但鏈表過長會影響查詢效率,所以在 JDK8 之后,當鏈表長度超過8且數組長度≥64時,鏈表會轉成紅黑樹,這樣即使有10萬條數據,查詢時間也能從O(n)降到O(log n)。我們之前做性能測試時,插入10萬條數據,紅黑樹比鏈表快了近10倍。”
?面試官追問?:
“你提到紅黑樹轉換,為什么是鏈表長度≥8且數組長度≥64才轉換?為什么不直接用紅黑樹?”
?你?(分析設計權衡):
“這是空間和時間的權衡。紅黑樹雖然查詢快,但每個節點需要額外存儲父節點、顏色標記等字段,內存是鏈表的2倍。如果數組長度小(比如16),哈希沖突可能只是暫時的,擴容后沖突會減少,此時用鏈表更省內存。
比如我們有個配置中心的功能,初始容量設得小,大部分情況鏈表長度不會超過4,用紅黑樹反而浪費內存。所以HashMap的設計者通過統計發現,哈希沖突導致鏈表長度≥8的概率極低(約千萬分之一),這時才值得用紅黑樹換時間。”
?面試官深入?:
“HashMap 的擴容機制是怎樣的?為什么每次擴容是2倍?”
?你?(結合位運算優化):
“擴容時,數組會翻倍到原來的2倍(比如16→32),這樣計算新索引可以直接用高位掩碼。比如舊容量是16(二進制10000),擴容后是32(100000),元素的新索引要么在原位置,要么在原位置+舊容量
。
這樣做的好處是避免重新計算哈希,只需判斷高位是否為1。比如我們有個數據遷移工具,HashMap擴容時遷移數據的時間減少了30%,因為位運算比重新哈希快得多。”
?面試官挑戰?:
“在多線程環境下,HashMap 可能導致死循環,你能解釋下原因嗎?”
?你?(結合JDK7源碼缺陷):
“在JDK7中,HashMap擴容時采用頭插法轉移鏈表。假設線程A和線程B同時擴容,A剛將節點1→2→3反轉為3→2→1,此時B開始操作,可能把1→3→2,形成環形鏈表。后續查詢時遍歷鏈表會進入死循環。
我們線上就遇到過這個問題!當時用HashMap緩存實時日志,高并發下CPU飆到100%,用jstack發現線程卡在get()
方法里。后來換成ConcurrentHashMap,問題解決。”
?面試官追問?:
“ConcurrentHashMap 在JDK7和JDK8的實現有什么區別?為什么JDK8要改成CAS+synchronized?”
?你?(對比設計演進):
“JDK7的ConcurrentHashMap用分段鎖(Segment),默認16個段,相當于16把鎖。比如我們有個風控系統,用ConcurrentHashMap計數,16個段的吞吐量上限明顯,無法充分利用多核CPU。
JDK8改成了CAS+synchronized
鎖單個桶頭節點。比如插入數據時,先用CAS嘗試無鎖更新,失敗后再鎖住頭節點。這樣鎖粒度更細,并發度更高。我們測試過,同樣的16線程寫操作,JDK8的吞吐量是JDK7的2倍。”
?面試官:??
“你提到HashMap的擴容是翻倍,比如16→32,那新位置的計算是怎么優化的?為什么用2的冪次方作為容量?”
?你(結合位運算+源碼):??
“HashMap的容量必須是2的冪次方,核心原因是為了將取模運算轉化為位運算,提升性能。比如哈希值h,數組長度n=16,計算索引時用 h & (n-1)
代替 h % n
。
舉個具體例子:假設h=25,n=16,25%16=9,而二進制25是11001,n-1=15是01111,按位與結果也是01001(即9)。這種位運算比取模快得多,尤其在大量數據插入時。
擴容時,新容量是舊容量的2倍,這樣新索引只有兩種可能:原位置或原位置+舊容量。比如舊容量16時,h=25的索引是9;擴容到32后,n-1=31(11111),此時計算h=25 & 31=25,而25在原索引9的基礎上,實際上是9+16=25。這種設計避免了重新計算哈希,只需判斷哈希值的最高位是0還是1。”
?面試官(追問哈希擾動函數):??
“你提到哈希值的計算,HashMap的hash()方法里為什么要做異或位移?”
?你(結合哈希碰撞優化):??
“HashMap的hash()方法并不是直接使用Object的hashCode,而是對hashCode做了一次擾動:
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
比如一個hashCode是 0x12345678,右移16位得到0x00001234,異或后變成0x1234444C。這樣做是為了讓高位參與運算,減少哈希沖突。
舉個實際例子:假設數組長度是16,大部分鍵的hashCode高位不同但低位相同(比如0x0000 0001和0xFFFF 0001),如果不做擾動,這兩個鍵會被映射到同一個桶。但擾動后,高位信息被混合到低位,分布更均勻。”
?面試官(轉向ConcurrentHashMap):??
“ConcurrentHashMap在JDK8中為什么要放棄分段鎖?CAS具體用在哪些地方?”
?你(對比設計演進+源碼細節):??
“JDK7的ConcurrentHashMap用Segment分段鎖,默認16個Segment,相當于16個獨立的HashMap。這種設計的問題是,并發度被Segment數量限制,比如16個Segment最多支持16個線程并發寫。
JDK8改成了對每個桶的頭節點加鎖(synchronized),同時用CAS實現無鎖化初始化。比如插入元素時:
- ?初始化桶數組?:用CAS設置數組的引用,避免多個線程重復初始化。
- ?插入空桶?:如果桶是空的,用CAS將新節點設置為頭節點(避免鎖)。
- ?更新size?:用CounterCell數組分散線程競爭,避免單一AtomicLong的瓶頸。
我們做過壓測,16線程并發寫入時,JDK8的ConcurrentHashMap吞吐量是JDK7的3倍,因為鎖粒度從段級別細化到桶級別。”
?面試官(追問CAS的ABA問題):??
“ConcurrentHashMap的CAS操作會遇到ABA問題嗎?怎么解決的?”
?你(結合內存模型+實戰場景):??
“ABA問題通常發生在‘先讀后寫’的場景,比如一個線程讀到值是A,準備改成C,但中間另一個線程把A→B→A。但ConcurrentHashMap的CAS操作大多是針對引用(比如桶頭節點),而引用地址不會重復復用。
舉個具體例子:線程1準備將頭節點從A改為C,此時線程2已經移除了A并重新插入了一個新節點(地址不同)。即使鍵值相同,新節點的對象地址也不同,CAS會失敗,線程1需要重試。所以實際上,ABA問題在這里不會造成數據錯亂。”
?面試官(擴展知識點):??
“除了ConcurrentHashMap,還有其他線程安全的Map結構嗎?比如CopyOnWriteArrayMap?”
?你(對比+選型建議):??
“Android中的CopyOnWriteArrayMap通過復制整個數組實現線程安全,適合讀多寫極少的場景(比如全局配置)。但它每次寫操作都要復制數組,性能差,大數據量下慎用。
如果是高并發寫場景,ConcurrentHashMap仍然是首選。但要注意,ConcurrentHashMap的迭代器是弱一致性的(遍歷時可能讀到其他線程的修改),而Hashtable的迭代器是強一致性的,但性能差。
比如我們有個實時日志系統,用ConcurrentHashMap緩存日志條目,每秒上萬次put操作,配合異步線程批量導出數據,這時候弱一致性迭代器反而能避免阻塞主線程。”
?面試官(綜合實戰題):??
“現在有一個需求:統計一篇文章中每個單詞的出現次數,用多線程處理,你會怎么設計?”
?你(結合ConcurrentHashMap特性):??
“我會將文章拆分成多個段落,每個線程處理一段。所有線程共享一個ConcurrentHashMap,鍵是單詞,值是AtomicInteger。
核心代碼如下:
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();// 每個線程處理一段文本
void processText(String text) {String[] words = text.split(" ");for (String word : words) {// 如果單詞不存在,原子性地初始化一個AtomicIntegermap.computeIfAbsent(word, k -> new AtomicInteger()).incrementAndGet();}
}
這樣做的好處:
- ?computeIfAbsent()是原子方法?:避免多個線程重復創建AtomicInteger。
- ?AtomicInteger自增無鎖競爭?:每個單詞的計數器獨立,線程間不會互相阻塞。
我們做過類似的關鍵詞統計功能,8線程處理10萬單詞,ConcurrentHashMap的耗時比Hashtable減少了85%。”
?面試官(陷阱題):??
“ConcurrentHashMap的size()方法返回的值一定準確嗎?為什么?”
?你(深入CounterCell設計):??
“不一定準確!ConcurrentHashMap的size()實現用了分片計數。每個線程修改size時,先嘗試用CAS更新baseCount,失敗后會將計數分配到CounterCell數組。最終size()的結果是baseCount加上所有CounterCell的值。
但高并發下,可能有線程正在更新CounterCell,導致size()讀到的中間結果不準確。比如我們有個監控系統,用ConcurrentHashMap緩存設備狀態,size()顯示1024,但實際可能是1023或1025。如果需要精確統計,可以用mappingCount()方法返回long類型,或者改用AtomicLong累加。”
面試官(開場切入):??
“我看你簡歷里提到熟悉Android中的數據結構優化,能說說HashMap在Java中的實現原理嗎?比如它怎么解決哈希沖突的?”
?你(自然引出項目經驗):??
“好的,HashMap底層其實是個數組,每個數組位置叫桶(bucket)。比如我們項目里用HashMap緩存用戶信息,用戶ID作為鍵。當兩個不同ID算出的哈希值相同時,就會在同一個桶里用鏈表串起來。
但鏈表太長的話,比如用戶量暴漲到幾十萬,查詢會變慢。后來發現JDK8做了優化——當鏈表長度超過8,并且整個數組長度達到64時,鏈表會自動轉成紅黑樹。您可能猜到了,紅黑樹的查找是O(log n),比鏈表的O(n)快得多。我們壓測時插入了10萬條數據,紅黑樹版本的查詢速度比鏈表快了近10倍,這個優化對高并發場景特別關鍵。”
?面試官(追問設計細節):??
“有意思,但為什么非要等到鏈表長度到8才轉紅黑樹?為什么不一開始就用紅黑樹?”
?你(結合數據談權衡):??
“這其實是空間換時間的經典取舍。紅黑樹每個節點要存父節點、左右子節點,還有顏色標記,內存占用是鏈表的2倍。如果數組本身很小,比如初始容量16,這時候擴容可能比轉樹更劃算。
舉個實際例子:我們團隊有個配置中心的模塊,初期數據量小,大部分桶里鏈表長度不超過3。如果這時候強行用紅黑樹,內存會多消耗一倍,反而得不償失。后來看源碼注釋才知道,HashMap設計者統計過哈希碰撞的概率,鏈表長度到8的概率不到千萬分之一。換句話說,這個閾值是在極端情況下才觸發的安全網。”
?面試官(場景化問題):??
“假設現在有個需求:實現一個購物車的商品數量統計,鍵是商品ID(長整型),值是該商品的數量。用HashMap還是SparseArray更合適?為什么?”
?你(對比+實戰舉例):??
“如果是在Android環境下,我會優先考慮SparseArray。因為商品ID通常是整數(比如從服務端返回的id=10001),用SparseArray能避免自動裝箱產生的Integer對象,內存更緊湊。之前我們做性能優化時,用Android Profiler對比過,同樣的1萬條數據,SparseArray比HashMap省了約40%的內存。
不過要注意,SparseArray的查找是二分法,時間復雜度O(log n),適合數據量小的場景。比如購物車一般商品數量在幾百個以內,這時候O(log n)和HashMap的O(1)差異不大。但如果商品數可能破萬,或者需要頻繁刪除/插入,就得回到HashMap或者ArrayMap的懷抱了。”
?面試官(陷阱題):??
“你剛才提到SparseArray用二分查找,那它的鍵數組必須是有序的嗎?如果我插入的鍵是亂序的會發生什么?”
?你(暴露原理+避坑):??
“沒錯,SparseArray的鍵數組必須保持有序!比如我們團隊有個新人曾經在插入時沒注意順序,結果發現用get()方法經常返回null。后來排查發現,亂序插入會導致二分查找錯位。
舉個例子:假設已經存了鍵為10和20,這時候插入一個鍵為15的,SparseArray會把它放在10和20之間,維持數組有序。但如果直接調用put(5, value),它會通過二分查找找到該插入的位置,然后把后面的元素整體右移——這個過程有點像ArrayList的插入,所以頻繁插入中間位置的話,性能會比HashMap差很多。”
?面試官(深入原理):??
“那你說說,SparseArray的刪除操作是怎么實現的?直接縮容數組嗎?”
?你(結合源碼邏輯):??
“其實SparseArray用了延遲刪除的策略。比如刪除一個鍵時,它并不會立刻縮容數組,而是把對應位置的值標記為DELETE(一個靜態Object對象)。這樣做的好處是,下次插入新數據時可以直接復用這個‘空洞’,避免頻繁擴容縮容。
不過這也帶來了隱患——如果長期不插入新數據,這些DELETE標記會一直占用內存。所以SparseArray提供了gc()方法,它會遍歷數組,清理所有DELETE標記并重新排列元素。我們項目里的做法是,在數據批量刪除后(比如清空購物車),手動調用一次gc(),避免內存泄漏。”
?面試官(開放問題):??
“如果讓你設計一個圖片緩存,支持LRU淘汰策略,你會用LinkedHashMap還是自己實現?”
?你(方案對比+決策):??
“如果是快速實現原型,可以直接用LinkedHashMap的accessOrder模式,重寫removeEldestEntry()方法。比如這樣寫(假裝手寫代碼):
new LinkedHashMap<K, V>(initialSize, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > MAX_CACHE_SIZE;}
};
但實際項目中,我們遇到過LinkedHashMap在并發場景下的線程安全問題。后來改用AndroidX的LruCache,它內部用LinkedHashMap加同步鎖,更穩妥。如果要追求更高并發,比如像Glide那樣的圖片加載庫,就得考慮用讀寫鎖或者分段鎖了,這時候可能需要自己封裝結構。”
?面試官(收尾驗證):??
“最后一個問題:ArrayList和LinkedList在中間插入元素時,時間復雜度都是O(n),那它們的實際性能有差異嗎?為什么?”
?你(微觀分析+實戰數據):??
“雖然理論復雜度相同,但實際差異很大!比如在中間插入一個元素,ArrayList需要把后面的元素逐個往后拷貝,而LinkedList需要遍歷到那個位置再修改指針。
我們做過一個極端測試:在長度為10萬的ArrayList和LinkedList的中間位置插入1000個元素。ArrayList耗時約120ms,而LinkedList竟然用了2000ms。原因在于,LinkedList的遍歷需要逐個訪問節點,CPU緩存不友好,而ArrayList的內存是連續的,System.arraycopy()底層用內存拷貝,速度極快。所以結論是:哪怕是O(n)的操作,也要看常數項的大小,不能只看復雜度。”
?基礎知識擴展:
SparseArray ??
?1. 設計背景與核心優勢?
?為什么需要 SparseArray???
在 Android 開發中,頻繁使用 HashMap<Integer, Object>
會導致 ?內存浪費? 和 ?性能下降,因為 Integer
鍵會觸發 ?自動裝箱?(創建大量小對象),而 HashMap 的鏈表/紅黑樹結構也會帶來額外內存開銷。
?SparseArray 的優化點?:
- ?避免自動裝箱?:直接使用
int
作為鍵,無需Integer
對象。 - ?內存緊湊?:基于兩個平行數組(
int[]
鍵數組,Object[]
值數組),無鏈表結構。 - ?二分查找優化?:鍵數組有序,查找時通過二分法實現(時間復雜度
O(log n)
)。
// 對比示例:內存占用差異
HashMap<Integer, String> map = new HashMap<>(); // 每個鍵值對占用約 32 字節
SparseArray<String> sparseArray = new SparseArray<>(); // 每個鍵值對占用約 16 字節
?2. 內部實現原理?
?數據結構?:
- ?鍵數組(
int[] mKeys
)??:有序存儲所有鍵。 - ?值數組(
Object[] mValues
)??:與鍵數組一一對應,存儲值或標記刪除(DELETED
)。
?核心操作?:
-
?插入(
put(int key, E value)
)??:- 通過二分查找確定插入位置。
- 若位置已有數據且標記為
DELETED
,直接覆蓋。 - 若數組已滿,觸發擴容(當前容量 ≤4 則擴容到8,否則翻倍)。
-
?查找(
get(int key)
)??:- 二分查找鍵數組,找到對應索引后返回值。
- 未找到時返回默認值(可指定)。
-
?刪除(
delete(int key)
)??:- 不立即縮容,僅將值標記為
DELETED
,后續插入可復用位置。 - 調用
gc()
方法時,清除所有DELETED
標記并縮容。
- 不立即縮容,僅將值標記為
// 源碼關鍵邏輯(簡化版)
public void put(int key, E value) {int i = binarySearch(mKeys, mSize, key); // 二分查找if (i >= 0) {mValues[i] = value; // 已存在則覆蓋} else {i = ~i; // 計算插入位置if (i < mSize && mValues[i] == DELETED) {mKeys[i] = key;mValues[i] = value;return;}// 擴容并插入新元素...}
}
?3. 性能對比與適用場景?
?**對比 HashMap<Integer, Object>
**?:
特性 | SparseArray | HashMap |
---|---|---|
?鍵類型? | int | Integer |
?內存占用? | 低(無對象頭、鏈表) | 高(自動裝箱+鏈表結構) |
?查找性能? | O(log n) | O(1) |
?插入/刪除性能? | O(n)(需移動元素) | O(1)(鏈表操作) |
?適用數據量? | 小規模(百級以內) | 中大規模 |
?使用場景?:
- ?鍵為
int
且數據量小?:如R.id.xxx
視圖緩存、枚舉配置。 - ?內存敏感場景?:如 RecyclerView 的 ViewHolder 緩存。
- ?低頻修改、高頻查詢?:如全局配置參數。
// 實戰案例:優化 RecyclerView 的 ViewHolder 緩存
private SparseArray<ViewHolder> mCachedViews = new SparseArray<>();@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {ViewHolder holder = mCachedViews.get(viewType);if (holder == null) {holder = new ViewHolder(...);mCachedViews.put(viewType, holder);}return holder;
}
?4. 常見坑點與替代方案?
?坑點?:
- ?鍵必須有序?:插入無序鍵會破壞二分查找邏輯,需手動排序。
- ?插入性能差?:大規模數據插入時,移動數組元素開銷大。
- ?刪除不縮容?:頻繁刪除需手動調用
gc()
避免內存泄漏。
?替代方案?:
- ?**
ArrayMap
**?:支持任意對象鍵,內存效率優于 HashMap。 - ?**
LongSparseArray
**?:鍵為long
類型的變種。 - ?**
androidx.collection.CircularArray
**?:循環數組,適合隊列場景。
?5. 面試高頻問題?
?Q1:SparseArray 和 HashMap 的主要區別是什么???
- ?鍵類型?:SparseArray 用
int
避免裝箱;HashMap 用Object
。 - ?內存占用?:SparseArray 更緊湊,適合小數據量。
- ?查找性能?:SparseArray 是 O(log n),HashMap 是 O(1)。
?Q2:什么情況下該用 SparseArray???
- 鍵為
int
,數據量小(百級以內),且需要低內存開銷時。
?Q3:SparseArray 的刪除邏輯有什么優化???
- 延遲刪除:標記為
DELETED
,插入時可復用位置;手動gc()
觸發縮容。
?Q4:SparseArray 的查找性能為什么是 O(log n)???
- 基于有序數組的二分查找,每次比較后范圍減半。
?Array、ArrayList 與 LinkedList 的區別詳解?
?1. 基本概念?
-
?Array(數組)??:
- ?固定長度,初始化時需指定大小。
- ?內存連續,支持快速隨機訪問(通過索引直接定位,時間復雜度
O(1)
)。 - ?示例?:
int[] arr = new int[10];
-
?ArrayList(動態數組)??:
- ?基于 Array 實現,可自動擴容。
- ?默認初始容量為 10,擴容時新容量為舊容量的 1.5 倍。
- ?示例?:
List<String> list = new ArrayList<>();
-
?LinkedList(雙向鏈表)??:
- ?通過節點指針連接元素,每個節點存儲前驅和后繼的引用。
- ?插入/刪除高效,但隨機訪問慢(需遍歷鏈表)。
- ?示例?:
List<Integer> linkedList = new LinkedList<>();
?2. 核心區別對比?
?特性? | ?Array? | ?ArrayList? | ?LinkedList? |
---|---|---|---|
?數據結構? | 固定長度數組 | 動態數組(自動擴容) | 雙向鏈表 |
?隨機訪問速度? | O(1) (最快) | O(1) | O(n) (最慢) |
?插入/刪除效率? | O(n) (需移動元素) | 尾部插入:O(1) (均攤);中間/頭部插入: O(n) | 頭尾插入:O(1) ;中間插入: O(n) (需遍歷) |
?內存占用? | 最低(僅數據) | 中等(動態數組 + 擴容開銷) | 最高(每個節點額外存儲指針) |
?適用場景? | 已知固定長度的數據 | 高頻隨機訪問,數據量變化不大 | 頻繁插入/刪除,無需隨機訪問 |
?3. ArrayList 的擴容機制與計算?
?擴容規則?:
- ?初始容量?:默認 10。
- ?擴容公式?:新容量 = 舊容量 + 舊容量 >> 1(即 1.5 倍)。
- ?觸發條件?:當添加元素時,當前元素數超過容量。
?擴容示例?:
假設依次添加 100 個元素到 ArrayList
:
- 初始容量 10,添加第 11 個元素時,擴容到 15。
- 添加第 16 個元素時,擴容到 22。
- 后續擴容依次為 33 → 49 → 73 → 109。
?總擴容次數?:5 次(容量從 10 → 15 → 22 → 33 → 49 → 73 → 109)。
?元素拷貝次數?:每次擴容需將舊數組元素復制到新數組。
- 10(初始) → 15:拷貝 10 次
- 15 → 22:拷貝 15 次
- 22 → 33:拷貝 22 次
- 33 → 49:拷貝 33 次
- 49 → 73:拷貝 49 次
- 73 → 109:拷貝 73 次
?總拷貝次數?:10 + 15 + 22 + 33 + 49 + 73 = ?202 次。
?性能優化?:
- ?預分配容量?:若已知數據量較大,初始化時指定容量,避免多次擴容。
List<Integer> list = new ArrayList<>(1000); // 直接分配 1000 容量
?4. 操作時間復雜度對比?
?操作? | ?Array? | ?ArrayList? | ?LinkedList? |
---|---|---|---|
隨機訪問(get(i) ) | O(1) | O(1) | O(n) |
尾部插入(add(e) ) | 不支持 | 均攤 O(1) | O(1) |
頭部插入(addFirst(e) ) | 不支持 | O(n) (需移動元素) | O(1) |
中間插入(add(i, e) ) | 不支持 | O(n) | O(n) (需遍歷) |
尾部刪除(removeLast() ) | 不支持 | O(1) | O(1) |
頭部刪除(removeFirst() ) | 不支持 | O(n) | O(1) |
?5. 實戰場景建議?
- ?高頻隨機訪問?:如按索引讀取數據 → ?ArrayList。
// 讀取第 100 個元素 String data = list.get(100); // O(1)
- ?頻繁頭尾插入/刪除?:如實現隊列或棧 → ?LinkedList。
// 實現棧(后進先出) linkedList.addFirst("item1"); // O(1) linkedList.removeFirst(); // O(1)
- ?內存敏感場景?:如嵌入式設備 → ?Array?(無額外內存開銷)。
int[] sensorData = new int[1000]; // 固定長度,內存緊湊
?6. 性能陷阱與規避?
- ?ArrayList 的中間插入?:
- ?問題?:在索引 0 處插入元素需移動所有元素,效率低下。
- ?優化?:若需頻繁中間操作,考慮使用 ?LinkedList? 或重新設計數據結構。
- ?LinkedList 的隨機訪問?:
- ?問題?:
get(1000)
需從頭遍歷,效率極低。 - ?優化?:改用 ?ArrayList? 或緩存常用節點。
- ?問題?: