集合框架结构图
你要把握集合的两个大的方向,就是Collection和Map
补充一下:HashTable也实现了Map接口。
补充一下:Vector也是在List下面的。
List
List 集合是有序的,并且可以插入重复的值,也可以为空值。是否允许为空具体还要看子类的实现。
Vector
底层数据结构是通过数组来维护的,它的操作是线程安全,你可以自己查看源码,它的操作都添加了synchronized关键字
ArrayList
它实现了List,有序的集合,并且它允许添加空值元素。它的底层是通过数组来实现,可以实现可变数组长度,它比较类似于 Vector 类,除了此类是不同步的。
ArrayList是线程不安全的,如果有多个线程操作,一定要有同步操作。在集合中,也给出了集合同步操作。
Collections.synchronizedList//该方法可以返回一个同步的ArrayList。
ArrayList在实现可变长度,实际上有一个加载因子系数,它会自动增长,你创建时,你可以给一个初始容量,这个初始容量,应该大于等于你的元素个数,另外,集合中元素个数超过容量,会按照加载因子扩充你的容量。
ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间
ListIterator
它是继承于Iterator,是它的增强版,它是双向迭代器,允许程序员按任一方向遍历列表。并且在迭代过程中,你可以进行插入、删除,它不会进行报错,这点和之前Iterator不一样,修改之后,要在下一次迭代可以看到修改的结果。
ListIterator it = list.listIterator();
while (it.hasNext()) {
System.out.println(it.next());
}
while(it.hasPrevious()) {
System.out.println(it.previous());
}
remove方法理解
LinkList
LinkList 是实现List接口,可以允许添加的元素为空,可以添加重复元素,此类也实现了Deque ,这个是双端队列,这个数据结构即拥有队列特性、也拥有栈的特性。所以里面提供栈和队列的操作方法。
LinkList是通过一个双向链表来维护的。
在遍历的时候可以通过双向链表的特性从前往后、或者从后往前,因为继承List,它也是可以随机访问。
linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好
LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间,来存放指向下一个的指针。
LinkList 不是线程同步的。
LinkList 遍历
while(ll.size()!=0) {
System.out.println(ll.removeLast());
}
这个方法可以实现LinkList的从后遍历,对应的removeFirst是从前遍历。但是这个方法不太好的就是它的元素都删除了。
String a;
while((a = ll.pollLast())!=null) {
System.out.println(a);
}
这种方式和上面的类似,但是它是只读的,不修改数据,对应的还有pollFirst是从前往后遍历。
其他方式的遍历,迭代器都和之前的类似。在这就不多说了。
Set
set集合是不允许相同的元素,最多只能存放一个null。set是无序的。编译时候不会有任何问题,但是运行出来,会自动去除重复的
HashSet
该类是实现了set接口,它不保证set迭代的顺序是一直不变的。可以接受一个为null的值。
TreeSet
基于 TreeMap 的 NavigableSet 实现。使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序
常用方法
TreeSet<Person> ts = new TreeSet<Person>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
// TODO Auto-generated method stub
return o1.getAge()-o2.getAge();
}
});
Person p1 = new Person("laohe", 13);
Person p2 = new Person("laohe", 34);
Person p3 = new Person("laohe", 70);
Person p4 = new Person("laohe", 3);
ts.add(p1);
ts.add(p2);
ts.add(p3);
ts.add(p4);
/*
* 如果TreeSet中的元素是不可以比较,那么会报异常
* ClassCastException - if the specified object
* cannot be compared with the elements currently
* in this set
*/
Iterator<Person> it = ts.iterator();
while(it.hasNext()) {
System.out.println(it.next().getAge());
}
Person p5 = ts.ceiling(p4);//返回set大于给定元素的最小元素,如果不存在返回空
Person p6 = ts.floor(p3);//返回set中小于等于给定元素的最大元素,如果不存在返回空
System.out.println(p6.getAge());
System.out.println(p5.getAge());
Set set=ts.descendingSet();//返回该TreeSet的逆序视图
Iterator<Person> it1 = set.iterator();
while(it1.hasNext()) {
System.out.println(it1.next().getAge());
}
SortedSet<Person> ss = ts.headSet(p3);//返回此 set 的部分视图,其元素严格小于 toElement
SortedSet<Person> ss1 = ts.tailSet(p4);//返回此 set 的部分视图,其元素大于等于 fromElement。
Iterator<Person> it2 = ss.iterator();
while(it2.hasNext()) {
System.out.println(it2.next().getAge());
}
Person p7 = ts.pollFirst();//获取并移除第一个(最低)元素;如果此 set 为空,则返回 null。
Person p8 = ts.pollLast();// 获取并移除最后一个(最高)元素;如果此 set 为空,则返回 null
Person p9 = ts.first();//获取Set的第一个元素
Person p10 = ts.last();//获取Set的最后一个元素
Map
将键映射到值的对象。map如果你的键是一样的话,在后面添加的,会覆盖前面的,所以map中,不存在两个一样的键。map中键和值都可以为空。
某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类
HashMap
hashmap中key是不可以相同的,但是值是可以相同的,不同的键可以有相同的值,但是相同的键,相同的值会覆盖,
hashmap 键值与null关系
hashmap中的key是可以为空
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key为null直接将hashcode取0。
对于value,肯定是可以为null的。这没有啥质疑的。
hashcode
提到HashMap,不得不说一下就是hashcode,hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的。
如果两个对象相同,就是适用于equals(java.lang.Object),object的方法是判断是不是同一个对象,那么这两个对象的hashCode一定要相同
如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面的。
两个对象的hashCode相同,并不一定表示两个对象就相同。
例如内存中有这样的位置
0 1 2 3 4 5 6 7
而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之如果不用hashcode而任意存放,那么当查找时就需要到这八个位置里挨个去找,或者用二分法一类的算法。
但如果用hashcode那就会使效率提高很多。
我们这个类中有个字段叫ID,那么我们就定义我们的hashcode为ID%8,然后把我们的类存放在取得得余数那个位置。比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。这样,以后在查找该类时就可以通过ID除 8求余数直接找到存放的位置了。
但是如果两个类有相同的hashcode怎么办那(我们假设上面的类的ID不是唯一的),例如9除以8和17除以8的余数都是1,那么这是不是合法的,回答是:可以这样。那么如何判断呢?在这个时候就需要定义 equals(这个equals是判断是否同一个对象)。
也就是说,我们先通过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过 equals 来在这个桶里找到我们要的类。
那么。重写了equals(),为什么还要重写hashCode()呢?
想想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不通过重写hashcode()来找到桶。
如果没有hash,你要查找一个东西,比如在ArrayList中,看是不是等于某一个值,有多少个元素,就要有多少次equals方法调用,而hash,先计算出它的hash值,在这个hash值的位置,看有个几个元素,在调用equals方法,这样省了不少事。
经过上面的介绍,你已经对hash算法有了了解,下面就来详细的介绍hashmap了。
这是hashmap的数据结构。
hashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
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;
}
每一个Entry(条目)就是数组中的元素,其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
这里为啥会出现链表,实际上,这是因为如果某两个对象计算出来的hash值相同,但是通过equals,判断不是同一个对象,所以我们会把他们放在一起,形成链表。
HashMap的实现原理:
- 利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中 - 获取时,直接找到hash值对应的下标,在进一步判断key是否相同对象(通过equals方法),从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比
hashmap的哈希冲突的解决方法
- 开放地址法
- 链地址法 (这时hashmap的解决办法)
- 二次哈希
- 建立一个公共溢出区
HashTable
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。键和值不可以为空
if (value == null) {
throw new NullPointerException();
}
//hashtable在存放数据的时候,就判断它的value是否为空。
int hash = key.hashCode();
可以看到hashtable存放是直接计算key的hashcode,如果为null,调用就会空指针异常,所以键也是不可以为空的。
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。
Hashtable 的实例有两个参数影响其性能:初始容量 和加载因子。通常,默认加载因子(.75)在时间和空间成本上寻求一种折衷。减少空间开销,必然查找速度就要慢。
//ht.put(null, "sdas");
//ht.put(1, null);
//在hashtable中是不允许将空键或者是空的值放入,否则会引起空指针异常。
HashTable遍历
Hashtable<Integer,String> ht = new Hashtable<>();
Iterator<Entry<Integer, String>> it = null;
ht.put(1, "one");
ht.put(2,"two");
ht.put(3,"three");
ht.put(4,"four");
Set<Entry<Integer, String>> set = ht.entrySet();
it= set.iterator();
while(it.hasNext()) {
Entry<Integer, String> entry = it.next();
if(entry.getKey()==2) {
it.remove();
}else {
System.out.println(entry.getValue());
}
ht.remove(2);
}
Set<Integer> keyset = ht.keySet();
Iterator<Integer> it1 = keyset.iterator();
while(it1.hasNext()) {
Integer name = it1.next();
System.out.println("key"+name+" value"+ht.get(name));
}
}
HashTable 方法
containsKey(Object key) 判断是否包含该key。
containsValue(Object value) 判断是否包含某个值。
在这里需要注意的是,在HashTable 很多方法中,都进行了同步操作,不需要你在另外操作。所以HashTable是线程安全的。
public synchronized boolean replace(K key, V oldValue, V newValue) {
fail-fast
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常。
ConcurrentModificationException
当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。 例如,某个线程在 Collection 上进行迭代时,迭代器的结构已经建立起来,通常不允许另一个线性修改该 Collection。
需要注意的是产生这个异常单线程中也可能产生。还有虽然他会fail-fast这个事件处理机制,但是对于抛出异常,它只是进全力去抛出,所以我们不能完全依赖此机制。
参考:https://baike.baidu.com/item/fail-fast/16329854?fr=aladdin
测试用例
上面已经说明java错误机制和异常,下面演示代码:
static Hashtable<Integer,String> ht = new Hashtable<>();
public static void main(String[] args) {
ht.put(1, "one");
ht.put(2,"two");
ht.put(3,"three");
ht.put(4,"four");
Set<Entry<Integer, String>> entry = ht.entrySet();
ConcurrentModificationException
Iterator it = entry.iterator();
Thread t = new Thread(new Test131().new MyTask());
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
while(it.hasNext()) {
System.out.println(it.next());
}
}
class MyTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
ht.remove(3);
}
}
分析及解决办法
看源码:
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
主要看两块,你会发现在迭代器执行next方法前,会先进行check,而在check中,主要判断modCount expectedModCount是否相等,不想等则抛出异常,原来是这样集合操作中,它都会记录集合中元素的数量,这点我看源码put、remove都涉及到modCount 值的变化,而在迭代器创建的时候,就已经执行赋值:
int expectedModCount = modCount;
这时,值都为0。比如,加入你集合添加了5个元素,这是你在创建好遍历对象的时候,expectedModCount = modCount = 5;
所以说,在迭代器创建好了,你在修改就会抛出异常,你的modCount 变化了,而expectedModCount 没有相应的变化。所以对于减少该异常,就有了一个解决办法,用迭代器自身的删除操作。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
你看它的源码,就多做了一部处理,即可解决这个问题。
解决方法
在删除元素的时候,可以采用迭代器的删除方法,这样就可以。
Hashtable<Integer,String> ht = new Hashtable<>();
Iterator<Entry<Integer, String>> it = null;
ht.put(1, "one");
ht.put(2,"two");
ht.put(3,"three");
ht.put(4,"four");
Set<Entry<Integer, String>> set = ht.entrySet();
it= set.iterator();
while(it.hasNext()) {
Entry<Integer, String> entry = it.next();
if(entry.getKey()==2) {
it.remove();
}else {
System.out.println(entry.getValue());
}
//ht.remove(2);产生异常
}
}
迭代器的注意点
hasNext() 方法是如果有下一个元素返回true。
next 方法是返回下一个元素值。
源码分析:
public E next() {
checkForComodification();//这个是检查,在迭代器开始遍历,有没有外部在修改元素。
int i = cursor;//这里cursor是游标的位置初始为0
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//将List转化为数组操作
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;//游标向后移动一位
return (E) elementData[lastRet = i];//返回下一个元素这里理解需要注意,因为它游标不是指向当前元素,而是前面的。
}
结合图理解:
理解完上面,下面你看这个问题:
System.out.println(it1.next());
System.out.println(it1.next());
//这个东西,对于新手还是要注意的,明明打印是两个一样的东西,为啥子就是不一样。原因就在上面执行完一次next(),游标cursor会加1,会变化,这两个值当然不一样,所以说,眼见的不一定为真,凡是还是要多看,多练。
Map.Entry (映射项)
我们在集合中常常见到这个,所以今天把这个拿出来说道说道。它是一个映射项,对应的就是键值对(键-值)。实现Map接口,Map.entrySet 方法返回映射的 collection 视图,然后遍历。
常用方法
获取对应的键和对应的值。
集合中线程不安全问题
在集合部分的api中,多处提到就是集合在多线程的操作下,会出现操作异常,而我们之前在多线程部分,知道用lock、synchronized关键字来解决。但是在集合部分,它有一个Collections类,给我们直接提供了包裹的同步集合操作。
上面只是截取几个常用的,还有很多自己可以参看api。
下面以HashSet为例,为大家演示集合在多线程操作下的问题。
用两个线程对同一个set集合进行操作,结束后返回set当中的size。在不同步的情况,返回size大于等于500,在同步情况下,返回500。
static Set set = new HashSet<>();
// static Set set = Collections.synchronizedSet(set1);
public static void main(String[] args) {
Test131 tt = new Test131();
MyTask task = tt.new MyTask();
Thread t = new Thread(task);
Thread t1 = new Thread(task);
t.start();
t1.start();
try {
t.join();
t1.join();//这两句代码,虽然和下面睡眠一样,都是等待线程任务执行完毕,但是采用方法是不同,主线程会等待子线程执行完,才执行。你也可以给线程时间,让它先一步一会,在同步。这样就形成main线程同步,但是还有两个线程操作。要学会这种方式。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
/*try {
Thread.sleep(5000);//在这里睡眠是为了让两个线程都执行好了,才打印数量。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
System.out.println(set.size());
}
class MyTask implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i =0;i<500;i++) {
set.add(i);
}
}
}