泛型

泛型的好处

image.png
image.png

泛型介绍

image.png
4. 可以在类声明时通过一个标识表示类中某个类的属性,或者是某个方法返回值的类型,或者是参数类型。

  1. public static void main(String[] args) {
  2. Person<String> a = new Person<String>("ss");
  3. }
  4. class Person<E>{
  5. E s;
  6. public Person(E s){
  7. this.s = s;
  8. }
  9. public E f(){
  10. return s;
  11. }
  12. }

特别强调:E具体的数据类型在定义Person对象的时候指定,即在编译期间就确定E是什么类型。

泛型的声明与实例化

image.png
image.png
类似C++的模板。
这里对HashMap的遍历进行泛型改写:!!!!

  1. public static void main(String[] args) {
  2. HashMap<String,Student> map = new HashMap<String,Student>(); //带泛型的声明
  3. map.put("ss",new Student("ss",20));
  4. map.put("mm",new Student("mm",19));
  5. map.put("hh",new Student("hh",30));
  6. //Set set = map.entrySet(); 不使用泛型的写法
  7. Set<Map.Entry<String, Student>> set = map.entrySet();
  8. //Set里面的结点类型就是它的泛型,为Map.Entry<String, Student>
  9. Iterator<Map.Entry<String, Student>> iterator = set.iterator();
  10. //迭代器指向的结点类型就是它的泛型,为Map.Entry<String, Student>
  11. while (iterator.hasNext()) {
  12. Map.Entry<String,Student> entry = iterator.next(); //原先是Object
  13. System.out.println(entry.getKey() + " " +entry.getValue());
  14. }
  15. for (Map.Entry<String, Student> entry : set) { //就不用Object了
  16. System.out.println(entry.getKey() + " " + entry.getValue());
  17. }
  18. }

这里有一个很好的写法:带泛型的数据类型(如HashMap)声明子类就用var,就不用泛型担心出错了。
image.png
image.png
查看源码可以发现,HashMap有的泛型结构,HashSet和Iterator有的泛型结构。

使用细节

image.png
2. 在给泛型具体类型后,可以传入该类型或者其子类类型。

  1. public static void main(String[] args) {
  2. Dog<A> a = new Dog<>(new A()); //用 new Dog<A>(new A()).var 快捷键
  3. Dog<A> b = new Dog<>(new B()); //输入子类
  4. }
  5. class A{}
  6. class B extends A{}
  7. class Dog<E>{
  8. E e;
  9. public Dog(E e) {
  10. this.e = e;
  11. }
  12. }

3. 泛型推荐使用形式

  1. List<Integer> list1 = new ArrayList<Integer>(); // 不推荐
  2. List<Integer> list1 = new ArrayList<>(); //推荐,因为编译器会自己进行类型推断

4. 泛型默认为Object

  1. ArrayList a = new ArrayList();
  2. ArrayList<Object> b = new ArrayList<>(); //两者等价

如果一个类A里有另外一个类B的对象,并且A需要用B的属性进行排序,最好在B中写一个compare方法(教程上说实现Comparable接口的compareTo方法,但是自己定义也行)。

  1. public int compare(MyDate a,MyDate b){
  2. int h = a.year*365 + a.month*30 + a.day;
  3. int t = b.year*365 + b.month*30 + b.day;
  4. return h-t;
  5. }

自定义泛型

image.png

  1. class Tiger<T,R,M>{ //Tiger后面泛型,一般把这种类就叫做自定义泛型类
  2. R r;
  3. M m;
  4. T t; // 属性使用泛型
  5. // T,R,M是泛型的标识符,一般是单个大写字母,可以有多个
  6. public Tiger(R r, M m, T t) { //构造器使用泛型
  7. this.r = r;
  8. this.m = m;
  9. this.t = t;
  10. }
  11. public void setR(R r){ //方法使用泛型
  12. this.r = r;
  13. }
  14. public M getM(){ //返回类使用泛型
  15. return m;
  16. }
  17. }

重点解释第二点和第三点。

  1. T[] ts = new T[5]; //报错,因为数组在new时不能确定T的类型,因此无法在内存开辟空间
  2. static R r2;
  3. //因为静态是和类相关的,在类加载时,对象还没有创建(也即r2没有创建)
  4. //所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化

自定义泛型接口

image.png

  1. interface Usb<U,R>{ //自定义接口泛型
  2. E e; //错误,因为接口里的属性默认为 public static final,为静态属性
  3. R get(U u);
  4. void hi(R r);
  5. default R method(U u){ //jdk8中使用默认方法
  6. System.out.println("默认方法");
  7. return null;
  8. }
  9. }

1. 在继承接口中指定泛型接口的类型

  1. interface IA extends Usb<String,Double>{}
  2. //当我们实现IA接口时,指定了U为String,R为Double
  3. //因此实现Usb接口的方法时,使用String替换U,使用Double替换R
  4. class AA implements IA{
  5. @Override
  6. public Double get(String s) {
  7. return null;
  8. }
  9. @Override
  10. public void hi(Double aDouble) {}
  11. }

2. 实现接口时确定

  1. class BB implements Usb<String,Double>{
  2. //实现接口时,直接指定泛型接口的类型
  3. @Override
  4. public Double get(String s) {
  5. return null;
  6. }
  7. @Override
  8. public void hi(Double aDouble) {
  9. }
  10. }

3. 不写默认为Object(但是会出现警告)

  1. class CC implements Usb{
  2. //等价 class CC implements Usb<Object,Object>,最好还是写上!
  3. ...
  4. }

自定义泛型方法

image.pngimage.png
注:2类型的确定只针对一次调用,可以输入一个Integer,下一条语句还可以输入一个Double.

  1. public<E> fly(E e){...}
  2. fly(10); //此时E是Integer
  3. fly("ccc") //此时E是String
  1. class Car{ //普通类
  2. public<T,R> void fly(T t,R r){} //泛型方法
  3. }
  4. class Fish<T,R>{ //泛型类
  5. public <U,M> void eat(U u,M m,T t,R r){} //泛型方法
  6. //可以使用类声明的泛型,也可以使用自己声明的泛型
  7. public void hi(T t){} //注意这是 使用了泛型的普通方法,并不是泛型方法
  8. }

泛型的继承和通配符

image.png

  1. List<Object> a = new LinkedList<String>(); //错误,String不能赋给Object

对不限于直接父类的解释:比如AA继承BB,Object就不是AA的直接父类(类似于爷爷类),但是也可以传入。

  1. class BB{}
  2. class AA extends BB{} //Object就不是AA的直接父类
  3. public void print(List<?> c){} //随意
  4. public void print1(List<? extends AA> c){} //AA及AA的子类们
  5. public void print2(List<? super AA> c){} //AA及AA的父类们 可以传入Object

Collection

Collection接口有两个重要的子接口 List和Set,实现子类都是单列集合。

集合体系图

image.png
image.png

Collection常用方法

Collection常用方法对于Collection以及下属子类都可以用,这里用ArrayList演示。
1. add: 添加单个元素

  1. ArrayList a = new ArrayList();
  2. a.add(10); //注意放入的是Integer类,其余同理
  3. a.add(true);
  4. System.out.println(a); // [10, true]

2. remove:删除指定元素
3. contains:查找元素是否存在
4. size:获取元素个数
5. isEmpty:判断是否为空
6. clear:清空

  1. a.remove(true);
  2. System.out.println(a); // [10]
  3. System.out.println(a.contains(10)); //true
  4. System.out.println(a.size()); // 1
  5. System.out.println(a.isEmpty()); //false
  6. a.clear();

7. allAll:添加多个元素(Collection集合)
8. containsAll:查找多个元素是否都存在
9. removeAll:删除多个元素

  1. ArrayList a = new ArrayList();
  2. ArrayList b = new ArrayList();
  3. b.add(20);
  4. b.add(30);
  5. a.addAll(b); //只能添加集合类
  6. System.out.println(a.contains(b));
  7. a.removeAll(b);

遍历方式

1. 使用Iterator迭代器
image.png

  1. Iterator iterator = a.iterator(); //得到一个集合的迭代器
  2. iterator.hasNext(); //判断是否还有下一个元素
  3. iterator.next() // 1.将迭代器往下移动 2.将下移后迭代器指向位置上的元素返回(刚开始在第一个元素的上面一格)

image.png
image.png
迭代器while循环的构造快捷键—— itit

  1. public static void main(String[] args) {
  2. ArrayList a = new ArrayList();
  3. for(int i = 0;i<10;i++){
  4. a.add(i+10);
  5. }
  6. Iterator iterator = a.iterator(); //迭代器构造方法
  7. while (iterator.hasNext()) { //快捷键:itit
  8. Object obj = iterator.next();
  9. System.out.println(obj);
  10. } //当while循环结束时,iterator指向最后一个元素
  11. iterator = a.iterator(); //重置迭代器
  12. }

2. 增强for循环(foreach)
增强for循环的底层仍然是迭代器,因此可以理解成简化版本的迭代器遍历。快捷键:I 或者 集合名.for(推荐)。

  1. public static void main(String[] args) {
  2. ArrayList a = new ArrayList();
  3. for(int i = 0;i<10;i++){
  4. a.add(i+10);
  5. }
  6. for (Object obj : a) {
  7. System.out.println(obj);
  8. }
  9. }

image.png

List集合

常用方法

image.pngimage.png
注意这是List集合独有的方法,并且一旦涉及到范围(比如subList方法),总是左闭右开的。List不能单独声明(因为是一个接口),需要用到List的实现子类。

  1. List a = new ArrayList();
  2. List b = new LinkedList();
  3. List c = new Vector();

ArrayList

注意事项

  1. ArrayList 可以加入多个null。
    2. ArrayList 是由数组来实现数据存储的。
    3. ArrayList 基本等同于Vector,但ArrayList是线程不安全的(没有synchronized),在多线程情况下,不建议使用ArrayList。

    ArrayList底层结构

  2. ArrayList 中维护了一个Object类型的数组elementData。
    trannsient Object[] elementData; // transient 表示瞬间,短暂的,表示该属性不会被序列化
    2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
    3. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。

    ArrayList底层源码

    image.png
    image.png
    image.png
    image.png
    image.png
    如果数组为初始分配数组,那么就返回最小需求容量和10之间的最大值(注意有初始化参数的并不是DEFAULTCAPACITY…这个数组)。否则返回最小需求容量。
    image.png
    1. modCount 记录集合被修改的次数 2. 如果elementData的大小不够(目前的长度小于最小需求容量),就调用grow方法去扩容。
    image.png
    1. private void grow(int minCapacity) {
    2. // overflow-conscious code
    3. int oldCapacity = elementData.length; //原先数组的长度
    4. // >>1表示/2,把原数组长度*1.5,赋给newCapacity
    5. int newCapacity = oldCapacity + (oldCapacity >> 1);
    6. if (newCapacity - minCapacity < 0)
    7. newCapacity = minCapacity; //针对第一次,newCapacity为0(0 + 0*0.5),此时minCapacity为10
    8. if (newCapacity - MAX_ARRAY_SIZE > 0)
    9. //如果超过了系统最大容量,用hugeCapacity判断
    10. newCapacity = hugeCapacity(minCapacity);
    11. elementData = Arrays.copyOf(elementData, newCapacity); //用Arrays类的copyOf方法(空余位置为null)
    12. }
    image.png
    image.png
    扩容后Debug时看不到数组的null?把 Hide null elements in arrays and collections 和 Enable alternative view for Collections classes去掉。

    Vector

    基本介绍

    image.png

    ArrayList和Vector比较

    image.png

    LinkedList

    说明

    image.png
    image.png
    注:LinkedList维护的双向链表没有头结点,第一个就是首元结点。remove方法默认删除第一个结点。里面的元素是LinkedList类里定义的Node。
    image.png
    遍历方法有:迭代器,增强for与普通for循环。
    1. public static void main(String[] args) {
    2. LinkedList a = new LinkedList();
    3. Iterator iterator= a.iterator();
    4. while (iterator.hasNext()) { //迭代器
    5. Object next = iterator.next();
    6. ...;
    7. }
    8. for (Object o : a) { //增强for
    9. ...;
    10. }
    11. for(int i = 0;i<a.size();i++){ //普通for循环
    12. ...;
    13. }
    14. }

    初始化源码

    1. void linkLast(E e) {
    2. final Node<E> l = last; //保存尾指针
    3. final Node<E> newNode = new Node<>(l, e, null); //三个参数分别对应prior,data,next
    4. last = newNode; //更新尾指针
    5. if (l == null)
    6. first = newNode; //如果原先尾指针为null,说明newNode为第一个结点,修改first
    7. else
    8. l.next = newNode; //否则把新结点接在最后一个结点后面
    9. size++;
    10. modCount++;
    11. }

    ArrayList和LinkedList比较

    image.png

    Set集合

    基本介绍

    image.png 注:取出的顺序是固定的,不会变。遍历方法:迭代器,增强for,不能用for循环因为没有索引,也没有get方法。
    LinkedHashSet可以使添加和取出的顺序一致(通过双向链表实现)。

    HashSet

    底层是HashMap 使用 Hash + equals 方法
    image.pngimage.pngimage.png
    1. public HashSet() {
    2. map = new HashMap<>(); //创建一个HashMap
    3. }

    HashSet的add方法

    1. 先获取元素的哈希值(hashCode方法) 2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号。 3. 如果该位置上没有其他元素,则直接存放4. 如果该位置上已经有其他元素,则需要进行equals判断,如果相等则不再添加。如果不相等,则以链表形式添加。
    image.png
    其中PRESENT是一个Object对象,只起到占位的作用。
    image.png
    key是输入的关键字,value就是PRESENT。hash方法计算出key的哈希值,注意并不是简单调用了hashCode方法,而是与 h>>>16进行了异或,计算的伪哈希值,最终在putVal方法中用 按位与 计算出索引。
    image.png 重点是理解 putVal这个方法,源码自己去看,这里只写说明。resize方法用于修改Node数组大小。afterNodeInsertion(evict) 无实际用处,是留给子类实现的方法。
    1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    2. Node<K,V>[] tab; Node<K,V> p; int n, i;
    3. if ((tab = table) == null || (n = tab.length) == 0)
    4. n = (tab = resize()).length;
    5. //table是HashMap的属性,是放Node的数组。刚开始table为null,resize对tab初始化,扩容到16个空间
    6. if ((p = tab[i = (n - 1) & hash]) == null) //计算出真正的索引,把对应位置赋给p
    7. //如果p为空,说明没有元素,创建一个结点,把内容放进去。
    8. tab[i] = newNode(hash, key, value, null);
    9. else {
    10. Node<K,V> e; K k;
    11. if (p.hash == hash &&
    12. ((k = p.key) == key || (key != null && key.equals(k))))
    13. e = p;
    14. //准备添加的key的hash值与当前索引位置对应链表的首元结点hash值相同。
    15. //并且满足下面两个条件之一:1. p指向的Node结点的key和准备加入的key是同一个对象
    16. // 2. p指向的Node结点的key用equals方法和准备加入的key比较后相同 此时不能加入,e指向p,不做任何处理
    17. else if (p instanceof TreeNode)
    18. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    19. // 如果p是红黑树,那么调用putTreeVal方法加入
    20. else {
    21. // 最后一种情况,说明虽然位置被占,但是与首元结点不相同,找首元结点对应的链表
    22. for (int binCount = 0; ; ++binCount) { //开始遍历链表
    23. if ((e = p.next) == null) {
    24. p.next = newNode(hash, key, value, null);// 依次和该链表的每一个元素比较后,都不相同(到了最后一个结点),则加入到该链表的最后
    25. if (binCount >= TREEIFY_THRESHOLD - 1) // 注意在把元素添加到链表后,立即判断该链表是否已经达到8个结点(也就是>=7)
    26. treeifyBin(tab, hash);// 如果已经达到,就调用 treeifyBin()。在这个方法里,要先进行判断 if(tab == null || (n=tab.length)<MIN_TREEIFY_CAPACITY) 也就是看table是否为空或者是否小于64,如果成立,先table扩容。如果不成立才转成红黑树。
    27. break;
    28. }
    29. if (e.hash == hash &&
    30. ((k = e.key) == key || (key != null && key.equals(k))))
    31. break;
    32. // 和该链表的每一个元素比较过程中,如果有相同情况,就直接break(判断条件与上方一致)
    33. p = e;
    34. }
    35. }
    36. if (e != null) { // 说明有重复元素(主要针对HashMap,用于覆盖)
    37. V oldValue = e.value; //记录value,HashSet都是PRESENT,但HashMap是自己定义的
    38. if (!onlyIfAbsent || oldValue == null)
    39. e.value = value; //HashMap要覆盖,HashSet就不用了,因为PRESENT是null
    40. afterNodeAccess(e);
    41. return oldValue; //add失败(不是null)
    42. }
    43. }
    44. ++modCount; //记录修改次数
    45. if (++size > threshold) //threshold在resize方法中,是表的临界值(初始12)
    46. resize(); //如果size大于临界值,扩容
    47. afterNodeInsertion(evict);//留给子类实现的方法,对HashMap来说是个空方法
    48. return null;//返回空 成功
    49. }
    要注意的一个地方:table扩容的两个时机—— 1. 大于临界值 2. 链表加入结点并且大于8个
    1. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    2. resize(); // treeifyBin的部分代码
    3. // 这告诉我们,如果链表上已经超过8个结点,但是table还没达到64,那么会先扩容

    重写判断是否加入的方法

    下面这个是需求:
    image.png
    需要重写Employee类的equals方法和hashCode方法(直接输入equals)
    image.png
    image.pngimage.png
    注意这样生成的方法就是重写后的方法,不用更改。 ```java @Override
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Employee employee = (Employee) o;
    return age == employee.age && Objects.equals(name, employee.name);
    }

@Override
public int hashCode() {
return Objects.hash(name, age); //name,age等属性都被塞进一个object数组里
}

  1. **如果类里面有别的类属性(比如A里面有B类的对象),那么B里面也要重写equalshashCode方法 **
  2. <a name="fj0x1"></a>
  3. ### LinkedHashSet
  4. <a name="pjLFJ"></a>
  5. #### 说明
  6. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23175776/1641792663271-b524bce4-b0cd-4d37-a851-658b4eed85c1.png#clientId=uce2aa2a4-ec39-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=281&id=u2c7d1fcc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=763&originWidth=1602&originalType=url&ratio=1&rotation=0&showTitle=false&size=1375989&status=done&style=none&taskId=uc64287aa-9114-4ba7-8a51-c80091e9ad0&title=&width=590)
  7. <a name="y91zw"></a>
  8. #### 底层机制示意图
  9. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23175776/1641792664204-257fb8b9-5a76-433a-83f6-a3a7945512a7.png#clientId=uce2aa2a4-ec39-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uca483748&margin=%5Bobject%20Object%5D&name=image.png&originHeight=960&originWidth=1907&originalType=url&ratio=1&rotation=0&showTitle=false&size=1865079&status=done&style=none&taskId=ubb705f9f-a24a-4365-aa3e-2d29ad960dd&title=)<br />(结点应该放在绿色框里,这里画的不清楚)<br />**1. LinkedHashSet 加入顺序和取出元素的顺序一致。**<br />**2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)**<br />3. LinkedHashSet 底层结构—— 数组table + 双向链表<br />4. 添加第一次时,**直接将数组table扩容到16**,数组table是 HashMap$Node[]类型,但存放的结点类型是 LinkedHashMap$Entry(多态),**是Node的子类(可以从左下角的structure查看)**
  10. ```java
  11. static class Entry<K,V> extends HashMap.Node<K,V> {
  12. Entry<K,V> before, after;
  13. Entry(int hash, K key, V value, Node<K,V> next) {
  14. super(hash, key, value, next);
  15. }
  16. }

TreeSet(TreeMap)

基本介绍

  1. 如果只有key,没有伴随数据value,可以使用TreeSet结构
  2. 如果既有key,又有伴随数据value,可以使用TreeMap结构(他们两个底层结构相同)
  3. 有序表和哈希表的区别是:有序表把key按照顺序组织起来(内部是有序的),而哈希表完全不组织。
  4. 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同。
  5. 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是数据类型的大小。
  6. 如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小(一个指针,8字节)

    操作API

    集合与泛型 - 图48

    构造方法

    正常的TreeSet声明应该是这样的:

    1. TreeSet a = new TreeSet();

    但是TreeSet有一个构造器,可以传入一个比较器Comparator(匿名内部类)

    1. public static void main(String[] args) {
    2. TreeSet a = new TreeSet(new Comparator() { //匿名内部类
    3. @Override
    4. public int compare(Object o1, Object o2) {
    5. return ((String)o1).compareTo((String)o2); //按照字符串大小比较
    6. }
    7. });
    8. a.add("jack");
    9. a.add("a");
    10. a.add("sss");
    11. a.add("mmm");
    12. System.out.println(a);// [a, jack, mmm, sss]
    13. }

    要注意一个问题:假设要求按照字符串的length来从小到大排序

    1. public static void main(String[] args) {
    2. TreeSet a = new TreeSet(new Comparator() {
    3. @Override
    4. public int compare(Object o1, Object o2) {
    5. return ((String)o1).length() - ((String)o2).length(); //从小到大
    6. }
    7. });
    8. a.add("jack");
    9. a.add("a");
    10. a.add("sss");
    11. a.add("mmm");
    12. System.out.println(a); // [a, sss, jack]
    13. }

    可以发现 “mmm” 并没有加入进去,那么我们就需要追一下源码了。

    1. Entry<K,V> t = root;
    2. if (t == null) {
    3. compare(key, key); // type (and possibly null) check
    4. root = new Entry<>(key, value, null);
    5. size = 1;
    6. modCount++;
    7. return null;
    8. } // 初始化,生成一个新结点
    9. int cmp;
    10. Entry<K,V> parent;
    11. Comparator<? super K> cpr = comparator; //重点在这里,把比较器赋过去
    12. if (cpr != null) {
    13. do { //对整个链表(key)进行循环,给当前key找适当位置
    14. parent = t;
    15. cmp = cpr.compare(key, t.key); //比较原结点与要加入的结点的key
    16. //这里会动态绑定到匿名内部类对象
    17. if (cmp < 0)
    18. t = t.left;
    19. else if (cmp > 0)
    20. t = t.right; //按照比较结果移动指针
    21. else //遍历过程中发现准备添加的key和当前已有的key相等
    22. return t.setValue(value); //由于Set的value为PRESENT,因此相当于没加
    23. } while (t != null); //循环结束后,t就指向结点应该加入的位置
    24. //parent为上一次,因此结束后还需要再移动一次
    25. }
    26. ... //没有比较器的情况
    27. Entry<K,V> e = new Entry<>(key, value, parent);
    28. if (cmp < 0)
    29. parent.left = e;
    30. else
    31. parent.right = e; //按照比较结果把结点e放在正确位置(parent是原来的t)
    32. fixAfterInsertion(e);
    33. size++; //结点个数加一
    34. modCount++; //修改次数加一
    35. return null;
    36. }

    “sss” 是先加入的,长度为3。因为自定义的比较器是比较长度的,而 “mmm” 的长度也为3,因此结果为0,直接不加入了(参考do里面的else情况)。
    由于TreeSet的底层是TreeMap,因此比较器初始化方法在TreeMap里。 ```java public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
    }

public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}

  1. **TreeSetTreeMap的底层都是TreeMap,因此TreeMap的源码不再做解析。**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23175776/1641794377162-82d82fe1-e15f-4f18-b494-8709e7923a43.png#clientId=uce2aa2a4-ec39-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=Z7RYD&margin=%5Bobject%20Object%5D&name=image.png&originHeight=409&originWidth=1845&originalType=url&ratio=1&rotation=0&showTitle=false&size=1176117&status=done&style=none&taskId=u781ab7e3-ce9e-41aa-8bf2-164dc2fa0d4&title=)
  2. <a name="xazgH"></a>
  3. #### 关于TreeSet加入自定义类
  4. ** 如果TreeSet没有重写Comparator,并且加入的类也没有实现Comparable接口,那么就会报错,因为add源码里需要赋予一个比较器。**
  5. ```java
  6. TreeSet a = new TreeSet();
  7. a.add(1); //这样是没有问题的,因为1相当于Integer,而Integer实现了Comparable接口
  1. public static void main(String[] args){
  2. TreeSet a = new TreeSet();
  3. a.add(new Car("AAA",2331313)); //报错 ClassCastException
  4. }
  5. class Car{
  6. String name;
  7. double price;
  8. public Car(String name, double price) {
  9. this.name = name;
  10. this.price = price;
  11. }
  12. }
  1. TreeSet a = new TreeSet(new Comparator() {
  2. @Override
  3. public int compare(Object o1, Object o2) {
  4. return 0;
  5. }
  6. }); //这样写,给TreeSet加入比较器,再加入Car类就没问题了
  7. class Car implements Comparable{ //这样写也没问题,Car类实现了Comparable接口
  8. String name;
  9. double price;
  10. public Car(String name, double price) {
  11. this.name = name;
  12. this.price = price;
  13. }
  14. @Override //重写compareTo方法
  15. public int compareTo(Object o) {
  16. return 0;
  17. }
  18. }

Map

都是双列集合,存放 K-V

集合体系图

image.png

接口特点

image.png
注:1. Set本来也是 Key - Value 结构,但是它的Value一直都是PRESENT,因此可以看作Key。
2. 因为key不允许重复,所以如果重复添加会导致覆盖。
3. 用 put 方法添加键值对,用 get 方法指定key返回value。
4. Hash表的操作可以视为常数阶。
5. Hash表的Key如果是基础类型(Integer等,包括String),那么Hash表会拷贝一份。如果是对象类型,那么一直都是8字节,保存的是地址。

内部创建EntrySet

k-v 最后是 HashMap$Node node = newNode(hash,key,value,null)。注意 k-v 为了方便程序员的遍历,还会在内部创建 EntrySet 集合,KeySet集合以及Values集合。其中KeySet里面存放key,Values(当然只是引用(地址)!不是真正的结点,真正的结点在Node中)。而EntrySet则是包括KeySet与Values,里面内容的运行类型为Node。
entrySet中,存放的元素的类型为Map.Entry(entrySet相当于一个数组)。真正存放k-v的地方还是 HashMap$Node。
为什么能够转型:因为 Node类实现了Entry接口。当把 Node对象存放到 entrySet 就方便我们的遍历,因为 Map.Entry 提供了重要的方法 getKey 以及 getValue。
如果只想使用key,那么就用KeySet。如果只想用value,那么用values。如果两者都想用,那么就用EntrySet。
image.pngimage.png

  1. public static void main(String[] args) {
  2. Map a = new HashMap();
  3. a.put("张三",18); //put方法加入键值对
  4. a.put("李四",20);
  5. a.put("王五",21);
  6. Set b = a.entrySet(); // 用entrySet遍历,entrySet父类是Set,因此用Set接收
  7. for (Object obj : b) {
  8. Map.Entry t = (Map.Entry)obj; //向下转型,编译类型为 Map.Entry
  9. System.out.println(t.getKey()+" " + t.getValue());
  10. }
  11. Set c = a.keySet(); //只能操作key
  12. Collection d = a.values(); //只能操作value
  13. }

基本方法

image.png

  1. public static void main(String[] args) {
  2. Map a = new HashMap();
  3. a.put("张三",18);
  4. a.put("李四",9);
  5. a.put("王五",31);
  6. a.put("尚",20);
  7. System.out.println(a);//{李四=9, 张三=18, 王五=31, 尚=20}
  8. a.remove("李四"); //根据某个Key删除结点
  9. System.out.println(a);//{张三=18, 王五=31, 尚=20}
  10. System.out.println(a.get("张三"));//18 根据Key返回value
  11. System.out.println(a.size());//3
  12. System.out.println(a.isEmpty());//false
  13. System.out.println(a.containsKey("王五"));//true 查找是否有这个Key
  14. a.clear(); //清除
  15. System.out.println(a.isEmpty());//true
  16. }

注:如果要修改元素的内容,也可以用put,因为可以覆盖。 P550

HashMap

遍历方法

会在泛型里进一步改写

用keySet

  1. public static void main(String[] args) {
  2. Map map = new HashMap(); //这里可以用泛型改写
  3. map.put("张三",18);
  4. map.put("李四",9);
  5. map.put("王五",31);
  6. map.put("尚",20);
  7. Set set = map.keySet(); //获取所有的key
  8. //第一种方法 增强for循环
  9. for (Object key : set) {
  10. System.out.println(key + "->" + map.get(key));//用get方法获取value
  11. }
  12. //第二种方法 迭代器
  13. Iterator iterator = set.iterator(); //获取set对象的迭代器
  14. while (iterator.hasNext()) {
  15. Object key = iterator.next();
  16. System.out.println(key + "->" + map.get(key));
  17. }
  18. }

用values

  1. Collection values = map.values(); //获取values
  2. for (Object key : values) { //增强for
  3. System.out.println(key); //注意 Map中没有从value获取key的方法
  4. }
  5. Iterator iterator = values.iterator(); //迭代器
  6. while (iterator.hasNext()) {
  7. Object key = iterator.next();
  8. System.out.println(key);
  9. }

用EntrySet

  1. Set set = map.entrySet(); // entrySet是Set的子类,是Map的内部类
  2. //1. 增强for
  3. for (Object entry : set) {
  4. // entry是Object类,需要向下转型(如果使用了泛型就不需要了)
  5. Map.Entry t = (Map.Entry)entry;
  6. System.out.println(t.getKey() + "->" + t.getValue());
  7. }
  8. //2. 迭代器
  9. Iterator iterator = set.iterator();
  10. while (iterator.hasNext()) {
  11. Map.Entry h = (Map.Entry)iterator.next(); //一步到位写法
  12. System.out.println(h.getKey() + "->" + h.getValue());
  13. }

P534 value为对象需要调用方法的情况

  1. for (Object o : s) {
  2. Map.Entry t = (Map.Entry)o;
  3. if(((Employee)t.getValue()).getSal()>18000) //注意方法前要加括号,作为一个整体
  4. if((Employee)t.getValue().getSal()>18000) //这样就是错的
  5. ...
  6. }
  7. for (Object o : s) { //可读性更强
  8. Map.Entry t = (Map.Entry)o;
  9. Employee ee = (Employee) t;
  10. if(ee.getSal()>18000)
  11. }

image.png

HashMap底层

image.png
image.png

为什么HashMap每次都要扩容2倍

除留余数法:
image.png
第一是因为哈希函数的问题
通过除留余数法方式获取桶号,因为Hash表的大小始终为2的n次幂(参照JavaGuide),因此可以将取模转为位运算操作,提高效率,容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,这也就是为什么要按照2倍方式扩容的一个原因

第二是因为是否移位的问题
是否移位,由扩容后表示的最高位是否1为所决定,并且移动的方向只有一个,即向高位移动。因此,可以根据对最高位进行检测的结果来决定是否移位,从而可以优化性能,不用每一个元素都进行移位,因为为0说明刚好在移位完之后的位置,为1说明不是需要移动oldCop,这也是其为什么要按照2倍方式扩容的第二个原因。

HashMap底层源码

因为HashSet底层就是HashMap,因此底层几乎完全相同。
1. 执行构造器 new HashMap(),初始化加载因子 loadfactor = 0.75,HashMap$Node[] table = null。

  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  3. }
  4. transient Node<K,V>[] table;

2. 执行put,调用hash方法,计算key的hash值(注意要进入一个方法最好使用 force step into)。

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }

3. 执行putVal 方法,具体注释已经在 HashSet中给出。

HashTable

基本介绍

image.png

扩容

底层有数组 Hashtable$Entry[],初始化大小为 11。临界值 threshold 为8 = 11 * 0.75。调用put方法里的 addEntry(hash, key, value, index); 当满足 if (count >= threshold) 扩容(rehash)

  1. int newCapacity = (oldCapacity << 1) + 1; //新容量计算方法
  2. threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); // 临界值计算方法

HashTable和HashMap对比

image.png

Properties

image.png
Java 读写Properties配置文件 - 旭东的博客 - 博客园 感兴趣可以看这篇文章。

注意事项

1. Properties 继承 Hashtable,是无序的。
2. 可以通过 k-v 存放数据,当然key和value不能为null。
3. 常用方法:增 put(key,value),删 remove(key),改 put(相同的key,value),查 get(key)。

开发中如何选择集合实现类

image.png
一组对象指的就是只有key,没有value。