集合
可以分为两大类:Collection和Map,其中Collection是一个个存放数据的,而Map是一对一对存放数据的
Collection是一个接口,有两个重要的子接口:List和Set,其中List是有序的可以重复的,Set是无序的不可重复的
List接口重要的实现类:ArrayList、LinkedList、Vector(已淘汰)
Set接口重要的实现类:HashSet(重要)、TreeSet
Collection接口实现类的特点:
1.Collection的实现类可以存放多个元素,每个元素可以是obj
2.有些Collection的实现类,可以存放重复元素,有些不可以
3.有些Collection的实现类,有些有序,有些无序
4.Collection接口没有直接实现的子类,是通过子接口List和Set实现的
Collection接口遍历元素的方式:
1.使用迭代器iterator
2.增加for
List接口:
1.List集合类中元素有序且可重复(添加和取出的顺序一致)
2.List中支持索引
3.List容器中的元素都对应一个整数型的序号记载在容器中的位置,可以根据序号存取容器中的元素
List的遍历方式:
1.迭代器
2.增强for
3.普通for
ArrayList
1.可以加入null,并且是多个
2.底层是由数组实现数据存储的
3.ArrayList基本等同于Vector,除了ArrayList是线程不安全的(但是效率高),有多线程的情况下不建议使用ArrayList()
ArrayList底层机制:
1.ArrayList中维护了一个Object类型的数组elementData
transient Object[] elementData;
2.创建ArrayList对象时,如果使用的是无参构造器,初始化elementData数组容量为0,第一次添加数据时,则扩容为10,再次需要扩容则为原来1.5倍
//(1)无参构造器初始化
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA如下,为空
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//(2)执行add
//第一次添加数据时,调用add方法,add方法里有 ensureCapacityInternal方法
//先确定是否要扩容,再添加
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//(2)执行ensureCapacityInternal
//ensureCapacityInternal方法具体实现,这是确定容量是否足够
//此时minCapacity当前最少需要的空间 = 1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//(3)执行calculateCapacity,确定minCapacity值
//再看calculateCapacity方法,这个方法是看要扩容的量的需求
//第一次传入的minCapacity是1,此时elementData是空的,可以看到
//返回了两者中较大的默认容量=10,也就是返回了10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
//(4)执行ensureExplicitCapacity
//可以看到真正扩容的是grow()方法
//判断条件是:需求量大于数组的长度就要扩容
//此时minCapacity=10,而数组长度=0,那么就要扩容
private void ensureExplicitCapacity(int minCapacity) {
//记录修改数组的次数,此时++完=1
modCount++;
// overflow-conscious code
//判断扩容条件
if (minCapacity - elementData.length > 0)
//真正扩容的操作
grow(minCapacity);
}
//(5)看grow方法
private void grow(int minCapacity) {
// overflow-conscious code
//用一个old变量接收数组长度=0
int oldCapacity = elementData.length;
//用一个new变量来接收 old + (old >> 1)
//此时new = 0 + 0 / 2 = 0
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
//第一次添加,就把min需求量赋给了new
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//可以看到此时维护的数组elementData指向了一个copy的数组,这个数组的需求量就是10
elementData = Arrays.copyOf(elementData, newCapacity);
}
//(6)折腾完毕ensureCapacityInternal这个方法
//终于可以添加了elementData[size++] = e;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
LinkedList
1.底层维护是一个双向链表和双端队列的特点
2.可以添加任意元素(可重复),包括null
3.线程不安全,没有实现同步
LinkedList底层机制:
1.底层维护了一个双向链表
2.LinkedList中每一个结点维护了两个指针first和last分别指向头和尾
3.每个节点(Node对象)里面又维护了pre,next和item,分别是代表指向前一个,指向后一个,内容,最终实现双向链表
4.LinkedList的元素添加和删除不是通过数组完成,效率相对较高
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
linkedList.remove(1);
System.out.println("linkedlist = " + linkedList);
//(1)初始化建立的是一个空数组
public LinkedList() {
}
//此时size = 0
transient int size = 0;
//(2)执行add方法
public boolean add(E e) {
linkLast(e);
return true;
}
//(3)执行linklast方法,顾名思义就是把元素添加到末尾
void linkLast(E e) {
//此时last初始化是为null的,所以辅助变量l也是null
final Node<E> l = last;
//new了一个新结点,前指针和后指针都为null,内容为添加的e
final Node<E> newNode = new Node<>(l, e, null);
//移动last指针,让last指向新创建的结点
last = newNode;
//此时l为null,说明我们是加入第一个结点
if (l == null)
//first也指向新结点,此时last和first都指向第一个结点
first = newNode;
//第二次添加,很明显此时l指向的是第一个结点,那么
//第一个结点的next就是第二个结点存放的位置,以此类推
else
l.next = newNode;
//此时size实际数据量=1,modcount修改次数也=1
size++;
modCount++;
}
//(4)添加成功,返回一个true
public boolean add(E e) {
linkLast(e);
return true;
}
//再看看删除是如何做的
//(1)看unlink方法
public E remove(int index) {
//checkElementIndex这个方法是检查代码健壮性的
checkElementIndex(index);
return unlink(node(index));
}
//(2)node(index)主要是看是从前往后找,还是从后往前找,提高效率
Node<E> node(int index) {
// assert isElementIndex(index);
//如果index小于链表长度的一半,从前往后找,下面以此类推
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
//(3)unlink方法
E unlink(Node<E> x) {
// assert x != null;
//定义一个变量element,把删除的x的内容赋给它
final E element = x.item;
//定义一个辅助变量,这个辅助变量指向的是删除的节点的下一个
//next 指向的就是 3
final Node<E> next = x.next;
//定义一个辅助变量,这个辅助变量指向的是删除的节点的前一个
//prev 指向的就是 1
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
//我这里删除的是索引为1的结点,也就是删除2
} else {
//第一个结点的next指向第三个
prev.next = next;
//把删除的第二个结点的prev置空
x.prev = null;
}
if (next == null) {
last = prev;
} else {
//第三个结点的prev指向第一个结点
next.prev = prev;
//第二个要删除的节点的next置空
x.next = null;
}
//内容置空
x.item = null;
size--;
modCount++;
return element;
}
Vector
1.类定义
2.Vector底层是一个对象数组
protected Object[] elementData;
3.Vector是线程同步的,是线程安全的
Vector<Object> v = new Vector<>();
v.add(1);
v.add("hello");
v.add(2.3);
System.out.println("vector = " + v);
//(1)调用无参构造器,初始化扩容为10
public Vector() {
this(10);
}
//(2)调用add方法,可以看出有 synchronized 修饰说明是线程安全的
//此时elementCount = 0
public synchronized boolean add(E e) {
//记录修改次数
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
//(3)ensureCapacityHelper方法
//此时minCapacity需求量=1,而length=10,不用扩容
//判断扩容条件是需求量 大于 数组长度
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//如果是需要扩容呢?
//(4)看看grow方法
//假设此时添加第11个元素
private void grow(int minCapacity) {
// overflow-conscious code
//定义变量old = 数组长度10
int oldCapacity = elementData.length;
//定义变量new = old + ((capacityIncrement > 0) ? capacityIncrement : old)
//也就是new = 10 + 10 = 20 扩容为原来的两倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//维护的数组指向了容量为20的复制的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
HashSet底层机制
1.HashSet底层是HashMap维护的,HashMap的底层是 数组 + 链表 + 红黑树
2.添加一个元素时,先得到一个hash值,这个hash值会转成索引值
3.找到索引值也就是找到了数组table的存储位置,看看这个索引位置是否以及存放了元素
4.如果没有存放元素,那么直接存入
5.如果这个位置已经有了元素,那么调用equals方法进行比较,如果相同,那么放弃添加,如果不相同,则添加到这个索引位置的链表的末尾
6.在java8中,如果一个链表的元素个数达到了8,且table大小>=64,那么这个链表就会进行树化
Set hashset = new HashSet<>();
hashset.add("java");
hashset.add("c++");
hashset.add("java");
System.out.println("hashset = " + hashset);
//1.调用无参构造器,可以看到底层维护的是HashMap
public HashSet() {
map = new HashMap<>();
}
//2.调用add方法,可以看到里面有一个put方法
//put方法返回了null,add返回才返回true,才能添加成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//3.调用put方法,可以看到放入的是一个key和value
//其中这里的value是共享的,
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//4.先看hash这个方法
//可以看到当kel != null时,返回(h = key.hashCode()) ^ (h >>> 16);
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//5.然后就到了最核心的putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义了辅助Node数组变量tab,Node变量p,int的n和i
Node<K,V>[] tab; Node<K,V> p; int n, i;
//此时很明显,tab = table == null 条件是成立的
//先进入resize()方法看看
if ((tab = table) == null || (n = tab.length) == 0)
//也就是说,此时 n = 16
n = (tab = resize()).length;
//这里是计算索引值的
//p = tab[i = (n - 1) & hash
//此时p这个结点指向了要加入的索引值的位置
//很明显第一次加入,p == null 是成立的
if ((p = tab[i = (n - 1) & hash]) == null)
//这个位置,直接把新节点放进去
//第一次加入完毕,直接看加入相同的元素会发生什么S
tab[i] = newNode(hash, key, value, null);
//加入第二个java,很明显要走else分支了 因为此时p != null
else {
//定义了一个辅助变量Node结点e,和一个Key值k
Node<K,V> e; K k;
//如果p指向的这个位置的hash值和要加入的结点的hash值一样
//并且满足 (1)p指向的结点的key值和要加入的结点的key值一样
//或者 (2)传入的结点对象虽然不为空
//但是通过动态绑定机制调用equals和p指向的结点比较后相同
//那么放弃添加
//把辅助变量e指向p,也就是它们都指向了同一个结点
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);
//如果虽然hash值一样,那么就在这个链表一个个比较
else {
for (int binCount = 0; ; ++binCount) {
//此时e 就是指向 第二个结点(因为第一个结点比较过了)
//直接判断第二个结点的下一个位置是不是null
if ((e = p.next) == null) {
//是null毫不犹豫加进去
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果equals有相等的,直接break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//不断移动指针
p = e;S
}
}
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;
}
//6.resize()方法
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 = 16
//newThr = 16 * 0.75 = 12
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//最后返回了一个newTab 大小为16
return newTab;
}
HashSet扩容和转红黑树
1.HashSet底层是HashMap,第一次添加的时候,扩容为16,加载因子为0.75,临界值则 = 16 * 0.75 = 12
2.如果table使用到了12的位置,那么就会扩容到16 * 2 = 32,此时临界值 =
32 * 0.75 = 24,以此类推
3.在java8中,如果一条链表的元素个数达到了8,并且table大小 >=64,那么就会转成红黑树,否则仍采用数组的扩容机制
4.提醒:所谓的加入一个元素就会size++,无论是加到数组的索引位置还是链表的位置
TreeSet
本质是和TreeMap一样的
public class TreeSet_ {
public static void main(String[] args) {
//1.当我们使用无参构造器,创建TreeSet时,仍然是无序的
//2.希望添加元素,按照字符串大小排序
//3.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
//并制定排序规则
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//return ((String)o2).compareTo((String)o1);
//如果按照长度大小排序,那么tom和lys只能加一个tom
return ((String)o2).length() - ((String)o1).length();
}
});
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("lys");
treeSet.add("a");
System.out.println("TreeSet = " + treeSet);
//1.构造器把传入的比较器对象
/*
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
*/
//2.在调用第二次add时,底层执行
/*
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); // 如果相等,也就是返回0,这个数据就加入不了(可以理解为替换)
} while (t != null);
}
*/
}
}
LinkedHashSet
1.是HashSet子类
2.底层是LinkedHashMap,底层维护了一个数组+双向链表
3.根据元素的HashCode值来确定元素的存储位置,同事使用链表来维护元素的次序,这使得元素看起来是以插入顺序存储的
4.不允许添加重复元素
说明:
1.LinkedHashSet中维护了一个hash表和双向链表,是有头尾指针的
2.每一个结点都有before和after熟悉,这样可以形成双向链表
3.在添加一个元素时,先求得hash值,再求索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表(如果已存在,不添加)
tail.next = newElement;
newElement.pre = tail;
tail = newElement;
4.这样的话,遍历它就可以确定插入顺序和取出顺序一致
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);
System.out.println("set = " + set);
1.第一次添加时,直接将table数组扩容到16,存放的结点类型是
LinkedHashMap(dollar)Entry
2.数组是HashMap(dollar)Node
数据是LinkedHashMap(dollar)Entry
3.说到底,LinkedHashSet的add是和HashSet一样的
Map接口特点
PS:这里是JDK8的Map的特点
1.Map和Collection并列存在,用于保存具有映射关系的数据k-v
2.Map中的k和v可以是任何引用类型的数据,会封装到HashMap(dollar)Node中
3.Map的key是不允许重复的
4.Map的value可以重复
5.Map的key和value都可以为null,但是key为null只能有一个
6.常用String类作为Map的key
7.key和value之间存在单向一对以的关系,可以通过key找到对应的value
8.Map存放数据k-v是放在一个Node中的,由因为Node实现了Entry接口,有些书也把一对k和v叫做一个Entry
HashMap
1.k和v最后是HashMap(dollar)Node node = newNode(hash,k,v,null)
2…为了方便遍历,会创建一个EntrySet集合,该集合存放的元素类型为Entry,而一个Entry对象有一个k和v => EntrySet < Entry < k , v > >这样一个关系
3…EntrySet中定义类型是Entry,但是存放的还是Hash$Node,因为
static class Node<k,v> implements Map.Entry<k,v>
4.添加时,发现相等的key则直接替换
Hashtable
1.存放的是键值对k-v
2.hashtable的键和值都不能为null
3.hashatable的使用方法基本上和HashMap一样
4.hashtable是线程安全的,hashMap线程不安全
简单说明一下Hashtable的底层
1.底层有一个Hashtable(dollar)Entry[] 初始化大小为11
2.threshold 8 = 11 * 0.75
3.扩容机制:
4.执行方法 addEntry(hash,key,value,index)
5.addEntry方法里有一个rehash()方法,当大于等于临界值进行扩容
6.在rehash里面有 new = ( old << 1) + 1 相当于 原长度 * 2 + 1
TreeMap
使用默认的构造器,创建TreeMap(有一个默认排序)
//1.构造器,把传入的匿名内部类传给TreeMap的comparator属性
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
//2.第一次添加,把K-V封装到Entry对象,放入root
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;
}
//3.从第二个元素开始就要比较了
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {//遍历所有key,给当前key找位置
parent = t;
//动态绑定到我们的匿名内部类
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
//如果准备加的key和已有的key相等,则不添加
//value是会替换的
return t.setValue(value);
} while (t != null);
}
总结:开发中如何选择集合实现类
1.先判断存储的类型(是存一组对象,还是存一组键值对)
2.一组对象:Collection接口
允许重复:List
增删多:LinkedList[底层维护的是一个双向链表]
改查多:ArrayList[底层维护了一个Object类型的可变数组]
不允许重复:Set
无序:HashSet[底层HashMap]
排序:TreeSet
插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
3.一组键值对:Map接口
键无序:HashMap[数组+链表+红黑树]
键排序:TreeMap
键插入和取出一致LinkedHashMap
读取文件:Properties