java集合

一、集合的理解和好处

数组(灵活性不够)

1、长度开始时必须指定,而且一旦指定,不能更改

2、保存的必须为同一类型的元素

3、使用数组进行增加/删除元素比较麻烦

集合

1、可以动态的保存任意多个元素,使用方便,允许类型不一致

2、提供一系列方便的操作对象方法:add、remove、set、get等

3、使用集合添加/删除新元素,很简洁

二、两组集合关系图

单列集合 Collection

List : 有索引 存取有序 数据不唯一
ArrayList :数组数据结构 特点:查询快 增删慢
LinkedList: 双链表数据结构 特点:查询慢 增删快

Set : 没有索引 存取无序 数据唯一
HashSet :哈希表 = 数组 + 链表 + 二叉树

双列集合 Map

HashMap<K,V> 键唯一 值不唯一 哈希表控制键
向键位置存储对象所属类必须重写 hashCode equals方法
遍历map集合:
keySet 获取所有键
entrySet 获取键值对 Map.Entry3.

二、Collection接口和常用方法

1、基本介绍

1、collection实现子类可以存放多个元素,每个元素可以是Object

2、有些Collection的实现类可以存放重复元素,有些不可以

3、Collection的实现类有些是有序的(List),有些不是有序的(Set)

4、Collection接口没有实现子类,是通过它的子接口Set和List来实现的

2、常用方法

因为Collection接口本身是不可以实例化的,因此用实现子类ArrayList进行演示

import java.util.ArrayList;
import java.util.List;

public class CollectionMethod {
    public static void main(String[] args) {
        //创建ArrayList,使用接口接收
        List list = new ArrayList();

        //add:添加单个元素
        list.add("java"); //字符型
        list.add(10); //整型   相当于list.add(new Integer(10))
        list.add(true); //同上
        //现在输出的就是一个一个对象 [java,10,true]
        System.out.println("add: list = " + list);

        //remove:删除指定元素
        list.remove(0);//根据下标删除
        list.remove("java");//根据指定对象删除,删除指定元素
        System.out.println("remove: list = " + list);

        //contains:查找元素是否存在
        System.out.println("contains: list = " + list.contains("java"));

        //size:返回元素的个数
        System.out.println("size: list = " + list.size());

        //isEmpty:判断是否为空
        System.out.println("isEmpty: list = " + list.isEmpty());

        //clear:清空
        list.clear();
        System.out.println("clear: list = " + list);

        //addAll:添加多个元素(可传入一个集合)
        ArrayList arrayList = new ArrayList();
        arrayList.add("红楼梦");
        arrayList.add("三国演义");
        list.addAll(arrayList);
        System.out.println("addAll: list = " + list);

        //containsAll:查找多个元素是否存在(可传入一个集合)
        System.out.println("containsAll: list = " + list.containsAll(arrayList));

        //removeAll:删除多个元素(可传入一个集合)
        list.removeAll(arrayList);
        System.out.println("removeAll: list = " + list);

    }
}

3、遍历方式

1、使用迭代器Iterator

基本介绍

1、Iterator对象成为迭代器,主要用于遍历Collection集合中的元素

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

3、Iterator的结构

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

迭代器原理

迭代器代码

 //得到一个集合的迭代器
Iterator iterator = list.iterator();
//判断集合是否还有元素,没有返回false,退出循环
while (iterator.hasNext()){
    //打印元素
    //next()  1、指针下移  2、将下移以后集合位置上的元素返回
    System.out.println(iterator.next());
}

迭代器原理图

注意:在调用it.next()方法之前必须调用it.hasNext()进行检测,如果不调用,且下一条记录无效时,直接调用it.next()方法会抛出NoSuchElementException异常

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

public class CollectionIterator {
    public static void main(String[] args) {
        Collection collection = new ArrayList();

        collection.add(new Book("三国演义","罗贯中",10.1));
        collection.add(new Book("红侯梦","曹雪芹",9.1));
        collection.add(new Book("三体","刘慈欣",12.1));

        //遍历集合
        //1、得到集合的迭代器
        Iterator iterator = collection.iterator();
        //2、使用while进行遍历
        while (iterator.hasNext()){
            //返回的是object,因为add添加的是一个object
            Object obj = iterator.next();
            //会去调用toString方法
           //object的编译类型是object,运行类型取决于存放的对象,这里存放的是Book对象,所以根据动态绑定机制回去调用toString方法
            System.out.println("obj = " + obj);
        }
        //3、当退出while退出循环后,这时iterator迭代器指向最后一个元素
        //iterator.next(); //异常 java.util.NoSuchElementException
        //4、需要重新遍历,需要重置迭代器
        iterator = collection.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj = " + obj);
        }
    }
}
class Book{
    private String bookName;
    private String author;
    private double price;

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

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

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

    public Book() {
    }

    @Override
    public String toString() {
        return "Book{" +
                "bookName='" + bookName + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                '}';
    }
}

2、增强for循环

增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样,只适用于遍历集合和数组

基本语法:

for( 元素类型 元素名 : 集合名或者数组名 ){

​ 访问对象

}

增强for的底层,在ArrayList的源码下

//源码,返回一个迭代器
public Iterator<E> iterator() {
    return new Itr();
}
//源码,调用hasNext判断是否是最后一个元素
public boolean hasNext() {
    return cursor != size;
}
//源码,使用next()方法进行遍历
@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
import java.util.ArrayList;
import java.util.Collection;

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

        Collection collection = new ArrayList();

        collection.add(new Book1("三国演义","罗贯中",10.1));
        collection.add(new Book1("红侯梦","曹雪芹",9.1));
        collection.add(new Book1("三体","刘慈欣",12.1));

        //1、本质与iterator基本一致
        //2、底层也是迭代器
        //3、可以理解为简化版的迭代器遍历
        for (Object o : collection) {
            System.out.println(o);
        }

        //数组使用
        int[] ints = {1,2,3,4,5,6,7};
        for (int anInt : ints) {
            System.out.println(anInt);
        }

    }
}
class Book1{
    private String bookName;
    private String author;
    private double price;

    public Book1(String bookName, String author, double price) {
        this.bookName = bookName;
        this.author = author;
        this.price = price;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

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

    public Book1() {
    }

    @Override
    public String toString() {
        return "Book{" +
                "bookName='" + bookName + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                '}';
    }
}

3、迭代器和增强for循环练习
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 练习
 * 创建三个dog对象{name,age},放入ArrayList中,赋给List引用,
 * 用迭代器和增强for循环进行遍历
 * 重写toString方法,输出name和age
 */
public class CollectionExercise {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add(new Dog("旺财",10));
        list.add(new Dog("狗蛋",12));
        list.add(new Dog("大白",8));
        list.add(new Dog("小黑",4));

        //1、使用迭代器进行遍历
        System.out.println("=============迭代器============");
        //获取迭代器
        Iterator iterator = list.iterator();
        //判断是否还有元素
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            //输出元素
            System.out.println("obj = " + obj);
        }

        //2、使用增强for循环进行遍历
        System.out.println("==============增强for循环==========");
        for (Object obj : list) {
            System.out.println("obj = " + obj);
        }
    }
}
class Dog{
    private String name;
    private Integer age;

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

    public Dog() {
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

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

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

三、List接口和用方法

1、基本介绍

List接口是Collection接口的子接口

1、List集合类中元素有序(即添加顺序和取出顺序一致),且元素可重复

//1、List集合类中元素有序(即添加顺序和取出顺序一致),且元素可重复
List list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
System.out.println("list = "+list);

//结果
list = [1, 2, 3]

2、List集合中的每个元素都有对应的顺序索引,即支持索引

//2、List集合中的每个元素都有对应的顺序索引,即支持索引
//索引从0开始
System.out.println(list.get(0));

3、List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

4、JDK API中List接口的常用实现类有 ArrayList、LinkedList和Vector

2、常用方法
import java.util.ArrayList;
import java.util.List;

public class ListMethod {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("张三丰");
        list.add("孙悟空");

        //list的常用方法
        //void add(int index, Object obj) 在index位置插入obj,在没有加上索引,则会插入最后一个元素
        //接口源码  void add(int index, E element);
        list.add(1,"刘备");
        System.out.println("add: list = " + list);

        //boolean addAll(int index, Collection col); 在index位置开始将col中的所有元素进行添加
        //接口源码 boolean addAll(int index, Collection<? extends E> c);
        List arrayList = new ArrayList();
        arrayList.add("喜洋洋");
        arrayList.add("美羊羊");
        arrayList.add("懒洋洋");
        arrayList.add("刘备");
        list.addAll(1,arrayList);
        System.out.println("addAll: list = " + list);

        //Object get(int index): 根据索引获取对应的元素
        //接口源码 E get(int index);
        System.out.println("get: list = " + list.get(1));

        //int indexOf(Object obj): 返回obj在集合中首次出现的位置
        //接口源码  int indexOf(Object o)
        System.out.println("indexOf: list = " + list.indexOf("刘备"));

        //int lastIndexOf(Object obj); : 返回obj在集合中末次出现的位置
        //接口源码int lastIndexOf(Object o);
        System.out.println("lastIndexOf: list = " + list.lastIndexOf("刘备"));

        //Object remove(int index): 移除指定index位置的元素,并返回此元素
        //接口源码 E remove(int index);
        System.out.println(("remove: list = " + list.remove(1)));

        //Object set(int index, Object obj): 设置指定index位置的元素为obj,相当于替换,前提是指定的索引必须存在,否则会抛出下标越界异常
        //接口源码 E set(int index, E element);
        list.set(1,"牛洋洋");
        System.out.println("set: list = " + list);

        //List subList(int fromIndex, int toIndex): 返回fromIndex到toIndex位置的子集合
        //接口源码 List<E> subList(int fromIndex, int toIndex);
        System.out.println("subList: list = " + list.subList(0,2));
        
    }
}
3、List练习
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 添加10个以上元素(比如 String "hello"),在2号位插入一个 喜洋洋
 * 获取第5个元素,删除第6个元素,修改第7个元素,再使用迭代器 进行遍历集合
 * 要求:使用List实现类ArrayList完成
 */
public class ListExercise {
    public static void main(String[] args) {
        List list = new ArrayList();

        for (int i = 0; i < 12; i++) {
            list.add("hello" + i);
        }
        System.out.println("list = " + list);

        //在2号位添加一个 喜洋洋
        System.out.println("============在2号位添加一个 喜洋洋============");
        list.add(1,"喜洋洋");
        System.out.println("list = " + list);

        //获取第5个元素
        System.out.println("============获取第5个元素============");
        System.out.println("list = " + list.get(4));

        //删除第6个元素
        System.out.println("============删除第6个元素============");
        list.remove(5);
        System.out.println("list = " + list);

        //修改第7个元素
        System.out.println("============修改第7个元素============");
        list.set(6,"懒洋洋");
        System.out.println("list = " + list);

        System.out.println("============使用迭代器遍历============");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            Object obj = iterator.next();
            System.out.println("obj = " + obj);
        }

    }
}
4、List的三种遍历方式

只要是List接口的实现子类(ArrayList、Vector、LinkedList),遍历方式一致

  • 方式一:使用iterator方式

    //迭代器
    Iterator iterator = list.iterator();
    while (iterator.hasNext()){
        Object object = iterator.next();
        System.out.println("object = " + object);
    }
    
  • 方式二:增强for循环

    //增强for
    for (Object object : list) {
        System.out.println("object = " + object);
    }
    
  • 方式三:使用普通的for

    //普通for
    for (int i = 0; i < list.size(); i++) {
        System.out.println("object = " + list.get(i));
    }
    
5、练习,冒泡排序

创建Book对象

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

    public Book() {
    }

    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 "名称:" + name + "\t\t价格:" + price + "\t\t作者:" + author;
    }
}

进行测试

import java.util.ArrayList;
import java.util.List;

public class ListExercise02 {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add(new Book("天龙八部","金庸",10));
        list.add(new Book("三体","刘慈欣",50));
        list.add(new Book("龙族","江南",20));
        list.add(new Book("西游记","吴承恩",30));

        //遍历
        for (Object o : list) {
            System.out.println(o);
        }

        //调用冒泡排序
        sort(list);
        System.out.println("===============排序过后=================");

        //遍历
        for (Object o : list) {
            System.out.println(o);
        }

    }

    //冒泡排序方法
    public static void sort(List list){
        int size = list.size();

        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - 1 - i; j++) {
                //取出对象Book,向下转型
                Book book1 = (Book) list.get(j);
                Book book2 = (Book) list.get(j + 1);

                //比较交换
                if (book1.getPrice() > book2.getPrice()){
                    //如果book1的价格比book2大,那么这两个位置的元素进行交换,使用set方法进行交换
                    list.set(j, book2);
                    list.set(j + 1, book1);
                }
            }
        }
    }
}
6、ArrayList底层结构和源码分析

ArrayList的注意事项:

  • permits all elements,including null ,ArrayList可以放入null,并且多个

    ArrayList arrayList = new ArrayList();
            arrayList.add(null);
            arrayList.add(null);
            arrayList.add(null);
            arrayList.add(null);
            arrayList.add("喜洋洋");
    
            System.out.println(arrayList);
    
  • ArrayList是由数组来实现数组存储的

  • ArrayList基本等同与Vector,除了ArrayList是线程不安全(执行效率高),在多线程情况下,不建议使用ArrayList

    //源码 没有加 synchronized, 没有做线程互斥
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

扩容机制

1、ArrayList中维护了一个Object类型的数组elementData, transient Object[] elementData

  • Object是一个所有类的父类,什么类型都可以往里面放
  • transient表示短暂的,瞬间的,如果有个属性被 transient修饰后,表示该属性不会被序列化

2、当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData的容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍

3、如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如需要再次扩容,则直接扩容elementData为1.5倍

源码分析:

测试代码:

import java.util.ArrayList;

/**
 * ArrayList源码分析
 */
public class ArrayListSource {
    public static void main(String[] args) {
        //无参构造
        ArrayList list = new ArrayList();
        //有参构造
        //ArrayList list = new ArrayList(8);

        //首次添加
        for (int i = 0; i <= 10; i++) {
            list.add(i);
        }

        //第二次扩容
        for (int i = 11; i <= 15 ; i++) {
            list.add(i);
        }

        //第三次扩容
        list.add(100);
        list.add(200);
        list.add(null);

    }
}

无参构造分析源码:

//无参构造
ArrayList list = new ArrayList();

第一步:ArrayList第一次初始化的就是创建一个空数组

/**
     * Constructs an empty list with an initial capacity of ten.
     */
public ArrayList() {
    //创建空数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 
}
//空数组定义
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

第二步:执行添加的方法,在添加元素之前,需要进行判断是否要扩容

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //执行添加元素之前,先确定数组容量是否够,不够就先扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //容量足够之后再进行添加操作
        elementData[size++] = e;
        return true;
    }

第三步:确定minCapacity最小容量,第一次扩容为10

//该方法用于判断是否扩容
private void ensureCapacityInternal(int minCapacity) {
    //这里明确集合是否是真的需要扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

//源码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //判断传进来的数组是否等于空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //不等于就开始进行比较,取最大值
        //DEFAULT_CAPACITY的默认值为10
        //minCapacity目前集合添加的第几个元素
        //如果默认值大于minCapacity,就返回默认的长度
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //不满足条件则返回需要的容量大小 minCapacity
    return minCapacity;
}

第四步:记录当前集合被修改的次数,判断最小容量是否足够,不够就执行扩容方法,每次添加元素都会执行该方法判断是否要扩容

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //记录当前集合被修改的次数,每次添加一个元素记录一次

        // overflow-conscious code
        //使用当前最小容量 减去 数组的长度,如果大于0,说明容量大小真的不够了,直接调用扩容
        if (minCapacity - elementData.length > 0)
            //进行扩容处理
            grow(minCapacity);
    }

第五步:进行扩容;使用扩容机制确定扩容大小;第一次newCapacity = 10, 第二次以及以后按照1.5倍扩容;扩容使用的是Arrays.copyOf(),使用Arrays.copyOf()的保留之前的数据,然后再进行扩容

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
         //将当前数组的长度记录到旧的容量变量中(oldCapacity)
        int oldCapacity = elementData.length;
        //计算新的容量,使用旧的长度(oldCapacity)加上旧的长度的(oldCapacity)1/2进行扩容,也就是1.5倍,就得到新的容量
        //例如 oldCapacity = 10,则 newCapacity = 10 + 10 *(1/2) = 15
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新的长度减去最小长度小于0,那么最小长度就为新的长度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新的长度减去规定的最大长度,就调用hugeCapacity()方法重新给新的长度赋值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //进行扩容,elementData生成扩容的新数组,填充null,
        //Arrays.copyOf(elementData, newCapacity);再给数组赋值
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

第六步:扩容完成后,返回到添加方法

   /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //扩容完毕后,进行赋值
        //size++是先赋值后再进行++操作,确保下标为0之前进行赋值,再向下移动下标
        elementData[size++] = e;
        return true;
    }

有参构造分析源码:

//有参构造
ArrayList list = new ArrayList(8);

第一步:对传入的参数进行判断,创建指定大小的elementData数组 new Object[initialCapacity]

    /**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) {
        //判断传入的参数是否大于0
        if (initialCapacity > 0) {
            //大于0的参数,创建一个指定大小的Object[initialCapacity]
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果传入的是0,那么就还是按照无参构造器进行使用,从0开始创建一个大小为10的数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //小于0的话,直接抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

第二步:与无参构造的执行流程一致,进行比较扩容

小结:如果是有参的构造器,那么第一次扩容就是按照elementData的1.5倍扩容,不会是默认的10个容量大小

7、Vector的底层结构和源码剖析

基本介绍

1、继承关系

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2、Vector底层也是对象数组

protected Object[] elementData;

3、Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized

源码例子

//源码带有synchronized关键字修饰
public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}

4、在开发中,需要线程同步安全时,考虑使用Vector

Vector与ArrayList比较

底层结构版本线程安全(同步)效率扩容倍数
ArrayList可变数组jdk1.2不安全,效率高如果是有参构造,那么就是1.5倍;如果是无参构造,第一次是10,第二次以及之后是1.5倍扩容
Vector可变数组jdk1.0安全,效率不高如果指定大小,每次直接是2倍扩容;如果是无参构造,默认是10,满后,按照2倍扩容

源码解读:

//创建集合
Vector vector = new Vector();
for (int i = 0; i < 10; i++) {
    vector.add(i);
}

第一步:构建无参构造器,实际上是执行了有参构造器,默认是10

//源码,默认给10的容量,实际上调用的是有参构造器,默认长度是10
public Vector() {
    this(10);
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

第二步:添加数据

public synchronized boolean add(E e) {
    modCount++;//记录集合的修改次数
    ensureCapacityHelper(elementCount + 1); //判断扩容
    elementData[elementCount++] = e;//给集合赋值
    return true;
}

第三步:判断是否需要真的扩容

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    //判断是否需要真正的扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);//扩容处理
}

第四步:执行扩容机制,计算扩容的算法与ArrayList不太一致,Vector使用的是2倍扩容

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //自动增量 capacityIncrement,默认为0,三元运算符,返回的是oldCapacity
    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);
}
8、LinkedList的底层结构和源码分析

基本介绍:

1、LinkedList底层实现双向链表和双端队列特点

2、可以添加任意元素(元素可以重复),包括null

3、线程不安全,没有实现线程同步

LinkedList是底层操作机制

1、LinkedList底层维护的是一个双向链表

2、LinkedList中维护了两个属性first和last分别指向首节点和尾结点

3、每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表

4、所以LinkedList的元素的添加和删除,不是通过数组实现的,相对来说效率较高。

5、双向链表的模拟实现


/**
 * 模拟一个简单的双向链表
 */
public class LinkedListNode {
    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;
        //下面的代码形成的关系 jack <- tom <- lucy
        lucy.pre = tom;
        tom.pre = jack;

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

        System.out.println("==============演示(从头到尾的输出)=============");
        //演示从头到尾遍历
        while (true){
            //如果first(首节点为空,则说明遍历到了最后,退出循环)
            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中添加一个元素(喜洋洋)

        //1、创建一个Node节点
        Node xyy = new Node("喜洋洋");
        //2、让 xyy 的 next 指向lucy,让 xyy 的 pre 指向 tom
        xyy.next = lucy;
        xyy.pre = tom;
        //3、让 lucy 的 pre 指向 xyy,让 tom 的 next 指向 xyy
        lucy.pre = xyy;
        tom.next = xyy;

        //first重新指向首位
        first = jack;
        System.out.println("==============添加元素后的遍历=============");
        //演示从头到尾遍历
        while (true){
            //如果first(首节点为空,则说明遍历到了最后,退出循环)
            if (first == null){
                break;
            }
            //打印首节点指向的元素
            System.out.println(first);
            //首节点移至下一位,再次循环判断打印
            first = first.next;
        }


    }
}

//定义一个Node类,Node对象表示双向链表的节点,可以被实例化
class Node{
    public Object item; //真正存放数据的地方
    public Node next;
    public Node pre;

    public Node(Object item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return "Node name = " + item;
    }
}

LinkedList的源码图解

添加节点的源码:

第一步:

//创建LinkedList
LinkedList linkedList = new LinkedList();
//执行添加
linkedList.add(1);

这时会调用一个无参构造的方法,初始的属性 first = null,last = null,size = 0

//首先会调用无参构造方法(源码)
public LinkedList() {}

第二步:执行add方法

//源码
public boolean add(E e) {
    linkLast(e);
    return true;
}

linkLast方法的实现,当只有一个元素时,first 和 last 都指向这个节点,元素都是从尾进行插入的,因此刚刚插入的数据都是赋值给last

当第一次添加数据,last = null,就会把新的元素赋值给 last ,根据 if (l == null) ,也将第一个元素赋值给 first

第二次插入元素,last不为空,因为之前已经有一个数据了,所以这个 l.next 指向下一个元素

void linkLast(E e) {
    final Node<E> l = last; 
    final Node<E> newNode = new Node<>(l, e, null); //赋值,因为last元素没有next,也就是没有下一个元素,所以next为null
    last = newNode; //第一个节点,last指向这个节点
    if (l == null) 
        first = newNode;
    else
        l.next = newNode;
    size++; //长度加1
    modCount++; //修改次数加1
}

删除第一个节点的源码:

//默认删除的是第一个节点
linkedList.remove();

第一步:调用删除第一个元素的方法

//源码(执行了删除第一个的方法)
public E remove() {
    return removeFirst();
}

第二步:判断第一个元素的first是否为空 first == null

//源码
public E removeFirst() {
    final Node<E> f = first; //将第一个元素的first赋值给 f
    if (f == null) //如果f为空,直接抛出异常
        throw new NoSuchElementException();
    return unlinkFirst(f); //不为空调用unlinkFirst方法,传入f
}

第三步:执行真正的删除方法

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item; //首先将第一个元素item赋值给element
    final Node<E> next = f.next; //再将第一个元素的next赋值给next
    f.item = null; //设置第一个元素的item为null
    f.next = null; // help GC 设置第一个元素的next设置为null
    first = next; //第二行已经给next赋值了,是第二个元素的值,也就是将fist指向第二个元素
    if (next == null) //所以这时的next不是空,而是第二个元素
        last = null; //跳过这个设置last为null的判断,这时的last是第三个元素
    else
        next.prev = null; //设置第二个元素的后指向pre为null
    size--; //长度减1
    modCount++; //修改次数加1
    return element; //返回被删除的元素
}
9、ArrayList和LinkedList的比较
底层结构增删的效率改查的效率
ArrayList可变数组较低,数组扩容较高
LinkedList双向链表较高,通过链表追加较低

如何选择ArrayList和LinkedList:

1、如果我们改查操作多,选择ArrayList

2、如果我们增删操作多,选择LinkedList

3、一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList

4、在一个项目中,根据业务进行灵活选择,也可能这样,一个模块使用ArrayList,另一个模块使用LinkedList

四、Set接口的使用方法

基本介绍:

1、无序(添加和取出的顺序不一致),没有索引

2、不允许重复的元素,所以最多包含一个null

3、jdk API 中Set接口的实现类有HashSet,TreeSet等

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

  • Set接口的遍历方式

    同Collection的遍历方式一样,因此Set接口是Collection接口的子接口

    1、可以使用迭代器

    2、使用增强for

    3、不能使用索引的方式来获取

基本演示

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

public class SetMethod {
    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("jack");
        set.add("tom");
        set.add("lucy");
        set.add(null);
        set.add(null);

        System.out.println("set = " + set);

        //遍历,使用迭代器
        System.out.println("---------------迭代器----------------");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            Object obj = iterator.next();
            System.out.println("obj = " + obj);
        }

        //遍历,使用增强for
        System.out.println("---------------增强for----------------");
        for (Object obj : set) {
            System.out.println("obj = " + obj);
        }
    }
}

1、HashSet的说明

1、HashSet实现了Set接口

2、HashSet实际上是HashMap

//构造器
Set set = new HashSet();
//源码
public HashSet() {
    map = new HashMap<>();
}

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

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

5、不能存放重复元素/对象

演示

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

public class HashSet01 {
    public static void main(String[] args) {
        Set set = new HashSet();

        //1、在执行add方法后,会返回一个boolean值
        //2、如果添加成功,返回true,否则返回false
        System.out.println(set.add("jack"));
        System.out.println(set.add("john"));
        System.out.println(set.add("tom"));
        System.out.println(set.add("jack"));
        System.out.println(set.add("lucy"));
        System.out.println(set.add("mary"));

        //3、通过remove指定删除一个对象
        set.remove("jack");

        //输出的顺序不保证与添加的顺序一致
        System.out.println("set = " + set);


        //重置set集合
        set = new HashSet();
        System.out.println("重置set = " + set);

        set.add("lucy"); //添加成功
        set.add("lucy");//添加失败

        //两个都添加成功,并且不重复
        set.add(new Dog("tom"));
        set.add(new Dog("tom"));

        System.out.println("添加set = " + set);

        //经典面试题,这两个是否添加成功而且不重复
        set.add(new String("java"));
        set.add(new String("java"));
        //结论,只添加了一个java,原理需要看源码的add
        System.out.println("面试set = " + set);
    }
}

class Dog{
    private String name;

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

2、数组链表的模拟

HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

/**
 * 需要debug去了解
 * 尤其是挂载的位置,next的指向会将这个数据结构变成一个链表
 * 每个table的索引上可能会有一条链表
 */
public class HashSetStructure {
    public static void main(String[] args) {
        //模拟一个HashSet的底层结构(也就是HashMap的底层结构)

        //1、创建一个数组,数组类型是Node[]
        //2、有些人直接把Node[]数组称为表
        Node[] table = new Node[16];
        System.out.println("Node = " + table);

        //创建节点
        Node john = new Node("john", null);
        //把节点放到索引为2的位置
        table[2] = john;
        //再创建一个节点
        Node jack = new Node("jack", null);
        //将jack挂载到john后面(也就是将jack加到john后面)
        john.next = jack;
        //再创建一个节点
        Node rose = new Node("rose", null);
        //将rose节点挂载到jack节点后面
        jack.next = rose;

        //将lucy放到索引为3的位置
        Node lucy = new Node("lucy", null);
        table[3] = lucy;
        System.out.println("Node = " + table);
    }
}
//节点,存储数据,可以指向下一个节点,从而形成链表
class Node{
    Object item; //存放数据
    Node next; //指向下一个节点

    public Node(Object item, Node next) {
        this.item = item;
        this.next = next;
    }
}

3、HashSet扩容解析

结论:

1、HashSet底层是HashMap

2、添加一个元素时,先得到hash值,hash值会转为索引值

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

4、如果没有,则直接存放数据

5、如果有,会调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后

6、java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEEIFY_CAPACITY(默认64),就会进行树化(红黑树)

4、源码解读

第一步:创建HashSet,走的是构造器,HashMap

HashSet set = new HashSet();

//源码
public HashSet() {
    map = new HashMap<>();
}
第一次,添加add源码

第二步:添加数据,执行add

set.add("java");
//源码
public boolean add(E e) {
    // private static final Object PRESENT = new Object();
    //PRESENT是起到占位作用,是一个静态不可改变的Object对象
    //e是传进来的值
    return map.put(e, PRESENT)==null; //底层的方法,返回的是null,则代表已经添加成功了
}

第三步:执行的是put方法,该方法会执行 hash(key) 得到key对应的hash值,但是得到的不是并不是key真正的 hashCode,为了降低hash冲突的几率,执行了一个算法

//源码
//key是传进来的值
//value是PRESENT,共享的
public V put(K key, V value) {
    //hash(key)计算出hash值,确定数据在表里面的索引位置
    return putVal(hash(key), key, value, false, true);
}

//源码,计算hash值,key是传进来的元素
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

第四步:执行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 ((tab = table) == null || (n = tab.length) == 0) //第一次添加初始状态table为空,长度等于0
        n = (tab = resize()).length; //经过扩容,resize(),提供了16个空间
    
    //1、根据key,得到hash 去计算这个key应该存放在table表的哪个索引位置,并且把这个位置的对象,赋值给 p
    //2、再判断这个p是否为空,如果p为空,则表示该位置没有存放元素,就创建 Node(key,value),其中这个key就是元素,value是占位PRESENT,最后一个null是next,指向下一个元素,hash是为了比较下一个存进来的值的hash是否相等
    //3、最后将这个 Node newNode(hash, key, value, null),放在 tab[i] 位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            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);
                    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;
    //threshold 通过resize()方法计算出的临界容量
    if (++size > threshold)
        resize(); //如果size大于临界容量,那么就开始扩容
    afterNodeInsertion(evict); //空方法
    return null; //代表成功了
}
第二次,添加重复元素

添加调用的方法,与第一次一致,但是执行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;
        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个节点,到达8个就调用treeifyBin()对当前这个链表进行树化(转成红黑树)
                //注意:在转成红黑树前需要进行判断,判断条件如下
                //tab不为null并且tab的长度不超过64
                //if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            		//resize();
                //如果上述条件成立,先进行table扩容,不成立再进行树化
                //(2)依次和该链表的每个元素比较后,如果存在一个相同的,直接break
                for (int binCount = 0; ; ++binCount) {//循环比较
                    //e是链表的第二个元素,p是第一个元素,依次类推,e是第三个元素时,则p是第二个元素
                    //判断e是否为null,如果是,则直接添加
                    if ((e = p.next) == null) {
                        // 链表中,给p的下一个值赋值
                        p.next = newNode(hash, key, value, null);
                        //判断是否需要进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //不是null,进行比较,如果链表上的值与要添加进来的值一致,直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//再将e的值赋值给p,进行e后移一个元素
                }
            }
            //判断这个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,就会增加1
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5、HashSet的扩容和转成红黑树机制

结论:

1、HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor = 0.75)= 12

2、如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推

3、在java8中,涂过一条链表的元素个数达到了 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

演示代码:

public class HashSetIncrement {
    public static void main(String[] args) {
        HashSet set = new HashSet();

        //1、HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor = 0.75)= 12
        //2、如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
        //添加
        for (int i = 0; i <= 100; i++) {
            set.add(String.valueOf(i));
        }

        //3、在java8中,涂过一条链表的元素个数达到了 TREEIFY_THRESHOLD(默认是8),
        // 并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),
        // 否则仍然采用数组扩容机制
        //如何将元素放到同一个链表上,首先保证hash值相同,但是值的内容不同

        for (int i = 0; i <= 12; i++) {
            set.add(new A(i)); //保证hash一致
        }

        System.out.println("set = " + set);

    }
}

class A{
    private int n;

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


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

疑问:

set集合中size是否是只是计算链表的第一个元素,还是计算table 中的所有元素?

测试:

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

public class HashSetIncrement {
    public static void main(String[] args) {
        HashSet set = new HashSet();
        /**
         * 判断size是否只是计算table中链表的第一个元素,还是计算所有的元素
         * 在添加元素时,底层会将元素封装为一个Node,再加入到table中,就算增加一个元素,size++,不只是table中链表的第一个元素
         */

        //先给table中的某条链表添加7个A对象
        for (int i = 0; i <= 7; i++){
            set.add(new A(i)); //加在同一条链表上
        }
        //再给table的另一条链表添加7个B对象
        for (int i = 0; i <= 7; i++) {
            set.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;
    }
}

结论:

1、在添加元素时,底层会将元素封装为一个Node,再加入到table中,就算增加一个元素,size++,不只是table中链表的第一个元素

2、即使table没有被填满,但是当size到达临界值,也会触发扩容机制,进行扩容

6、HashSet练习

第一题:

要求:

1、定义一个Employee类,该类包含private成员属性name,age

2、创建3个Employee对象,放入HashSet中

3、当name和age值相同时,人为是相同员工,不能添加到HashSet集合中

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

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

        HashSet set = new HashSet();

        //经过重写equals和hashCode后,当名字和年龄相同后,hashSet就会认为添加的元素相同,加入不成功
        set.add(new Employee("tom",18));
        set.add(new Employee("jack",19));
        set.add(new Employee("tom",18));


        System.out.println("set = " + set);

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

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

    public Employee() {
    }

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

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

第二题:

要求:

1、定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyDate类型(属性包括:year,month,day)

2、创建3个Employee放入HashSet中

3、当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中

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

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

        HashSet set = new HashSet();

        set.add(new Employee02("tom",12,new MyData(2012,12,5)));
        set.add(new Employee02("jack",12,new MyData(2012,12,5)));
        set.add(new Employee02("tom",12,new MyData(2012,12,5)));

        System.out.println("set = " + set);
    }
}
class Employee02{
    private String name;
    private int sal;
    private MyData birthday;

    public Employee02() {
    }

    public Employee02(String name, int sal, MyData birthday) {
        this.name = name;
        this.sal = sal;
        this.birthday = birthday;
    }

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

    public MyData getBirthday() {
        return birthday;
    }

    public void setBirthday(MyData birthday) {
        this.birthday = birthday;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee02 that = (Employee02) o;
        return sal == that.sal &&
                Objects.equals(name, that.name) &&
                Objects.equals(birthday, that.birthday);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, sal, birthday);
    }
}
class MyData{
    private int year;
    private int month;
    private int day;

    public MyData(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public MyData() {
    }

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

    @Override
    public String toString() {
        return "MyData{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyData myData = (MyData) o;
        return year == myData.year &&
                month == myData.month &&
                day == myData.day;
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }
}

7、LinkedHashSet

介绍:

1、LinkedHashSet是HashSet的子类

2、LinkedHashSet底层是一个LinkedHashMap,底层维护一个 数组 + 双向链表

3、LinkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的

4、LinkedHashSet不允许有重复元素

说明:

1、在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet 有 head 和 tail)

2、每一个节点有 before 和 after 属性,这样可以形成双向链表

3、在添加一个元素时,先求hash值,在求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和HashSet一样】)

tail.next = newElement //简单指定

newElement.pre = tail

tail = newElement;

4、这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致

分析底层:

检测代码:

import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetSource {
    public static void main(String[] args) {
        Set set = new LinkedHashSet();

        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("漩涡鸣人",1001));
        set.add(123);
        set.add("HELLO");

        System.out.println("set = " + set);

    }
}

class Customer{
    private String name;
    private int num;

    public Customer() {
    }

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

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

解读:

1、LinkedHashSet 添加和取出元素/数据的顺序是一致的

2、LinkedHashSet底层维护的是一个LinkedHashMap(是HashMap的子类)

3、LinkedHashSet底层结构(数组table + 双向链表)

4、添加第一次时,直接将数组table扩容到16,存放的接待您类型是LinkedHashMap$Entry

5、数组时HashMap N o d e [ ] 存放的元素 / 数据是 L i n k e d H a s h M a p Node[] 存放的元素 / 数据是LinkedHashMap Node[]存放的元素/数据是LinkedHashMapEntry(数组多态,Entry继承了HashMap的Node,继承关系是在内部类进行完成的)

6、底层的添加方法还是走的HashSet添加方法

add()源码:

    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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                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);
                        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;
    }

练习:

要求:

Car类(属性:name,price),如果name和price一样,则认为是相同元素,就不添加

需要重写equals方法和hashCode方法

import java.util.LinkedHashSet;
import java.util.Objects;

public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet set = new LinkedHashSet();

         set.add(new Car("奥迪",100));
        set.add(new Car("宝马",110));
        set.add(new Car("法拉利",100));
        set.add(new Car("奥迪",100));

        System.out.println("set = " + set);

    }
}
class Car{
    private String name;
    private int price;

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

    public Car() {
    }

    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 +
                '}';
    }

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

五、Map接口和常用方法

特点介绍:

注意:这里讲的是JDK8的Map接口特点

1、Map与Collection并列存在。用于存放具有映射关系的数据:Key-Value

2、Map中的key和value可以是任何引用类型的数据,会封装HashMap$Node对象中

3、Map中的key不可以重复,原因和HashSet一致(替换机制,会将key相同的旧值替换成新的值)

4、Map中的value可以重复

5、Map的key可以为null,value也可以为null,但是key为null,只能有一个,value为null,可以为多个

6、常用String类作为Map的key,只要是Object的子类都可做key

7、key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value

8、Map存放数据的key-value示意图,一对k-v是放在一个Node中的,有因为Node实现了Entry,有的书上也说一对k-v就是一个Entry

//第8点源码
static class Node<K,V> implements Map.Entry<K,V>
import java.util.HashMap;
import java.util.Map;

public class Map_ {
    public static void main(String[] args) {
        //实现类的特点
        Map map = new HashMap();

        //1、Map与Collection并列存在。用于存放具有映射关系的数据:Key-Value(双列元素)
        //底层是hash,Node结点
        //2、Map中的key和value可以是任何引用类型的数据,会封装HashMap$Node对象中
        map.put("num1","hello");//k-v
        map.put("num2","world");//k-v

        //3、Map中的key不可以重复,原因和HashSet一致,当有相同的key,等价于替换
        map.put("num2","hhhh");//k-v

        //4、Map中的value可以重复
        map.put("num3","hhhh");//k-v

        //5、Map的key可以为null,value也可以为null,但是key为null,只能有一个,value为null,可以为多个
        map.put(null,null);
        map.put(null,"abc");//替换map.put(null,null);中的值
        map.put("num4",null);
        map.put("num5",null);

        //6、常用String类作为Map的key,只要是Object的子类都可做key
        map.put(1,"张三");
        map.put(new Object(),"李四");

        //7、key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
        System.out.println("key = 1 " + map.get(1));
        System.out.println(map.get("key = num5 " + map.get("num5")));

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

代码解读:

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapSource_ {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("num1","hello");//k-v
        map.put("num2","world");//k-v

        //1、k-v最后是存在  HashMap$Node node = newNode(hash, key, value, null); 的对象中
        //2、k-y为了方便遍历,还会在底层创建一个EntrySet集合,该集合存放元素的数据类型是Entry,
        // 而一个Entry对象就有k、v EntrySet<Entry<K,V>> 即:transient Set<Map.Entry<K,V>> entrySet;
        //3、在entrySet中,定义的类型是Map.Entry,但是实际上存放的还是 HashMap$Node,因为HashMap$Node实现了Map.Entry的接口
        //即 HashMap$Node implements Map.Entry
        //4、把 HashMap$Node对象 存放到entrySet就可方便遍历
        //因为Map.Entry提供了两个重要方法,即 K getKey(); 和 V getValue();

        Set set = map.entrySet();
        System.out.println(set.getClass());
        for (Object object : set) {
            //System.out.println(object.getClass());
            //为了从HashMap$Node对象中取出k-v
            //1、首先进行向下转型
            Map.Entry entry = (Map.Entry) object;
//            System.out.println("key = " + entry.getKey());
//            System.out.println("value = " + entry.getValue());
            System.out.println(entry.getKey() + "-" + entry.getValue());
        }

        //获取map集合中的所有key
        Set keySet = map.keySet();
        System.out.println("keySet = " + keySet);

        //获取map集合中的所有value
        Collection values = map.values();
        System.out.println("values = " + values);
    }
}


为了方便管理,把底层的每个node封装为一个Entry,再把每个Entry放在EntrySet中,关系如下,每个key和value是通过内存地址指向的

1、Map接口常用方法

import java.util.HashMap;
import java.util.Map;

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

        Map map = new HashMap();

        map.put("book",new Book("三国",120));
        map.put("雷军","小米");
        map.put("任正非","华为");
        map.put("库克","苹果");
        map.put("马斯克","特斯拉");
        map.put("孙悟空",null);
        map.put(null,"唐僧");

        //remove:根据key删除value,根据键删除映射关系
        map.remove(null);

        //get:根据键获取值,根据key获取value
        System.out.println("get = " + map.get("雷军"));

        //size:获取元素个数
        System.out.println("size = " + map.size());

        //isEmpty:判断集合个数是否为0
        System.out.println("isEmpty = " + map.isEmpty());

        //containsKey:查找键是否存在,返回boolean值
        System.out.println("containsKey = " + map.containsKey("任正非"));

        //clear:清除
        map.clear();
        System.out.println("clear = " + map);

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


    }
}
class Book{
    private String name;
    private int price;

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

    public Book() {
    }

    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 "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

2、Map的遍历方法

import java.util.*;

public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();

        map.put("雷军","小米");
        map.put("任正非","华为");
        map.put("库克","苹果");
        map.put("马斯克","特斯拉");
        map.put("孙悟空","猪八戒");
        map.put("白龙马","唐僧");

        //第一组:先取出所有的key,通过key取出所有的value
        Set keySet = map.keySet();
        System.out.println("=================第一组遍历方式=====================");
        //(1)增强for循环
        System.out.println("-----------第一种:增强for循环-------------");
        for (Object key : keySet) {
            System.out.println(key + "-" + map.get(key));
        }
        //(2)迭代器
        System.out.println("-----------第一种:迭代器-------------");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()){
            //这个iterator.next()就是一个个的key
            Object key = iterator.next();
            System.out.println(key + "-" + map.get(key));
        }

        //第二组:把所有的value取出,value是一种集合,
        System.out.println("=================第二组遍历方式=====================");
        Collection values = map.values();
        //(1)增强for循环
        System.out.println("-----------第一种:增强for-------------");
        for (Object value : values) {
            System.out.println("value = " + value);
        }
        //(2)迭代器
        System.out.println("-----------第二种:迭代器-------------");
        Iterator valueIterator = values.iterator();
        while (valueIterator.hasNext()){
            Object value = valueIterator.next();
            System.out.println("value = " + value);
        }

        //第三组:通过EntrySet来获取key-value
        System.out.println("=================第三组遍历方式=====================");
        Set entrySet = map.entrySet(); //类型 EntrySet<Entry<k,v>>
        //(1)增强for
        System.out.println("-----------第一种:增强for-------------");
        for (Object entry : entrySet) {
            //将 entry 转为 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            //在进行取值
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        //(2)迭代器
        System.out.println("-----------第二种:迭代器-------------");
        Iterator entryIterator = entrySet.iterator();
        while (entryIterator.hasNext()) {
            //这里的next的类型是HashMap$Node,但是Node继承了 Map.Entry
            Object next =  entryIterator.next();
            //所以这里就可以将next转为 Map.Entry
            Map.Entry m = (Map.Entry) next;
            System.out.println(m.getKey() + "-" + m.getValue());
        }

    }
}

练习:

要求:

使用HashMap添加3个员工对象

键:员工id

值:员工对象

并且遍历显示工资大于18000的员工(遍历方式不少于两种)

员工类:姓名,工资,员工id

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

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

        Map map = new HashMap();

        map.put(1001,new Employee("张三",15000,1001));
        map.put(1002,new Employee("李四",19000,1002));
        map.put(1003,new Employee("王五",20000,1003));
        map.put(1004,new Employee("赵六",11000,1004));
        map.put(1005,new Employee("孙七",18000,1005));
        map.put(1006,new Employee("张八",14000,1006));

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

        //获取所有的key
        Set keySet = map.keySet();

        //遍历方式一
        //(1)增强for循环
        System.out.println("-----------第一种遍历:增强for-------------");
        for (Object key : keySet) {
            //根据key获取值,也就是Employee对象
            Object object = map.get(key);
            //向下转型
            Employee employee = (Employee) object;
            //判断薪资
            if (employee.getSal() > 18000){
                System.out.println(employee);
            }
        }
        //(2)迭代器
        System.out.println("-----------第二种遍历:迭代器-------------");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()){
            //所有的key
            Object key = iterator.next();
            //根据key获取对应的Employee对象
            Object object = map.get(key);
            //向下转型
            Employee employee = (Employee) object;
            //判断薪资
            if (employee.getSal() > 18000){
                System.out.println(employee);
            }
        }
        //(3)使用EntrySet
        System.out.println("-----------第三种遍历:entrySet-------------");
        Set entrySet = map.entrySet();
        for (Object entry : entrySet) {
            //将 entry 转为 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            //取出Employee对象,并且向下转型,因为Object对象
            Employee employee = (Employee) m.getValue();
            //判断薪资
            if (employee.getSal() > 18000){
                System.out.println(employee);
            }
        }

    }
}
class Employee{
    private String name;
    private int sal;
    private int id;

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

    public Employee() {
    }

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

    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 +
                '}';
    }
}

3、HashMap小结

1、Map接口的常用实现类:HashMap、Hashtable和Properties

2、HashMap是Map接口使用频率最高的实现类

3、HashMap是以key-value的方式来存储数据(数据类型是HashMap$Node类型)

4、key不能重复,但是值可以重复,允许使用null键和null值

5、如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,value会替换)

6、与HashSet一样,不保存映射的顺序,因为底层是以hash表方式来存储的(HashMap底层 数组 + 链表 + 红黑树)

7、HashMap没有实现同步,所以线程不安全

4、HashMap的底层机制及源码剖析

示意图:

table表是个数组,数组中放有链表或者是一棵树,每个节点就是一个Node,Node这个类型又实现了Map.Entry接口

1、(k1,v1)是一个Node实现了Map.Entry<K,V>,查看HashMap的源码可以看到

2、jdk7.0的HashMap底层实现 [数组 + 链表],jdk8.0底层 [数组 + 链表 + 红黑树]

扩容机制:

1、HashMap底层维护了Node类型的数组table,默认为null

2、当创建爱你对象时,将加载因子(loadfactor)初始化为0.75

3、当添加 key-value 时,通过key的哈希值得到在table中的索引,然后判断该索引是否有元素,如果没有元素,直接添加,如果该索引有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,直接替换value;如果不相等需要判断是树结构还是链表结构,做出相应的处理,如果添加时发现容量不够,则需要扩容

4、第1次添加,则需要扩容table容量为16,临界值(threshold)为12

5、以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,以此类推

6、在java8中,如果一条链表的元素超过TREEIFY_THRESHOLD(默认为8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进化为红黑树

源码剖析:

测试代码:

import java.util.HashMap;

public class MapSource {
    public static void main(String[] args) {
        HashMap map = new HashMap();

        map.put("java",10);
        map.put("mysql",10);
        map.put("java",11);

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

    }
}

第一步:执行构造器,初始化加载因子,table数组类型是HashMap$Node类型

HashMap$Node[] table = null;

//源码:初始化加载因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//加载因子的值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

第二步:执行put方法

//源码:hash(key)计算key的hash值
public V put(K key, V value) {
    //执行putVal方法
    //此时的key就是传进来的key,value是传进来的value
    return putVal(hash(key), key, value, false, true);
}

//执行的hash()
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

第三步:执行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,第一次添加时table一定是为null的
    if ((tab = table) == null || (n = tab.length) == 0)
        //table为null时,首先进行扩容处理,resize()扩容方法
        n = (tab = resize()).length;//扩容完毕后,返回一个空的table表,有16个空间
    //计算得到了hash值,将这个k-y放到hash值对应的table表索引中,同时将这个值赋值给 p 变量,但是要确保这个索引位置没有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        //在table[i]的位置插入该元素,i是根据hash值得到的索引位置,也就是这个元素在table表中的索引位置
        //元素的类型是Node
        tab[i] = newNode(hash, key, value, null);
    else { //当key重复时,就是进入这里
        Node<K,V> e; K k; //临时变量
        //三种情况
        //(1)p的hash值是否与hash值是否相同,这个hash是现在加入的元素的hash值,p.hash是已经存在table中的元素的hash值
        //(2)新加入的key是否与p的key是否是同一个对象
        //(3)新的key不为空但是内容相同
        //这三种情况出现都不在进行添加
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; //把p赋值给e,辅助变量
        else if (p instanceof TreeNode) //如果这个p是树
            //按照红黑树方式添加
            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个元素,如果达到了进行树化,TREEIFY_THRESHOLD = 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 当binCount = 8 时,也就是添加到第9个元素时,开始调用树化方法,但是并不是马上树化(转成红黑树)
                        treeifyBin(tab, hash); //树化
                    break;//查询到了链表中有一样的元素,跳出循环,不进行添加
                }
                //与上面的判断一致
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;//不在执行添加,直接跳出循环
                p = e; //将e赋值给p
            }
        }
        //key重复,e不为null
        if (e != null) { // existing mapping for key
            V oldValue = e.value; //将e的值赋值给旧值oldValue
            //判断如果这个旧值oldValue为空或者onlyIfAbsent取反满足条件
            if (!onlyIfAbsent || oldValue == null)
                //将e的value值替换为新的value
                e.value = value;
            afterNodeAccess(e);
            return oldValue;//返回旧的value
        }
    }
    ++modCount;
    //++size是添加元素后的size,再与临界值比较,如果达到了临界值,开始扩容
    if (++size > threshold)
        resize(); //扩容
    afterNodeInsertion(evict);
    return null;
}

第四步:扩容方法 resize()

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //声明一个旧的table,将空的table赋值给oldTab
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //判断旧的table容量
        int oldThr = threshold; //临界值赋值给旧的临界值
        int newCap, newThr = 0; //声明新的容量和新的临界值,赋值为0
        if (oldCap > 0) { //判断旧的table容量是否大于0
            if (oldCap >= MAXIMUM_CAPACITY) { //如果旧的table容量大于额定的容量
                threshold = Integer.MAX_VALUE; //给临界值重新赋值
                return oldTab; //返回旧的table的容量
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
        }
        else if (oldThr > 0) //判断旧的临界值是否大于0
            newCap = oldThr;
        else {//当上面条件都满足,则就是给默认的table表额定容容量           
            newCap = DEFAULT_INITIAL_CAPACITY; //指定容量 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //计算临界值
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr; //这里就是给定的临界值
        @SuppressWarnings({"rawtypes","unchecked"})
     	//这里则生成已经扩容好的table,容量大小由newCap指定,而table表的类型是Node类型
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

5、Hashtable的扩容机制与源码分析

基本介绍:

1、存放的元素时键值对:k-v

2、Hashtable的键和值不能为null,否则抛出NullPointerException异常

3、Hashtable使用方法基本和HashMap一样

4、Hashtable是线程安全的,HashMap是线程不安全的

5、Hashtable的底层是一个Entry数组

演示代码:

import java.util.Hashtable;
import java.util.Map;

public class HashTableMethod {
    public static void main(String[] args) {
        Hashtable table = new Hashtable();

        table.put("java",100); //ok
        table.put(null,100); //异常
        table.put("mysql",null); //异常
        table.put("redis",20); //ok
        table.put("redis",100); //替换上面键相同的元素

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

    }
}

分析底层:

1、底层有数组 Hashtable$Entry[] ,Entry是Hashtable的内部类,继承了Map.Entry,初始大小为11,临界值为8

//源码,初始化调用无参构造,11为初始大小,0.75为加载因子
public Hashtable() {
    this(11, 0.75f);
}

2、执行put方法

public synchronized V put(K key, V value) {
    // 这里规定的是value不能为null
    if (value == null) {
        throw new NullPointerException();
    }

    // 创建一个Entry类型的数组table
    Entry<?,?> tab[] = table;
    //计算key的hash
    int hash = key.hashCode();
    //计算元素应该存放的下标位置
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    //向下转型,取出table在index位置上的Entry元素
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //for循环中判断,如果entry不为null,那就需要进行比较,这个entry上的元素是否与现在插入的元素的key一致
    for(; entry != null ; entry = entry.next) {
        //判断该位置上的key是否相同
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            //进行替换
            entry.value = value;
            return old;
        }
    }
    //执行真正的添加方法
    addEntry(hash, key, value, index);
    return null;
}

3、执行真正的添加方法 addEntry

private void addEntry(int hash, K key, V value, int index) {
    modCount++; //记录集合修改次数

    Entry<?,?> tab[] = table; //创建一个空的table
    //判断table表中的总数是否大于或者等于临界值
    if (count >= threshold) {
        // 如果超过,则进行扩容
        rehash();
        tab = table;
        hash = key.hashCode(); //获取key的hash值
        index = (hash & 0x7FFFFFFF) % tab.length; //计算元素在table表中的下标index
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    //找到之前元素在table表中的位置
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //在table表中index位置存放新的Entry类型元素
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

4、扩容rehash()方法

扩容计算机制:oldCapacity * 2 + 1

扩容后临界值计算机制:(int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1) 新的容量 * 加载因子后,与最大容量 + 1 进行取最小值,取扩容后的临界值

protected void rehash() {
    //获取当前table的长度
    int oldCapacity = table.length;
    //声明一个Entry类型数组的table
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //新的容量计算,旧的容量向左移一位,相当于*2,再加上1
    int newCapacity = (oldCapacity << 1) + 1;
    //判断新的容量如果大于了额定的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        //再次判断旧的容量是否等于额定的最大容量
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        //以最大容量为标准
        newCapacity = MAX_ARRAY_SIZE;
    }
    //按照最新的容量创建一个Entry类型的数组,大小为已经扩容后的大小
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    //重新计算临界值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    //将这个新的数组赋值给table表
    table = newMap;

    //将旧的table表中的元素移动到新的数组上
    for (int i = ol dCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

6、Properties的基本介绍

1、Properties类继承了Hashtable类并且实现了Map接口,也是一种使用键值对类型存储数据

2、使用特点与Hashtable类似

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

4、xxx.properties 文件通常作为配置文件

基本使用:

import java.util.Properties;

public class Properties_ {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //添加,k-y
        properties.put("java",100);
        properties.put("mysql",100);
        properties.put("python",100);
        properties.put("php",100);
        
        //相同的key,value也会被替换,相当于修改
        properties.put("php",300);

        //因为 properties 是 Hashtable 的子类,所以key和value都不能为null
        //properties.put(null,100); //异常
        //properties.put("html",null);//异常

        //通过key获取value
        System.out.println("get = " + properties.get("java"));

        //通过key移除元素
        properties.remove("mysql");
        System.out.println("remove = " + properties);



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

六、总结

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

1、先判断存储类型(一组对象或一组键值对)

2、一组对象 [单列集合]:Collection接口

  • 允许重复:List接口实现类

    增删操作多:LinkedList [底层维护了一个双向链表]

    改查操作多:ArrayList [底层维护 Object 类型的可变数组]

  • 不允许重复:Set接口实现类

    无序:HashSet [底层是HashMap,维护了一个哈希表,即(数组 + 链表 + 红黑树)]

    排序:TreeSet

    插入和取出顺序一致:LinkedHashSet,维护数组 + 双向链表

3、一组键值对 [双列集合]:Map接口实现类

​ 键无序:HashMap [底层是:哈希表 jdk7:数组 + 链表,jdk8:数组 + 链表 + 红黑树 ]

​ 键排序:TreeMap

​ 键插入和取出顺序一致:LinkedHashMap

​ 读取文件:Properties

七、TreeSet和TreeMap的源码剖析

1、TreeSet

特点:

1、相比于HashSet来说,可排序

2、使用无参构造器,创建TreeSet时,使用的是自然排序

3、底层使用的是TreeMap,是Entry类型

4、源码区别

//TreeSet的add方法源码
public boolean add(E e) {
    //e是存放进来的值,PRESENT是一个空的Object对象
    //private static final Object PRESENT = new Object();
    return m.put(e, PRESENT)==null;
}

默认排序:

import java.util.TreeSet;

public class TreeSet_ {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();

        treeSet.add("java");
        treeSet.add("mysql");
        treeSet.add("php");
        treeSet.add("python");

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

实现接口的匿名内部类进行排序:

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

public class TreeSet_ {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //调用的是String的compareTo方法进行比较
                //原理是取出每个字符的长度,进行ASCII码比较
                return ((String)o1).compareTo((String)o2);
            }
        });


        treeSet.add("mysql");
        treeSet.add("php");
        treeSet.add("python");
        treeSet.add("java");


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

源码:

第一步:构造器把传入的比较对象(Comparator),赋给了 TreeSet 底层的 TreeMap 属性

//源码
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

第二步:添加元素,添加第一个元素时,不会走比较器

public V put(K key, V value) {
    Entry<K,V> t = root; //取出元素,树化
    if (t == null) { //只有元素为null时走这里,就是第一次添加时走这里
        //传了两个相同的key,检查key是否为空
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    //辅助变量
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    //比较器,声明TreeSet时,传入的比较器,在这里赋值
    Comparator<? super K> cpr = comparator;
    //当这个比较器不为null时,进行比较
    //cpr就是一个匿名内部类
    if (cpr != null) {
        do {
            parent = t;
            //动态绑定到匿名内部类(对象)compare方法
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value); //如果相等,cmp为0,数据就加入不了,相当于替换
        } while (t != null);
    }
    //这里是没有匿名内部类的逻辑
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            //根据compareTo进行比较
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

2、TreeMap

特点:

1、传入的是k-y,比较key

默认排序:

在使用无参构造器时,这里的排序是根据ASCII码进行比较的,字母表顺序排序

import java.util.TreeMap;

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

        TreeMap treeMap = new TreeMap();

        treeMap.put("java",100);
        treeMap.put("mysql",200);
        treeMap.put("php",300);
        treeMap.put("js",400);

        System.out.println("treeMap = " + treeMap);
    }
}
//输出打印 treeMap = {java=100, js=400, mysql=200, php=300}

使用匿名内部类进行排序:

import java.util.Comparator;
import java.util.TreeMap;

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

        //传入匿名内部类
        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //比较规则:按照传入的k(String)的大小进行排序
                return ((String)o2).compareTo((String)o1);
            }
        });

        treeMap.put("java",100);
        treeMap.put("mysql",200);
        treeMap.put("php",300);
        treeMap.put("js",400);

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

//打印输出 treeMap = {php=300, mysql=200, js=400, java=100}

源码:

第一步:接收传入的实现了匿名内部类(对象)Comparator,传给了TreeMap的comparator属性

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

第二步:调用add方法

 public V put(K key, V value) {
     Entry<K,V> t = root;
     //第一次添加走这里
     if (t == null) {
         //传了两个相同的key,检查key是否为空
         compare(key, key); // type (and possibly null) check

         root = new Entry<>(key, value, null);
         size = 1;
         modCount++;
         return null;
     }
     int cmp;
     Entry<K,V> parent;
     // split comparator and comparable paths
     //接收传入的匿名内部类(对象)比较方法
     Comparator<? super K> cpr = comparator;
     //进入比较器
     if (cpr != null) {
         //遍历,如果遍历完后没有一个数据与传进来的数据相同,那就会挂到root上
         //如果有一个数据相同,那就不会挂载
         do {
             parent = t;
             //这里的值相等判断与HashMap有区别
             //HashMap判断值是否相等是通过hash或者equals方法等进行判断的
             //这里的相等是通过compare方法决定的
             cmp = cpr.compare(key, t.key);
             if (cmp < 0)
                 t = t.left;
             else if (cmp > 0)
                 t = t.right;
             else
                 return t.setValue(value); //有相同的,替换
         } while (t != null);
     }
     //走默认的比较规则
     else {
         if (key == null)
             throw new NullPointerException();
         @SuppressWarnings("unchecked")
         //默认的比较规则
         Comparable<? super K> k = (Comparable<? super K>) key;
         do {
             parent = t;
             cmp = k.compareTo(t.key);
             if (cmp < 0)
                 t = t.left;
             else if (cmp > 0)
                 t = t.right;
             else
                 return t.setValue(value);
         } while (t != null);
     }
     Entry<K,V> e = new Entry<>(key, value, parent);
     //判断元素挂载在左边还是挂载在右边
     if (cmp < 0)
         parent.left = e;
     else
         parent.right = e;
     fixAfterInsertion(e);
     size++;
     modCount++;
     return null;
    }

八、Collections工具类

基本介绍:

1、Collections是一个操作Set、List和Map等集合的工具类

2、Collections中提供了一系列静态方法对集合元素进行排序、查询和修改等操作

排序操作:(均为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集合中的 i 处元素和 j 处元素进行交换

测试代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Collections_ {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("java");
        list.add("mysql");
        list.add("css");
        list.add("html");
        list.add("js");

        //reverse(List):反转 List 中元素顺序
        Collections.reverse(list);
        System.out.println("reverse = " + list);

        //shuffle(List):对 List 集合元素进行随机排序
        Collections.shuffle(list);
        System.out.println("shuffle = " + list);

        //sort(List):根据元素的自然顺序对指定List集合元素按升序排序
        Collections.sort(list);
        System.out.println("sort = " + list);

        //sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
        Collections.sort(list, new Comparator<Object>() {
            @Override
            public int compare(Object o1, Object o2) {
                //自定义排序规则
                return ((String)o1).length() - ((String)o2).length();
            }
        });
        System.out.println("sort自定义排序 = " + list);

        //swap(List,int,int):将指定List集合中的 i 处元素和 j 处元素进行交换
        Collections.swap(list, 2, 3);
        System.out.println("swap = " + list);

    }
}

查找、替换:

1、Object max(Collection):根据元素的自然顺序,返回给定集合中最大元素

2、Object max(Collection,Comparator):根据Comparator指定的顺序,返回集合中最大的元素

3、Object mix(Collection):根据元素的自然顺序,返回给定集合中最小元素

4、Object mix(Collection,Comparator):根据Comparator指定的顺序,返回集合中最小的元素

5、int frequency(Collection,Object):返回指定集合中指定元素出现次数

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

7、boolean replayceAll(List list,Object oldVal,Object newVal):使用新的值替换List对象的所有旧值

演示代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Collections_ {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("java");
        list.add("mysql");
        list.add("css");
        list.add("html");
        list.add("js");

        //Object max(Collection):根据元素的自然顺序,返回给定集合中最大元素
        System.out.println("自然排序最大值:" + Collections.max(list));

        //Object max(Collection,Comparator):根据Comparator指定的顺序,返回集合中最大的元素
        System.out.println("自定义规则最大值:" + Collections.max(list, new Comparator<Object>() {
            @Override
            public int compare(Object o1, Object o2) {
                //自定义比较规则
                return ((String)o1).length() - ((String)o2).length();
            }
        }));

        //参考max即可
        //Object mix(Collection):根据元素的自然顺序,返回给定集合中最小元素
        //Object mix(Collection,Comparator):根据Comparator指定的顺序,返回集合中最小的元素

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

        //void copy(List dest,List src):将src的内容复制到dest中
        ArrayList dest = new ArrayList();
        //拷贝前需要给dest赋值,长度与list现在的长度一致
        for (int i = 0; i < list.size(); i++) {
            dest.add("");
        }
        //拷贝
        Collections.copy(dest,list);
        System.out.println("dest = " + dest);

        //boolean replayceAll(List list,Object oldVal,Object newVal):使用新的值替换List对象的所有旧值
        Collections.replaceAll(list,"java","javaee");
        System.out.println("replaceAll = " + list);

    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值