Java集合框架

一.概述

1. 对象存储

在java最初版本中需要存储多个对象可以使用数组实现,数组的特点是长度一旦初始化之后确定下来就不能进行改变,这使得它失去了扩展性;此外,数组中提供的方法较少,一些常用操作需要手动实现,效率较低,尽管它初始化的时候限制了其中元素的类型安全性较高。现在我们设想一个场景,如果需要存储不重复、有序的数据,这应该怎么实现呢?数组遍历?这效率显然就很低下了。再进一步,如果我们需要存储键值对数据呢?使用数组实现就有点捉襟见肘了。

2. 集合框架

在java1.2之后,集合框架横空出世。简单地说,集合也可以认为是一种容器,可以动态地存储对象的引用,利用其中的方法有助于高效地访问。与其他数据结构类库一样,集合类库也将接口与实现进行分离,因此我们在使用集合框架时,实际上调用的是特定接口的实现类。在java.util包中提供了一个泛型集合框架,其中包括多个接口以及实现,包括List、Set、Map等接口以及ArrayList、HashMap的实现。

3. 接口分类

集合中有两个基本的接口:Collection和Map

3.1 Collection接口

Collection 接口是 List、Set 和 Queue 接口的父接口,继承于Iterable接口。该接口中定义了一些基本的方法,既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合,对于该接口没有任何直接实现,但提供了其具体的子接口的实现。

  • Set接口:该接口扩展自Collection接口,是不包含重复元素的集合(根据equals方法来确定元素是否一致),并且元素没有特定顺序
  • SortedSet接口:Set的扩展接口,与Set相似,不同的是其中的元素是有序
  • List接口:List是可重复的、有序的集合,每个元素都有对应的顺序索引,实现类主要有ArrayList、LinkedList等
  • Queue接口:在queue中元素具有隐含的顺序,且每个queue都有一个head元素

3.2 Map接口

Map与Collection并列存在,用于保存具有映射关系的键值对数据,Map 中的 key 和 value 都可以是任何引用类型的数据,因为Map 中的 key 用Set来存放,所以不允许重复,因此key值的所对应的对象须重写hashCode()和equals()方法。因为key是唯一的,所以总是可以通过key去找到对应的value。Map还有一个扩展接口为sortedMap,不同的是其中的key是有序的。

3.3 Iterator接口

这是一个迭代器接口,使用该接口可以从集合中每次返回其中的一个元素。还有一个List对象的迭代器接口—— Listiterator接口,与Iterator接口相比,其中增加了一些与List有关的方法。此外,java.lang包下的Iterable接口也是集合框架的一部分,它是一个可提供Iterator的对象,可用于增强for。

3.4 接口树

二、迭代器

1. Iterator接口与Iterable接口

1.1 概述

实现了Iterator接口的类表示一个迭代器,而实现了Iterable接口的类表示的是这个类是可迭代的

Collection接口继承于Iterable接口,也就是说Collection接口是可迭代的。调用Collection接口的Iterator方法返回的是一个迭代器对象,该对象主要用于遍历Collection集合中的元素,这也是前面说到的集合框架相较于数组的特点:提供了一种新的方式进行存储元素的遍历,因为创建的是一个新的对象,所以并不会对原来的集合对象产生影响,并且迭代器内部实现了遍历过程的诸多细节。需要注意的是,集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。此外还有一个ListIterator接口拓展了Iterator接口,在其中添加了一些方法使其可迭代操作一个经过排序的List对象,可以使用hasNext与next向前迭代,也可以使用hasPrevious与previous向后迭代

1.2 迭代器对象

一个迭代器对象具有几个用于遍历元素的方法,游标默认在集合第一个元素之前

  • hasNext() 返回的是下一个位置是否存在元素,可以用来检测是否到达集合末端
  • next() 返回迭代下一个元素,如果下一位没有元素会报NoSuchElementException错误
  • remove() 删除迭代最近返回的元素,需要先调用next()确保正确删除元素,如果已经在调用next之后调用了remove,再次调用会抛出一个IllegalStateException异常

2. 使用迭代器遍历操作

第一种方式:通过hasNext、next方法遍历集合,以后面要讲的ArrayList为例

  @Test
  public void test1(){
    Collection coll1 = new ArrayList();
    coll1.add("jack");
    coll1.add(123);
    //获取迭代器对象
    Iterator itColl1 = coll1.iterator();
    //hasNext() 判断下一位是否有元素
    while (itColl1.hasNext()){
      //next() 获取下一位的元素
      System.out.println("2)"+itColl1.next()); // output: jack
    }
  }

第二种方式:java5之后可以实现foreach循环迭代访问集合与数组,底层使用iterator实现

public class ForEachText {
  @Test
  public void test1() {
    Collection coll1 = new ArrayList();
    coll1.add("Tom");
    coll1.add(123);
    // 使用增强for循环遍历元素 这种方式遍历无需下标
    for (Object obj : coll1) {
      System.out.println(obj); // output:Tom 123
    }
  }
}

三、Collection子接口的实现

1. Collection接口中定义的方法

在该接口中定义了一些用于操作集合数据的通用方法,但并无具体实现,而是在子接口中实现了具体的操作。

  1. coll1.add(object e) 将元素e添加到coll1中
  2. coll1.size() 查看coll1的长度
  3. coll1.addAll(Collection coll2) 把coll2的元素添加到coll1的后面
  4. coll1.clear() 清除coll1中的数据 但是该对象仍存在
  5. coll1.isEmpty() 判断coll1是否数据为空 (并非空指针)
  6. coll1.contains(object obj) 判断当前集合是否包含obj 注意:这里如果是对比的是String的内容是否一致的话将会返回true,因为String重写了equals方法。如果其他类没有重写equals方法就会是false。因此装入collection的数据最好都要重写equals方法
  7. coll1.containsAll(Collection coll2)方法 判断coll2的数据是否都在coll1中
  8. coll1.remove(object obj) 移除集合中的元素obj
  9. coll1.removeAll(Collection coll2) 从coll1中移除coll2的所有元素
  10. coll1.retainAll(Collection coll2) 处理后coll1的内容为coll1与coll2的交集
  11. coll1.equals(Collection coll2) 将coll1与coll2中所有元素进行比较
  12. coll1.hashCode() 计算coll1的哈希值
  13. coll1.toArray() 实现集合转数组 注:数组到集合的转换可以利用Arrays.asList() 方法实现
  14. coll1.iterator() 返回一个迭代器对象

2. Set

Set(集)是一种不包括重复元素的无序的集合。它扩展了Collection但是没有增加新的方法。如果向set中连续添加同一个元素,第一次会返回true,之后都将返回false。那么如何判断是否是同个元素呢?Set用到的是添加元素对象的equals方法以及hashcode方法,这些方法的具体细节下面会讲到。在Java中实现了Set接口的类有LinkedHashSet、HashSet、TreeSet等。

2.1 HashSet

HashSet是一个用散列表实现的Set,是Set接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。HashSet按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。HashSet不能保证元素的排列顺序,它也不是线程安全的。对于HashSet来说,如果两个元素相等,那么这两个元素的hashcode必须相等,并且equals必须返回true。因此,存放在HashSet中的元素一定要重写hashCode方法以及equals方法,保证散列码的生成。

当我们向HashSet中添加元素时,会发生什么呢?首先,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。例如:hashCode是1000,底层数组长度为15,那么1000%15即是该元素的索引,虽然这种计算很low... 元素越是散列分布,说明散列函数设计得越好,如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果 为true,添加失败;如果为false,那么会保存该元素,因为该数组的位置已经有元素了(散列冲突),所以会通过链表的方式继续链接。散列冲突发生后,元素a将与已存在该索引的数据以链表形式存储:在jdk7中是元素a放到数组中,指向原来的链表;在jdk8中是原来的链表放在数组中,指向元素a;因此HashSet的底层为数组+链表。

@Test
public void test1() {
  Set set = new HashSet();
  set.add(123);
  set.add("abc");
  set.add(new User("tom",12));
  set.add(123);
  Iterator setIt = set.iterator();
  while(setIt.hasNext()){
    System.out.println(setIt.next());
  }
}
 //output:abc  123  User{name='tom', age=12}
 //从结果可以看见重复的元素并没有被存储到HashSet中

此外,HashSet除了空参构造器之外还有一个带初始容量和负载因子参数的构造器,这里的初始容量表示底层数组的大小,负载因子指明当HashSet达到多少占用率进行扩容,这两个参数的具体细节会在HashMap处剖析。

2.2 LinkedHashSet

LinkedHashSet 是 HashSet 的子类,它根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。LinkedHashSet 不允许集合元素重复。对其进行迭代所需的时间只与大小成比例,与容量无关。

2.3 TreeSet

TreeSet 是 SortedSet 接口的实现类,它会将内容存储在一个树形结构中,可以确保集合元素处于排序状态。TreeSet底层使用红黑树结构存储数据,对于树的修改或者搜索的时间复杂度为O(logn)。对于TreeSet有两种排序方法,一种是自然排序(默认),另一种是定制排序。他有四个构造器:

public TreeSet(){}  //创建一个新的树集 其中的元素必须实现Comparable接口
public TreeSet(Collection <? extends E> coll){}  //生成树集 将coll中的元素加入树集
public TreeSet(Comparator <? super E> comp){}  //创建一个树集 根据comp指定顺序排序(定制排序)
public TreeSet(SortedSet <E> set){}  //将set的内容及排序方式迁移生成新的树集

在自然排序中,TreeSet会调用元素的compareTo方法比较大小,之后按照升序排序,因此添加入TreeSet中的元素必须实现Comparable接口的compareTo方法

import org.junit.Test;
import java.util.*;
//自定义类User
class User implements Comparable{
  private String name;
  private Integer age;
  /******************省略构造器 toString方法等**********************/
  // 在自定义类中实现Comparable接口的compareTo方法的双层排序
  public int compareTo(Object o) {
    if(o instanceof User){
      User user = (User)o;
      if(this.age.compareTo(user.age)==0) return this.name.compareTo(user.name);
      else return this.age.compareTo(user.age);
    }
    else{
      throw new RuntimeException("类型不匹配");
    }
  }
}

//自然排序实例
class TreeSetTest{
  @Test
  public void test() {
    TreeSet<Integer> set = new TreeSet<>();
    set.add(123);
    set.add(-5);
    set.add(234);
    Iterator<Integer> setIt = set.iterator();
    while(setIt.hasNext()){
      System.out.println(setIt.next());
    }
    //output: -5  123  234
    TreeSet<Object> set2 = new TreeSet<>();
    set2.add(new User("Tom",15));
    set2.add(new User("Jack",20));
    set2.add(new User("Oliver",12));
    set2.add(new User("Adam",20));
    Iterator<Object> set2It = set2.iterator();
    while (set2It.hasNext()){
      System.out.println(set2It.next());
    }
    //output: User{name='Oliver', age=12}  User{name='Tom', age=15}
    //        User{name='Adam', age=20}  User{name='Jack', age=20}
  }
}

定制排序通过Comparator接口来实现,其中需要重写compare(T o1,T o2)方法,如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。要实现定制序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

public class TreeSetTest1 {
  @Test
  public void test3() {
    // 定制排序 lambda表达式
    TreeSet<User> set = new TreeSet<>((o1, o2) -> {
    if(o1 != null && o2 != null){  return o1.getName().compareTo(o2.getName());  }
    else{  throw new RuntimeException("类型不匹配");  }
    });
    set.add(new User("Tom",15));
    set.add(new User("Jack",20));
    set.add(new User("Oliver",12));
    set.add(new User("Adam",20));
    Iterator<User> setIt = set.iterator();
    while(setIt.hasNext()){
      System.out.println(setIt.next());
    }
  }
}

经典样例:

class Test{ 
  @Test
  public void test2() {
    HashSet set = new HashSet();
    Person p1 = new Person("AA",1001);
    Person p2 = new Person("BB",1002);
    set.add(p1);
    set.add(p2);
    p1.name = "CC";
    set.remove(p1);
    System.out.println(set);
    //output:[Person{name='CC', age=1001}, Person{name='BB', age=1002}]
    //因为移除的是p1(AA 1001) 但实际上存储在set中的p1已经修改为(CC 1001)
    set.add(new Person("CC",1001));
    System.out.println(set);
    /*output:
    [Person{name='CC',age=1001},Person{name='CC',age=1001},Person{name='BB',age=1002}]
    其中一个(CC 1001)在set中的索引实际上的(AA 1001)的hashCode计算出来的索引 因此有两个共存
    */
    set.add(new Person("AA",1001));
    System.out.println(set);
    /*output:
    [Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}, Person{name='BB', age=1002}]
    新增加的(AA 1001)的hashCode和其中一个由p1转变而来的(CC 1001)的hashCode是一样,但如上面增加元素时的阐述,
    当hashCode一致时,会调用元素的equals方法,比较他们的name和age是否一致,很明显这里的AA和CC不一致,因此仍旧可以增加成功
    */
  }
}

3. List

前面讲到用数组存储数据存在一些缺点,所以通常使用List替代数组,List接口拓展了Collection接口,定义了规定元素顺序的集合。集合每个元素都有特定的位置(从0开始)。因此对List使用add方法时,会增加在尾部,移除元素时会将其后元素向前移动。也就是说,List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引,可以根据序号存取容器中的元素。List除了从Collection集合继承的方法外,还添加了一些根据索引来操作集合元素的方法。

  • void add(int index, Object ele):在index位置插入ele元素
  • boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index, Object ele):设置指定index位置的元素为ele
  • List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

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

3.1 ArrayList

ArrayList 是 List 接口的主要实现类,它将元素存在一个数组中。本质上,ArrayList是对象引用的一个”变长”数组。 关于ArrayList的初始容量,在 JDK1.7中,ArrayList像单例模式中的饿汉式,在底层直接创建一个初始容量为10的数组,在 JDK1.8中,ArrayList像单例模式中的懒汉式,底层创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组。那么如果初始容量不足呢?这时候会开始ArrayList的扩容机制。在jdk7与jdk8中是将ArrayList扩充为原来的1.5倍。ArrayList有三个构造器:

public ArrayList(){}  //使用默认容量创建一个ArrayList
public ArrayList(int initalCapacity){}  //创建一个底层数组大小为initalCapacity的ArrayList
public ArrayList(Collection <? extends E> coll){} //创建一个包含coll所有元素的ArrayList 初始容量为coll的1.1倍

ArrayList的一波方法示例:

@Test
public void test3() {
  ArrayList list = new ArrayList();
  list.add("tom");
  list.add(123);
  list.add(new Person("jack",15));
  list.add(123);
  System.out.println(list); //output:[tom, 123, Person{name='jack', age=15}, 123]
  // 1) list.add(index,Object obj) 向list中索引为index的位置插入obj
  list.add(1,"jerry");
  System.out.println(list); 
  //output:[tom, jerry, 123, Person{name='jack', age=15}, 123]
  // 2) list.addAll(index,List ls) 向list中索引为index的位置插入ls的全部元素
  List ls = Arrays.asList(1, 2, 3);
  list.addAll(2,ls);
  System.out.println(list); 
  //[tom, jerry, 1, 2, 3, 123, Person{name='jack', age=15}, 123]
  // 3) list.get(index) 获取list中索引为index的元素
  System.out.println(list.get(0)); //oputput:tom
  // 4) list.indexOf(Object obj) 返回obj在list中首次出现的索引 不存在即返回-1
  System.out.println(list.indexOf(123)); //output:5
  // 5) list.lastIndexOf(Object obj) 返回obj在list中末次出现的索引 不存在即返回-1
  System.out.println(list.lastIndexOf(123)); //output:7
  // 6) list.set(index,Object obj) 将list中索引为index的元素更换为obj
  list.set(2,654);
  System.out.println(list);
  //output:[tom, jerry, 654, 2, 3, 123, Person{name='jack', age=15}, 123]
  // 7) list.remove(index) 将list中索引为index的元素删除
  // 需要注意的是 如果输入一个整型数据 默认情况下是按照索引删除 
  // 如果要删除其中的数据 需要new Integer(xx) 去进行删除
  Object obj = list.remove(2); // 这里返回的是被删除的元素
  System.out.println(obj); //output:654
  System.out.println(list);
  //output:[tom, jerry, 2, 3, 123, Person{name='jack', age=15}, 123]
  // 8) list.subList(fromIndex,Index) 返回一个从fromIndex到Index(左闭右开)的list子集合
  List list1 = list.subList(3, 6);
  System.out.println(list1);
  //output:[3, 123, Person{name='jack', age=15}]
}

3.2 LinkedList

LinkedList是一个双向链表,它对于随机读取的效率低于ArrayList,但对于插入数据的效率高于ArrayList。LinkedList内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为双向链表中保存数据的基本结构。Node除了保存数据,还定义了两个变量:prev变量记录前一个元素的位置,next变量记录下一个元素的位置。

transient Node<E> first;
transient Node<E> last;
private static class Node<E> { 
  E item;
  LinkedList.Node<E> next;
  LinkedList.Node<E> prev;
  Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
  }
}

LinkedList提供了两个构造器,一个为空参构造器,一个可以创建包含指定Collection元素的LinkedList,此外还实现了Collection的一些方法以及自定义的方法:

@Test
public void test() {
  var list1 = new LinkedList<String>();
  list1.add("test1");
  list1.add("test2");
  list1.add("test3");
  var list2 = new LinkedList<String>();
  list2.add("demo1");
  list2.add("demo2");
  list2.add("demo3");
  list2.add("demo4");
  // 1) addFirst 将元素加到链表头部
  // 2) addLast 将元素加到链表尾部
  list1.addFirst("first");
  list1.addLast("last");
  System.out.println(list1); //output:[first, test1, test2, test3, last]
  // 3) getFirst 获取链表头部元素
  System.out.println(list2.getFirst()); //output:demo1
  // 4) getLast 获取链表尾部元素
  System.out.println(list2.getLast()); //output:demo4
  // 5) removeFirst 删除链表头部元素
  // 6) removeLast 删除链表尾部元素
  list2.removeFirst();
  list2.removeLast();
  System.out.println(list2); //output:[demo2, demo3]
}

3.3 Vector

Vector是一个古老的集合,大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。 在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

ps:ArrayList/LinkedList/Vector的异同?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

1)ArrayList和LinkedList的异同 二者都线程不安全,相对线程安全的Vector,执行效率高。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
2)ArrayList和Vector的区别 Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于 强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用 ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大 小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。

4. Queue

Queue接口拓展了Collection接口,队列定义了一个head位置,它是下一个要被移除的元素。队列通常是先进先出的操作(栈是后进先出),或者按照指定的顺序进行。可以使用element方法获取队列头,如果队列为空则抛异常;使用peek方法可以返回队列头,队列为空不抛异常;同时,最好使用offer方法添加元素,使用poll方法移除元素,因为使用这两个方法添加/移除元素失败时不会抛出异常,而Collection的add和remove方法失败后抛异常。上面提到的LinkedList类提供了Queue的简单实现,但应避免插入null。

4.1 Deque

Java6中引入了Deque接口(双端队列),它是Queue接口的子接口,双端队列允许在头部和尾部增删元素,不允许在中间添加元素。ArrayDeque和LinkedList类实现了这个接口,这两个类都提供双端队列。

@Test
public void test1() {
  var arr = new ArrayDeque<String>();
  //1)添加元素到队列头部、尾部 有两种方法
  //1.1)第一种:addFirst() addLast() 如果队列已满添加失败会抛出IllegalStateException异常
  //1.2)第二种:offerFirst() offerLast() 如果队列已满添加失败会返回false
  arr.add("tom1");
  arr.add("tom2");
  System.out.println(arr);  //output:[tom1, tom2]
  arr.addFirst("tom3");
  System.out.println(arr); //output: [tom3, tom1, tom2]
  arr.offerLast("tom4");
  System.out.println(arr); //output: [tom3, tom1, tom2, tom4]
  //2)如果队列不为空 删除头部元素、尾部元素的方法有两种
  //2.1)第一种:removeFirst() removeLast() 如果队列为空会抛出NoSuchElementException异常
  //2.2)第二种:pollFirst() pollLast() 如果队列为空 删除失败会返回null
  arr.removeFirst();
  System.out.println(arr); //output: [tom1, tom2, tom4]
  arr.pollLast();
  System.out.println(arr); //output: [tom1, tom2]
  //3)如果队列不为空 获取头部元素、尾部元素的方法有两种
  //3.1)第一种:getFirst() getLast() 如果队列为空会抛出一个NoSuchElementException异常
  //3.2)第二种:peekFirst() peekLast() 如果队列为空 获取失败会返回null
  String first = arr.getFirst();
  String last = arr.peekLast();
  System.out.println(first+"\t"+last); //output:tom1  tom2
}

4.2 PriorityQueue

优先队列采用堆这种数据结构,元素可以按照任意顺序插, 但是删除的时候总是会删除最小的那个。优先队列主要用于任务调度,当启动新任务时会把优先级最高的任务从队列中删除(习惯上优先级最高的是1)。这里添加的对象(元素)需要实现compareTo方法。

@Test
public void test1() {
  var pq = new PriorityQueue<LocalDate>();
  pq.add(LocalDate.of(2019, 6, 1));
  pq.add(LocalDate.of(2019, 3, 18));
  pq.add(LocalDate.of(2020, 8, 20));
  // 调用remove时会把元素中最小的那个删除掉
  pq.remove();
  System.out.println(pq); //output:[2019-06-01, 2020-08-20]
}

4.3 阻塞队列

还有一种队列是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要实现类包括:ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。虽然接口并未定义阻塞方法,但是实现类扩展了父接口,实现了阻塞方法。[origin]

四、Map接口的实现

Map接口与Collection接口并列存在,用于保存具有映射关系的键值对数据,Map 中的 key 和 value 都可以是任何引用类型的数据。常用String类作为Map的“键”,key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到 唯一的、确定的 value。Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。

Map的实现类继承结构如下:


1. HashMap

HashMap使用散列表实现Map,其根据每个键的hashCode方法确定位置,因此key元素对应类要重写equals()和hashCode()。同时所有的value构成的集合是无序的、可以重复的。因此value元素对应类要重写equals()方法HashMap允许使用null键和null值,与HashSet一样,不保证映射的顺序。在HashMap中,一个key-value键值对构成一个entry,所有的entry构成的集合是无序的、不可重复的。HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

1.1 构造器

public HashMap() 使用默认初始容量以及负载因子创建一个新的HashMap
public HashMap(int init) 使用给定init和默认负载因子创建一个HashMap
public HashMap(int init,float loadFactor) 使用init个散列位和给定的loadFactor(非负)创建一个HashMap
public HashMap(Map <? extends K, ? extends V> map) 创建一个HashMap并从map中复制内容,初始容量基于map大小,负载因子默认

1.2 常用方法

集合框架并没有将映射表本身视为一个集合,所以它和Collection是平级的。但可以获得映射表的视图,这是一组实现了Collection接口或者它的子接口的视图。有3个视图,它们分别是:键集、值集和键值对集

Set<K> keyset(); // 生成一个键集
Collection<K> values(); //生成一个值集
Set<Map.Entry<K,V>> entrySet(); //生成一个键值对集

keySet方法返回的是实现了Set接口的类的对象,这个类的方法对原映射表进行操作。Set接口扩展了Collection接口,因此可以与使用任何集合一样使用keySet。对于entrySet同理。

// ********************HashMap常用方法**********************
@Test
public void test1() {
  HashMap<String,Object> map = new HashMap<>();
  HashMap<String,Object> mapCp = new HashMap<>();
  //1) 添加键值对 put(key,value) 
  map.put("id","1");
  map.put("id",2);  // 对于相同的key执行put操作 实际上是进行value的替换
  map.put("age",15);
  mapCp.put("address","China");
  //2) 批量添加键值对 putAll(HashMap xx)
  map.putAll(mapCp);
  //3) get(key) 获取key对应的value 不做移除操作
  Object id = map.get("id");
  //4) remove(key) 对key对应的键值对作移除操作 返回的是key对应的value
  map.remove("name");
  //5) boolean containsKey(key) 查询是否包含key
  //   boolean containsValue(value) 查询是否包含value
  map.containsKey("id");
  map.containsValue(15);
  //6) map.clear() 将map中的所有元素依次置为null 但map此时并非null 调用size()不会空指针
  map.clear();
  map.size();
}
@Test
public void test2() {
  HashMap<String,Object> map = new HashMap<>();
  map.put("name","tom");
  map.put("age",18);
  map.put("area","sz");
  //1)keySet方法 取出所有的key 返回一个set
  Set<String> mks = map.keySet();
  // 接着可以使用迭代器迭代输出
  Iterator<String> mksIt = mks.iterator();
  //2)values方法 取出所有的value 返回一个collection
  Collection<Object> mvs = map.values();
  // 接着可以使用迭代器迭代输出
  Iterator<Object> mvsIt = mvs.iterator();
  //3)entrySet方法 取出所有的Entry数组 返回一个set
  Set<Map.Entry<String, Object>> mes = map.entrySet();
  // 接着可以使用迭代器迭代输出
  Iterator<Map.Entry<String, Object>> mesIt = mes.iterator();
  while(mesIt.hasNext()){
    Map.Entry<String, Object> entry = mesIt.next();
    System.out.println("key is "+entry.getKey()+"|values is "+entry.getValue());
  }
  // 如果需要遍历key-values 只需要知道其中一个即可自由组合遍历出 需要灵活运用方法
}

1.3 底层实现原理

HashMap的底层实现原理Java7与Java8有所不同。在Java7及以前版本,HashMap是数组+链表结构(即为链地址法)。在Java8版本中HashMap是数组+链表+红黑树实现。

调用HashMap的空参构造器实例化时,底层创建了长度是16的一维数组Entry[] table(在Java8中只有当调用put方法后才会创建数组,并且类型为Node)。当HashMap开始添加元素,调用map.put(key1,value1)的时候,首先调用key1所在类的hashCode()方法,计算key1的哈希值,此值通过算法计算出 Entry数组在HashMap底层数组中的存放位置(索引位置),接着判断数组此位置上是否有存在元素:
----①如果该位置为空,则key1-value1键值对插入成功;
----②如果该位置不为空(意味着存在一个或以链表形式存在的多个数据),则比较key的hash值:
--------①如果key1的hash值与已存在数据的hash值都不相同,则插入成功;
--------②如果key1的hash值与已存在的数据(key2-value2)的hash值相同,则调用key1所在类的equals方法:
----------------①equals返回true,则value1覆盖value2;
----------------②equals返回false,则key1-value1添加成功;
需要注意的是,对于除第一种之外的添加成功的情况,key1-value1将与已存在该索引的数据以链表形式存储。

1.4 扩容机制

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize(再次散列)

当HashMap中的元素个数超过数组大小与loadFactor乘积时,就会进行数组扩容。 前面也讲到了,loadFactor 的默认值为0.75,这是一个折中的取值,也就是说,默认情况下,当HashMap中元素个数超过12(16*0.75)的时候,就把数组的大小扩大一倍为32,然后重新计算每个元素在数组中的位置,因为这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,预设元素的个数能够有效的提高HashMap的性能。在Java8中,当HashMap中的一个链的对象个数达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize时判断树的结点个数低于6个,也会把树再转为链表。

负载因子值的大小,对HashMap有什么影响

  1. 负载因子的大小决定了HashMap的数据密度。
  2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  3. 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  4. 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

2. LinkedHashMap

LinkedHashMap 是 HashMap 的子类,在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序。与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。由于维护链表结构所产生的开销,LinkedHashMap的性能可能会比HashMap差一点,但是其迭代所需时间只与LinkedHashMap的大小成比例,而与容量无关。

3. TreeMap

TreeMap存储键值对时,需要根据键值对进行排序,它可以保证所有的键值对处于有序状态。TreeSet底层使用红黑树结构存储数据TreeMap 的 Key 的排序,这里同样有两种排序方法:如果使用自然排序,那么TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException异常;如果使用定制排序,需要在创建 TreeMap 时传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序,此时不需要 Map 的 Key 实现Comparable 接口。关于排序的内容与TreeSet基本一致,这里不再赘述。TreeMap判断两个key相等的标准是两个key通过compareTo()方法或者compare()方法返回0。一般来说只有在需要排序或者hashCode方法实现太差时才会使用TreeMap。

TreeMap有几个构造器,分别为:

public TreeMap() 创建一个TreeMap,其中的键按照自然排序
public TreeMap(Map <? extends K, ? extends V> map) 等价于先调用TreeMap再将map中的键值对加进去
public TreeMap(Comparator <? super K> comp) 创建一个TreeMap,按照定制排序排列键
public TreeMap(SortedMap<K, ? extends V> map) 创建TreeMap,初始内容与排序方式皆与map相同

4. Hashtable

Hashtable是个古老的 Map 实现类,不同于HashMap,Hashtable是线程安全的。Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。但是Hashtable 不允许使用 null 作为 key 和 value。与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序。Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

4.1 Properties

Properties 类是 Hashtable 的子类,该对象用于处理属性文件。由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型。存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法。

// 利用properties类读取配置文件 需要用到io流
@Test
public void test1() throws Exception {
  FileInputStream fis = null;
  Properties pro = new Properties();
  fis = new FileInputStream("top/jtszt/ReflectionTest/jdbc.properties");
  pro.load(fis);
  String name = pro.getProperty("name");
  String password = pro.getProperty("password");
  System.out.println("name is "+name+", password is "+password);
  fis.close();
  }
 }

五. Collections与Arrays

1. Collections工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。Collections提供了多个synchronizedXx()方法,把线程不安全的list/Collection/map转换为线程安全的

@Test
public void test1() {
  List<Integer> list = new ArrayList<>();
  list.add(41);
  list.add(13);
  list.add(55);
  list.add(55);
  //1) reverse(list)方法 反转list中的元素
  Collections.reverse(list); // 对list做修改 没有返回值
  //2) shuffle(list)方法 对list进行随机打乱排列
  Collections.shuffle(list);
  //3) sort(list) 对list进行排序 需要实现compareTo方法
  Collections.sort(list);
  //4) swap(list,i,j) 对list中的i处的元素和j处的进行交换
  Collections.swap(list,0,3);
  //5) max(Collection) 返回Collection中最大的元素(基于compareTo方法)
  //6) min(Collection) 返回Collection中最小的元素(基于compareTo方法)
  //7) frequency(Collection,i) 返回Collection中i出现的次数
  int frequency = Collections.frequency(list, 55);
  //8) copy(list dest,list src) 把src的内容复制到dest中
  // 应该先新建一个带src.size()个数null的一个list
  List<Object> dest = Arrays.asList(new Object[list.size()]);
  Collections.copy(dest,list);
  // 9) 利用synchronizedList(list)方法 将list转换为线程安全的
  List<Integer> list1 = Collections.synchronizedList(list);
  }
 }

2. Arrays工具类

Arrays类提供了用于处理数组的静态方法,其中大多数都有完备的重载形式:一个用于基本数据类型数组,一个用于Object数组。在实际开发中,有时候需要在数组与集合之间进行转换,这时就可以利用Arrays.asList方法实现数组到集合的转换,也可以使用toArray()方法实现集合到数组的转换。集合在转化为具体类型数组时需要强制类型转换,并且要使用带参数的toArray方法,参数为对象数组。此外还有其他的一些方法:

  1. sort:按升序排序数组
  2. binarySearch:在有序数组中查找给定的键。该方法将返回键的下标或对安全插人点进行编码的负值
  3. fill:使用给定的值填充数组
  4. equals 和 deepEquals:如果传入的两个数组是同一个对象,或都是null,或大小相同且包含等价的内容,则返回 true。这两个方法没有子数组版本,用于 Object[ ]的equals方法将调用数组中每个元素的 Object.equals方法。因为该方法没有对嵌套数组进行特殊处理,所以一般不能用它来比较包含数组的数组。deepEquals方法对两个 object[ ]的等价性进行的是递归检查,而且考虑了嵌套数组的等价性。
  5. hashCode和deepHashCode:基于给定数组的内容返回一个散列码。因为用于 Object[ ] 的 deepHashcode方法考虑了嵌套数组的内容,所以 deepHashCode 方法将递归地计算 Object[ ] 的散列码。
  6. toString 和 deepToString:返回数组内容的字符串表示。所返回的字符串由数组的元素列表组成,元素之间由逗号分隔,并且整个列表用[ ]括了起来。数组类型的内容会通过String.valueof转换成字符串。用于 0bject[ ]的toString方法可以使用 object.toString方法把所有嵌套数组转换成字符串。 deepToString方法可以通过递归地将嵌套数组转换为由该方法定义的字符串来返回 object[ ] 的字符串表示。

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容