Java中集合框架的实现遵循的是设计与实现分离,具体的实现方式是通过接口编程实现的;接口定义集合规范,具体的实现类通过不同的方式实现操作——比如Queue可以通过循环数组和链表两种方式实现,具体内容在后边详细介绍;
Collection接口
Java中集合类的基本接口是Collection接口:Collection主要规定了两个方法(其余方法在之后介绍):
public interface Collection<E>{
boolean add();
Interator interator<E>();
}
add()方法规定集合的增加操作,元素增加成功后返回true,失败后返回false(比如 Set中增加重复元素时会添加失败,或者在链表增加元素时分配内存失败都会导致add()方法返回false)
interator()方法返回一个迭代器(迭代器后边介绍)对象,这个迭代器对象用于一次返回集合中的元素;
Iterator迭代器
Interator迭代器用来依次访问集合中的元素,其中包含了四个主要的方法:
public interface Iterator<E>
{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer <? super E> action);
}
Iterator就是通过反复调用next()方法来完成对集合中元素的依次访问;但是需要注意的是在next ()方法调用之前必须嗲用hasNext()方法来进行判断,否在在集合末尾next()方法会抛出NoSuchElementException;
如果需要访问集合中的所有元素则为该集合请求一个Iterator对象,通过该迭代器在hasNext()返回false之前反复调用next()方法:
Collection<String> c=...;
Iterator <String> iter =c.iterator();
while(iter.hasNext())
{
String element=iter.next();
//dong something with element...
}
同时上面的循环操作可同样通过for each循环来完成:
for (String element : c){
do something with element;
}
这里的for each循环就是编译器级别的的对迭代器的支持——该循环可以支持任何实现了Interable接口的对象——
public interface Iterable<E>
{
Iterator <E> iterator();
}
可以看出Iterable接口中只有一个抽象方法iterator(),同时Collection接口扩展了Iterable接口,所以所有实现了Collection接口的类的对象都可以使用for each 循环;
使用迭代器访问集合中的元素的时候需要注意的是迭代器对于元素的访问顺序不做保证,只能保证能够通过迭代对集合中的元素遍历,访问顺序由集合的具体实现决定;(比如ArrayList顺序访问,HashMap随机访问);
需要注意的是:Java集合类库中的迭代器与其他类库中的迭代器的实现是不同的:Java集合类库中的迭代器访问元素只能通过next()方法来完成,next()方法查找该元素,返回该元素的引用并将迭代器的位置后移(这里感觉《Java核心技术卷》一书中说的不是很容易理解,所以这里写的是自己的理解,如有问题欢迎阅读的同学指出),但是例如C++中的迭代器是通过数组索引(即指针)来实现的,如果需要访问一个元素可以直接通过下标来完成——不需要查找操作(这里的查找的意思应该是从内存个中读取值)便可直接将迭代器的位置移到这里从而访问该下标位置对应的值;但是使用Java中的迭代器只能通过next()方法查找,next()方法需要上面提到的三步来完成迭代器的移动——查找操作与位置变更是相连的,(每次进行查找的时候都需要将迭代器的位置依次移动到需要查找的位置上;)——需要注意的是这里并不是Java语言的缺陷,反而是Java安全编程的一种体现,这样可以防止指针越界等问题;
迭代器可以理解为位于两个元素之间,当调用next()方法的时候,迭代器越过下一个元素,并返回刚才越过的元素的引用;——而不是理解为指向该元素的指针,如果是指针的话便可以直接随机访问而不是上述的依次访问;——这里只是形象的理解,以便于更好的理解迭代器的工作方式而不是迭代器真正的实现原理;
迭代器Interator中的remove()操作
Iterator中的remove方法会删除迭代器上次返回的元素的引用——这里说明remove是依赖于next()方法的——当删除某个元素的时候因该首先让迭代器越过这个元素,然后进行删除——即在调用remove()方法之前应该调用next()方法;
删除集合中的第一个元素:
Iterator <String> iter=collection.iterator();
iter.next();//跳过该元素
iter.remove();//然后删除该元素
当连续删除两个元素的时候(非第一个):
iter.remove();
iter.next();
iter.remove;
也即是说,在调用remove()方法删除该元素的时候必须首先调用next()方法越过该元素;
泛型实用方法
Collection接口中实现了许多通用的方法,用来实现对与集合的通用的操作:
public interface Collection <E>{
int size();
boolean isEmpty();
boolean contains(Object obj);
boolean containsAll(Collection <? extends E> from);
boolean equals(Object other);
boolean addAll(Collection <? extends E> from);
boolean remove(Object obj);
boolean removeAll(Collection<?>c);
Object[] toArray();
void clear();
}
具体的集合
现在说一说Java类库中的具体的集合的实现类有哪些及其使用方法;具体的集合类有以下这些:
ArrayList //可以动态增长和缩减的索引序列
LinkedList //可以在任何位置高效增加或删除的有序序列
ArrayDeque //用循环数组实现的双端队列
HashSet //无序集合,实用hash实现,查找快
TreeSet //有序集
EnumSet //包含枚举类型值的集合
LinkedHashSet //可以记住 元素插入次序
PriorityQueue //删除最小元素优先队列
HashMap //键/值对存储
TreeMap //键值有序排列
EnumMap //键值属于枚举类型
LinkedHashMap //可以记住键值添加次序
WeakHashMap //其值在无用时可被垃圾回收器回收
IdentityHashMap //用==而不是equals比较键值的映射表
以上所有的集合类中,以Map结尾的类实现了Map接口,其他的均实现了Collection接口;
-
LinkedList(链表)
Java中提供了双向链表——LinkedList,LikedList实现了ListIterator接口:
interface ListIterator<E> extends Iterator<E>
{
void add(E element);
E previous();
booleean hasPrevious();
}
这里的previous()方法与next()方法一样,返回越过的元素——但是这里需要注意的是previous()方法使迭代器向前走,而next()方法使迭代器向后走,因而在同一位置上调用这两个方法返回的元素不是同一个;previous()方法返回的是上次调用next()返回的元素;
LinkedList类中的listIterator方法返回一个实现了ListIterator接口的迭代器对象,该对象的add()方法用于在迭代器位置之前添加一个元素: (LinkedList中添加元素有两种方法:LinkedList.add(将元素添加在链表结尾),Iterator的子接口ListIterator中的add方法(通过迭代器将元素添加在指定位置),且该方法不反悔boolean值,即默认每次操作都会添加成功;)
List <String> linkedlist = new LinkedList<> ();
//使用LinkedList类中的add()方法向链表末尾添加元素;
linkedlist.add("Amy");
linkedlist.add("Bob");
linkedlist.add("Hu");
//使用ListInterator中的add()方法在指定位置添加元素;
ListIterator <String> Liter=linkedlist.listIterator();//用该方法返回迭代器对象
iter.next();//使用迭代器跳过第一个元素,
iter.add("Yu")//使用迭代器的add()方法在第一个元素后边的位置添加新的元素;
打印链表:
Amy Yu Bob Hu
对于迭代器中的add()方法:
- 多次调用将按照调用次序在迭代器位置前依次添加新的元素
- 调用新的使用listIterator()方法返回的并指向链表头迭代器的add()方法的时候,将在链表头添加新的元素;
- 当迭代器的hasNext()方法返回false,调用add()方法将在链表末尾添加元素(同LinkedList的add()方法)
与add()类似的还有remove()方法;需要注意的是remove()方法删除的是迭代器越过的对象,这点在调用next()和previous()方法之后的结果不同;set方法将替换调用next()方法和previous()方法返回的对象;
安全性
LinkedList是线程不安全的,也就是说可以有多个线程同时对链表进行操作;比如同时给链表声明两个迭代器,当一个迭代器遍历链表的时候另一个迭代器对链表进行了修改,就会出现问题,迭代器的实现中对于这样的情况能够检测到并且抛出异常——称为并发修改异常,基本原理就是一个集合的迭代器都会各自维护一个修改次数的记录,迭代器的方法在执行之前都会拿自己维护的修改次数和集合本省维护的值做比较,当两个值不同时便会抛出该异常;解决办法:
- 使用(同一个)迭代器访问集合,并使用该迭代器修改集合
- 使用集合自身的方法修改集合,使用集合本省的方法访问集合;
- 使用多个迭代器访问集合时一定要保证修改操作(增加或者删除)是同步进行的;
这里需要注意的是并发修改异常只会被结构性修改(增加或者删除链表节点)触发,使用多个迭代器的set()方法修改节点的值不会触发该异常;
相关接口和实现类中的方法——
java.util.List<E>
ListIterator<E> listIterator()
//返回一个列表迭代器,用来访问列表中的元素
ListIterator<E> listIterator(int index);
void add(int i, E element);
//在指定位置添加元素
void addAll(int i, Collection<? extends E> elements);
//将整个集合的元素添加到指定位置;
E remove (int index);
//删除指定位置的元素并返回该元素;
E get(int index);
//返回指定位置的元素
E set(int index, E element);
//用新元素替代指定位置上的原来的元素,并返回原来的元素;
int indexOf(Object element);
//返回指定元素的索引(第一次出现),失败返回-1;
int lastIndexOf(Object element);
//返回指定元素在列表中最后一次出现的位置,失败则返回-1;
java.util.ListIterator<E>
void add(E newElement);
//在迭代器当前位置前添加一个新元素
void set(E newElement);
//用新元素替代next()或previous()方法上次访问返回的元素;
boolean hasPrevious();
E previous();
int nextIndex();
//返回下一次调用next()方法时将要返回的元素的索引;
int previousIndex()
java.util.LinkedList
LinkedList();
//构造一个空链表
LinkedList(Collection <? extends E> elements);
//构造一个链表,并将参数集合中的元素添加到链表中;
void addFirst(E element);
//将指定元素添加在链表头部
void addLast(E element);
//
E removeFirst();
//删除链表头元素并返回;
E removeLast();
-
ArrayList(数组列表)
使用get(int index)和set(int index)方法随机访问集合中的元素——通过索引或者下标
-
HashSet(散列集)
快速获取特定对象——contains(obj)\get(E element)——通过元素本身来访问;
散列表为每个对象产生一个散列值,散列码是由对象的实例域产生的一个整数——也就是说具有不同数据域的对象将会产生不同的散列值;当自定义类的时候要保证该类的hashCode()方法与该equals()方法兼容;也就是说如果A.equals(B)==true,则A对象和B对象的散列码应该相同;
Java中的散列表用链表数组实现——即链接散列法;每个链表称为桶,存储时将散列码与桶的总数取余,然后得到桶的索引,插入时如果桶满,则需要将新对象与桶中已有的对象进行比较,看该对象是否已经存在桶中——
JavaSE8中,如果桶满时,将会从链表变成平衡二叉树,以方便在产生过多冲突时提高散列表的性能;
一般来说,桶数应该为元素个数的75%-150%;且最好将桶数设置为一个素数;
再散列
当元素过多时,原来的散列表无法进行高性能散列的时候就需要重新为这些元素建立散列表;何时需要再散列是由散列表的装载因子决定的;装载因子是指散列表的已存储量对总存储量的占比;一般来说装载因子设置为0.75;
-
TreeSet(树集)
红黑树实现的排序集合,将插入的元素进行排序,在遍历集合的时候将按排序后的顺序返回;(使用树集的元素必须能够进行排序——这些元素必须实现Comparable接口)
-
队列与双端队列(ArrayDeque)
在头部和尾部同时添加或者删除元素;且队列长度可动态增长;
-
PriorityQueue(优先队列)
优先队列使用堆来实现;任务调动中一般使用优先级队列;
映射Map
映射用来存放键值对;Java类库中实现了两个映射集合——hashMaP和treeMap;
基本映射操作
散列映射和树的排序映射都是对于键来说的,对键所关联的值无法产生影响;一般来说,散列比树排序的速度要高,但是树可以保证插入的元素是有序存放的,而散列是随机存放的;根据需求选取相应的数据结构即可;
映射中的键必须是唯一的,同一个键不能映射两个值,如果对同一个键插入调用put()插入两个不同的值,则第二个值会覆盖第一个值,而同时put()方法会返回第一个值;
Map中常用的方法:
java.util.Map<K,V>
V get(Object K);
//获取与键对应的值;对象不存在的时候返回null;键值可以为null
V put(K, value);
//将键与对应的值插入到map中;若键对应的值已经存在,则更新值且返回旧值;当键为新键的时候返回
null;(该方法的键可以为null;但是值不可以为null)
default/V getORrDefault(K,defaultValue);
//获得与键关联的值,若键不存在,则返回defaultValue;此方法可以用于更新值;
boolean containsKey( k);
//判断map中是否存在该键;
boolean containsValue(V);
//判断map中是否存在该值对象;
default void forEach();
//对map中所有的键值对进行遍历访问;
更新映射项
更新映射项就是更新map中某个键对应的value的值;一般情况下,获取键对应的原值,跟新value即可——
V put( K, newValue);
但是这里存在一个特殊情况,就是需要更新的键不存在——即该键第一次出现,这时需要将新的键值对插入map中——举个例子:统计一个文本中的每个单词出现的次数;我们可以根据map提供的方法通过较为繁杂的方法来实现:每次统计之前判断containsKey(word);如果是第一次出现则put(word,1),否则将原来的word对应的value加一后更新该单词对应的次数value中,——
Map<String> map=new Map<String>();
word=fiel.firstWord;
while(file isnot null){
if (map.containsKey(word))map.put( word, get(word)+1);
else(put(word,1));
word=nextword;
}
//因为put(K,V)方法当K不存在的时候会返回null;所以必须先进行判断。
上述方法中每次都需要调用containsKey()方法;且代码较为繁杂,我们可以使用getOrDefault()方法来进行操作——
put(word, getOrDefault( word , 0)+1 );
映射视图
视图是map以集合的形式展示,有三种视图:
KeySet(键集)、ValueCollection(值集合)、Key/ValueSet(键值对集);这三种视图分别可以通过以下方法返回:
Set <E> keys=map.SetKey();//返回键集
Collection <E> coll=map.values();//返回值集合
Set <Map.Entry<K,V>> entrySet();//返回
WeakHashMap
当对键的唯一引用来自散列条目时,WeakHashMap将和垃圾回收器删除该键值对;
内部原理:
WeakHashMap使用弱引用(WeakReference)对象保存散列键,对于当WeakReference对象没有其他引用时(而不用管这个若引用有没有对应的value),就将该对象添加到ReferenceQueue中,然后WeakHashMap定期检查ReferenceQueue,和自身用来存储键值对的数组table进行对比,将ReferenceQueue中已经存在的WeakReference删除;
LinkedHashSet与LinkedHashMap