1.JAVA基础——集合2

时间:2022-09-03 00:22:20

一些常用集合:

ArrayList:ArrayList封装了一个动态再分配的Object[]数组,在ArrayList的源码中有这样一个属性,添加到ArrayList中的元素最终其实都是添加到这个Object数组中

private transient Object[] elementData;

ArrayList的add(Object o)实现也比较简单

public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

先将容量加1(包括对数组扩容等操作),然后将数组的下一个对象设为要添加的元素即可。

ArrayList的get(int index)取值则直接通过Object[]数组的下标取值。

所以,ArrayList有以下特性:快速随机访问(直接通过下标取值,建议遍历ArrayList采用for循环get方式)、添加、删除元素很慢(1.数组大小需要随着元素数量变化而变化;2.往中间添加、删除元素会导致后面所有元素整体后移、前移)

 

LinkedList:LinkedList是通过双向循环链表来实现的,看LinkedList中的属性header

private transient Entry<E> header = new Entry<E>(null, null, null);

//无参构造方法
public LinkedList() {
    header.next = header.previous = header;
}

而Entry是LinkedList的一个内部类:

private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

在使用new LinkedList()创建一个LinkedList后,它会在LinkedList内部生成一个Entry(Entry是一个包含前节点、自身还有后节点的对象),并将该Entry设置成一个空的双向循环链表。下面代码是将LinkedList中的Entry类提取出来,测试Entry双向循环链表的实现过程。根据打印结果可以得知,root—test1—test2—test3整体形成了一个双向循环链表。

public static void main(String[] args) {
    TestJVM tj = new TestJVM();
    Entry<String> header = new Entry<String>("root", null, null);
    header.next = header.previous = header;
    tj.addBefore("test", header);
    tj.addBefore("test2", header);
    tj.addBefore("test3", header);
    System.out.println(header.element);
    
    System.out.println(header.previous.element);
    System.out.println(header.previous.previous.element);
    System.out.println(header.previous.next.element);
    
    System.out.println(header.next.element);
    System.out.println(header.next.previous.element);
    System.out.println(header.next.next.element);
    System.out.println(header.next.next.previous.element);
    System.out.println(header.next.next.next.element);
    
    System.out.println(header.next.next.next.previous.element);
    System.out.println(header.next.next.next.next.element);
}

private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

private Entry<String> addBefore(String e, Entry<String> entry) {
    Entry<String> newEntry = new Entry<String>(e, entry, entry.previous);
    newEntry.previous.next = newEntry;
    newEntry.next.previous = newEntry;
    return newEntry;
}

LinkedList的add方法调用就是上面的addBefore方法,首先根据要添加的对象创建一个entry对象,然后将旧链表的首元素的previous指向自身,再讲最后一个元素的next指向自身,这样就实现了在链表最后添加元素,并形成双向循环链表(保持添加顺序)。

所以,LinkedList有以下特征:1.添加、删除元素很快(只需要修改元素位置的前后链接就可以);2.随机访问很慢(本质不支持随机访问,虽然有get(int index)方法,但其实是从header顺序或逆序查找目标对象。对于LinkedList的遍历,建议通过Iterator迭代器遍历)。

 

Vector:Vector本质也是动态增长的数组,但是与ArrayList不同,vector支持线程同步,vector的遍历是由枚举实现的:

public Enumeration<E> elements() {
        return new Enumeration<E>() {
            int count = 0;

            public boolean hasMoreElements() {
                return count < elementCount;
            }

            public E nextElement() {
                synchronized (Vector.this) {
                    if (count < elementCount) {
                        return elementData(count++);
                    }
                }
                throw new NoSuchElementException("Vector Enumeration");
            }
        };
}

并且实现了线程锁,所以,Vector随机取值很快(数组),但是遍历很慢(线程锁)。此外,ArrayList的扩容增长是扩展50%+1,Vector则是扩展一倍。

 

HashSet:HashSet的实现只是封装了一个HashMap对象来存储所有集合元素。

这些元素都被放在HashMap的key中,而value则是一个由 HashSet创建的静态Object对象——PRESENT。它的绝大部分方法都是通过调用HashMap的方法来实现的,因此HashSet和 HashMap在本质上是一样的。

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{ static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * 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<E,Object>(); } ............ }

由于HashMap中,存入键值对的key只有一个能为空,且不为空的key会判断是否存在其他key相等,所以HashSet中存放元素也是一样,只能有一个为null,且不能重复。判断重复的方法是:先比较hashCode()方法的返回值是否相同,不同则两个值不一样,相同则通过equals()方法比较是否相同。
关于HastSet的内部实现可参照HashMap的实现。

TreeSet:TreeSet与HashSet最大的不同在于,TreeSet的底层实现是通过一个NavigableMap来实现的,NavigableMap接口的官方实现是TreeMap。

在使用默认的TreeSet构造方法的时候,实际上就是构造了一个TreeMap作为其属性。

    /**
     * The backing map.
     */
    private transient NavigableMap<E,Object> m;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
public TreeSet() { this(new TreeMap<E,Object>()); }

从上面TreeSet的默认构造方法可以看出,TreeSet的m属性本质是一个TreeMap。TreeMap是通过红黑树实现的排序集合,所以TreeSet中的元素也是排序了的,默认使用元素自身实现comparable接口实现的compareTo方法进行比较。对于TreeSet中的元素,一般认为是不能为空,但其实并非如此。

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
        // TBD:
        // 5045147: (coll) Adding null to an empty TreeSet should
        // throw NullPointerException
        //
        // compare(key, key); // type check
            root = new Entry<K,V>(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 = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<K,V>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }

 TreeSet底层通过TreeMap来实现,而根据上面TreeMap方法可以看出,在TreeMap的add方法里面,第一个元素的key为null的时候是可以正常添加的,而且如果TreeMap的构造方法中实现了comparator,后续元素的key为null也是可以的,只是如果没有实现comparator,后续元素的key如果为空,则会抛出NullPointerException。所以可以有以下

public static void main(String[] args) throws Exception {
    TreeSet treeSet = new TreeSet(new MyComparator());
    treeSet.add(null);
    treeSet.add(null);
    System.out.println(treeSet.size());

    TreeSet treeSet2 = new TreeSet();
    treeSet2.add(null);
    treeSet2.add(null);//该行会报空指针异常
    System.out.println(treeSet.size());
}

在上面代码中,treeSet由于构造方法中定义了comparator,而treeSet2则没有,所以在treeSet2中第二个添加方法中则会报空指针异常。

LinkedHashSet:LinkedHashSet集成自HashSet,其构造方法调用HashSet的一个构造方法:

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
      map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
    }

所以,可知LinkedHashSet底层通过LinkedHashMap实现。
LinkedHashSet中的元素采用插入顺序排序。

-----------------------------------------------------------------------------------------------------------------

一、List接口

List接口继承自Collection接口,有序且允许重复。

List接口提供以下方法:

void add(int index, Object element);//在指定位置添加指定元素

boolean addAll(int index, Collection c);//将集合所有元素添加到指定位置

Object get(int index);//获取指定位置元素

int indexOf(Object o);//获取第一个出现元素o的位置,没有则返回-1;

int lastIndexOf(Object o);//返回最后一个元素o的位置,没有则返回-1;

Object remove(int index);//移除指定位置的元素;

Object set(int index, Object element);//用元素element取代index位置上的元素,并返回旧元素。

 

ListIterator listIterator();//返回一个列表迭代器,用于访问列表中的元素;

ListIterator listIterator(int index);//返回一个列表迭代器,用于从指定位置index开始访问列表中的元素;

List subList(int fromIndex, int toIndex);//返回从指定位置fromIndex(包含)到toIndex(不包含)范围中各个元素的列表视图.

 

注:对子视图的更改对底层list也有影响。

 

LinkedList类 ArrayList类:

相同:两个类都实现了Cloneable接口,都提供了两个构造函数,一个无参的,一个接受一个Collection。

区别:如果要支持随机访问(RandomAccess),而不必在除尾部的任何位置插入或除去元素,ArrayList提供了可选的集合;如果要频繁从列表中间位置添加和除去元素,并且只需要顺序(元素的添加顺序)访问列表元素,LinkedList更好。

  1. LinkedList类:提供了处理列表两端元素的方法:
    • void addFirst(Object element);//将element添加到列表开头
    • void addLast(Object element);//将element添加到列表结尾
    • Object getFirst();//返回列表开头元素
    • Object getLast();//返回列表结尾元素
    • Object removeFirst();//删除并返回列表开头元素
    • Object removeLast();//删除并返回列表结尾元素
  2. ArrayList类:它封装了一个动态再分配的Object[]数组,每个ArrayList都有一个capacity,用于表示存储列表中元素数组的容量。当元素添加到ArrayList中时,其capacity在常量时间内自动增加。在向一个ArrayList中添加大量元素时,可使用ensureCapacity方法增加capacity,这样可以减少增加重分配的数量。
    • void ensureCapacity(int minCapacity);//将ArrayList容量增加到minCapacity;
    • void trimToSize();//整理ArrayList容量为当前列表大小,可使用该方法减少ArrayList对象的存储空间。

二、Set接口

Set接口继承自Collection接口,无序且不允许重复。

HashSet类和TreeSet类

相同:Set集合接口的两种实现,都继承自AbstractSet类

区别:TreeSet实现自SortedSet,当需要从集合中以有序方式插入和抽取元素的时候,TreeSet实现比较合适,注意,添加到TreeSet 的元素必须是可排序的。

  • HashSet:HashSet的实现只是封装了一个HashMap对象来存储所有集合元素,这些元素都被放在HashMap的key中,而value则是一个由HashSet创建的静态Object对象——PRESENT。它的绝大部分方法都是通过调用HashMap的方法来实现的,因此HashSet和HashMap在本质上是一样的。
    public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable{
        static final long serialVersionUID = -5024744406713321676L;
    
        private transient HashMap<E,Object> map;
    
        // Dummy value to associate with an Object in the backing Map
        private static final Object PRESENT = new Object();
    
        /**
         * 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<E,Object>();
        }
    ............
    }

    上面是HashSet的源码,可以看出其构造方法其实就是new了一个HashMap对象。

    HashSet的add()方法在添加元素时,实际上是调用HashMap的put()方法来添加key-value对。而在HashMap中,put()方法首先调用hashCode()方法判断两个对象的返回值是否相等,如果不相等则直接返回false,如果相等则进一步调用equals()方法比较,相等才返回true。也就是说,HashSet的add()方法(以及HashMap的put()方法)需要校验两点:插入值的hashCode()方法和equals()方法。
  • TreeSet:TreeSet是实现自SortedSet,SortedSet会对自身元素进行排序(按compareTo()方法或compare()方法比较大小),在添加一个元素时,它首先会比较顺序,如果比较方法返回0,则表示这俩对象顺序一样,那么新元素就没有添加进去。也就是说,在TreeSet中,add()方法通过调用元素自身实现的compareTo()方法或TreeSet创建是指定compartor比较器的compare方法,来进行判断新元素是否存在

HashSet和TreeSet的构造方法:

  • (1) HashSet(): 构建一个空的哈希集
    (2) HashSet(Collection c): 构建一个哈希集,并且添加集合c中所有元素
    (3) HashSet(int initialCapacity): 构建一个拥有特定容量的空哈希集
    (4) HashSet(int initialCapacity, float loadFactor): 构建一个拥有特定容量和加载因子的空哈希集。LoadFactor是0.0至1.0之间的一个数
  • (1) TreeSet():构建一个空的树集
    (2) TreeSet(Collection c): 构建一个树集,并且添加集合c中所有元素
    (3) TreeSet(Comparator c): 构建一个树集,并且使用特定的比较器对其元素进行排序
    “comparator比较器没有任何数据,它只是比较方法的存放器。这种对象有时称为函数对象。函数对象通常在“运行过程中”被定义为匿名内部类的一个实例。”
    TreeSet(SortedSet s): 构建一个树集,添加有序集合s中所有元素,并且使用与有序集合s相同的比较器排序

LinkedHashSet类:

LinkedHashSet是HashSet的一个扩展,HashSet是无序不重复集合,TreeSet虽然有序,但顺序是对象比较后的顺序,LinkedHashSet实现了可跟踪添加HashSet元素的顺序。它的迭代器按照元素的插入顺序来访问各个元素,并提供了一个可以快读访问各个元素的有序集合,

三、HashMap、TreeMap和LinkedHashMap类

 HashMap、TreeMap是Map的常规实现,其中TreeMap实现自SortedMap接口。

HashMap:

HashMap的实现:以“链表散列”的形式实现。先看源码:

    /**
     * The table, resized as necessary. Length MUST Always be a power of     two.
     */
    transient Entry[] table;
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;

        ......
    }

上面的table是一个由Entry<k, v>组成的数组,HashMap存储的键值对就是Entry对象,Entry是HashMap中实现了Map.Entry的内部类,它持有指向下一 个元素的引用。假设table[1]的元素是一个Entry[]对象m,而m.next是另一个Entry对象n,那么,m和n都是存放在 table[1]这个位置上的元素。

也就是说:HashMap中存放的是数组,而数组的元素则是持有下一个元素的链表,这样,HashMap的实现就形成了“链表散列”的形式。

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        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;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

上面是HashMap的put方法:1.如果key为空,则调用putForNullKey方法,2.根据key的HashCode计算hash值,然后根据hash值计算出其在table中的对应索引,如果索引处的Entry对象e不为空,那么循环判断e和e.next的hash属性及key属性是否与要添加的新元素的这两个属性相同,相同即会把新添加的元素的value覆盖原来的e的value,并返回原来的value,3.当2中的循环没有找到可以替换的元素时,表示table中的Entry及他们的next对象没有key重复的情况,那么就会调用addEntry方法往HashMap中新添加一个Entry对象。

addEntry方法:新建一个entry对象,放在table的bucketIndex位置上,并把原来的table[bucketIndex]对象设置为新加对象的next属性。

上面内容也就证明了HashMap基本结构确实是table数组构成的,只是数组中的Entry对象包含对其他Entry的引用,而这些引用也是HashMap中的元素。那么,可以推测的是:HashMap在取值的时候,不能直接根据table的下标取值,因为对于Entry的引用元素是无法通过下标取得的,所以,HashMap的取值基本逻辑为:先根据key的Hash值计算出该key在table中的位置,然后循环这个位置上Entry对象及其引用,比对其hash和key属性,直到找到目标元素。

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

根据以上内容,可以总结:HashMap的基本实现是由一个table数组构成,但并不是所有元素都存放在table中,因为有的元素会以table数组中的Entry元素引用的形式存放,这使得整个table数组构成一个链表散列

关于HashMap的性能参数:

  • HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

属性说明:

initialCapacity:指的是初始容量,即HashMap中数组table数组的容量,默认是16;

loadFactor:指的是负载因子,计算公式为:HashMap的实际容量(存放Entry对象的个数)/HashMap中数组table数组的容量。

根据上面定义可知:table数组容量衡量了HashMap所占用空间的大小,而负载因子则表示空间的利用率,当负载因子较大时,表示该HashMap对空间的利用更充分,但是,结合之前对HashMap的分析,如果table[bucketIndex]的元素有很多层引用,那么寻找指定元素的方法就是循环遍历每一个引用对象并比对,也就是说,这个引用层级越多时,查找效率越低,所以得出结论:负载因子越大,该HashMap对空间的利用更充分,但元素查找的效率越低;负载因子越小,该HashMap对空间的利用效率更低,但元素查找的效率越高。

TreeMap:

TreeMap的实现:TreeMap的实现是通过红黑树实现的,红黑树节点位置通过其属性comparator进行比较获知。

    private final Comparator<? super K> comparator;

    private transient Entry<K,V> root = null;

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
        // TBD:
        // 5045147: (coll) Adding null to an empty TreeSet should
        // throw NullPointerException
        //
        // compare(key, key); // type check
            root = new Entry<K,V>(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 = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<K,V>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

查看其put方法可知:往TreeSet中添加元素的会根据key进行排序,然后插入到排序位置。

TreeMap通过红黑树实现元素的存储,其包含一个Entry对象的root属性,Entry对象包含key、value以及left、right、parent属性,TreeMap以添加的第一个元素为root,其后添加的元素根据key进行比较,如果树中没有该key,则按比较结果添加该元素。

LinkedHashMap:

LinkedHashMap集成自HashMap,HashMap存储是通过链表散列的方式实现,而LinkedHashMap则是通过哈希表和双向链表来保存元素。保存在LinkedHashMap中的数据可以以特定规则排序(访问排序和插入排序),如果是访问排序,LinkedHashMap会将元素按照元素本身的最后访问顺序进行排序;如果是插入排序,则会将元素按照插入时的顺序进行排序。LinkedHashMap包含属性accessOrder,当该属性为true时为访问排序,false时为插入排序。默认情况下accessOrder为false(插入排序)。

四、Vector、Stack和HashTable

 Vector和HashTable类似,分别是ArrayList和HashMap的线程安全版本。与他们不同的是,Vector扩容是增长1倍,而ArrayList则是增长50%然后加1;HashTable中key和value都不能为Null,且默认初始化大小为11。

Stack继承自Vector。

推荐用ArrayList代替Vector,用HashMap代替HashTable。

五、Collections和Arrays

Collections和Arrays是Java提供的工具类,分别提供了一系列方法对Collection和array进行操作的静态方法。包括sort(排序)、min、max、empty等方法。