一.Map接口
Map接口表示的就是双列集合.双列集合的元素是按照键值对的方式来存储的.并且不能有重复的键,但是值可以重复.也就是说键值的对应关系必须是一对一,值键的对应关系可以是一对多.
Map接口中的常用方法
理论上Map跟List差不多,都可以存储任何类型的数据.但是为了后面操作集合方便,我们还是需要指定集合能存储的数据类型,这里包括键值型以及值类型.这里还需要使用到我们之前提到的泛型语法.这样规定了存储的数据类型之后,我们进行数据操作时就不会造成数据紊乱的情况.
这里我们通过一段代码看看Map接口下的常用方法:
public class MapDemo {
public static void main(String[] args) {
//父类引用指向子类对象
Map<Integer,String> map = new HashMap<>();
//向集合中添加元素
map.put(6, "6");
map.put(5, "5");
//得到此集合中键对应的值
System.out.println(map.get(6));
//判断集合包不包含指定的键
System.out.println(map.containsKey(6));
//判断集合包不包含指定的值
System.out.println(map.containsValue(5));
//集合的长度
map.size();
//判断集合是不是空集
System.out.println(map.isEmpty());
//清空整个集合
map.clear();
}
}
二.Map的实现类
上个章节我们已经提到了Map的具体实现类
Map接口下主要有三个实现类:
1.HashMap.这个类的元素存储顺序是无序的.底层是数组,按照哈希算法进行判断是否是重复的以及排序.这个在这个章节后面我们再说.
2.Hashtable:这是一个线程安全的Map.
3.TreeMap:这个类的元素存储也是无序的,但是我们可以通过实现Comparable接口来实现元素的排列是有序的.底层是树结构.这个等我们了解了数据结构之后再介绍这种数据结构.
HashMap
HashMap集合是一个双列集合.并且**存储的元素不能重复.**那么为什么存储的元素不能重复呢?怎么判断元素是否重复?
判断元素是否重复
我们的add方法在底层进行操作的时候,就会判断集合中是否已经包含了此元素(只看键).在底层使用的是**hashCode()和equals()**这两个方法来判断内容是否重复.
在之前学习API的时候我们提到过Object类,那么其实这两个方法是Object类中的方法.值得注意的是,**hashCode()这个方法被native修饰.**代表就是此方法是本地方法,也就是调用操作系统的方法,java并没有实现.
在Object类中,HashCode的作用是获取对象在内存中的地址.equals的作用与"=="作用相同,也是比较对象的地址.
然而在实际除了我们的自建类,java中的所有类几乎都重写了这两种方法.重写过后的HashCode方法就不是获取对象的地址了,而是经过一系列简单计算计算出对象的**哈希值.这个哈希值是int类型.其次equals方法也被重写了,这个在之前的API章节中也有讲过.重写之后的equals方法作用也不是比较对象的地址是否相同了,而是比较对象的内容是否相同.很明显equals方法比HashCode方法效率要低一些.因此hash类型判断元素是否重复首先使用的是hashCode方法(因为效率高),但是有些时候尽管内容是不同的但是哈希值却一样,这个时候再调用equals方法(效率低但是准确).**这样做就会保证相同的元素不会加到集合中(之后的会把之前的覆盖).
当然hashMap是这样,我们上一节单列集合中的hashSet也是这样,毕竟是hashMap分割出去的,底层实现是完全一样的.
我们先看一下hashMap的常用方法
import java.util.HashMap;
public class HashDemo {
public static void main(String[] args) {
HashMap<Integer,String> hashMap = new HashMap<>();
hashMap.put(1, "1");
hashMap.put(2, "2");
hashMap.put(1, "11");
hashMap.put(4, "4");
hashMap.put(5, "55");
hashMap.put(3, "3");
//hashMap可以存储为null的键
hashMap.put(null, "0");
//{null=0, 1=11, 2=2, 3=3, 4=4, 5=55}
System.out.println(hashMap);
//拿到键对应的值
System.out.println(hashMap.get(1));
//判断此集合包不包含对应的键
System.out.println(hashMap.containsKey(6));
//判断集合包不包含对应的值
System.out.println(hashMap.containsValue("6"));
//返回键那一列的Set集合
System.out.println(hashMap.keySet());
//返回值对应的那一列的Collection单列集合
System.out.println(hashMap.values());
//删除键对应的键值对,并返回键对应的值
System.out.println(hashMap.remove(5));
//更改键对应的值,返回更改前的值
System.out.println(hashMap.replace(1, "111"));
}
}
那么hashMap的底层结构到底是怎样的呢.
hashMap底层结构
在上面我们已经提到了hashMap存储元素的顺序是无序的,但是元素的排列是有序的,元素排列的顺序是按照计算出来的哈希值进行存储的,那么具体的底层结构是怎样的,我们具体来看看.
自java8之后,底层存储的数据结构有三种,分别是数组,链表,红黑树.
我们以hashSet为例介绍hashMap底层存储逻辑.
hashSet底层结构是哈希表也就是数组,存储元素的过程中会根据存储的元素计算出一个hash值,再进行运算计算出元素在数组的位置(一般是对哈希值进行取模).将元素封装到一个node对象中,将对象存储到对应的位置,如果之后有元素计算出来存储的位置相同的话,会加在这个元素之后,也就是说数组里面存储的是链表,当链表长度也超过一定数值,会转换为红黑树.
哈希数组底层数组初始长度为16,负载因子是0.75,当数组里面有个元素已经变为链表结构并且长度超过8时,会先扩容哈希数组,要么就是数组长度已经超过了0.75倍的长度,也会触发扩容,每次扩容后的长度是之前的2倍.直到到64,这个时候如果链表长度超过了8,那么链表就会转换为红黑树存储结构.
我们一起来看看put方法的源码
put 方法源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
//hash(key)这个方法就计算出来key的哈希值
}
hash(key)方法源码
static final int hash(Object key) {
int h;
//在这里就可以看出hashMap可以存储一个为null的键,返回key的hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal方法源码
final V putVal ( int hash, K key, V value,boolean onlyIfAbsent,
boolean evict){
Node<K, V>[] tab;//这就是底层的hash数组
Node<K, V> p;//一个node对象
int n, i;//n就是hash数组长度,
//在我们new出来hashMap对象时,并没有创建出node对象,在添加元素的时候才会创建数组
if ((tab = table) == null || (n = tab.length) == 0)
//resize()是hash表扩容方法
n = (tab = resize()).length;
//i = (n - 1) & hash就是取模运算,计算出值应该在数组的位置
if ((p = tab[i = (n - 1) & hash]) == null)
//如果数组没有node对象,new出来一个node对象
tab[i] = newNode(hash, key, value, null);
else {
Node<K, V> e;
K k;
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) {
if ((e = p.next) == null) {
//往链表添加元素
p.next = newNode(hash, key, value, null);
//如果链表长度>=8,会调用转换为红黑树的方法,但是哈希数组长度也得满足长度为64的限制条件
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
//如果添加相同元素,覆盖之前的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
那我们现在来总结一下hashMap底层put方法的存储逻辑
哈希表的每个空间存储的都是一个node对象.底层是对node对象进行操作的,存储的过程与之前的hashSet也是一模一样.就是三种数据结构的综合利用.
这里需要提一下遍历的问题.因为哈希表底层存储的是node对象,因此遍历起来不是很方便,要是自己来的话还得解封装,非常麻烦.hashMap为了方便起见给我们提供了一个内部类Entry.我们可以使用Map接口里的方法**entrySet().**将hash表所有的node对象里的键值对内容封装在一个Set集合中.然后可以对这个Set集合进行遍历,遍历的方法在上节内容也提到了,推荐使用迭代器遍历.同时这个类也为我们提供了getKey()和getValue()等方法.
遍历
import java.util.HashMap;
public class BianLiDemo {
public static void main(String[] args) {
HashMap<Integer,String> hashMap = new HashMap<>();
hashMap.put(1, "1");
hashMap.put(2, "2");
hashMap.put(3, "3");
hashMap.put(4, "4");
hashMap.put(6, "6");
hashMap.put(5, "5");
for (Integer i :hashMap.keySet()) {
//输出键那一列
System.out.println(i);
}
for (String s:hashMap.values()) {
//输出值那一列
System.out.println(s);
}
}
}
import java.util.HashMap;
import java.util.Set;
import static java.util.Map.*;
public class BianLiDemo1 {
public static void main(String[] args) {
HashMap<Integer,String> hashMap = new HashMap<>();
hashMap.put(1, "1");
hashMap.put(2, "2");
hashMap.put(3, "3");
hashMap.put(4, "4");
hashMap.put(6, "6");
hashMap.put(5, "5");
//返回Entry的Set集合
Set<Entry<Integer, String>> entry = hashMap.entrySet();
for (Entry<Integer, String> s:entry) {
//需要什么就调用什么
s.getKey();
System.out.println(s);
}
}
}
HashTable
HashTable类与HashMap的主要区别是:
HashTable不可以存储一个为null的键,但是HashMap可以.
HashTable类存储的不再是node对象,而是Entry对象.
HashTable是线程安全的.
TreeMap
这个类在之后我们再详细介绍
三.Collections工具类
注意我们之前上节说到单列集合的时候提到了Collection接口.那个接口是单列集合的根接口,跟我们这个有很大区别.这个类是为集合提供工具,也可以说是集合工具类.
Collections类与Arrays类相似,Arrays是为数组提供的工具类.Collections包含对集合进行操作的多态算法.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CSDemo {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
/*
将这些元素全部加到集合里面
这个方法里面有两个参数,Collection<? super T> c,T...elements
第一个参数c是集合,使用到了泛型通配符,通配符下限
第二个参数代表可变长度参数,是利用数组数组实现的,也就是elements是一个数组
一个参数列表中最多只能有一个可变长度参数,并且必须在末尾位置
*/
Collections.addAll(arrayList,5,6,1,3,4);
//与Arrays类似,必须先排序才能使用二分查找
Collections.sort(arrayList);
System.out.println(Collections.binarySearch(arrayList, 4));//3
//逆序排序
Collections.reverse(arrayList);
//交换两个索引元素的位置
Collections.swap(arrayList, 0, 3);
//打乱元素顺序
Collections.shuffle(arrayList);
//list里的最大值
System.out.println(Collections.max(arrayList));
//用指定元素将集合填满
Collections.fill(arrayList, 9);
//将集合中所有为5的元素替换为4
Collections.replaceAll(arrayList, 5, 4);
//返回一个为空的List集合,其实是一个内部类emptyList对象,这个集合不能操作使用,用于逻辑处理
List list = Collections.emptyList();
}
}
import java.util.ArrayList;
import java.util.Collections;
public class CSDemo1 {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
Collections.addAll(arrayList, 1,2,3,5,4,6);
ArrayList<Integer> arrayList1 = new ArrayList<>();
Collections.addAll(arrayList1, 5,6,1,3,2);
/*
我们要想将集合1中的内容复制到集合2中,可以使用copy方法
但是要注意使用条件:
被复制的集合(也就是第一个参数)泛型类型是第二个集合的父类
下面那个copy方法使用会报错,因为arrayList1长度比arrayList小,存不进去那么多数据
*/
Collections.copy(arrayList, arrayList1);
Collections.copy(arrayList1, arrayList);
}
}