HashSet、TreeSetソースコード分析

HashSet、TreeSetソースコード分析

HashSetとTreeSetの2つのクラスは、Mapに基づいて組み立てられます。私たちの研究の焦点は、SetがMapの既存の機能を使用して、独自の目標を達成する方法、つまり、既存の機能を革新してから、変更の詳細を学ぶ価値があるかどうかを確認します。

一:HashSet

1.1、HashSetクラスの注釈

ソースコードを見て、最初にクラスのコメントを確認する必要があります。クラスのコメントの内容を見てみましょう。次の図にいくつかのスクリーンショットを示します。
ここに写真の説明を挿入
クラスのコメントは、主に次の4つのポイントについて説明しています。

  1. 基盤となる実装はHashMapに基づいているため、反復中に挿入順序またはその他の順序で反復することは保証されません。
  2. 追加、削除、コンタニン、サイズ、およびその他のメソッドの時間のかかるパフォーマンスは、データ量の増加に伴って増加しません。これは主に、データ量に関係なく、ハッシュの競合に関係なく、HashMapの基礎となる配列データ構造に関連しています。 、時間の複雑さはO(1)*です。
  3. スレッドが安全でない場合、セキュリティが必要な場合は自分でロックするか、Collections.synchronizedSetを使用してください。
  4. 反復プロセス中にデータ構造が変更されると、すぐに失敗し、ConcurrentModificationExceptionがスローされます。

ListとMapのクラスアノテーションも以前に見たことがありますが、クラスアノテーションにはポイント2、3、4が記載されていることがわかりました。したがって、List、Map、Setの類似点について質問された場合は、2と言えます。 、3、43点。

1.2HashSetはHashMapをどのように組み合わせるのですか

クラスアノテーション1から、HashSetの実装はHashMapに基づいていることがわかりました。Javaでは、基本的なクラスに基づいて革新的な実装を実装する方法が2つあります。

  1. 基本クラスを継承し、基本クラスのメソッドをオーバーライドします。たとえば、HashMapを継承し、addメソッドをオーバーライドします。
  2. 基本クラスのメソッドを呼び出すことにより、基本クラスを結合し、基本クラスの機能を再利用します。

HashSetはHashMapを組み合わせて使用​​し、その利点は次のとおりです。

  1. 継承とは、親クラスと子クラスが同じものであり、SetとMapが元々2つのことを表現したかったため、継承は適切ではなく、Java構文の制限により、子クラスは1つの親クラスしか継承できず、その後の拡張は困難です。
  2. 組み合わせはより柔軟で、既存の基本クラスを任意に組み合わせることができ、基本クラスのメソッドに基づいてメソッドを拡張および配置でき、基本クラスのメソッド名と一致する必要なしに、メソッド名に任意の名前を付けることができます。

私たちの仕事では、同様の問題が発生した場合、私たちの原則は、可能な限り構成を使用し、継承を少なくすることです。

組み合わせは、HashMapをそれ自体のローカル変数の1つとして扱うことです。以下は、HashSetの組み合わせた実装です。

// 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT
private transient HashMap<E,Object> map;
// HashMap 中的 value
private static final Object PRESENT = new Object();

これらの2行のコードから、次の2つのことがわかります。

  1. addメソッドなどのHashSetを使用する場合、入力パラメーターは1つだけですが、結合されたMapのaddメソッドには、keyとvalueの2つの入力パラメーターがあります。Mapに対応するキーはaddの入力パラメーターであり、valueは2行目です。コード内のPRESENT、ここでのデザインは非常に巧妙です。Mapの値をデフォルト値のPRESENTに置き換えます。
  2. HashSetが共有されている場合、後続のすべての操作でロックがないため、複数のスレッドがHashSetにアクセスすると、スレッドの安全性の問題が発生します。

HashSetをHashMapに基づいて実装する場合は、最初に組み合わせ方法を選択し、次にデフォルト値を使用してマップのValue値を置き換えます。デザインは非常に巧妙で、ユーザーに優れたエクスペリエンスを提供し、使いやすくシンプルです。作業でこのアイデアから学ぶこともできます。基礎となる複雑な実装をラップすることができます。一部のデフォルトの実装は自分で食べることができるため、吐き出すインターフェイスは可能な限りシンプルで使いやすくなっています。

1.2.1、初期化

HashSetの初期化は比較的簡単で、新しいHashMapを直接使用するだけです。さらに興味深いのは、元のコレクションデータが初期化されるときに、HashMapの初期容量が計算されることです。ソースコードは次のとおりです。

// 对 HashMap 的容量进行了计算
public HashSet(Collection<? extends E> c) {
    
    
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

HashSetの他のメソッドに関しては、比較的単純です。つまり、Mapのapiのパッケージ化の一部であり、次のaddメソッドが実装されています。

public boolean add(E e) {
    
    
    // 直接使用 HashMap 的 put 方法,进行一些简单的逻辑判断
    return map.put(e, PRESENT)==null;
}

addメソッドから、組み合わせのメリットを確認できます。メソッドの入力パラメータ、名前、戻り値はすべてカスタマイズできます。継承されている場合は機能しません。

1.2.2まとめ

HashSetの特定の実装は、主に次のように参照する価値があります。通常、コードを作成するときに、それを参照できます。

  1. 組み合わせまたは継承の分析と把握。
  2. 複雑なロジックのパッケージ化を実行して、吐き出すインターフェイスをできるだけシンプルで使いやすくします。
  3. 他のapiを組み合わせるときは、組み合わせたapiについて詳しく学習し、apiをより適切に使用できるようにしてください。

2:TreeSet

TreeSetの一般的な構造はHashSetに似ています。基本的な組み合わせはTreeMapであるため、TreeMapキーの並べ替えの機能を継承します。繰り返しを行う場合、キーの並べ替え順序に従って繰り返すこともできます。主にTreeMapの再利用について説明します。 2つのアイデア:

2.1、TreeMapを再利用するというアイデア

シナリオ1:TreeSetのaddメソッド、そのソースコードを見てみましょう:

public boolean add(E e) {
    
    
    return m.put(e, PRESENT)==null;
}

ご覧のとおり、最下層はHashMapのput機能を直接使用しており、直接使用するだけです。

2.2、TreeMapを再利用する2番目のアイデア

シナリオ2:TreeSetの要素を反復する必要があります。これは、addのようになり、HashMapの既存の反復機能を直接使用します。たとえば、次のようになります。

// 模仿思路一的方式实现
public Iterator<E> descendingIterator() {
    
    
    // 直接使用 HashMap.keySet 的迭代能力
    return m.keySet().iterator();
}

これがアイデア1の実現です。TreeSetはTreeMapを組み合わせて、パッケージ化のためにTreeMapの基本機能を直接選択しますが、TreeSetの実際の実装は完全に逆です。ソースコードを見てみましょう。

// NavigableSet 接口,定义了迭代的一些规范,和一些取值的特殊方法
// TreeSet 实现了该方法,也就是说 TreeSet 本身已经定义了迭代的规范
public interface NavigableSet<E> extends SortedSet<E> {
    
    
    Iterator<E> iterator();
    E lower(E e);
}  
// m.navigableKeySet() 是 TreeMap 写了一个子类实现了 NavigableSet
// 接口,实现了 TreeSet 定义的迭代规范
public Iterator<E> iterator() {
    
    
    return m.navigableKeySet().iterator();
}

TreeMapのNavigableSetインターフェイスの実装ソースコードは次のとおりです。

// TreeMap 为了满足 Set 的功能,实现了 Set 定义的 NavigableSet 的接口
static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
    
    
    private final NavigableMap<E, ?> m;
    KeySet(NavigableMap<E,?> map) {
    
     m = map; }

    public Iterator<E> iterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).keyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
    }

    public Iterator<E> descendingIterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).descendingKeyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).descendingKeyIterator();
    }

	// 这些都是 NavigableSet 接口里面的方法
    public int size() {
    
     return m.size(); }
    public boolean isEmpty() {
    
     return m.isEmpty(); }
    public boolean contains(Object o) {
    
     return m.containsKey(o); }
    public void clear() {
    
     m.clear(); }
    public E lower(E e) {
    
     return m.lowerKey(e); }
    public E floor(E e) {
    
     return m.floorKey(e); }
    public E ceiling(E e) {
    
     return m.ceilingKey(e); }
    public E higher(E e) {
    
     return m.higherKey(e); }
    public E first() {
    
     return m.firstKey(); }
    public E last() {
    
     return m.lastKey(); }
    public Comparator<? super E> comparator() {
    
     return m.comparator(); }
    public E pollFirst() {
    
    
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
    public E pollLast() {
    
    
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
    public boolean remove(Object o) {
    
    
        int oldSize = size();
        m.remove(o);
        return size() != oldSize;
    }
    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                  E toElement,   boolean toInclusive) {
    
    
        return new KeySet<>(m.subMap(fromElement, fromInclusive,
                                      toElement,   toInclusive));
    }
    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
    
    
        return new KeySet<>(m.headMap(toElement, inclusive));
    }
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    
    
        return new KeySet<>(m.tailMap(fromElement, inclusive));
    }
    public SortedSet<E> subSet(E fromElement, E toElement) {
    
    
        return subSet(fromElement, true, toElement, false);
    }
    public SortedSet<E> headSet(E toElement) {
    
    
        return headSet(toElement, false);
    }
    public SortedSet<E> tailSet(E fromElement) {
    
    
        return tailSet(fromElement, true);
    }
    public NavigableSet<E> descendingSet() {
    
    
        return new KeySet<>(m.descendingMap());
    }

    public Spliterator<E> spliterator() {
    
    
        return keySpliteratorFor(m);
    }
}

TreeMapのソースコードから、TreeMapがTreeSetによって定義されたさまざまな特別なメソッドを実装していることがわかります。

このアイデアは、TreeSetがインターフェイスの仕様を定義し、TreeMapが実装を担当するというものであることがわかります。実現のアイデアとアイデアは反対です。

TreeSetとTreeMapを実装するための2つのアイデアを要約します。

  1. TreeSetは、TreeMapの一部の関数を直接使用し、それ自体を新しいapiにラップします。
  2. TreeSetは、必要なapiを定義し、インターフェイス仕様を自分で定義して、TreeMapに実装させます。

スキーム1と2の呼び出し関係は、TreeSetがTreeMapを呼び出すというものですが、関数の実現関係は完全に反対です。1つ目は、関数の定義と実現がTreeMapにあり、TreeSetは単なる呼び出しです。2番目のTreeSetはインターフェイスを定義します。その後、TreeMapに内部ロジックを実装させ、TreeSetがインターフェイス定義を担当し、TreeMapが特定の実装を担当します。この場合、インターフェイスはTreeSetによって定義されるため、実装はTreeSetが最も必要とするものである必要があります。TreeSetはラップする必要もなく、値を直接返すことができます。あなたはそれを吐き出すことができます。

これら2つの再利用のアイデアの理由について考えてみましょう。

  1. addのような単純なメソッドの場合、主にaddのメソッドは複雑なロジックなしで実装するのが比較的簡単であるため、Idea 1を直接使用します。したがって、TreeSetはそれ自体で実装するのが比較的簡単です。
  2. アイデア2は主に、反復シナリオなどの複雑なシナリオに適しています。TreeSetには複雑なシナリオがあります。たとえば、最初の値を取得できるようにするため、たとえば最後の値を取得できるようにするために、最初から反復できる必要があります。さらに、TreeMapの基本構造はより複雑です。TreeSet TreeMapの基礎となる複雑なロジックについては明確でない場合があります。現時点では、TreeSetを使用してこのような複雑なシーンロジックを実装すると、TreeSetはそれを処理できません。TreeSetにインターフェイスを定義させ、TreeMapに実装を任せることをお勧めします。TreeMapは、基礎となる複雑な構造について非常に明確です。実装は正確で簡単です。

2.3まとめ

TreeSetとTreeMapの2つの異なる再利用のアイデアは非常に重要です。これらは仕事でよく見られます。特に、dubboの汎化呼び出し、DDDでの依存関係の反転など、2番目のアイデアは、原則がTreeSetの2番目のタイプです。アイデアの再利用。

3:まとめ

結合されたHashMapクラス拡張のしきい値に関するHashSetの詳細な理解と設計は、学ぶ価値があります。TreeSetのTreeMapに対する2つの再利用のアイデア、特に2番目の再利用のアイデアは学ぶ価値があります。

おすすめ

転載: blog.csdn.net/weixin_38478780/article/details/107979931