面试题中菜鸟应该知道的集合类

175 阅读15分钟

面试题中菜鸟应该知道的集合类以及方法

作为一个刚入行的菜鸟,多少会对数组集合这种东西有点不了解,接下来我们来看看java集合类大概分为哪些吧。

Collection ├List │ ├LinkedList │ ├ArrayList │ └Vector │ └Stack └Set ├HashSet └LinkedHashSet Map ├Hashtable ├HashMap ├WeakHashMap └LinkedHashMap 以上是JDK在java.util包下提供的集合类及其结构,最上层基础接口是Collection和Map, Collection分成2个子接口,分别是List、Set,最下层加黑体的就是实体集合类,Map下就是其实体集合 类。 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(); // 得到下一个元素 }
while(it.hasNext()) { 
	Object obj = it.next(); // 得到下一个元素
}

Collection接口还提供了如下方法:

Object[] toArray(); //返回对象数组
<T> T[] toArray(T[] a); //返回指定对象的数组
boolean contains(Object o); //是否包含某元素
boolean add(E e); //添加元素
boolean remove(Object o); //删除元素
boolean containsAll(Collection<?> c); //主集合是否包含参数集合中的全部元素
boolean addAll(Collection<? extends E> c); //将参数集合中的全部元素加入主集合
boolean removeAll(Collection<?> c); //从主集合中删除参数集合中的全部元素
void clear(); //清空主集合
default Spliterator<E> spliterator(); //可分割迭代器,JDK1.8之后引入,特别针对并发加强

List接口除Collection接口方法外,还提供了:

default void sort(Comparator<? super E> c); //将参数集合排序插入主列表中
int indexOf(Object o); //返回指定对象在列表中的第一个序号
int lastIndexOf(Object o); //返回指定对象在列表中的最后一个序号
ListIterator<E> listIterator(); //返回列表的排序迭代器

Set接口除Collection接口方法外,并没有提供更多的接口。 Map接口提供了以下方法:

boolean isEmpty(); //判断主map对象是否为空
boolean containsKey(Object key); //判断主map中是否包含指定的key值
boolean containsValue(Object value); //判断主map中是否包含指定的value对象
V get(Object key); //根据key值从map中取到value对象
V put(K key, V value); //根据key值将value放置到map中
V remove(Object key); //根据key值从map中除去value对象
void putAll(Map<? extends K, ? extends V> m); //将子map中所有的对象都置入到主map中
void clear(); //将主map清空
Set<K> keySet(); //返回map的key值集合
Collection<V> values(); //返回map的value集合

我们来接着看一下集合类中哪些是线程安全的

下面是这些线程安全的同步的类:

vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在 web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。 statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 ConcurrentHashMap:是一种高效但是线程安全的集合。 除了这些之外,其他的都是非线程安全的类和接口。 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非 线程安全的类。 为什么版本升级会出现一些线程不安全的集合呢?因为线程不安全的集合普遍比线程 安全的集合效率高的多。随着业务的发展,特别是在web应用中,为了提高用户体验减少用户的等待时 间,页面响应速度(也就是效率)是优先考虑的。而且对线程不安全的集合加锁以后也能达到安全的效果 (但是效率会低,因为会有锁的获取以及等待)。其实在jdk源码中相同效果的集合线程安全的比线程不安 全的就多了一个同步机制,但是效率上却低了不止一点点,因为效率低,所以已经不太建议使用了。

List, Set, Map是否继承自Collection接口?存取元素时,各有什么特点?

Map并非继承Collection接口。 Set里面不允许有重复的元素 存元素:add方法有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该 元素时,则返回true;当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返 回结果为false。 取元素:没法说取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。 List表示有先后顺序的集合 存元素:多次调用add(Object)方法时,每次加入的对象按先来后到的顺序排序,也可以插队,即 调用add(int index,Object)方法,就可以指定当前对象在集合中的存放位置。 取元素:方法1:Iterator接口取得所有,逐一遍历各个元素 方法2:调用get(index i)来明确说明取第几个。 Map是双列的集合 存放用put方法:put(obj key,obj value),每次存储时,要存储一对key/value,不能存储重复的 key,这个重复的规则也是按equals比较相等。 取元素:用get(Object key)方法根据key获得相应的value。 也可以获得所有的key的集合,还可以 获得所有的value的集合, 还可以获得key或value组合成的集合,在分别获取迭代器一一遍历。 List以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,无序排列。Map 保存keyvalue值,key值不可重复,value可重复。

Collection和Collections的区别是什么?

1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。 Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最 大化的统一操作方式。 List,Set,Queue接口都继承Collection。 直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些 基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。 2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排 序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于 Java的Collection框架。

HashMap和Hashtable的区别

HashTable

  • 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改 数据时锁住整个HashTable,效率低
  • 初始size为11,扩容:newsize = olesize*2+1
  • 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生 无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)
    在这里插入图片描述
    图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。 HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多, Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机 制。HashMap内部有: 变量size,它记录HashMap的底层数组中已用槽的数量; 变量threshold,它是HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量* 加载因子) 变量DEFAULT_LOAD_FACTOR = 0.75f,默认加载因子为0.75 HashMap扩容的条件是:当size大于threshold时,对HashMap进行扩容 扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加 到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。因此,我们在用HashMap的时,最好能提前预估下HashMap中元素的个数,这样有助于提高HashMap的性能。 HashMap共有四个构造方法。构造方法中提到了两个很重要的参数:初始容量和加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中槽的数量(即哈希数组的长度),初始容量是创建哈希表时的容量(从构造函数中可以看出,如果不指明,则默认为16),加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 resize 操作(即扩容)。 下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。 另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个 数,且最大值不能超过2的30次方。

我们来看看Array 和 ArrayList 他们之间有什么区别

Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。 Array大小是固定的,ArrayList的大小是动态变化的。 ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时 候,这种方式相对比较慢。

存储内容比较:

  • Array数组可以包含基本类型和对象类型,
  • ArrayList却只能包含对象类型。

但是需要注意的是:Array数组在存放的时候一定是同种类型的元素。ArrayList就不一定了,因为 ArrayList可以存储Object。

空间大小比较:

  • 它的空间大小是固定的,空间不够时也不能再次申请,所以需要事前确定合适的空间大小。
  • ArrayList的空间是动态增长的,如果空间不够,它会创建一个空间比原空间大约0.5倍的新数组, 然后将所有元素复制到新数组中,接着抛弃旧数组。而且,每次添加新的元素的时候都会检查内部 数组的空间是否足够。

方法上的比较:

  • ArrayList作为Array的增强版,当然是在方法上比Array更多样化,比如添加全部addAll()、删除全部 removeAll()、返回迭代器iterator()等。

适用场景:

如果想要保存一些在整个程序运行期间都会存在而且不变的数据,我们可以将它们放进一个全局数组 里,但是如果我们单纯只是想要以数组的形式保存数据,而不对数据进行增加等操作,只是方便我们进 行查找的话,那么,我们就选择ArrayList。而且还有一个地方是必须知道的,就是如果我们需要对元素 进行频繁的移动或删除,或者是处理的是超大量的数据,那么,使用ArrayList就真的不是一个好的选 择,因为它的效率很低,使用数组进行这样的动作就很麻烦,那么,我们可以考虑选择LinkedList。

ArrayList、Vector、LinkedList 的存储性能和特性?

ArrayList 和Vector他们底层的实现都是一样的,都是使用数组方式存储数据,此数组元素数大于实际 存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动 等内存操作,所以索引数据快而插入数据慢。

Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList 差,因此已经是Java中的遗留容器。

LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可 以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序 号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较 快。

Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、 Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安 全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList 方法将其转换成线程安全的容器后再使用。

如何实现数组和 List 之间的转换?

List转数组:toArray(arraylist.size())方法 数组转List:Arrays的asList(a)方法

迭代器 Iterator 是什么?Iterator 怎么使用?有什么特点?

迭代器(Iterator), 提供了一些方法专门处理集合中的元素.例如删除和获取集合中的元素.该对象就叫做 迭代器(Iterator). 该对象比较特殊,不能直接创建对象(通过new),该对象是以内部类的形式存在于 每个集合类的内部。 特点 功能简单,并且能单向移动,对已集合类中的任何一个实现类,都可以返回这样一个Iterator对 象。 使用方法 iterator()要求容器返回一个 Iterator。第一次调用Iterator 的next()方法时,它返回序列的第一个元 素。 使用next()获得序列中的下一个元素。 使用hasNext()检查序列中是否还有元素。 使用remove()将上一次返回的元素从迭代器中移除。 为什么要用迭代器 Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出 来,从而避免向客户端暴露集合的内部结构。 例如,如果没有使用Iterator,遍历一个数组的方法是使用索引:

 for(int i=0; i<array.size(); i++) { ... get(i) ... } 

客户端都必须事先知道集合的内部结构,访问代码和集合本身是紧耦合,无法将访问逻辑从集合类和 客户端代码中分离出来,每一种集合对应一种遍历方法,客户端代码无法复用。 更恐怖的是,如果以后需要把ArrayList更换为LinkedList,则原来的客户端代码必须全部重写。 为解决以上问题,Iterator模式总是用同一种逻辑来遍历集合:

 for(Iterator it = c.iterater(); it.hasNext(); ) { ... }

Iterator 和 ListIterator 有什么区别?

Iterator和ListIterator主要区别在以下方面:

  1. ListIterator有add()方法,可以向List中添加对象,而Iterator不能
  2. ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有 hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
  3. ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功 能。
  4. 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍 历,不能修改。