• Java集合总结


      Java集合类是一种特别有用的工具类,可用于存储数量不等的对象,并可用于实现常用的数据结构,如栈、队列等。Java集合大致可分为Set、List、Queue、Map四种体系,Set代表无序、不可重复的集合;list代表有序、重复的集合;Map代表具有映射关系的集合;Queue代表一种队列集合实现。

    一、Java集合概述

      为了保存数量不确定的数据,以及保存具有映射关系的数据,Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java集合就像一种容器,可以把多个对象丢进该容器中,从Java5增加了泛型以后,Java集合能记住容器中对象中的数据类型,使得编码更加简单、健壮。所有的集合类都位于java.util包下,后来为了处理多线程环境下并发安全问题,Java5提供了一些多线程支持的集合类。集合只能保存对象(实际上是对象的引用变量)。

      

       Java的集合类主要由两个接口派生而出:Collection和Map,collection和map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类;set和list接口是collection接口派生的两个子接口,它们分别代表了无序集合和有序集合;queue是Java提供的队列实现,类似于list。其中,ArrayList、HashSet、LinkedList和TreeSet是经常使用到的集合类。

      

       所有的map实现类用于保存具有映射关系的数据,map保存的每项数据都是key-value对,也就是由key和value两个值组成,map里的key是不可重复的,key用于标识集合里的每项数据。其中,HashMap和TreeMap是经常使用到的集合类。

      可以把Java集合分为三大类,其中set集合类似于一个罐子,把一个对象添加到set集合时,set集合无法记住添加这个元素的顺序,所以set里的元素不能重复;list集合非常像一个数组,它可以记住每次添加的顺序、且list的长度可变;map集合也像一个罐子,只是它里面的每项数据都由两个值组成(键值对)。如果访问list集合中的元素,可以直接根据元素的索引来访问;如果访问map集合中的元素,可以根据每项元素的key来访问其value;如果访问set集合中的元素,只能根据元素本身访问。

    collection接口定义方法

      collection接口是list、set、queue的父接口,所以该接口里面定义的方法可用于操作这三个集合类。

       容器的功能主要为添加对象、删除对象、清空容器、判断容器是否为空,返回collection集合的个数等。

    遍历集合方式

    • 使用lambda表达式遍历集合
    • iterator接口被称作迭代器,它是collection接口的父接口,所以可使用iterator遍历集合元素,接口中主要定义了两个方法,hasNext()如果有元素可迭代返回true和next()返回迭代的下一个元素
    • 使用foreach遍历集合元素
    • 使用stream操作集合
    public class Main {
    
        public static void main(String[] args) {
            Collection col = new ArrayList<>();
            col.add("世");
            col.add("界");
            col.add("你");
            col.add("好");
            //使用lamdba遍历集合
            col.forEach(obj-> System.out.print(obj));
            Iterator it = col.iterator();
            //使用iterator遍历集合,遍历的不是集合元素本身,系统只是把集合元素的值赋值给迭代变量,在循环中修改迭代变量的值没有实际意义
            while(it.hasNext()){
                String str = (String)it.next();
                System.out.print(str);
            }
            //使用foreach遍历集合元素,遍历的不是集合元素本身,系统只是把集合元素的值赋值给迭代变量,在循环中修改迭代变量的值没有实际意义
            for(Object obj:col){
                System.out.print(obj);
            }
            //使用stream流遍历集合
            ArrayList<String> list = new ArrayList<>();
            list.add("世");
            list.add("界");
            list.add("你");
            list.add("好");
            list.stream().forEach(System.out::print);
        }
    }

    注意:当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量(就如同参数传递是值传递,基本数据类型传递的是值,引用类型传递的仅仅是对象的引用变量),所以修改迭代变量的值对集合元素本身没有任何影响。

    二、List

      List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素 。List集合默认按元素的添加顺序设置元素的索引,例如第一个添加的元素索引为0,第二个添加的元素索引为1......
      List作为Collection接口的子接口,可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里增加了一些根据索引来操作集合元素的方法。

    接口中定义的方法:

    • void add(int index, Object element)::在列表的指定位置插入指定元素(可选操作)。
    • addAll(Collection<? extends E> c) :追加指定集合的所有元素到这个列表的末尾,按他们的指定集合的迭代器返回(可选操作)。 
    • Object get(index):返回列表中指定位置的元素。
    • int indexOf(Object o): 返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
    • int lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。
    • remove(int index):移除此列表中指定位置的元素(可选操作)。 
    • Object set(int index, Object element):用指定元素替换列表中指定位置的元素。
    • subList(int fromIndex, int toIndex) :返回一个视图之间的指定 fromIndex,包容,和 toIndex这份名单的部分,独家。 
    • Object[] toArray(): 返回按适当顺序包含列表中的所有元素的数组(从第一个元素到最后一个元素)。
    • void replaceAll(UnaryOperator operator):根据operator指定的计算规则重新设置List集合的所有元素。
    • void sort(Comparator c):根据Comparator参数对List集合的元素排序。

    Arraylist  

      ArrayList和Vector作为List类的两个典型实现,完全支持之前介绍的List接口的全部功能。ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initalCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超过了该数组的长度时,它们的initalCapacity会自动增加。

       ArrayList底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素 ;当使用List<String> list=new ArrayList<>(3)方式创建ArrayList集合时

    //动态Object数组,用来保存加入到ArrayList的元素
    transient Object[] elementData;
    /**
         * 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);
            }
        } 

    当使用List<String> list = new ArrayList<>()方式创建ArrayList,不指定集合的大小

    /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

    在这里可以看出,private static final int DEFAULT_CAPACITY = 10;默认容量为10,当向数组添加元素List.add("1")时;先调用add(E e)方法

    /**
         * 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);  // 数组的长度加1
            elementData[size++] = e;
            return true;
        }
     
    在该方法中,先调用了一个ensureCapacityInternal()方法,顾名思义:该方法用来确保数组中是否还有足够容量。经过一系列方法(不必关心),最后有个判断:如果剩余容量足够存放这个数据,则进行下一步,如果不够,则需要执行一个重要的方法:
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            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);
        }

    可以看到,ArrayList是一个动态扩容的数组,vector也是这样的,如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initalCapacity的大小,这样可以提高性能。Arraylist和Vector的初始容量都是10,ArrayList的扩容增量方式为原容量的1.5倍+1,比如容量为10,那么第一次扩容后容量是16;Vector的扩容增量方式为原容量的一倍,比如Vector的原容量为10,第一次扩容增量为20。

    ArrayList和Vector的区别

    1.ArrayList是线程不安全的,Vector是线程安全的。
    2.Vector的性能比ArrayList差。

    ArrayList三种遍历方式

    public class List {
        public static void main(String[] args) {
            ArrayList<Integer> list = new ArrayList<>();
            //通过迭代器遍历
            Iterator<Integer> iterator = list.iterator();
            if(iterator.hasNext()){
                iterator.next();
            }
            //随机访问,通过索引去遍历
            int size = list.size();
            for(int i=0;i<size;i++){
                System.out.println(list.get(i));
            }
            //for遍历循环
            for(Integer l:list){
                System.out.println(l);
            }
            //TODO遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低。
        }
    }

    LinkedList

      LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈"来使用,也可以当成队列来使用。
      LinkedList的实现机制与ArrayList完全不同。ArrayList内部是以数组的形式来保存集合中的元素的,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色。底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素

    由于LinkedList双端队列的特性,所以新增了一些方法:

    • void addFirst(E e):将指定元素插入此列表的开头。
    • void addLast(E e): 将指定元素添加到此列表的结尾。
    • E getFirst(E e): 返回此列表的第一个元素。
    • E getLast(E e): 返回此列表的最后一个元素。
    • boolean offerFirst(E e): 在此列表的开头插入指定的元素。
    • boolean offerLast(E e): 在此列表末尾插入指定的元素。
    • E peekFirst(E e): 获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。
    • E peekLast(E e): 获取但不移除此列表的最后一个元素;如果此列表为空,则返回 null。
    • E pollFirst(E e): 获取并移除此列表的第一个元素;如果此列表为空,则返回 null。
    • E pollLast(E e): 获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
    • E removeFirst(E e): 移除并返回此列表的第一个元素。
    • boolean removeFirstOccurrence(Objcet o): 从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。
    • E removeLast(E e): 移除并返回此列表的最后一个元素。
    • boolean removeLastOccurrence(Objcet o): 从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。

    LinkedList的本质

    LinkedList调用默认构造函数,创建一个链表。由于维护了一个表头,表尾的Node对象的变量。可以进行后续的添加元素到链表中的操作,以及其他删除,插入等操作。也因此实现了双向队列的功能,即可向表头加入元素,也可以向表尾加入元素
    /**
         * 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;
    
        /**
         * Constructs an empty list.
         */
        public LinkedList() {
        }
    private static class Node<E> {
            //表示集合元素的值
            E item;
           //指向下个元素
            Node<E> next;
         //指向上个元素
            Node<E> prev;
    ...................................省略
        }

    由此可以具体了解链表是如何串联起来并且每个节点包含了传入集合的元素。

    LinkedList新增元素的操作,向表尾节点加入新的元素

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

    如何将“双向链表和索引值联系起来的”?

     public E get(int index) {
            checkElementIndex(index);//检查索引是否有效
            return node(index).item;
        }
    Node<E> node(int index) {
            // assert isElementIndex(index);
    
            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;
            }
        }
      首先会比较“index”和“双向链表长度的1/2”;若前者小,则从链表头开始往后查找,直到index位置;否则,从链表末尾开始先前查找,直到index位置。这就是“双线链表和索引值联系起来”的方法。
    到此我们便会明白,LinkedList在插入、删除元素时性能比较出色,随机访问集合元素时性能较差。

    LinkedList遍历方式

    LinkedList支持多种遍历方式。
    1.通过迭代器遍历LinkedList
    2通过快速随机访问遍历LinkedList
    3.通过for循环遍历LinkedList
    4.通过pollFirst()遍历LinkedList
    5.通过pollLast()遍历LinkedList
    6通过removeFirst()遍历LinkedList
    7.通过removeLast()遍历LinkedList
    实现都比较简单,就不贴代码了。
    其中采用逐个遍历的方式,效率比较高。采用随机访问的方式去遍历LinkedList的方式效率最低。LinkedList也是非线程安全的。

    ArrayList与LinkedList性能对比

    ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
    LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。LinkedList应使用采用逐个遍历的方式遍历集合元素。
    如果涉及到“动态数组”、“栈”、“队列”、“链表”等结构,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
    1、 对于需要快速插入,删除元素,应该使用LinkedList。
    2、 对于需要快速随机访问元素,应该使用ArrayList。
    3、 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

    三、Set

    HashSet

      HashSet是Set接口的典型实现,实现了Set接口中的所有方法,并没有添加额外的方法,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素。因此具有很好的存取和查找性能。
    HashSet特点
    •  不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
    • HashSet不是同步的,如果多个线程同时访问一个HashSet,则必须通过代码来保证其同步。
    • 集合元素值可以是null。

      当向HashSet集合中添加一个元素时,HashSet会调用该对象的hashCode()方法获取该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。

    equals()

    1. 若某个类没有覆盖equals()方法,当它的通过equals()比较两个对象时,实际上是比较两个对象是不是同一个对象。这时,等价于通过“==”去比较这两个对象,即两个对象的内存地址是否相同。
    2. 我们可以覆盖类的equals()方法,来让equals()通过其它方式比较两个对象是否相等。通常的做法是:若两个对象的内容相等,则equals()方法返回true;否则,返回fasle.

    hashCode()

    hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

    hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。虽然,每个Java类都包含hashCode() 函数。但是,仅仅当创建某个“类"的散列表时,该类的hashCode() 才有用。更通俗地说就是创建包含该类的HashMap,Hashtable,HashSet集合时,hashCode() 才有用。因为HashMap,Hashtable,HashSet就是散列表集合。
    在散列表中,hashCode()作用是:确定该类的每一个对象在散列表中的位置;其它情况下类的hashCode() 没有作用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

    1. 若某个类没有覆盖hashCode()方法,当它的通过hashCode()比较两个对象时,实际上是比较两个对象是不是同一个对象。这时,等价于通过“==”去比较这两个对象,即两个对象的内存地址是否相同。
    2. 我们可以覆盖类的hashCode()方法,来让hashCode()通过其它方式比较两个对象是否相等。通常的做法是:若两个对象的内容相等,则hashCode()方法返回true;否则,返回fasle。

    HashSet中判断集合元素相等

    1. 如果有两个元素通过equal()方法比较返回false,但它们的hashCode()方法返回不相等,HashSet将会把它们存储在不同的位置。
    2. .如果有两个元素通过equal()方法比较返回true,但它们的hashCode()方法返回不相等,HashSet将会把它们存储在不同的位置。
    3. 如果两个对象通过equals()方法比较不相等,hashCode()方法比较相等,HashSet将会把它们存储在相同的位置,在这个位置以链表式结构来保存多个对象。这是因为当向HashSet集合中存入一个元素时,HashSet会调用对象的hashCode()方法来得到对象的hashCode值,然后根据该hashCode值来决定该对象存储在HashSet中存储位置。
    4. 如果有两个元素通过equal()方法比较返回true,但它们的hashCode()方法返回true,HashSet将不予添加。
    注意:HashSet是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,将会导致性能下降。所以如果重写类的equals()方法和hashCode()方法时,应尽量保证两个对象通过hashCode()方法返回值相等时,通过equals()方法比较返回true。
    public class Set {
        public static void main(String[] args) {
            HashSet<R> set = new HashSet<R>();
            set.add(new R(5));
            set.add(new R(3));
            set.add(new R(2));
            set.add(new R(1));
            System.out.println(set);
            Iterator<R> iterator = set.iterator();
            R next = iterator.next();
            next.count = 3;
            System.out.println(set);
            set.remove(new R(3));
            System.out.println(set);
            System.out.println(set.contains(new R(3)));
            System.out.println(set.contains(new R(1)));
        }
    }
    
    class R{
        int count;
        public R(int count){
            this.count = count;
        }
    
        @Override
        public String toString() {
            return "R{" + "count=" + count + '}';
        }
    
        @Override
        public boolean equals(Object o) {
            if(this==o){
                return true;
            }
            if(o!=null&&o.getClass() == R.class){
                R r = (R)o;
                return this.count ==r.count;
            }
            return false;
        }
    
        @Override
        public int hashCode() {
            return this.count;
        }
    }

     

     当程序把可变对象添加到HashSet中之后,如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。HashSet底层是一个HashMap(保存数据),实现set接口,默认初始容量为16,加载因子为0.75:当元素个数超过容量的0.75倍时,进行扩容,扩容增量为原容量的1倍,即当容量为16时,第一次扩容为32。

    LinkedHashSet

    LinkedHashSet是HashSet对的子类,也是根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,使得元素是以插入的顺序来保存的。当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。但是由于要维护元素的插入顺序,在性能上略低与HashSet,但在迭代访问Set里的全部元素时有很好的性能。
    注意:LinkedHashSet依然不允许元素重复,判断重复标准与HashSet一致。 

    TreeSet

    TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。TreeSet集合里面的元素必须存放同一种类型的数据,否则是会报错的。存放对象类型的时候,最好是重写COMPARATOR()方法此外,TreeSet还提供了几个额外的方法。

    • comparator():返回对此 set 中的元素进行排序的比较器;如果此 set 使用其元素的自然顺序,则返回null。
    • first():返回此 set 中当前第一个(最低)元素。
    • last(): 返回此 set 中当前最后一个(最高)元素。
    • lower(E e):返回此 set 中严格小于给定元素的最大元素;如果不存在这样的元素,则返回 null。
    • higher(E e):返回此 set 中严格大于给定元素的最小元素;如果不存在这样的元素,则返回 null。
    • subSet(E fromElement, E toElement):返回此 set 的部分视图,其元素从 fromElement(包括)到 toElement(不包括)。
    • headSet(E toElement):返回此 set 的部分视图,其元素小于toElement。
    • tailSet(E fromElement):返回此 set 的部分视图,其元素大于等于 fromElement。

    TreeSet的排序方式

    TreeSet中所谓的有序,不同于之前所讲的插入顺序,而是通过集合中元素属性进行排序方式来实现的。
    TreeSet支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。

    1、自然排序

      Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小了。当一个对象调用该方法与另一个对象比较时,例如obj1.compareTo(obj2),如果该方法返回0,则表明两个对象相等;如果该方法返回一个整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明oj1小于obj2。
      TreeSet会调用集合中元素所属类的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,即把通过compareTo(Object obj)方法比较后比较大的的往后排。这种方式就是自然排序。
    注意:TreeSet中只能添加同一种类型的对象,否则无法比较,会出现异常。

    TreeSet中判断集合元素相等

    对于TreeSet集合而言,判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0——如果通过compareTo(Object obj)方法比较返回0,TreeSet则会认为它们相等,不予添加入集合内;否则就认为它们不相等,添加到集合内。
    TreeSet是根据红黑树结构找到集合元素的存储位置。

    2、定制排序

      TreeSet的自然排序是根据集合元素中compareTo(Object obj)比较的大小,以升序排列。而定制排序是通过Comparator接口的帮助。该接口包含一个int compare(T o1,T o2)方法,该方法用于比较o1,o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。
      如果要实现定制排序,则需要在创建TreeSet时,调用一个带参构造器,传入Comparator对象。并有该Comparator对象负责集合元素的排序逻辑,集合元素可以不必实现Comparable接口。
    public class Set {
        public static void main(String[] args) {
            //倒叙
            Comparator<Integer> comparator = new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    if (o1 > o2) {
                        return -1;
                    } else if (o1 < o2) {
                        return 1;
                    } else {
                        return 0;
                    }
                }
            };
            TreeSet<Integer> set = new TreeSet<>(comparator);
            set.add(1);
            set.add(2);
            set.add(3);
            set.add(4);
            System.out.println(set);
        }
    }

    总结:无论使用自然排序还是定制排序,都可以通过自定义比较逻辑实现各种各样的排序方式。

    注意:如果向TreeSet中添加了一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整它们。总之,推荐不要修改放入TreeSet集合中元素的关键实例变量。
    TreeSet也是非线程安全的。

    HashSet、TreeSet的性能对比

    EnumSet内部以位向量的形式存储,结构紧凑、高效,且只存储枚举类的枚举值,所以最高效。HashSet以hash算法进行位置存储,特别适合用于添加、查询操作。LinkedHashSet由于要维护链表,性能比HashSet差点,但是有了链表,LinkedHashSet更适合于插入、删除以及遍历操作。而TreeSet需要额外的红黑树算法来维护集合的次序,性能最次。

    但是具体使用要考虑具体的使用场景:
    当需要一个特定排序的集合时,使用TreeSet集合。
    当需要保存枚举类的枚举值时,使用EnumSet集合。
    当经常使用添加、查询操作时,使用HashSet。
    当经常插入排序或使用删除、插入及遍历操作时,使用LinkedHashSet。

    四、Map

    HashMap与Hashtable的区别

    1. Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能好一些;但如果有多个线程访问同一个Map对象时,是盗用Hashtable实现类会更好。
       
    2. Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但是HashMap可以使用null作为key或value。
      Map用户保存具有映射关系的数据,因此Map集合里保存着两组数,一组值用户保存Map里的key,另一组值用户保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。
      key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的value。
     

    Map集合最典型的用法就是成对地添加、删除key-value对,然后就是判断该Map中是否包含指定key,是否包含指定value,也可以通过Map提供的keySet()方法获取所有key组成的集合,然后使用foreach循环来遍历Map的所有key,根据key即可遍历所有的value。
    public class Map {
        public static void main(String[] args) {
            HashMap<String, Integer> map = new HashMap<String,Integer>();
            map.put("Monday",1);
            map.put("Tuesday",2);
            map.put("Wednesday ",3);
            map.put("Thursday ",4);
            //判断是否包含指定的key
            System.out.println(map.containsKey("Monday"));
            //判断是否包含指定的value
            System.out.println(map.containsValue(2));
            //循环遍历
            Set<String> set = map.keySet();
            for(String s:set){
                System.out.println(s+":"+map.get(s));
            }
            map.remove("Monday");
            System.out.println(map);
        }
    }

    HashMap

    HashMap判断key与value相等的标准

    key判断相等的标准

    类似于HashSet,HashMap与Hashtable判断两个key相等的标准是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等,则认为两个key是相等的。

    value判断相等的标准

    HashMap与Hashtable判断两个value相等的标准是:只要两个对象通过equals()方法比较返回true即可。

    注意:HashMap中key所组成的集合元素不能重复,value所组成的集合元素可以重复。

    注意:如果是加入HashMap的key是个可变对象,在加入到集合后又修改key的成员变量的值,可能导致hashCode()值以及equal()的比较结果发生变化,无法访问到该key。一般情况下不要修改。
    HashMap的本质
    // 默认构造函数。
    HashMap()
    
    // 指定“容量大小”的构造函数
    HashMap(int capacity)
    
    // 指定“容量大小”和“加载因子”的构造函数
    HashMap(int capacity, float loadFactor)
    
    // 包含“子Map”的构造函数
    HashMap(Map<? extends K, ? extends V> map)
    从构造函数中,了解到两个重要的元素:容量大小(capacity)以及加载因子(loadFactor)。
    容量(capacity)是哈希表的容量,初始容量是哈希表在创建时的容量(即DEFAULT_INITIAL_CAPACITY = 1 << 4)。
    加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
    通常,默认加载因子是 0.75(即DEFAULT_LOAD_FACTOR = 0.75f), 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 resize操作次数。如果容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

    HashMap遍历方式

    • 遍历HashMap的键值对:第一步:根据entrySet()获取HashMap的“键值对”的Set集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合。
    • 遍历HashMap的键:第一步:根据keySet()获取HashMap的“键”的Set集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合。
    • 遍历HashMap的值:第一步:根据value()获取HashMap的“值”的集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合。

      HashMap的初始容量为16,加载因子为0.75,当元素个数超过初始容量的0.75倍时,进行扩容,扩容为原容量的一倍,即当容量为16时,扩容为32。

    LindkedHashMap实现类

    HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap使用双向链表来维护key-value对的次序。
    LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但是因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时有较好的性能。迭代输出LinkedHashMap的元素时,将会按照添加key-value对的顺序输出。
    本质上来讲,LinkedHashMap=散列表+循环双向链表

    HashTable

    是一个古老的Map实现类

    TreeMap

    TreeMap是SortedMap接口的实现类。TreeMap 是一个有序的key-value集合,它是通过红黑树实现的,每个key-value对即作为红黑树的一个节点。

    TreeMap排序方式

    TreeMap有两种排序方式,和TreeSet一样。

    自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则会抛出ClassCastException异常。

    定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。

    TreeMap中判断两个元素key、value相等的标准

    类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的。

    TreeMap中判断两个value相等的标准是:两个value通过equals()方法比较返回true。

    注意:如果使用自定义类作为TreeMap的key,且想让TreeMap良好地工作,则重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,它们通过compareTo()方法比较应该返回0。如果两个方法的返回结果不一致,TreeMap与Map接口的规则就会冲突。

    除此之外,与TreeSet类似,TreeMap根据排序特性,也添加了一部分新的方法,与TreeSet中的一致。

    TreeMap的本质

    R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

    红黑树的特性:
    (1)每个节点或者是黑色,或者是红色。
    (2)根节点是黑色。
    (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
    (4)如果一个节点是红色的,则它的子节点必须是黑色的。
    (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    注意:
    (01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
    (02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

     可以说TreeMap的增删改查等操作都是在一颗红黑树的基础上进行操作的。

    TreeMap遍历方式

    • 遍历TreeMap的键值对:第一步:根据entrySet()获取TreeMap的“键值对”的Set集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合
    • 遍历TreeMap的键:第一步:根据keySet()获取TreeMap的“键”的Set集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合。
    • 遍历TreeMap的值:第一步:根据value()获取TreeMap的“值”的集合。第二步:通过Iterator迭代器遍历“第一步”得到的集合。

    Map实现类的性能分析及适用场景

    HashMap与Hashtable实现机制几乎一样,但是HashMap比Hashtable性能更好些。
    LinkedHashMap比HashMap慢一点,因为它需要维护一个双向链表。
    TreeMap比HashMap与Hashtable慢(尤其在插入、删除key-value时更慢),因为TreeMap底层采用红黑树来管理键值对。
    适用场景:
    一般的应用场景,尽可能多考虑使用HashMap,因为其为快速查询设计的。
    如果需要特定的排序时,考虑使用TreeMap。
    如果仅仅需要插入的顺序时,考虑使用LinkedHashMap。

     五、Queue

      Queue用户模拟队列这种数据结构,队列通常是指“先进先出”(FIFO,first-in-first-out)的容器。队列的头部是在队列中存放时间最长的元素,队列的尾部是保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。

    接口中定义的方法

    六、操作集合的工具类Collections

      Java提供了一个操作set、list和map等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。

    排序操作

    • static void reverse(List<?> list) :反转指定列表中元素的顺序。  
    • static void shuffle(List<?> list) :随机置换指定列表使用随机默认源。 
    • void sort(List<T> list) :指定列表为升序排序,根据其元素的 natural ordering。  
    • static <T> void sort(List<T> list, Comparator<? super T> c) :根据指定的比较器指定的顺序对指定的列表进行排序。  
    • static void swap(List<?> list, int i, int j) :在指定的列表中的指定位置上交换元素。

    • static void rotate(List<?> list, int distance) :按指定的距离旋转指定列表中的元素。  
    public class List {
        public static void main(String[] args) {
            ArrayList<Integer> list = new ArrayList<>();
            list.add(2);
            list.add(-5);
            list.add(3);
            list.add(0);
            System.out.println(list);//[2, -5, 3, 0]
            //将list集合的顺序反转
            Collections.reverse(list);
            System.out.println(list);//[0, 3, -5, 2]
            //将list按升序排序
            Collections.sort(list);
            System.out.println(list);//[-5, 0, 2, 3]
            //将list集合元素随机排序
            Collections.shuffle(list);
            System.out.println(list);//[0, 2, 3, -5]
        }
    }

    查找、替换操作

    • static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) :使用二进制搜索算法搜索指定对象的指定列表。  
    • T max(Collection<? extends T> coll) :返回最大元素的集合,根据其元素的自然排序。  
    • static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) :返回给定集合的最大元素,根据指定的比较器诱导的顺序。
    • T min(Collection<? extends T> coll) :返回最小的元素的集合,根据其元素的自然排序。 
    • static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) :返回给定集合的最小元素,根据指定的比较器诱导的顺序。  
    • static <T> void fill(List<? super T> list, T obj) :用指定元素替换指定列表的所有元素。  
    • static int frequency(Collection<?> c, Object o) :返回指定集合中等于指定对象的元素的数目。  
    • static int indexOfSubList(List<?> source, List<?> target) :返回指定的源列表中指定的目标列表的第一个发生的起始位置,或-如果没有这样的发生,则- 1。  
    • static int lastIndexOfSubList(List<?> source, List<?> target) :返回指定的源列表中指定的目标列表的最后一个发生的起始位置,或-如果没有这样的发生,则- 1。  
    • static <T> boolean replaceAll(List<T> list, T oldVal, T newVal) :将列表中的某一特定值的所有出现替换为另一个。  、
    public class List {
        public static void main(String[] args) {
            ArrayList<Integer> list = new ArrayList<>();
            list.add(2);
            list.add(-5);
            list.add(3);
            list.add(0);
            System.out.println(list);//[2, -5, 3, 0]
            System.out.println(Collections.max(list));//3
            System.out.println(Collections.min(list));//-2
            System.out.println(Collections.replaceAll(list,0,1));//true
            System.out.println(list);//[2, -5, 3, 1]
            System.out.println(Collections.frequency(list,-5));//1
            Collections.sort(list);
            System.out.println(Collections.binarySearch(list,3));//3
        }
    }

    同步控制

      Collections类中提供了多个synchronizedXxx()方法,该方法可以指定将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

      Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayQeque、LinkedList、HashMap和TreeMap都是线程不安全的。如果有多个线程访问他们,有超过一个线程试图修改他们,就会引发线程安全问题。

     

  • 相关阅读:
    【Linux】【Services】【SaaS】Docker+kubernetes(2. 配置NTP服务chrony)
    【Linux】【Services】【DNS】使用Bind搭建DNS服务
    【Linux】【Services】【SaaS】Docker+kubernetes(1. 基础概念与架构图)
    【Services】【Web】【LVS】lvs基础概念
    jquery.cookie.js 配置
    SQL Server2008 TIME类型
    Ext.MessageBox
    Ext.Form 自动填写表单内容
    Ext 中xtype一览
    ExtGrid
  • 原文地址:https://www.cnblogs.com/yfstudy/p/13415433.html
Copyright © 2020-2023  润新知