十、集合框架

1. 概念

        Java集合框架(Java Collections Framework简称JCF)是为表示和操作集合,而规定的一种统一的标准的体系结构。集合框架包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

        集合就是用于存储对象的容器。 只要是对象类型就可以存进集合框架中。集合的长度是可变的。 集合中不可以存储基本数据类型的值。

2. 集合和数组的区别

        数组和集合相比,数组的缺点是它长度是固定的,没有办法动态扩展。而集合存储数据时是没有长度限制的,是可以动态扩展的。集合容器因为内部的数据结构不同,有多种不同的容器对象。这些容器对象不断的向上抽取,就形成了集合框架。

        数组:定容               集合:长度可以动态扩展

3. 为什么要使用集合

        在讲为什么要使用集合之前,我们首先讲一个案例:

  案例:手撕可变长度的容器      

        首先创建一个MyArray类,实现可以动态扩展数组的功能,并包含添加数据,打印数组和查询数组下标对应的数据内容这三个功能。代码如下:

package com.qy151.test1;

import java.util.Arrays;

/**
 * @作者:刘壬杉
 * @创建时间 2022/4/15
 **/
public class MyArry {
    private int size;        //创建一个int变量size,默认值为0
    private Object [] arr ;  //创建一个Object[]数组
    //创建无参构造
    public MyArry(){
        this(3);             //调用无参构造时,this(3) 代表的是调用有参构造并传入参数值为3
                             //即调用无参构造时,inSize = 3.
    }
    //创建有参构造
    public MyArry(int inSize){
        if (inSize<0){
            throw new RuntimeException("长度不合法");  //抛出异常
        }
        arr = new Object[inSize];
    }
    //创建addDate方法,实现将数据填入数组的共能。
    public void addDate(Object obj){
        if (size>=arr.length){
            Object[] newArr = Arrays.copyOf(arr, size * 2);
            arr = newArr;
        }
        arr[size] = obj;
        size++;
    }
    //创建getDate方法,实现查询下标所对应的数据内容。
    public Object getData(int index){            
        if (index>=size){            //判断是否超过数组长度
            throw new ArrayIndexOutOfBoundsException("下标越界");//抛出异常
        }
        Object obj = arr[index];
        return obj;
    }
    //创建String方法,实现打印数组的功能(for循环遍历数组)
    public void String(){
        for (int i = 0 ;i<size;i++){
            System.out.println(arr[i]);
        }
    }
}

        接下来创建一个测试类检测功能是否实现:

package com.qy151.test1;

/**
 * @作者:刘壬杉
 * @创建时间 2022/4/14
 **/
public class Test1 {
    public static void main(String[] args) {
        MyArry arr = new MyArry(2);
        arr.addDate("aaa01");
        arr.addDate("aaa02");
        arr.addDate("aaa03");
        arr.addDate("aaa04");
        Object obj = arr.getData(3);
        System.out.println(obj);
        arr.String();
    }
}

 运行结果如下:

         那么,我们可以想到自定义一个动态扩容的数组类,Java的开发人员一样可以想到。

于是,java官网 基于数组 根据不同的数据结构,创建了多个类。这些类统称为——集合框架。

4. 集合的架构

5. List接口及其实现类

5.1 List集合的特点

  • List集合是有序集合: 数据的添加和存储次序一致
  • List集合可以存储重复的数据
  • List集合中的数据可以通过下标访问

 

 5.2 ArrayList实现类

5.2.1 特点

特点:

  • 实现了List接口
  • 可以动态扩容(我们只管存,长度不够,底层会自动的扩容)
  • 通过下标可以快速访问数据
  • 查找快,插入删除慢
  • ArrayList底层是数组,对数组做了封装
  • 可以存储任意类型的数据,包括null
  • 数据按照存储次序排列
  • 数据可以重复
  • 多线程访问时不安全

5.2.2 ArrayList的创建

List list1 = new ArrayList();  //默认情况下,ArrayList集合对象的长度默认为10

List list2 = new ArrayList(3);  //创建一个长度为3的ArrayList集合对象

 5.2.3 ArrayList添加操作

       add与addAll的区别:

一个案例讲明二者的区别。

首先创建一个变量名为list1的ArrayList对象。内容为[a,b,c]。

再创建一个list2对象,让其内容为[1,2,3]。

将list1的内容赋给list2:

        lsit2.add(list1):将list1作为一个对象整个放在list2的下标为0的位置。

                                此时list2的内容为:[1,2,3,[a,b,c]]

        list2.addAll(list1):将list1中的元素逐个添加到list2中。

                                此时list2的内容为:[1,2,3,a,b,c]

public class Test2 {
    public static void main(String[] args) {
        List list1 = new ArrayList(3); 
        //.add()  :  可以添加任意类型的数据。例如 String/new Date等等。。。
        list1.add("aaa01");
        list1.add("aaa02");
        list1.add("aaa03");
        list1.add(new Date());
        list1.add(1234);
        System.out.println(list1);   
        //打印结果为:[aaa01, aaa02, aaa03, Fri Apr 15 18:11:33 CST 2022, 1234]
        list1.add(2,"aaa01");
        System.out.println(list1);
        List list2 = new ArrayList();
        list2.add("a");
        list2.add("b");
        list1.addAll(list2);
        System.out.println(list1);
        list1.add(2,"aaa01");        //下标为2的位置添加数据“aaa01”
        System.out.println(list1);
        //打印结果为:[aaa01, aaa02, aaa01, aaa03, Fri Apr 15 18:11:33 CST 2022, 1234]
        List list2 = new ArrayList();
        list2.add("a");
        list2.add("b");
        list1.addAll(list2);    //添加多个元素,把list2中的每个元素一一添加到list1中
        System.out.println(list1);
        //[aaa01, aaa02, aaa01, aaa03, Fri Apr 15 18:11:33 CST 2022, 1234, a, b]
        list1.addAll(0,list2);    //下标为0的位置,将list2中的元素添加到list1中
        System.out.println(list1);
        //[a, b, aaa01, aaa02, aaa01, aaa03, Fri Apr 15 18:11:33 CST 2022, 1234, a, b]
    }
}

5.2.4 ArrayList删除操作

remove() :可以通过内容进行删除,也可以通过下标进行删除操作。

clear()     : 清空集合中的元素。

     public class Test2 {
    public static void main(String[] args) {
        List list1 = new ArrayList(3); 
        //.add()  :  可以添加任意类型的数据。例如 String/new Date等等。。。
        list1.add("aaa01");
        list1.add("aaa02");
        list1.add("aaa03");

        list1.remove("aaa01");     //删除第一个内容为“aaa01”的数据(之后的都不删除)
        System.out.println(list1);
        //[aaa02,aaa03]   
        list1.remove(0);          //删除下标为2的数据  
        //[aaa03]
        System.out.println(list1);
        //[aaa02,aaa03]
        list1.clear();            //清空集合中的元素
        //[]
        
    }
}   

5.2.5 ArrayList修改操作

set()   :实现将指定下标位置的内容修改的操作。

public class Test2 {
    public static void main(String[] args) {
        List list1 = new ArrayList(3); 
        //.add()  :  可以添加任意类型的数据。例如 String/new Date等等。。。
        list1.add("aaa01");
        list1.add("aaa02");
        list1.add("aaa03");

        list1.set(0,"java111");        //将下标为0的数据修改为“java111”,其他内容不变。
        System.out.println(list1);
        
    }
}

5.2.6 ArrayList查询操作

        .get( )    :根据下标获取元素

        .size( )  :获取集合元素的个数

        .contains( ):判断元素是否在集合中

        .indexOf( ) :查询元素在集合中第一次出现的位置
 

public class Test2 {
    public static void main(String[] args) {
        List list1 = new ArrayList(3);
        list1.add("aaa01");
        list1.add("aaa02");
        list1.add("aaa03");
        //查询的方法
        Object o = list1.get(1);    //根据下标获取元素
        int size = list1.size();    //获取集合元素的个数
        boolean f = list1.contains("aaa01");    //判断元素是否在集合中
        int index = list1.indexOf("aaa02");    //查询元素在集合中第一次出现的位置

        //for循环遍历集合中的元素
        for(int i = 0;i < list1.size();i++){
            Object m = list1.get(i);
            System.out.println(m);
        }
        //foreach遍历集合中的元素
        for (Object z : list1){
            System.out.println(z);
        }

    }
}

5.2.7 ArrayList 底层原理

我们需要了解一个类的底层原理,可以从构造函数入手:

List list1 = new ArrayList(3);在底层就是:
/**
 * 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) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

也就是说,我们的ArrayList的底层就是一个 Object[]数组,赋值给了this.elementData.

        当我们去调用list1.add("aaa01");时,此时底层就是往elementData添加数据。

    /** add的底层源码
     * 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;           //将数据添加到elemetData数组中
        return true;
    }

    /** size的底层源码
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
        获取当前list2存储了多少个数据:int size = list2.size();在底层其实就是返回成员变量size的值
    /**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }

        获取指定数据艘次出现的位置:int index = list1.indexOf("aaa01");底层就是获取“aaa01”在数组中首次出现的位置。

   /**
     * Returns the index of the first occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the lowest index <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
     * or -1 if there is no such index.
     */
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

        判断当前list是否包含某个数据:boolean contains = list1.contains("aaa01");底层是调用了当前类的indexOf 获取指定索引并判断是否>=0:

    /**
     * Returns <tt>true</tt> if this list contains the specified element.
     * More formally, returns <tt>true</tt> if and only if this list contains
     * at least one element <tt>e</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;e==null&nbsp;:&nbsp;o.equals(e))</tt>.
     *
     * @param o element whose presence in this list is to be tested
     * @return <tt>true</tt> if this list contains the specified element
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

        获取指定索引位置的数据:Object obj = list1.get(1);底层就是 获取数组中指定索引位置的数据

    /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /*rangeCheck底层源代码*/
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /*elementData底层源代码*/
    E elementData(int index) {
        return (E) elementData[index];
    }

5.3 LinkedList实现类

5.3.1 LinkedList的创建

        和ArrayList的添加基本相同。多了两个内容:

1).addFirst()  :在最前面的位置添加内容

2).addLast()  :在最后面的位置添加内容

public class Test3 {
    public static void main(String[] args) {
        LinkedList list1 = new LinkedList();
        list1.add("张三");
        list1.add("李四");
        list1.add("王五");
        //[张三, 李四, 王五]
        System.out.println(list1);
        list1.addFirst("张二");
        list1.addLast("赵六");
        //[张二, 张三, 李四, 王五, 赵六]
        System.out.println(list1);
        //[张二, 张三, 鸣人, 李四, 王五, 赵六]
        list1.add(2,"鸣人");
        System.out.println(list1);
    }
}

5.3.2 LinkedList 修改和删除操作

        与ArrayList的修改和删除操作基本相同。删除多了两个内容:

1) .removeFirst()   :删除最前面的数据。

2).removeLast()  :删除最后面的数据。

public class Test3 {
    public static void main(String[] args) {
        LinkedList list1 = new LinkedList();
        list1.add("张三");
        list1.add("李四");
        list1.add("王五");
        //[张三, 李四, 王五]
        System.out.println(list1);
        list1.addFirst("张二");
        list1.addLast("赵六");
        //[张二, 张三, 李四, 王五, 赵六]
        System.out.println(list1);
        //[张二, 张三, 鸣人, 李四, 王五, 赵六]
        list1.add(2,"鸣人");
        System.out.println(list1);

        //修改
        list1.set(2,"佐助");
        //[张二, 张三, 佐助, 李四, 王五, 赵六]
        System.out.println(list1);

        //删除
        list1.remove(0);
        //[张三, 佐助, 李四, 王五, 赵六]
        System.out.println(list1);
        list1.remove("张三");
        //[佐助, 李四, 王五, 赵六]
        System.out.println(list1);
        list1.removeFirst();
        //[李四, 王五, 赵六]
        System.out.println(list1);
        list1.removeLast();
        //[李四, 王五]
        System.out.println(list1);
    }
}

5.3.3 LinkedList 查询操作

        与ArrayList的查询关键字大致相同,多了一些内容:

1).getFirst() :获取第一个下标所对应的数据

2)  .getLast() :获取最后一个下标所对应的数据

3) .toArray()  :将对象转换为数组

public class Test4 {
    public static void main(String[] args) {
        LinkedList list1 = new LinkedList();
        list1.add("张三");
        list1.add("李四");
        list1.add("王五");
        //[张三, 李四, 王五]
        System.out.println(list1);
        Object first = list1.getFirst();
        System.out.println(first);      //张三
        Object last = list1.getLast();
        System.out.println(last);       //王五
        Object obj = list1.get(2);
        System.out.println(obj);        //王五
        int size = list1.size();
        System.out.println(size);       //3
        boolean empty = list1.isEmpty();
        System.out.println(empty);      //false
        int m = list1.indexOf("张三");
        System.out.println(m);          //0
        boolean contains = list1.contains("王五");
        System.out.println(contains);   //true
        Object[] objects = list1.toArray();
        System.out.println(Arrays.toString(objects));   //[张三, 李四, 王五]
        
        //for循环遍历对象
        for (int i = 0;i<size;i++){
            Object o = list1.get(i);
            System.out.println(o);
        }

    }
}

5.3.4  LinkedList的底层源码

        老规矩,学习底层原理,我们就要从学习构造函数开始:

   /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

        我们可以发现LinkedList 底层什么也没有,但是注意,此时有三个成员变量被初始化:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

        由此可以看出,在new一个LinkedList对象时,会随之产生三个成员变量,即:int size, Node first和 Node last。并且没有赋值,所以他们的初始值为size=0,first=null,last=null。

接下来用一张图让大家明白大致的实现方式:

当我们调用  linkedList.add("张三");  在底层其实调用了LinkedLast(e);

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

此时LinkedLast(e);操作是这个样子的:


    /**
     * Links e as last element.
     */
    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++;
    }
       //此时当前类有一个私有静态内部类 是Node
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

这段代码看起来比较难以理解,接下来我们还是用画图的方法给大家刨析以下他的实现原理:

linkedList.add("张三");

        首先代码第一行,让last赋值给l,l就是我们的prev;然后第二行创建一个newNode对象,其中e就是我们的 item = 张三;next = null; prev = l = last = null;然后第三行执行if判断语句。当我们向对象中写入第一个数据时,l = null,所以执行if里面的代码块。此时,我们让first  = 创建的Node。最后让size++,modCount++(modeCount的意义我们以后再讲)。

        也就是说,此时LinkedList中,我们的first和last都指向了我们对象中唯一的数据。这就很符合我们的链表结构。

         如果我们再添加一个对象,linkedList.add("李四"); 那么又会变成什么样子呢?聪明的小伙伴不妨先自己试着去模拟的画一下,看看是否和我下面的这张图是一样的:

         当我们再添加一个“李四”对象的时候,首先,我们的l=last=“张三”对象,然后我们创建了一个新的Node,其中:prev = l = "张三"对象, item = e = "李四",next = null。然后执行if判断语句,此时很关键,我们的l是不是等于null呢?并不是,此时的l等于的是"张三"这个对象,所以我们要执行else里面的代码块。让我们的l.next = 我们新创建的"李四"对象。注意区分"l",此时的l指的是我们第一行执行的Node l(见本段标记部分)。最后不要忘了size++,modeCount++。

        如果小伙伴们还没有看明白的话,我们再添加一个对象帮助大家理解。原理和添加"李四"对象相同。见下图:

        在了解了创建和添加的底层原理后,我们再来看一看LinkedList的查询操作原理:

   注:(size>>1)移位运算。可以理解为:size/2.

    public E get(int index) {
        checkElementIndex(index);     //检查索引编号是否越界
        return node(index).item;      //获取指定索引的node对象,并且将其item存储的数据进行返回
    }
    
    //此时获取node
    
    Node<E> node(int index) {
        // assert isElementIndex(index);
        //判断的目的是: 索引在size的前半部分还是后半部分,提高效率。
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    假如我们 linkedlist.get(1);在调用get方法时,先让我们的x = first = "张三"对象,然后我们判断inedx(此时是我们填入的1)是否大于size/2?(此时让我们的size=3,linkedlist对象为[张三,李四,王五])很明显,比index<size/2,所以索引在size的前半部分,从前往后找。接下来是for循环。第一次循环时,i=0,x = x.next = "李四对象",第二次循环,i=1,i<index不成立,循环结束,输出x,即输出的内容即为李四对象,也就是我们要的结果。

 5.4 ArrayList 与LinkedList的区别

        在我们对ArrayList和LinkedList有了大致的了解以后,我们就可以大致归纳以下二者区别以及优缺点:

ArrayList:他的底层是数组结构,它的查询效率高,但是他的添加和删除效率低,因为它涉及到数据的迁移。常见方法有:add();size();indexOf();contains();get();remove();isEmpty();

LinkedList:他得底层是双向链表结构,他的增加和删除效率高,但是他的查询效率低。因为他要一个节点一个节点的往后找。常见的方法有:addFirst();addLast();getFirst();getLast();

6.Set集合及其实现类

6.1Set集合的特点

  • Set接口是无序的
  • Set接口中的数据不允许重复
  • Set接口无法通过下标访问数据
  • 查找慢,插入删除快(底层数据结构是哈希表和红黑树)
  • Set集合使用equals()和hashCode()方法实现元素去重

 6.2HashSet实现类

6.2.1特点

  • HashSet是Set接口的实现类
  • 线程不安全

6.2.2 创建HashSet对象

        有了之前我们学习list集合的经验,Set集合这里的操作大致相同,我们就不做过多的累述。

public class Test02 {
    public static void main(String[] args) {
        HashSet  hashSet= new HashSet();     //默认长度为16,负载因子为0.75

        HashSet  hashSet1 = new HashSet(16);//初始容器的大小

        //loadFactor:--->0.7f 表示负载因子 当空间使用70%时 要求扩容
        HashSet hashSet2 = new HashSet(16,0.7f);
    }
}

怎么理解负载因子?负载因子就好比时我们饮水机的水桶,当你4个水桶用掉了75%也就是剩1桶水(即负载因子0.75)时,就会给送水工打电话。给送水工打电话这个行为就好比HashSet对象进行了扩容。

6.2.3 HashSet添加操作

     //添加操作
        hashSet.add("java01");
        hashSet.add("java02");
        hashSet.add("java04");
        hashSet.add("java03");
        hashSet.add("java02");

        HashSet set2=new HashSet();
        set2.add("刘德华");
        set2.add("张学友");
        set2.add("黎明");
        
        hashSet.addAll(set2); //把set2中的每个元素添加到hashset中
        System.out.println(hashSet); //元素不能重复 而且无序

6.2.4 HashSet删除操作

        //删除
        hashSet.remove("黎明");
        //清空容器集合
        hashSet.clear();

6.2.5 HashSet其他常用操作

        因为Set集合时无序的,所以我们没有办法用get()方法做修改操作。

          //判断是否为空
          boolean empty = hashSet.isEmpty(); 
         //判断元素是否在容器中
          boolean b = hashSet.contains("刘德华");

6.2.6 HashSet的遍历

(1)通过foreach遍历

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

(2)通过迭代器来遍历

 //迭代器遍历
        Iterator iterator = hashSet.iterator();//获取迭代器对象 有序:有下标
        while (iterator.hasNext()){//判断是否指针能够移动
            Object next = iterator.next();//指针移动并获取当前的元素
            System.out.println(next);
        }

 这里我们引出了一个迭代器Iterator的概念,我们单独建一个标题来讲述。

6.2.7迭代器Iterator

        迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,可在容器对象(container,例如链表数组)上遍访的接口,设计人员无需关心容器对象的内存分配的实现细节。

        HashSet类中没有提供根据集合索引获取索引对应的值的⽅法,因此遍历HashSet时需要使⽤Iterator迭代器。Iterator的主要⽅法如下:

         当我们用迭代器进行HashSet遍历时,首先获取迭代器对象,然后通过hasNext()方法判断指针是否能够移动,如果可以移动,就通过next()方法让指针移动并获取当前的元素。不能移动的话,就输出Object next。

 6.2.8 HashSet底层源码

 在创建一个HashSet的对象时,底层创建的是HashMap。我们说hashset的底层原理时,我们就在后HashMap的原理就行。 讲HashMap时给大家说原理。

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

6.3TreeSet实现类

        TreeSet 基于TreeMap 实现。TreeSet可以实现有序集合,但是有序性需要通过比较器实现。

6.3.1 TreeSet集合特点

  • 有序
  • 不重复
  • 添加、删除、判断元素存在性效率比较高
  • 线程不安全

 6.3.2TreeSet对元素排序方式

1) 如果是基本数据类型和String类型,无需其它操作,可以直接进行排序。

2) 对象类型元素排序,需要实现Comparable接口,并覆盖其compareTo方法。

3) 自己定义实现了Comparator接口的排序类,并将其传给TreeSet,实现自定义的排序规则。

例子1:存储String类型的数据:

TreeSet treeSet=new TreeSet();
        treeSet.add("java05");
        treeSet.add("java03");
        treeSet.add("java04");
        treeSet.add("java01");
        treeSet.add("java02");
        treeSet.add("java04");

        System.out.println(treeSet);

例子2:存储对象类型的数据:

public class Test04 {
    public static void main(String[] args) {
        TreeSet treeSet=new TreeSet();
        treeSet.add(new Student("张三",17));
        treeSet.add(new Student("李四",16));
        treeSet.add(new Student("王五",16));
        treeSet.add(new Student("老六",15));

        System.out.println(treeSet);
    }
}

此时运行,我们就会发现,运行出现了如下的错误:

 这就验证了TreeSet中的元素必须实现Comparable接口 方可放入TreeSet 。

那么,我们怎么才能把对象类型的数据放入TreeSet中呢?实现方式有以下两种:

1)让你的类实现Comparable接口

public class Test {
    public static void main(String[] args) {
        TreeSet treeSet=new TreeSet(); //TreeSet不允许重复元素
        treeSet.add(new Student("张三",17));
        treeSet.add(new Student("李四",16));
        treeSet.add(new Student("王五",16));
        treeSet.add(new Student("老六",15));

        System.out.println(treeSet);
    }
}
class Student implements Comparable{
     private String name;
     private Integer age;

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

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

    public Student() {
    }

    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;
    }
   //排序:---返回如果大于0 表示当前元素比o大  如果返回-1 当前添加的元素比o小  返回0表示相同元素。
    @Override
    public int compareTo(Object o) {
        Student student= (Student) o;
        System.out.println(this+"===================>"+o);

        if(this.age>student.age){
            return 1;
        }
        if(this.age<student.age){
            return -1;
        }

        return 0;
    }
}

2)在创建TreeSet时指定排序的对象

        我们之前 创建过TreeSet 对象。 TreeSet treeSet=new TreeSet(); 但是在创建对象时 并没有为 其指定排序得规则,那么就要求该集合得元素有排序规则。 如果元素得类 已经创建完成,不能修改该类得源码,这时我们又想把该类得对象放入得 TreeSet 容器中。 这时就需要你在创 TreeSet 时指定排序得规则。
 public class MyComparator implements Comparator {

    //需要比对得两个对象
     @Override
     public int compare(Object o1, Object o2) {
     Student s1= (Student) o1;
     Student s2= (Student) o2;
     if(s1.getAge()>s2.getAge()){
         return 1;
     }else if(s1.getAge()<s2.getAge()){
         return -1;
     }else {
         return 0;
     }
   }
 }

 public class Demo01 {
 public static void main(String[] args) {
     //Comparator<? super E> comparator
     //为TreeSet容器指定了排序规则
     TreeSet treeSet=new TreeSet(new MyComparator());
     treeSet.add(new Student(18,"张三"));
     treeSet.add(new Student(17,"李四"));
     treeSet.add(new Student(19,"王五"));
     treeSet.add(new Student(19,"赵六"));
     System.out.println(treeSet);
     }
 }

7. Map集合及其实现类

        map中得每个元素属于键值对模式。 如果往 map 中添加元素时 需要添加 key
value. 它也属于一个接口,该接口常见得实现类有 : HashMap.

7.1 如何创建HashMap对象

//默认初始化大小为16 负载因子为0.75 
Map map=new HashMap(); 
//初始化大小
Map map2=new HashMap(16);
//初始化大小 负载因子
Map map3=new HashMap(16,0.78f);

 7.2 添加操作

//默认初始化大小为16 负载因子为0.75
 Map map=new HashMap();
//添加操作 key: name value: 张三
 map.put("name","张三"); 
//注意: 要求map得key必 须唯一。 
map.put("age",18); map.put("name","王五"); 
//因为key不能重复,所以后 者会把前者覆盖
Map m1=new HashMap(); m1.put("k1","v1"); 
m1.put("k2","v2"); map.putAll(m1); 
//把m1中得每个元素 添加到map中 
map.putIfAbsent("age",28) ;
//如果指定得key存在, 则不放入map中,如果不存在则放入map中 
System.out.println(map);

7.3 删除操作

//删除操作 
map.remove("age2");
//根据指定得key移除元素 
System.out.println(map); 
map.clear(); 
//清空map容器 
System.out.println(map);

7.4 修改操作

//修改操作 
map.replace("name","刘德华");
//替换元素 
System.out.println(map);

7.5 查询操作

public static void main(String[] args) { 
    Map map=new HashMap(); 
    map.put("k1","v1");
    map.put("k4","v4"); 
    map.put("k2","v2"); 
    map.put("k3","v3"); 
    //查询操作 
    boolean f = map.containsKey("k5");
    //判断map是否 存在指定得key 
    Object v = map.get("k5"); 
    //根据指定的key获取对应 得value值
    System.out.println(v); 
    Set keys = map.keySet();
    //返回该map中所有得key 
    System.out.println(keys); 
    //遍历
    map. for(Object k:keys){ 
        Object value= map.get(k);
        System.out.println(k+"================>"+value);
    }
}

7.6 HashMap的底层原理

        JDK1.7 和 JDK1.8他们是有区别得。
   JDK1.7使用得数据结构: 数组+链表  而且链表插入模式为头部插入(造成死循环)。
   jdk1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。

首先我们从构造函数入手:

  /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

当我们new一个HashMap时,在底层是以数组的形式表现出来的。默认长度为16.

接下来我们讲解一下调用put方法时,底层的原理:

Map map = new HashMap();

map.put("k1","v1");当我们put("k1","v1")时,底层会根据key的hash值计算出一个整数。通过下面的代码我们可以验证这个事情:

public class Test {
    public static void main(String[] args){
        String s = "k1";
        System.out.println(s.hasCode());
    }
}

//最终的输出结果为3366

根据hash整数求出在数组中的位置。k1.hash(3366)%16 即可得到0~15之间的一个下标。根据所得的下标放入对应的数组中。数组中实际存储的时地址,地址指向的是Node对象。

 

 我们再put一组key-value值,让key为“aa”,value为"bb",假设此时key的hash值计算结果也是6,那么在底层是怎么存放数据的呢?我们看下面的图:

 由上图我们就可以明白,如果key.hash%16计算后得到的结果相同,这时我们便称之为hash碰撞(冲突),此时我们变需要比对他们的equals方法,如果equals不同,则将后来的数据挂在单向链表上(尾插法)。

        但是现在又会出现一个问题,如果我们的hash冲突个数比较多,此时只使用链表结构,则会出现插入和查询效率低的现象。这是我们就会用到底层原理的最后一个结构:红黑二叉树。

        注意:当hash冲突个数超过8个时才会使用红黑二叉树。为什么不能直接用红黑树呢?因为创建一个红黑树非常耗费资源,但是他的效率很高。所以数据少的时候我们不用红黑二叉树。

        为什么使用了红黑二叉树以后查询和修改的效率就提高了呢?

        我们现在粗线的讲解一下红黑二叉树的原理。假设我们要查询 7,如果不用红黑二叉树,我们需要从头到尾一个接一个的查询,最终要查询7次。使用红黑二叉树,我们首先将索引和1比较,比1大,向右边查找,再和3比较,比3大,向右查找,最后查找到了7。所以用红黑二叉树,我们一共就查询了3次,这样大大提高了我们的查询和插入的效率。

        最后我们在总结一波:

JDK1.8 HashMap原理
Hashmap得原理,存储元素使用得put(key,value),根据key得hash计算出相应得哈希值,根据相应得算法求出该元素在数组中得位置, 如果求出得哈希值相同,则称为哈希冲突,会根据equals来判断元素是否一致,如果equals不同,则存入单向链表上, 如果哈希碰撞得个数超过8个,则把链表转换为红黑二叉树。


JDK1.7和JDK1.8 HashMap得区别。
   JDK1.7使用得数据结构: 数组+链表  而且链表插入模式为头部插入(造成死循环)。
   jdk1.8使用得数据结构: 数组+链表+红黑树 而且链表得插入模式为尾部插入。

我们来看一下put方法的底层代码

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))))
                 //如果key得hash值相同,判断key得equals是否相同,替换原来得元素
                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);
                        // 判断链表得长度是否超过8个
                        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;
    }

8.  泛型

8.1 什么是泛型

        泛型就是限制我们得数据类型。 那我们使用泛型有什么意义呢?
        我们原来在定义集合时,是如下得定义方式:
List list=new ArrayList();//该集合没有使用泛型
list.add("java01");
list.add("java02");
String str= (String) list.get(0);//获取元素 需要
进行强制类型转换
System.out.println(str); 
获取元素时,不方便对元素进行相应得其他操作。 需要对其强转以后才能使用相应的方法,所以这时就需要用到泛型。

8.2 泛型的使用

语法:      

         List<类型> list=new ArrayList<类型>();

public static void main(String[] args) {
List<String> list=new ArrayList<>();//这里就限制了集合中每个元素得类型。
list.add("aaa");
list.add("bbb"); //因为集合中只能添加String类型
list.add("ccc"); //因为集合中只能添加String类型
String s = list.get(0); //在获取元素时 默认就是相应得数据类型 而无需在进行转换



//<K,V>:K:表示键得泛型 V:表示值得泛型
HashMap<String,Integer> map=new HashMap<>();
map.put("a",1);
map.put("b",2);
Set<String> keys = map.keySet();
} 

8.3 自定义泛型

        基本的语法格式如下:

public class 类名<标识,标识....> {
标识 变量名;
public 标识 方法名(){
}
public void 方法名(标识 参数名){
}
.........
} 

下面用一个实例来讲明:

        我们自定义一个坐标类,可以实现输入并打印x坐标,y坐标:

public class Test1 {
    public static void main(String[] args) {
       Point<String> p = new Point<>();
       p.setX("东京15°");
       p.setY("北纬11°");
       p.show();
    }
}

class Point<T>{
    private T x;
    private T y;

    public void show(){
        System.out.println("x的坐标:"+x+" y的坐标:"+y);
    }

    public T getX() {
        return x;
    }

    public void setX(T x) {
        this.x = x;
    }

    public T getY() {
        return y;
    }

    public void setY(T y) {
        this.y = y;
    }
}


版权声明:本文为lrs998563原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。