[Java]集合和泛型

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java]集合和泛型相关的知识,希望对你有一定的参考价值。

一、集合

1. 集合体系图

  1. Set 集合体系图

  1. List 集合体系图

  1. Map 集合体系图

2. Collection 接口方法

Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的

// 只要是 Collection 接口的实现类都可以做实参
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean containsAll(Collection<?> c);

3. Iterator 迭代器

  1. Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素
  2. 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象即可以返回一个迭代器
  3. Iterator 对象仅用于遍历集合,本身并不存放数据对象
  4. 使用增强 for 循环遍历集合时,底层仍然使用的 Iterator 进行遍历
// 得到某个集合的迭代器
Iterator iterator = collection.iterator();
// 判断是否还有下一个元素
while(iterator.hasNext())
   System.out.println(iterator.next());

4. List 接口方法

  1. 实现 List 的集合类中的元素有序(加入顺序与取出顺序一致),允许存在重复
  2. 支持使用对应的索引值直接获取元素的值
// 在索引为 index 的位置插入 element,后续元素后移
void add(int index, E element);
// 指定起始索引的子集合 [fromIndex,toIndex)
List<E> subList(int fromIndex, int toIndex);

5. ArrayList

  1. ArrayList 可以添加多个空元素,底层由 Object[] 数组实现来存储数据
  2. ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全的,执行效率比 Vector 高,多线程条件下最好不使用 ArrayList
  3. ArrayList 中维护了一个 Object 类型的数组 elementData 即 transient Object[] elementData
  4. 若创建 ArrayList 对象时使用的是无参构造器,则 elementData 初始容量为 0,第 1 次添加后设置容量为 10,此后按照 elementData 容量的 1.5 倍进行扩容
  5. 若使用的是指定容量大小的构造器,则 elementData 的初始容量为指定大小,需要扩容时按照 1.5 进行扩容
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) 
   // overflow-conscious code
   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
   // minCapacity is usually close to size, so this is a win:
   elementData = Arrays.copyOf(elementData, newCapacity);

6. Vector

  1. Vector 底层存储数据时是一个对象数组即 protected Object[] elementData
  2. Vector 是线程安全的,在开发中,需要考虑线程同步安全时,考虑使用 Vector 而不是 ArrayList
  3. 创建 Vector 的对象时若使用无参构造器,则默认初始化容量为 0,当第 1 次添加元素时设置容量大小为 10,此后按 2 倍扩容;若使用了有参构造器,则初始容量为指定大小,此后按照 2 倍扩容
private void grow(int minCapacity) 
   // overflow-conscious code
   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + ((capacityIncrement > 0) capacityIncrement : oldCapacity);
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
   if (newCapacity - MAX_ARRAY_SIZE > 0)
       newCapacity = hugeCapacity(minCapacity);
   elementData = Arrays.copyOf(elementData, newCapacity);

7. LinkedList

  1. LinkedList 具有双向链表和双端队列特点,可以添加多个 null 元素,允许重复、线程不安全
  2. LinkedList 底层维护了一个双向链表,类中有两个属性 transient Node<E> first 和 transient Node<E> last 分别指向头节点和尾节点
  3. 每个节点中又维护了 prev、next、item 三个属性,所以 LinkedList 元素的增加与删除效率较高
// LinkedList 的内部类 Node
private static class Node<E> 
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) 
        this.item = element;
        this.next = next;
        this.prev = prev;
    


// 添加新节点到表尾
void linkLast(E e) 
   final Node<E> l = last;
   final Node<E> newNode = new Node<>(l, e, null);
   last = newNode;
   if (l == null)
       first = newNode;
   else
       l.next = newNode;
   size++;
   modCount++;


// 移除头节点
private E unlinkFirst(Node<E> f) 
   // assert f == first && f != null;
   final E element = f.item;
   final Node<E> next = f.next;
   f.item = null;
   f.next = null; // help GC
   first = next;
   if (next == null)
       last = null;
   else
       next.prev = null;
   size--;
   modCount++;
   return element;

8. Set 接口方法

  1. Set 中的元素无序,没有索引,因而只有两种遍历方式
  2. Set 不允许重复元素,故最多只包含一个 null

9. HashSet

  1. HashSet 的底层使用的是 HashMap,HashMap 底层是(数组 + 链表 + 红黑树)
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() 
   map = new HashMap<>();

  1. 确定元素 hash 值的算法
// 确定元素 hash 值的算法
static final int hash(Object key) 
   int h;
   // key 的 hashCode 与 key 无符号右移 16 位的值进行按位异或 ^ 运算得到元素得 hash 值
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  1. 元素存放过程:先通过 hash(Object) 方法获得本次元素的 hash 值,将 hash 值与本次哈希表大小 -1 的值进行按位与运算获得此元素在哈希表中的位置号,如果该位置上没有其它元素则直接存放;如果存在,则遍历链表判断是否已经存在相同元素,如果存在则不添加并且返回元素值,否则创建新的节点连接到链表尾(判断两个元素是否相同的条件:当前元素的 hash 值与哈希表位置号上元素的 hash 值相同且值相同(引用相同)或者要添加的元素不为空且 equals 比较相同)
  2. 扩容机制:在 Java 8 中,如果哈希表的大小 >= 64 且某条链表的长度 >= 8 则会将该条链表树化为红黑树
  3. HashMap 实现扩容的方法:第一次添加时 table 扩容到 DEFAULT_INITIAL_CAPACITY 16,加载因子为 loadFactor 0.75,临界值为 threshold 12,如果 table 已添加 threshold 个元素,则 table 按照 2 倍方式扩容到 32,临界值为 24,依次类推
static final int MIN_TREEIFY_CAPACITY = 64;
static final int TREEIFY_THRESHOLD = 8;
  1. HashMap 为键值对,所以在 HashSet 采用 HaspMap 实现存储时,value 为系统给定的 PRESENT
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

// 判断加入的元素个数是否不小于临界值,是则对哈希表进行扩容
if (++size > threshold)
   resize();
  1. HashMap 实现添加元素的方法
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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 为 HashMap 属性,Node 类型的数组,进行第一次扩容
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
   // 用本次哈希表长度减 1 与本次元素的 hash 值进行按位与运算获得元素在哈希表中的位置号
   if ((p = tab[i = (n - 1) & hash]) == null)
       // 位置号上未存储元素则直接存放
       tab[i] = newNode(hash, key, value, null);
   else 
       // 判断是否存在相同的元素,不存在则存放到此条链表的末尾
       Node<K,V> e; K k;
       // 如果当前元素的 hash 值与哈希表位置号上元素的 hash 值相同且值相同(引用相同)或者 要添加的元素不为空且 equals 比较相同,则不添加
       if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
           e = p;
       // 判断当前位置号是否是红黑树数据结构,是则按照红黑树方式添加
       else if (p instanceof TreeNode)
           e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       else 
           // 遍历此位置号上的链表元素,判断是否与当前需要加入的元素相同
           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;
               
               // 如果存在相同的元素则不添加
               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)
               e.value = value;
           afterNodeAccess(e);
           return oldValue;
       
   
   ++modCount;
   // size 加入到哈希表中的元素个数
   // 判断加入的元素个数是否不小于临界值,是则对哈希表进行扩容
   if (++size > threshold)
       resize();
   // HashMap 的空方法,留给子类实现以扩展功能	
   afterNodeInsertion(evict);
   // 返回 null 代表元素添加成功
   return null;

10. LinkedHashSet

  1. LinkedHashSet 底层使用 LinkedHashMap,底层维护了一个 数组 + 双向链表
  2. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的;不允许添加重复元素
  3. LinkedHashSet 有 head 和 tail 指针,使用使用头尾指针遍历 linkedHashSet 时取出元素的顺序与插入元素的顺序一致
  4. LinkedHashSet 在添加一个元素时,先求 hash 值,再求在哈希表中的索引号,然后判断元素是否添加
  5. LinkedHashSet 底层数组的类型是 HashMap$Node[],其中存放的元素是 LinkedHashMap$Entry 类型
/**
* Basic hash bin node, used for most entries.  (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V>
       
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V>    

11. Map

  1. Map 与 Collection 并列存在,用于保存具有映射关系的键值对:key - value
  2. Map 中的 key 和 value 可以是任何引用类型,k - v 会封装到 HashMap$Node 对象中,因为 Node 实现了 Entry 接口
  3. Map 中的 key 不允许重复,value 可以重复;key 最多有一个 null,value 可以有多个 null;当 key 相同而 value 不同时,用新的 value 替换旧的 value
  4. key - value 的值存放到 HashMap$Node 中,为单独方便遍历 key 或 value,使用 KeySet(Set 类型) 引用到 Node 中所有的 key,使用 Values (Collection 类型)引用到所有的 value;将对应的 key - value 封装到 Entry 中,再将 Entry 存放到 EntrySet 中
// EntrySet
final class EntrySet extends AbstractSet<Map.Entry<K,V>>
// KeySet
final class KeySet extends AbstractSet<K>
//  Values
final class Values extends AbstractCollection<V>

  1. EntrySet 使用示例
/* EntrySet 使用示例 */    
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("李春雄", 21);

// 通过 Entry 遍历
Set<Map.Entry<Object, Object>> entries = hashMap.entrySet();
for (Map.Entry<Object, Object> entry : entries) 
   System.out.println(entry);
   System.out.println("key = " + entry.getKey());
   System.out.println("value = " + entry.getValue());


// 通过 KeySet 遍历所有的 key
Set<Object> keySet = hashMap.keySet();
for (Object object : keySet) 
   System.out.println("key = " + object);


// 通过 Values 遍历所有的 value
Collection<Object> values = hashMap.values();
for (Object value : values) 
   System.out.println(value);

  1. Map 的遍历
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("李春雄", 21);

// 使用增强 for 的地方也可以使用迭代器

// 1. 通过 EntrySet 遍历
Set<Map.Entry<Object, Object>> entrySet = hashMap.entrySet();
for (Map.Entry<Object, Object> entry : entrySet) 
   System.out.println(entry);


// 2. 通过 KeySet 遍历
Set<Object> keySet = hashMap.keySet();
for (Object key : keySet) 
   System.out.println(key + " - " + hashMap.get(key));


// 3. 使用 Map.Entry 的 getKey() 和 getValue() 方法
for (Map.Entry<Object, Object> entry : entrySet) 
   System.out.println(entry.getKey() + " - " + entry.getValue());

12. HashMap

  1. key - value 封装在 HashMap$Node;HashMap 线程不安全
// key 相同而 value 不同时的替换 value 的机制
if (e != null)  // existing mapping for key
   V oldValue = e.value;
   if (!onlyIfAbsent || oldValue == null)
       e.value = value;
   afterNodeAccess(e);
   return oldValue;

  1. JDK 7.0 的 HashMap 底层实现是(哈希表 + 链表),JDK 8.0 是(哈希表 + 链表 + 红黑树)
  2. 若某颗红黑树元素个数较少,则会触发剪枝行为即将红黑树转换为链表

13. HashTable

  1. Hashtable 的键和值都不能为 null,为 null 则抛出 NullPointerException,使用方法与 HashMap 基本一致;Hashtable 线程安全,HashMap 线程不安全
  2. Hashtable 初始化容量大小为 11,数组类型为 Hashtable$Entry;加载因子 loadFactor 0.75;按原有容量的 2 倍加 1 的机制进行扩容
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;

14. 集合选型规则

15. TreeSet

// TreeSet 实现排序
// TreeMap 的 put(K,V) 方法
public V put(K key, V value) 
   Entry<K,V> t = root;
   if (t == null) 
       compare(key, key); // type (and possibly null) check

       root = new Entry<>(key, value, null);
       size = 1;
       modCount++;
       return null;
   
   int cmp;
   Entry<K,V> parent;
   // split comparator and comparable paths
   Comparator<? super K> cpr = comparator;
   if (cpr != null) 
       do 
           parent = t;
           cmp 

以上是关于[Java]集合和泛型的主要内容,如果未能解决你的问题,请参考以下文章

[Java]集合和泛型

java集合框架和泛型

初识集合和泛型

集合框架和泛型编程

初识Java集合及包装类和泛型的基本使用

07.集合和泛型