Map常用方法
Collection和Map
集合里又有俩大派,单方式存储和键值对存储,Collection和Map
- java.util.Collection 单方式存储的超级父接口
- java.util.Map 键值对存储的超级父接口
Map是用于操作成对对象的集合,具有key-value映射关系的集合。
HashMap
--LinkedHashMap
HashTable
--Properties【操作属性文件】
TreeMap
无序和排序
且注意:无序和排序是不一样的,无序有序指的是存进来的顺序和取出的顺序是否一致。排序是按照key大小排序。
set集合和map集合
1.HashSet底层是HashMap.
2.由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
Map 添加/删除
- Object put(Object key,Object value) //key相同的,直接value替换原来的value,key不换。
- Object remove(Object key)
- void putAll(Map t)
- void clear()
map集合的put()返回值细节
@Test
public void test3(){
HashMap hashMap = new HashMap();
// 使用put方法时,若指定的键(key)在集合中没有,
// 则没有这个键对应的值,返回null,并把指定的键值添加到集合中
Object p1 = hashMap.put("1", "2");//p1=null
// 若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值)
// 并把指定键所对应的值,替换成指定的新值
Object p2 = hashMap.put("1", "3");//p2=2
System.out.println("p1="+p1+","+"p2="+p2);//p1=null,p2=2
}
Map 元素获取
- Object get(Object key)
- boolean containsKey(Object key)
- boolean containsValue(Object value)
- int size();//获取map中有几对
- boolean isEmpty()
- boolean equals(Object obj);
map键相同时,换value不换key验证【自己编写的,骄傲】
重写equals和hashcode()故意只用了name,是为了方便测试
public class Car {
private String name;
private Integer price;
public Car() {
}
public Car(String name, Integer price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
测试类:由结果可知,是换了value没换key
@Test
public void test5() {
HashMap hashMap= new HashMap();
hashMap.put(new Car("小白",18),"999");
hashMap.put(new Car("小白",19),"1000");
//Map集合key相同时,value值会替换,但是key值不会被替换
System.out.println(hashMap);//{Car{name='小白', price=18}=1000}
}
HashMap
HashMap集合是一个存取无序的集合,存储元素和取出元素的顺序有可能不一致
HashMap允许空key空value并且是线程不安全的
哈希表
哈希表(Hash Table),也被称作散列表,是一种重要的数据结构。
哈希表是基于键(Key)的映射存储机制,通过哈希函数(Hashing Function)将键转换为数组的一个索引位置,从而能够快速定位到数据
哈希函数
理想的哈希函数有两个特性:
- 对于同一个输入值,产生相同的哈希值;
- 对于不同的输入值,产生不同的哈希值。
哈希冲突: 对于不同的输入值,产生了相同的哈希值,这就叫冲突,冲突越少,哈希算法的质量越高。
每个对象都有一个hashCode值,类似于自身的身份证号码。
该hashCode值默认出厂来自于Object类的这个方法,返回的是一个int整数
//发现只有方法的声明,没有方法的实现,也即java需要通过Native关键字调用系统底层函数,给我返回值。 //native方法,表示调用底层第3方函数,C语言系统生成的东西 public native int hashCode();
哈希冲突:两个不同的对象居然碰撞出来同一个hashCode值,即 hashCode哈希值冲突了
-------------------------
演示哈希冲突1
//演示哈希冲突1:
System.out.println("Aa".hashCode());//2112
System.out.println("BB".hashCode());//2112
演示哈希冲突2:上万次的计算后,hash值会冲突
//演示哈希冲突2:上万次的计算后,hash值会冲突
Set set = new HashSet();
int hashCode;
for (int i = 1; i <=110000 ; i++) {
hashCode = new Object().hashCode();
if(set.contains(hashCode))
{
System.out.println("----出现了hash冲突,在第几次:"+i+"\t hashCode: "+hashCode);
continue;
}
set.add(hashCode);
}
System.out.println(set.size());
哈希冲突,每个人的环境下出现的哈希冲突在第几次一般是固定的。
何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?
只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后使用链表+红黑树解决哈希碰撞。
HashMap中常见2种算法
HashMap中的Hash算法: key的HashCode值无符号右移16位做异或运算
//HashMap源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
计算索引的算法:数组长度减一与上hash值
tab[i = (n - 1) & hash]
jdk7 HashMap的底层实现原理
HashMap底层结构:jdk1.7 中由数组+链表实现
HashMap map= new HashMap();
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
....可能已经执行过多次put...
map.put(key1,value1)
首先,调用key1所在类的hashcode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空。此时的key1-value1添加成功。---情况1
如果此位置上的数据不为空,比较key1和已经存在的一个或多个数据的哈希值:
如果key1的哈希值与已经存在的数据的哈希值都不同,此时key1-value1添加成功。----情况2
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在
类的equals(key2)
如果equals()返回false:此时key1-value1添加成功。----情况3
如果equals()返回true:使用value1替换value2.
补充:关于情况2和情况3;此时key1-value1和原来的数据以链表的方式存储,在不断的添加过程,会涉及到扩容的问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来
jdk8 HashMap底层实现原理
HashMap底层结构:jdk1.8 中由数组+链表+红黑树实现
假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化。
HashMap jdk8存储过程
说明:
1.size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。
2.threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 )。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 。
相较于jdk7在底层实现方面的不同:
1.new HashMap():底层没有创建一个长度为16的数组
2.jdk8 底层的数组是:Node[],而非Entry[]
3.首次调用put()方法的时候,底层创建长度为16的数组HashMap map = new HashMap() 在内存中占几个字节 占16位,采用懒加载
4.jdk7底层结构只有:数组+链表,jdk8中底层结构:数组+链表+红黑树
当数组的某一个索引位置上的元素以链表形式存在的数据个数>8 且当前数组长度>64时,
此时此索引位置上的所有数据改为使用红黑树存储。
将链表转换成红黑树前会判断,即使阈值大于 8,但是数组长度小于 64, 此时并不会将链表变为红黑树。而是选择进行数组扩容
链表阈值为什么是8
选择8因为符合泊松分布,超过8的时候,概率已经非常小了,所以我们选择8这个数字。
一个bin中链表长度达到8个元素的概率为0.00000006,
============
hashmap放入的是k,v键值对,你说是数组,请问什么类型的数组?
Node<K,V>类型数组
Node 是HashMap自己定义的静态内部类
//以下是HashMap源码部分
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
}
HashMap扩容初始容量
设置初始容量大小的必要性
HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。 随着元素的不断增加,HashMap会有可能发生多次扩容
应该设置初始化容量为多少?
当我们明确知道HashMap中元素的个数(initialCapacity )的时候,
把默认容量(构造器的参数)设置成 initialCapacity/ 0.75F + 1.0F
反例:
JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂。
如果我们设置的默认值是7(明确要存储7个元素,然后构造器的参数也就傻乎乎的传了7),经过Jdk处理之后,会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。
使用公式 initialCapacity/ 0.75F + 1.0F
7/0.75 + 1 = 10 ,10经过Jdk处理之后,会被设置成16,这就大大的减少了扩容的几率。
为什么initialCapacity必须是2的n次幂?如果输入值不是2的幂比如10会怎么样?
因为2的n次方可以使得元素尽量均匀分配到数组中,避免hash冲突
在确定要存储的元素在数组中的具体位置时,HashMap用某种算法尽量把数据分配均匀,这样每个链表长度大致相同,这个算法实际就是取模。
但是:计算机中直接求余效率不如位移运算
实际上hash%length等于hash&(length-1)的前提是length是2的n次幂
2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1
20%16=4
20&(2^4-1)=20&15= 4
0001 0100
& 0000 1111
------------------
0000 0100
默认情况下HashMap的容量是16,但是,如果用户通过构造函数指定了一个数字作为容量,那么Hash会选择大于该数字的第一个2的幂作为容量。(3->4、7->8、9->16)
//HashMap类源码
//构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap加载因子
加载因子的大小是主要就是决定了HashMap的数据密度,加载因子为什么默认是0.75
//以下是HashMap的源码部分
public HashMap() {
//DEFAULT_LOAD_FACTOR = 0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
加载因子越大,填满的数据就越多,空间利用率就高(唯一好处),但哈希冲突的几率就变大。数组中的链表也会越容易长,这样的话造成查询和插入时的比较次数增多,性能会下降。
加载因子越小,填满的数据就越少,哈希冲突的几率就减少了,但浪费了空间,而且还会提高扩容的触发几率,扩容会很影响性能
触发扩容:会把所有元素rehash之后再放在扩容后的容器中,这是一个相当耗时的操作,所以扩容是件影响性能的。
默认情况下:
第一次扩容的时候:16*0.75 =12,
扩容机制:2倍。 2^4,2^5,2^6//以下是HashMap的源码部分 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold }
HashMap设计过程中是否有错误?
虚线:接口实现;实线:继承
HashMap直接父类 Map,此时HashMap与Map 是父子关系
HashMap直接父类abstractMap,而abstractMap直接父类map 此时HashMap 与Map 是爷孙关系
Map接口其余实现类
1、LinkedHashMap
LinkedHashSet 底层是 LinkedHashMap完成的
LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的
2、HashTable(古老实现)
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
}
HashMap和HashTable底层都是哈希表数据结构
HashTable线程安全,效率低,属于悲观锁
HashMap线程不安全,效率高HashMap集合:可以存储null值,null键
Hashtable集合,不能存储null值,null键
3、TreeMap
TreeMap 是根据key排序的。
TreeSet底层TreeMap。若使用自定义类作为TreeMap的key,所属类需要重写equals()和hashCode()方法,且equals()方法返回true时,compareTo()方法应返回0
4、Properties集合
Properties是线程安全的,采用key,value存储数据,但是key和value都只能是string类型。
Properties集合是一个唯一和IO流相结合的集合
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
public class Properties extends Hashtable<Object,Object> { }
在当前模块右键创建文件: hello.properties【因为test测试用例默认当前路径是moudle,main方法默认当前路径是项目路径】,hello.properties文件内容如下:
username=admin
password=123456
@Test
public void test() throws IOException {
// 1、获取properties实例
Properties props=new Properties();
// 2、通过load()加载属性文件
props.load(new FileInputStream("hello.properties"));
// 3、getProperty(StrIng key);拿到用户名,密码,
System.out.println("用户名: "+props.getProperty("username"));
System.out.println("密码: "+props.getProperty("password"));
}
properties文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。
也就是说hello.properties文件这么写,运行的结果也是一样的
username admin
password:123456
Collections和Collection
1、排序操作
(均为Collections中的static方法)
- reverse(List):反转 List 中元素的顺序
- shuffle(List):对 List 集合元素进行随机排序
- sort(List):根据元素的自然顺序【自然排序】对指定 List 集合元素按升序排序
- sort(List,Comparator):根据指定的 Comparator 产生的顺序【定制排序】对 List 集合元素进行排序
- swap(List,int i, int j):将指定 list 集合中的 i 处元素和 j 处元素进行交换
2、查找、替换
(均为Collections中的static方法)
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中。要满足:dest.size>=src.size
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
Vector 和 Enumeration 接口:
Vector采用数组结构存储元素,是线程安全的,效率低
Enumeration 接口是 Iterator 迭代器的 “古老版本”
@Test
public void test1() {
Vector vec = new Vector();
vec.addElement("AA");
vec.addElement("BB");
vec.addElement("CC");
Enumeration elements = vec.elements();
while (elements.hasMoreElements()) {
Object element = elements.nextElement();
System.out.println(element);
}
}
Map集合的遍历
Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。
Map中的key:无序的、不可重复的,使用set存储所有的key。key所在的类要重写equals()和hashCode()
Map中的Value:无序的、可重复的,使用Collection存储所有的value。value所在的类要重写equals()
一个键值对:key-value 构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
//获取map中所有的key,是自定义的一个类KeySet实现了Set接口,再增强for,迭代器
Set keySet()
//获取map中所有的value,没办法根据value获取key。返回的是 new Values()
Collection values();
//Map.Entry对应一个key-value 是一个封装的内部类
Set entrySet()
1、map集合的遍历: values()
演示很容易出错的写法
public void test3() { HashMap hashMap = new HashMap(); hashMap.put("1", "a"); hashMap.put("2", "b"); hashMap.put("3", "c"); Collection collection = hashMap.values(); System.out.println(collection.getClass());//class java.util.HashMap$Values List list= (List) collection;//抛出异常ClassCastException }
正确的写法:
@Test
public void test6() throws IOException {
HashMap hashMap = new HashMap();
hashMap.put("1", "a");
hashMap.put("2", "b");
hashMap.put("3", "c");
Collection collection = hashMap.values();
ArrayList list = new ArrayList();
list.add(collection);
System.out.println(list);
}
2、map集合的遍历:keySet()
Set<Interger> keys=map.keySet() //获取所有的key,返回一个set集合
for(Interger key:keys){
system.out.println("value="+map.get(key)) //调用map.get(key),获取对应key的value值
}
3、map集合的遍历:entrySet()
把(key-value)作为一个整体一对一对地存放到Set集合当中的。
public Set<Map.Entry<K,V>> entrySet() { }
@Test
public void test3() {
HashMap<String, String> hashMap = new HashMap();
hashMap.put("1", "a");
hashMap.put("2", "b");
hashMap.put("3", "c");
//遍历 map
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
//格式化输出
System.out.printf("key= %s and value= %s \n",key,value);
}
}
-------------------------------------------------
比较keyset()和entrySet()
keySet(): 迭代后只能通过get()取key
entrySet():迭代后可以e.getKey(),e.getValue()取key和value。返回的是Entry接口
最要命的是:entrySet要比keySet快一倍左右。
总结一波每个集合的底层所用到的数据结构
java中有很多的集合。不同的集合,底层会对应不同的数据结构。
- ArrayList: 底层是数组。
- LinkedList:底层是双向链表
- Vector:底层是数组,线程安全的,效率低
- HashSet:底层是HashMap,放到HashSet集合的元素等同于放到HashMap集合key部分了。
- TreeSet: 底层是TreeMap,放到TreeSet集合中的元素等同于放到TreeMap集合key部分了
- HashMap:底层是哈希表
- HashTable:底层也是哈希表,只不过线程安全的,效率低
- Properties:是线程安全的,并且key和value只能存储字符串String
- TreeMap:底层是二叉树。TreeMap集合key可以自动按照大小顺序排序。