文章目录
1 Collection集合接口
存储单值的顶级接口。该接口下最常用的一些方法:
public boolean add(E e)
添加元素;public boolean addAll(Collection<? extends E> c)
添加所有元素public boolean clear()
清空集合,让根节点为空。public boolean contains(Object o)
查询数据是否存在。需要equals方法支持。public int size()
获取数据长度public Object[] toArray()
将集合变为对象数组返回public Iterator<E> iterator()
获得集合的iteratorpublic <T> T[] toArray(T[] a)
将集合变为指定类型的对象数组返回public boolean remove(Object o)
删除元素
在JDK1.5之前,通常都是操作的Collection接口。在JDK1.5之后,应该操作Collection的两个子接口:
- List接口:允许数据重复
- Set接口:不允许数据重复
2 List接口
List接口对Collection接口的方法进行了扩充。
public void add(int index, E element)
在指定位置添加数据public E get(int index)
得到指定下标的数据public E set(int index, E element)
将指定下标的数据修改public int indexOf(Object o)
获得元素的下标public ListIterator<E> listIterator()
返回ListIterator接口对象。
在JDK 9之后,List增加了一系列的静态方法,可以快速创建List。
public static <E> List<E> of(E... elements)
List有三个子类:
- ArrayList(90%):
- LinkedList(8%):
- Vector(2%):
2.1 ArrayList子类
继承关系
- ArrayList本质上是基于数组实现的(这也是为什么可以通过set(int index, E element)和get(int index)等操作下标的方法操作)。
- 如果使用ArrayList的无参构造方法,会默认构造一个空数组。在进行第一次数据追加时,比较增加的数据长度和默认长度的大小关系,取较大的一个数值作为容量进行新数组开辟。注意:默认长度为10。
在JDK1.9之后:
ArrayList默认的构造只会使用默认的空数组,在进行第一次数据追加时才会开辟数组,默认的开辟长度为10。
在JDK1.9之前:
ArrayList默认的构造实际上就会开辟大小为10的数组。
-
如果调用有参构造方法初始化initialCapacity,则会构造一个该容量大小的数组。
-
如果在进行数据追加时发现数组长度不够,则会进行新数组开辟并且将原数组数据拷贝到新数组中。数组长度的增长量是数组长度的1/2。假设原始长度为10,则下一次增长量为5(即新数组长度为15).
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
- 在使用ArrayList的时候一定要估算你的数据量,如果超过10个则使用有参构造创建,避免垃圾数组空间产生。
2.2 LinkedList子类
Linkedlist是一个双向链表,除了常规的add, set, get, remove方法之外,还提供了一系列***First
, ***Last
方法,用于对头部或尾部进行操作。例如:addFirst, removeFirst, getLast等等。
此外,这个类还提供了栈的相关操作,例如push,pop等。
继承关系
如果只观察功能会发现LinkedList和ArrayList是一样的。但两者在实现上有很大区别。
- LinkedList是基于链表实现的
- LinkedList不提供具有初始容量的有参构造方法(只有无参构造方法)
- 所有数据保存在Node类里。
- add方法可以添加null
- add方法添加数据时,直接追加到链表末尾(通过linkLast方法)。为了提高效率,在LinkedList类之中始终保存最后一个节点,这样避免了追加操作时大量的递归操作。
ArrayList和LinkedList有什么区别?
- 前者基于数组实现,后者基于链表实现;
- 通过get(int index)方法获取数据时,ArrayList的时间复杂度为
O(1)
,LinkedList的时间复杂度为O(n)
;- ArrayList默认的初始化数组长度为10,当数组长度不够时进行动态扩容,保存大数据时可能会造成垃圾的产生与性能下降。这个时候可以使用LinkedList。
2.3 Vector子类
这个类从JDK1.0就出现了。考虑到JDK1.2之后很多开发者已习惯了Vector,且很多系统也使用了这个类,因此Java将其保留了下来。
从定义上来看Vector类和ArrayList类是一样的。
Vector类如果使用无参构造方法,则一定会开辟一个长度为10的数组。而后其余的操作和ArrayList是相同的。
Vector和ArrayList的区别:
Vector类中的操作方法都是使用synchronized进行同步处理,而ArrayList之中没有。因此Vector类之中的方法在多线程访问过程中是线程安全的,但性能比ArrayList低。
3 Set接口
Set接口不能像List接口那样操作Index。
JDK1.9之后Set接口也增加了许多的of静态方法进行实例化。当使用of方法保存有重复元素时,会抛出异常。
为Set接口常规的实例化是通过子类实现的。两个重要子类:HashSet和TreeSet。
3.1 HashSet子类
用的最多的子类,最大的特点是保存的数据是无序的。
继承结构:
3.2 TreeSet子类
与HashSet的不同是TreeSet保存的数据是有序的。添加数据时默认按照数据升序排列。
TreeSet保存自定义类对象时需要实现Comparable接口。覆写compareTo方法时需要注意:必须将类的所有属性进行比较,否则当两个对象的一个或几个属性相同时,TreeSet会认为两者是相同的从而造成部分数据不能正确存入。
结论:TreeSet依靠compareTo方法确认重复。
由于TreeSet需要让数据对象的compareTo方法考虑所有的类属性,当类属性很多时非常麻烦,因此开发中首选HashSet保存数据。
3.3 重复元素说明
在Java程序之中真正重复元素的判断是利用hashCode()和equals()方法共同判断的,只有在有排序的情况下(比如TreeSet)才会利用Comparable接口来实现。
4 输出
4.1 Iterator迭代输出(95%)
通过Iterable接口的iterator()方法进行实例化。
在迭代输出的时候如果使用了Collection类提高的remove方法会造成并发更新异常,导致程序异常。此时应该使用iterator.remove()方法进行删除。
4.2 ListIterator双向迭代输出(0.1%)
Iterator只允许从前向后单向输出,这个类可以进行双向输出。这个类在Collection类中没有实例化方法,但在List子接口中有。换句话说,这个类是专门为List的子类设计的。
两个方法:hasPrevious()
、previous()
这个实现是类似于指针的。因此想实现从后向前输出,必须先进行从前向后输出。
4.3 Enumeration枚举输出
JDK1.0就有了,这个输出接口只为Vector一个类服务。
Enumeration类对象通过Vector中的elements()方法实例化。
遍历Enumeration对象时类似于Iterator方法。
Enumeration<String> enu = v.elements();
while (enu.hasMoreElements()) {
System.out.println(enu.nextElement());
}
继承结构:
4.4 ForEach输出(4.9%)
for(String str: list){
...
}
5 Map接口
保存二元偶对象的最大父接口。(Collection只能保存单个对象)。
在开发里面,Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。
Map接口简介。
该接口为一个独立的父接口,在接口对象实例化时需要设置K和V。Map里有以下的操作方法需要记住。
public V put(K key, V value);
public V get(Object key);
思考:为什么get方法里的key是Object类型?
因为Map在查找数据时是找到Key相同的Entry,这里的key比较时调用的正是Object类中提供的equals和hashCode方法,因此用Object类型。
其次,如果指定Key为K类型,考虑如下的代码
class S<K>{
public void contains(K k){
System.out.println("S<K>,contains(K k)");
}
}
class Foo{
}
class SubFoo extends Foo{
}
public void doSomeReading(S<? extends Foo> foos) {
Foo f = new Foo();
SubFoo subFoo = new SubFoo();
foos.contains(f); //这里eclipse会提示出错,这里只有填null时才不会报错
foos.contains(subFoo); //同样错误
foos.contains(null);
}
原来在S<K>的定义中,我们明确contains(K k)函数只能接受一个明确类型的参数。但是在doSomeReading函数中,编译器无法确定到底是什么类型,它是Foo类型,还是SubFoo类型,还是SubSubFoo类型?编译器无从得知,所以它只允许null类型的参数。
public Set<Map.Entry<K,V>> entrySet()
将Map集合转成Set集合。public Set<K> keySet()
获得Key的set集合
由此可见,Map中的key是不可能重复的。
从JDK1.9之后,Map接口里也提供了一系列静态的of方法供用户使用。该方法如果传入重复的key则会报异常:IllegalArgumentException,如果传入null则会报异常:NullPointerException。
正常使用Map应该使用其子类实例化。
5.1 HashMap子类
该类的主要特点是无序存储。(Hash —— 无序,Tree——有序)。存储的时候采用对象数组+链表/红黑树
的形式保存。对象数组的每一个位置代表一个桶。对象的Hash码经过一系列计算对应到对象数组的某一个桶下。
HashMap采⽤Entry数组来存储key-value对,每⼀个键值对组成了⼀个Entry实体,Entry类实际上是⼀个单向的链表结 构,它具有Next指针,可以连接下⼀个Entry实体。 只是在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树!
- 第一问: 为什么使用链表+数组:要知道为什么使用链表首先需要知道Hash冲突是如何来的:
答: 由于我们的数组的值是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。
- 第二问 我⽤LinkedList代替数组结构可以吗?
对于题目的意思是说,在源码中我们是这样的
java Entry[] table=new Entry[capacity]; // entry就是一个链表的节点
现在进行替换,进行如下的实现
java List<Entry> table=new LinkedList<Entry>();
是否可以行得通? 答案当然是肯定的。
- 第三问 那既然可以使用进行替换处理,为什么有偏偏使用到数组呢?
因为⽤数组效率最⾼! 在HashMap中,定位节点的位置是利⽤元素的key的哈希值对数组⻓度取模得到。此时,我们已得到节点的位置。显然数组的查找效率⽐LinkedList⼤(底层是链表结构)。
那ArrayList,底层也是数组,查找也快啊,为啥不⽤ArrayList? 因为采⽤基本数组结构,扩容机制可以⾃⼰定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率⾼。 ⽽ArrayList的扩容机制是1.5倍扩容(这一点我相信学习过的都应该清楚),那ArrayList为什么是1.5倍扩容这就不在本⽂说明了。
5.1.1 Hash冲突:得到下标值:
我们都知道在HashMap中 使用数组加链表,这样问题就来了,数组使用起来是有下标的,但是我们平时使用HashMap都是这样使用的:
HashMap<Integer,String> hashMap=new HashMap<>();
hashMap.put(2,"dd");
可以看到的是并没有特地为我们存放进来的值指定下标,那是因为我们的hashMap对存放进来的key值进行了hashcode(),生成了一个值,但是这个值很大,我们不可以直接作为下标,此时我们想到了可以使用取余的方法,例如这样:
key.hashcode()%Table.length;
即可以得到对于任意的一个key值,进行这样的操作以后,其值都落在0-Table.length-1 中,但是 HashMap的源码却不是这样做?
它对其进行了与操作,对Table的表长度减一再与生产的hash值进行相与:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
我们来画张图进行进一步的了解。
这里我们也就得知为什么Table数组的长度要一直都为2的n次方,只有这样,减一进行相与时候,才能够达到最大的n-1值。
举个栗子来反证一下:
我们现在 数组的长度为 15 减一为 14 ,二进制表示 0000 1110 进行相与时候,最后一位永远是0,这样就可能导致,不能够完完全全的进行Table数组的使用。违背了我们最开始的想要对Table数组进行最大限度的无序使用的原则
,因为HashMap为了能够存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表⻓度⼤致相同。
此时还有一点需要注意的是: 我们对key值进行hashcode以后,进行相与时候都是只用到了后四位,前面的很多位都没有能够得到使用
,这样也可能会导致我们所生成的下标值不能够完全散列
。
解决方案:将生成的hashcode值的高16位与低16位进行异或运算,这样得到的值再进行相与,一得到最散列的下标值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.1.2 保存数据put方法。
使用HashMap保存数据时(put方法),可以保存null数据(key或value都可以为null)。
当存入重复key时,会将新的Value覆盖掉旧的Value。同时返回的旧的Value。
存入不重复的key时,返回null。
5.1.3 分析源码
(a) 构造方法
public HashMap(){
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
当使用无参构造的时候,会出现有一个loadFactor的属性,默认值是DEFAULT_LOAD_FACTOR = 0.75f
。
(b) put方法
public V put(K key, V value){
return putVal(hash(key), key, value, false, true);
}
在使用put方法进行数据保存时,会调用一个putVal方法,对key进行hash处理。
putVal方法里提供一个Node节点类进行数据保存。在这个方法里还调用了一个resize方法实现数据扩容。
面试题一:在进行HashMap的put操作时,如何实现容量扩充的?
- 首先HashMap定义了一个初始化容量DEFAULT_INITIAL_CAPACITY = 16。即默认初始化容量为16。
- 当数据量达到
容量 * 阈值(DEFAULT_LOAD_FACTOR)
的时候(第一次扩充的时候即达到16 * 0.75 = 12),就会进行容量的扩充。- 采用成倍模式扩容。即新的容量是旧容量的2倍。
面试题二:请解释HashMap的工作原理(JDK1.8之后开始的,因为1.8之后考虑到海量大数据的保存)。
在HashMap之中进行数据存储的依然是Node类。那么这种情况下只能使用两种结构:链表或二叉树。
当数据不超过8个时,采用链表保存;超过8个后,将链表转为红黑树并通过左旋右旋保证查询的性能。当数据量减少到6时,会从红黑树转为链表
5.2 LinkedHashMap子类
HashMap本身保存数据是无序的(有序与否对Map没有影响)。如果希望保存的数据是按照其增加的顺序保存,则可以采用LinkedHashMap子类(基于链表实现)。既然是链表保存,则尽量不要保存特别大的数据量。
在开发中首选HashMap,如果确定了数据保存量则可以使用LinkedHashMap。
5.3 Hashtable子类
Hashtable是从JDK1.0开始提供的,与Vector、Enumeration属于最早一批的动态数组实现类。
Hashtable与HashMap的区别:
- HashMap中的方法都属于异步操作(非线程安全),可以保存null数据(key和Value都可以为null)。
- Hashtable中的方法都属于同步方法(线程安全),保存key和value都不能为空
5.4 TreeMap子类
TreeMap属于有序的Map集合类型,它可以按照key进行排序。这时Key需要实现Comparable接口。
注意:TreeMap不可以保存key为null的数据。
6 Map.Entry内部接口
对于List(LinkedList)而言,数据通过Node节点类按链表形式保存。而在HashMap里虽然也可以见到Node内部类定义,但通过源码可以发现Node内部类本质上实现了Map.Entry接口。
static class Node<K, V> implements Map.Entry<K, V> {}
所以可以得出结论:所有的key和value数据都被封装在Map.Entry内部接口中。该接口提供了两个重要方法。
public K getKey();
public V getValue();
7 Iterator输出Map集合
首先,Map接口中并没有提供返回iterator的方法。
有以下几种方法:
一、先得到keySet,然后通过get(K key)方法输出value;
二、返回Map.Entry的set,然后通过Iterator遍历该set。
8 Hash冲突
- 使用put方法保存数据时,会先将key做一个hash处理,最后保存的数据也保存了这个hash值。
- 使用Get方法取出数据时,同样会先将传入的key做hash处理获得对应的hash码,在匹配的过程中只有key满足hash值相等且equals为true时才能实现匹配。
因此,如果想要自定义Key类型,必须要覆写hashCode方法和equals方法(通过IDE可自动生成)。
面试题:当使用HashMap进行数据保存时,出现Hash冲突(HashM码相同)时Hash Map会如何处理?
为了保证数据都能保存下来,使用链表来保存数据。
9 Stack栈
Stack类是Vector的子类,但并不采用Vector提供的方法,它有自己的入栈和出栈操作:
E push(E element)
E pop()
10 Queue队列
Queue的实现可以通过LinkedList完成。
boolean offer(E e)
E poll()
另外还可以通过PriorityQueue实现Queue,PriorityQueue具有排序功能,可以将元素按照compareTo定义的顺序取出。
11 Properties属性操作
在国际化程序一节,我们提到了*.properties文件,它是以"key=value"的形式保存的数据,这和Map很像。唯一的区别在于其保存的内容只能是字符串。在java.util包里面提供了一个Properties类,此类是Hashtable的子类。
public class Properties extends Hashtable<Object, Object>
可以发现在继承Hashtable的时候,Hashtable定义泛型为Object。Properties是不需要操作泛型的,因为其只能操作String。
String setProperty(String key, String value)
(不用关注返回值)设置属性String getProperty(String key)
返回属性,不存在时返回NullString getProperty(String key, String defaultValue)
返回属性,不存在时返回默认值void store(OutputStream out, String comments) throws IOException
将属性输出void load(InputStream inStream) throws IOException
将属性加载到Properties对象
Properties操作和Map很像,为什么要单独列出这个类?因为它还有额外的属性存储和加载的方法。在实际开发中,Properties通常用于读取配置资源的信息。ResourceBundle读取的是信息资源文件。
12 Collections工具类
这个类可以操作Map,List,Set和Queue。