Java中的集合

1. Java中的ListSetMap有何区别?

Java中的集合主要分为三类:List(列表)、Set(集合)、Map(键值对), 三者的关系如下所示:

Map、List、Set

以下是Collection的源码:

public interface Collection<E> extends Iterable<E> {

  int size();
  boolean isEmpty();
  boolean contains(Object o);
  Iterator<E> iterator();
  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();
  
  default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
      if (filter.test(each.next())) {
        each.remove();
        removed = true;
      }
    }
    return removed;
  }
  ...
}
  • List中的元素以线性方式存储,是有顺序的,值可以重复;
  • Map中的元素是一种键和值映射的集合,存储是无序的,每一个元素都是一个键值对,其中,键不允许重复,值可以重复;
  • Set中的元素是无序的,值不允许重复。但元素在集合中的位置是由元素的hashCode决定,即位置是固定的(但是这个位置不是用户可以控制的,所以对于用户来说Set中的元素还是无序的)
2 ArrayListLinkedList的区别?
  • ArrayList是基于数组的数据结构,LinkedList是基于链表的数据结构;
  • ArrayList适用于查询操作;LinkedList适用于插入和删除操作;
3 请说一下HashMapHashtable的区别

Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。

3.1 父类不同

Hashtable是基于陈旧的Dictionary类的,HashMapJava 1.2引进的,继承了抽象类AbstractMap实现了Map接口:

public class Hashtable<K,V> extends Dictionary<K,V> 
  implements Map<K,V>, Cloneable, java.io.Serializable { }

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {  }
3.2 线程安全不一样

Hashtable中的方法是同步的,而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就需要增加同步处理:

3.3 允不允许null

Hashtable中,keyvalue都不允许出现null值,否则会抛出NullPointerException异常。而在HashMap中,null可以作为键,这样的键只有一 个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

3.4 遍历方式的内部实现上

不同HashtableHashMap都使用了Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

3.5 哈希值的使用不同

Hashtable直接使用对象的hashCode。而HashMap重新计算hash值。

3.6 内部实现方式的数组的初始大小和扩容的方式不一样

HashTable中的hash数组初始大小是11,增加的方式是old * 2 + 1HashMaphash数组的默认大小是16,而且一定是2的指数。

4 ArrayList的扩容机制

先来看看构造方法,以下是无参数构造方法:

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  
  transient Object[] elementData; 
  
  // Constructs an empty list with an initial capacity of ten.
  public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  }

}

这是无参数的构造方法,是对elementData(元素的数组)进行赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组,我们暂时叫做默认空数组),明明是空数组,但是注释确实创建容量是10

再来看带参数的构造方法:

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
      this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
      this.elementData = EMPTY_ELEMENTDATA;
    } else {
      throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
  }

}

再看另一个构造方法:

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  
  public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
      // c.toArray might (incorrectly) not return Object[] (see 6260652)
      if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
      // replace with empty array.
      this.elementData = EMPTY_ELEMENTDATA;
    }
  }
}

就是直接将集合转为数组赋值给elementData,同时对size赋值,并且如果size不等于0时,c.toArray might (incorrectly) not return Object[] (see 6260652);等于0时,直接赋值空的数组。

接着看add方法,先看一个参数的:

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
  }

}

就是ensureCapacityInternal(size + 1)并且将e添加到size++的位置,我们来看ensureCapacityInternal(size + 1)方法:

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
  }

}

DEFAULT_CAPACITY = 10。也就是当空参数时创建ArrayList时,minCapacityDEFAULT_CAPACITY的最大值,显然创建无参数构造方法时minCapacity = 0。这时结果= 10,然后看ensureExplicitCapacity(minCapacity):

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
      grow(minCapacity);
  }

}

显然调用无参数构造方法时minCapacity - elementData.length > 0是成立的,我们再看grow(minCapacity)

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

  private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
      newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
      newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
  }
}

oldCapacity是元素个数newCapacityoldCapacity + oldCapacity / 2,即oldCapacity1.5倍。当1.5倍的元素个数小于minCapacity时,newCapacity = minCapacity。显然创建无参数ArrayList就是这种情况,这就解决了开头的第一个问题。调用无参数构造方法创建的是10个元素的长度。

我们再看当1.5倍元素个数大于MAX_ARRAY_SIZE时,newCapacity = hugeCapacity(minCapacity):

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
      throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
      Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
  }
}

到这里我们可以总结一下了:无参构造方法调用add之后创建的是10个长度的数组。有参数则直接创建指定长度的数组。扩容时,小于MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8一次扩容1.5倍,超过则直接Integer.MAX_VALUE

5 HashMap的实现原理

HashMap实际上是一个“链表散列”的数据结构,即数组和 链表的结合体。 它是基于哈希表的Map接口的非同步实 。

  • 数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
  • 链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
  • HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易;

例如我们以下图为例,看一下HashMap的内部存储结构:

HashMap的内部存储结构

关于HashMap的存取过程,可参照下图:

HashMap的存取过程

  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals
  4. 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

6 LinkedHashMap的工作原理和使用方式

查看LinkedHashMap源码发现是继承HashMap实现Map接口。也就是HashMap的方法LinkedMap都有。LinkHashMapHashMap的主要区别是:LinkedHashMap是有序的,HashMap是无序的。 LinkedHashMap通过维护一个双向链表实现有序,也正是因为要维护这个链表,内存上有更大的开销。

线性结构分为顺序结构和链表结构:

  • 顺序结构:在内存中是一块完整有序内存。所以我们在查询的时候时候直接索引index,便可找到要查询的数据,速度非常快,缺点是插入删除慢。有点类似班级排队时(一列纵队),每个人都知道自己在第几个位置。老师只要说第三个位置,那这个同学立马知道老师要找的是自己。这时候要插入一个同学到第二个位置,所以之前第二个位置开始往后的每个同学的位置都要+1。所以比较慢;
  • 链表结构:通过结点头记录该结点的上一个结点和下 一个下一个结点(就是传统的双链表,单链表就是只 记录下一个结点,循环链表就是最后一个结点的下一 个结点指向第一个结点)。正是因为这种关系,所以 链表结构不需要一块完整的内存,而且插入删除相对快,但是查询相对慢。但是因为要维护结点头,所以 内存开销相对大一点。有点类似于班级排队时,每个 人虽然不知道自己的位置,但是知道自己前面是谁和后面是谁。当要插入一个同学b时到c前面时,只要c同学记住自己之前是a,现在换成bb记住自己前面是a,后面是c。所以想对来说插入很快。删除类似。但 是当老师按位置查询时,就要先从第一个开始计数, 知道找到老师要找的数字。所以查询慢。

7 ConcurrentHashMap的理解

并发集合常见的有ConcurrentHashMapConcurrentLinkedQueueConcurrentLinkedDeque等。并发集合位于java.util.concurrent包下 ,是jdk1.5之后才有 的。

Java中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了synchronized同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率ConcurrentHashMap是线程安全的HashMap的实现,默认构造同样有initialCapacityloadFactor属性,不过还多了一个concurrencyLevel属性,三属性默认值分别为160.7516其内部使用锁分段技术,维持这锁Segment的数组, 在Segment数组中又存放着Entity[]数组,内部hash算法将数据较均匀分布在不同锁中。

put操作:并没有在此方法上加上synchronized,首先对key.hashcode进行hash操作,得到keyhash值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可 允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

get(key)首先对key.hashCode进行hash操作,基于其值找到对应的Segment对象,调用其get方法完成当前操作。而Segmentget操作首先通过hash值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry

在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry产生不一致性,那么ConcurrentHashMap是如何保证的?

对象数组大小的改变只有在put操作时有可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如 HashEntry对象数组大小发生改变,读操作可看到最新的对象数组大小。

在获取到了HashEntry对象后,怎么能保证它及其next属性构成的链表上的对象不会改变呢?

这点ConcurrentHashMap采用了一个简单的方式,即HashEntry对象中的hashkeynext属性都是final的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于next属性构建的链表是不会发生变化的。

ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于putremove等改变集合对象的操作,基于volatileHashEntry链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap能够保持极好的 并发支持,尤其是对于读远比插入和删除频繁的Map而言,而它采用的这些方法也可谓是对于Java内存模型、并发机制深刻掌握的体现。

总结:ConcurrentHashMap比起HashMap,是线程安全的,比起Hashtable是高效的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值