本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Set接口
Set接口:存储无序、不可重复的数据
- HashSet:采用哈希算法实现的Set
- HashSet的底层是用HashMap实现的,因此查询效率较高,由于采用hashCode算法直接确定元素的内存地址,增删效率也挺高的。
- 作为Set的主要实现类;线程不安全的,可以存储null值
- LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
- TreeSet:底层使用红黑树存储数据;可以按照添加对象的指定属性,进行排序。
1、Set接口概述
- Set:存储无序的、不可重复的数据
- 以HashSet为例说明:
- 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值
- 不可重复性:保证添加的元素按照equals()判断时,不能返回true。即,相同的元素只能添加一个
- 以HashSet为例说明:
- Set接口是Collection的子接口,set接口中没有提供额外的方法,使用的都是Collection中声明的方法。
- 要求:
- 向Set(主要指HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()方法。
- 重写的hashCode()和equals()方法尽可能保持一致性。
- Set集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败;
- Set判断两个对象是否相同不是使用==运算符,而是根据equals()方法判断。
添加元素的过程:以HashSet为例
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中存放的位置(即为索引位置),判断数组此位置上是否已经有元素。
- 如果此位置上没有其他元素,则元素a添加成功。 --> 情况1
- 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a于元素b的hash值:
- 如果hash值不相同,则元素a添加成功 --> 情况2
- 如果hash值相同,进而需要调用元素a所在类的equals()方法:
- equals()方法返回true,则元素a添加失败
- equals()方法返回false,则元素a添加成功 --> 情况3
对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上的数据以链表的方式存储。
- JDK7:元素a放到数组中,指向原来的元素。
- JDK8:原来的元素放在数组中,指向元素a。
- 总结:(元素a)七上八下
2、Set实现类之一:HashSet
- HashSet是Set接口的典型实现,大多数时候使用Set集合时都使用这个实现类。
- HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。
- HashSet具有以下特点:
- 不能保证元素的排列顺序
- HashSet不是线程安全的
- 集合元素可以是null
- HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等。
- 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即“相等的对象必须具有相等的散列码”。
- HashSet底层:数组+链表
重写hashCode()方法的基本原则:
- 在程序运行时,同一个对象多次调用hashCode()方法应该返回相同的值。
- 当两个对象的equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应相等。
- 对象中用作equals()方法比较的Field,都应该用来计算hashCode值。
Eclipse/IDEA工具里hashCode()重写:
以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals()方法和hashCode()方法。
问题:为什么用Eclipse/IDEA重写hashCode()方法,有31这个数字?
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)
- 并且31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以由 i*31== (i << 5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果用一个数来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除。(减少冲突)
3、Set实现类之二:LinkedHashSet
- LinkedHashSet是HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
- LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet插入性能略低于hashSet,但对于频繁的遍历操作,LinkedHashSet效率高于hashSet。
- LinkedHashSet不允许集合元素重复。
4、Set实现类之三:TreeSet
- TreeSet是SortSet接口的实现类,TreeSet可以确保集合元素处于排序状态。
- TreeSet底层使用红黑树结构存储数据。
- 向TreeSet中添加的数据,要求是相同类的对象,不能添加不同类的对象。
- 新增的方法如下:
Comparator comparator()Object first()Object last()Object lower(Object e)Object higher(Object e)SortedSet subSet(formElement,toElement)SortedSet headSet(toElement)SortedSet tailSet(formElement)
- TreeSet两种排序方法:自然排序(实现Comparable接口)和定制排序(Comparator)。默认情况下,TreeSet采用自然排序。
- 自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals()。
- 定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals()。
Set接口总结
1、存储数据的特点:无序的、不可重复的元素
- 具体的:以HashSet为例说明:
- 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值
- 不可重复性:保证添加的元素按照equals()判断时,不能返回true。即,相同的元素只能添加一个
2、元素添加过程(以HashSet为例)
添加元素的过程:以HashSet为例
我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中存放的位置(即为索引位置),判断数组此位置上是否已经有元素。
- 如果此位置上没有其他元素,则元素a添加成功。 --> 情况1
- 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a于元素b的hash值:
- 如果hash值不相同,则元素a添加成功 --> 情况2
- 如果hash值相同,进而需要调用元素a所在类的equals()方法:
- equals()方法返回true,则元素a添加失败
- equals()方法返回false,则元素a添加成功 --> 情况3
对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上的数据以链表的方式存储。
- JDK7:元素a放到数组中,指向原来的元素。
- JDK8:原来的元素放在数组中,指向元素a。
- 总结:(元素a)七上八下
HashSet底层:数组+链表结构 (前提:JDK7)
3、常用方法
Set接口是Collection的子接口,set接口中没有提供额外的方法,使用的都是Collection中声明的方法。
4、常用实现类
Set接口:存储无序、不可重复的数据
- HashSet:采用哈希算法实现的Set
- HashSet的底层是用HashMap实现的,因此查询效率较高,由于采用hashCode算法直接确定元素的内存地址,增删效率也挺高的。
- 作为Set的主要实现类;线程不安全的,可以存储null值
-
LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
- 在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据
- 对于频繁的遍历操作,LinkedHashSet效率高于HashSet。
- TreeSet:底层使用红黑树存储数据;可以按照添加对象的指定属性,进行排序。
5、存储对象所在类的要求:
- HashSet/LinkedHashSet:
- 要求:向Set(主要指HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()方法。
- 要求:重写的hashCode()和equals()方法尽可能保持一致性。
- TreeSet:
- 自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再是equals()。
- 定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再是equals()。
6、TreeSet的使用
6.1 使用说明
- 向TreeSet中添加的数据,要求是相同类的对象,不能添加不同类的对象。
- TreeSet两种排序方法:自然排序(实现Comparable接口)和定制排序(Comparator)。默认情况下,TreeSet采用自然排序。
6.2 常用的排序方式
- 自然排序(实现Comparable接口)
- 定制排序(Comparator)
Map接口
1、Map实现类的结构
- Map:双列数据,存储 key-value 对的数据
-
HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
- LinkedHashMap :保证在遍历Map元素时,可以按照添加的顺序实现遍历。
- 原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
- 对于频繁的遍历操作,此类的指向效率要高于HashMap。
- LinkedHashMap :保证在遍历Map元素时,可以按照添加的顺序实现遍历。
-
TreeMap:保证按照添加的key-value对进行遍历,实现排序遍历。此时考虑key的自然排序或定制排序。
- 底层使用红黑树数据结构进行存储。
-
Hashtable:作为古老的Map实现类;线程安全的,效率低;不能存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型。
-
HashMap底层结构:
- JDK7及之前:数组+链表
- JDK8:数组+链表+红黑树
面试题:
- HashMap的底层实现原理。
- HashMap和Hashtable的异同。
- CurrentHashMap和Hashtable的异同。
2、Map结构的理解
- Map中的Key:无序的、不可重复的,使用Set存储所有的key。 ---> key所在的类要重写equals()方法和hashCode()方法 (以HashMap为例 )
- Map中的value:无序的、可重复的,使用Collection存储所有的value。---> value所在的类要重写equals()方法 (以HashMap为例 )
- 一个键值对:key-value构成了一个Entry对象
- Map中的Entry:无序的、不可重复的,使用Set存储所有的Entry。
Map实现类之一:HashMap
1、HashMap引入
问题:建立国家英文简称和中文全名间的键值映射,并通过key对value进行操作,如何实现数据的存储和操作呢?
分析: Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。
最常用的实现类是HashMap。
2、HashMap的数据结构
1)HashMap概述
HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。
在API中给出了相应的定义:
//1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value,可以为null,(HashMap和HashTable大致上是一样的,除了hashmap是异步的,和允许key和value为null),
//这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
//假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)成正比。
//因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the
"capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
//HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量,在容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。
//当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量.
An instance of HashMap has two parameters that affect its performance:
initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity,the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
//通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting
it perform automatic rehashing as needed to grow the table
2)HashMap在JDK1.8以前数据结构和存储原理
【链表散列】
通过数组和链表结合在一起使用,就叫做链表散列。这其实就是hashmap存储的原理图。
【HashMap的数据结构和存储原理】
HashMap的数据结构就是用的链表散列。
HashMap底层的实现原理:(以JDK7为例说明)
分成两个部分:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //就是我们说的map的key
V value; //value值,这两个都不陌生
Entry<K,V> next;//指向下一个entry对象
int hash;//通过key算过来的你hashcode值。
}
Entry的物理模型图:
第二步:构造好了entry对象,然后将该对象放入数组中。大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。
【Hash存放元素的过程】
通过key、value封装成一个entry对象,然后通过key的值来计算该entry的hash值,通过entry的hash值和数组的长度length来计算出entry放在数组中的哪个位置上面,每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash值来确定将该对象存放在数组中的哪个位置上。
HashMap底层的实现原理:(以JDK7为例说明)
HashMap map = new HashMap();
- 在实例化以后,底层创建了长度是16的一位数组Entry[] table
...可能已经执行过多次put...
map.put(key1,value1);
- 首先,调用key1所在类的hashCode()方法计算key1的哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
- 如果此位置上的数据为空,此时的key1-value1添加成功。 ---- 情况1
- 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
- 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。 ---- 情况2
- 如果key1的哈希值与已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)
- 如果equals()返回false:此时key1-value1添加成功。 ---- 情况3
- 如果equals()返回true:此时value1替换value2。
补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的形式存储。
在不断的添加过程中,会涉及到扩容问题,当超出临界值且要存放的位置非空时,进行扩容。默认的扩容方式:扩容为原来的2倍,并将原有的数据复制过来。
3)JDK1.8后HashMap的数据结构
上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
JDK8相较于JDK7在底层实现方面的不同:
new HashMap();:底层没有创建一个长度为16的数组
- JDK8底层的数组是:Node[]类型数组,而非Entry[]类型数组
- 首次调用put()方法时,底层创建一个长度为16的数组
- JDK7底层结构只有 数组+链表,JDK8底层结构为:数组+链表+红黑树
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64 ,此时此索引位置上的索引数据改为使用红黑树存储。
HashMap源码中的重要常量
DEFAULT_INITIAL_CAPACITY:HashMap的默认容量,16
MAXIMUM_CAPACITY:HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,= 0.75
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化成红黑树。 = 8
UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化成链表。
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。= 64(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
table:存储元素的数组,总是2的n次幂
entrySet:存储具体元素的集合
size:HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数
threshold:扩容的临界值,= 容量 * 填充因子,16*0.75=12
loadFactor:填充因子
4)HashMap的属性
HashMap的实例有两个参数影响其性能。
- 初始容量:哈希表中桶的数量
- 加载因子:哈希表在其容量自动增加之前可以达到多满,的一种尺度
当哈希表中条目数超出了当前容量加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
【loadFactor加载因子】
定义:loadFactor译为佳载因子。加载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。
把loadFactor变为1最好吗?存的数据很多,但是这样会有一个问题,就是在通过key拿到value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals()来依次比较链表中的元素,拿到value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,就能直接得到value值。
如果把loadFactor变得很小,在数组中的位置就会太稀,即分散的太开,浪费很多空间,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
【桶】
根据前面画的HashMap存储的数据结构图,数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
【capacity】
capacity译为容量,代表数组的容量,即数组的长度,同时也是HashMap中桶的个数。默认值是16。
一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
【size的含义】
size就是在该HashMap的实例中实际存储的元素的个数
【threshold的作用】
int threshold;
threshold = capacity * loadFactor,当 Size>=threshold 时,就要考虑对数组的扩增了,threshold是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑,实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。
什么时候会扩增数组的大小?
在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,这才能扩增数组。
我们通过一张HashMap的数据结构图来分析:
3、HashMap源码分析
1)HashMap的层次关系与继承结构
【HashMap继承结构】
上面就继承了一个abstractMap,用来减轻实现Map接口的编写负担。
【实现接口】
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响
- Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅拷贝,即对拷贝对象的改变会影响被拷贝的对象。
- Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
2)HashMap的类属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
3)HashMap的构造方法
有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数组的长度。)和初始化加载因子。
注意,hashMap中table数组一开始就已经是个没有长度的数组了。
构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个是初始化加载因子,另一个是用threshold记录下数组初始化的大小。
【HashMap()】
//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75
//初始化容量:也就是初始化数组的大小
//加载因子:数组上的存放数据疏密程度。
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
【HashMap(int)】
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
【HashMap(int,float)】
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能大于最大值,否则为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 填充因子不能小于或等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 初始化填充因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
【HashMap(Map<? extends K, ? extends V> m)】
public HashMap(Map<? extends K, ? extends V> m) {
// 初始化填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将m中的所有元素添加至HashMap中
putMapEntries(m, false);
}
【putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函数将m的所有元素存入本HashMap实例中】
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) { // pre-size
// 未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 计算得到的t大于阈值,则初始化阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
4)常用方法
Map接口常用方法:
- 添加、删除、修改操作
put(key,value):将指定key-value添加到(或修改)当前Map对象中putAll(Map m):将m中的所有key-value对存放到当前map中remove(key):移除指定key的key-value对,并返回valueclear():清空当前map中的所有数据
- 元素的查询操作
get(key):获取指定key对应的valuecontainsKey(key):是否包含指定的keycontainsValue(value):是否包含指定的valuesize():返回map中的key-value对的个数isEmpty():判断当前map是否为空equals(obj):判断当前map和参数对象obj是否相等
-
元视图 操作的方法
keySet():返回所有key构成的Set集合values():返回所有value构成的Collection集合entrySet():返回所有key-value对构成的Set集合
-
put(K key,V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点
是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。
get(Object key)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode(int hash,Pbject key)
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// table已经初始化,长度大于0,根据hash寻找table中的项也不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 桶中第一项(数组元素)相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个结点
if ((e = first.next) != null) {
// 为红黑树结点
if (first instanceof TreeNode)
// 在红黑树中查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 否则,在链表中查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。
resize()方法
final Node<K,V>[] resize() {
// 当前table保存
Node<K,V>[] oldTab = table;
// 保存table大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 保存当前阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 之前table大小大于0
if (oldCap > 0) {
// 之前table大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值为最大整形
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍,使用左移,效率更高
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 阈值翻倍
newThr = oldThr << 1; // double threshold
}
// 之前阈值大于0
else if (oldThr > 0)
newCap = oldThr;
// oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个
元素会调用resize函数,会进入这一步)
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新阈值为0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY
?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 初始化table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 之前的table已经初始化过
if (oldTab != null) {
// 复制元素,重新进行hash
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两
个不同的链表,完成rehash
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
在resize前和resize后的元素布局如下:
上图只是针对了数组下标为2的桶中的各个元素在扩容后的分配布局,其他各个桶中的元素布局可以以此类推。
HashMap总结
- HashMap是Map接口使用频率最高的实现类。
- 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
- 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()方法。
- 所有的value构成的集合是Collection:无序的、可重复的。所以,value所在的类要重写:equals()方法。
- 一个key-value构成一个entry
- 所以的entry构成的集合是Set:无序的、不可重复的
- HashMap判断两个key相等的标准是:两个key通过equals()方法返回true,hashCode值也相等。
- HashMap判断两个value相等的标准是:两个value通过equals()方法返回true。
【关于数组扩容】
从putVal源代码中可以知道,当插入一个元素时size就加1,若size大于threshold时,就会进行扩容。
假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75。此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还有31个桶是空的,也会进行扩容处理。
此时还有31个桶是空的,好像似乎不需要进行扩容处理,但是是需要扩容处理的。因为此时capacity大小可能不适当。
前面知道,扩容处理会遍历所有的元素,时间复杂度很高;经过一次扩容处理后,元素会更加均匀的分布在各个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于元素在桶中均匀分布所带来的好处。
【总结】
- hashMap在JDK8以前是一个链表散列的数据结构,而在JDK8以后是一个数组+链表+红黑树的数据结构。
- 通过源码学习,hashMap是一个能快速通过key获取到value值的一个集合。原因是内部使用了hash查找值的方法。
Map实现类之二:LinkedHashMap实(了解)
- LinkedHashMap是HashMap的子类
- 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
- 与LinkedHashSet类似,LinkedHashMap可以维护Map的迭代顺序:迭代顺序与key-value对的插入顺序一致
- 能够记录添加的元素的先后顺序
- 对于频繁的遍历操作,效率比HashMap高
Map实现类之三:TreeMap
- TreeMap存储key-value对时,需要根据key-value对进行排序。TreeMap可以保证所有的key-value对处于有序状态。
- TreeMap底层使用红黑树结构存储数据。
- 向TreeMap中添加key-value,要求key必须是由同一个类创建的对象。因为要按照key进行排序:自然排序、定制排序
- TreeMap的key的排序:
-
- 自然排序:TreeMap的所有的key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException
-
- 定制排序:创建TreeMap时,传入一个Comparator对象,改对象负责对TreeMap中的所有key进行排序。此时不需要Map的key实现Comparable接口。
- TreeMap判断两个key相等的标准:两个key通过compareTo()方法或compare()方法返回0。
Map实现类之四:Hashtable
- Hashtable是个古老的Map实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
- Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
- 与HashMap不同,Hashtable不允许使用null作为key和value。
- 与HashMap一样,Hashtable也不能保证其中key-value对的顺序。
- Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
Map实现类之五:Properties
- Properties类是Hashtable的子类,该对象用于处理属性文件(配置文件) 。
- 由于属性文件里的key、value都是字符串类型, 所以Properties里的key和value都是字符串类型。
- 存取数据时,建议使用
setProperty(String key,String value)方法和getProperty(String key)方法。
Collections工具类
操作数组的工具类:Arrays
【前言】
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
这个类不需要创建对象,内部提供的都是静态方法。
1、Collections概述
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。如果为此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出 NullPointerException。
- Collections是一个操作Set、List和Map等集合的工具类。
- Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。
- 排序操作:(均为static方法)
reverse(List):反转List中的元素shuffle(List):对List集合元素进行随机配许sort(List):根据元素的自然顺序对指定List集合元素按升序排序sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序swap(List,int i,int j):将指定List集合中的索引i处元素和索引j处元素进行交换
- 查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素Object min(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最小元素int frequency(Collection,Object):返回指定集合中指定元素出现的次数void copy(List dest,List src):将src中的内容复制到dest中boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
2、排序操作
【方法】
1)static void reverse(List<?> list):
反转列表中元素的顺序。
2)static void shuffle(List<?> list) :
对List集合元素进行随机排序。
3) static void sort(List<T> list)
根据元素的自然顺序 对指定列表按升序进行排序
4)static <T> void sort(List<T> list, Comparator<? super T> c) :
根据指定比较器产生的顺序对指定列表进行排序。
5)static void swap(List<?> list, int i, int j)
在指定List的指定位置i,j处交换元素。
6)static void rotate(List<?> list, int distance)
当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。
【演示】
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//输出:[3, -2, 9, 5, -1, 6]
System.out.println(list);
//集合元素的次序反转
Collections.reverse(list);
//输出:[6, -1, 5, 9, -2, 3]
System.out.println(list);
//排序:按照升序排序
Collections.sort(list);
//[-2, -1, 3, 5, 6, 9]
System.out.println(list);
//根据下标进行交换
Collections.swap(list, 2, 5);
//输出:[-2, -1, 9, 5, 6, 3]
System.out.println(list);
/*//随机排序
Collections.shuffle(list);
//每次输出的次序不固定
System.out.println(list);*/
//后两个整体移动到前边
Collections.rotate(list, 2);
//输出:[6, 9, -2, -1, 3, 5]
System.out.println(list);
}
}
3、查找、替换操作
【方法】
1) static <T> int binarySearch(List<? extends Comparable<? super T>>list, T key)
使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。
注意:此前必须保证List集合中的元素已经处于有序状态。
2)static Object max(Collection coll)
根据元素的自然顺序,返回给定collection 的最大元素。
3)static Object max(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最大元素。
4)static Object min(Collection coll):
根据元素的自然顺序,返回给定collection 的最小元素
5)static Object min(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最小元素。
6) static <T> void fill(List<? super T> list, T obj) :
使用指定元素替换指定列表中的所有元素。
7)static int frequency(Collection<?> c, Object o)
返回指定 collection 中等于指定对象的出现次数。
8)static int indexOfSubList(List<?> source, List<?> target) :
返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。
9)static int lastIndexOfSubList(List<?> source, List<?> target)
返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1。
10)static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
使用一个新值替换List对象的所有旧值oldVal
【演示:实例使用查找、替换操作】
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsTest1 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//[3, -2, 9, 5, -1, 6]
System.out.println(list);
//输出最大元素9
System.out.println(Collections.max(list));
//输出最小元素:-2
System.out.println(Collections.min(list));
//将list中的-2用1来代替
System.out.println(Collections.replaceAll(list, -2, 1));
//[3, 1, 9, 5, -1, 6]
System.out.println(list);
list.add(9);
//判断9在集合中出现的次数,返回2
System.out.println(Collections.frequency(list, 9));
//对集合进行排序
Collections.sort(list);
//[-1, 1, 3, 5, 6, 9, 9]
System.out.println(list);
//只有排序后的List集合才可用二分法查询,输出2
System.out.println(Collections.binarySearch(list, 3));
}
}
4、同步控制
- Collectons提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
- 正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。
- Collections提供了多个静态方法可以把他们包装成线程同步的集合。
【方法】
1)static <T> Collection<T> synchronizedCollection(Collection<T> c)
返回指定 collection 支持的同步(线程安全的)collection。
2)static <T> List<T> synchronizedList(List<T> list)
返回指定列表支持的同步(线程安全的)列表。
3)static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定映射支持的同步(线程安全的)映射。
4)static <T> Set<T> synchronizedSet(Set<T> s)
返回指定 set 支持的同步(线程安全的)set。
【实例】
import java.util.*;
public class TestSynchronized
{
public static void main(String[] args)
{
//下面程序创建了四个同步的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}
补充:Enumeration
- Enumeration接口是Iterator迭代器的古老版本
5、Collections设置不可变集合
【方法】
1)emptyXxx()
返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map。
2)singletonXxx():
返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map。
3)unmodifiableXxx():
返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。
上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本。
【实例】
import java.util.*;
public class TestUnmodifiable
{
public static void main(String[] args)
{
//创建一个空的、不可改变的List对象
List<String> unmodifiableList = Collections.emptyList();
//unmodifiableList.add("java");
//添加出现异常:java.lang.UnsupportedOperationException
System.out.println(unmodifiableList);// []
//创建一个只有一个元素,且不可改变的Set对象
Set unmodifiableSet = Collections.singleton("Struts2权威指南");
//[Struts2权威指南]
System.out.println(unmodifiableSet);
//创建一个普通Map对象
Map scores = new HashMap();
scores.put("语文" , 80);
scores.put("Java" , 82);
//返回普通Map对象对应的不可变版本
Map unmodifiableMap = Collections.unmodifiableMap(scores);
//下面任意一行代码都将引发UnsupportedOperationException异常
unmodifiableList.add("测试元素");
unmodifiableSet.add("测试元素");
unmodifiableMap.put("语文",90);
}
}
6、总结和测试
【JavaBean】
实体类:Pojo
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Employee { //Javabean, Enter实体类
private int id;
private String name;
private int salary;
private String department;
private Date hireDate;
public Employee(int id, String name, int salary, String department,
String hireDate) {
super();
this.id = id;
this.name = name;
this.salary = salary;
this.department = department;
DateFormat format = new SimpleDateFormat("yyyy-MM");
try {
this.hireDate = format.parse(hireDate);
} catch (ParseException e) {
e.printStackTrace();
}
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public Date getHireDate() {
return hireDate;
}
public void setHireDate(Date hireDate) {
this.hireDate = hireDate;
}
}
【测试类代码如下】
import java.util.ArrayList;
import java.util.List;
public class Test01 {
public static void main(String[] args) throws Exception {
//一个对象对应了一行记录!
Employee e = new Employee(0301,"狂神",3000,"项目部","2017-10");
Employee e2 = new Employee(0302,"小明",3500,"教学部","2016-10");
Employee e3 = new Employee(0303,"小红",3550,"教学部","2016-10");
List<Employee> list = new ArrayList<Employee>();
list.add(e);
list.add(e2);
list.add(e3);
printEmpName(list);
}
public static void printEmpName(List<Employee> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i).getName());
}
}
}
泛型
1、为什么要有泛型
泛型的概念
- 把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection,List,ArrayList,这个就是类型参数,即泛型。
- 所谓泛型,就是**允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型****。这个类型参数将在使用时(例如:继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入世纪的类型参数,也称为类型实参)。
- Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
- 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
为什么要有泛型呢?直接Object不是也可以存储数据吗?
- 解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
- 解决获取元素数据时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。
- Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。
如何解决以下强制类型转换时容易出现的异常问题?
- List的get(int index)方法获取元素
- Map的get(Object key)方法获取元素
- Iterator的next()方法获取元素
分析:通过泛型 , JDK1.5使用泛型改写了集合框架中的所有接口和类
2、在集合中使用泛型
以ArrayList为例:
以HashMap为例:
- 从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型,正如:List,这表明该List只能保存字符串类型的对象。
- JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
3、自定义泛型结构
1)泛型类、泛型接口
- 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
- 泛型类的构造器如下:public GenericClass(){}
而下面是错误的:public GenericClass(){}
- 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
- 泛型不同的引用不能相互赋值。
- 尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
- 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型一定要一路使用。要不用,一路都不要用。
- 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
- JDK1.7泛型的简化操作:ArrayLiat filst = new ArrayList<>();
- 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
- 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
- 异常类不能是泛型的。
- 不能使用new E[]。但是可以:
E[] elements = (E[]) new Object[capacity];- 参考:ArrayList源码中声明:
Object[] elementData,而非泛型参数类型数组。
- 参考:ArrayList源码中声明:
- 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型。
- 子类不保留父类的泛型:按需实现
- 没有类型 擦除
- 具体类型
- 子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
- 子类不保留父类的泛型:按需实现
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型。
2)泛型方法
- 方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
- 泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名 ([泛型标识 参数名称]) 抛出的异常
- 泛型方法声明泛型时也可以指定上限
- 泛型方法,可以声明为静态的,原因:泛型参数是在调用方法时确定的,并非在实例化时确定。
4、泛型在继承上的体现
如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型。
比如:String是Object的子类,但是List并不是List
5、通配符的使用
- 使用类型通配符:?
- 比如:List<?>、Map<??>
- List<?>是是List<String>、List<Object>等各种泛型List的父类。
- 读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
- 写入list中的元素时,不行。因为我们不知道c的元素类型,不能向其中添加对象。
- 唯一的例外是null,它是所有类型的成员。
- 将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object());//编译时错误
因为我们不知道c的元素类型,不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以无法传任何东西进去。
- 另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但我们知道,它总是一个Object。
通配符的使用注意点:
- 注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
}
- 注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
}
- 注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();
有限制的通配符
- 允许所有泛型的引用调用
-
通配符指定上限
- 上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
-
通配符指定下限
- 下限super:使用时指定的类型不能小于操作的类,即>=
- 举例:
- <?extends Number> ( 无穷小 , Number]
只允许泛型为Number及Number子类的引用调用 - <? super Number> [Number , 无穷大 )
只允许泛型为Number及Number父类的引用调用 - <? extends Comparable>
只允许泛型为实现Comparable接口的实现类的引用调用
- <?extends Number> ( 无穷小 , Number]
6、泛型应用举例
1)泛型嵌套
2)实际案例
用户在设计类的时候往往会使用类的关联关系,例如,一个人中可以定义一个信息的属性,但是一个人可能有各种各样的信息(如联系方式、基本信息等),所以此信息属性的类型就可以通过泛型进行声明,然后只要设计相应的信息类即可。