一文汇总Java集合应用


上面是一个详细的集合框架图,下面是常用的集合类的关系图


1、List

List集合是一个元素有序(每个元素都有对应的顺序索引,第一个元素索引为0)、且可重复的集合。

List集合常用方法
  • void add(int index, E element);:将元素element插入到List集合的index处;
  • boolean addAll(int index, Collection<? extends E> c);:将集合c插入到List集合的index起始处;
  • E remove(int index);:移除并返回index处的元素;
  • int indexOf(Object o);:返回对象o在List集合中第一次出现的位置索引;
  • int lastIndexOf(Object o);:返回对象o在List集合中最后一次出现的位置索引;
  • E set(int index, E element);:将index索引处的元素替换为新的element对象,并返回被替换的旧元素
  • E get(int index);:返回集合index索引处的对象;
  • List<E> subList(int from, int to);:返回从from(包含)到to(不包含)组成的子集合;
  • void sort(Comparator<? super E> c) :根据Comparator参数对List集合元素进行排序;
  • void replaceAll(UnaryOperator<E> operator) :根据operator指定的计算规则重新设置集合的所有元素。
  • ListIterator<E> listIterator();:返回一个ListIterator对象,该接口继承了Iterator接口,在Iterator接口基础上增加了以下方法,具有向前迭代功能且可以增加元素:
    bookean hasPrevious():返回迭代器关联的集合是否还有上一个元素;
    E previous();:返回迭代器上一个元素;
    void add(E e);:在指定位置插入元素;
  • 下面是菜鸟教程中的链接
  • clear() 删除 arraylist 中的所有元素
    clone() 复制一份 arraylist
    contains() 判断元素是否在 arraylist
    removeAll() 删除存在于指定集合中的 arraylist 里的所有元素
    size() 返回 arraylist 里元素数量
    isEmpty() 判断 arraylist 是否为空
    toArray() 将 arraylist 转换为数组
    toString() 将 arraylist 转换为字符串
    ensureCapacity() 设置指定容量大小的 arraylist
    lastIndexOf() 返回指定元素在 arraylist 中最后一次出现的位置
    retainAll() 保留 arraylist 中在指定集合中也存在的那些元素
    containsAll() 查看 arraylist 是否包含指定集合中的所有元素
    trimToSize() 将 arraylist 中的容量调整为数组中的元素个数
    removeRange() 删除 arraylist 中指定索引之间存在的元素
    removeIf() 删除所有满足特定条件的 arraylist 元素
    forEach()遍历 arraylist 中每一个元素并执行特定操作

1.1、ArrayList

ArrayList概述

   ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess(随机访问), Cloneable(克隆), java.io.Serializable)这些接口。
  ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了添加、删除、修改、遍历等功能。
  ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们可以通过元素的序号快速获取元素对象;这就是快速随机访问
  ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
  和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList

ArrayList源码解析
  1. 1. ArrayList包含了两个重要的对象:elementDatasize
    elementData是"Object[]类型的数组",它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData的容量默认是10。elementData数组的大小会根据ArrayList容量的增长而动态的增长。size则是动态数组的实际大小。
// 默认初始化容量为10
    private static final int DEFAULT_CAPACITY = 10;
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA默认为空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 才开始构造的时候是一个空list,只有当第一个元素add的时候,扩展到DEFAULT_CAPACITY值,即长度为10.
    transient Object[] elementData; 
// 构造成10长度的空list;   
    public ArrayList() { 
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 
    } 
  1. 2. ArrayList 是使用数组去保存数据的。当我们构造ArrayList时;使用默认构造函数,ArrayList的默认大小是10
  2. 3. 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。
  3. 4. ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
  4. 5. ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
ArrayList的三种遍历方式
public class TestOne {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(2);
        list.add(5);
        demo(list);
    }
    public static void demo(List<Integer> list){
        //第一种,增强for循环
        for (Integer l : list) {
            System.out.println(l);
        }
        System.out.println("-------------");
        //第二种,普通for循环随机访问index
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        System.out.println("-------------");
        //第三种,迭代器
        Iterator<Integer> it = list.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
        }
    }
}

遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低。

1.2、LinkedList

LinkedList概述

  LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList的本质是双向链表。1)LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。2) LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量:previous, next, element。(其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。)
size是双向链表中节点的个数。
  LinkedList 实现 List接口,能对它进行队列操作。
  LinkedList 实现 Deque接口,即能将LinkedList当作双端队列使用。
  LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  LinkedList 是非同步的。(若要实现同步 List list = Collections.synchronizedList(new LinkedList(...));)

LinkedList源码分析
  1. 1. 访问性
    LinkedList实际上是通过双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低
  2. 2. 根据索引值操作
    既然LinkedList是通过双向链表的,但是它也实现了List接口,也就是说,它实现了get(int index)remove(int index)等根据索引值来获取、删除节点的函数。
  3. 3. LinkedList是如何实现List的这些接口的,如何将双向链表和索引值联系起来的?其实,它是通过一个计数索引值来实现的。例如,当程序调用get(int index)方法时,首先会比较location双向链表长度的1/2;如果前者大,则从链表头开始向后查找,直到location位置;否则,从链表末尾开始向前查找,直到location位置

总结

  1. 1. LinkedList 实际上是通过双向链表去实现的。包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值上一个节点下一个节点
  2. 2. LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  3. 3. LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  4. 4. 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。
LinkedList的多种遍历方式

除了上述的和ArrayList一样的三种遍历方式之外,还可以通过对应的方法遍历。

  1. 4. 通过pollFirst()来遍历LinkedList,获取并移除此列表的第一个元素;如果此列表为空,则返回 null
while(list.pollFirst() != null){

}
  1. 5. 通过pollLast()来遍历LinkedList,获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
while(list.pollLast() != null) {

}
  1. 6. 通过removeFirst()来遍历LinkedList,移除并返回此列表的第一个元素
try {
        while(list.removeFirst() != null) {
	}
    } catch (NoSuchElementException e) {
}
  1. 7. 通过removeLast()来遍历LinkedList,移除并返回此列表的最后一个元素
try {
    while(list.removeLast() != null) {
     }
    } catch (NoSuchElementException e) {
}

注意:建议不要采用通过下标随机访问的方式去遍历LinkedList,而采用增强for循环

常见问题

ArrayList底层动态扩容的原理?

  ArrayList底层采用数组实现,当使用不带参数的构造方法生成ArrayList对象时,底层实际会生成一个长度为10Object类型数组,如果增加的元素个数超过10个,则ArrayList底层会新生成一个数组,长度为原数组的1.5倍+1,然后将原数组的内容复制到新数组中去,后续增加的内容都会放入新数组中,当新数组无法容纳新元素时,重复上述步骤。

ArrayList和LinkedList的区别?
  1. 底层实现:ArrayList实现是基于动态数组的数据结构(新建一个数组进行扩容,然后copy原来数组中内容,实现数组可增长);LinkedList是基于双向链表的数据结构,其每个对象除了数据本身外,还有两个引用,分别指向前一个元素和后一个元素。
  2. 查询:对于随机访问get和set,ArrayList支持;LinkedList不支持,因为LinkedList要移动指针。
  3. 增删:对于新增和删除操作add和remove,在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
  4. 应用场景:ArrayList适合一列数据的后面添加数据而不是在前面或中间,且需要随机访问元素;LinkedList适合在一列数据的前面或中间添加或删除数据,且按照顺序访问其中的元素。
  5. 消耗内存:LinkedList比ArrayList消耗更多的内存,LinkedList中的每个节点都存储前后节点的引用。(双向链表)
ArrayList和Vector的区别?
  1. 线程安全性:ArrayList是非线程安全的,Vector是线程安全的,如果需要再迭代的时候对列表进行改变,使用CopyOnWriteArrayList。
  2. 效率:ArrayList是非同步的,效率高;Vector是同步的,效率低;
ArrayList和CopyOnWriteArrayList的区别
  1. 和ArrayList继承于AbstractList不同,CopyOnWriteArrayList没有继承于AbstractList,它只是实现了List接口。
  2. ArrayList的iterator()函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList是自己实现Iterator。
  3. ArrayList的Iterator实现类中调用next()时,会“调用checkForComodification()比较'expectedModCount'和'modCount'的大小”;但是,CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常!
Iterater和ListIterator区别
  1. 遍历目标:可以使用Iterator来遍历Set和List集合;而ListIterator只能遍历List。
  2. 遍历方向:Iterator只可以后向顺序遍历;而ListIterator可以双向遍历。
  3. 功能区别:ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

2、Set

Set集合的概念

  Set集合类似于一个容器,程序把很多对象保存到Set集合中,Set集合对添加顺序不记录,当有重复的对象保存到Set集合时,不会新增后加的重复对象。

Set集合的特点
  1.        Set集合无重复元素,add()方法添加相同元素时,返回false;
  2.        Set集合add()方法不记录顺序;
  3. 2.1、HashSet

  4. HashSet介绍

      HashSet是按照哈希算法进行存储元素的,具有良好的查询和存取性能。

    HashSet特点
  5. 集合元素值可以为null;
  6. 不保证元素的排列顺序,有可能排列顺序与添加顺序不同;
  7. 非同步集合,多线程访问HashSet时,是不安全的,需要通过同步代码保证同步。
  8. 元素不可重复相同,通过equals()hashCode()方法一起判断是否相同。
HashSet添加元素过程

add()方法:

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

  通过add()方法向HashSet存入元素时,HashSet会调用该对象的hashCode()方法获取hashCode值,然后根据hashCode值决定这个元素在HashSet的存储位置。如果有两个元素通过equals()方法返回true,但hashCode()方法返回值不同,则HashSet会存储到集合的不同位置,依旧可以成功添加该元素。

示例1:HashSet中元素对象的相同性依据

1)创建ClassA类,重写equals()方法

public class ClassA {
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

2)创建ClassB类,重写hashCode()方法

public class ClassB {
    @Override
    public int hashCode() {
        return 0;
    }
}

3)创建ClassC类,重写equals()hashCode()方法

public class ClassC {
    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object obj) {
        return true;
    }
}

4)测试主类:

public class DemoApplication {
    public static void main(String[] args) {
	// 创建集合
        HashSet hashSet = new HashSet();
        // 添加元素
        hashSet.add(new ClassA());
        hashSet.add(new ClassA());
        hashSet.add(new ClassB());
        hashSet.add(new ClassB());
        hashSet.add(new ClassC());
        hashSet.add(new ClassC());

        System.out.println("hashSet: ");
        hashSet.forEach(obj -> System.out.println(obj));
        }
}

5)运行结果:

hashSet: 
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassB@0
com.example.andya.demo.bean.ClassC@1
com.example.andya.demo.bean.ClassA@1edf1c96
com.example.andya.demo.bean.ClassA@368102c8

  从上述运行结果可以看出:
1)HashSet集合添加顺序和集合内部元素顺序不一定相同;
2)HashSet添加元素时:

  • ClassA类重写了equals()方法,但是两个对象的hashCode()返回了不同的hashCode值,所以HashSet会将这两个对象保存在哈希表中的不同位置。
  • ClassB类重写了hashCode()方法,但是两个对象的equals()方法返回的是不同的对象地址,所以HashSet会将这两个对象保存到同一个位置,并通过链表链接。这种方式不建议有,因为集合中出现链式结构来存储相同hash值的元素时,查询速度会变慢,性能下降。
  • 只有ClassC是当作一个对象来添加到集合中,只有当equals()hashCode()方法都重写的时候,才可以作为判断对象是否相同的依据。

3)总结:若需要将某个类的对象保存到HashSet集合时,我们需要重写这个类的equals()hashCode()方法,只有保证了两个对象通过equals()方法返回true,hashCode()方法返回值相同时,才是相同对象。

重写hashCode()方法的准则
  1. 可重复性:程序运行时,同一个对象多次调用hashCode()方法返回的是相同值;
  2. 当两个对象通过equals()方法比较后返回的是true,则两个对象的hashCode()方法应返回相同的hash值;
  3. 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode值;
HashSet集合为何查询速度快?

  我们回答这个问题前,先看一下HashSet添加元素的流程,使用add()方法添加元素时,HashSet会根据这个元素的hashCode值计算存储位置进行存储。当我们查询元素时,也是通过该元素的hashCode值快速定位到该元素在集合中的位置,其实HashSet底层是通过HashMap实现的。可以观察到源码:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    private transient HashMap<E,Object> map;
    //构造方法
    public HashSet() {
        map = new HashMap<>();
    }
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    //add方法
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}

2.2、LinkedHashSet

LinkedHashSet介绍

  LinkedHashSet是HashSet子类,同样是根据元素的hashCode值判断元素的存储位置,且同时使用链表维护元素的顺序,给人的直观效果就是集合的顺序就是元素插入的顺序保存的。
  下面我们通过一组示例来看一下LinkedHashSet的保存元素次序效果。

LinkedHashSet示例

1)主类

public class DemoApplication {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add("1");
        linkedHashSet.add("2");
        linkedHashSet.add(3);
        System.out.println("旧的集合:" + linkedHashSet);
        linkedHashSet.remove("1");
        linkedHashSet.add(1);
        System.out.println("新的集合:" + linkedHashSet);
        }
}

2)运行结果

旧的集合:[1, 2, 3]
新的集合:[2, 3, 1]

  从上述运行结果中,我们可以看到LinkedHashSet的元素保存顺序即为添加元素的顺序。

2.3、TreeSet

TreeSet介绍

  TreeSet是SortedSet接口实现类,TreeSet是一种排序集合,该对象中只能添加一种类型元素,否则,会抛出java.lang.ClassCastException异常;
  与HashSet集合采用hash算法来决定元素的存储位置有些不一样,TreeSet是采用红黑树数据结构存储集合元素。

TreeSet方法

除了Collection集合的常用方法外,TreeSet集合还有如下常用方法:

  • Comparator<? super E> comparator():如果TreeSet采用定制排序,该方法返回定制排序所使用的Comparator,如果TreeSet采用自然排序,则返回null;
  • E first():返回集合中的第一个元素;
  • E last():返回集合中的最后一个元素;
  • E lower(E e):返回集合找找那个微语指定元素之前的元素(小于指定元素的最大元素)
  • E higher(E e):返回集合中位于指定元素之后元素(大于指定元素的最小元素)
  • SortedSet<E> subSet(E from, E to) :返回Set的子集合,从from(包含)到to(不包含)
  • SortedSet<E> headSet(E toElement):返回此Set的子集,该子集是由小于toElement的元素组成;
  • SortedSet<E> tailSet(E toElement):返回此Set的子集,该子集是由大于toElement的元素组成;
示例

1)运行主类:

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(2);
        treeSet.add(1);
        treeSet.add(6);
        treeSet.add(3);
        treeSet.add(8);
        System.out.println(treeSet);
        //查看排序性
        treeSet.remove(3);
        treeSet.add(5);
        System.out.println(treeSet);
        System.out.println("first()方法: " + treeSet.first());
        System.out.println("last()方法: " + treeSet.last());
        System.out.println("lower()方法: " + treeSet.lower(5));
        System.out.println("higher()方法: " + treeSet.higher(5));
        System.out.println("subSet()方法: " + treeSet.subSet(2,6));
        System.out.println("headSet()方法: " + treeSet.headSet(5));
        System.out.println("tailSet()方法: " + treeSet.tailSet(5));
        }
}

2)运行结果:

[1, 2, 3, 6, 8]
[1, 2, 5, 6, 8]
first()方法: 1
last()方法: 8
lower()方法: 2
higher()方法: 6
subSet()方法: [2, 5]
headSet()方法: [1, 2]
tailSet()方法: [5, 6, 8]

  从上述运行结果可以看出,TreeSet不是根据添加元素的顺序进行排序,而是通过元素实际大小进行排序。

TreeSet的compareTo()方法详解

  TreeSet支持两种排序方式:自然排序自定义排序(定制排序)

自然排序

介绍:
  TreeSet是通过Comparable接口的compareTo(Object obj)方法比较元素之间的大小关系,然后将元素按照升序排列,即为自然排序。
实现原理:
  实现Comparable接口,并实现compareTo(Object obj)方法,返回一个整数值。当一个对象调用该方法与另一个对象进行比较时,如obj1.compareTo(obj2),该方法若返回0,则表明obj1和obj2这两个对象相等;若返回一个正整数,则表明obj1大于obj2;若返回一个负整数,则表明obj1小于obj2。然后根据红黑树结构寻找存储位置,当元素相同,若此时使用集合add()方法时,无法将新对象添加到集合内。
  TreeSet通过元素对应的类重写的compareTo(Object obj)方法来比较对象大小,就要求重写元素对象对应类的equals()方法时,需要保证该方法与compareTo(Object obj)方法保持一致的结果,当两个对象通过equals()方法返回true时,两个对象通过compareTo(Object obj)方法应该返回0。

示例
1)对象类

public class OnePerson implements Comparable<OnePerson>{
    private int age;
    private String name;
    public OnePerson(int age, String name) {
        this.age = age;
        this.name = name;
    }
    //重写equals()方法与compareTo()方法保持一致的效果
    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj != null && obj.getClass() == OnePerson.class) {
            OnePerson onePerson = (OnePerson) obj;
            return onePerson.age == this.age;
        }
        return  false;
    }
    //重写compareTo()方法,以age作为排序比较标准
    @Override
    public int compareTo(OnePerson o) {
        return this.age > o.age
                ? 1 : this.age < o.age
                ? -1 : 0;
    }

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

2)运行类

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

        OnePerson onePerson1 = new OnePerson(5, "xiaoming");
        OnePerson onePerson2 = new OnePerson(3, "yaoyao");
        OnePerson onePerson3 = new OnePerson(8, "huangming");
        
        treeSet.add(onePerson1);
        treeSet.add(onePerson2);
        treeSet.add(onePerson3);
        
        treeSet.forEach(onePerson -> System.out.println(onePerson));
        }
}

3)运行结果

OnePerson{age=3, name='yaoyao'}
OnePerson{age=5, name='xiaoming'}
OnePerson{age=8, name='huangming'}

  从上述运行结果看出,对象类OnePerson添加到TreeSet时是按照age大小进行排序添加进集合。

自定义排序

介绍:
  自然排序是根据元素大小进行升序排列,如果需要实现降序等排列需要自定义排序,通过Comparator接口,该接口内有int compare(T o1, T o2)方法,使用该方法比较o1和o2的大小:若返回正整数,则表示o1>o2;若返回负整数,则表示o1<o2;若返回0,则表示o1==o2.
实现原理:
  创建TreeSet集合对象时,提供一个Comparator对象与该集合进行关联,在Comparator对象中实现集合元素的排序逻辑。

示例
1)对象类

public class OnePerson2{
    private int age;
    private String name;

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

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

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

2)运行主类:
Lambda方式

public class DemoApplication {
    public static void main(String[] args) {
    	TreeSet treeSet = new TreeSet((o1, o2) ->
        {
            OnePerson2 onePerson1 = (OnePerson2)o1;
            OnePerson2 onePerson2 = (OnePerson2)o2;
            //根据OnePerson对象的age属性来判断大小,age越大,OnePerson对象越小,降序
            return  onePerson1.getAge() > onePerson2.getAge() ? -1
                : onePerson1.getAge() < onePerson2.getAge() ? 1 : 0;
        });

        treeSet.add(new OnePerson2(5, "xiaohong"));
        treeSet.add(new OnePerson2(2, "huangming"));
        treeSet.add(new OnePerson2(9, "yaoling"));

        treeSet.forEach(onePerson -> System.out.println(onePerson));
        }
}

compare()方式

public class DemoApplication {
    public static void main(String[] args) {
        TreeSet<OnePerson2> treeSet = new TreeSet(new Comparator<OnePerson2>() {
            @Override
            public int compare(OnePerson2 o1, OnePerson2 o2) {
                return o1.getAge() > o2.getAge() ? -1
                        : o1.getAge() < o2.getAge() ? 1 : 0;
            }
        });
        treeSet.add(new OnePerson2(5, "xiaohong"));
        treeSet.add(new OnePerson2(2, "huangming"));
        treeSet.add(new OnePerson2(9, "yaoling"));

        treeSet.forEach(onePerson -> System.out.println(onePerson));
    }
}

3)运行结果

OnePerson2{age=9, name='yaoling'}
OnePerson2{age=5, name='xiaohong'}
OnePerson2{age=2, name='huangming'}

  从上述运行结果看出,通过Comparator接口的lambda表达式实现了自定义降序。

2.4、EnunSet

EnumSet介绍

  EnumSet是一个枚举类的集合类,集合内的所有元素都必须是指定枚举类型的枚举值,枚举类型在创建EnumSet时显式或者隐式地指定;EnumSet也是有顺序的,该顺序是由枚举值在Enum类中的定义顺序决定。
  EnumSet不允许加入null元素,若尝试添加null元素,会抛出NullPointerException异常。

EnumSet常用方法
  • EnumSet<E> allOf(Class<E> elementType):创建一个包含指定枚举类全部枚举值的EnumSet集合。
  • EnumSet<E> noneOf(Class<E> elementType):创建一个元素类型为指定枚举类型的空EnumSet集合。
  • EnumSet<E> of(E first, E... rest) :创建同一类型枚举值的一个或多个枚举值的EnumSet集合。
  • EnumSet<E> range(E from, E to):创建一个包含从from到to枚举值范围内所有枚举值的EnumSet集合。
  • EnumSet<E> complementOf(EnumSet<E> s):创建一个元素类型和指定的EnumSet集合相同的EnumSet集合,新EnumSet集合包含原EnumSet集合不包含的,新的EnumSet集合和原集合所有元素加起来是枚举类的所有枚举值。
  • EnumSet<E> copyOf(EnumSet<E> s):创建一个与指定EnumSet集合具有相同元素类型、相同元素值的集合。
  • EnumSet<E> copyOf(Collection<E> c) :使用一个普通集合创建EnumSet集合。但是该普通集合中的元素必须是枚举值,否则会抛出ClassCastException异常。
示例

1)枚举类

public enum ColourEnum {
    RED,
    ORANGE,
    YELLOW,
    GREEN,
    BLUE,
    INDIGO,
    PURPLE
}

2)运行主类

public class DemoApplication {
    public static void main(String[] args) {
        //allOf()获取枚举类的全部枚举值
        EnumSet<ColourEnum> colourEnumEnumSet = EnumSet.allOf(ColourEnum.class);
        System.out.println("allOf()方法:" + colourEnumEnumSet);
        //创建空集合,指定集合元素是ColourEnum类的枚举值
        EnumSet<ColourEnum> colourEnumEnumSet1 = EnumSet.noneOf(ColourEnum.class);
        colourEnumEnumSet1.add(ColourEnum.GREEN);
        colourEnumEnumSet1.add(ColourEnum.RED);
        System.out.println("noneOf()方法:" + colourEnumEnumSet1);
        //指定枚举值创建枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet2 = EnumSet.of(ColourEnum.BLUE, ColourEnum.ORANGE);
        System.out.println("of()方法:" + colourEnumEnumSet2);
        //指定范围创建枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet3 = EnumSet.range(ColourEnum.ORANGE, ColourEnum.INDIGO);
        System.out.println("range()方法:" + colourEnumEnumSet3);
        //补充枚举值,合集是整个ColourEnum
        EnumSet<ColourEnum> colourEnumEnumSet4 = EnumSet.complementOf(colourEnumEnumSet3);
        System.out.println("complementOf()方法:" + colourEnumEnumSet4);
        //复制枚举集合
        EnumSet<ColourEnum> colourEnumEnumSet5 = EnumSet.copyOf(colourEnumEnumSet4);
        System.out.println("copyOf()方法:" + colourEnumEnumSet5);
    
    }
}

3)运行结果

allOf()方法:[RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PURPLE]
noneOf()方法:[RED, GREEN]
of()方法:[ORANGE, BLUE]
range()方法:[ORANGE, YELLOW, GREEN, BLUE, INDIGO]
complementOf()方法:[RED, PURPLE]
copyOf()方法:[RED, PURPLE]

2.5、各种Set对比

1)HashSet性能比TreeSet好(添加、查询),只有当需要保持排序的Set时,才使用TreeSet,否则使用HashSet。
2)HashSet比LinkedHashSet性能好(添加、删除)。
3)LinkedHashSet比HashSet性能好(遍历查询)。
4)EnumSet是所有Set性能最好的,但缺陷是只能保存同一个枚举类的枚举值作为集合元素。
5)HashSet、TreeSet和EnumSet都是线程不安全的,需要通过Collections工具类的synchronizedSet(Set<T> s)synchronizedSortedSet(SortedSet<T> s)方法来包装Set集合。

3、Map

Map集合介绍

  Map(也称为字典、关联数组)是用于保存具有映射关系的数据,保存两组值,keyvalue,这两组值可以是任何应用类型的数据。
  Map的key不允许重复(底层Map的keySet()返回的是key的Set集合,所以key不会重复),即Map中对象的任意两个key通过equals()方法得到的都是false。而,Map的value值是可以重复的(Map的底层values()方法返回类型是Collection,可以存储重复元素),通过key总能找到唯一的value,Map中的key组成一个Set集合,所以可以通过keySet()方法返回所有key。Set底层也是通过Map实现的,只不过value都是null的Map来实现的。

下面是HashSet的底层源码:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E,Object> map;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }   
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }  
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}
Map实现类

  Map典型的实现类是HashMap、Hashtable(HashMap子类还有LinkedHashMap)、SortedMap子接口及实现类TreeMap、WeakHashMap、IndentityHashMap等。
  Map有一个内部类Entry,该类封装了key-value对,有如下三个方法:

  • K getKey();:获取Entry中的key值;
  • V getValue();:获取Entry中的value值;
  • V setValue(V value);:设置Entry中的value值,并返回新设置的value值。
Map常用方法
  • int size();:返回Map的key-value对的长度。
  • boolean isEmpty();:判断该Map是否为空。
  • boolean containsKey(Object key);:判断该Map中是否包含指定的key。
  • boolean containsValue(Object value);:判断该Map是否包含一个或多个value。
  • V get(Object key);:获取某个key所对应的value;若不包含该key,则返回null。
  • V put(K key, V value);:向Map添加key-value对,当Map中有与该key相等的key-value对,则覆盖旧的
  • V remove(Object key);:移除指定的key所对应的key-value对,若成功删除,则返回移除的value值。
  • void putAll(Map<? extends K, ? extends V> m);:将指定的Map中的key-value对全部复制到该Map
  • void clear();:清除Map中的所有key-value对。
  • Set<K> keySet();:获取该Map中所有key组成的Set集合。
  • Collection<V> values();:获取该Map中所有value组成的Collection。
  • Set<Map.Entry<K, V>> entrySet();:返回该Map中Entry类的Set集合。
  • boolean remove(Object key, Object value) :删除指定的键值对,成功,返回true;否则返回false

3.1、HashMap

HashMap底层是数组+链表的形式,实现Map.Entry接口,数组是Entry[]数组,是一个静态内部类,Entry是key-value键值对,持有一个指向下一个元素的next引用,这就构成链表(单向链表)。
  HashMap底层是数组和链表的结合体。底层是一个线性数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。数组是Entry[]数组,静态内部类。Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用next,这就构成了链表。根据指定的hash值找到在table中的索引;HashMap底层数组的长度是2^n,默认是16,负载因子为0.75,所以最大容量阈值threshold = (int)(capacity * loadFactor);16*0.75=12,当超过这个阈值的时候,开始扩容,即每次扩容增加一倍。

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 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
    }
底层读写元素原理


HashMap类中有两个常量:

1
2
static final int TREEIFY_THRESHOLD = 8
static final int UNTREEIFY_THRESHOLD = 6
当链表中节点数量大于等于TREEIFY_THRESHOLD时,链表会转成红黑树。

当链表中节点数量小于等于UNTREEIFY_THRESHOLD时,红黑树会转成链表。

为什么TREEIFY_THRESHOLD的默认值被设定为8?

HashMap中有这样一段注释

Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million

意思就是HashMap节点分布遵循泊松分布,按照泊松分布的计算公式计算出了链表中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小。

另一方面红黑树平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是链表和红黑树之间的转换也很耗时。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。

put实现原理
当我们往HashMap中put元素的时候:当程序试图将一个key-value对放入HashMap中时,

  1. 程序首先根据该 key 的hashCode() 返回值再hash,决定该 Entry 的存储位置;
  2. 若Entry的存储位置上为null,直接存储该对象;若不为空,两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。
  3. 循环遍历链表,如果这两个 Entry 的 key 通过equals() 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的value,但key不会覆盖;如果这两个 Entry 的 key 通过equals()比较返回 false,将该对象放到数组中,然后将数组中原有的Entry对象链接到此对象后面。新添加的 Entry 将与集合中原有 Entry 形成Entry 链,而且新添加的Entry位于 Entry 链的头部

get实现原理
从HashMap中get元素时,

  1. 首先计算key的hash值,通过再hash函数,找到数组中对应位置的某一元素;
  2. 然后通过key的equals()方法(key同不同)在对应位置的链表中找到需要的元素。
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
遍历Map的四种方式
public static void main(String[] args) {  
  Map<String, String> map = new HashMap<String, String>();  
  map.put("1", "value1");  
  map.put("2", "value2");  
  map.put("3", "value3");      
  //第一种:普遍使用,二次取值  
  System.out.println("通过Map.keySet遍历key和value:");  
  for (String key : map.keySet()) {  
   System.out.println("key= "+ key + " and value= " + map.get(key));  
  }      
  //第二种  
  System.out.println("通过Map.entrySet使用iterator遍历key和value:");  
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();  
  while (it.hasNext()) {  
   Map.Entry<String, String> entry = it.next();  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }      
  //第三种:推荐,尤其是容量大时  
  System.out.println("通过Map.entrySet遍历key和value");  
  for (Map.Entry<String, String> entry : map.entrySet()) {  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }  
  //第四种  
  System.out.println("通过Map.values()遍历所有的value,但不能遍历key");  
  for (String v : map.values()) {  
   System.out.println("value= " + v);  
  }  
 }  

3.2、HashTable

  Hashtable是和HashMap一样,属于Map典型的实现类,区别于HashMap的是,Hashtable是线程安全的Map实现,但是性能低。Hashtable不允许使用null作为key和value;若将null值存入Hashtable,会抛出NullPointerException异常;而HashMap可以使用null作为key或value。

3.3、LinkedHashMap

  LinkedHashMap是HashMap的子类,使用双向链表维护key-value对的顺序(只是关注key的顺序),迭代顺序和key-value插入Map中的顺序保持一致。

3.4、Properties

  Properties类是Hashtable类的子类。通常作为处理属性配置文件比较好。可以将Map对象中的key-value配置写入到属性文件中,反之,也可以将属性文件中的key=value加载到Map对象中。注意,属性文件中的属性key-value只能是字符串类型。提供以下几个方法来操作Properties

  • synchronized Object setProperty(String key, String value):设置属性值,底层是通过put来实现。
    public synchronized Object setProperty(String key, String value) {
        return put(key, value);
    }
  • String getProperty(String key):获取指定属性名的属性值,底层是通过Map的get(Object key)来实现。
    public String getProperty(String key) {
        Object oval = super.get(key);
        String sval = (oval instanceof String) ? (String)oval : null;
        return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
    }
  • String getProperty(String key, String defaultValue):类似于getProperty(String key)方法,在此基础上多一个功能是党Properties中不存在指定的key时,该方法指定默认值去获取。
  • synchronized void load(InputStream inStream) throws IOException :从属性文件以输入流的方式加载key-value对,并将这些key-value追加到Properties中。
  • void store(OutputStream out, String comments):将Properties中的key-value对以输出流的方式输出到指定的属性文件中。
  • 3.5、TreeMap

  TreeMap是SortedMap接口的实现类,TreeMap底层是红黑树数据结构,每个key-value作为红黑树的一个节点。TreeMap存储节点时,根据key对节点进行排序,主要是自然排序和自定义排序。类似于TreeSet。

3.6、WeakHashMap

  WeakHashMap用法基本和HashMap类似,不同的是WeakHashMap是对实际对象的弱引用,弱引用就是当WeakHashMap的key所引用的对象没有被其他强引用变量进行引用时,key所对应的对象就可能会被垃圾回收,WeakHashMap会自动删除该key所对应的key-value对。而HashMap的key是保留实际对象的强引用,强引用就是当HashMap对象不被销毁的时候,HashMap所有key所引用的对象就不会被垃圾回收。

3.7、IdentityHashMap

  IdentityHashMap也基本类似于HashMap,有一些特殊的地方在于:判断两个key相等。HashMap中判断key相等只需要判断两个key通过equals()方法比较返回true,而IdentityHashMap是仅当两个key通过(key1==key2)相等时才认为相等。IdentityHashMap同样支持null作为key和value。

3.8、EnumMap

  EnumMap是Map的枚举实现类,所有key都必须是单个枚举类的枚举值。创建EnumMap的时候必须显式或者隐式指定对应的枚举类。
  EnumMap在内部是以数组的形式保存元素,根据key的自然顺序,即枚举值在枚举类中定义的顺序来保存key-value对的顺序。EnumMap不允许null作为key,但是可以使用null作为value。

3.9、常见问题

HashMap的扩容resize原理,如何扩容?

  HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table。当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化,当添加完元素后,当HashMap内的元素对象存储的爆满时,就容易出现hash冲突的现象,这个时候就需要底层数组扩容。当然,HashMap这种扩容的操作也是比较消耗性能的,因为这需要原数组中的数据重新计算其在新数组中的位置。
  扩容的时机是什么?当HashMap中的元素个数超过数组大小时(即负载因子loadFactor),HashMap底层就会进行数组的扩容,loadFactor的默认值为0.75,这是一个折中的取值。数组大小默认为16,当HashMap中元素对象个数超过16*0.75=12时,就把数组的大小扩容到2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,因此实际使用的时候,若已知晓HashMap大概需要存储多少元素时,则预设元素的个数就可以提高HashMap的性能。

扩容过程:

若threshold(阈值)不为空,table的首次初始化大小为阈值,否则初始化为缺省值大小16
默认的负载因子大小为
0.75,当一个map填满了75%的bucket时候(即达到了默认负载因子0.75),就会扩容,扩容后的table大小变为原来的两倍(扩容后自动计算每个键值对位置,且长度必须为16或者2的整数次幂)

若不是16或者2的幂次,位运算的结果不够均匀分布,显然不符合Hash算法均匀分布的原则。

反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

假设扩容前的table大小为2的N次方,元素的table索引为其hash值的后N位确定扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing

因此,table中的元素只有两种情况:
元素hash值第N+1位为0:不需要进行位置调整
元素hash值第N+1位为1:将当前位置移动到 
原索引+未扩容前的数组长度  的位置
扩容或初始化完成后,resize方法返回新的table

折中:负载因子loadFactor衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表空间利用率高。查找一个元素的平均时间为o(1+a),当负载因子变大的时候,空间利用率高了,但是查询效率降低;当负载因子变小的时候,元素就会稀疏,但是查询效率高,所以要折中时间和空间

HashMap的存储结构?

1.7版本:数组+链表

1.8版本:数组+链表+红黑树

图中,紫色部分即代表哈希表,也称为哈希数组(默认数组大小是16,每对key-value键值对其实是存在map的内部类entry里的),数组的每个元素都是一个单链表的头节点,跟着的绿色链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就会采用头插法将其放入单链表中。

解决hash冲突的方法有哪些?
  1. 开放定址法:线性探测再散列、二次探测再散列、随机探测再散列;
  2. 再哈希法:使用不同的hash函数进行再次hash;
  3. 链地址法:在数组中冲突元素后面拉一条链路,存储重复的元素;
  4. 建立一个公共溢出区:其实就是建立一个表,存放那些冲突的元素。
HashMap中什么时候会产生冲突?

HashMap中调用hashCode()方法来计算hashCode。由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产生。

Java8中HashMap和LinkedHashMap如何解决冲突?
  1. Java8之前,HashMap和其他基于map的类都是通过链地址法解决冲突,它们使用单向链表存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。为了解决在频繁冲突时Hashmap性能降低的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)
  2. Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们。
  3. 在Java 7中为了优化常用类对ArrayList和HashMap采用了延迟加载的机制,在有元素加入之前不会分配内存,这会减少空的链表和HashMap占用的内存。
  4. 这种动态解决hash冲突的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。这一特性在所有基于hash table的类中没有,例如HashtableWeakHashMap只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的情况下使用平衡树。

  以上就是Java中HashMap如何处理冲突。这种方法被称为链地址法,因为使用链表存储同一桶内的元素。通常情况HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均采用这种方法处理冲突。从JDK 8开始,HashMap,LinkedHashMap和ConcurrentHashMap为了提升性能,在频繁冲突的时候使用平衡树来替代链表。因为HashSet内部使用了HashMap,LinkedHashSet内部使用了LinkedHashMap,所以他们的性能也会得到提升。

HashMap和TreeMap?
  1. 查询插入等用途:在Map中插入、删除和定位元素,HashMap适合;若要按顺序遍历键,则TreeMap适合。
  2. 优化:HashMap可以调优初始容量和负载因子;TreeMap没有调优选项,因为树总处于平衡状态。
  3. 实现接口:都实现了Cloneable接口。TreeMap实现SortMap接口,能够把它保存的记录根据键排序(默认按键的升序),HashMap继承AbstractMap;
  4. 底层实现TreeMap底层是数组+红黑树HashMap底层是数组+链表法(从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值(默认为8)便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。)TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)。HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是桶(数组),链表或红黑树。适用于在Map中插入、删除和定位元素。
    结论
    如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap
Hashmap和Hashset区别?

HashSet底层是通过HashMap实现的。add的时候,调用map的put方法,value始终是PRESENT,所以HashSet是所有value值都相同的HashMap

  1. 实现接口:HashSet实现了Set集合的接口不允许有重复的值(准确说应该是元素作为key,value是定义为final的对象),将对象存储在HashSet之前,需要确保对象已经重写了equals和hashCode方法,这样才能比较两个对象的值是否相等,确保set中没有存储相等的对象。HashMap实现了Map集合接口,对键值对进行映射。Map中不允许重复的键
  2. 存储元素:HashMap存储的是键值对,不允许有重复的键;Hashset存储的是对象,不能有重复的对象元素。
  3. 添加元素方法:HashMap使用put方法将元素放入map中,HashSet使用add方法将元素放入set中。
  4. hashCode值的计算:HashMap中使用键对象计算hashcode的值,HashSet中使用成员对象来计算hashcode值。
  5. 效率:HashMap比较快,因为使用唯一的键来获取对象,HashSet比HashMap慢。
  6. 底层实现HashSet是所有value值都相同的HashMap。HashSet内部使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个object而已。private transient HashMap<E,Object> map;只是包含了hashmap的key。
  7. HashMap的Put和Get原理?

put()原理:

1.根据key获取对应hash值:int hash = hash(key.hash.hashcode())

2.根据hash值和数组长度确定对应数组引int i = indexFor(hash, table.length);
简单理解就是
i = hash值%模以 数组长度(其实是按位与运算)。如果不同的key都映射到了数组的同一位置处,就将其放入单链表中。且新来的是放在头节点。

get()原理:

通过hash获得对应数组位置,遍历该数组所在链表(key.equals())

HashMap和Hashtable?


两者都是用key-value方式获取数据。
异:

  1. null值HashMap允许null值作为key和value;Hashtable不允许null的键值;
  2. 顺序性:HashMap不保证映射的顺序不变,但是作为HashMap的子类LinkedHashMap默认按照插入顺序来进行映射的顺序;Hashtable无法保证;
  3. 线程安全HashMap是非同步的(非线程安全),效率高;Hashtable是同步的(线程安全的),效率低。(HashMap同步通过Collections.synchronizedMap()实现)
  4. 快速失败机制:迭代HashMap采用fail-fast快速失败机制(快速失败机制:是一个线程或软件对于其故障做出的响应,用来即时报告可能会导致失败的任何故障情况,如果一个Iterator在集合对象上创建了,其他线程想结构化的修改该集合对象,抛出并发修改异常ConcurrentModificationException);而HashTable的enumerator迭代器不是fail-fast的(Hashtable的上下文同步:一个时间点只能有一个线程可以修改哈希表,任何线程在执行Hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放;
  5. 父类:HashMap继承AbstractMap,Hashtable继承Dictionary;
  6. 数组默认大小HashMap底层数组的默认大小是16,扩容是2*oldHashtable底层数组默认是11,扩容方式是2*old+1
  7. 效率HashMap是非线程安全的,单线程下效率高Hashtable是线程安全的,方法都加了synchronized关键字进行同步,效率较低
  8. 计算hash方式不同HashMap是二次hash,对key的hashCode进行二次hash,获得更好的散列值;而Hashtable是直接使用key的hashCode对table数组进行取模。
如何让HashMap同步?

  通过Collections集合工具类中的synchronizedMap()方法实现同步:Map map = Collections.synchronizedMap(hashMap);
Collections.synchronizedMap()实现原理是Collections定义了一个 SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的。

并发集合和普通集合?

并发集合常见的有 ConcurrentHashMapConcurrentLinkedQueueConcurrentLinkedDeque 等。并发集合位于 java.util.concurrent 包下,是 jdk1.5 之后才有的。
在 java 中有普通集合、同步(线程安全)的集合、并发集合。
普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。
线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。

注意:hashmap不是线程安全的,hashmap在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和ReHash(为key重新计算所在位置),而ReHash在并发的情况下可能会形成链表环。在执行get的时候,会触发死循环,引起CPU的100%问题。:jdk8已经修复hashmap这个问题了,jdk8中扩容时保持了原来链表中的顺序。但是HashMap仍是非并发安全,在并发下,还是要使用ConcurrentHashMap

ConcurrentHashMap的put()和get()原理?

put()原理:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.获取可重入锁

4.再次通过hash值,定位到Segment当中数组的具体位置。

5.插入或覆盖HashEntry对象。

6.释放锁。


get()原理:

1.为输入的Key做Hash运算,得到hash值。

2.通过hash值,定位到对应的Segment对象

3.再次通过hash值,定位到Segment当中数组的具体位置。


由此可见,和hashmap相比,ConcurrentHashMap在读写的时候都需要进行二次定位。先定位到Segment,再定位到Segment内的具体数组下标。

ConcurrentHashMap为何不支持null键和null值?

HashMap是支持null键和null值,而ConcurrentHashMap却不支持
查看源码如下:
HashMap:

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

ConcurrentHashMap:

2    final   V   putVal  (K   key, V   value, boolean   onlyIfAbsent) {
3        if (key == null || value == nullthrow  new  NullPointerException();
4        int  hash = spread(key.hashCode());
5        int  binCount = 0;
6        ......
7    }

原因:通过get(k)获取对应的value时,如果获取到的是null,此时无法判断它是put(k,v)的时候value为null,还是这个key从来没有做过映射(即没有找到这个key)。而HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)m.get(key),m可能已经不同了。

HashMap在JDK1.7和JDK1.8的区别?

ConcurrentHashMap1.7和1.8的区别?


1.8的实现已经抛弃了Segment分段锁机制,利用Node数组+CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。


CAS:

CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制。java.util.concurrent包中大量使用了CAS原理。
JDK1.8 中的CAS:
Unsafe类,在sun.misc包下,不属于Java标准。
Unsafe类提供一系列增加Java语言能力的操作,如内存管理、操作类/对象/变量、多线程同步等。其中与CAS相关的方法有以下几个:

1//var1为CAS操作的对象,offset为var1某个属性的地址偏移值,expected为期望值,var2为要设置的值,利用JNI来完成CPU指令的操作
2public  final  native  boolean  compareAndSwapObject(Object  var1, long  offset, Object  expected, Object  var2);
3public  final  native  boolean  compareAndSwapInt(Object  var1, long  offset, int  expected, int  var2);
4public  final  native  boolean  compareAndSwapLong(Object  var1, long  offset, long  expected, long  var2);

CAS缺点:

  • ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A->B->A 的过程。
    解决方案:添加版本号作为标识,每次修改变量值时,对应增加版本号;做CAS操作前需要校验版本号。JDK1.5之后,新增AtomicStampedReference类来处理这种情况。

  • 循环时间长开销大。如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销。

  • 只能对一个变量进原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。

注意:HashMap中的元素如果为类对象,则该对象需要重写equals()hashCode()方法。

千万不要这样使用Arrays.asList !

使用Arrays.asList()的原因无非是想将数组或一些元素转为集合,而你得到的集合并不一定是你想要的那个集合。

而一开始asList的设计时用于打印数组而设计的,但jdk1.5开始,有了另一个比较更方便的打印函数Arrays.toString(),于是打印不再使用asList(),而asList()恰巧可用于将数组转为集合。

一、错误用法
如果你这样使用过,那你可要注意了。
1、错误一
将基本类型数组作为asList的参数,或者对转换后的集合进行增删。
int []arr={1,2,3};
List list = Arrays.asList(arr);
System.out.println(list.size()); //输出的结果为1
//list.add(4);   //添加和删除会直接报错
//list.remove(1);
猜一下结果?

你是不是以为上面 ?那个 list 是 java.util.ArrayList ?

答案很确定:NO !

答案:由于Arrays.ArrayList参数为可变长泛型,而基本类型是无法泛型化的,所以它把int[] arr数组当成了一个泛型对象,所以集合中最终只有一个元素arr。由于asList产生的集合并没有重写add,remove等方法,所以它会调用父类AbstractList的方法,而父类的方法中抛出的却是异常信息。

2、错误二
将数组作为asList参数后,修改数组或List
        int []arr={1,2,3};
        List list = Arrays.asList(arr);
        arr[1]=33;
        list.set(1,44);
        System.out.println(list.toString());
        System.out.println(arr.toString()); 
猜一下输出结果?
答案:
由于asList产生的集合元素是直接引用作为参数的数组,所以当外部数组或集合改变时,数组和集合会同步变化,这在平时我们编码时可能产生莫名的问题。

验证

我们通过asList()源码可发现其原因,但为了更直观,我们先通过IDEA debug来看看结果。

        List list = Arrays.asList(arr);  //通过数组转换的list
        ArrayList lists = new ArrayList(list);  //真正的list

通过在这两句话进行调试可以发现:


其实Arrays.asList返回的是 java.util.Arrays.ArrayList,这个家伙是谁呢?

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;
        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
        @Override
        public int size() {
            return a.length;
        }
        @Override
        public Object[] toArray() {
            return a.clone();
        }
        @Override
        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            int size = size();
            if (a.length < size)
                return Arrays.copyOf(this.a, size,
                                     (Class<? extends T[]>) a.getClass());
            System.arraycopy(this.a, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }
        @Override
        public E get(int index) {
            return a[index];
        }
        @Override
        public E set(int index, E element) {
            E oldValue = a[index];
            a[index] = element;
            return oldValue;
        }
        @Override
        public int indexOf(Object o) {
            E[] a = this.a;
            if (o == null) {
                for (int i = 0; i < a.length; i++)
                    if (a[i] == null)
                        return i;
            } else {
                for (int i = 0; i < a.length; i++)
                    if (o.equals(a[i]))
                        return i;
            }
            return -1;
        }
        @Override
        public boolean contains(Object o) {
            return indexOf(o) != -1;
        }
        @Override
        public Spliterator<E> spliterator() {
            return Spliterators.spliterator(a, Spliterator.ORDERED);
        }
        @Override
        public void forEach(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            for (E e : a) {
                action.accept(e);
            }
        }
        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            Objects.requireNonNull(operator);
            E[] a = this.a;
            for (int i = 0; i < a.length; i++) {
                a[i] = operator.apply(a[i]);
            }
        }
        @Override
        public void sort(Comparator<? super E> c) {
            Arrays.sort(a, c);
        }
    }

但它和ArrayList貌似很像唉!有什么不同吗?

不同之处

Arrays.ArrayList 是工具类 Arrays 的一个内部静态类,它没有完全实现List的方法,而 ArrayList直接实现了List 接口,实现了List所有方法。



  • 长度不同 和 实现的方法不同

    Arrays.ArrayList是一个定长集合,因为它没有重写add,remove方法,所以一旦初始化元素后,集合的size就是不可变的。

  • 参数赋值方式不同
    Arrays.ArrayList将外部数组的引用直接通过“=”赋予内部的泛型数组,所以本质指向同一个数组。ArrayList是将其他集合转为数组后copy到自己内部的数组的。

那我们应该如何把数组转换为集合呢?这里有四种方法

1:遍历数组,遍历的同时加入到集合中

int []arr={1,2,3};
List list1=new ArrayList<Integer>();
for (int i = 0; i < arr.length; i++) {
     list1.add(arr[i]);
}

2:使用工具类

List list1=new ArrayList<Integer>();
Collections.addAll(list1,1,2,3);  //注意这里的1,2,3不能使用已经定义好的数组
 System.out.println(list1);

注意 addALl方法中的第二个变量不能写数组,否则,它会把数组作为一个单独的变量放到list中

3.使用java8中的Stream

int []arr={1,2,3};
List list1=Arrays.stream(arr).boxed().collect(Collectors.toList());
System.out.println(list1);

4.两个集合类结合
将Arrays.asList返回的集合作为ArrayList的构造参数

int []arr={1,2,3};
List list1=new ArrayList<>(Arrays.asList(1,2,3,4));
System.out.println(list1.size());

注意:这里和第二种方法一样,也不能直接把arr作为参数放入asList中


全部评论