(一)Map集合结构图
(二)Map集合中常用的方法
(1)往Map集合中添加元素
- V
- put(K key, V value)
- 将指定的值与该映射中的指定键相关联(可选操作)。
这个和Collection不一样,Collection用的是add();
(2)清空
- void
- clear()
- 从该地图中删除所有的映射(可选操作)。
Collection中,也有这个方法。
(3)判断包含key和value
- boolean
- containsKey(Object key)
- 如果此映射包含指定键的映射,则返回 true 。
- boolean
- containsValue(Object value)
- 如果此地图将一个或多个键映射到指定的值,则返回 true 。
在Collection中是通过 Cotains,这和底层原理有关系了,因为Map是key-value存储的,Collection只是存储一个value。
(4)通过Key获取value值
- V
- get(Object key)
- 返回到指定键所映射的值,或 null如果此映射包含该键的映射。
通过key值获取value值,这个和Colleciton中的List很像,然后Set中是没有下标的,所以Set没有。
(5)判断集合是否包含元素
- boolean
- isEmpty()
- 如果此地图不包含键值映射,则返回 true 。
这个和Collection的一样。
(6) 获得集合中的元素数量
- int
- size()
- 返回此地图中键值映射的数量。
这个和Colleciton中的一样。
(7) 删除集合中的某个元素
- V
- remove(Object key)
- 如果存在(从可选的操作),从该地图中删除一个键的映射。
这个和Colleciton中的一模一样。
(8)获取所有的value
- Collection<V>
- values()
- 返回此地图中包含的值的Collection视图。
Map<Integer,String> m1 = new HashMap<>();
m1.put(1,"赵晓东");
m1.put(2,"赵静瑶");
m1.put(3,"赵景康");
Collection<String> c1 = m1.values();
for(String s : c1){
System.out.println(s);
}
赵晓东
赵静瑶
赵景康
(9) 获取Map集合中的所有Set集合,这个也很重要,可以循环Map
- Set<K>
- keySet()
- 返回此地图中包含的键的Set视图。
(10)将Map集合转换为set集合。这个很重要,可以循环Map
- Set<Map.Entry<K,V>>
- entrySet()
- 返回此地图中包含的映射的Set视图。
(三)Map集合循环
Map集合的循环一共有两种方式
(1) 获取所有的key,通过遍历Key,来遍历value。这时候就需要了KeySet()方法进行转换了。
Map<Integer,String> m1 = new HashMap<>();
m1.put(1,"老师");
m1.put(2,"校长");
m1.put(3,"学生");
/*通过将Map方法获取Set*/
Set<Integer> key =m1.keySet();
/*转换成Set方法后,通过获取iterrator进行遍历*/
Iterator<Integer> I1=key.iterator();
while (I1.hasNext()){
Integer NumberKey = I1.next();
String s1=m1.get(NumberKey);
System.out.println(s1);
}
老师
校长
学生
同时也可以通过增强for循环来获取Key
for(Integer key1 : key){
System.out.println(m1.get(key1));
}
(2)把Map集合直接全部转化成Set集合。
这个时候需要调用的是Map的entrySet()方法了。
Map<Integer,String> m1 = new HashMap<>();
m1.put(1,"老师");
m1.put(2,"校长");
m1.put(3,"学生");
/*将Map对象转换为Set*/
Set<Map.Entry<Integer,String>> set = m1.entrySet();
/*转换为Set之后,再调用Iterator*/
Iterator<Map.Entry<Integer,String>> mset=set.iterator();
/*接下来就是遍历*/
while (mset.hasNext()){
Map.Entry<Integer,String> m2=mset.next();
Integer I1 =m2.getKey();
String S1 = m2.getValue();
System.out.println(I1+"--->"+S1);
}
1--->老师
2--->校长
3--->学生
我们来看一下转换过程
其实这个时候,Map.Entry<Integer,String>为一个结点。 这个结点就是Set的形式,然后再通过set的getkey和getvalue获取元素即可。其实Map.Entry<k,v>为节点的类型。
当然这个也能使用增强for循环。
for(Map.Entry<Integer,String > p1 :set){
System.out.println(p1.getKey()+"---->"+p1.getValue());
}
(四)HashMap(哈希表)
(1)HashMap集合底层是哈希表/散列表的数据结构
(2)哈希表是一个怎样的数据结构?
哈希表是一个数组和单向链表的结合体。数组:在查询方面效率很高,在随机增删方面效率很低。单向链表:在随机增删方面效率很高,在查询方面效率很低。哈希表将以上的两种数据结构融合在一起,充分发挥他们的各自优点
(3)HashMap集合底层源代码(哈希表在代码的原理实现)
Public class HashMap{
//hashMap底层实际上就是一个数组(一维数组)
Node<k,v> table;
//静态内部类
static class Node<K,V> implements Map.Entry<K,V>{
finalinthash; //哈希值(哈希值是key()和hashCode()方法执行结果,hash值通过哈希函数/算法)
finalKkey; 存储到Map集合中的那个Key
Vvalue; 存储到map集合中的那个value
Node<K,V> next; 下一个节点的内存地址
Node(inthash,Kkey,Vvalue,Node<K,V>next){
this.hash=hash;
this.key=key;
this.value=value;
this.next=next;
}
}
从以上的源代码我们可以看出,HashMap里面有Node数组,数组里面有hash,key,value,next。其中hash是哈希值(哈希值是key的hashCode()方法的执行结果,hash值通过哈希函数/算法。可以转换存储成数组的下标),key,value就是存储的键值对,next就是下一个节点的内存地址。
综上所述:哈希表/散列表,一维数组,这个数组中每一个元素是一个单向链表(数组和链表的结合体)。
很明显的看出来,里面是结点的方式。所以,它到底是怎么样子的呢?
(4)map.put(k,v)和v=map.get(k)的原理
这个是特别重要的,也就是说,存是如何存的,取是如何取的。
(I)先将k,v封装到Node对象当中
(II)底层会调用k的hashCode()方法得到hash值,然后通过哈希函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上,如果说下标对应的位置上有链表,此时就会拿着k和链表上每一个节点的k进行equals,如果所有的equals方法都返回false,那么这个新节点将会被添加到链表的末尾。如果其中有一个equals返回了true,那么会将这个节点的value将会被覆盖。(hashCode是Object自带的,如果不重写,则返回地址。)其实这就是数据结构里面学过的。
v=map.get(k)实现原理:
(III)先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上,如果这个位置上什么也没有,返回Null,如果这个位置上有单向链表,那么会拿着参数k和单向链表上的每个结点中的k进行equals,如果所有equals方法返回false,那么get方法返回null,只有其中有一个结点k和参数k的equals返回true,那么此时这个节点的value就是我们要找的value。get方法最终返回这个要找的value。
(a)为什么哈希表的随机增删,以及查询效率都很高?
因为增删是在链表上完成,查询也不需要扫描,只需要部分扫描。
所以说HashMap上面的k会先后调用两个方法,一个是hashCode()另一个是equals,所以这两个都需要重写。equals默认是比较内存地址,但是我们要比较的是里面的内容。
这里可以联想为什么HashMap集合的key部分的特点是无序,不可重复?
无序:因为调用了hashCode()方法,通过哈希算法得出来的值不一定是连续的,可能是随便的到哪一个单向链表上的,学过数据结构的人都知道,计算位置的方法会有很多,其中有一个%取余数。所以说,不可能是连续的,那只能是无序的。
不可重复:不可重复是因为,k值调用了equals方法。这个方法会对内容进行比较,如果内容相同的话,会覆盖掉value,所以说HashMap存储的数据时不可重复的。
放在HashMap集合key部分的元素其实就是放在了HashSet集合中了,所以HashSet集合中的元素也要需要重写HashCode()和equals。
所以,同一个单向链表上的所有结点的Hash是相同的。同一个链表上一个key,equals一定是false。无序不可重复。
(b)如果HashCode()重写的时候,写成一个固定值,那么就会成为单向链表。就会发挥不出功能。这种情况称为散列分布不均匀。
(c)如何HashCode()的返回值都不一样,行吗?不行那么就称为一维数组了。就没有链表的概念了,也是散列分布不均匀。
所以说HashCode()重写需要有一定的原理。
(5)同时重写HashCode()和equals()
我们上面说了,为什么要重写HashCode()和equals(),那么equals方法有可能调用,也有可能不调用。
拿put(k,v)举例,什么时候equals不会调用?
哈希值经过哈希算法转换为数组下标,数组下标位置上如果是null,equals不需要执行。
拿get(k)举例,什么时候equals不会调用?
K.hashCode()方法返回哈希值,哈希值经过哈希算法转换数组下标。
数组下标位置上如果是Null,equals不需要执行。
我们重写了equals,但是没有重写hashCode()
Student s1 = new Student("zhangsan");
Student s2 = new Student("zhangsan");
/*查看equals和hashCode*/
/*没有重写equals方法之前*/
// System.out.println(s1.equals(s2));//false
/*重写了equals方法之后*/
System.out.println(s1.equals(s2));//true
System.out.println(s1.hashCode());//-1432604525
System.out.println(s2.hashCode());//-795136960
Set<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
System.out.println(students.size());
true
460141958
1163157884
2
按理说student.size()应该是1,但是结果是2,现在我们重写hashCode方法,再进行测试。
true
-1432604525
-1432604525
1
student.size()的结果就变成了1.
所以说最终结论:放在HashMap集合中的key部分的,以及放在HashSet集合中的元素,需要同时重写HashCode方法和equals方法。
但是在JDK8之后,如果哈希表单向链表中元素超过8个,单向链表这种结构会变成红黑树数据结构,当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。
(五)HashMap和HashTable的区别
(1)HashMap中可以存放Null,而HashTable中不可以。
HashMap存放Null值
public static void main(String[] args) {
Map m1 = new HashMap();
m1.put(null,null);
System.out.println(m1.size());
}
1
HashTable存放
Map m2 = new Hashtable();
m2.put(null,null);
Exception in thread "main" java.lang.NullPointerException
at java.util.Hashtable.put(Hashtable.java:459)
at com.javase.Map.MapTable.main(MapTable.java:13)
(六)Properties
Properties是一个Map集合,继承了Hashtable,properties的key和value都是String类型。Properties被称为属性类对象。并且是线程安全的。
Properties pro = new Properties();
/*需要掌握Properties的两个方法,一个存,一个取*/
pro.setProperty("url","zcdee");
pro.setProperty("gdsa","dsarf");
pro.setProperty("username","root");
/*通过key值进行取*/
String s1 = pro.getProperty("url");
String s2 = pro.getProperty("username");
System.out.println(s1);
System.out.println(s2);
zcdee
root
(七)TreeSet
(1)TreeSet集合底层实际上是一个TreeMap
(2)TreeMap集合底层是一个二叉树
(3)放到TreeSet集合中的元素,等同于放到了TreeMap集合Key部分了。
(4)TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为可排序集合。
(5)那么什么时候可能用到TreeSet呢?
编写程序从数据库当中取出数据,在页面展示用户信息的时候按照生日升序或者降序。这个时候可以使用TreeSet集合,因为TreeSet集合放进去,拿出来就是有序的。
TreeSet<Integer> t1 = new TreeSet<>();
t1.add(10000);
t1.add(100);
t1.add(1000);
for(Integer i1 : t1){
System.out.println(i1);
}
运行结果为
100
1000
10000
(6)对自定义的类型来说,TreeSet可以排序吗?
不可以,以下程序中对于Person类型来说,无法排序,
为什么不可以排序呢,因为美音指定Customer对象之家你的比较规则。谁大谁小并没有说明。当然出现这个异常的原因是因为没有实现Comparable接口。
(7)那么怎么进行排序呢?
(a)让Customer实现这个Comparable接口
(b)重写方法
之所以String,Integer能进行排序,是因为他们实现了Comparable接口和方法。
所以说CompareTo方法的返回值很重要:
返回0表示相同,value会覆盖,返回>0,会继续在右子树上找。返回<0,会继续在左子树上找。
(八)自平衡二叉树
(1)遵循左小右大原则存放。
(2)遍历二叉树的时候三种方式:
前序遍历:根左右。 中序遍历:左根右 。 后序遍历:左右根。
(3)TreeSet集合/TreeMap集合采用的是:中序遍历方式Iterator迭代器采用的是中序遍历方式,左根右。
(九)TreeSet的第二种比较方式
public static void main(String[] args) {
/*创建TreeSet集合的时候,需要使用这个比较器*/
TreeSet<WuGui> Wuguis = new TreeSet<>(new WuGuiComparator());
WuGui w1 = new WuGui(800);
WuGui w2 =new WuGui(521);
WuGui w3 = new WuGui(888);
Wuguis.add(w1);
Wuguis.add(w2);
Wuguis.add(w3);
for(WuGui s : Wuguis){
System.out.println(s);
}
}
}
class WuGui{
private int age;
public WuGui(int age) {
this.age = age;
}
public WuGui() {
}
@Override
public String toString() {
return "WuGui{" +
"age=" + age +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class WuGuiComparator implements Comparator<WuGui>{
@Override
public int compare(WuGui o1, WuGui o2) {
return o1.getAge() - o2.getAge();
}
WuGui{age=521}
WuGui{age=800}
WuGui{age=888}
最终结论:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
第一种:放在集合中的元素实现java.lang.Comparable接口。
第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
Comparable和Comparator怎么选择呢?
当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。
(九)Collections工具类
(1)Collections实现对List进行排序
编程线程安全的和排序
/*ArrayList集合不是线程安全的*/
List<String > list1 = new ArrayList<>();
/*变成线程安全的*/
Collections.synchronizedList(list1);
/*排序*/
list1.add("abf");
list1.add("abx");
list1.add("abc");
list1.add("abe");
Collections.sort(list1);
for (String l1 : list1){
System.out.println(l1);
}
abc
abe
abf
abx
当我们往list里面传入WuGui,并且没有实现comparable方法的时候
就会报错,所以使用Collections的时候必须要对Wugui2进行实现Comparable接口
class WuGui2 implements Comparable<WuGui2>{
int age;
public WuGui2(int age) {
this.age = age;
}
@Override
public int compareTo(WuGui2 o) {
return this.age -o.age;
}
@Override
public String toString() {
return "WuGui2{" +
"age=" + age +
'}';
}
List<WuGui2> WuGuis = new ArrayList<>();
WuGuis.add(new WuGui2(1000));
WuGuis.add(new WuGui2(2999));
Collections.sort(WuGuis);
for(WuGui2 wg : WuGuis){
System.out.println(wg);
}
对List集合中元素排序,需要保证List集合中的元素实现了Comparable接口。
(2)Collecitons实现对Set进行排序
Collections是不能直接对Set进行排序的,这时候需要将Set集合转换成List集合。
Set<String > s1 =new HashSet<>();
s1.add("abd");
s1.add("abc");
s1.add("abe");
List<String> l1 = new ArrayList(s1);
Collections.sort(l1);
for(String l2 :l1){
System.out.println(l2);
}
abc
abd
abe
总结:现在终于对集合有了大致的了解,首先是集合是干什么的,既然集合是存储数据的,那么和数组有什么关系,有Map和Collection他们都有什么特点,他们都是怎么进行遍历的,在Map集合 中HashMap为什么要重写hashCode和equals方法,以及get和put方法存储数据的原理是什么等等,还有最大的感受是在学Java之前一定要把数据结构学好了。