目录
Java集合是什么?
集合可以说是一种保存对象的容器,他可以将我们需要的多个对象进行存储,来方便我们对多个对象可以进行的一些操作。我们最先接触的Java容器时数组,但是为什么现在又要接触集合?这就要说明二者的区别了。我们在声明一个数组的时候,就需要确定数组的长度和数组所能存放元素的类型,然后才可以进行其他操作,这样数组的局限性也就体现出来了:当我们存储的数据在增多并且超过我们原先声明的长度时,我们需要扩展数组的长度才可以继续往里添加数据;而且数组中提供给我们操作数据的方法比较少;最后就是数组存储的数据只能是有序可以重复的,数据的特点比较单一,无法应对现在我们的复杂需求。所以集合就出现了,他可以很好的解决数组存在的问题。
Java集合的分类
Java集合分为俩类:单列数据集合Collection和双列数据集合Map。在这俩个接口下又有他们具体的实现类,我们可以根据自己的具体业务需求来创建适合我们自己的集合。
Collection接口
- List接口:存储有序、可重复的数据。
- ArrayList(实现类)、LinkedList(实现类)、Vector(实现类)
- set接口:存储无序、不可重复的数据。
- HashSet(实现类)、LinkedHashSet(实现类)、TreeSet(实现类)
Map接口
- HashMap(实现类)
- LinkedHashMap(实现类)
- TreeMap(实现类)
- HashTable(实现类)
- Properties(实现类)
Collection子接口--List接口
List是用来存储有序、可重复的数据。他的三个实现类分别的ArrayList、LinkedList、Vector。List集合我们常常可以认为是“动态数组”的概念,因为其底层实现也是数组,但是增加了自动扩容的机制。
ArrayList(JDK 8为例)
ArrayList作为List的主要实现类存在,它的底层是用一个object类型的数组来存储数据。(下图为ArrayList源码)
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access
在我们new一个ArrayList时,集合初始化的时候,ArrayList不会先创建这个集合。(下图为ArrayList源码)
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
/** * Shared empty array instance used for default sized empty instances. We * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when * first element is added. */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
而是等我们调用add()方法对数据进行添加到时候,ArrayList才会去为我们创建一个长度为10的集合。(下图为ArrayList源码)
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; }
/** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10;
ArrayList线程不安全,但是带来的好处就是效率高。
当添加的数据大小超过初始化大小,ArrayList会进行自动扩容操作,然后将旧的数据拷贝到新扩容好的集合中,在进行add新数据。默认情况下,集合会扩容之前集合大小的1.5倍。(下图为ArrayList源码)
/** * 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); }
LinkedList
LinkedList底层实现是使用双向链表实现,在初始化时,不仅创建一个数据节点,还维护着节点俩端的一对指针,指向该数据节点的前一个和后一个数据节点。
/** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
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; } }
在调用add()方法添加数据时,LinkedList需要从链表的最后一个元素开始的后面开始添加,将最后一个元素的尾指针指向添加的元素,然后将添加的元素的头指针指向链表最后的元素,即可完成添加操作。
/** * Appends the specified element to the end of this list. * * <p>This method is equivalent to {@link #addLast}. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { linkLast(e); return true; }
/** * Links e as last element. */ 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++; }
由于底层结构的原因,LinkedList更适合做插入和删除操作,只需要将元素的指针移动即可完成操作,但是对于查询操作,也需要从头开始遍历,效率反而不如ArrayList。
Vector
vector伴随着jdk 1.0而诞生,底层也是用object类型的数组对数据进行保存。
* @author Lee Boynton * @author Jonathan Payne * @see Collection * @see LinkedList * @since JDK1.0 */ public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { /** * The array buffer into which the components of the vector are * stored. The capacity of the vector is the length of this array buffer, * and is at least large enough to contain all the vector's elements. * * <p>Any array elements following the last element in the Vector are null. * * @serial */ protected Object[] elementData;
Vector初始化时,默认创建一个长度为10的数组进行存储数据。
/** * Constructs an empty vector so that its internal data array * has size {@code 10} and its standard capacity increment is * zero. */ public Vector() { this(10); }
Vector底层方法都有synchronized关键字修饰,所以线程安全,但是效率比较低。
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
在扩容方面,Vector默认扩容原来数组长度的2倍。
Collection子接口--set接口
set集合存储无序、不可重复的数据。set中的无序性表现为非索引顺序,set中的元素需要根据自身的hashCode值进行计算元素所存放的位置。set中的不可重复性表现为添加元素.equals()返回为false才可以添加成功。
HashSet
hashSet作为set接口的主要实现类,其线程不安全,效率较高。新添加的元素与已在位置上的元素用链表形式存放,在jdk8中,hashSet底层是用数组+链表+红黑树
- HashSet添加元素的过程(向set中添加数据时,需要将添加数据所在的类重写hashCode和equals方法):
在我们new一个HashSet的时候,底层其实为我们new了一个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<>(); }
在我们往HashSet中add数据时,底层其实将数据存放在了map中存放的key的位置,value的位置是一个常量,可以 理解为没有数据。
public boolean add(E e) { return map.put(e, PRESENT)==null; }
- 在添加数据时,首先会调用被添加元素的hashCode值计算其的hash值,然后通过底层的算法将该元素的存放位置进行计算得到
2. 如果该索引位置上没有元素,就直接添加成功。
3. 如果该索引位置上有其他元素(不同的hash值经过计算可能得到相同的索引位置),就比较其俩个的hash值,如果hash值不同则添加成功。
4. 如果二者的hash值相同,就需要调用被添加元素的equals方法进行比较,返回false即可添加成功。
LinedHashSet
linkedHashSet是HashSet的子类,在HashSet的基础上增加了一对指针,遍历其内部数据时,可以按照添加的顺序遍历(这不能说明LinkedHashSet是存放有序的数据)。对于频繁遍历的操作,LinkedHashSet效率要高于HashSet。
TreeSet
TreeSet底层是用红黑树对数据进行存储,它的查询效率很高。存储时需要对指定对象的指定属性进行排序操作,所以treeSet中添加的数据必须是相同类的对象并且不可以添加相同的数据。TreeSet可以确保集合的元素处于一种排好序的状态。
Collection中的常用的方法
- add
- addAll
- size
- isEmpty
- clear
- contains
- remove
- removeAll
- retainsAll
- equals
List接口中常用的方法
- add
- addAll
- get
- indexOf
- set
- remove
- subList
- size
- iterator
Set接口中常用方法:
Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。
Map接口
map存储的是双列数据,存储的是k-v键值对类型的数据。在map中保存的k-v对,key不可以重复并且无序,value可以重复,底层保存这对k-v使用的是Node对象来进行保存的。Node保存数据的特点是:无序、不可重复。在使用map存储数据时, map对象所在的类要重写equals()和hashCode()方法。
HashMap(JDK 8)
作为Map接口的主要实现类,线程不安全,但是效率比较高,可以存储null值的key和value。HashMap底层是利用数组+链表+红黑树进行存储。
1. 我们在new 一个HashMap的时候,底层没有立刻为我们创建出一个存放数据的数组,在jdk7中,底层是为我们创建了一个长度为16的Entry数组。
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
2. 在我们往HashMap中首次put数据时,才会为我们创建一个长度为16的Node[ ]数组,在JDK 7中,为我们创建的是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> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
3. 我们调用put传想要存放的数据,HashMap底层进入putVal方法中,如果首次添加,tab ==null, 就会进入resize()方法中,resize()方法中在首次就是为我们造好了长度为16的Node[ ]数组,同时也声明了一个newThr的临界值变量。
4. 如果数组不为空,不是第一次添加就不会进入resize()方法中,就会执行下面的操作。首先HashMap会对传入数据的key做一个运算,求出数据的key存放在数组中的位置,如果此位置上为null,也就是此位置上没有数据,所以就把该值存放在此位置上,即添加成功。
5. 如果不为null,则表明存放该数据的位置上已经有值,HashMap底层会将待存储的数据与已经在位置上的数据进行hash的比较, 如果hash值不同就要调用待存储数据的equals方法比较二者,如果返回结果为false就进入循环,继续比较在该位置上的其他元素,如果都返回false则按照链表的方式在该位置的链表末尾添加该数据,如果返回true就进入替换逻辑,将待存储数据的value替换旧的数据的value。
6. 在此期间还有一段逻辑,就是在你添加数据时会进行判断该位置上存在的数据节点的长度,如果长度大于8会将此位置保存数据的方式转变为 红黑树存储。
7. 在treeifyBin这个方法中我么可以看到,并不是链表长度到8就立马变为红黑树存储,另一个条件就是数组长度要大于64,考虑资源情况,可能数组长度不够64,可以使用扩容数组长度来达到数据平衡存储,所以底层选择扩容resize(),当两个条件都满足就会转变为红黑树存储。
8. 在我们不断地存储数据,总会有数据放不下的情况,所以HashMap也会面临自动扩容的问题,但是相对于ArrayList装满了才去扩容,HashMap选择了一个叫做临界值的常量来控制扩容时机:默认临界值=容量大小*加载因子=16*0.75 = 12。为什么会提前扩容?因为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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; 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); 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; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; return newTab; }
LinkedHashMap
在HashMap的基础上增加了一对指针,存储时根据元素key的hashCode值来决定元素存放的位置。在遍历时可以按照添加顺序进行遍历集合,因为每个数据元素都记录着自己的前后元素的位置。对于频繁遍历集合的操作,效率要比HashMap高。
LinkedHashMap是HashMap的子类,在添加数据时,也是调用父类中的putVal()方法进行添加操作。而LinkedHashMap重写了父类中的newNode()方法,在new一个数组的时候,LinkedHashMap维护了数据的一对前后指针。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; }
/** * HashMap.Node subclass for normal LinkedHashMap entries. */ static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
Hashtable
Hashtable伴随着jdk 1.0 所诞生,他线程安全,但是效率不高,不可以存储null的key和value值。
Properties
是HashTable的子类,一般用来处理配置文件。properties中的key和value都是字符串类型的。
TreeMap
底层实现是使用红黑树,也就是排序二叉树。对于存储数据时,数据添加的位置需要根据自定义比较规则来判断数据key的大小,再由红黑树进行存储。
Map接口中常用的方法
- put
- putAll
- remove
- replace
- clear
- get
- containsKey
- containsValue
- size
- isEmpty
- equals
Collections
collections是一个工具类,用来操作Set,List,Map集合的。collections中提供了一些方法可以很高效的对集合中的元素进行操作,例如排序、查询、修改,反转等等操作。
collections常用方法:
- reverse
- shuffle
- sort
- swap
- min
- max
- list
- copy
- replaceAll