JavaSE---集合(三万字啰嗦集合基础及底层源码)

Java集合框架详解:ArrayList、LinkedList、HashSet、HashMap与泛型

目录

一、集合的框架体系

 1、数组和集合

2、集合 

二、Collection接口和常用方法

2.1、Collection 接口的实现类

2.2、Collection 接口常用方法

2.3、Collection 接口遍历元素

2.3.1、使用 Iterator(迭代器)

2.3.2、使用for 循环增强遍历集合

2.3.3、Lambda表达式遍历集合 

三、List接口

3.1、List 接口

 3.2、List接口方法

3.3、ArrayListt 底层结构和源码分析 

3.4、LinkedList 底层结构 

3.4.0、源码中的几个好想法

3.4.1 、LinkedList新增元素的底层分析add

3.4.2 、LinkedList删除元素的底层分析 remove

3.5、ArrayList 和 LinkedList 比较 

四、Set接口

4.1、Set 接口基本介绍

4.2、Set 接口的常用方法 

4.3、Set 接口的遍历方式 

4.4、Set 接口实现类-HashSet

4.4.1、HashSet 的全面说明

4.4.2、HashSet 底层机制说明

4.5、Set 接口实现类-LinkedHashSet

4.6、TreeSet 

五、泛型

5.1、泛型的理解和好处

5.2、泛型的作用

5.2.1、泛型类

 5.2.2、泛型方法

5.3 、泛型使用的注意事项和细节

 5.4、自定义泛型类

5.5、自定义泛型接口 

5.6、自定义泛型方法 

5.7、泛型通配符 

六、Map接口

6.1、Map 接口和常用方法

6.1.1、Map 接口实现类的特点

6.1.2、map 接口常用方法 

6.2、Map遍历方式

6.2.1、通过map.keySet()

6.2.2、通过map.values()

6.2.3、通过map.entrySet()

 6.3、Map 接口实现类-HashMap

6.3.1、HashMap 底层机制及源码剖析

 6.4、Map 接口实现类-Hashtable

6.4.1、HashTable 的基本介绍

6.5、HashMap和HashTable对比 

6.6、Map 接口实现类-Properties 

七、集合实现类的总结

1、Collection接口---List接口

 2、Collection接口---Set接口

八、Collections工具类

8.1、Collections 工具类介绍

8.2、排序操作 

8.3、其他方法

九、Redis---非关系型数据库中的集合使用

十、各大容器中的集合


一、集合的框架体系

 1、数组和集合

  1. 数组是java语言内置的数据类型,他是一个线性的序列,所以可以快速访问其他的元素。        
    1. 长度开始时必须指定,而且一旦指定不能更改。数组对象的堆内存地址是连续线性的表结构。
    2. 保存的必须为统一类型的元素
    3. 使用数组进行增删元素比较麻烦。
      1. 增:创建新的容量更大的数组,拷贝,添加新的对象
  2. 数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。
  3.  ArrayList就是基于数组创建的容器类。
  4. 若程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,此时数组不适用。
  5. 联系:使用相应的集合中的toArray()方法和数组的Arrays.asList(数组)方法可以实现互相转换。

2、集合 

1、Collection 接口有两个重要的子接口 List、Set , 他们的实现子类都是 单列集合。
2、 Map 接口的实现子类 是 双列集合 ,存放的 K-V
3、 集合不能直接存储基本数据类型,也不能直接存储Java对象,集合中存放的是Java对象的内存地址(引用)

二、Collection接口和常用方法

2.1、Collection 接口的实现类

1、Collection实现子类可以存放多个Object对象元素

2、有些Collect的实现子类可以存放重复元素(List实现类),有些不可以(Set实现类)

3、有些Collect接口时有序的(List实现类,TreeSet),有些是无序的(HashSet--根据Hash值计算的位置)

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

2.2、Collection 接口常用方法

1、add(E  e):添加单个元素,返回boolean,添加是否成功。 list.add(10);自动装箱

        add(int index, E element) 指定位置添加指定元素。

2、addAllCollection<? extends E> c):将指定集合中的所有元素添加到集合中,返回boolean,添加是否成功。

3、remove(Object o)删除单个元素,返回boolean,删除是否成功。

        remove(int index)删除索引位置的元素

        注意:基本数据类型是通过自动装箱,以包装类对象存储。

        remove(0)删除第一个元素。除元素0---remove(Integer.valueOf(0))

4、removeAllCollection<? extends E> c):删除指定集合中的所有元素,返回boolean,删除是否成功。

5、contains(Object o):查找元素是否存在。System.out.println(list.contains("jack"));

6、containsAllCollection<? extends E> c):是否包含指定集合的所有元素

7、isEmpty:判断是否为空。System.out.println(list.isEmpty());

8、clear:清空集合中所有元素。list.clear();

9、iterator()返回该集合元素的迭代器  Iterater iterater = list.iterater()

                iterator().hasNext()指向下一个元素,如果下一个元素没了,返回false

                iterator().next()返回下一个集合元素对象。

while (iterator.hasNext()){
      System.out.println(iterator.next());
}

10、size() 返回集合中的元素数。int count = list.size()

11、toArray() 将集合中元素 转为数组储存  Object[]  object = list.toArray() 

2.3、Collection 接口遍历元素

2.3.1、使用 Iterator(迭代器)

1、Iterator(迭代器):Iterator对象称为迭代器,主要是用于遍历Collection集合中的元素。

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

3、Iterator仅用于集合的遍历,本身并不存放任何对象

 

在调用iterator.next()方法前必须先调用iterator.hasNext()方法进行检测。 

当退出 while 循环后 , 这时 iterator 迭代器,指向最后的元素

快速生成 while的快捷键 => itit

使用  ctrl + j  显示所有的快捷键的的快捷键

2.3.2、使用for 循环增强遍历集合

1、增强for循环,可以替代iteroator迭代器。本质上一样,只能用于遍历集合或数组

2、基本语法                

       

for (元素类型 元素名 :集合名或数组名) {
      访问元素
}


for (Object object : list) {
      System.out.println(object);
}

2.3.3、Lambda表达式遍历集合 

list.forEach(object ->{
    System.out.println(object);
});


map.forEach((k,v) -> {
    System.out.println(k);
    System.out.println(v);
})

三、List接口

3.1、List 接口

List 接口下面主要有两个实现 ArrayList LinkedList ,他们都是有顺序的,也就是放进去 是什么顺序,取出来还是什么顺序,也就是基于线性存储,可以看作是一个可变数组。
        1、元素有序,添加去除顺序一致,且可重复。
        2、每个元素对应有顺序索引,和数组一样,线性表结构
        3、List集合中元素都对应一个整数型的序号加载其在容器中的位置,可以根据序号存取容器中的元素。

 3.2、List接口方法

1、void add(int index, Object ele):index 位置插入 ele 元素

        //index = 1 的位置插入一个对象:list.add(1, "xiaoming");

2、Object  get(int index):获取指定 index 位置的元素

3、int indexOf(Object obj):返回 obj 在集合中首次出现的位置

4、int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置

5、Object remove(int index):移除指定 index 位置的元素,并返回此元素

     Object remove(Object ele)删除指定元素

        也是通过equals比较后查出来删除的

6、Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换。

7、List subList(int fromIndex, int toIndex):返回从 fromIndex toIndex 位置的子集合,左开右闭。

注意:其余包含在Collection接口中的方法

8、boolean  contains(Object obj):判断是否包含。底层调用的是equals方法

 public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

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

因此,对于重写equals方法的String来说,尤为注意:

举例:

List<Car> list = new ArrayList<>();

String s = new String("jack");
list.add(s);
String x = new String("jack");

//虽然没有把x放到list集合中,但是有相同内容的s在list集合中,s.equals(x)是true
//因此包含,返回true
System.out.println(list.contains(x));//true

注意:要添加到List集合中对象,需要重写equals方法(保证内容相等,集合就包含)

           要添加到Set集合中对象,需要重写equals方法和hashCode方法(保证内容等,哈市等,元素相同,不能再添加)

3.3、ArrayListt 底层结构和源码分析 

ArrayList注意事项:

        1、可以加入多个null元素

        2、ArrayList是由数组来实现数据存储的。

        3、ArrayList基本上等同于Vectot,除了ArrayList是线程不安全的

源码分析:

        属性:transient Object[]  elementData----是个数组,transient 表示短暂的,该属性不会被序列化。

                   size ---元素多少

        1)执行new ArrayList();无参构造器会给elementData赋默认值,初始容量为0。   

public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

            static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

        

        2)、初始化数组大小 

         private static final int DEFAULT_CAPACITY = 10;

        3)、当添加元素第一次大于10时,会执行grow()方法扩容1.5倍

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //(oldCapacity >> 1)向右移位,相当于变为一半
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
}

         之后每一次执行add()操作时都会先确定当前数组容量,注意第一次容量为10(无参构造),即elementData.length = 10,list集合中元素个数是size,每次用size+1和elementData.length相比,即元素个数+1使用局部变量minCapacity表示当前list集合元素个数多一个的个数,即minCapacity=size+1。中要是比现在数组长度大(size+1 > elementData.length)则执行grow()方法扩容1.5倍

        然后再去执行添加元素的操作,即elementData[size++] = e;注意size表示目前集合中元素个数。

 1、ArrayList底层维护了一个Object[]类型的数组,当使用无参构造器创建ArrayList对象时,则创建一个空的数组elementData并初始化elementData容量为10。

   2、第一次添加元素,则扩容elementData为10,之后再需要扩容,则在此基础上容量扩为1.5倍,自动扩容。

3、如果使用指定大小的构造器,则初始化elementData容量为指定大小,如果需要扩容,则直接按照elementData的1.5倍扩容。每次add()同样要确定元素个数+1(size+1)和elementData.length的大小,元素个数+1 > 数组当前容量elementData.length,则需要扩容1.5倍。即elementData.length*1.5。

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底层结构是一个可变的动态扩容的数组

注意:modCount记录了集合被修改的次数。

           ArrayList底层使用的是数组,动态扩容----使用Arrays.copyOf()

           copyOf(T[] original, int newLength)   ----两个参数:要拷贝的数组,赋予新的数组长度,返回一个新的数组。如果拷贝长度 < 0 就抛出异常NegativeArraySizeException

        如果拷贝的长度 > arrs.length 就在新数组的后面 增加 null

        该方法的底层使用的是 System.arraycopy() 。

 //原数组  长度为5
int[] arr = {1,2,3,4,5};
int[] ints = Arrays.copyOf(arr, 10);
//[1, 2, 3, 4, 5, 0, 0, 0, 0, 0] 新数组的长度为:10 
System.out.println(Arrays.toString(ints));

3.4、LinkedList 底层结构 

1、LinkedList的曾实现了双向链表和双端队列特点。

2、可以添加任意元素,也可以重复,包括添加多个null

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

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

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

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

4、因此,LinkedList的元素的添加和删除,不是通过数组来完成的,相对效率较高。

3.4.0、源码中的几个好想法

1、创建node对象---newNode

final Node<E> newNode = new Node<>(l, e, null);

2、由给定索引值index,返回节点对象

        判断index在左还是右(index和size/2比大小),在左就从first开始循环找, x = x.next;在右就从last开始循环找x = x.prev;

//该方法传入一个索引值,返回该索引下的节点
Node<E> node(int index) {
        // assert isElementIndex(index);
        /*
            如果要插入的索引位置在集合的左边:size>>1即集合中元素数量的一半
            从第一个节点first开始找到索引为index的那个节点
        */
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        /*
            如果要插入的索引位置在集合的右边:
            从最后一个节点last开始找到索引为index的那个节点
        */
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

3、由LinkedList集合中元素获得节点对象

        for在循环遍历所有节点,判断每个节点属性target.eques(x.item)

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

3.4.1 、LinkedList新增元素的底层分析add

1、注意:双向链表有多个Node对象的节点实现,第一个节点对象为first,最后一个节点对象为last。每个Node节点对象中又有三个属性:Node Pre,Node next ,<E> item(存放的对象元素)。

2、使用无参构造器新增元素的底层实现:

        1)、使用add()无参方法增加元素

                第一步:使用add()无参方法增加第一个元素对象时<E> e 时,直接把t元素对象放在最后节点里即last = e;执行linkLast(t)方法:

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

       第二步:其中,创建新的节点对象,并赋值给last。final Node<E> newNode = new Node<>(l, e, null);  构造器形参的 传递的实参l 表示该节点的前一个节点即ewNod的pre属性,因为新加元素放到最后一个节点,因此next形参位置为null。就可以把元素放在newNode对象中的item属性上第一次添加,l(last)为null。因此,把新创建的节点对象newNode也赋值给first,即first和last都是第一次新增对象。之后size++元素个数由0变为1,modCount++集合被修改次数加1。

        第三步:随后使用add()无参方法增加其他元素时,先把last节点放到l上(临时变量l中放的是没增元素之前的last,方便增加元素后让其next属性指向新增对象,使其由last节点变为倒数第二个,next指向新的last)。再创建新的节点newNode对象,把增加的元素放在newNode对象的iteam中,然后把newNode对象变为新的最后一个节点last,并把上一次最后一个节点的l.next = newNode;

         2)、使用add(int index , E element)有参方法增加元素,先判断index的有效性。

                指定在index索引处增加元素

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
}

          主要看看函数linkBefore(element , node( index ) );

          其中node( index )返回的是索引是index的一个节点

          双向链表可以通过这种方式,根据给的索引值找到对应的节点:

//该方法传入一个索引值,返回该索引下的节点
Node<E> node(int index) {
        // assert isElementIndex(index);
        /*
            如果要插入的索引位置在集合的左边:size>>1即集合中元素数量的一半
            从第一个节点first开始找到索引为index的那个节点
        */
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        /*
            如果要插入的索引位置在集合的右边:
            从最后一个节点last开始找到索引为index的那个节点
        */
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
//e就是我们要增加的元素,succ是该集合索引为index的节点Node
void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        /*
            pred为index索引节点的前一个节点,新建一个节点对象,item存入我们的元素,前一个节点                
            pred,后一个节点就是集合索引为index的节点Node

            当然也可以先找到index索引下的节点的红藕一个节点final Node<E> next_ = succ.next; 
            final Node<E> newNode = new Node<>(succ, e, next_);
            succ.next = newNode;
            next_.prev = newNode;
            size++;
            modCount++;
        */
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
}

        3)、使用addAll(Collection<? extends E> c)将一个子集合中元素全都增加进去:

                  执行的是在指定索引下增加子集合的方法。批量增加,先把子集合元素通过Collection中的toArray()方法转为更好操作的数组。计算除要增加的子集合元素个数numNew,size+=numNew。从最后一个元素后添加。

                

public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }

 public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
        //集合转为数组
        Object[] a = c.toArray();
        //计算增加子集合元素数量,为了计算增加后的集合总个数size+=numNew
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        //从最后一个节点后增加,即默认不给index形参的哪个函数
        if (index == size) {
            succ = null;
            pred = last;
        } else {
        //succ = node(index);返回给定索引index处的节点对象
            succ = node(index);
            pred = succ.prev;
        }
        //循环增加子集合中元素

        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            //所有元素添加完即循环结束,再设置next参数,指向下一个节点
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            //每次循环最后把新的节点赋值给前一个节点
            pred = newNode;
        }
        //此时是index = size的情况,找不到对应节点对象为null,即从最后添加

        if (succ == null) {
            //循环结束pred就是子集合最后一个元素,因为succweinull,该子集合是从最后增加的。
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

3、使用有参构造器,新增元素的底层实现:

        执行无参构造,再执行addAll()方法一步到位

public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
}

3.4.2 、LinkedList删除元素的底层分析 remove

删除某个节点:

        1、先找到该节点x(由所给index找----for循环,在左边由first节点开始找,在右边由last节点开始找)

        2、由该节点得到该节点的属性prev、item(返回元素)、next。让prev.next == next且x.prev = null;和next.prev == prev且x.next = null;还要判断prev是不是等于null----prev是空,first = next即可。还要判断next是不是等于null----next是空,last = prevt即可.

        3、size--;

        4、modCount++;

List接口中有两个remove相关方法remove(index)和remove(Object o)

List ll = new LinkedList();ll.remove();LinkedList实现Deque接口特有的方法。不行,List接口中remove()方法都要有实参。

LinkedList ll = new LinkedList();中有特有remove()默认删除第一个节点元素。

1、先用默认这个linkedList.remove(); // 这里默认删除的是第一个结点,很好理解

        first == null;抛出异常,不为nulll,把first的item(要删除的元素内容),元素作为返回值返回和next节点属性取出重新赋值给first    first= first.next(next为null,则原集合只有一个元素,此时让last = null即可删除完)若next不为空,next.prev = null。即将这个节点变为了first,最后size--,modCount++;

        

public E remove() {
        return removeFirst();
    }

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

2、List中的remove(index)删除指定索引的元素---返回删除的元素(节点的item),调用的都是unlunk(Node<E> x)删除的x节点。

        

public E remove(int index) {
        checkElementIndex(index);
    //node(index)获取给的索引的节点(for循环,在左边由first节点开始找,在右边由last节点开始找)
        return unlink(node(index));
    }

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

3、List中的remove(Object o)删除指定元素(根据某节点的item删除节点)--返回boolean

        根据节点的item找到对应的节点方法----循环遍历所有节点

public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

3.5、ArrayList LinkedList 比较 

 如何选择ArrayList和LinkedList:

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

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

        3、一般来说,在程序中,4大多数都是查询,因此大多数情况下会选择ArrayList

        4、在一个项目中,根据业务灵活选择,可能一个模块用的是ArrayList,另外一个模块用的是LinkedList。

注意:要添加到List集合中对象,需要重写equals方法(保证内容相等,集合就包含)

           要添加到Set集合中对象,需要重写equals方法和hashCode方法(保证内容等,哈市等,元素相同,不能再添加)

四、Set接口

4.1、Set 接口基本介绍

1、无序(添加和去除的顺序不一致),没有索引的概念

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

        注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定

3、Set接口的实现类有:HashSet、TreeSet以及HashSet子类LinkedHashSet

4.2、Set 接口的常用方法 

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

4.3、Set 接口的遍历方式 

Collection 接口遍历方式一样:

        1、迭代器

        2、增强for循环

        3、不能用索引方式获取

4.4、Set 接口实现类-HashSet

4.4.1、HashSet 的全面说明

1、HashSet实现了Set接口

2、HashSet实际上是HashMap  

public HashSet() {
        map = new HashMap<>();
    }

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

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

5、不能有重复元素/对象。

//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
//3. 可以通过 remove 指定删除哪个对象
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
set.remove("john");
System.out.println("set=" + set);//3 个

//4 Hashset 不能添加相同的元素/数据?
set.add("lucy");//添加成功
set.add("lucy");//加入不了
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//Ok
System.out.println("set=" + set);

set.add(new String("zs"));//ok
set.add(new String("zs"));//加入不了.

4.4.2、HashSet 底层机制说明

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

先模拟简单的数组+链表:

        

 1、HashSet底层是HashMap

2、添加一个元素时,先得到hash值,由此hash值转换成存放的索引值

3、找到存储数据表table(放节点对象的数组),看到这个索引位置是否已经存放的有元素,如果没有则直接加入,如果有元素,调用equals比较,如果相同,就放弃添加,如果不同,则添加到最后

4、在Java8之后,如果有一条链表的元素个数达到TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。

HashSet源码分析:

1、新增add(E e)

        

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

//k中村的是添加的元素,value中不重要是一个new Object

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//计算hash值,^异或运算:两个二进制数值如果在同一位上相同,则结果中该位为0,否则为1
//hash = h ^ (h >>> 16)  高16位不动;低16位与高16位做异或运算;高16位的参与,增加了结果的随机性
//

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

执行 putVal(hash(key), key, value, false, true);函数:第一次增加元素时,进行Node[]数组的扩容,执行resize()函数,返回新的table(原数据拷贝后,仅扩容的数组)第一次数组大小为默认值DEFAULT_INITIAL_CAPACITY=16,阈值threshold=16*0.75=12,即数组长度 >12就扩容2倍,不等到16满。将由hash计算出的索引下数组元素赋值给临时变量p,判断 p 是否为 null,如果 p null, 表示还没有存放元素, 就创建一个 Node (key=元素,value=PRESENT),就放在该位置 tab[i] = newNode(hash, key, value, null)。

 Node<K,V>[] tab; Node<K,V> p; int n, i;
        //其中table是Node[]数组,节点数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //将用hash计算出索引对应的Node节点赋值给p
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
//(1) 根据 key ,得到 hash 去计算该 key 应该存放到 table 表的哪个索引位置
// 并把这个位置的对象,赋给 p
//(2) 判断 p 是否为 null
//(2.1) 如果 p null, 表示还没有存放元素 , 就创建一个 Node (key="java",value=PRESENT)
//(2.2) 就放在该位置 tab[i] = newNode(hash, key, value, null)
如果不是第一次添加元素:
        还是先由对象hash值计算出要放在哪个节点数组下,此时table不是null,继续判断p是不是null(p是由hash计算出元素即key应该存放到他变了表哪个索引位置,赋值给的p)p为null,直接放,结束增加程序。如果此时p不为null,则进入else语句,判断有对象的这个p和我们要加的元素key的hash()方法和equals()方法是不是相等。相等的话直接返回Value一个常量,不增加这个元素key。
Node<K,V> e; K k;
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);

不是红黑树,如果 table 对应索引位置,已经是一个链表, 就使用 for 循环比较,该索引下的节点对象链表的next属性是不是null,依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后。依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break

注意在把元素添加到链表后,立即判断 该链表是否已经达到 8个结点,, 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树),注意,在转成红黑树时,要进行判断, 判断条件:

for (int binCount = 0; ; ++binCount) {
          if ((e = p.next) == null) {
                  p.next = newNode(hash, key, value, null);
       // TREEIFY_THRESHOLD=8,即链表长度超过7,就要转换为红黑树。数组中存放
                  if (binCount >= TREEIFY_THRESHOLD - 1) 
                         treeifyBin(tab, hash);
                  break;
                    }
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //数组长度<64 则取扩容,两倍
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//64
            resize();

 分析HashSet的扩容机制和转成红黑树机制:

        1、HashSer的底层是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),就会进行红黑树化,没到64仍采用数组扩容机制。

因此,equals方法和hashCode方法要同时重写,保证内容相等就加入不了Set集合中,如String就同时重写了equals方法和hashCode方法。

Set set = new HashSet();
String s = new String("jack");
String x = new String("jack");
String y = "jack";
String z = "jack";        
set.add(s);
set.add(x);
set.add(y);
set.add(z);
System.out.println(set.size());//1
//jack只有一个jack
for (Object o : set) {
     System.out.println(o);
}

注意:要添加到List集合中对象,需要重写equals方法(保证内容相等,集合就包含)

           要添加到Set集合中对象,需要重写equals方法和hashCode方法(保证内容等,哈市等,元素相同,不能再添加)

4.5、Set 接口实现类-LinkedHashSet

1、LinkedHashSet是HashSet的子类

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

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

4、LinkedHashSet不允许添加重复元素

一个有意思的HashSet添加元素练习

import java.util.HashSet;
import java.util.Objects;
public class Homework01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();//ok
        Person p1 = new Person(1001,"AA");//ok
        Person p2 = new Person(1002,"BB");//ok
        set.add(p1);//ok
        set.add(p2);//ok
        //此处修改了p1的属性
        p1.name = "CC";
        //此时移除p1,因为是根据修改过的值计算的hash能定位出错,因为心在p1的hash空间是最开始没改CC时计算出的。
        set.remove(p1);
        System.out.println(set);//2个元素
        //这个对象的hash是根据新的CC算的,原来p1虽然也是CC,但它是根据AA计算的hash,只是放好位置后属性改了。因此可以放进去
        set.add(new Person(1001,"CC"));
        System.out.println(set);//3个元素
//这个AA和最开始的p1是一个位置,有元素,则以链表形式往后加。
        set.add(new Person(1001,"AA"));
        System.out.println(set);//4个元素

    }
}

class Person {
    public String name;
    public int id;

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

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

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

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

注意:要添加到List集合中对象,需要重写equals方法(保证内容相等,集合就包含)

           要添加到Set集合中对象,需要重写equals方法和hashCode方法(保证内容等,哈市等,元素相同,不能再添加)

4.6、TreeSet 

底层实际上是一个TreeMap,TreeMap底层实际上是一个二叉树

TreeSet集合中的元素,无序不可重复,但是,可以按照元素大小顺序排序,称为可排序集合

如果不指定元素的排序规则,放入TreeSet中异常。

1、元素(对象)实现Comparable<T>接口及compareTo(T  t)方法

        return   this.属性 - t.属性;    升序   

        return   t.属性 - this.属性;    降序   

        

2、 比较器方式:实现Comparator<T>接口及int compare(T o1, T o2);方法

        int compare(T o1, T o2):

             若返回值>0 如果o1>o2,交换o1、o2位置,如果o1<o2,不交换o1、o2位置,升序

             若返回值<0 如果o1>o2,不交换o1、o2位置,如果o1<o2,交换o1、o2位置,降序

public class Person{
    private int age;
    
}

class PersonComparator implements Comparator<Person>{
    @overvide
    public int compare(Person p1,Person p2){
        return p1.age - p2.age;
    }
}
//TreeSet的有参构造
Set<Person> treeSet  =  new TreeSet(PersonComparator);

最好的办法是使用匿名内部类:

Set<Person> treeSet  =  new TreeSet(new Comparator<Person>{
    @overvide
    public int compare(Person p1,Person p2){
        return p1.age - p2.age;
    }
});

五、泛型

5.1、泛型的理解和好处

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?

        就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参)。然后在使用/调用时传入具体的类型(类型实参)。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

//1. 当我们 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是 Dog 类型 (细节后面说...)
//2. 如果编译器发现添加的类型,不满足要求,就会报错
//3. 在遍历的时候,可以直接取出 Dog 类型而不是 Object
//4. public class ArrayList<E> {} E 称为泛型,那么 Dog->E
ArrayList<Dog> arrayList = new ArrayList<Dog>();
arrayList.add(new Dog("旺财", 10));
arrayList.add(new Dog("发财", 1));
arrayList.add(new Dog("小黄", 5));

//假如我们的程序员,不小心,添加了一只猫
//添加不了
//arrayList.add(new Cat("招财猫", 8));

        1、编译时,检查添加元素的类型 ,提高安全性

        2、减少类型转换次数,提高效率

        如果不使用泛型:Dog-加入--Object--取出--Dog,涉及类型转换

        使用泛型:Dog--Dog--Dog  放入时和取出时,不需要类型转换

5.2、泛型的作用

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

5.2.1、泛型类

interface 接口 <T>{} 和 class  类 <K,V>{}

        1、其中T,K,V不代表值,而是表示类型

        2、任意字母都可以

泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,类中方法返回类型,或类中方法的参数类型。此时类中方法知识使用类定义时的泛型,而非方法声明时的泛型。
class Person<E> {
E s ;//E 表示 s 的数据类型, 该数据类型在定义 Person 对象的时候指定,即在编译期间,就确定 E 是什么类型
public Person(E s) {//E 也可以是参数类型
this.s = s;
}
public E f() {//返回类型使用 E
return s;
}

//注意,特别强调: E 具体的数据类型在定义 Person 对象的时候指定,即在编译期间,就确定 E 是什么类型
Person<String> person = new Person<String>("xiaoming");

定义的泛型类就必须要传入参数吗?

        其实不是的,如果我们传入了参数那么这个泛型类才能真正的起到了限制类型的作用。否则的话就是可以传入任何类型的对象

 5.2.2、泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型 。包括返回值类型,形参类型。

只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。

/**
 * 说明:
 * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

静态方法有一种情况需要注意一下,在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

5.3 、泛型使用的注意事项和细节

1、interface 接口 <T>{} 和 class  类 <K,V>{}...T,E只能是引用类型

2、在给泛型指定具体类型后,可以传入该类型过着其子类类型

3、定义有泛型,声明时可以不写,默认是Object

 5.4、自定义泛型类

class 类名 <T,R...>{//表示可以有多个泛型

        成员;

}

注意:

        1、普通成员可以使用泛型(属性,方法)

        2、使用泛型的数组不能初始化

        3、静态方法中不能使用类的泛型

        4、泛型类的类型,是在创建对象的时候确定(一维实例化对象需要指定确定的类型)

        5、如果在创建对象时,没有指定类型,默认为Object

public static void main(String[] args) {

        //T=Double, R=String, M=Integer
        Tiger<Double,String,Integer> g = new Tiger<>("john");
        g.setT(10.9); //OK
        //g.setT("yy"); //错误,类型不对
        System.out.println(g);
        Tiger g2 = new Tiger("john~~");//OK T=Object R=Object M=Object
        g2.setT("yy"); //OK ,因为 T=Object "yy"=String 是Object子类
        System.out.println("g2=" + g2);

    }
class Tiger<T, R, M> {
    String name;
    R r; //属性使用到泛型
    M m;
    T t;
    //因为数组在new 不能确定T的类型,就无法在内存开空间
    T[] ts;、
     public Tiger(String name, R r, M m, T t) {//构造器使用泛型
        this.name = name;
        this.r = r;
        this.m = m;
        this.t = t;
    }

    //因为静态是和类相关的,在类加载时,对象还没有创建
    //所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化
//    static R r2;
//    public static void m1(M m) {
//
//    }

    public void setR(R r) {//方法使用到泛型
        this.r = r;
    }

    public M getM() {//返回类型可以使用泛型.
        return m;
    }
    

       
    

5.5、自定义泛型接口 

interface 接口名 <T,R...>{}

注意:

        1、接口中,静态方法不饿能使用泛型

        2、泛型接口的类型,在继承接口或者实现接口时确定,如果没有指定类型,默认为Object

        3、接口中的泛型不可以用于成员变量中

interface IUsb<U, R> {

    int n = 10;
    //U name; 不能这样使用

    //普通方法中,可以使用接口泛型
    R get(U u);

    void hi(R r);

    void run(R r1, R r2, U u1, U u2);

    //在jdk8 中,可以在接口中,使用默认方法, 也是可以使用泛型
    default R method(U u) {
        return null;
    }

5.6、自定义泛型方法 

修饰符 <T,R...> 返回类型  方法名(参数列表){}

注意:

        1、泛型方法,可以定义在普通类中, 也可以定义在泛型类中

        2、当调用方法时,传入参数,编译器,就会确定类型

        3、成员方法中使用类的泛型,不属于泛型方法

        4、

5.7、泛型通配符 

泛型通配符是用英文的问号 来表示的。

1、泛型没有继承性

        List<Object> list = new ArrayList<String>();编译不能通过的。

        如果是 List<?> c ,可以接受任意的泛型类型

2、<?>:支持任意泛型类型

        说明: List<?> 表示 任意的泛型类型都可以接受

3、<? extends A> :支持A类即A类的子类,规定了泛型的上限

4、<? super A> :支持A类即A类的父类,规定了泛型的下限

总结常用的 T,E,K,V,?

本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的:

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

六、Map接口

6.1、Map 接口和常用方法

6.1.1、Map 接口实现类的特点

1、Map接口与Collection接口并列存在,用于保存具有映射关系的数据

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

3、Map中的key不允许重复,原因和HashSet一样。Map中的value允许重复

4、Map中的key可以是null,value也可以为null,注意key只能有一个null,value的null可以有多个

5、常用String类作为Map的key

6、key和value之间存在一对一,即通过指定的key总能找到对应的value

7、Map存放的数据,一对key-value是放在一个HashMap$Node对象中,由于因为Node类实现了Entry接口,也可以说一对key-value就是一个Map.Entry

6.1.2、map 接口常用方法 

1、增加:map.put(key , value)

2、删:map.remove(key )

3、改:map.put(key , value)覆盖

4、查:map.get(key)

Map map = new HashMap();
map.put("邓超", new Book("", 100));

map.put("邓超", "孙俪");//替换-
map.put("王宝强", "马蓉");//OK
map.put("宋喆", "马蓉");//OK

//remove:根据键删除映射关系
map.remove(null);
//get:根据键获取值
Object val = map.get("鹿晗");
//size:获取元素个数
//isEmpty:判断个数是否为 0
//containsKey:查找键是否存在

6.2、Map遍历方式

6.2.1、通过map.keySet()

先取出 所有的 Key-----key是存在Set中 , 通过 Key 取出对应的 Value
Set keyset = map.keySet();

//增强for
for (Object key : keyset) {
    System.out.println(key + "-" + map.get(key));
}

//迭代器
Iterator iterator = keyset.iterator();
    while (iterator.hasNext()) {
        Object key = iterator.next();
        System.out.println(key + "-" + map.get(key));
}

6.2.2、通过map.values()

通过 map.values(),把所有的 values 取出---values是存在Collection中
Collection values = map.values();
//增强 for
for (Object value : values) {
    System.out.println(value);
}
//迭代器
Iterator iterator2 = values.iterator();
    while (iterator2.hasNext()) {
        Object value = iterator2.next();
        System.out.println(value);
}

6.2.3、通过map.entrySet()

通过 map.entrySet()得到EntrySet对象,进而来获取 k-v
注意:EntrySet<Map.Entry<K,V>>
// EntrySet<Map.Entry<K,V>>
Set entrySet = map.entrySet();
Set<Map.Entry<K,V>> entrySet = map.entrySet();<

//增强 for
for (Object entry : entrySet) {
for (<Map.Entry<K,V> m : entrySet) {
    //将 entry 转成 Map.Entry
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
}
//迭代器
Iterator iterator3 = entrySet.iterator();
    while (iterator3.hasNext()) {
        Object entry = iterator3.next();
//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry(getKey,getValue)                   
    //向下转型 Map.Entry
        Map.Entry m = (Map.Entry) entry;
        System.out.println(m.getKey() + "-" + m.getValue());
}

 6.3、Map 接口实现类-HashMap

1、Map接口实现类:HashMap、HashTable和Properties

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

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

                数组(table节点数组Node[])+链表Node对象(实现Map.Entry,有属性K,V,next-->就是因为这个next才形成单向链表   实现Map.Entry))HashMap中有属性Set<Map.Entry<K,V>> entrySet,Set集合里面放的K,V,比通过keySet更快查询。

        Set<Map.Entry<K,V>> entrySet  该Set集合元素类型是Map.Entry<K,V>,其中有方法getKey()获得key,getValue()获得value。

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

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

6、与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的

7、HashMap没有实现同步,因为是线程不安全的,方法没有做同步互斥的操作,没有synchronized

6.3.1、HashMap 底层机制及源码剖析

 扩容机制与HashSet一样:

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

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

3、当添加元素key-value时,通过key的hash值获取table数组的索引。然后判断该索引下是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备增加的key是否相等,如果相等,直接替换value,如果不等,需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。

4、第一次添加元素时,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)

5、之后再扩容,则需要扩容table容量为原来的2倍(16*2=32),临界值为原来的2倍24,以此类推。

6、Java8之后,如果一条链表元素个数超过TREEIFY_THRESHOLD(默认是8),并且table大小>MIN_TREEIFY+CAPACITY(默认是64),此时就会进行树话(红黑树)。

//1. 执行构造器 new HashMap()
//初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
//2. 执行 put 调用 hash 方法,
//计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {//K = "java" value = 10
    return putVal(hash(key), key, value, false, true);
}
//3. 执行 putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;//辅助变量
    //如果底层的 table 数组为 null, 或者 length =0 , 就扩容到 16
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把加入的 k-v
    //, 创建成一个 Node ,加入该位置即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;//辅助变量
    // 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
    // 并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真)
    // 就认为不能加入新的 k-v
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处理

        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 个,到 8 个,后
                        //就调用 treeifyBin 方法进行红黑树的转换
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                                break;
                    }
                //如果在循环比较过程中,发现有相同,就 break,就只是替换 value

                        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; //替换,key 对应 value
                    afterNodeAccess(e);
                    return oldValue;
            }
        }
++modCount;//每增加一个 Node ,就 size++
if (++size > threshold[12-24-48])//如 size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}

//5. 关于树化(转成红黑树)
//如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容. //否则才会真正的树化 -> //剪枝
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
}

 6.4、Map 接口实现类-Hashtable

6.4.1、HashTable 的基本介绍

1、存放的元素是键值对:即K-V

2、HashTable的键和值都不能为null,否则会抛出空指针异常

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

4、HashTable是线程安全的(synchronized),HashMap是线程不去安全的

        Hashtable table = new Hashtable();//ok
        table.put("john", 100); //ok
        //HashTable的键和值都不能为null
        //table.put(null, 100); //异常 NullPointerException
        //table.put("john", null);//异常 NullPointerException
        table.put("lucy", 100);//ok
        table.put("lic", 100);//ok
        table.put("lic", 88);//替换
        table.put("hello1", 1);
        table.put("hello2", 1);
        table.put("hello3", 1);
        table.put("hello4", 1);
        table.put("hello5", 1);
        table.put("hello6", 1);
        System.out.println(table);

6.5、HashMap和HashTable对比 

6.6、Map 接口实现类-Properties 

 1、1. Properties 继承 Hashtable实现Map,也是一种键值对的形式保存数据。和HashTable类似

2、key-value为String类型,主要用于配置文件。xxx.properties文件中加载数据到Properties对象并进行读取和修改。

Properties properties = new Properties();
try {
      InputStream is = Map01.class.getClassLoader().getResourceAsStream("xxx.properties");
      properties.load(is);
} catch (IOException e) {
       e.printStackTrace();
}

七、集合实现类的总结

1、Collection接口---List接口

1、ArrayList:底层维护了一个动态扩容(1.5倍)数组Object[] elementData,有序

        无参构造器:this.elementData =DEFAULTCAPACITY_EMPTY_ELEMENTDATA;static                              final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

                                初始10个---->1.5倍

        有参构造器: ArrayList(int initialCapacity),初始化elementData数组容量

                                初始手动赋值,之后1.5倍扩容

2、LinkedList:底层维护了一个Node[] element对象实现双向链表。Node中有属性:E item;存放数据 Node<E> next;指向下一个节点 和Node<E> prev;指向前一个节点。

                final Node<E> newNode = new Node<>(prev, element, next);

     LinkedList类中有属性firstlast都是Node对象,分别为双向链表的第一个和最后一个节点。

 2、Collection接口---Set接口

1、HashSet:底层是Hashmap,使用map的key存储HashSet元素,value是一个常量对象。

                        数组+单向链表+红黑树结构。

        数组:transient Node<K,V>[] table;存放节点对象的        

                   Node类实现了Map.Entry<K,V>接口,一对k-v对应一个Map.Entry<K,V>,使用Set存储HashMap中的元素所有的k-v,transient Set<Map.Entry<K,V>> entrySet

        初始化容量16,临界0.75,扩容2倍

        keySet内部类:存储键       Values:值      entrySet:把键值对放在Set集合中。

        链表:通过Node节点中的next属性实现

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next)

2、LinkedHashset:继承了HashSet,但其底层是LinkedHashMap,使用map的key存储HashSet元素,value是一个常量对象。

public LinkedHashSet() {
    super(16, .75f, true);
}
 map = new LinkedHashMap<>(initialCapacity, loadFactor);

                数组+双向链表+红黑树

        数组:transient Node<K,V>[] table;存放节点对象

                 内部类Entry继承HashMap.Node<K,V>,新增了两个属性,befor和after,实现双向链表。

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

LinkedHashMap类中有属性LinkedHashMap.Entry<K,V>  head;和LinkedHashMap.Entry<K,V>  tail;分别表示双向链表的头尾节点。

        注意:hashMap中数组+单向链表是table数组中存放Node节点(hash,k,v,next属性),再LinkedHashMap中是table数组中存放LinkedHashMap.Entry节点(LinkedHashMap中的内部类,继承了HashMap中的Node,又增加了before和after属性表示前后节点)实现双向链表,其余的和HashMap一样。

        初始:16个容量,阈值12,超过12之后数组容量扩容2倍---32,阈值也2倍---24,以此类推。

八、Collections工具类

8.1、Collections 工具类介绍

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

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

8.2、排序操作 

1、reverse(List):反转List集合中的元素顺序

2、shuffle(List):对List集合元素进行随机排序

3、sort(List):根据元素的自然顺序对指定List集合元素按升序排序

4、sort(List,Comparator):根据指定Comparator产生的顺序对List集合元素进行排序。元素实现Comparable接口实现CompareTo(obj)方法、实现Comparator接口及其方法public int compare(Object o1, Object o2)---匿名内部类

5、swap(List,int,int):将指定list集合中i处元素和j处元素进行交换

6、Collections.copy(dest, list);

8.3、其他方法

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

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

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

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

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

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

7、boolean replaceAll(List listObject oldValObject newVal):使用新值替换 List 对象的所有旧值 

九、Redis---非关系型数据库中的集合使用

十、各大容器中的集合

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值