本文主要介绍常用的集合类,集合类主要用来保存数据,主要由Collection和Map接口派生而出。
对于如何选择哪种集合类来存储,这里有一篇详细的文章:
https://blog.csdn.net/zhangqunshuai/article/details/80660974
Iterator
Iterator主要用来遍历Collection中的元素,定义了下面几个方法:
1、boolean hasNext() 如果被迭代的集合元素还没有被遍历完,则返回true
2、 Object next() 返回集合里的下一个元素
3、void remove() 删除集合里上一个next方法返回的元素
LinkedList实现的就是链表,下面的程序创建了两个链表,将他们合并在一起,然后从第二个链表中每隔一个元素删除一个元素,最后测试removeAll方法:
package dajiang;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
public class LinkedListTest {
public static void main(String[] args)
{
List<String> a=new LinkedList<>();
a.add("Bob");
a.add("Car1");
a.add("Erica");
List<String> b=new LinkedList<>();
b.add("Amy");
b.add("Doug");
b.add("France");
b.add("Gloria");
//merge the words from b into a
ListIterator<String> aIter =a.listIterator();
Iterator<String> bIter=b.iterator();
while(bIter.hasNext())
{
if(aIter.hasNext())
aIter.next();
aIter.add(bIter.next());
}
System.out.println(a);
bIter=b.iterator();
while(bIter.hasNext())
{
bIter.next();
if(bIter.hasNext())
{
bIter.next();
bIter.remove();
}
}
System.out.println(b);
a.removeAll(b);
System.out.println(a);
}
}
使用迭代器就非常容易地对链表进行各种操作辣~但是需要注意以下的点:
1、迭代器是快速失败机制,如果在迭代过程中检测到集合被修改(通常是程序中的其他线程修改),程序立即引发Concurrent ModificationException异常,这样可以避免共享资源而引发的潜在问题。
如果某个迭代器在修改集合时,另一个迭代器对其进行遍历,遍历的时候就会抛出异常。为了避免发生并发修改的异常,可以根据需要给容器附加许多迭代器,但是这些迭代器只能读取列表,另外,再单独附加一个又能读又能写的迭代器。
有一个方法可以检测到并发修改的问题:集合可以跟踪改写规则的次数,每个迭代器都维护一个独立的计数值,再每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作计数值一致,如果不一致,抛出Concurrent ModificationException异常。
2、在调用next方法前必须调用hasNext()方法判断,不然有可能会抛出异常。
3、remove() 删除集合里上一个next方法返回的元素,如果调用remove之前没有调用next方法是不合法的,必须先要调用next越过要删除的元素。(JAVA迭代器实际上是认为位于两个元素之间,当调用NEXT时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用)
Map(映射)集合
map常用方法:
1、get(Object key)
2、put(key,value)
3、boolean containsKey(Key)
4、boolean containsValue(Value)
映射用来存放键值对,如果提供了键。就能查找相应的值。这里介绍HashMap和TreeMap。
散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树,散列或者比较函数都只能作用于键。
散列的顺序会比树的顺序更快,如果不需要按照排列顺序访问键,就最好选择散列。
键是唯一的,如果对同一个键调用两次put方法,第二个值就会取代第一个值。
HashMap源码解析:
1、整体结构
数组+链表+红黑树,当链表的长度大于等于8时,链表会转化成为红黑树,当红黑树的大小小于等于6时,红黑树会转化成为链表。
hashmap允许null值,且线程不安全,我们可以自己在外部加锁;影响因子默认时0.75.
2、常见属性:
//初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶上的链表长度大于等于8时,链表转化成红黑树
static final int TREEIFY_THRESHOLD = 8;
//桶上的红黑树大小小于等于6时,红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//当数组容量大于 64 时,链表才会转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//记录迭代过程中 HashMap 结构是否发生变化,如果有变化,迭代时会 fail-fast
transient int modCount;
//HashMap 的实际大小,可能不准(因为当你拿到这个值的时候,可能又发生了变化)
transient int size;
//存放数据的数组
transient Node<K,V>[] table;
// 扩容的门槛,有两种情况
// 如果初始化时,给定数组大小的话,通过 tableSizeFor 方法计算,数组大小永远接近于 2 的幂次方,比如你给定初始化大小 19,实际上初始化大小为 32,为 2 的 5 次方。
// 如果是通过 resize 方法进行扩容,大小 = 数组容量 * 0.75
int threshold;
//链表的节点
static class Node<K,V> implements Map.Entry<K,V> {
//红黑树的节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
put:
链表的新增:
当链表的长度大于等于8且整个数组大小大于64时,此时链表会转化成为红黑树 ,当数组大小小于64时,只会触发扩容,不会转成红黑树
查找:
1、根据hash算法定位数组的索引位置,equals判断当前节点是否时我们需要寻找的key,是的话直接返回,不是的话往下
2、判断当前节点有无next节点,有的话判断是链表类型,还是红黑树类型
3、分别走链表和红黑树不同类型的查找方法。
Set集合
Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则会添加失败,返回false。
HashSet
hashset具有以下特点:
1、不能保证元素的排列顺序,顺序可能和添加顺序不同
2、不是同步的,多线程不安全
3、集合元素值可以是 null
当向HashSet集合中存入一个元素时,HashSet会调用该对象的Hashcode()方法得到该对象的hashcode值,根据这个值来决定对象在Hashset中的存储位置。Hashset集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashcode()方法返回值也相同。
package dajiang;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class SetTest {
public static void main(String[] args)
{
Set<String> words=new HashSet<>();
long totaltime=0;
for(int i=0;i<5;i++)
{
Scanner in =new Scanner(System.in);
String word=in.next();
long callTime=System.currentTimeMillis();
words.add(word);
callTime=System.currentTimeMillis()-callTime;
totaltime+=callTime;
}
Iterator<String> iter=words.iterator();
for(int i=0;i<5&&iter.hasNext();i++)
{
System.out.println(iter.next());
}
System.out.println("...");
System.out.println(words.size()+"distinct word"+totaltime+"millisecond");
}
}
输出结果:
TreeSet
树集和散列集非常类似,但是树集是一个有序的集合,可以以任意顺序将元素插入到集合中,在对集合进行遍历的时候,每个值将自动的按照排序后的顺序呈现。里面的数据结构使用的是红黑树,每次将一个元素添加到树中时,都会被放置在正确的排序位置上,因此,迭代器总是以排好序的方式访问每一个元素。
因为底层使用的是红黑树,所以containKey,get,put,remove等方法的时间复杂度都是log(n)
如果外部有传进来的Comparator比较器,首先用外部的,如果外部比较器为空,则使用key自己实现Comparable compareTo方法。
新增节点:
1、判断红黑树的节点是否为空,为空的话,新增节点为根节点
Entry<K,V> t = root;
//红黑树根节点为空,直接新建
if (t == null) {
// compare 方法限制了 key 不能为 null
compare(key, key); // type (and possibly null) check
// 成为根节点
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2、根据红黑树左小右大的特性,进行判断,找到应该新增的父结点
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//自旋找到 key 应该新增的位置,就是应该挂载那个节点的头上
do {
//一次循环结束时,parent 就是上次比过的对象
parent = t;
// 通过 compare 来比较 key 的大小
cmp = cpr.compare(key, t.key);
//key 小于 t,把 t 左边的值赋予 t,因为红黑树左边的值比较小,循环再比
if (cmp < 0)
t = t.left;
//key 大于 t,把 t 右边的值赋予 t,因为红黑树右边的值比较大,循环再比
else if (cmp > 0)
t = t.right;
//如果相等的话,直接覆盖原值
else
return t.setValue(value);
// t 为空,说明已经到叶子节点了
} while (t != null);
}
3、在父结点的左边或者右边插入新增节点
//cmp 代表最后一次对比的大小,小于 0 ,代表 e 在上一节点的左边
if (cmp < 0)
parent.left = e;
//cmp 代表最后一次对比的大小,大于 0 ,代表 e 在上一节点的右边,相等的情况第二步已经处理了。
else
parent.right = e;
4、着色旋转,达到平衡,结束
Treeset的特性:
1、查找过程中发现key已经存在,直接覆盖
2、TreeMap是禁止key是null值得。