数组与集合

数组与集合

数组

数组是一组具有相同类型的有序数据的集合。数组中的一个数据成员称为数组元素。数组生成后,数组长度不可改变。
字符串数组:字符串数组在初始化时创建
对象数组:可以将不同数据类型的元素封装成对象数组

Object a[] = new Object[3];
a[0] = new Integer(199901);
a[1] = new String("王平");
a[2] = new Double(75.68);
System.out.println("学号   姓名  平均分");
for(int i = 0; i < 3; i++)            
System.out.print(a[i] + " ");

Java中的二维(多维)数组是特殊的一维数组
二维定义与初始化

int tmp[][];
tmp = new int[2][3];
tmp = { {1,2}, {3,4}, {5,6} }
//数组的长度:tmp.length

Arrays 数组工具类
Arrays类中定义了5种类型的方法:

  • asList()将指定数组作为List返回;
  • equals()比较两个数组是否相等;
  • sort()对不同类型的数组排序;
  • binarySearch()在不同类型的数组中用二分查找算法搜索特定值;
  • fill(int[] a, int fromIndex, int toIndex, int val)用一个指定的值填充数组。

数组与集合的比较:
1、数组和集合用于“保存多个数据/对象”。
2、数组大小固定,且同一个数组只能存放类型一样的数据(基本类型/引用类型)
3、JAVA集合可以存储和操作数目不固定的一组数据。
4、若程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用。当无法预知数据量,或者需要比下标更灵活的存取方式时,使用集合。 
5、使用相应的toArray()和Arrays.asList()方法可以相互转换。

集合

集合类存放于java.util包中。
集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,集合中的对象指的是集合中对象的引用(reference)。

集合类型主要有3种:set(集)、list(列表)和map(映射)。

主要的集合类
Collection 集合接口
List允许重复元素
Set 不允许重复元素
Map 键/值对

Collection接口

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个 Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
  如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子
while(it.hasNext()) 
{
  Object obj = it.next(); // 得到下一个元素
}

iterator接口

booleanhasNext() 如果仍有元素可以迭代,则返回true。
Enext() 返回迭代的下一个元素
voidremove() 从迭代器指向的集合中移除迭代器返回的最后一个元素(可选操作)。
由Collection接口派生的两个接口是List和Set。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

List类(元素有序,可重复)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
List接口继承了Collection 接口以定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。

ArrayList:由数组实现的List。允许对元素进行快速随机访问,但是向List中间插入与移除元素的速度很慢。ListIterator只应该用来由后向前遍历ArrayList,而不是用来插入和移除元素。因为那比LinkedList开销要大很多。ArrayList支持随需要而增长的动态数组,可以灵活地删除或插入元素
创建ArrayList
ArrayList al=new ArrayList();
主要方法
add(Object o) //追加或者插入
clear() //清空
contains() //查询
get(int index) //索引
isEmpty() //判空
remove(int index/Object o) //删除
toArrayList() //转换成数组

ArrayList自动扩容原理

(一)ArrayList三种初始化
1、默认的构造器,将会以默认的大小来初始化内部的数组

public ArrayList();

2、用一个ICollection对象来构造,并将该集合的元素添加到ArrayList

public ArrayList(Collection<? extends E> c)

3、用指定的大小来初始化内部的数组

public ArrayList(int initialCapacity)

(二)扩容概括

扩容概括为两步:
1、扩容—把原来的数组复制到另一个内存空间更大的数组中

2、添加元素—把新元素添加到扩容以后的数组中

ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

ArrayList是基于数组实现的List类。会自动的进行扩容,采用Arrays.copyOf()实现

如果在创建ArrayList时,可以知道ArrayList元素的数量最好指定初始容量,这样可以避免ArrayList的自动多次扩容问题。

与 LinkedList 相比,ArrayList 适用于频繁查询的场景,而不适用于频繁修改的场景; 与 Vector 相比,ArrayList 的所有操作都是非线程安全的。

线程不安全

在进行添加操作时,尽量不要指定位置操作。

删除操作尽量不要指定位置删除。

ArrayList虽然为我们带来了好处,但我们也要注意他的不足,否则你的程序性能会直线下滑。

(三)自动扩容从ArrayList初始化开始

1、无参构造方法:创建一个空数组

transient Object[] elementData;
private static final Object[] EMPTY_ELEMENTDATA = {};
 
 
/**
 *无参构造方法实现创建一个空数组,并且为这个数组赋一个空值
 *
*/
public ArrayList() {
        super();
        //为数组赋空值
        this.elementData = EMPTY_ELEMENTDATA;    
    }

2、扩容从add( )开始

 /**
     *1、 ArrayList 扩容从add()开始
     *2、add方法有两条语句如下:
     *   ensureCapacityInternal(size + 1); 判断是否需要调用扩容方法
     *   elementData[size++] = e;  调用add方法添加的值赋值给数组。
     * 
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

3、ensureCapacityInternal:判断是否需要扩容
ArrayList 在进行添加操作前,会检验内部数组容量并选择性地进行数组扩容。在 ArrayList 中,通过私有方法 ensureCapacityInternal 来进行数组的扩容操作。如图:
在这里插入图片描述
每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

    //1、add方法调用ensureCapacityInternal方法
private void ensureCapacityInternal(int minCapacity) {
    //2、判断当前数组是否为空
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    //3、调用扩容判断的方法,将当前最小扩容值传进去。
        ensureExplicitCapacity(minCapacity);
    }
 
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
     /**
     * 4、minCapacity:最小扩容值
     *elementData.length:数组空间长度
     * 判断是否扩容原理:
     * (最小扩容值 - 数组空间长度 > 0)则调用扩容方法grow(minCapacity);
     * 举例:第一次调用add方法,最小扩容值为11,数组长度为10 那么(11- 10 > 0)调用扩容,扩容后是当 
     *前值的1.5倍=15。            
     * 第二次调用add方法,最小扩容值+1是12,数组长度为15那么(12- 15 > 0)不大于0,不调用扩容方法
     */
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

4、grow(minCapacity) :计算扩容空间大小,并调用复制数组方法。

    private void grow(int minCapacity) {
        // overflow-conscious code
    //1、获取现在数组的长度
        int oldCapacity = elementData.length;
    //2、扩容后的数组长度
    //  oldCapacity >> 1    右移运算符   原来长度的一半 再加上原长度也就是每次扩容是原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    //3、判断扩容后的长度是否小于需要的最小扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
    //4、判断扩容后的长度是否超过规定的最大值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
    //5、调用复制数组方法,将老的数组复制到新的数组中,并将需要添加的元素加到数组最后一位
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

LinkedList类:对顺序访问进行了优化,向List中间插入与删除的开销并不大,随机访问则相对较慢。(使用ArrayList代替。)还具有下列方法:addFirst(), addLast(), getFirst(), getLast(), removeFirst() 和 removeLast(), 这些方法 (没有在任何接口或基类中定义过)使得LinkedList可以当作堆栈、队列和双向队列使用。LinkedList类是链接列表数据结构,插入、删除对象的效率更高。

Vector:实现一个类似数组一样的表,自动增加容量来容纳所需的元素。使用下标存储和检索对象就像在一个标准的数组中。也可用一个迭代器从一个Vector中检索对象。Vector是唯一的同步容器类!当两个或多个线程同时访问时也是性能良好的。Vector类似于ArrayList,支持多线程;线程同步机制使其性能变差。
Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

Stack:Stack从Vector派生而来,并且增加了方法实现栈!一种后进先出的存储结构。Stack是实现后进先出的栈。

面向位置的操作包括插入某个元素或 Collection 的功能,还包括获取、除去或更改元素的功能。在 List 中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置。

实现Set接口的类(元素不可重复)

在这里插入图片描述
Set接口同样是Collection接口的一个子接口,它表示数学意义上的集合概念,Set中不包含重复的元素。

Set接口继承Collection接口,而且它不允许集合中存在重复项。所有原始方法都是现成的,没有引入新方法。具体的Set实现类依赖添加的对象的equals()方法来检查等同性。

HashSet:使用HashMap的一个集的实现。虽然集定义成无序,但必须存在某种方法能相当高效地找到一个对象。使用一个

HashMap对象:实现集的存储和检索操作是在固定时间内实现的。(HashSet类集使用散列表进行存储,关键字的内容被生成唯一值,称散列码hashcode,散列码被用作与关键字相连的数据的存储下标)

TreeSet:是使用树结构存储的Set接口,对象按升序存储,访问和检索很快;适用于大量数据的快速检索排序。
为优化 HashSet 空间的使用,可调优初始容量和负载因子。TreeSet 不包含调优选项,因为树总是平衡的,保证了插入、删除、查询的性能为log(n)。

HashSet 和 TreeSet 都实现 Cloneable 接口。
当从集合中以有序的方式抽取元素时,TreeSet实现会有用处。为了能顺利进行,添加到TreeSet的元素必须是可排序的。

实现Map接口的类(键值对)

在这里插入图片描述
映射接口(Map):映射(Map)是一个存储关键字和值的关联或者说是关键字/值对的对象。给定一个关键字,可以得到它的值。关键字和值都是对象,每一对为一项。关键字必须是唯一的,但值可以重复。有些映射可以接收null关键字/值,有的则不行
HashMap类使用散列表实现Map接口

Map map=new HashMap(); 
map.put("a", "aaa"); 
map.put("b", "bbb"); 
map.put("c", "ccc"); 
map.put("d", "ddd"); 
Iterator iterator = map.keySet().iterator(); 
while (iterator.hasNext()) {
   Object key = iterator.next(); 
  System.out.println("map.get(key) is :"+map.get(key));
 } 

运行结果:

map.get(key) is :ddd
map.get(key) is :bbb
map.get(key) is :ccc
map.get(key) is :aaa

TreeMap类:TreeMap类通过使用树实现Map接口。允许快速检索; 保证元素按照关键字升序排序。
HashTable类:Java.util.Hashtable提供了种方法让用户使用哈希表,而不需要考虑其哈希表真正如何工作。插入、检索、删除。HashTable实现了线程安全的HashMap

Map接口不是Collection接口的继承。而是从自己的用于维护键-值关联的接口层次结构入手。按定义,该接口描述了从不重复的键到值的映射。

可把这个接口方法分成三组操作:改变、查询和提供可选视图。

改变操作允许从映射中添加和除去键-值对。键和值都可以为null。但是不能把Map作为一个键或值添加给自身。

Object put(Object key, Object value)  //返回值是被替换的值。
Object remove(Object key)
void putAll(Map mapping)
void clear()

查询操作允许检查映射内容:

Object get(Object key)
boolean containsKey(Object key)
boolean containsValue(Object value)
int size()
boolean isEmpty()

提供可选视图方法允许您把键或值的组作为集合来处理。
public Set keySet() //映射中键的集合必须是唯一的,用Set支持
public Collection values() //映射中值的集合可能不唯一,用Collection支持
public Set entrySet() //返回一个实现Map.Entry接口的元素Set

Map.Entry 接口

Map的entrySet()方法返回一个实现Map.Entry接口的对象集合。集合中每个对象都是底层Map中一个特定的键-值对。通过这个集合迭代,可获得每一条目的键或值并对值进行更改。但是,如果底层Map在Map.Entry接口的setValue()方法外部被修改,此条目集就会变得无效,并导致迭代器行为未定义。

HashMap 类和 TreeMap 类

“集合框架”提供两种常规的Map实现:HashMap和TreeMap。和所有的具体实现一样,使用哪种实现取决于特定需要。在Map中插入、删除和定位元素,HashMap是最好的选择。但如果要按顺序遍历键,那么TreeMap会更好。根据集合大小,先把元素添加到HashMap,再把这种映射转换成一个用于有序键遍历的TreeMap可能更快。使用HashMap要求添加的键类明确定义了hashCode()实现。有了TreeMap实现,添加到映射的元素一定是可排序的。
为了优化HashMap空间的使用,可调优初始容量和负载因子。TreeMap没有调优选项,因为该树总处于平衡状态。

HashMap和TreeMap都实现Cloneable接口。
Hashtable类和Properties类是Map接口的历史实现。

HashTable: 实现一个映象,所有的键必须非空。为了能高效的工作,定义键的类必须实现hashcode()方法和equal()方法。这个类是前面java实现的一个继承,并且通常能在实现映象的其他类中更好的使用。

HashMap: 实现一个映象,允许存储空对象,而且允许键是空(由于键必须是唯一的,当然只能有一个)。

WeakHashMap: 实现这样一个映象:通常如果一个键对一个对象而言不再被引用,键/对象对将被舍弃。这与HashMap形成对照,映象中的键维持键/对象对的生命周期,尽管使用映象的程序不再有对键的引用,并且因此不能检索对象。

TreeMap: 实现这样一个映象,对象是按键升序排列的。

接口java.util.Map,包括3个实现类:HashMap、Hashtable、TreeMap。当然还有LinkedHashMap、ConcurrentHashMap 、WeakHashMap。
Map是用来存储键值对的数据结构,键值对在数组中通过数组下标来对其内容索引的,而键值对在Map中,则是通过对象来进行索引,用来索引的对象叫做key,其对应的对象叫value。

Map与Collection在集合框架中属并列存在

Map是一次添加一对元素,Collection是一次添加一个元素。
Map存储的是键值对。
Map存储元素使用put方法, Collection使用add方法。
Map集合没有直接取出元素的方法, 而是先转成Set集合, 再通过迭代获取元素。
Map集合中键要保证唯一性。

Map的两种取值方式keySet、entrySet

keySet:先获取所有键的集合, 再根据键获取对应的值。  
entrySet:先获取map中的键值关系封装成一个个的entry对象, 存储到一个Set集合中,再迭代这个Set集合, 根据entry获取对应的key和value。向集合中存储自定义对象。
HashMap: 内部结构是哈希表,不是同步的。允许null作为键,null作为值。
TreeMap: 内部结构是二叉树,不是同步的。可以对Map集合中的键进行排序。

HashMap概述

HashMap是基于哈希表的Map接口的非同步实现,此实现提供所有可选的映射操作,并允许使用null值和null键。它不保证映射的顺序,HashMap是Hashtable的轻量级实现(非线程安全的实现),它们都完成了Map接口。

HashMap的数据结构

哈希表是由数组+链表组成的,(jdk1.8之前的)数组的默认长度为16。
为什么是数组+链表?
数组对于数据的访问如查找和读取非常方便,链表对于数据插入非常方便。
链表可以解决hash值冲突(即对于不同的key值可能会得到相同的hash值)
数组里每个元素存储的是一个链表的头结点。而组成链表的结点其实就是hashmap内部定义的一个类:Entity
Entity包含三个元素:key,value和指向下一个Entity的next。

HashMap的存取

HashMap的存储 put:null key总是存放在Entry[]数组的第一个元素
元素需要存储在数组中的位置。先判断该位置上有没有存有Entity,没有的话就创建一个Entity<k,v>对象,新的Entity插入(put)的位置永远是在链表的最前面。
HashMap的读取 get:先定位到数组元素,再遍历该元素处的链表。
覆盖了equals方法之后一定要覆盖hashCode方法,原因很简单,比如,String a = new String(“abc”); String b = new String(“abc”); 如果不覆盖hashCode的话,那么a和b的hashCode就会不同。
HashMap是基于hashing的原理,使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当给put()方法传递键和值时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。

什么是Iterator?

一些集合类提供了内容遍历的功能,通过java.util.Iterator接口。这些接口允许遍历对象的集合。依次操作每个元素对象。当使用 Iterators时,在获得Iterator的时候包含一个集合快照。通常在遍历一个Iterator的时候不建议修改集合本省。

Iterator与ListIterator有什么区别?

Iterator:只能正向遍历集合,适用于获取移除元素。ListIerator:继承Iterator,可以双向列表的遍历,同样支持元素的修改。

什么是HaspMap和Map?

Map是接口,Java 集合框架中一部分,用于存储键值对,HashMap是用哈希算法实现Map的类。
HashMap与HashTable有什么区别?对比Hashtable VS HashMap
两者都是用key-value方式获取数据。Hashtable是原始集合类之一(也称作遗留类)。HashMap作为新集合框架的一部分在Java2的1.2版本中加入。它们之间有一下区别:
● HashMap和Hashtable大致是等同的,除了非同步和空值(HashMap允许null值作为key和value,而Hashtable不可以)。
● HashMap没法保证映射的顺序一直不变,但是作为HashMap的子类LinkedHashMap,如果想要预知的顺序迭代(默认按照插入顺序),你可以很轻易的置换为HashMap,如果使用Hashtable就没那么容易了。
● HashMap不是同步的,而Hashtable是同步的。
● 迭代HashMap采用快速失败机制,而Hashtable不是,所以这是设计的考虑点。

在Hashtable上下文中同步是什么意思?

同步意味着在一个时间点只能有一个线程可以修改哈希表,任何线程在执行hashtable的更新操作前需要获取对象锁,其他线程等待锁的释放。

怎样使Hashmap同步?

首先,HashMap不支持线程的同步。同步,指的是在一个时间点只能有一个线程可以修改hash表,任何线程在执行Hashtable的更新操作前都需要获取对象锁,其他线程则等待锁的释放。实现HashMap的同步的方法:
第一种方法:
直接使用Hashtable,但是当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以,效率很低,现在基本不会选择它了。
第二种方法:
HashMap可以通过此语句进行同步:Collections.synchronizeMap(hashMap);
HashMap可以通过Map m = Collections.synchronizedMap(new HashMap())来达到同步的效果。具体而言,该方法返回一个同步的Map,该Map封装了底层的HashMap的所有方法,使得底层的HashMap即使在多线程的环境中也是安全的。
第三种方法:
直接使用JDK 5 之后的 ConcurrentHashMap,如果使用Java 5或以上的话,请使用ConcurrentHashMap。
Hashtable的put和get方法均为synchronized的是线程安全的。
将HashMap默认划分为了16个Segment,减少了锁的争用。
写时加锁,读时不加锁减少了锁的持有时间。
volatile特性约束变量的值在本地线程副本中修改后会立即同步到主线程中,保证了其他线程的可见性。
value外,其他的属性都是final的,value是volatile类型的,都修饰为final表明不允许在此链表结构的中间或者尾部做添加删除操作,每次只允许操作链表的头部。

为什么HashMap是线程不安全的,实际会如何体现?

第一:如果多个线程同时使用put方法添加元素。假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。
第二:如果多个线程同时检测到元素个数超过数组大小*loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。
HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
HashMap 的 hashcode 的作用?什么时候需要重写?如何解决哈希冲突?查找的时候流程是如何?
当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值。实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了。HashMap有一个叫做Entry的内部类,它用来存储key-value对。上面的Entry对象是存储在一个叫做table的Entry数组中。table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。key的hashcode()方法用来找到Entry对象所在的桶。如果两个key有相同的hash值(即冲突),他们会被放在table数组的同一个桶里面(以链表方式存储)。key的equals()方法用来确保key的唯一性。key的value对象的equals()和hashcode()方法根本一点用也没有。
Hashtable 概述
也是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。
Hashtable中的映射不是有序的。
Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。

什么时候使用Hashtable,什么时候使用HashMap?

基本的不同点是:Hashtable同步,HashMap不是同步的,所以无论什么时候有多个线程访问相同实例的可能时,就应该使用Hashtable,反之使用HashMap。非线程安全的数据结构能带来更好的性能。
如果在将来有一种可能—你需要按顺序获得键值对的方案时,HashMap是一个很好的选择,因为有HashMap的一个子类 LinkedHashMap。所以如果你想可预测的按顺序迭代(默认按插入的顺序),你可以很方便用LinkedHashMap替换HashMap。反观要是使用的Hashtable就没那么简单了。同时如果有多个线程访问HashMap,Collections.synchronizedMap()可以代替,总的来说HashMap更灵活。
Hashtable与HashMap区别:HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。HashMap不能保证随着时间的推移Map中的元素次序是不变的。
(1)继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
(2)线程安全性不同
Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。
(3)是否提供contains方法
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。
Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
(4)key和value是否允许null值
其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。
Hashtable中,key和value都不允许出现null值。
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
(5)两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
(6)hash值不同
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
(7)内部实现使用的数组初始化和扩容方式不同
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。
HashMap中hash数组的默认大小是16,而且一定是2的指数。

Hashmap与Currenthashmap区别

Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个段,诸如get,put,remove等常用操作只锁当前需要用到的桶。Hashtable是线程安全的,它的方法是同步了的,可以直接用在多线程环境中。而HashMap则不是线程安全的。在多线程环境中,需要手动实现同步机制。

Hashmap与Linkedhashmap区别

Linkedhashmap是hashmap子类多了after和behind方法。
LinkedHashMap比HashMap多维护了一个链表。

TreeMap

TreeMap的底层使用了红黑树来实现,像TreeMap对象中放入一个key-value 键值对时,就会生成一个Entry对象,这个对象就是红黑树的一个节点,其实这个和HashMap是一样的,一个Entry对象作为一个节点,只是这些节点存放的方式不同。存放每一个Entry对象时都会按照key键的大小按照二叉树的规范进行存放,所以TreeMap中的数据是按照key从小到大排序的。

Arraylist 和 HashMap 如何扩容?负载因子有什么作用?如何保证读写进程安全?
HashTable默认初始11个大小,默认每次扩容的因子为0.75,
HashMap默认初始16个大小(必须是2的次方),默认每次扩容的因子为0.75。
ArrayList,默认初始10个大小,每次扩容是原容量的一半。
Vector,默认初始10个大小,每次扩容是原容量的两倍,
StringBuffer、StringBuilder默认初始化是16个字符,默认增容为(原长度+1)*2。
负载因子有什么作用,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷。

HashMap、LinkedHashMap、TreeMap、WeakHashMap

HashMap里面存入的键值对在取出时没有固定的顺序,是随机的。一般而言,在Map中插入、删除和定位元素,HashMap是最好的选择。
由于TreeMap实现了SortMap接口,能够把它保存的记录根据键排序,因此,取出来的是排序后的键值对,如果需要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
LinkedHashMap是HashMap的一个子类,如果需要输出的顺序和输入相同,那么用LinkedHashMap可以实现、它还可以按读取顺序来排列。
WeakHashMap中key采用的是“弱引用”的方式,只要WeakHashMap中的key不再被外部引用,它就可以被垃圾回收器回收。
而HashMap中key采用的是“强引用的方式”,当HashMap中的key没有被外部引用时,只有在这个key从HashMap中删除后,才可以被垃圾回收器回收。

“你用过HashMap吗?” “什么是HashMap?你为什么用到它?”

几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。

你知道HashMap的工作原理吗? 你知道HashMap的get()方法的工作原理吗?

“HashMap是基于hashing的原理,使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象(其实就是得到value,在java里嘛,万物皆对象)。当给put()方法传递键和值时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

当两个对象的hashcode相同会发生什么?

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

如果两个key的hashcode相同,你如何获取值对象?

当调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
注意:面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap在链表中存储的是键值对,否则他们不可能回答出这一题。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。

Hashmap的存储过程?

HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后通过indexFor(hash,length)求该key-value对的存储位置,计算方法是先用hash&0x7FFFFFFF后,再对length取模,这就保证每一个key-value对都能存入HashMap中,当计算出的位置相同时,由于存入位置是一个链表,则把这个key-value对插入链表头。HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。

HashMap扩容问题?

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。
HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。
默认加载因子为0.75,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

HashMap的复杂度

HashMap整体上性能都非常不错,但是不稳定,为O(N/Buckets),N就是以数组中没有发生碰撞的元素。

什么叫做快速失败特性?

从高级别层次来说快速失败是一个系统或软件对于其故障做出的响应。一个快速失败系统设计用来即时报告可能会导致失败的任何故障情况,它通常用来停止正常的操作而不是尝试继续做可能有缺陷的工作。当有问题发生时,快速失败系统即时可见地发错错误告警。在Java中,快速失败与iterators有关。如果一个iterator在集合对象上创建了,其它线程欲“结构化”的修改该集合对象,并发修改异常 (ConcurrentModificationException) 抛出。

为什么Vector类认为是废弃的或者是非官方地不推荐使用?或者说为什么我们应该一直使用ArrayList而不是Vector?
应该使用ArrayList而不是Vector是因为默认情况下你是非同步访问的,Vector同步了每个方法,你几乎从不要那样做,通常有想要同步的是整个操作序列。同步单个的操作也不安全(如果你迭代一个Vector,你还是要加锁,以避免其它线程在同一时刻改变集合).而且效率更慢。当然同样有锁的开销即使你不需要,这是个很糟糕的方法在默认情况下同步访问。你可以一直使用Collections.sychronizedList来装饰一个集合。
事实上Vector结合了“可变数组”的集合和同步每个操作的实现。这是另外一个设计上的缺陷。Vector还有些遗留的方法在枚举和元素获取的方法,这些方法不同于List接口,如果这些方法在代码中程序员更趋向于想用它。尽管枚举速度更快,但是他们不能检查如果集合在迭代的时候修改了,这样将导致问题。尽管以上诸多原因,Oracle也从没宣称过要废弃Vector。

哪些集合类是线程安全的?

线程安全:就是当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用。防止出现数据不一致或者数据被污染的情况。
线程不安全:就是不提供数据访问时的数据保护,多个线程能够同时操作某个数据,从而出现数据不一致或者数据污染的情况。
对于线程不安全的问题,一般会使用synchronized关键字加锁同步控制。
线程安全工作原理: jvm中有一个main memory对象,每一个线程也有自己的working memory,一个线程对于一个变量variable进行操作的时候, 都需要在自己的working memory里创建一个copy,操作完之后再写入main memory。 当多个线程操作同一个变量variable,就可能出现不可预知的结果。
而用synchronized的关键是建立一个监控monitor,这个monitor可以是要修改的变量,也可以是其他自己认为合适的对象(方法),然后通过给这个monitor加锁来实现线程安全,每个线程在获得这个锁之后,要执行完加载load到working memory 到 use && 指派assign 到 存储store 再到 main memory的过程。才会释放它得到的锁。这样就实现了所谓的线程安全。

线程安全(Thread-safe)的集合对象:

Vector 线程安全
HashTable 线程安全
StringBuffer 线程安全

非线程安全的集合对象:

ArrayList
LinkedList
HashMap
HashSet
TreeMap
TreeSet
StringBulider

为什么Set、List、map不实现Cloneable和Serializable接口?

集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它 自己的方式对元素进行保存和排序。有的集合类允许重复的键,有些不允许。
克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由 集合类的具体实现来决定如何被克隆或者是序列化。

HashMap的key必须惟一,value可重复

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值