13-集合(基本使用+源码分析)

1、引言

前面我们保存多个数据使用的是数组,但是数组也有很多不足的地方

  • 长度开始时必须指定,一旦指定,就不可以再更改
  • 保存的必须为同一类型的元素
  • 使用数组进行增加元素的示意代码 -比较麻烦

2、集合的框架体系

3、Collection&Map

3.1、常用方法(以ArrayList为例)

public class CollectionMethod {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        List list = new ArrayList();
        // 添加单个元素
        list.add("jack");
        list.add(10); // 相当于list.add(new Integer(10))
        list.add(true);
        System.out.println("list=" + list);
        // 删除指定元素
        list.remove(1); // 删除第一个元素 如果参数是int类型 remove的是指定索引
        list.remove("jack1"); // 指定删除某个元素
        System.out.println("list=" + list);
        // 查找元素是否存在
        System.out.println(list.contains("jack"));
        // isEmpty 集合是否为空
        System.out.println(list.isEmpty());
        // clear 清空集合
        list.clear();
        // addAll 添加多个元素
        ArrayList list2 = new ArrayList();
        list2.add("红楼梦");
        list2.add("三国演义");
        list.addAll(list2);
        System.out.println("list=" + list);
        // containsAll:查找多个元素是否都存在
        System.out.println(list.containsAll(list2));
        // removeAll: 删除多个元素
        list.add("聊斋");
        list.removeAll(list2);
        System.out.println("list=" + list);
    }
}

3.2、迭代器

  • Iterator对象称为迭代器,主要用于遍历Collection集合中的元素
  • 所有实现了Collection接口的集合类都有一个iterator方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
  • Iterator仅用于遍历集合,其本身并不存放对象

package collection;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class CollectionIterator {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        Collection col = new ArrayList();
        col.add(new Book("三国演义", "罗贯中", 10.1));
        col.add(new Book("小李飞刀", "古龙", 5.1));
        col.add(new Book("红楼梦", "曹雪芹", 34.6));
        // 遍历集合
        /**
         * 1. 先得到集合对应的迭代器
         */
        Iterator iterator = col.iterator();
        while (iterator.hasNext()) {
            // 返回下一个元素,类型是Object
            // 虽然编译类型是Object 但是运行类型仍然是Book
            Object obj = iterator.next();
            System.out.println("obj=" + obj);
        }
        /**
         * 快速生成while + 迭代器循环 :itit
         * ctrl + j 所有快捷提示键
         */
        while (iterator.hasNext()) {
            Object next =  iterator.next();
        }
        // 3. 当退出while循环后 这时iterator迭代器 会指向最后的元素 NosuchElementException
        iterator.next();
        // 4. 如果希望再次遍历 需要重置迭代器 将迭代器的指向重新指向第一个元素
        iterator = col.iterator();

    }

}

class Book {
    private String name;
    private String author;
    private double price;
    public Book(String name, String author, double price) {
        this.name = name;
        this.author = author;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                '}';
    }
}

3.3、List专用方法

public class Collection04ArrayList {
    @SuppressWarnings("all")
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("张三丰");
        list.add("贾宝玉");
        list.add("贾宝玉1");
        // add : 在index位置上插入ele元素
        list.add(1, "韩顺平"); // 不带index默认加到最后
        System.out.println("list=" + list);
        // indexof 返回obj在集合中首次出现的位置
        System.out.println(list.indexOf("贾宝玉"));
        // lastIndexOf 返回obj在集合中最后一次出现的位置
        System.out.println(list.lastIndexOf("贾宝玉"));
        // remove 移除指定index位置的元素(后面的元素往前移动一个元素) 并返回此元素
        System.out.println(list.remove(1));
        System.out.println(list.get(1));
        // set 设置指定index位置的元素为ele,相当于替换
        // 与add不同的是 set是替换 add是在硬塞
        list.set(1, "玛丽");
        System.out.println(list);
        // subList 返回从fromIndex 到 toIndex 位置的子集合
        List returnList = list.subList(0, 2);
        System.out.println("returnList=" + returnList);
    }
}

3.4、ArrayList

元素可以为空

3.5、ArrayList源码跟踪

3.6、Vector

 

3.7、LinkedList初识

 

 

package collection;

public class Collect05LinkedList {
    public static void main(String[] args) {
        Node jack = new Node("jack");
        Node tom = new Node("tom");
        Node hsp = new Node("hsp");
        // 连接三个节点 形成双向链表
        jack.next = tom;
        tom.next = hsp;
        hsp.pre = tom;
        tom.pre = jack;

        Node first = jack;
        Node last = hsp;

        // 演示 遍历
        while (true) {
            if (first == null) {
                break;
            }
            System.out.println(first);
            first = first.next;
        }

        // 演示链表添加节点
        Node smith = new Node("smith");
        smith.next = hsp;
        smith.pre = tom;
        hsp.pre = smith;
        tom.next = smith;

        // 添加完smith后
        first = jack;
        while (true) {
            if (first == null) {
                break;
            }
            System.out.println(first);
            first = first.next;
        }

    }
}
// 定义一个Node类 表示双向链表的一个节点
class Node {
    public Object item;
    public Node next;
    public Node pre;
    public Node(Object name) {
        this.item = name;
    }
    public String toString() {
        return "Node name=" + item;
    }
}

3.8、LinkedList源码

package collection;

import java.util.LinkedList;

public class Col07LinkedListCrud {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        // 新增
        linkedList.add(1);
        linkedList.add(2);
        // 删除
        // 不带参数默认删除第一个节点
        // 带参数删除指定节点
        linkedList.remove();
        linkedList.remove(1);
        System.out.println(linkedList);
        // 修改某个节点对象
        linkedList.set(1, 999);
    }
}

3.9、Set

3.10、HashSet

package collection;


import java.util.HashSet;
import java.util.Set;

@SuppressWarnings("all")
public class Collection07Set {
    public static void main(String[] args) {
        /*Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");
        set.add("jack");
        set.add(null);
        set.add(null);*/

        /**
         * HashSet源码解析
         * 1、 public HashSet() {
         *         map = new HashMap<>();
         * }
         * 2、HashSet可以存放空值,但是只能存一个空值,即元素不可以重复
         *
         */
        Set set = new HashSet<>();
        System.out.println(set.add("john"));
        System.out.println(set.add("lucy"));
        System.out.println(set.add("john"));
        System.out.println(set.add("jack"));
        System.out.println(set.add("rose"));
        set.remove("john");
        set.clear();

        set.add("lucy");
        set.add("lucy");
        set.add(new Dog("Tom"));
        set.add(new Dog("Tom"));
        System.out.println("set=" + set); // 最终返回的仍然是两个Tom 因为这两个对象在堆中是不一样的

        // 继续上强度
        set.clear();
        set.add(new String("hsp"));
        set.add(new String("hsp")); // 这样是加不进去了 为什么呢? new String不是两个对象吗?这里要看源码才能知道原因
        System.out.println("set=" + set);

    }
}
class Dog {
    private String name;
    public Dog(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

HashSet:

1、HashSet底层是HashMap

2、添加一个元素时,先得到hash值,然后将hash值转化为索引值

3、找到存储数据表table,看这个索引位置是否已经有元素存在

4、如果没有,就直接加入

5、如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后,这里一定要注意,equals是程序员决定的,换言之,程序员可以通过重写equals方法决定什么才是想等,什么才是不相等!

6、在Java8中,如果一条链表的元素个数超过8,并且table的大小大于等于64,就会树化,转为红黑树

        /**
         * 1、 构造器实际上new了个hashMap
         * public HashSet() {
         *         map = new HashMap<>();
         * }
         * 2、add方法
         * public boolean add(E e) {
         *         return map.put(e, PRESENT)==null;
         * }
         * 3、执行put 得到key对应的hash值
         * public V put(K key, V value) {
         *         return putVal(hash(key), key, value, false, true);
         * }
         * 4、hash的过程
         * static final int hash(Object key) {
         *         int h;
         *         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         * }
         * 5、putVal(前方高能)
         * final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
         *                    boolean evict) {
         *         Node<K,V>[] tab; Node<K,V> p; int n, i;
         *         // 代表了第一次扩容
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *         // 1、根据key 得到hash 去计算该key应该存放到table表的哪个索引位置 并吧这个位置的对象 赋给P
         *         // 2、判断p 是否为 null
         *         // 2.1、如果p是空 表示这个位置还没有存元素,就创建一个node
         *         // 2.2、如果p不是空 就放在该位置
         *         if ((p = tab[i = (n - 1) & hash]) == null)
         *             tab[i] = newNode(hash, key, value, null);
         *         else {
         *             // 一个开发技巧提示:在需要局部变量(辅助变量)的时候,哪里需要哪里创建 不要一开始就咔咔咔创建一大推
         *             Node<K,V> e; K k;
         *             // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
         *             // 并且满足 下面两个条件之一的
         *             // 1、准备加入的key 和 p 指向的Node节点 的key 是同一个对象
         *             // 2、p指向的node节点的key的equals和准备加入的key比较后相同 (就是说怎么比较 是程序员自己决定的 就比如key类型是String,比较实际上String重写了equals方法,比较的实际上是内容而不是地址)
         *             // 如果key是对象 你完全可以重写里面的equals方法 编写你自己自定义的equals规则
         *             if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
         *                 e = p;
         *             // 判断key是不是一颗红黑树
         *             // 如果是一颗红黑树 就调用putTreeVal方法
         *             else if (p instanceof TreeNode)
         *                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         *             // 如果table对应的索引位置,已经是一个链表,该步就是循环链表里的元素 看看要插入的这个元素是否跟链表里的元素相同的,然后就会有两种情况
         *             // 1、依次和链表里的每一个元素比较后,都不相同,则加到链表的屁股后面 然后break
         *             //   1.1、注意在把元素添加到链表后,立即判断,该链表是否已经达到了8个节点,如果达到,就调用红黑树(treeifyBin(tab, hash))树化,对当前这个链表进行树化,如果table数组长度小于64,不会树化
         *             //   if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
         *                  resize();
         *                  如上代码所示,如果table长度小于64,会先对table扩容
         *                                如果table长度大于64,才进行转成红黑树
         *             // 2、依次和链表里每一个元素比较后,如果有相同的情况 直接break
         *             else {
         *                 for (int binCount = 0; ; ++binCount) {
         *                     if ((e = p.next) == null) {
         *                         p.next = newNode(hash, key, value, null);
         *                         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;
         *     }
         *
         *
         */

3.10.1、hashSet的扩容调试

package collection;

import java.util.HashSet;
import java.util.Objects;

public class Collection09SetIncrement {
    public static void main(String[] args) {
        /**
         * HashSet底层是hashmap 第一次添加时,table数组扩容到16
         * 临界值是 16*装载因子(0.75) = 12
         * 如果table数组使用到了临界值12 就会扩容到 16 * 2 = 32
         * 新的临界值就是 30*0.75=24 依次类推
         */
        HashSet<Object> hashSet = new HashSet<>();
        /*for (int i = 1; i <= 100; i++) {
            hashSet.add(i); // 1 2 3 4 5 ... 100
        }
        hashSet.clear();*/
        /**
         * 什么时候才能达到阈值呢?
         * 是看table的个数吗? 还是看所有table上的链表/红黑数的节点总数?
         * 答案是后者
         */
        for (int i = 1; i <= 12; i++) {
            hashSet.add(new A(i));
        }
        System.out.println("hashSet=" + hashSet);
    }
}

class A {

    private int n;
    public A (int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100;
    }
}

3.10.2、练习1

package collection;

import java.util.HashSet;
import java.util.Objects;

public class Collection10SetPractice {

    /**
     * 1、创建三个Employee对象放入HashSet中
     * 2、当 name 和 age 的值相同时,认为是相同员工,不能添加到HashSet中
     * @param args
     */
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("milan",3));
        hashSet.add(new Employee("smith",3));
        hashSet.add(new Employee("milan",3));
        System.out.println(hashSet);
    }

}
class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @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);
    }
}

3.11、LinkedHashSet

特点:有顺序!

3.12、HashMap

3.13、HashMap底层机制

扩容示意图

树化示意图

源码分析:

package collection;

import java.util.HashMap;

@SuppressWarnings({"all"})
public class Collection12HashMap {
    public static void main(String[] args) {
        /*HashMap map = new HashMap();
        // 相同的key替换
        map.put("no1","韩顺平");
        map.put("no2","张无忌");
        map.put("no1","张三丰");
        map.put("no3","张三丰");
        map.put(null,null);
        map.put(null,"abc");
        map.put("no4",null);
        System.out.println(map); // {no2=张无忌, null=abc, no1=张三丰, no4=null, no3=张三丰}*/

        HashMap map = new HashMap();
        map.put("java", 10);
        map.put("php", 10);
        map.put("java", 20);
        System.out.println(map);

        // 解读hashMap源码
        /**
         * 1、执行构造器
         *    初始化加载因子 0.75
         *    hashMap$Node[] table = null
         * 2、执行put方法 调用hash方法计算出key的hash值
         *     public V put(K key, V value) {
         *         return putVal(hash(key), key, value, false, true);
         *     }
         * 3、执行 putVal 前方持续高能
         * final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
         *                    boolean evict) {
         *         Node<K,V>[] tab; Node<K,V> p; int n, i;
         *         // 如果底层的table 数组为null 或者length=0 就扩容到16
         *         if ((tab = table) == null || (n = tab.length) == 0)
         *             n = (tab = resize()).length;
         *         // 取出hash值对应的table表的索引位置的node节点 如果为空 就直接把加入的 k-v 创建成一个node 加入到该位置
         *         if ((p = tab[i = (n - 1) & hash]) == null)
         *             tab[i] = newNode(hash, key, value, null);
         *         else {
         *             Node<K,V> e; K k;
         *             // 如果table的索引位置的key的hash值和新加的key的hash值相同 并满足 (table现有的节点的key和准备添加的key是同一个对象 || equals返回为真)
         *             if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
         *                 e = p;
         *             else if (p instanceof TreeNode) // 如果当前的table的已有的node是红黑树 就按照红黑树的方式处理
         *                 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个 到了8个后 就调用treeifyBin树化
         *                         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 后面替换key value
         *                         break;
         *                     p = e;
         *                 }
         *             }
         *             if (e != null) { // existing mapping for key
         *                 V oldValue = e.value;
         *                 if (!onlyIfAbsent || oldValue == null)
         *                     e.value = value; // 替换key对应的value
         *                 afterNodeAccess(e);
         *                 return oldValue;
         *             }
         *         }
         *         ++modCount; // 每增加一个Node 就size++
         *         if (++size > threshold)
         *             resize();
         *         afterNodeInsertion(evict);
         *         return null;
         *     }
         *
         *
         *     5、关于树化 如果table表为空 或者大小还不够64 暂时不会树化 而是进行扩容
         *     另外还有一个剪枝的概念 就是如果红黑树的节点一直删除 删除到了树化的标准后 就开始剪枝,重新转化为链表
         *     final void treeifyBin(Node<K,V>[] tab, int hash) {
         *         int n, index; Node<K,V> e;
         *         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
         *             resize();
         *         else if ((e = tab[index = (n - 1) & hash]) != null) {
         *             TreeNode<K,V> hd = null, tl = null;
         *             ...
         *         }
         */


    }
}

3.14、HashTable

介绍

与HashMap平级

3.15、Properties

是hashtable的子类,简介实现了Map接口

3.16、如何选择正确的集合或者map?

3.17、TreeSet

带排序的Set,自定义排序规则

package collection;

import java.util.Comparator;
import java.util.TreeSet;

@SuppressWarnings({"all"})
public class Collection13TreeMap {
    public static void main(String[] args) {

        /**
         * 1、当我们使用无参构造器。创建TreeSet时,仍然是无序的
         * 2、如果我现在想按照字符串大小排序
         * 3、使用TreeSet提供的一个构造器 可以传入一个比较器 并指定规则
         * 4、简单看源码
         *
         * 1、构造器会把传入的比较器对象赋给TreeSet的底层的TreeMap属性 this.comparator
         * 2、在调用 treeSet.add("tom") 在底层会执行进行比较
         */
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String)o1).length() - ((String)o2).length(); // [a, sp, tom, jack]
                //return ((String)o1).compareTo((String)o2); // [a, jack, sp, tom]
            }
        });
        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("sp");
        treeSet.add("a");
        System.out.println(treeSet);

    }
}

3.18、TreeMap

仍然可以传比较器:

具体做法同TreeSet,比较的是键值(K)

3.19、工具类Collections

 

4、易错点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值