背景
在我们的日常开发中经常会使用到集合,比如ArrayList、HashMap等,你是不是平时就只用这俩?哈哈 别装就是说的你,开个玩笑。集合框架是在Java.Util包下的,它是从Java1.2才开始完善的,在此之前呢,也有几个比如Vecctor、Dictionary等。
Collection
我们来简单看一下Java整个集合框架的架构:
在介绍之前先说一下:任何架构(我认为是的),都具有高度的抽象思维,大师们会把最公共的功能抽象成为接口,比如这里最顶层接口Collection / Map, 其次呢在接口的基础上定义一些公共的抽象类去实现接口,并且实现一些最基本的的方法,最后具体实现类会根据自身特点呢实现其他方法,或者重写抽象类实现的方法。所以我们在架构设计的时候应该也要学会采用这种高度的抽象思维。
比如:
在AbstractCollection抽象类里实现了 remove()方法
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
ArrayList根据自己底层是数组的特点又对该方法进行了重写:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
在上面的架构图中我们明显可以看到,最顶层的接口是:Collection 和 Map,一个是单元素的,一个是K,V 键值对形式的。 Collection 接口实现了Iterator 接口,这个借口的作用是迭代器,用户集合内元素的遍历操作。
在Collection 接口下,又分为三种子接口:List 、Set、Queue。List接口的实现类存放的元素是有序的(但是内存空间并不一定是连续的),Set是接口的实现类存放的是无序的,Queue队列,也是有序的,他是一种First In First Out 先进先出的数据结构。
List
在List的接口实现类中,最主要有三个 ArrayList 、LinkedList 、Vetcor
ArrayList底层是Object[] ,在堆中是一段连续的内存,LinkedList 底层是静态内部类构成的双向链表,所以呢,他在堆中内存地址不一定是连续的,并且呢LinkedList还实现了Queue队列,也可以进行首尾操作。
两者的区别呢:ArrayList 遍历下标查询速度是比LinkedList快的,因为LinkedList在查找元素过程中伴随着一个寻址的过程。ArrayList插入或者删除是非常慢的,在删除中间元素时,我们需要移动后面的元素组成一个新的数组,而LinkedList是双向链表的形式,删除中间元素只需要将前一个节点指向被删元素的下一个节点,下一个节点的前节点指向。如下图:添加也是一样的套路。
在ArrayList里,数组的初始化大小为10,添加元素超过数组长度时会动态扩容,创建一个新的数组出来,然后将旧数组的赋值到新数组中,然后将旧数组回收。
但是在这个过程中,扩容并且实现数据复制是非常消耗性能的,所以我们在使用时尽量定义好数组的长度,比如 List<String> list = new ArrayList<>(24); //这是数组长度为24的集合。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
//扩容过程,新数组长度为原数组长度的1.5倍。数组最大长度为Integer.MAX_VALUE.
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由于也实现了Queue的子接口 Deque,所以他也具备队列中的方法,比如添加头节点,删除尾节点等等。
//实现接口方法
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
...
还有一个Vector是在Java1.0中出现的,他和ArrayList的功能上基本一致,这里就不过多介绍了,区别在于:
1、Vector的方法都被synchronized的修饰的,他是线程安全的,Arraylist是线程不安全的。
2、Vector扩容之后的数组长度是原来的2倍,ArrayList只有1.5倍。
Set
Set的主要实现类:HashSet、LinkedHashSet、TreeSet。
HashSet 其实自己呢啥也不是,只不过是借住了HashMap的key作为自己的集合,HashMap是键值对存在的,所以map的value其实是一个不可变的Object对象。如下:
//内部维护了一个hashMap,值都存在HashMap的key里
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 添加元素时,元素就相当于map的key,value为 不可变object对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
所以利用HashMap的特点,HashSet的值是不可以重复的,而且是无序的。
LinkedHashSet 也是一样耍流氓,其底层利用了LinkedHashMap, 在LinkedHashMap中维护了一个双向链表,从而来保存插入顺序。
//LinkedHashSet的结构 这里是定义HashMap的初始化大小,以及负载因子等。
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
TreeSet 也是一样,其底层就是TreeMap,具体如👇的Map中介绍。
//TreeSet的结构
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
Queue
Queue是基于队列的一种结构,它可以保证元素先进先出的特点,Deque是双端队列的实现,继承于Queue。LinkedList又是实现与Qeque双端队列,所以如果要使用队列,通常是直接使用LinkedList。PriorityQueue叫做优先队列,它的特点是为每个元素提供一个优先级,优先级高的元素会优先出队列(默认排序是自然排序,队头元素是最小元素,可以通过comparator比较器修改排序的比较)。
该数据结构的实现主要是在JUC包下的阻塞队列中。
Map
Map的接口实现主要是:HashMap、LinkedHashMap、TreeMap 和HashTable。
HashMap 在1.8中底层数据结构是:数组+链表+红黑树的形式,他是key / value形式的存储元素,元素的存储位置主要是由key的hash值来决定的。hash值就是对象在内存中存储的一串地址码也叫散列码。
所以HashMap中存的元素也是无序的,且不重复,它允许key和value都为null。
LinkedHashMap中,他继承了HashMap,他没有自己的add() remove()等方法,都是利用了父类HashMap的方法,在LinkedHashMap中维护了一个Entry 也是继承了HashMap中的Node内部类,并且Entry里存在before after双向链表,从而记录了插入元素时的顺序。
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);
}
}
TreeMap中,在put()的时候,是保证key实现了Comparator接口,否则则报错,就是通过比较器的形式,来保证插入元素的大小顺序。
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 = 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();
@SuppressWarnings("unchecked")
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<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
HashTable 是在Java1.0中出现了,他在1.2中也实现了Map,所以他也是键值对形式的集合,他的功能实现上也是和HashMap没有很大区别,只不过HashTable的所有方法都是synchronized修饰的,类似于Vector 和 ArrayList的关系,所以HashTable是线程安全的HashMap,只不过就算他是线程安全,在多线程情况下我们也会考虑其他线程安全的Map 比如 ConcurrentHashMap Collections.synchronizedMap等,因为直接在方法上加synchronized 效率实在是太低了。
最后总结:
内容借鉴:
https://www.runoob.com/java/java-collections.html
感谢大家的观看,欢迎大佬指点。
后面会持续更新多优质好文,点关注不迷路。😂 同时小弟公众号👇,感谢大家的支持。