Java集合详解

Java集合详解

前言

集合的理解和好处

  1. 前面我们保存多个数据使用的是数组,数组有一些不足的地方,比如:
  2. 数组的不足之处
    • 长度开始时必须指定,而且一旦指定,不能更改
    • 保存的必须为同一类型的元素
    • 使用数组进行增加/删除元素的示意代码—比较麻烦
  3. 写出Person数组扩容示意代码
Person[] p1 = new Person[1];
p1[0] = new Person();
//增加新的Person对象
Person[] p2 = new Person[p1.length];//新建数组
for(){}//拷贝p1数组的元素到p2
p2[p2.length-1] = new Person();//添加新的对象
  1. 因此,我们就用到了集合,集合有很多有点
    • 可以动态保存任意多个对象,使用比较方便
    • 提供了一系列方便的操作对象的方法:add、remove、set、get等
    • 使用集合添加/删除新元素的示意代码–简洁明了

1. 集合框架体系

  1. Java的集合类有很多,主要分为两大类(单列集合、双列集合)
  2. collection接口有两个重要的接口 List Set ,他们实现的子类都是单列集合
  3. Map接口的实现子类 是双列集合,存放 K-V
  4. 详见下图:

2. Collection 接口和常用方法

2.1 Collection 接口实现类的特点

public interface Collection extends Iterable

  1. collection实现子类可以存放多个元素,每个元素可以是Object
  2. Collection的实现类,有些可以存放重复的元素,有些不可以
  3. Collection的实现类,有些是有序的(List),有些不是有序的(Set)
  4. Collection接口没有直接的实现子类,是通过他的子接口Set和List来实现的
  5. 因为Collection接口常用方法有很多,这里以实现子类 ArrayList 来演示
    • add 添加单个元素
    • remove 删除指定元素
    • contains 查找某个元素是否存在
    • size 获取元素个数
    • isEmpty 判断是否为空
    • clear 清空
    • addAll 添加多个元素
    • containsAll 查找多个元素是否都存在
    • removeAll 删除多个元素
  6. 代码如下:
public static void main(String[] args) {
        //以接口来接收 List
        List list = new ArrayList();
        //1. add 添加单个元素
        list.add("jack");
        list.add(10);
        list.add(true);
        System.out.println("List = "+list);//List = [jack, 10, true]

        //2. remove 删除指定元素
        list.remove(0);//指定删除第一个元素
        list.remove(true);//指定删除true
        System.out.println("List = "+list);//List = [10]

        //3. contains 查找某个元素是否存在
        //这里就直接放在一个代码表示了
        System.out.println(list.contains(10));//存在元素 0 所以输出true

        //4. size 获取元素个数
        System.out.println(list.size());//1

        //5. isEmpty 判断是否为空
        System.out.println(list.isEmpty());//false

        //6. clear 清空
        list.clear();
        System.out.println("List = "+list);//List = []

        //7. addAll 添加多个元素
        ArrayList list2 = new ArrayList();
        list2.add("金瓶梅");
        list2.add("贾宝玉");
        list.addAll(list2);
        System.out.println("List = "+list);//List = [金瓶梅, 贾宝玉]

        //8. containsAll 查找多个元素是否都存在
        System.out.println(list.containsAll(list2));//true

        //8. removeAll 删除多个元素
        list.add("西游记");
        list.add("武松");
        System.out.println("List = "+list);//List = [金瓶梅, 贾宝玉, 西游记, 武松]

        list.removeAll(list2);
        System.out.println("List = "+list);//List = [西游记, 武松]
    }

2.2 Collection 接口遍历元素方式 1——使用Iterator(迭代器)

2.2.1 基本介绍
  1. 我们都知道Collection的子类都是实现了Collection
  2. 但是Collection上面有一个父接口 Iterable
  3. 我们看 Iterable 源码
public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.//返回T类型元素的迭代器。
     *
     * @return an Iterator.//@返回一个迭代器
     */
    Iterator<T> iterator();
  1. 看源码所得:

    • Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素。

    • 所有实现了 Collection 接口的集合类都有一个iterator() 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器

    • Iterator 的结构图见迭代器的执行原理:

    • Iterator 仅用于遍历集合,Iterator 本身不存放对象

2.2.2 迭代器的执行原理
  1. 迭代器的执行原理

    • Iterator iterator = coil.iterator();//得到一个集合的迭代器

    • //hasNext(); 判断是否还有下一个元素

    • while(iterator.hasNext()){

    • //next()作用: ①指针下移②将下移以后集合位置上的元素返回

    • System.out.println(iterator.next());

    • }

2.2.3 Iterator 接口的方法
  • hasNext()
  • next()
  • remove
  • 如下图

  1. 调用Iterator 接口的方法时的注意点
    • 调用 Iterator.next() 方法之前必须调用 iterator.hasNext() 方法进行检测。
    • 如果不调用,且下一条记录无效,直接调用 Iterator.next() 会抛出异常
    • NoSuchElementException异常
  2. 下面对遍历集合进行简单的代码操作:
public class CollectionIterator {
    public static void main(String[] args) {
        Collection col = new ArrayList();
        col.add(new Book("小李飞刀", "古龙", 66));
        col.add(new Book("笑傲江湖", "金庸", 68));
        col.add(new Book("红楼梦", "曹雪芹", 55));
        System.out.println(col);

        //现在希望能够遍历 col 集合
        //1. 先得到 col 对于的迭代器
        Iterator iterator = col.iterator();
        //2. 使用 while 循环遍历
        while (iterator.hasNext()) {
            //返回下一个元素,类型是object
            Object next = iterator.next();
            System.out.println(next);
        }
        /*
        //显示所有快捷键的快捷键 ctrl + j
        //正常写while循环太慢了,这里有个快捷键 itit
        while (iterator.hasNext()) {
            Object next =  iterator.next();
        }
         */
        //3. 当退出 while 循环后,这时Iterator迭代器,指向最后的元素
        //iterator.next();//NoSuchElementException 无此类元素异常

        //4. 如果需要再次遍历,需要重置我们的迭代器
        iterator = col.iterator();//重置迭代器
        System.out.println("=======第二次遍历========");
        //再次遍历 col 集合
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
        }
    }
}

class Book {
    String name;
    String author;
    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 +
                '}';
    }
}

2.3 Collection 接口遍历对象方式 2——增强for 循环

2.3.1 基本介绍
  1. 增强for循环可以替代Iterator迭代器:增强for就是简化版的Iterator;本质是一样的,只能用于遍历集合或数组
  2. 基本语法
for(元素类型 元素名 : 集合名或者数组名){
    访问元素
}
  1. 案例演示
public class CollectionFor {
    public static void main(String[] args) {
        //抑制错误提示
        @SuppressWarnings({"all"})
        Collection col = new ArrayList();
        col.add(new Book("小李飞刀", "古龙", 66));
        col.add(new Book("笑傲江湖", "金庸", 68));
        col.add(new Book("红楼梦", "曹雪芹", 55));
        System.out.println(col);//原始输出

        //增强for循环
        //1. 增强for ,在collection集合
        //2. 增强for , 仍然是迭代器
        //3. 增强for ,就是简化版本的迭代器遍历
        //4. 这里的快捷键 I
        /*
        for(元素类型 元素名 : 集合名或者数组名){
            访问元素
        }
         */
        for (Object book : col) {
            System.out.println("book = " + book);
        }
    }
}

2.4 练习题

2.4.1 练习题1
  1. 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
  2. 用迭代器和增强 for 循环两种方式来遍历
  3. 重写 Dog 的 toString 方法, 输出 name 和 age
  4. 代码如下:
public class CollectionExercise01 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Dog("旺财", 3));
        list.add(new Dog("来福", 5));
        list.add(new Dog("富贵", 10));
        System.out.println("=======迭代器=======");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object dog = iterator.next();
            System.out.println(dog);
        }
        System.out.println("=======增强for=======");
        for (Object dog : list) {
            System.out.println(dog);
        }
    }
}

class Dog {
    /*
    1. 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用
    2. 用迭代器和增强 for 循环两种方式来遍历
    3. 重写 Dog 的 toString 方法, 输出 name 和 age
     */
    private String name;
    private int age;

    //构造器
    public Dog(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;
    }

    //重写子类toString
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3. List 接口和常用方法

3.1 List 接口基本介绍

  1. List 接口是 Collection 接口的子接口
  2. List 集合类中元素有序(即添加顺序和取出顺序一致)、且可以重复;
  3. List集合中的每个元素都有其对应的顺序索引,即支持索引
  4. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  5. JDK API 中List 接口的实现类有很多:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2iv9xtt-1683703986229)(E:\Java study\Markdown\本地图片\List接口的实现类.png)]

3.2 List 接口的常用方法

  1. List 接口的方法有特别多
  2. 这里列举几个常用的方法
  3. 详见代码
public class ListMethod {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("张飞");
        list.add("关羽");
        list.add("lucy");
        //1. void add(int index, Object ele):在 index 位置插入 ele 元素
        //在 index = 1 的位置插入一个对象 刘备
        list.add(1, "刘备");
        System.out.println("List = " + list);
        //输出 List = [张飞, 刘备, 关羽, lucy]

        //2. boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
        List list2 = new ArrayList();
        list2.add("Jack");
        list2.add("lucy");
        list2.add("Tom");
        //在 list 的 index = 1 的位置插入所有 list2 的对象
        list.addAll(1, list2);
        System.out.println("List = " + list);
        //输出 List = [张飞, Jack, lucy, Tom, 刘备, 关羽, lucy]

        //3. Object get(int index):获取指定 index 位置的元素
        //获取指定第三个位置的元素
        System.out.println(list.get(2));//输出 lucy

        //4. int indexOf(Object obj):返回 obj 在集合中首次出现的位置
        System.out.println(list.indexOf("lucy"));//输出 2

        //5. int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
        System.out.println(list.lastIndexOf("lucy"));//输出 6

        //6. Object remove(int index):移除指定 index 位置的元素,并返回此元素
        list.remove(0);//移除第 0 个位置的元素
        System.out.println("List = " + list);
        //输出 List = [Jack, lucy, Tom, 刘备, 关羽, lucy]

        //7. Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换.
        list.set(1, "Andy");//将第二个位置的元素 lucy 替换成了 Andy
        System.out.println("List = " + list);
        //输出 List = [Jack, Andy, Tom, 刘备, 关羽, lucy]

        //8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
        //  注意返回的子集合 fromIndex <= subList < toIndex
        List returnList = list.subList(0, 3);//[0,3)
        System.out.println("returnList =" + returnList);
        //输出 returnList =[Jack, Andy, Tom]
    }
}
3.2.1 练习

要求:

  1. 添加 10 个以上的元素(比如 String “hello” );
  2. 在 3 号位插入一个元素"营养快线",
  3. 获得第 5 个元素;
  4. 删除第 6 个元素;
  5. 修改第 7 个元素;
  6. 在使用迭代器遍历集合;
  7. 要求使用 List 的实现类 ArrayList 完成。
  8. 代码如下:
public class ListExercise {
    public static void main(String[] args) {
        /*
        //1. 添加 10 个以上的元素
        //2. 在 3 号位插入一个元素"营养快线",
        //3. 获得第 5 个元素;
        //4. 删除第 6 个元素;
        //5. 修改第 7 个元素;
        //6. 在使用迭代器遍历集合;
         */
        //1. 添加 10 个以上的元素(比如 String "hello" );
        List list = new ArrayList();
        list.add("张飞");
        list.add("吕布");
        list.add("刘备");
        list.add("关羽");
        list.add("赵云");
        list.add("孙尚香");
        list.add("貂蝉");
        list.add("大乔");
        list.add("小乔");
        list.add("纲手");
        System.out.println("list ="+list);
        //输出 list =[张飞, 吕布, 刘备, 关羽, 赵云, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]

        //2. 在 3 号位插入一个元素"营养快线",
        list.add(2,"营养快线");
        System.out.println("list ="+list);
        //输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 赵云, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]

        //3. 获得第 5 个元素;
        System.out.println(list.get(4));//输出 关羽

        //4. 删除第 6 个元素;
        list.remove(5);
        System.out.println("list ="+list);
        //输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 孙尚香, 貂蝉, 大乔, 小乔, 纲手]

        //5. 修改第 7 个元素;
        list.set(6,"娃哈哈");
        System.out.println("list ="+list);
        //输出 list =[张飞, 吕布, 营养快线, 刘备, 关羽, 孙尚香, 娃哈哈, 大乔, 小乔, 纲手]

        //6. 在使用迭代器遍历集合;
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object newList =  iterator.next();
            System.out.println("newList ="+newList);
        }
        /* 输出
            newList =张飞
            newList =吕布
            newList =营养快线
            newList =刘备
            newList =关羽
            newList =孙尚香
            newList =娃哈哈
            newList =大乔
            newList =小乔
            newList =纲手
         */
    }
}

3.3 List 的三种遍历方式

  1. 方式一:使用迭代器 iterator
  2. 方式二:使用增强for循环
  3. 方式三:使用普通for循环
public class ListFor {
    public static void main(String[] args) {
        //List接口的实现子类 Vector LinkedList
        //List list = new Vector();
        //List list = new LinkedList();
        List list = new ArrayList();
        list.add("张飞");
        list.add("吕布");
        list.add("刘备");
        list.add("关羽");
        list.add("赵云");
        System.out.println("list =" + list);
        //输出 list =[张飞, 吕布, 刘备, 关羽, 赵云]

        //1. 方式一:使用迭代器 iterator
        System.out.println("======迭代器=====");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println("obj = " + obj);
        }
        /*输出
            ======迭代器=====
            obj = 张飞
            obj = 吕布
            obj = 刘备
            obj = 关羽
            obj = 赵云
         */

        //2. 方式二:使用增强for循环
        System.out.println("======增强for=====");
        for (Object o : list) {
            System.out.println("o = " + o);
        }
        /*输出
            ======增强for=====
            o = 张飞
            o = 吕布
            o = 刘备
            o = 关羽
            o = 赵云
         */

        //3. 方式三:使用普通for循环
        System.out.println("======普通for=====");
        for (int i = 0; i < list.size(); i++) {
            System.out.println("对象 = " + list.get(i));
        }
        /*输出
            ======普通for=====
            对象 = 张飞
            对象 = 吕布
            对象 = 刘备
            对象 = 关羽
            对象 = 赵云
         */
    }
}
3.3.1 练习题
  1. 使用List的实现类添加三本图书,并遍历,打印效果如下:

    名称:XX 价格:XX 作者:XX

    名称:XX 价格:XX 作者:XX

    名称:XX 价格:XX 作者:XX

  2. 要求:按照价格排序,从低到高(使用冒泡法)

  3. 要求使用ArrayList、LinkedList和Vector三种集合实现

  4. 代码如下:

public class ListExercise02 {
    public static void main(String[] args) {

        //3. 要求使用ArrayList、LinkedList和Vector三种集合实现
        //使用Vector集合实现  List list = new Vector();
        //使用LinkedList集合实现  List list = new LinkedList();
        List list = new ArrayList();
        list.add(new Book("西游记", 66, "吴承恩"));
        list.add(new Book("红楼梦", 55, "曹雪芹"));
        list.add(new Book("水浒传", 58, "施耐庵"));
        //1. 使用List的实现类添加三本图书,并遍历:
        for (Object o : list) {
            System.out.println(o);
        }

        System.out.println("======排序后=======");
        //2. 要求:按照价格排序,从低到高(使用冒泡法)
        sort(list);
        for (Object o : list) {
            System.out.println(o);
        }

    }

    //先创建一个方法,使用冒泡排序方法进行排序
    public static void sort(List list) {

        int listSize = list.size();
        for (int i = 0; i < listSize - 1; i++) {
            for (int j = 0; j < listSize - i - 1; j++) {
                //取出对象Book
                Book book1 = (Book) list.get(j);
                Book book2 = (Book) list.get(j + 1);
                double temp = 0;
                if (book1.getPrice() > book2.getPrice()) {
                    //集合直接用set方法就进行元素位置交换
                    list.set(j, book2);
                    list.set(j + 1, book1);
                }
            }
        }
    }
}

class Book {
    private String name;
    private double price;
    private String author;

    public Book(String name, double price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "名称:" + name + "\t\t" +
                "价格:" + price + "\t\t" +
                "作者:" + author;
    }
}

4. ArrayList 底层结构和源码分析

4.1 注意事项

  1. permits all elements , including null , ArrayList 可以加入null , 并且多个
  2. ArrayList 是由数组来实现数据储存的
  3. ArrayList 基本等同于 Vector , 除了 ArrayList 是线程不安全(执行效率高)
  4. 在多线程的情况下,不建议使用ArrayList
public class ArrayListDetail {
    public static void main(String[] args) {
        //ArrayList 是线程不安全,看源码,没有 synchronized 关键字
        /*
        public boolean add(E e) {
                ensureCapacityInternal(size + 1);  // Increments modCount!!
                elementData[size++] = e;
                return true;
            }
         */
        ArrayList arrayList = new ArrayList();
        arrayList.add(null);//可以加入null
        arrayList.add("Jack");
        System.out.println(arrayList);
    }

}

4.2 ArrayList 的底层操作机制源码分析(重点、难点)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89ZB2vmF-1683703986230)(E:\Java study\Markdown\本地图片\ArrayList底层源码图.png)]

  1. 建议大家自己来Debug一下ArrayList容量的创建和扩容的流程
public class ArrayListSource {
    public static void main(String[] args) {

        //使用无参构造器创建ArrayList对象
        ArrayList list = new ArrayList();

        //使用for循环给list集合添加1-10数据
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }

        //使用for循环给list集合添加11-15数据
        for (int i = 11; i <= 15; i++) {
            list.add(i);
        }

        list.add(100);
        list.add(200);
        list.add(null);
    }
}

5. Vector 底层结构和源码剖析

5.1 Vector 的基本介绍

  1. Vector类定义说明
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  1. Vector底层也是一个对象数组,protected Object[] elementData;
  2. Vector是线程同步的,即线程安全的,Vector类的操作方法基本都带有Synchronized关键字
  3. 因此在开发中,需要线程同步安全时,考虑使用Vector

Vector底层结构和ArrayList的比较

  1. 底层结构都是可变数组

  2. 版本:

    • ArrayList是jdk1.2以后出现的
    • Vector是jdk1.0以后出现的
  3. 线程安全(同步)效率

    • ArrayList是不安全的,但是效率高
    • Vector是安全的,但是效率不高
  4. 扩容倍数

    • ArrayList:
      • 有参构造一开始指定,后面按照1.5倍扩容
      • 无参构造第一次是10,第二次开始按照1.5倍扩容
    • Vector
      • 有参第一次指定,后面按照2倍扩容
      • 无参构造默认是10,后面按照2倍扩容

  5. 建议大家自己来Debug一下Vector容量的创建和扩容的流程,下面拿vector无参构造器演示一下:

public class Vector_ {
    public static void main(String[] args) {
        //无参构造
        Vector vector = new Vector();
        //普通for循环增加元素
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        vector.add(100);
        //1. new Vector() 底层
        /*
        public Vector() {
            this(10);//容量为10
        }
        //如果是有参构造的情况下
        //Vector vector = new Vector(8);
        //第一步走的是源码为
        public Vector(int initialCapacity) {
            this(initialCapacity, 0);
        }
         */
        //2. vector.add(i)
        //2.1 添加数据到vector集合
        /*
        public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
         */
        //2.2 确定是否需要扩容
        /*
        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
         */
        //3. 如果需要的数组大小不够用了,就进行扩容
        //  扩容的算法:
        //  int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
        //                                             capacityIncrement : oldCapacity);
        //简而言之就是扩容2倍
        //有参构造器开始指定容量,后面算法和无参的一样
        /*
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                             capacityIncrement : oldCapacity);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
         */
    }
}

6. LinkedList 底层结构

6.1 LinkedList 的全面说明

  1. LinkedList底层实现了双向链表和双端队列特点
  2. 可以添加任意元素(元素可以重复),包括null
  3. 线程不安全,没有实现同步

6.2 LinkedList底层结构

6.2.1 基本介绍
  1. LinkedList底层维护了一个双向链表
  2. LinkedList中维护了两个属性first和last分别指向首节点和尾结点
  3. 每个节点(Node对象)里面维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
  4. 所以LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率比较高。
  5. 示意图如下

  1. 下面用代码模拟一个简单的双向链表:
public class LinkedList01 {
    public static void main(String[] args) {
        //模拟一个简单的双向链表
        Node jack = new Node("jack");
        Node tom = new Node("tom");
        Node lucy = new Node("lucy");

        //链接三个结点,形成双向链表
        //jack -> tom -> lucy
        jack.next = tom;
        tom.next = lucy;
        //lucy -> tom -> jack
        lucy.pre = tom;
        tom.pre = jack;

        //让 first 引用指向 Jack ,就是双向链表的头结点
        Node first = jack;
        //让 last 引用指向 Jack ,就是双向链表的尾结点
        Node last = lucy;

        //演示:从头到尾进行遍历
        System.out.println("====从头到尾进行遍历====");
        while (true) {
            if (first == null) {
                break;
            }
            System.out.println(first);
            first = first.next;
        }

        //演示:从尾到头进行遍历
        System.out.println("====从尾到头进行遍历====");
        while (true) {
            if (last == null) {
                break;
            }
            System.out.println(last);
            last = last.pre;
        }

        //演示双向链表添加/删除对象
        //在 tom 和 Lucy 之间插入一个对象 张飞 ,名字为 name
        //1. 先创建一个Node结点
        Node name = new Node("张飞");
        //2.先把 tom 的 next 指向 name ,name 的 next 指向 lucy
        //从而重新形成链接 jack -> tom -> name -> lucy
        tom.next = name;
        name.next = lucy;
        //同理把pre也重新链接
        //lucy -> name -> tom -> jack
        lucy.pre = name;
        name.pre = tom;

        //让first再次指向jack ,形成双向链表的头结点
        first = jack;
        //演示:从头到尾进行遍历新的链表
        System.out.println("===从头到尾进行遍历新的链表===");
        while (true) {
            if (first == null) {
                break;
            }
            System.out.println(first);
            first = first.next;
        }

        //让 last 引用重新指向 Jack ,形成双向链表的尾结点
        last = lucy;
        //演示:从尾到头进行遍历新的链表
        System.out.println("===从尾到头进行遍历新的链表===");
        while (true) {
            if (last == null) {
                break;
            }
            System.out.println(last);
            last = last.pre;
        }
    }
}

//定义一个Node类, 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;
    }
}
6.2.2 LinkedList 的增删改查案例
public class LinkedListCRUD {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        //1. 演示一个添加的源码阅读
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("=====最初的LinkedList=====");
        System.out.println("LinkedList = " + linkedList);

        //2. 演示一个删除的源码阅读
        linkedList.remove();//默认删除第一个结点
        System.out.println("=====删除后=====");
        System.out.println("LinkedList = " + linkedList);

        //3. 修改某个结点对象
        System.out.println("=====修改后=====");
        linkedList.set(1, 999);//将第二个元素改成999
        System.out.println("LinkedList = " + linkedList);

        //4. 得到某个结点对象
        //get(1) 得到双向链表的第二个对象
        System.out.println(linkedList.get(1));//输出 999

        //遍历
        //因为 LinkedList 实现了List的接口,
        //所以遍历方式跟前面一样
        System.out.println("=====迭代器遍历=====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
        }

        System.out.println("=====增强for循环遍历=====");
        for (Object o : linkedList) {
            System.out.println(o);
        }

        System.out.println("=====普通for循环遍历=====");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }

        // 添加的源码阅读
        //1. LinkedList linkedList = new LinkedList();
        /*   public LinkedList() {
             }
         */
        //2. 这时 LinkedList 的属性为空
        //   first = null ; last = null;
        //3. 执行 添加
        /*
        public boolean add(E e) {
            linkLast(e);
            return true;
        }
         */
        //4. 将新的结点加入到双向链表的最后
        /*
        void linkLast(E e) {
            final Node<E> l = last;
            final Node<E> newNode = new Node<>(l, e, null);
            last = newNode;
            if (l == null)
                first = newNode;
            else
                l.next = newNode;
            size++;
            modCount++;
        }
         */

        //删除的源码阅读
        //linkedList.remove();
        //1. 执行removeFirst();
        /*
        public E remove() {
            return removeFirst();
        }
         */
        //2. 执行下面的方法
        /*
        public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
         */
        //3. 执行 unlinkFirst ,将 f 指向双向链表的第一个结点拿掉
        /*
        private E unlinkFirst(Node<E> f) {
            // assert f == first && f != null;
            final E element = f.item;
            final Node<E> next = f.next;
            f.item = null;
            f.next = null; // help GC
            first = next;
            if (next == null)
                last = null;
            else
                next.prev = null;
            size--;
            modCount++;
            return element;
        }
         */
    }
}

6.3. ArrayList 和 LinkedList 比较

  1. 底层结构:
    • ArrayList 的底层结构是可变数组
    • LinkedList 的底层结构是双向链表
  2. 增删的效率
    • ArrayList 增删的效率较低 [ 数组的扩容 ]
    • LinkedList 增删的效率较高 [ 通过链表追加 ]
  3. 改查的效率
    • ArrayList 改查的效率较高
    • LinkedList 改查的效率较低

  1. 那么我们该如何选择ArrayList和LinkedList呢?
    1. 如果我们改查的操作多,选择ArrayList
    2. 如果我们增删的操作多,选择LinkedList
    3. 一般来说,在程序中,80%-90%都是查询,因此大部分的情况下都会选择ArrayList
    4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList
    5. 因此,我们要根据实际情况来选择。

7. Set 接口和常用方法

7.1 Set 接口基本介绍

  1. Set接口是无序的(添加和取出的顺序不一致),没有索引;
  2. 不允许重复元素,所以最多包含一个null;
  3. JDK API中set接口的实现类也有很多;
    在这里插入图片描述

7.2 Set 接口的常用方法

  1. 和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样

7.3 Set 接口的遍历方式

  1. 同Collection的遍历方式一样,因为Set接口是Collection接口的子接口
    • 使用迭代器进行遍历
    • 增强for循环进行遍历
    • 不能使用索引的方式来获取
public class SteMethod {
    public static void main(String[] args) {
        //1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
        //2. Set接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
        //3. Set接口存放数据是无序的(即添加的顺序和取出的顺序是不一致的)
        //4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他是固定的
        Set set = new HashSet();
        set.add("jack");
        set.add("lucy");
        set.add("tom");
        set.add("lucy");
        set.add("sun");
        set.add(null);
        set.add(null);
        System.out.println(set);
        //输出 [null, tom, lucy, sun, jack]

        //遍历
        //1. 迭代器进行遍历
        System.out.println("=====迭代器进行遍历=====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println("next = " + next);
        }

        //删除
        set.remove(null);

        //2. 增强for循环进行遍历
        System.out.println("=====增强for循环进行遍历=====");
        for (Object o : set) {
            System.out.println("o = " + o);
        }

        //Set 接口对象不能通过索引来获取
        //没有set.get()方法
    }
}

8. Set接口实现类——HashSet

8.1 HashSet的全面说明

  1. HashSet 实现了Set接口

  2. HashSet 实际上是HashMap(看源码)

  3. HashSet 可以存放null值,但是只能有一个null

  4. HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果(即不保证元素的存放顺序和取出顺序是一致的)

  5. HashSet 不能有重复的元素/对象

public class HashSet_ {
    public static void main(String[] args) {
        //1. 构造器走的源码
        /*
        public HashSet() {
            map = new HashMap<>();
        }
         */
        //2. HashSet 可以存放null值,但是只能有一个null
        Set hashSet = new HashSet();
        hashSet.add(null);
        hashSet.add("jack");
        hashSet.add("lucy");
        hashSet.add(null);
        hashSet.add("tom");
        System.out.println(hashSet);
        //输出 [null, tom, lucy, jack]
    }
}
  1. 案例
public class HashSet01 {
    public static void main(String[] args) {
        //1. 执行一个 add 方法后,会返回一个 boolean 值
        //2. 如果添加成功,会返回 true ,否则返回 false
        //3. 可以通过 remove 指定删除哪个对象
        Set set = new HashSet();
        System.out.println(set.add("jack"));//true
        System.out.println(set.add("lucy"));//true
        System.out.println(set.add("tom"));//true
        System.out.println(set.add("sun"));//true
        System.out.println(set.add("jack"));//false

        set.remove("tom");
        System.out.println("set = " + set);//3个
        //输出 set = [lucy, sun, jack]

        //4. HashSet 不能添加相同的元素/数据
        set = new HashSet();
        set.add("lucy");
        set.add("lucy");
        //下面两个是不同的对象,可以都存放进去
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//OK
        System.out.println("set = " + set);//3个
        //输出 set = [Dog{name='tom'}, Dog{name='tom'}, lucy]

        //再加深一下
        //后面进行源码分析
        //add 到底发生了什么 ===> 后面看底层机制
        set.add(new String("sun"));//ok
        set.add(new String("sun"));//加入不了
        System.out.println("set = " + set);//4个
        //输出 set = [Dog{name='tom'}, Dog{name='tom'}, lucy, sun]
    }
}

class Dog {//定义Dog类
    private String name;

    public Dog(String name) {
        this.name = name;
    }

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

9. Set接口实现类——HashSet

9.1 HashSet底层机制说明

  1. 分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
  2. 下面模拟简单的数组+链表结构
public class HashSetStructure {
    public static void main(String[] args) {
        //模拟一个HashSet的底层(HashMap底层机制)

        //1. 创建一个数组,数组的类型是Node[]
        //2. 有些人直接把 Node[] 数组称为表table
        Node[] table = new Node[16];

        //3. 创建结点
        Node jack = new Node("jack", null);
        table[2] = jack;
        Node lucy = new Node("lucy", null);
        jack.next = lucy;//将lucy挂载到jack
        Node tom = new Node("tom", null);
        lucy.next = tom;

        //4. 创建新的结点
        Node rose = new Node("rose", null);
        table[3] = rose;
        

        System.out.println("table = " + table);
    }
}

class Node {//结点,存储数据,可以指向下一个结点,从而形成链表
    Object item;//存放数据
    Node next;//指向下一个结点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}
  1. 示意图

  1. 分析HashSet的添加元素底层是如何实现的(hash()+equals())

    • HashSet底层是HashMap
    • 添加一个元素时,先得到Hash值—>会转成—>索引值
    • 找到存储数据表 table ,看这个索引位置是够已经存放的元素
    • 如果没有就直接加入
    • 如果有,调用 equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后
    • 在 Java8 中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD (默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY (默认是64),就会进行树化(红黑树)。
    • 下面进行代码分析
    public class HashSetSource {
        public static void main(String[] args) {
            //1. 先执行的是一个构造器
            //  public HashSet()
            /*
            public HashSet() {
                map = new HashMap<>();
            }
             */
            HashSet hashSet = new HashSet();
            hashSet.add("jack");
            hashSet.add("lucy");
            hashSet.add("jack");
            hashSet.add("tom");
            System.out.println("set = " + hashSet);
    
            //2. 执行 add()
            /*
            public boolean add(E e) {//e = "jack" value = PRESENT
                return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
            }
             */
    
            //3. 执行 put() 该方法会执行 hash(key) 得到 key 对应的 hash 值 算法 h = key.hashCode()) ^ (h >>> 16)
            /*
            public V put(K key, V value) {//key = jack valeu = PRESENT(共享)
                return putVal(hash(key), key, value, false, true);
            }
             */
    
            //4. 执行 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 就是 HashMap 的一个数组,类型是 Node[]
                    //if 语句表示如果当前 table 是 null, 或者 大小=0
                    //    就是第一次扩容,到 16 个空间.
                    if ((tab = table) == null || (n = tab.length) == 0)
                        n = (tab = resize()).length;
    
                    //(1)根据 key,得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置
                    //  并把这个位置的对象,赋给 p
                    //(2)判断 p 是否为 null
                    //(2.1) 如果 p 为 null, 表示还没有存放元素, 就创建一个 Node (key="java",value=PRESENT)
                    //(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)
                    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 比较后相同
                        // 就不能加入
                        if (p.hash == hash &&
                            ((k = p.key) == key || (key != null && key.equals(k))))
                            e = p;
                        //再判断 p 是不是一颗红黑树,
                        //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
                        else if (p instanceof TreeNode)
                            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                        else {//如果 table 对应索引位置,已经是一个链表, 就使用 for 循环比较
                              //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后
                               //	注意在把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点
                               //	, 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
                               //	注意,在转成红黑树时,要进行判断, 判断条件
                               //	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                               //	resize();
                               //	如果上面条件成立,先 table 扩容.
                               //	只有上面条件不成立时,才进行转成红黑树
                              //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break
                            for (int binCount = 0; ; ++binCount) {
                                if ((e = p.next) == null) {
                                    p.next = newNode(hash, key, value, null);
                                    if (binCount >= TREEIFY_THRESHOLD(8) - 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;
                    //size 就是我们每加入一个结点 Node(k,v,h,next), size++
                    if (++size > threshold)
                        resize();//扩容
                    afterNodeInsertion(evict);
                    return null;
                }
             */
        }
    }
    
  2. 分析HashSet的扩容和转成红黑树机制

    • Hash底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75=12
    • 如果table数组使用到了临界值12,就会扩容到162=32,新的临界值就是320.75=24,以此类推
    • 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
    public class HashSetIncrement {
        public static void main(String[] args) {
            /*
            HashSet 底层是 HashMap, 第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是 0.75 = 12
            如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
            新的临界值就是 32*0.75 = 24, 依次类推
             */
            //代码演示
            HashSet hashSet = new HashSet();
            //for (int i = 1; i <= 100; i++) {
            //    hashSet.add(i);//1,2,3,4,5...100
            //}
            
            /*
            在 Java8 中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),
            并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树),
            否则仍然采用数组扩容机制
            */
            //代码演
            //for (int i = 1; i <= 12; i++) {
            //    hashSet.add(new A(i));//
            //}
            
            /*
            当我们向 hashset 增加一个元素,-> Node -> 加入 table , 
            就算是增加了一个 size++
            */
            //代码演示
            for (int i = 1; i <= 7; i++) {//在 table 的某一条链表上添加了 7 个 A 对象
                hashSet.add(new A(i));//
            }
    
            for (int i = 1; i <= 7; i++) {//在 table 的另外一条链表上添加了 7 个 B 对象
                hashSet.add(new B(i));//
            }
        }
    }
    
    class B {
        private int n;
    
        public B(int n) {
            this.n = n;
        }
    
        @Override
        public int hashCode() {
            return 200;
        }
    }
    
    class A {
        private int n;
    
        public A(int n) {
            this.n = n;
        }
    
        @Override
        public int hashCode() {
            return 100;
        }
    }
    

9.2 HashSet 课堂练习

练习1
  1. 定义一个Employee类,该类包含:private成员属性name,age;
  2. 要求:
    • 创建3个Employee对象放入HashSet中
    • 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
public class HashSetExercise01 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("jack", 15));
        hashSet.add(new Employee("lucy", 18));
        hashSet.add(new Employee("tom", 20));
        hashSet.add(new Employee("jack", 15));//加入不成功
        System.out.println("hashSet =" + hashSet);
        //输出 [Employee{name='jack', age=15}, 
        //      Employee{name='tom', age=20}, 
        //      Employee{name='lucy', age=18}]
    }
}

/*
1. 定义一个Employee类,该类包含:private成员属性name,age;
2. 要求:
   - 创建3个Employee对象放入HashSet中
   - 当name和age的值相同时,认为是相同员工,不能添加到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 String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //如果 name 和 age 值相同,则返回相同的 hash 值
    @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);
    }
}
练习2
  1. 定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型);

  2. 其中 birthday为MyDate类型(属性包括:year,month,day);

  3. 要求:

    • 创建3个Employee放入HashSet中
    • 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
  4. 思路:

    • ① 因为Employee类里面的birthday要用MyDate类,所以先定义好 MyDate 类:
    public class MyDate {
        private int year;
        private int month;
        private int day;
    
        public MyDate(int year, int month, int day) {
            this.year = year;
            this.month = month;
            this.day = day;
        }
    
        public int getYear() {
            return year;
        }
    
        public void setYear(int year) {
            this.year = year;
        }
    
        public int getMonth() {
            return month;
        }
    
        public void setMonth(int month) {
            this.month = month;
        }
    
        public int getDay() {
            return day;
        }
    
        public void setDay(int day) {
            this.day = day;
        }
    
        //重写 toString 方法,方便输出
        @Override
        public String toString() {
            return "MyDate{" +
                    "year=" + year +
                    ", month=" + month +
                    ", day=" + day +
                    '}';
        }
    
        //重写 equals 和 hashCode 方法
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            MyDate myDate = (MyDate) o;
            return year == myDate.year && month == myDate.month && day == myDate.day;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(year, month, day);
        }
    }
    
    • ② 定义Employee类
    public class Employee {
        private String name;
        private double sal;
        private MyDate birthday;
    
        public Employee(String name, double sal, MyDate birthday) {
            this.name = name;
            this.sal = sal;
            this.birthday = birthday;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getSal() {
            return sal;
        }
    
        public void setSal(double sal) {
            this.sal = sal;
        }
    
        public MyDate getBirthday() {
            return birthday;
        }
    
        public void setBirthday(MyDate birthday) {
            this.birthday = birthday;
        }
    
        //重写 toString 方法,方便输出
        @Override
        public String toString() {
            return "\nEmployee{" +
                    "name='" + name + '\'' +
                    ", sal=" + sal +
                    ", birthday=" + birthday +
                    '}';
        }
    
        //重写 name 和 birthday 的 equals 和 hashCode 方法
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Employee employee = (Employee) o;
            return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, birthday);
        }
    }
    
    • 最后创建HashSet类,在里面放入Employee
    • 因为要对name和birthday的值进行比较,所以我们重写 name 和 birthday 的 equals 和 hashCode 方法
    public class HashSetExercise02 {
        public static void main(String[] args) {
            /*
            1. 定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型);
            2. 其中 birthday为MyDate类型(属性包括:year,month,day);
            3. 要求:
               - 创建3个Employee放入HashSet中
               - 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
             */
            HashSet hashSet = new HashSet();
            hashSet.add(new Employee("jack", 20000, new MyDate(1988, 11, 26)));
            hashSet.add(new Employee("lucy", 18000, new MyDate(1990, 12, 26)));
            hashSet.add(new Employee("tom", 15000, new MyDate(1993, 11, 26)));
            hashSet.add(new Employee("jack", 16000, new MyDate(1988, 11, 26)));//添加失败
            System.out.println("HashSet = " + hashSet);
            //输出 HashSet = [
            //  Employee{name='tom', sal=15000.0, birthday=MyDate{year=1993, month=11, day=26}},
            //  Employee{name='lucy', sal=18000.0, birthday=MyDate{year=1990, month=12, day=26}},
            //  Employee{name='jack', sal=20000.0, birthday=MyDate{year=1988, month=11, day=26}}]
        }
    }
    

10. Set 接口实现类-LinkedHashSe

10.1 LinkedHashSet 的全面说明

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
  3. LinkedHashSet根据元素的 hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet 不允许添重复元素

10.2 LinkedHashSet 练习

  1. Car类(属性name,price)
  2. 如果两个Car的name和price一样,则认为是相同元素,就不能添加
public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("雷克萨斯", 400000));
        linkedHashSet.add(new Car("奔驰", 500000));
        linkedHashSet.add(new Car("宝马", 600000));
        linkedHashSet.add(new Car("保时捷", 800000));
        linkedHashSet.add(new Car("雷克萨斯", 400000));//添加失败

        System.out.println(linkedHashSet);
        //输出[
        //  Car{name='雷克萨斯', price=400000}, 
        //  Car{name='奔驰', price=500000}, 
        //  Car{name='宝马', price=600000}, 
        //  Car{name='保时捷', price=800000}]
    }
}

/*
1. Car类(属性name,price)
2. 如果两个Car的name和price一样,则认为是相同元素,就不能添加
 */
class Car {
    private String name;
    private int price;

    public Car(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "\nCar{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    //重写 equals 方法 和 hashCode
    //当 name 和 price 相同时, 就返回相同的 hashCode 值, equals 返回 t
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return price == car.price && Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}

11. Map 接口和常用方法

11.1 Map 接口实现类的特点 [很实用]

JDK8的Map接口特点:

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
  2. Map 中的 key 和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
  3. Map 中的 key 不允许重复,原因和HashSet一样,前面分析过源码
  4. Map中的value 可以重复
  5. Map 的key 可以为 null,value也可以为null,注意key为null,只能有一个,value为null,可以多个.
  6. 常用String类作为Map的 key
  7. key 和value之间存在单向一对一关系,即通过指定的 key总能找到对应的value
public class Map_ {
    public static void main(String[] args) {
        /*
        Map 接口实现类的特点, 使用实现类 HashMap
        1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
        2. Map 中的 key 和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
        3. Map 中的 key 不允许重复,原因和HashSet一样,前面分析过源码
        4)Map中的value 可以重复
        4. Map 的key 可以为 null,value也可以为null,注意key为null,只能有一个
        5. value为null,可以多个.
        6. 常用String类作为Map的 key
        7. key 和value之间存在单向一对一关系,即通过指定的 key总能找到对应的value
         */
        Map map = new HashMap();
        map.put("N01", "孙悟空");//k-v
        map.put("N02", "猪八戒");//k-v
        map.put("N03", "沙和尚");//k-v
        map.put("N04", "唐三藏");//k-v
        map.put("N04", "白龙马");//当有相同的key,等价于替换
        map.put(null, null); //k-v
        map.put(null, "abc"); //等价替换
        map.put("no5", null); //k-v
        map.put("no6", null); //k-v
        map.put(1, "三藏");
        map.put(new Object(), "白骨精");

        System.out.println(map);
        //输出{
        // N03=沙和尚,
        // null=abc,
        // N02=猪八戒,
        // 1=三藏,
        // N04=白龙马,
        // no6=null,
        // no5=null,
        // java.lang.Object@1b6d3586=白骨精,
        // N01=孙悟空}

        //通过 get 方法,传入 key ,会返回对应的 value
        System.out.println(map.get(1));//三藏
    }
}
  1. 下图为Map存放数据的key-value示意图:

    • 一对k-v是放在一个HashMap$Node中的,
    • 又因为Node 实现了 Entry接口,
    • 有些书上也说一对k-v就是一个Entry(如图)

11.2 Map 接口常用方法

  1. remove:根据键删除映射关系
  2. get:根据键获取值
  3. size:获取元素个数
  4. isEmpty:判断个数是否为 0
  5. clear:清除 k-v
  6. containsKey:查找键是否存在
  7. 下面进行简单的操作
public class MapMethod {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("孙悟空", new Book("", 1));//OK
        map.put("孙悟空", "紫霞仙子");//替换上面的
        map.put("牛魔王", "铁扇公主");//OK
        map.put("猪八戒", "高老庄");//OK
        map.put("沙和尚", "流沙河");//OK
        map.put("白龙马", "唐三藏");//OK

        System.out.println("map = " + map);
        //输出map = {
        // 白龙马=唐三藏,
        // 牛魔王=铁扇公主,
        // 沙和尚=流沙河,
        // 孙悟空=紫霞仙子,
        // 猪八戒=高老庄}


        //1. remove:根据键删除映射关系
        map.remove("白龙马");
        System.out.println("newMap = " + map);
        //输出newMap = {
        // 牛魔王=铁扇公主,
        // 沙和尚=流沙河,
        // 孙悟空=紫霞仙子,
        // 猪八戒=高老庄}

        //2. get:根据键获取值
        Object value = map.get("孙悟空");
        System.out.println("value = " + value);
        //输出 value = 紫霞仙子

        //3. size:获取元素个数
        int size = map.size();
        System.out.println("k-v =" + size);
        //输出 k-v =4

        //4. isEmpty:判断个数是否为 0
        System.out.println(map.isEmpty());//false

        //5. clear:清除 k-v
        map.clear();//全部清空
        System.out.println("map=" + map);// map={}

        //6. containsKey:查找键是否存在
        boolean 白龙马 = map.containsKey("白龙马");
        System.out.println("白龙马 = " + 白龙马);//白龙马 = false
    }
}

class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

11.3 Map 接口遍历方法

map遍历比List和Set复杂一点,但是基本的原理是一样的

  1. containsKey:查找键是否存在
  2. keySet:获取所有的键
  3. values:获取所有的值
  4. entrySet:获取所有关系k-v
  5. 代码演示:
public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("孙悟空", "紫霞仙子");//OK
        map.put("牛魔王", "铁扇公主");//OK
        map.put("猪八戒", "高老庄");//OK
        map.put("沙和尚", "流沙河");//OK
        map.put("白龙马", "唐三藏");//OK

        //第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
        Set set = map.keySet();
        System.out.println("====第一种方式====");
        //增强for
        for (Object key : set) {
            System.out.println(key + "_" + map.get(key));
            //白龙马_唐三藏
            //牛魔王_铁扇公主
            //沙和尚_流沙河
            //孙悟空_紫霞仙子
            //猪八戒_高老庄
        }

        System.out.println("====第二种方式====");
        //通过迭代器
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + "_" + map.get(key));
            //白龙马_唐三藏
            //牛魔王_铁扇公主
            //沙和尚_流沙河
            //孙悟空_紫霞仙子
            //猪八戒_高老庄
        }

        //第二组: 把所有的 values 取出
        System.out.println("====第三种方式====");
        //增强for
        Collection values = map.values();
        for (Object value : values) {
            System.out.println(value);
            //唐三藏
            //铁扇公主
            //流沙河
            //紫霞仙子
            //高老庄
        }

        System.out.println("====第四种方式====");
        //迭代器
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object value = iterator1.next();
            System.out.println(value);
            //唐三藏
            //铁扇公主
            //流沙河
            //紫霞仙子
            //高老庄
        }

        //第三组: 通过 EntrySet 来获取 k-v
        System.out.println("====第五种方式====");
        Set entrySet = map.entrySet();
        //增强for
        for (Object entry : entrySet) {
            //将entry转为Map.entry
            //向下转型
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "_" + m.getValue());
            //白龙马_唐三藏
            //牛魔王_铁扇公主
            //沙和尚_流沙河
            //孙悟空_紫霞仙子
            //猪八戒_高老庄
        }

        System.out.println("====第六种方式====");
        //迭代器
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Object entry = iterator2.next();
            //将entry转为Map.entry
            //向下转型
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "_" + m.getValue());
            //白龙马_唐三藏
            //牛魔王_铁扇公主
            //沙和尚_流沙河
            //孙悟空_紫霞仙子
            //猪八戒_高老庄
        }
    }
}

11.4 Map 接口练习

  1. 使用HashMap添加三个员工对象
  2. 要求:
    • 键:员工id
    • 值:员工对象
  3. 并遍历显示工资>18000的员工(遍历方式至少两种)
  4. 员工类:姓名、工资、员工id
public class MapExercise {
    public static void main(String[] args) {
        Map hashMap = new HashMap();
        hashMap.put(126, new Employee("张三", 12000, 126));
        hashMap.put(128, new Employee("李白", 19000, 128));
        hashMap.put(132, new Employee("王琳", 21000, 132));
        //第一种:keySet -> 增强for
        System.out.println("====增强for遍历====");
        Set set = hashMap.keySet();
        for (Object key : set) {
            //先获取value
            Employee employee = (Employee) hashMap.get(key);
            if (employee.getSal() > 18000) {
                System.out.println(employee);
            }
        }
        //第二种:entrySet -> 迭代器
        System.out.println("====迭代器遍历====");
        Set entry = hashMap.entrySet();
        Iterator iterator = entry.iterator();
        while (iterator.hasNext()) {
            Map.Entry m = (Map.Entry) iterator.next();
            //通过 entry 取得 key 和 value
            Employee employee = (Employee) m.getValue();
            if (employee.getSal() > 18000) {
                System.out.println(employee);
            }
        }
    }
}

/*
1. 使用HashMap添加三个员工对象
2. 要求:
   - 键:员工id
   - 值:员工对象
3. 并遍历显示工资>18000的员工(遍历方式至少两种)
4. 员工类:姓名、工资、员工id
 */
class Employee {
    private String name;
    private double sal;
    private int id;

    public Employee(String name, double sal, int id) {
        this.name = name;
        this.sal = sal;
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public double getSal() {
        return sal;
    }

    public void setSal(double sal) {
        this.sal = sal;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                ", id=" + id +
                '}';
    }
}

12. Map 接口实现类-HashMap*

12.1 HashMap 小结

  1. Map接口的常用实现类:HashMap、Hashtable和Properties。
  2. HashMap是Map接口使用频率最高的实现类。
  3. HashMap 是以 key-val对的方式来存储数据(HashMap$Node类型)[案例 Entry]
  4. key不能重复,但是值可以重复,允许使用null键和null值。
  5. 如果添加相同的key,则会覆盖原来的key-val,等同于修改(kev不会替换,val会替换)
  6. 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。(idk8的hashMap底层 数组+链表+红黑树)
  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

12.2 HashMap 底层机制及源码剖析

1. 扩容机制[和HashSet相同:
  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75.
  3. 当添加key-val 时,通过kev的哈希值得到在table的索引。然后判断该索引处是否有元素:
    • 如果没有元素直接添加。
    • 如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等:
    • 如果相等,则直接替换val;
    • 如果不相等需要判断是树结构还是链表结构,做出相应处理。
    • 如果添加时发现容量不够,则需要扩容。
  4. 第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75
  5. 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24.依次类推
  6. 在Java8中,如果一条链表的元素个数超过TREEIFYTHRESHOLD(默认是8),并且table的大小 >=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)
  7. 看如下示意图

  1. 下面进行代码分析:
public class HashMapSource1 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("java", 10);//ok
        map.put("php", 10);//ok
        map.put("java", 20);//替换 value

        System.out.println("map=" + map);//


        /*源码解读 HashMap 的源码

        1.执行构造器 new HashMap()
        初始化加载因子 loadfactor = 0.75
        HashMap$Node[] table = null

        2.执行 put 调用 hash 方法,计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16)
        public V put(K key, V value) {//K = "java" value = 10
            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, 如果为 null, 就直接把加入的 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 返回真)
        // 就认为不能加入新的 k-v
        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 && //如果在循环比较过程中,发现有相同,就 break,就只是替换 value
                    ((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; //替换,key 对应 value
            afterNodeAccess(e);
            return oldValue;
                }
            }
            ++modCount;//每增加一个 Node ,就 size++
            if (++size > threshold[12-24-48])//如 size > 临界值,就扩容
                resize();
            afterNodeInsertion(evict);
            return null;
            }


        5. 关于树化(转成红黑树)
        //如果 table 为 null ,或者大小还没有到 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();
        }
        */
    }
}
2. 模拟HashMap触发扩容、树化情况,并Debug验证
public class HashMapSource2 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        for (int i = 1; i <= 12; i++) {
            hashMap.put(i, "hello");
        }

        hashMap.put("aaa", "bbb");

        System.out.println("hashMap=" + hashMap);//12 个 k-v

        //自己设计代码去验证,table 的扩容
        //0 -> 16(12) -> 32(24) -> 64(64*0.75=48)-> 128 (96) ->
    }
}

class A {
    private int num;

    public A(int num) {
        this.num = num;
    }

    //所有的 A 对象的 hashCode 都是 100
    //	@Override
    //	public int hashCode() {
    //	return 100;
    //	}

    @Override
    public String toString() {
        return "\nA{" +
                "num=" + num + '}';
    }
}

13. Map 接口实现类-Hashtable

13.1 HashTable 的基本介绍

  1. 存放的元素是键值对:即 K-V
  2. hashtable的键和值都不能为null,否则会抛出NullPointerException
  3. hashTable 使用方法基本上和HashMap一样
  4. hashTable是线程安全的(synchronized),hashMap是线程不安全的
  5. 简单看下底层结构 HashTable的应用案例
  6. 下面的代码是否正确,如果错误,为什么?
HashTableExercise.java Hashtable table=new Hashtable)://ok 
table.put("john", 100); //ok 
table.put(null,100);//异常 
table.put("john", null);//异常 
table.put("lucy", 100);//ok 
table.put("lic" 100);//ok 
table.put("lic"88);//替换 
System.out.printIn(table);

13.2 Hashtable 和 HashMap 对比

14. Map 接口实现类-Properties

14.1 基本介绍

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

  2. 他的使用特点和Hashtable类似

  3. Properties 还可以用于从 xxx.properties文件中,加载数据到Properties类对象并进行读取和修改

  4. 说明:工作后xxx.properties文件通常作为配置文件,这个知识点在1O流举例,有兴趣可先看文章

    https://www.cnblogs.com/xudong-bupt/p/3758136.html

14.2 基本使用

public class Properties_ {
    public static void main(String[] args) {
        //代码解读
        //1. Properties 继承  Hashtable
        //2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 null

        //增加
        Properties properties = new Properties();
        //properties.put(null, "abc");//抛出 空指针异常
        //properties.put("abc", null); //抛出 空指针异常
        properties.put("john", 100);//k-v
        properties.put("lucy", 100);
        properties.put("lic", 100);
        properties.put("lic", 88);//如果有相同的 key , value 被替换

        System.out.println("properties=" + properties);
        //通过 k 获取对应值
        System.out.println(properties.get("lic"));//88

        //删除 properties.remove("lic");
        System.out.println("properties=" + properties);
        
        //修改
        properties.put("john", "约翰");
        System.out.println("properties=" + properties);
    }
}

15. 总结-集合选型规则(重点记住)

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

  1. 先判断存储的类型(一组对象[单列]或一组键值对[双列])

  2. 一组对象[单列]:Collection接口

    • 允许重复:List
      • 增删多:LinkedList[底层维护了一个双向链表]
      • 改查多:ArrayList[底层维护 Object类型的可变数组
    • 不允许重复:Set
      • 无序:HashSet「底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)
      • 排序:TreeSet [下面会举例说明]
      • 插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
  3. 一组键值对[双列]:Map

    • 键无序:HashMap[底层是:哈希表jdk7:数组+链表,jdk8:数组+链表+红黑树]
    • 键排序:TreeMap [下面会举例说明]
    • 键插入和取出顺序一致:LinkedHashMap
    • 读取文件 Properties

16. TreeSet与TreeMap

TreeSet源码解读

public class TreeSet_ {
    public static void main(String[] args) {

        //1. 当我们使用无参构造器,创建 TreeSet 时,仍然是无序的
        //2. 现在希望添加的元素,按照字符串大小来排序
        //3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
        //	并指定排序规则
        //4. 下面简单阅读一下源码
        /*
        1.构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
            public TreeMap(Comparator<? super K> comparator)
                { this.comparator = comparator;
            }

        2.在 调用 treeSet.add("tom"), 在底层会执行到
        if (cpr != null) {//cpr 就是我们的匿名内部类(对象)
            do {
                parent = t;
                //动态绑定到我们的匿名内部类(对象)compare cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                t = t.left; else if (cmp > 0)
                t = t.right;
                else //如果相等,即返回 0,这个 Key 就没有加入
                return t.setValue(value);
            } while (t != null);
        }
        */
        
        //TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //下面 调用 String 的 compareTo 方法进行字符串大小比较
                //要求加入的元素,按照长度大小排序
                //return ((String) o2).compareTo((String) o1);
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        //添加数据. treeSet.add("jack");
        treeSet.add("tom");//3
        treeSet.add("sp");
        treeSet.add("a");
        treeSet.add("abc");//3

        System.out.println("treeSet=" + treeSet);
    }
}

TreeMap源码阅读

public class TreeMap_ {
    public static void main(String[] args) {
        //使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)
        /*
        要求:按照传入的 k(String) 的大小进行排序
        */
        //	TreeMap treeMap = new TreeMap();
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //按照传入的 k(String) 的大小进行排序
                //按照 K(String) 的长度大小排序
                //return ((String) o2).compareTo((String) o1);
                return ((String) o2).length() - ((String) o1).length();
            }
        });
        treeMap.put("jack", "杰克");
        treeMap.put("tom", "汤姆");
        treeMap.put("kristina", "克瑞斯提诺");
        treeMap.put("smith", "斯密斯");
        treeMap.put("hsp", "韩顺平");//加入不了

        System.out.println("treemap=" + treeMap);

        /*
        1.构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator
        public TreeMap(Comparator<? super K> comparator) {
            this.comparator = comparator;
        }
        2.调用 put 方法
        2.1第一次添加, 把 k-v 封装到 Entry 对象,放入 root
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        2.2以后添加
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do { //遍历所有的 key , 给当前 key 找到适当位置
                parent = t;
                cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的 compare
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else	//如果遍历过程中,发现准备添加 Key 和当前已有的 Key 相等,就不添加
                    return t.setValue(value);
            } while (t != null);
        }
         */
    }
}

17. Collections 工具类

17.1 Collections 工具类介绍

  1. Collections是一个操作 SetList和Map等集合的工具类
  2. Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

17.2 排序操作:(均为static 方法)

  1. reverse(List):反转 List中元素的顺序
  2. shuffle(List):对List集合元素进行随机排序
  3. sort(List):根据元素的自然顺序对指定 List集合元素按升序排序
  4. sort(List,Comparator):根据指定的 Comparator产生的顺序对List集合元素进行排序
  5. swap(List,int,int):将指定list集合中的处元素和j处元素进行交换

17.3 查找、替换

  1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  2. Object max(Collection,Comparator):根据Comparator指定的顺序,
    返回给定集合中的最大元素
  3. Object min(Collection)
  4. Object min(Collection,Comparator)
  5. int frequency(Collection,Object):返回指定集合中指定元素的出现次数
  6. void copy(List destListsrc):将src中的内容复制到dest中
  7. boolean replaceAll(List list,Object oldValObject newVal):使用新值替换 List 对象的所有旧值

17.4 Collections 案例演示

public class Collections_ {
    public static void main(String[] args) {
        //创建 ArrayList 集合,用于测试.
        List list = new ArrayList();
        list.add("tom");
        list.add("smith");
        list.add("king");
        list.add("milan");
        list.add("tom");
        
        //reverse(List):反转 List 中元素的顺序
        Collections.reverse(list);
        System.out.println("list=" + list);
        //shuffle(List):对 List 集合元素进行随机排序
//        	for (int i = 0; i < 5; i++) {
//                Collections.shuffle(list);
//                System.out.println("list=" + list);
//            }

        //sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
        Collections.sort(list);
        System.out.println("自然排序后");
        System.out.println("list=" + list);
        //sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
        //我们希望按照 字符串的长度大小排序
        Collections.sort(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //可以加入校验代码.
                return ((String) o2).length() - ((String) o1).length();
            }
        });
        System.out.println("字符串长度大小排序=" + list);
        //	swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

        //比如 Collections.swap(list, 0, 1);
        System.out.println("交换后的情况");
        System.out.println("list=" + list);

        //Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
        System.out.println("自然顺序最大元素=" + Collections.max(list));
        //Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
        //比如,我们要返回长度最大的元素
        Object maxObject = Collections.max(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        System.out.println("长度最大的元素=" + maxObject);

        //Object min(Collection)
        //Object min(Collection,Comparator)
        //上面的两个方法,参考 max 即可

        //int frequency(Collection,Object):返回指定集合中指定元素的出现次数
        System.out.println("tom 出现的次数=" + Collections.frequency(list, "tom"));

        //void copy(List dest,List src):将 src 中的内容复制到 dest 中

        ArrayList dest = new ArrayList();
        //为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size()一样
        for (int i = 0; i < list.size(); i++) {
            dest.add("");
        }
        //拷贝
        Collections.copy(dest, list);
        System.out.println("dest=" + dest);

        //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
        //如果 list 中,有 tom 就替换成 汤姆
        Collections.replaceAll(list, "tom", "汤姆");
        System.out.println("list 替换后=" + list);
    }
}

18. 集合练习

1. 练习一

  1. 封装一个新闻类,包含标题和内容属性,提供get、set方法,重写toString方法,打印对象时只打印标题:
  2. 只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化两个对象:
    • 新闻一:新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧
    • 新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
  3. 将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
  4. 在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加
  5. 在控制台打印遍历出经过处理的新闻标题;
  6. 代码编辑:
public class Homework01 {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add(new News("新冠确诊病例超千万,数百万印度教信徒赴恒河\"圣浴\"引民众担忧"));
        arrayList.add(new News("男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生"));
        //取出新闻长度
        int size = arrayList.size();
        //倒序遍历
        for (int i = size - 1; i >= 0; i--) {
            //System.out.println(arrayList.get(i));
            News news = (News) arrayList.get(i);
            //直接调用
            System.out.println(processTitle(news.getTitle()));
            //男子突然想起2个月前钓的鱼还在...
            //新冠确诊病例超千万,数百万印度...
        }
    }

    //专门写一个方法,处理实现新闻标题
    //processTitle 处理
    //加个 static 静态方法 ,可以直接调用
    public static String processTitle(String title) {
        if (title == null) {
            return "";
        }
        if (title.length() > 15) {
            //截取0-15    [0,15)
            return title.substring(0, 15) + "...";
        } else {
            return title;
        }
    }
}

/*
1. 封装一个新闻类,包含标题和内容属性,提供get、set方法,重写toString方法,打印对象时只打印标题:
2. 只提供一个带参数的构造器,实例化对象时,只初始化标题;并且实例化两个对象:
新闻一:新冠确诊病例超千万,数百万印度教信徒赴恒河“圣浴”引民众担忧
新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生
3. 将新闻对象添加到ArrayList集合中,并且进行倒序遍历;
4. 在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加
5. 在控制台打印遍历出经过处理的新闻标题;
 */
class News {
    private String title;
    private String content;

    //构造器,只初始化标题
    public News(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    //重写toString方法,打印对象时只打印标题:
    @Override
    public String toString() {
        return "News{" +
                "title='" + title + '\'' +
                '}';
    }
}

2. 练习二

  1. 使用ArrayList 完成对 对象 Car {name,price}的各种操作
  2. 要求
    • add:添加单个元素
    • remove:删除指定元素
    • contains:查找元素是否存在
    • size:获取元素个数
    • isEmpty:判断是否为空
    • clear:清空
    • addAll:添加多个元素
    • containsAll:查找多个元素是否都存在
    • removeAll:删除多个元素
    • 使用增强for和 迭代器来遍历所有的car,需要重写 Car 的toString方法
    • Car car=new Car("宝马400000);
    • Car car2 =new Car("宾利"5000000);
  3. 代码如下:
public class Homework02 {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        Car car = new Car("宝马", 400000);
        Car car2 = new Car("宾利", 5000000);

        //- add:添加单个元素
        arrayList.add(car);
        arrayList.add(car2);
        System.out.println("arrayList =" + arrayList);
        //输出arrayList =[Car{name='宝马', price=400000}, Car{name='宾利', price=5000000}]

        //- remove:删除指定元素
        arrayList.remove(1);
        System.out.println("arrayList =" + arrayList);
        //输出arrayList =[Car{name='宝马', price=400000}]

        //- contains:查找元素是否存在
        System.out.println(arrayList.contains(car));//true

        //- size:获取元素个数
        System.out.println(arrayList.size());//1

        //- isEmpty:判断是否为空
        System.out.println(arrayList.isEmpty());//false

        //- clear:清空
        //arrayList.clear();

        //- addAll:添加多个元素
        boolean b = arrayList.addAll(arrayList);
        System.out.println(b);//true
        System.out.println("arrayList =" + arrayList);
        //输出arrayList =[Car{name='宝马', price=400000}, Car{name='宝马', price=400000}]

        //- containsAll:查找多个元素是否都存在
        boolean bom = arrayList.containsAll(arrayList);
        System.out.println(bom);//true

        //- removeAll:删除多个元素
        //arrayList.removeAll(arrayList);//相当于清空

        //- 使用增强for和 迭代器来遍历所有的car,需要重写 Car 的toString方法
        //增强for
        System.out.println("====增强for====");
        for (Object o : arrayList) {
            System.out.println(o);
            //Car{name='宝马', price=400000}
            //Car{name='宝马', price=400000}
        }

        //迭代器
        System.out.println("====迭代器====");
        Iterator iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println(next);
            //Car{name='宝马', price=400000}
            //Car{name='宝马', price=400000}
        }
    }
}

class Car {
    private String name;
    private int price;

    public Car(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Car{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

3. 练习三

  1. 使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员工的姓名和工资,
  2. 存入数据如下:
    • jack-650元;
    • tom-1200元;
    • smith–2900元;
  3. 将iack的工资更改为2600元
  4. 为所有员工工资加薪100元;
  5. 遍历集合中所有的员工
  6. 遍历集合中所有的工资
  7. 代码如下:
public class Homework03 {
    public static void main(String[] args) {
        Map m = new HashMap();
        m.put("jack", 650);
        m.put("tom", 1200);
        m.put("smith", 2900);
        System.out.println(m);
        //{tom=1200, smith=2900, jack=650}

        m.put("jack", 2600);
        System.out.println(m);
        //{tom=1200, smith=2900, jack=2600}

        Set set = m.keySet();
        for (Object key : set) {
            m.put(key, (Integer) m.get(key) + 100);
        }
        System.out.println(m);
        //{tom=1300, smith=3000, jack=2700}

        //遍历集合中所有的员工
        //遍历 EntrySet
        Set entrySet = m.entrySet();
        //迭代器
        Iterator iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            System.out.println(entry.getKey() + "_" + entry.getValue());
            //tom_1300
            //smith_3000
            //jack_2700
        }

        //遍历集合中所有的工资
        //取出value
        System.out.println("遍历所有工资");
        Collection values = m.values();
        for (Object value : values) {
            System.out.println("工资 =" + value);
            //工资 =1300
            //工资 =3000
            //工资 =2700
        }

    }
}

/*
1. 使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员工的姓名和工资,
2. 存入数据如下:
   - jack-650元;
   - tom-1200元;
   - smith--2900元;
3. 将iack的工资更改为2600元
4. 为所有员工工资加薪100元;
5. 遍历集合中所有的员工
6. 遍历集合中所有的工资
 */
class Employee {
    private String name;
    private int sal;

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

    public String getName() {
        return name;
    }

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

    public int getSal() {
        return sal;
    }

    public void setSal(int sal) {
        this.sal = sal;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                '}';
    }
}

4. 练习四

  1. 试分析HashSet和TreeSet分别如何实现去重的
  2. HashSet的去重机制:
    • hashCode()+equals(),
    • 底层先通过存入对象,进行运算得到一个 hash值,通过hash值得到对应的索引,
    • 如果发现table索引所在的位置,没有数据,就直接存放
    • 如果有数据,就进行equals比较[遍历比较],如果比较后,不相同,就加入,否则就不加入.
  3. TreeSet的去重机制:
    • 如果你传入了一个Comparator匿名对象,就使用实现的compare去重,
    • 如果方法返回0,就认为是相同的元素/数据,就不添加,
    • 如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重

5. 练习五

  1. 下面代码运行会不会抛出异常,并从源码层面说明原因。[考察 读源码+接口编程+动态绑定]

    TreeSet treeSet =new TreeSet(); 
    treeSet.add(new Person());
    
  2. 代码分析

    public class Homework05 {
        public static void main(String[] args) {
            TreeSet treeSet = new TreeSet();
            //分析
            //add方法,因为 TreeSet() 构造器没有传入Comparator接口的匿名内部类
            //所以在底层 Comparable<? super K> k = (Comparable<? super K>) key;
            //即 把 Person 转成 Comparable 类型
            treeSet.add(new Person());
            //ClassCastException
            //类型转换异常
        }
    }
    
    class Person {}
    //让 Person 类实现 Comparable就不会报错
    /*
    class Person implements Comparable{
        @Override
        public int compareTo(Object o) {
            return 0;
        }
    }
     */
    

6. 练习六

  1. 已知:Person类按照id和name重写了hashCode和equals方法,
  2. 问下面代码输出什么?
HashSet set = new HashSet()://ok
Person p1 = new Person(1001,"AA")//ok 
Person p2 =new Person(1002,"BB");//ok 
set.add(p1)://ok
set.add(p2);//ok 
p1.name ="CC"; 
set.remove(p1); 
System.out.printin(set);//2 
set.add(new Person(1001,"CC"));
System.out.println(set);//3
set.add(new Person(1001,"AA"));
System.out.println(set);//4
  1. 代码如下:
public class Homework06 {
    public static void main(String[] args) {
        HashSet set = new HashSet();//ok
        Person p1 = new Person(1001, "AA");//ok
        Person p2 = new Person(1002, "BB");//ok
        set.add(p1);//ok
        set.add(p2);//ok
        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);
        //输出 [Person{id=1002, name='BB'}, 
        //      Person{id=1001, name='CC'}]
        
        set.add(new Person(1001, "CC"));
        System.out.println(set);
        //输出 [Person{id=1002, name='BB'}, 
        //      Person{id=1001, name='CC'}, 
        //      Person{id=1001, name='CC'}]
        
        set.add(new Person(1001, "AA"));
        System.out.println(set);
        //输出 [Person{id=1002, name='BB'}, 
        //      Person{id=1001, name='CC'}, 
        //      Person{id=1001, name='CC'}, 
        //      Person{id=1001, name='AA'}]
    }
}

class Person {
    private int id;
    public String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getNum() {
        return id;
    }

    public void setNum(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

7. 练习七

  1. 试写出Vector和ArrayList的比较?
  2. 从底层结构进行比较:
    • ArrayList是可变数组
    • Vector是可变数组 Object[]
  3. 从版本进行比较
    • ArrayList是 jdk1.2 诞生的
    • Vector是 jdk1.0 诞生的
  4. 从线程安全(同步)效率进行比较
    • ArrayList是不安全,但是效率高
    • Vector是安全,但是效率不高
  5. 从扩容倍数进行比较
    • ArrayList:
    • 如果使用有参构造器按照1.5倍扩容;
    • 如果是无参构造器
      • 1.第一次扩容10
      • 2.从第二次开始按照1.5倍扩容
    • Vector:
      • 如果是无参,默认10,满后,按照2倍扩容
      • 如果是指定大小创建Vector,则每次按照2倍扩容.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dominator945

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值