前言:为什么要使用Java集合容器
现在在我们写项目的时候Java集合类容器是最常用来存放对象或者数据的,在接触容器之前我们最常使用的就是数组,那么为什么不继续使用数组而要换成集合容器呢?
在使用数组时会存在几个不足:
- 数组的大小在创建的时候就必须声明,声明后不能更改。
- 数组只能存放同一类型的数据。
- 在数组中使用增删改查较为麻烦。
Java集合容器就解决这些问题
- 集合可以在大小不够时实现一定的扩容
- 集合可以保存任意对象
- 集合提供了一系列增删改查的方法,方便人们的使用
Java集合框架的开始
java最初的版本只为了最常用的数据结构提供了很少的一组类
Vector、Stack、Hashtable、BitSet与Enumeration接口
Enumeration接口提供了一种用于访问任意容器中的各个元素的抽象机制——《Java核心技术》
Enumeration接口源码
public interface Enumeration<E> {
/**
* Tests if this enumeration contains more elements.
*
* @return <code>true</code> if and only if this enumeration object
* contains at least one more element to provide;
* <code>false</code> otherwise.
*/
boolean hasMoreElements(); //用来判断集合里是否还有元素,有则返回true,无则返回false
/**
* Returns the next element of this enumeration if this enumeration
* object has at least one more element to provide.
*
* @return the next element of this enumeration.
* @exception NoSuchElementException if no more elements exist.
*/
E nextElement();// 返回集合的下一个元素,如果无元素返回则抛出:NoTouchElementException异常
}
在Java1.2后设计者推出了一组功能完善的数据结构类库—Java集合框架
可见Java集合容器的创建的就是为了推出了一组功能完善的数据结构类库用来给人们使用,存放数据。所以在理解集合容器中,数据结构是特别关健的一步。
集合接口和实现分离
和现代的数据结构结构类库常见做法一样,java集合类库也将接口与实现分离——《Java核心技术》
特点:只有在构造集合对象时,才会使用具体的类–可以使用接口类型来存放集合引用
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 只有在构造集合对象时,才会使用具体的类--可以使用接口类型来存放集合引用
List list = new ArrayList();
// 使用时看不到具体的实现类
list.add("learn");
}
好处:一旦想使用其他的实现,只要改变构造集合对象时使用的集合引用就可以了
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 只有在构造集合对象时,才会使用具体的类--可以使用接口类型来存放集合引用
//List list = new ArrayList(); -> List list = new LinkedList(); 改变构造集合对象时使用的集合引用
List list = new LinkedList();
// 使用时看不到具体的实现类
list.add("ArrayList");
// 改变构造集合对象时使用的集合引用
list.add("LinkedList");
}
Java集合学习的正式开始
集合常见的接口及实现类(记住)
Collection接口
集合类的基本接口是collection接口(Map接口也是),且它是单列集合——(存放一个而不是一对元素)
两个基本方法
boolean add (E element) // 向集合添加元素。如果添加元素确实改变了集合就返回true;如果集合没有发生变化就返回false。
Iterator<E> iterator();// 返回一个迭代器。可以使用这个迭代器依次访问集合中的元素
iterator接口(迭代器)
iterator接口包含4个方法
E next(); // 反复调用next方法,可以逐个访问集合中的每一个元素。如果到达了集合的末尾则抛出一个NoSuchElementException异常。因此在调用next方法前先调用hasNext方法判断是否还存在元素
boolean hasNext();// hasNext方法判断是否还存在元素
void remove();// 删除上次调用next方法时返回的元素
default void forEachRemaining(Consumer<? super E?> action);// 可以实现这个方法来对每一个元素执行你提供的lambda表达式
从上面的描述中可以知道iterator接口中的方法
E next(); // 反复调用next方法,可以逐个访问集合中的每一个元素。如果到达了集合的末尾则抛出一个NoSuchElementException异常。因此在调用next方法前先调用hasNext方法判断是否还存在元素
boolean hasNext();// hasNext方法判断是否还存在元素
和Enumeration接口中的方法
E nextElement();// 返回集合的下一个元素,如果无元素返回则抛出:NoTouchElementException异常
boolean hasMoreElements(); //用来判断集合里是否还有元素,有则返回true,无则返回false
作用一样,但是Java集合框架的设计者不喜欢Enumeration接口里累赘的方法名所以引入了具有较短方法名的新接口iterator
遍历的方式
-
使用迭代器
-
使用增强for–foreach
使用foreach循环可以更加简练的表达同样的循环操作–编译器将其转成带有迭代器的循环
-
使用forEachRemaining方法
@SuppressWarnings({"all"})
public static void main(String[] args) {
Collection collection = new ArrayList();
// 添加一个字符串
boolean test = collection.add("LEARN");
System.out.print(test+"\t");
// 添加一个整形数据
boolean test_ = collection.add(1);
System.out.print(test_+"\t");
// 添加一个对象
boolean test_1 = collection.add(new Object());
System.out.println(test_1+"\t");
Iterator iterator = collection.iterator();
// 遍历的方式
// 第一种 使用迭代器
System.out.println("第一种 使用迭代器");
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("第二种 使用增强for--foreach");
// 第二种 使用增强for--foreach
for (Object tmp : collection) {
System.out.println(tmp);
}
// 第三种 使用forEachRemaining方法
System.out.println("第三种 使用forEachRemaining方法");
// 注意使用一次迭代器进行遍历后
// 如果想重新遍历一次集合就要重新创建迭代器
iterator = collection.iterator();
iterator.forEachRemaining(tmp-> System.out.println(tmp));
}
迭代器模型
Java集合类库中的迭代器与其他类库中的迭代器在概念上有很大的不同,在传统的集合库里,例如C++的标准模板库,迭代器是按数组索引建模的。如果给定这样一个迭代器,可以查找储存在指定位置上的元素,就像知道索引i就可以直接查找数组元素a[i]。不需要查找元素,也可以向前移动一个位置。这与不需要执行查找操作而通过i++将数组索引向前移动一样。但是Java迭代器并不是这样处理的。查找操作和位置变更紧密耦合。查找元素的唯一方法就是调用next方法,而在执行查找操作的同时,迭代器的位置就会随之向前移动。
因此可以认为Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过元素的引用 —《Java核心技术》
还可以把Iterator.next和InputStream.read看成等效的。InputStream.read读取一个字节就会消耗一个字节。下次调用就会消耗并返回输入的下一个字节。这和Iterator.next有异曲同工之妙。
再举一个例子
传统的集合库里的迭代器执行查找
就像外卖小哥送外买,小哥如果知道门牌号的话就可以直接找到顾客,不知道的话就只能一个一个的敲门了,如果他突然觉得有一个门不像点的起外卖的样子就可以不敲这个门而直接去下一个门口。
简单来说传统的集合库里的迭代器就是查找和位置变更关系不大
Java迭代器执行查找
就像闯关拿奖,你想拿到一个电冰箱,可是要闯过一关才可以知道并且得到这个奖品,并且只能闯完一关才可以前往下一关,那你只能一个一个的闯关,直到你拿到你想要的电冰箱
查找操作和位置变更紧密耦合。查找元素的唯一方法就是调用next方法,而在执行查找操作的同时,迭代器的位置就会随之向前移动。
List集合(列)
List集合是一个有序集合。元素会增加到指定位置。可以采用两种方式访问元素:使用迭代器进行访问,或者使用一个整数索引来访问。后面这种方法称之为随机访问,因为这样可以按任意顺序访问元素》与之不同,使用迭代器访问时,必须顺序的访问元素—《Java核心技术》
List集合的遍历
除了Collection接口的遍历方法,还可以使用普通的for循环进行遍历
public static void main(String[] args) {
// 使用无参构造器
List l = new ArrayList();
for (int i = 0; i < 5; i++) {
l.add("List" + i);
}
// 使用普通for循环进行遍历
for (int i = 0; i < l.size(); i++) {
System.out.println(l.get(i));
}
}
ArrayList集合
之前也说过集合框架的创建是为了提供一些数据结构类库给人们进行存放数据那么在ArrayList集合里则封装了一个动态再分配的对象数组。
来总结一下ArrayList的特点
- ArrayList底层维护了一个Object类型的数组
- ArrayList线程不安全(多线程不建议使用ArrayList)
- 如果选择无参构造器则,初始化数组大小为0(jdk7是10),第一次扩容为10,第二次则扩容为旧容量的1.5倍
- 如果选择有参构造器则,初始化数组大小为你提供的参数大小,之后扩容为旧容量的1.5倍
- ArrayList可以存放重复数据(可以加入null,并且不限定个数)
源码分析
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 使用无参构造器
List l = new ArrayList();
for (int i = 0; i < 5; i++) {
l.add("List" + i);
}
}
Vector类
Vector类也封装了一个对象数组
Vector类的特点
- Vector底层也封装了对象数组
- Vector线程安全(线程同步),Vector类的操作方法带有synchronized关键字
- Vector类使用无参构造器时默认数组大小为10,如果需要扩容则扩容为原来的两倍
- Vector类可以使用构造函数自定义数组初始大小和每次扩容大小
- 也可以存放重复数据(可以加入null,并且不限定个数)
我们也可以简单看一下它的源码
public static void main(String[] args) {
List list = new Vector();
list.add("test");
}
LinkList集合
LinkList的特点
- LinkList类底层封装的是一个双向链表。
- LinkList集合线程未同步(线程不安全)。
- LinkList也可以存放重复数据(可以加入null,并且不限定个数)。
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new LinkedList();
list.add("List");
list.add("List");
}
下面我来简单介绍一下具体的添加方法
总结一下List集合
List集合其实并不是特别难,底层使用的数据结构也不是特别困难,但是也有几个要点要记住
-
List集合的多种遍历方法
a. 使用迭代器
b.使用foreach循环
c. 使用普通for循环
d.使用forEachRemaining方法
-
List集合各个具体实现的扩容方法(重点)
ArrayList集合具体扩容方法
private void grow(int minCapacity) {// minCapacity 如果使用无参构造函数第一次扩容则为10 // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // oldCapacity + (oldCapacity >> 1) -> 1.5*oldCapacity if (newCapacity - minCapacity < 0) // 如果扩容还是不够就扩容到minCapacity大小 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0)// 如果大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)则扩容到Integer.MAX_VALUE newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
Vector集合具体扩容方法
private void grow(int minCapacity) {// minCapacity 如果使用无参构造函数第一次扩容则为10 // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);// capacityIncrement为自定义扩容大小,如果不定义则为0,并使用默认扩容大小(扩容为原来的两倍) if (newCapacity - minCapacity < 0) // 如果扩容还是不够就扩容到minCapacity大小 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0)// 如果大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)则扩容到Integer.MAX_VALUE newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
LinkList集合具体扩容方法
// 使用尾插法把新接点插入尾部,具体步骤可以看介绍LinkList时的图 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++; }
-
在什么情况下选择什么类型的具体实现类
在需要线程安全的情况下
建议使用Vector集合,因为它的所有方法都是同步的,都使用synchronized 关键字 加了一个显式锁
在不需要保证线程安全的情况下,又要进行改查的操作比较多
建议使用ArrayList集合,因为它底层是一个数组方便进行查询
在不需要保证线程安全的情况下,又要进行增删的操作比较多
建议使用LinkList集合,因为它底层是一个双向链表方便进行增加和删除节点
Set集合(集)
从现在开始,集合的难度提升了一截因为,Set集合的具体实现类所封装的数据结构要复杂一点。
Set接口等同于Collection接口,不过其方法的行为有更加严谨的定义。集的add方法不允许增加重复的元素。要适当的定义集的equals方法:只有两个集包含同样的元素就认为他们是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同的元素的集会得到相同的散列码–《Java核心技术》
Set集合不能使用普通for进行遍历,因为他是无序的。
Hashset集合
HashSet的特点
- HashSet底层封装的是一个散列表(数组+链表+红黑树)
- HashSet不能存放相同的元素,可以添加一个null
- HashSet存放元素的顺序和取出对象的顺序不一样
- HashSet取出对象的顺序固定
- HashSet实际上是一个HashMap,存放的元素作为k值,使用一个对象统一作为v值
散列表模型
源码分析
@SuppressWarnings({"all"})
public static void main(String[] args) {
Set set = new HashSet();
set.add("HashSet");
set.add(new Object());
}
由于putVal方法十分复杂所以特别把它提出来讲
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) // table数组在第一次使用时也就是刚刚初始化后为null,进行第一次扩容数组,table为存放记录的数组
n = (tab = resize()).length; // resize为扩容函数
if ((p = tab[i = (n - 1) & hash]) == null) // 计算出存放位置的索引,i = (n - 1) & hash 相当于 hash %(i=(n-1)) ,如果为空就意味着未被填充直接把元素放到该位置上的数组里
tab[i] = newNode(hash, key, value, null);
else {// 如果不为空就意味着该位置已经被填充
Node<K,V> e; K k;
// 把该位置上的元素和需要放入数组的元素进行比较,如果相同就使用局部变量e来存放该位置节点上的元素
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) {
// 如果移到链表末尾也没发现一样的,就插入元素到末尾(1.7之前是使用头插法,1.8后使用尾插法)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果相等的话直接退出,此时e为与传入元素相等的元素
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为空说明存在重复元素,进行值的替换,返回替换的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改一次后加一
++modCount;
// 如果存放在集合里的元素大于存放元素阀值(为当前最大存放数*装填因子(默认为0.75)),进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize为扩容函数
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 使用临时变量oldTab存放原来的数组地址
int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap如果为null,则为0,负责则为原来的数组长度
int oldThr = threshold;// oldThr存放原来的储存阀值
int newCap, newThr = 0;
if (oldCap > 0) { // 如果原来的oldCap大于0
if (oldCap >= MAXIMUM_CAPACITY) { // 如果原来的oldCap大于MAXIMUM_CAPACITY (1 << 30)
threshold = Integer.MAX_VALUE; // 使储存阀值为 MAX_VALUE (0x7fffffff)
return oldTab; // 返回旧数组地址
}
// 如果原来的oldCap 乘 2小于 MAXIMUM_CAPACITY 并且 原来的oldCap大于等于DEFAULT_INITIAL_CAPACITY (16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
// 使新阀值为旧阀值的两倍
newThr = oldThr << 1; // double threshold
}
// 如果oldCap = 0 ,且 oldThr > 0,说明自定义了阀值 ,则把初始容量设置为阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 如果使用无参构造器后,第一次扩容就 把数组大小扩容为 16 ,阀值设置为 16*0.75 = 12
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;
@SuppressWarnings({"rawtypes","unchecked"})
// 扩容后,按扩容后大小定义一个newTab并且把其地址交给table
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;
}
}
}
}
}
// 符合新建数组地址
return newTab;
}
LinkHashSet集合
LinkHashSet集合的特点
-
LinkHashSet是HashSet的子类
-
LinkHashSet与HashSet的不同就是,他在原来散列表的基础上做了一点改变(数组+双向链表)
-
LinkHashSet通过HashCode值来确定保存元素位置,但是由于增加了一个双向链表,所以维护了数组的插入顺序
-
LinkHashSet是有序的
-
LinkHashSet不允许存放重复元素
-
LinkHashSet底层是LinkHashMap
数据结构模型
源码分析
public class LinkHashSet_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Set set = new LinkedHashSet();
set.add("LHS");
}
}
LinkHashSet集合的增加方法和HashSet的增加方法基本一致只是重载了一些方法
例如
if ((p = tab[i = (n - 1) & hash]) == null) // 计算出存放位置的索引,i = (n - 1) & hash 相当于 hash %(i=(n-1)) ,如果为空就意味着未被填充直接把元素放到该位置上的数组里
/*****/
tab[i] = newNode(hash, key, value, null);
/*****/
/**************************************************************************************/
for (int binCount = 0; ; ++binCount) {
// 如果移到链表末尾也没发现一样的,就插入元素到末尾(1.7之前是使用头插法,1.8后使用尾插法)
if ((e = p.next) == null) {
/*****/
p.next = newNode(hash, key, value, null);
/*****/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果相等的话直接退出,此时e为与传入元素相等的元素
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { // newNode返回的是一个LinkedHashMap.Entry对象
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p); // 执行插入数据
return p;
}
TreeSet集合(树集)
TreeSet集合的特点
- TreeSet的集合是一个有序集合,可以以任意顺序插入到集合里去
- TreeSet集合如果使用无参构造器,则按默认升序排序
- TreeSet底层使用的是一个红黑树
- TreeSet集合提供了一个构造器,可以传入一个比较器,来指定一个排序方法
Set set = new TreeSet(new Comparator() {
// 指定比较方法
// 如((String)o1).compareTo((String) o2) 比较字符串大小
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String) o2);
}
});
- TreeSet底层是TreeMap
源码分析
@SuppressWarnings({"all"})
public static void main(String[] args) {
// Set set = new TreeSet();
Set set = new TreeSet(new Comparator() {
// 指定比较方法
// 如((String)o1).compareTo((String) o2) 比较字符串大小
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String) o2);
}
});
set.add("cdfde");
set.add("cdfd");
set.add("cdf");
set.add("cd");
}
put方法源码分析
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) { // 第一次添加
compare(key, key); // type (and possibly null) check 检查类型是否为null
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; // 把我们传入比较器给 cpr
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
// 等于0则说明key值(或者对应的key类型)已经存在,则修改key对应的value(TreeSet里则为不存入key值)
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;
}
总结一下Set集合
- Set集合的遍历方式
a.使用迭代器遍历
b.使用增强for遍历
**c.使用forEachRemaining方法 **
@SuppressWarnings({"all"})
public static void main(String[] args) {
Set set = new HashSet();
set.add("HashSet");
set.add(new Object());
set.add("a");
set.add("bd");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("=========================");
for (Object next:
set) {
System.out.println(next);
}
System.out.println("=========================");
Iterator iterator = set.iterator();
iterator.forEachRemaining(next ->System.out.println(next));
}
-
HashSet的扩容方法
为了尽量少的避免哈希碰撞,设置了装载因子(默认为0.75),第一次扩容数组为16,装载阀值为 16 * 0.75 = 12, 所以当存放元素总数到达阀值时,数组扩容为两倍且进行再散列,如果数组的一个位置链表大小为8且数组大小为64,则进行树化该链表来存放元素。
-
HashSet和LinkHashSet以及TreeSet的区别
HashSet是无序的,它存放元素是按照计算他们的散列码来存放
LinkHashSet在HashSet的基础上使用了双向链表,来维持元素的插入顺序,看起来像是按顺序插入的
TreeSet是一个有序集合,它可以按照比较器来存放相关的元素
-
Set集合和Map有千丝万缕的关系
HashSet和LinkHashSet以及TreeSet的底层都是对应Map的具体集合实现类
Map集合(映射)
和collection接口一样,map接口也是集合的基本接口,但是map是双列集合,说明存放的元素为 k-y 形式,之前的Set集合(集),可以让你快速的查找现有的元素,但是需要你提供一个和所寻找的元素相同的副本,这无疑是不现实的事。在更多的情况下我们需要的是可以提供一个关键信息来寻找对应的元素,就像查字典通过首字母来查找对应的页数。
Map 集合常用集合实现类
HashMap集合
HashMap集合的特点
- HashMap的底层维护的是一个散列表
- 创建对象会把装载因子初始化为0.75
- 第一次扩容为16,之后扩容为原来的两倍
- Map集合不能重复key值
- Map 集合可以重复value值
之前也说过HashSet集合的底层其实就是HashMap所以,HashMap集合底层的数据结构和源码分析和HashSet是大径相同的。
public static void main(String[] args) {
Map map = new HashMap();
map.put("key","vaule");
}
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) // table数组在第一次使用时也就是刚刚初始化后为null,进行第一次扩容数组,table为存放记录的数组
n = (tab = resize()).length; // resize为扩容函数
if ((p = tab[i = (n - 1) & hash]) == null) // 计算出存放位置的索引,i = (n - 1) & hash 相当于 hash %(i=(n-1)) ,如果为空就意味着未被填充直接把元素放到该位置上的数组里
tab[i] = newNode(hash, key, value, null);
else {// 如果不为空就意味着该位置已经被填充
Node<K,V> e; K k;
// 把该位置上的元素和需要放入数组的元素进行比较,如果相同就使用局部变量e来存放该位置节点上的元素
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) {
// 如果移到链表末尾也没发现一样的,就插入元素到末尾(1.7之前是使用头插法,1.8后使用尾插法)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果相等的话直接退出,此时e为与传入元素相等的元素
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为空说明存在重复元素,进行值的替换,返回替换的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改一次后加一
++modCount;
// 如果存放在集合里的元素大于存放元素阀值(为当前最大存放数*装填因子(默认为0.75)),进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
LinkHashMap集合
LinkHashMap特点
-
是HashMap的子集
-
在散列表的基础上使用了双向链表维护了插入顺序
-
维护了数组的插入顺序
-
不允许存放key值相同的元素
源码分析
public static void main(String[] args) {
Map map = new LinkedHashMap();
map.put("key","value");
}
TreeMap集合
TreeMap集合特点
- 是一个有序集合,可以以任意顺序插入到集合里去
- 如果使用无参构造器,则按默认升序排序
- 使用的是一个红黑树(平衡二叉树)
- 提供了一个构造器,可以传入一个比较器,来指定一个排序方法
源码分析
public static void main(String[] args) {
Map map = new TreeMap();
map.put("cd","123");
}
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) { // 第一次添加
compare(key, key); // type (and possibly null) check 检查类型是否为null
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; // 把我们传入比较器给 cpr
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
// 等于0则说明key值(或者对应的key类型)已经存在,则修改key对应的value(TreeSet里则为不存入key值)
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集合
HashTable集合特点
-
存放的元素是键值对
-
hashtable的建和值都不可以为null
-
它是线程安全的
-
HashTable底层是一个Entry[]数组,使用无参构造器时默认大小为11,装载因子为0.75。
-
按照 原来大小乘二加一扩容
源码分析
public static void main(String[] args) {
Map map = new Hashtable();
map.put("key",123);
}
public Hashtable(int initialCapacity, float loadFactor) {// initialCapacity 初始数组大小, loadFactor 装载因子
if (initialCapacity < 0) // 如果数组大小为负数 抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 如果loadFactor 装载因子为非正数或者为非数抛出异常
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)// 如果数组大小为0则使其等于一
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity]; // 创建数组
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); // 设置装载阀值
}
public synchronized V put(K key, V value) { // synchronized关键字,保证方法同步
// Make sure the value is not null
if (value == null) { // value为空则抛出空指针异常
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; // 计算散列码来作为索引
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) { // 查看是否有重复的元素
V old = entry.value; // 有则替换value值
entry.value = value;
return old;// 返回旧value值
}
}
addEntry(hash, key, value, index); // 执行添加元素方法
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++; // 集合修改次数加一
Entry<?,?> tab[] = table;
if (count >= threshold) { // 存在集合元素数大于阀值
// Rehash the table if the threshold is exceeded
rehash(); // 扩容
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e); // 添加元素
count++; // 集合元素数加一
}
protected void rehash() { // 扩容集合方法
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1; // 扩容为原来的两倍加一
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {// 再散列,重新存放元素
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
Map集合总结
- Map集合是双列集合 (存放元素为key-value)
- Map集合中Key值不可以重复,Value值可以重复
- Map集合中存在一对一关系(Key对应一个Value)
- Map集合的遍历方式【**************】
Map集合的遍历方式(重点)
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap();
map.put("key","value");
map.put("key","123");
map.put("key","aaaa");
// 第一类方法 通过使用Key集合来取出对应的value值
Set set = map.keySet();
// 因为使用的是Set集合所以Set集合的遍历方法都可以使用来取出key值
// 迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(map.get(next));
}
// foreach
for (Object o :set) {
System.out.println(map.get(o));
}
// 第二类方法 通过value集合来遍历
Collection values = map.values();
// Collection 接口的遍历方法都可以使用
// 迭代器 \ foreach
// 第三类 通过EntrySet 来获取 (重点)
Set entrySet = map.entrySet();
// 迭代器
Iterator entrySetIterator = entrySet.iterator();
while (entrySetIterator.hasNext()) {
Map.Entry next = (Map.Entry) entrySetIterator.next();
System.out.println(next.getKey()+"-"+next.getValue());
}
// for each
for (Object o :entrySet) {
Map.Entry tmp = (Map.Entry) o;
System.out.println(tmp.getKey()+"-"+tmp.getValue());
}
}
后言
Java集合十分重要,我在这里也只是介绍了大概,还是需要好好的学习,在此推荐《Java核心技术》这本书,和B站韩顺平老师的Java合集视频https://www.bilibili.com/video/BV1YA411T76k,看完后相信会对你有一定的提高。我在看完后感觉受益匪浅。为了方便以后的复习所以整理了这些内容。如果有不足的地方,希望各位能够斧正。