Java学习——Java集合(下)
文章目录
一、Iterator
Iterator 对象称为迭代器(设计模式的一种),迭代器可以对集合进行遍历,但每一个集合内部的数据结构可能是不尽相同的,所以每一个集合存和取都很可能是不一样的,虽然我们可以人为地在每一个类中定义 hasNext() 和 next() 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。
迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 hasNext()和next()方法
Iterator 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModificationException 异常。
可以使用如下的几种方式:
- Iterator 迭代输出(90%)、ListIterator(5%)、Enumeration(1%)、foreach(4%)
(1). Iterator
Iterator 属于迭代输出,基本的操作原理:是不断的判断是否有下一个元素,有的话,则直接输出。
此接口定义如下:
`public interface Iterator<E>`
要想使用此接口,则必须使用 Collection 接口,在 Collection 接口中规定了一个 iterator()方法,可以用于为 Iterator 接口进行实例化操作。
此接口规定了以下的三个方法:
通过 Collection 接口为其进行实例化之后,一定要记住,Iterator 中的操作指针是在第一条元素之上,当调用 next()方 法的时候,会返回迭代器的下一个元素,并且更新迭代器的状态(指针向下移动),使用 hasNext()可以检查序列中是否还有元素。
示例:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Demo1 {
public static void main(String[] args) {
Collection<String> all = new ArrayList<String>();
all.add("A");
all.add("B");
all.add("C");
Iterator<String> iter = all.iterator(); //定义ArrayLsit的迭代器
while (iter.hasNext()) { // 判断是否有下一个元素
String str = iter.next(); // 返回下一条内容,并更新迭代器状态
System.out.print(str + " ");
}
}
}
结果:
以上的操作是 Iterator 接口使用最多的形式,也是一个标准的输出形式。
但是在使用 Iterator 输出的时候有一点必须注意,在进行迭代输出的时候如果要想删除当前元素,则只能使用 Iterator 接口中的 remove()方法,而不能使用集合中的 remove()方法。否则将抛出异常ConcurrentModificationException 。
示例:
而我们注释掉调用集合的删除方法,而使用迭代器的删除方法:
(2). ListIterator
ListIterator ,更强大的Iterator的子类,用于各种List类的访问,并支持双向移动。
此接口定义如下:
public interface ListIterator<E> extends Iterator<E>
此接口是 Iterator 的子接口,此接口中定义了以下的操作方法:
如果要想使用 ListIterator 接口,则必须依靠 List 接口进行实例化。
示例:
List 接口中定义了以下的方法:ListIterator listIterator()
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Demo1 {
public static void main(String[] args) {
List<String> all = new ArrayList<String>();
all.add("A");
all.add("B");
all.add("C");
all.add("D");
all.add("E");
ListIterator<String> iter = all.listIterator();
System.out.println("从前向后输出:");
while (iter.hasNext()) {
System.out.print(iter.next() + " ");
}
System.out.println("\n从后向前输出:");
while (iter.hasPrevious()) {
System.out.print(iter.previous() + " ");
}
}
}
结果:
二、Map 接口
Map(映射)集合表示一种非常复杂的集合,允许按照某个键来访问元素。Map集合是由两个集合构成的,一个是键(key)集合,一个是值(value) 集合。键集合是Set类型,因此不能有重复的元素。 而值集合是Collection类型,可以有重复的元素。 Map集合中的键和值是成对出现的。
Map集合更适合通过键快速访问值。
此接口与 Collection 接口没有任何的关系,是第二大的集合操作接口。此接口常用方法如下:
这四种比较常见:HashMap , Hashtable , LinkedHashMap,TreeMap
- HashMap存取速度快,它根据键的hashcode值存储数据,线程不安全,只有一个键允许为null,对值没有限制,对数据进行遍历的话读取的随机的。如果想要同步,可以用Collections的synchronizedMap方法使HashMap线程安全或者使用ConcurrentHashMap。
- Hashtable和HashMap相似,但是线程安全,存取速度相对于Hashmap较慢,键和值都不能为null。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
- LinkedHashMap是HashMap的子类,保存了数据存入的先后顺序,当用Iterator遍历的时候按照存入的先后顺序进行遍历。
- TreeMap对键进行排列,当用Iterator遍历的时候默认按照键的升序进行。
(1). HashMap
数据结构:基础的数据节点Node 继承Map.Entry 接口实现的key-value的数据节点。 基本的存储的结构为Key-Value的Node节点的数组。
结构底层
JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
哈希冲突
HashMap是使用哈希表来存储的。当我们要新增或查找某个元素,就把当前元素的关键字通过哈希函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。而哈希冲突就是两个不同的元素,通过哈希函数计算后得出的实际存储地址相同。或者说,当我们对某个元素进行哈希运算后得到一个存储地址,然而要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突。为解决冲突问题,可以采用开放地址法和链地址法等,在Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。**在每个数组中都一个链表结构,当数据被哈希函数计算后,就得到数组下标,把数据放在对应下标元素的数组中,如果数组中已经有元素,就转变为链表存在已存在的数据后面。**哈希函数十分重要,好的哈希函数要把不同的键计算出来的结果十分分散的分布,分散的越均匀,发生Hash碰撞的概率就越小,map的存取效率就会越高,存储空间的利用率越好。
也就是说,HashMap = 数组(位桶) + 链表 , 数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到这个hash值对应的数组后面,但是形成了链表。同一各链表上的Hash值是相同的,所以说数组存放的是链表。在JDK1.8之后,当链表长度太长时(阈值为8),链表就转换为红黑树,这样大大提高了查找的效率,也就是HashMap = 数组(位桶) + 链表 + 红黑树。
- threshold:临界值,当实际大小(容量*填充因子)超过临界值时,会进行扩容
- TREEIFY_THRESHOLD:树化的最小长度8。
为啥设定为8,TreeNodes占用空间是普通Nodes的两倍,建立树是需要时间及空间成本的。因此此处基于时间与空间的权衡定位8,具体可以看源码。
- UNTREEIFY_THRESHOLD:树变成链表的阀值6。
- MIN_TREEIFY_CAPACITY:hashMap进行树化的最低条件table的大小达到64,否则只是进行扩容。
- Map 最大大小:static final int MAXIMUM_CAPACITY = 1 << 30;
负载因子:hashMap的负载因子默认为0.75,当hashMap中的元素达到 3/4就进行元素的扩容。
负载因子大小的关系,若负载因子为1,那么在出现大量的hash膨胀的情况下,元素会较密集,并且都是用链表或者红黑树的方式连接,导致查询效率较低。若负载因子为0.5 那么就会造成空间的浪费,元素分布较为稀疏。
put 操作
-
首先判断table是否需要扩容,若需要进行扩容操作
-
计算当前元素hash经过散列后是否有元素存在,若不存在元素直接添加。
-
若存在元素,分下面两个判断
- 替换:若旧元素的hash值与新添加元素一致,且新添加Node 的key调用equals方法一致,则直接替换旧节点。
- 拉链法:
普通链表:循环判断链表节点是否为key相同替换情况,若均不是需要替换情况,则定位到链表尾部添加新节点。
红黑树:树形遍历判断是否存在,不存在添加。
扩容
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),每次扩容的大小为 <<1,表示原来的2倍大小。
- 计算扩容新的table长度size 与threshold 的长度
- 遍历旧table,如果节点,无哈希冲突的情况,e.hash&(newCap-1)直接定位到新的位置。
- 出现哈希冲突的情况,由于每次扩容的大小默认为2的n次方,因此重散列的位置只会为当前位置或者当前位置+旧数组大小两个位置。
- 如果节点存在哈希冲突,则根据位运算计算最新的位置是否为0,为0表示无需移动节点。为1表示移动到oldCap+j的位置。
- 针对出现红黑树的哈希冲突,同理。此处针对红黑树冲突的需要判断重散列的节点是否需要重新建立红黑树。
- 如果初始化容量大小不为2的幂次方,那么在初始化的时候,会计算threshold为大于初始化数的最近2的幂次方数,在实际使用的时候声明为table的大小。
HashMap红黑树查找
红黑树建立是基于Hash的大小来建立的。这里的hashcode 为hashMap换算过的hash。hash小的为左子树, hash 大的为右子树
针对hash重复的情况:
- 使用equal的方法进行匹配,相同返回。
- 若存在左节点或右节点缺失,则直接进入未缺失的节点查找。(left==null ==> findByRight),均不存在返回null。
- 左右子节点均存在,判断是否为相同的class,及class是否继承comparable接口,
- 若为相同的class且都继承则直接通过comparable判断左右节点。
- 若不同的class、无继承comparable接口或者经过comparable接口比较的结果相等。
- 递归调用左节点查找,若未找到,递归调用右节点查找。
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
// hash 相同 使用equal比较
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 左右子树缺失,直接进入存在子树的部分
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
// 基于class的比较,若都继承comparable接口,则使用compareTo比较
// 若class 均不继承comparable 接口,或者compare接口比较后相同,进入左右子树递归查询。
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
针对建立红黑树或者添加树节点,若使用equal及class的compare 均无法确定添加节点的方向。 则使用对象的类名进行判断,若类名依然相同,则使用System根据对象地址换算的hashcode编码判断添加方向。
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
美团关于HashMap的讲解
Java 1.7与1.8区别
1.8还有三点主要的优化:
- 数组+链表改成了数组+链表或红黑树;
- 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
好处:
- 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
- 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题。
hashMap头插法和尾插法区别
此类的定义如下:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
此类继承了 AbstractMap 类,同时可以被克隆,可以被序列化下来。
示例:
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "张三");
map.put(2, "李四");
map.put(3, "王五");
Set<Integer> set = map.keySet(); // 得到全部的key 的Set集合
Collection<String> value = map.values(); // 方法一:得到map的value值 的Collection集合
Iterator<Integer> iter1 = set.iterator();
Iterator<String> iter2 = value.iterator(); //方法一:定义一个迭代器,迭代输出 Collection集合的value值
System.out.println("全部的key:");
while (iter1.hasNext()) {
System.out.print(iter1.next() + " ");
}
System.out.println("\n全部的value:");
while (iter2.hasNext()) { //方法一:通过迭代器输出value值
System.out.print(iter2.next() + " ");
}
System.out.println("输出HashMap中存入到数据:"); //方法二:通过foreach遍历 map中的get方法,传入key获取对应的value值
for (Integer key : set){
System.out.println(key + "->" + map.get(key));
}
}
}
HashMap 本身是属于无序存放的。
(2).旧的子类:Hashtable
Hashtable 是一个最早的 key->value 的操作类,本身是在 JDK 1.0 的时候推出的。其基本操作与 HashMap 是类似的。
也就是说,我们将HashMap中的示例代码中的 HashMap 换成 Hashtable ,是不会报错的,能够正常使用的。
操作的时候,可以发现与 HashMap 基本上没有什么区别,而且本身都是以 Map 为操作标准的,所以操作的结果形式都一样。但是 Hashtable 中是不能向集合中插入 null 值的。
HashMap 与 Hashtable 的区别
在整个集合中除了 ArrayList 和 Vector 的区别之外,另外一个最重要的区别就是 HashMap 与 Hashtable 的区别。
(3).排序的子类:TreeMap
TreeMap底层是是红黑树算法的实现。
TreeMap中的元素,key是升序的唯一;value是无序,不唯一
TreeMap 是允许 key 进行排序的操作子类,其本身在操作的时候将按照 key 进行排序,另外,key 中的内容可以 为任意的对象,但是要求对象所在的类必须实现 Comparable 接口(如同TreeSet那般)。
平衡二叉树:它是或者它的左右子树的的高度差不超过1,并且左右子树也是平衡二叉树
查找二叉树:所有左子树的比根节点小,所有右子树都比根节点大(正是有这中结构,才会有TreeMap中的key有序)
所以,自平衡二叉查找树就就是两种树形的结合,就是红黑树(自平衡二叉查找树)
示例:
Map<String, String> map = new TreeMap<String, String>();
map.put("ZS", "张三");
map.put("LS", "李四");
map.put("WW", "王五");
map.put("ZL", "赵六");
map.put("SQ", "孙七");
Set<String> set = map.keySet(); // 得到全部的key
Iterator<String> iter = set.iterator();
while (iter.hasNext()) {
String i = iter.next(); // 得到key
System.out.println(i + " --:> " + map.get(i));
}
此时的结果已经排序成功了,但是从一般的开发角度来看,在使用 Map 接口的时候并不关心其是否排序,所以此类 只需要知道其特点即可。
(3).关于 Map 集合的输出
在 Collection 接口中,可以使用 iterator()方法为 Iterator 接口实例化,并进行输出操作,但是在 Map 接口中并没有此 方法的定义,所以 Map 接口本身是不能直接使用 Iterator 进行输出的。
因为 Map 接口中存放的每一个内容都是一对值,而使用 Iterator 接口输出的时候,每次取出的都实际上是一个完整的对象。如果此时非要使用 Iterator 进行输出的话,则可以按照如下的步骤进行:
- 使用 Map 接口中的 entrySet()方法将 Map 接口的全部内容变为 Set 集合。
- 可以使用 Set 接口中定义的 iterator()方法为 Iterator 接口进行实例化。
- 之后使用 Iterator 接口进行迭代输出,每一次的迭代都可以取得一个 Map.Entry 的实例 。
- 通过Map.Entry 进行 key 和 value 的分离。
Map.Entry 本身是一个接口。此接口是定义在 Map 接口内部的,是 Map 的内部接口。此内部接口使用 static 进行定义, 所以此接口将成为外部接口。
实际上来讲,对于每一个存放到 Map 集合中的 key 和 value 都是将其变为了 Map.Entry 并且将 Map.Entry 保存在了 Map 集合之中。
在 Map.Entry 接口中以下的方法最为常用:
上述的Map的输出过程大概类似于,我们在HashMap中的方法一的步骤。,就不做多范例了。
总结
Java集合(下),( ̄︶ ̄)↗ !!!类集小结:
- 类集就是一个动态的对象数组,可以向集合中加入任意多的内容。
- List 接口中是允许有重复元素的,Set 接口中是不允许有重复元素。
- 所有的重复元素依靠 hashCode()和 equals 进行区分
- List 接口的常用子类:ArrayList、Vector
- Set 接口的常用子类:HashSet、TreeSet
- TreeSet 是可以排序,一个类的对象依靠 Comparable 接口排序
- Map 接口中允许存放一对内容,key -> value
- Map 接口的子类:HashMap、Hashtable、TreeMap
- Map 使用 Iterator 输出的详细步骤