欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java集合框架(综合)[云图智联]

程序员文章站 2022-04-27 23:40:19
...

免费学习视频欢迎关注云图智联:https://e.yuntuzhilian.com/

1、集合概述

现实生活中集合:很多事物凑在一起。

数学中的集合:具有共同属性的事物的总体。

Java中的集合类:是一种工具类,就像是容器,储存任意数量的具有共同属性的对象。在编程时,常常需要集中存放多个数据,当然我们可以使用数组来保存多个对象。但数组长度不可变化,一旦初始化数组时指定了数组长度,则这个数组长度是不可变的,如果需要保存个数变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:语文—79,数学—80,这种数据看上去像两个数组,但这个两个数组元素之间有一定的关联关系。

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供集合类。集合类主要负责保存其他数据,因此集合类也被称为容器类。所有容器类都位于Java.util包下。集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用);而集合里只能保存对象(实际上也是保存对象的引用,但通常习惯上认为集合里保存的是对象)。

Java集合框架由Java类库的一系列接口、抽象类以及具体实现类组成。我们这里所说的集合就是把一组对象组织到一起,然后再根据不同的需求操纵这些数据。集合类型就是容纳这些对象的一个容器。也就是说,最基本的集合特性就是把一组对象放一起集中管理。根据集合中是否允许有重复的对象、对象组织在一起是否按某种顺序等标准来划分的话,集合类型又可以细分为许多种不同的子类型。

Java集合框架为我们提供了一组基本机制以及这些机制的参考实现,其中基本的集合接口是Collection接口,其他相关的接口还有Iterator接口、RandomAccess接口等。这些集合框架中的接口定义了一个集合类型应该实现的基本机制,Java类库为我们提供了一些具体集合类型的参考实现,根据对数据组织及使用的不同需求,只需要实现不同的接口即可。Java类库还为我们提供了一些抽象类,提供了集合类型功能的部分实现,我们也可以在这个基础上去进一步实现自己的集合类型。

Java集合框架的优势有以下几点:

1)这种框架是高性能的。对基本类集(动态数组,链接表,树和散列表)的实现是高效率的。一般很少需要人工去对这些“数据引擎”编写代码(如果有的话)。
2)框架允许不同类型的类集以相同的方式和高度互操作方式工作。
3)类集是容易扩展和/或修改的。为了实现这一目标,类集框架被设计成包含一组标准的接口。对这些接口,提供了几个标准的实现工具(例如LinkedList,HashSet和TreeSet),通常就是这样使用的。如果你愿意的话,也可以实现你自己的类集。为了方便起见,创建用于各种特殊目的的实现工具。一部分工具可以使你自己的类集实现更加容易。
4)增加了允许将标准数组融合到类集框架中的机制。

2、集合接口和迭代器接口

Java的容器类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。通过Collection与Map接口导出其他子接口及实现类的框架示意图如下图所示:

Java集合框架(综合)[云图智联]

查看jdk中Collection类的源码后会发现如下内容:

public interface Collection<E> extends Iterable<E> {  
    //实现Collection接口的通用方法  
    int size();  
    boolean isEmpty();  
    boolean contains(Object o);  
    Iterable<E> iterable();  
    Object[] toArray();  
    <T> T[] toArray(T[] a);  
    boolean add(E e);  
    boolean remove(Object o);  
    boolean containsAll(Collection<?> c);  
    boolean addAll(Collection<? extends E> c);  
    boolean removeAll(Collection<?> c);  
    boolean retainAll(Collection<?> c);  
    void clear();  
    boolean equals(Object o);  
    int hashCode();  
}  

通过源码发现Collection是一个接口类,其继承了Java迭代器接口Iterable。

Collection接口有三个主要的子接口:List、SetQueue,注意Map不是Collection的子接口,这个要牢记。Collection中可以存储无序元素,可以重复组和各自独立的元素,即其内的每个位置仅持有一个元素,同时允许有多个null元素对象。

JDK不提供Collection接口的具体实现,而是提供了更加具体的子接口(如Set、List和Queue)的实现。那么Collection接口的存在有何作用呢?存在即是道理。

原因在于:为所有容器的实现类(如ArrayList实现了List接口,HashSet实现了Set接口)提供了两个“标准”的构造函数来实现:一个无参的构造方法;一个带有Collection类型参数的单参数构造方法。实际上,因为所有通用的容器类都遵从Collection接口,用第二种构造方法允许了容器之间相互的复制。

Collection接口中的方法如下:

Java集合框架(综合)[云图智联]

其中,iterator方法用于返回一个实现了Iterator接口的对象,可以使用这个迭代器对象依次访问集合中的元素。Iterator接口包含3个方法:

public interface Iterator<E>{  
    E next();  
    boolean hasNext();  
    void remove();  
}  

通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException异常,因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true后反复调用next方法。例如:

Collection<String> c = ...;  
Iterator<String> iter = c.iterator();  
while(iter.hasNext()){  
    String element = iter.next();  
    do something with element  
}  

从Java SE5.0起,这个循环可以采用一种更优雅的缩写方式。用for each循环可以更加简练地表示同样的循环操作:

for(String element:c){  
    do something with element  
}  

上面我们一共提到了两个和迭代器相关的接口:Iterable接口和Iterator接口,从字面意义上来看,前者的意思是“可迭代的”,后者的意思是“迭代器”。所以我们可以这么理解这两个接口:实现了Iterable接口的类是可迭代的;实现了Iterator接口的类是一个迭代器。

迭代器就是一个我们用来遍历集合中的对象的东西。也就是说,对于集合,我们不是像对原始类型数组那样通过直接访问元素来迭代,而是通过迭代器来遍历对象。

这么做的好处是将对于集合类型的遍历行为与被遍历的集合对象分离,这样一来我们无需关心该集合类型的具体实现是怎样的。

只要获取这个集合对象的迭代器,便可以遍历这个集合中的对象了。而像遍历对象的顺序这些细节,全部由它的迭代器来处理。现在我们来梳理一下前面提到的这些东西:首先,Collection接口实现了Iterable<E>接口,这意味着所有实现了Collection接口的具体集合类都是可迭代的。

那么既然要迭代,我们就需要一个迭代器来遍历相应集合中的对象,所以Iterable<E>接口要求我们实现iterator方法,这个方法要返回一个迭代器对象。

一个迭代器对象也就是实现了Iterator<E>接口的对象,这个接口要求我们实现hasNext()、next()、remove()这三个方法。

其中hasNext方法判断是否还有下一个元素(即是否遍历完对象了),next方法会返回下一个元素(若已经没有下一个元素了,调用它会抛出一个NoSuchElementException异常),remove方法用于移除最近一次调用next方法返回的元素(若没有调用next方法而直接调用remove方法会报错)。

我们可以想象在开始对集合进行迭代前,有一个指针指向集合第一个元素的前面,第一次调用next方法后,这个指针会“扫过”第一个元素并返回它,调用hasNext方法就是看这个指针后面还有没有元素了。也就是说这个指针始终指向刚遍历过的元素和下一个待遍历的元素之间。

由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。Java类库的设计者认为:这些实用方法中的某些方法非常有用,应该将它们提供给用户使用,这样,类库的使用者就不必自己重构这些方法了。

当然如果实现Collection接口的每一个类都提供如此多的例行方法将是一件很烦人的事情,为了能够让实现者更容易地实现这个接口,Java类库提供了一个抽象类AbstractCollection,它将基础方法size和iterator抽象化了,但是提供了通用例行方法。例如:

public abstract class AbstractCollection<E> implements Collection<E> {  
    public abstract Iterator iterator();  
    public abstract int size();  
    public boolean isEmpty() {  
        return size() == 0;  
    }  
    public boolean contains(Object o) {  
        Iterator e = iterator();  
        if (o==null) {  
            while (e.hasNext()){  
                if (e.next()==null)  
                    return true;  
            }  
        }   
        else {  
            while (e.hasNext()) {  
                if (o.equals(e.next()))  
                return true;  
            }     
        }  
        return false;  
    }  

    public Object[] toArray() {  
        Object[] result = new Object[size()];  
        Iterator e = iterator();  
        for (int i=0; e.hasNext(); i++) {  
            result[i] = e.next();  
        }     
        return result;  
    }  

    public boolean remove(Object o) {  
        Iterator e = iterator();  
        if (o==null) {  
            while (e.hasNext()) {  
               if (e.next()==null) {  
                    e.remove();  
                    return true;  
             }  
         }  
      }   
      else {  
         while (e.hasNext()) {  
              if (o.equals(e.next())) {  
                 e.remove();  
                 return true;  
              }  
         }  
      }  
     return false;  
    }  
    ......  
}  

AbstractCollection的直接子类有:AbstractList、AbstractQueue、AbstractSet。

3、Collection接口层次结构

下图是关于Collection的类的层次结构:

Java集合框架(综合)[云图智联]

Set接口:

一个不包括重复元素(包括可变对象)的Collection,是一种无序的集合。Set不包含满足a.equals(b)的元素对a和b,并且最多有一个null。实现了Set接口的类有:EnumSet、HashSet、TreeSet等。有一种众所周知的数据结构,可以快速查找所需要的对象,这就是散列表(hash table)。

散列表为每个对象计算一个整数,称为散列码(hash code)。

散列码由对象的实例域产生的一个整数。

准确地说,具有不同数据域的对象将产生不同的散列码。如果自定义类,就要负责这个类的hashCode方法,注意,自己实现的hashCode方法应该与equals方法兼容,即若a.equals(b)为true,a与b必须具有相同的散列码。

散列码可以是任何整数,包括整数或负数。

在Java中,散列表用链表数组实现,每个列表称为桶。计算对象的散列码并对桶数取余,得到的结果就是保存这个对象的桶的索引。

如果想要更多地控制散列表的性能,就要指定一个初始的桶数。通常,将桶数设置为预计元素个数的75%~150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为素数,以防键的集聚。

当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入新表中,废弃旧表。

装填因子(load factor)决定何时对散列表进行再散列。

例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。

Java集合类库提供了一个HashSet类,它实现了基于散列表的集。

可以用add方法添加元素。contains方法已经被重新定义,用来快速查看是否某个元素已经出现在集中,它只在某个桶中查找元素,而不必查看集合中的所有元素。

树集(TreeSet)是一个有序集合,当前使用的数据结构是红黑树。

将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比还是快很多的。

TreeSet中的对象需要实现Comparable接口,这样才能进行元素之间的比较,如果一个类的创建者没有实现Comparable接口,可以创建一个Comparator类来规定原类对象之间的比较规则,将一个Comparator对象实例传递给TreeSet的构造器即可。

List接口:

一个有序的Collection(也称序列),元素可以重复。列表通常允许满足e1.equals(e2)的元素对e1和e2,并且列表允许多个null元素。

实现List的类有:ArrayList、LinkedList、Vector、Stack等。其中,最常用的是ArrayList和LinkedList。

List接口是有序集合、元素可以重复,次序是List接口最重要的特点,它是以元素的添加的顺序作为集合的顺序,因此List的实现类中有可以通过来操作集合元素的方法。

其中ArrayList底层是通过数组实现的,数组的初始长度为10,可以扩展数组。

LinkedList底层是通过双向链表实现的,因此LinkedList可以在首尾添加删减元素,因此可以作为栈、队列、双端队列使用。

Queue接口:

Queue接口的实现类在使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。

offer()方法向队列中加入元素,不成功时返回false。

而直接使用add()方法插入,若队列已满则抛出异常。

remove()和poll()方法删除并返回队列一端的元素。前者队列为空时抛出异常,后者返回null。

Java SE 6中引入了Deque接口,它是Queue接口的子接口,Deque的实现类为ArrayDeque,可以实现双端队列,底层是通过数组实现,ArrayDeque有两个标志位分别指向数组的头与尾,因此才可以实现双端队列。

当需要使用LIFO(后进先出)堆栈时。应优先使用Deque接口而不是遗留Stack类。在将双端队列用作堆栈时,元素被推入双端队列的开头并从双端队列开头弹出。堆栈的方法完全可以等效成Deque的某些方法,对应关系如下:

push(e)---addFirst(e)  
pop()---removeFrist()  
top()-----peekFirst()

优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前的优先级队列中最小的元素。

优先级队列并没有对所有的元素进行排序。

优先级队列使用了堆,堆是一个可以自我调整的二叉村,对树执行添加和删除操作时,总能保证最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器提供比较器的类对象。

优先级队列的典型应用是任务调度。

每一个任务有一个优先级,将优先级最高的任务从队列中删除(由于习惯上将1设为“最高”优先级,所以会将最小的元素删除)。

与TreeSet迭代不同,这里的迭代并不是按照元素的排列顺序迭代的。删除时总是删掉剩余元素中优先级数最小的元素。

还有一种队列是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要实现类包括:ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。

虽然接口并未定义阻塞方法,但是实现类扩展了父接口,实现了阻塞方法。

4、Map接口的层次结构

下面的图是Map接口的层次结构图

Java集合框架(综合)[云图智联]

Map是一个键值对的集合。也就是说,一个映射不能包含重复的键,每个键最多映射到一个值。该接口取代了Dictionary抽象类。实现map接口的类有:HashMap、TreeMap、HashTable、Properties、EnumMap。

散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索村。散列或比较函数只能作用于键,与键关联的值不能进行散列或比较。

如何选择散列映射表与树映射表。散列稍微快一些,如果不需要按照排列顺序访问键,就最好选择散列。映射表中键必须是唯一的,不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值就会取代第一个值。实际上,put方法会返回用这个键参数存储的上一个值。

集合框架并没有将映射表本身视为一个集合(其他的数据结构框架则将映射表视为对pairs的集合,或者视为用键作为索引的值的集合)。然而,可以获得映射表的视图,这是一组实现了Collection接口或者它的子接口的视图。

有3个视图,它们分别是:键集、值集和键/值对集,键与键/值对形成了一个集,这是因为在映射表中一个键只能有一个副本。下列方法将返回这3个视图(条目集的元素是静态内部类Map.Entry的对象)。

Set<K> keyset();  
Collection<K> values();  
Set<Map.Entry<K,V>> entrySet();  

注意,keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个其他类的对象。初看起来,keyset方法创建了一个新集,并将映射表中的所有键都填进去,然后返回这个集。

但是,情况并非如此。取而代之的是:keySet方法返回一个实现了Set接口的某个类的对象,这个类的方法对原映射表进行操作。

Set接口扩展了Collection接口,因此可以与使用任何集合一样使用keySet。例如,可以杖举映射表中的所有键:

Set<String> keys = map.keySet();  
for(String key: keys){  
    System.out.println(key);  
}  

如果想要同时查看键与值,可以通过杖举各个条目(entries)查看,以避免对值进行查找。

for(Map.Entry<String, Employee> entry : staff.entrySet()){  
    String key = entry.getKey();  
    Employee value = entry.getValue();  
    System.out.println("key = " + key + ", value = " + value);  
}  


5、数组与集合之间的转换

由于Java平台API中大部分内容都是在集合框架创建之前设计的,所以,有时候需要在传统的数组与现代的集合之间进行转换。如果数组要转换为集合,Arrays.asList的包装器就可以实现这个目的。例如:

String[] values = ...;  
HashSet<String> staff = new HashSet<String>(Arrays.asList(values)); 

反过来,将集合转换为数组采用toArray()方法。转化为Object[]类型数组方法如下:

Object[] listArray = list.toArray();  
Object[] setArray = set.toArray();  

转化为具体类型数组:

String[] array1 = (String[])list.toArray(new String[list.size()]);  
String[] array2 = (String[])list.toArray(new String[0]);  

在转化为其它类型的数组时需要强制类型转换,并且要使用带参数的toArray方法,参数为对象数组,将list中的内容放入参数数组中,当参数数组的长度小于list的元素个数时,会自动扩充数组的长度以适应list的长度。因此,最好在数组构造时指明其长度。

6、List接口实现类

List接口继承了Collection接口,并对父接口进行了简单的扩充:同时List接口又有三个常用的实现类ArrayList、LinkedList和Vector。

1)ArrayList(数组线性表)

ArrayList数组线性表的特点为:用类似数组的形式进行存储,因此它的随机访问速度极快。ArrayList数组线性表的缺点为:不适合于在线性表中间频繁地进行插入和删除操作。

因为每次插入和删除都需要移动数组中的元素。可以这样理解ArrayList就是基于数组的一个线性表,只不过数组的长度可以动态改变而已。

对于使用ArrayList的开发者而言,下面几点内容一定要注意啦,尤其找工作面试的时候经常会被问到。

①如果在初始化ArrayList的时候没有指定初始化长度的话,默认的长度为10,源码中就是这样设置的:

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

②ArrayList在增加新元素的时候如果超过了原始的容量的话,ArrayList扩容的方案为:上一次的容量*1.5+1。

代码如下(Java1.8中ArrayList的扩容代码已经变了,这里暂时没有更新):

public void ensureCapacity(int minCapacity){  
    modCount++;  
    int oldCapacity = elementData.length;  
    if (minCapacity > oldCapacity) {  
        Object oldData[] = elementData;  
        int newCapacity = (oldCapacity * 3)/2 + 1;  
        if (newCapacity < minCapacity)  
            newCapacity = minCapacity;  
        // minCapacity is usually close to size, so this is a win:  
        elementData = Arrays.copyOf(elementData, newCapacity);  
    }  
}  

③ArrayList是线程不安全的,在多线程的情况下不要使用。

如果一定在多线程使用List,可以使用Vector,因为Vector和ArrayList基本一致,区别在于Vector中的绝大部分方法都使用了同步关键字修饰,这样在多线程的情况下不会出现并发错误,还有就是它们的扩容方案不同,ArrayList是扩容方案是:原始容量*3/2+1,而Vector允许设置默认的增长长度,Vector的默认扩容方式为原来的2倍。切记Vector是ArrayList的多线程的一个替代品。

④ArrayList实现遍历的几种方法

public class Test{  
    public static void main(String[] args) {  
        List<String> list=new ArrayList<String>();  
        list.add("Hello");  
        list.add("World");  
        list.add("HAHAHAHA");  
        //第一种遍历方法使用foreach遍历List,这是推荐的通用方法
        for (String str : list) {              
            System.out.println(str);  
        }  
        //第二种遍历,把list变为数组相关的内容进行遍历  
        String[] strArray=new String[list.size()];  
        list.toArray(strArray);  
        for(int i=0;i<strArray.length;i++) {  
            System.out.println(strArray[i]);  
        }  
        //第三种遍历 使用迭代器进行遍历  
        Iterator<String> ite=list.iterator();  
        while(ite.hasNext()){  
            System.out.println(ite.next());  
        }  
    }  
}  

2)LinkedList(链式线性表)

LinkedList的链式线性表的特点为:适用于需要在链表中间频繁进行插入和删除操作的场合。LinkedList的链式线性表的缺点为:随机访问速度较慢。查找一个元素需要从头开始一个一个的找。LinkedList是用双向循环链表实现的。对于使用LinkedList的开发者而言,下面几点内容一定要注意啦,尤其找工作面试的过程时候经常会被问到。

①简述LinkedList和ArrayList的区别和联系。

ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表的数据结构。

ArrayList数组线性表的特点为:类似数组的形式进行存储,内存连续,因此它的随机访问速度极快。ArrayList数组线性表的缺点为:不适合于在线性表中间需要频繁进行插入和删除操作。

因为每次插入和删除都需要移动数组中的元素。

LinkedList的链式线性表的特点为:适合于在链表中间需要频繁进行插入和删除操作。

LinkedList的链式线性表的缺点为:随机访问速度较慢。查找一个元素需要从头开始一个一个的找。

②LinkedList的内部实现是怎样的

对于这个问题,最好看一下jdk中LinkedList的源码。这样会醍醐灌顶的。LinkedList的内部是用基于双向循环链表的结构来实现的。在LinkedList中有一个类似于C语言中结构体的Entry内部类。在Entry的内部类中包含了前一个元素的地址引用和后一个元素的地址引用类似于C语言中指针

③LinkedList不是线程安全的

注意LinkedList和ArrayList一样也不是线程安全的,如果要在多线程并发环境中使用LinkedList,需要在要求同步的方法上加上同步关键字synchronized。

3)Vector(向量)

Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),即线程安全的。因此,开销就比ArrayList要大,正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。

引申:线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

7、HashMap与HashTable

HashMap和HashTable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。

HashMap的工作原理、ArrayList与Vector的比较是有关Java集合框架的最经典的问题。

HashTable是个过时的集合类,存在于Java API中很久了。

在Java 4中被重写了,它实现了Map接口,所以自此以后也成了Java集合框架中的一部分。

HashTable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题。

Key-Value键值存储的示意图如下图所示:

Java集合框架(综合)[云图智联]

Hashtable和HashMap的内部数据结构相似,如下图所示:

Java集合框架(综合)[云图智联]

其基本内部数据结构是一个Entry数组和链表的结合体。Entry数组的元素为实现了Map.Entry

Map map = new HashMap();  
map.put("Rajib Sarma","100");  
map.put("Rajib Sarma","200");   //The value "100" is replaced by "200".  
map.put("Sazid Ahmed","200");  
Iterator iter = map.entrySet().iterator();  
while (iter.hasNext()) {  
    Map.Entry entry = (Map.Entry) iter.next();  
    Object key = entry.getKey();  
    Object val = entry.getValue();  
}  


HashTable和HashMap区别主要集中在线程安全性、同步(synchronization)和速度上,分别有以下几点:

1)继承层次不同,二者都实现了Map接口,但HashTable继承了Dictionary类,而HashMap继承了AbstractMap类。

public class Hashtable extends Dictionary implements Map  
public class HashMap extends AbstractMap implements Map 

2)在HashTable中,key和value都不允许出现null值。

在HashMap中,null可以作为键,这样的键只有一个;

可以有一个或多个键所对应的值为null。

当get()方法返回null值时,既可以表示HashMap中没有该键,也可以表示该键所对应的值就是null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。

3)HashMap是非synchronized,而HashTable是synchronized,这意味着HashTable是线程安全的,多个线程可以共享一个HashTable;

而如果程序员没有手工进行正确的同步的话,多个线程是不能共享HashMap的。

由于HashTable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。

如果你不需要同步,只应用于单线程,那么使用HashMap性能要好过HashTable。

HashMap可以通过下面的语句进行同步:

Map m = Collections.synchronizeMap(hashMap);  

4)哈希值的使用不同,HashTable直接使用对象的HashCode,根据key值计算index的代码是这样的:

int hash = key.hashCode();    
int index = (hash & 0x7FFFFFFF) % tab.length;   

当hash数组的长度较小,并且Key的hashCode低位数值分散不均匀时,不同的hash值计算得到相同下标值的几率较高。

HashMap不直接使用对象的HashCode,而是重新计算hash值,而且用与运算代替了求模运算,代码如下:

static int indexFor(int h,int length) {    
    return h & (length-1);  
}  
static int hash(Object x) {    
    int h = x.hashCode();   
    h += ~(h << 9);  
    h ^= (h >>> 14);  
    h += (h << 4);  
    h ^= (h >>> 10);  
    return h;  
}    
int hash = hash(k);  
int i = indexFor(hash, table.length);  

这种计算方式优于HashTable,通过对Key的hashCode做移位运算和位的与运算,使其能更广泛地分散到数组的不同位置上去。

5)HashTable中hash数组默认大小是11,初始化时可以指定initial capacity(数组初始长度),扩容方式是old*2+1。

HashMap中hash数组的默认大小是16,而且长度始终保持为2的n次方,初始化时同样可以指定initial capacity(数组初始长度),若不是2的次方,HashMap将选取第一个大于initial capacity的2n次方值作为其初始长度。

6)遍历方式的内部实现上不同。HashTable与HashMap都使用了Iterator。

而由于历史原因(HashTable继承了Dictionary类),HashTable还使用了Enumeration的方式。一般单线程情况下,HashMap能够比HashTable工作得更好、更快,主要得益于它的散列算法,以及没有作线程同步。

应用程序一般在更高的层面上实现了保护机制,而不是依赖于这些底层数据结构的同步,因此,HashMap能够在大多数应用中满足需要。

推荐使用HashMap,如果需要同步,可以使用同步工具类将其转换成支持同步的HashMap。

(想要了解更多的职场,职业规划方面的经验,文章第一时间发布于云图智联官网)

免费学习视频欢迎关注云图智联:https://e.yuntuzhilian.com/