集合概述
- 集合、数组都是对多个数据进行存储操作的结构,简称Java容器
- 数组在存储数据方面的弊端:
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。
- 同时无法直接获取存储元素的个数
- 数组存储的数据是有序的、可以重复的。---->存储数据的特点单一
- 集合的出现有效解决了数组的部分弊端
- 集合分类:
- Java 集合可分为 Collection 和 Map 两种体系
- Collection接口:单列数据,定义了存取一组对象的方法的集合
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
- Map接口:双列数据,保存具有映射关系“key-value对”的集合
Collection接口继承树
Map接口继承树
Collection接口方法
- Collection接口介绍:
- Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合
- JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现
- 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型
- 常用方法:
- 添加:
add(Object obj)
:将obj添加进集合中addAll(Collection coll)
:将coll所有元素添加进集合中
- 是否包含某个元素:
boolean contains(Object obj)
:调用obj的equals方法与集合中所有元素比较,如果有返回值为true的,返回true;如果所有元素均没有,则返回falseboolean containsAll(Collection c)
:也是调用元素的equals方法来比较的。判断集合c中的元素是否均在调用该方法的集合中
- 删除:
boolean remove(Object obj)
:删除指定元素obj(通过元素的equals方法判断),只会删除找到的第一个元素boolean removeAll(Collection coll)
:从当前集合删除coll中的所有元素(通过equals方法判断)
- 获取有效元素的个数:
int size()
:返回集合中存储元素的个数
- 清空集合:
void clear()
- 是否是空集合:
boolean isEmpty()
:
- 取两个集合的交集:
boolean retainAll(Collection c)
:把交集的结果存在当前集合中,不影响c
- 集合是否相等:
boolean equals(Object obj)
:判断当前集合是否与obj(一般是集合)相等(利用元素的equals方法),集合元素的顺序要看集合是否是有序集合
- 集合转成对象数组:
Object[] toArray()
- 数据变集合:
List<String> list = Arrays.asList(new String[]{"A","B","C"})
- 获取集合对象的哈希值:
hashCode()
:返回当前对象的哈希值
- 遍历:
iterator()
:返回Iterator接口的实例,用于集合遍历
- 添加:
Iterator迭代器接口
- 说明:
- Iterator对象称为迭代器(迭代器设计模式:提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生),主要用于遍历 Collection 集合中的元素
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前
- 常用方法:
hasNext()
:判断下一个指针处有没有元素,返回一个boolean值next()
:指针下移一位,并将下移后集合位置上的元素返回- 如果next()之后没有元素还调用了next(),抛出NoSuchElementException异常
remove()
:在集合中删除迭代器指针指向位置的元素- 注意1:如果还未调用next()直接调用remove()会报IllegalStateException错,因为指针指向的是第一元素之前的位置
- 注意2:在上一次调用next()之后已经调用了 remove 方法,再调remove都会报IllegalStateException
- 利用Iterator遍历Collection
//推荐 while(iterator.hasNext()){ //next():①指针下移 ②将下移以后集合位置上的元素返回 System.out.println(iterator.next()); } //不推荐 Iterator iterator = coll.iterator();//coll为一个集合 for(int i = 0;i < coll.size();i++){ System.out.println(iterator.next()); }
//错误写法 Iterator iterator = coll.iterator();//coll为一个集合 while((iterator.next())!=null){ //在判定的时候指针就下移一位,这是打印next又下移了一位 Syetem.out.println(iterator.next()); } //错误写法 while(coll.iterator().hasNext()){ //每次iterator()方法都会返回一个新的iterator System.out.println(coll.iterator().next()); }
- 使用foreach遍历集合、数组
- foreach的底层还是使用Iterator实现
- 遍历集合
Collection coll = new ArrayList(); coll.add(123); coll.add("ABC"); for(Object o:coll){ System.out.println(o); }
- 遍历数组,注意遍历字符串数组时字符串的不可变性
int[] ints = new int[]{1,2,3}; for(int i:ints){ System.out.println(i); } //遍历字符串 String[] strs = new String[]{"A","B","C"}; for(String str:strs){ str = "D"; } //此时原数组strs中元素没有改变,因为赋给了str,str值改变时是把新的字符串赋给了它 //而不是在原来字符串上修改
- 补充:Enumeration
- Enumeration 接口是 Iterator 迭代器的 “古老版本”
Enumeration stringEnum = new StringTokenizer("a-b*c-d-e-g", "-"); while(stringEnum.hasMoreElements()){ Object obj = stringEnum.nextElement(); System.out.println(obj); }
- Enumeration 接口是 Iterator 迭代器的 “古老版本”
Collection子接口之一:List接口
- 概述:
- List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。可以根据索引取出对应元素
- JDK API中List接口的实现类常用的有:
ArrayList
、LinkedList
、Vector
- 三个实现类的异同:
- 同:三者都实现了List接口,存储有序可重复的数据
- 异:ArrayList是List接口的主要实现类,线程不安全但效率高,底层用
Object[] elementData
存储数据 - 异:Vector是List接口的古老实现类,线程安全但效率不高,底层用
Object[] elementData
存储数据 - 异:LinkedList底层用双向链表存储数据,对于频繁的插入、删除操作效率比ArrayList高
- List接口常用方法:
void add(int index, Object ele)
:在index位置插入ele元素,如果index大于size,会报角标越界异常boolean addAll(int index, Collection eles)
:从index位置开始将eles中的所有元素添加进来,如果index大于size,会报角标越界异常Object get(int index)
:获取指定index位置的元素int indexOf(Object obj)
:返回obj在集合中首次出现的位置,如果不存在,返回-1int lastIndexOf(Object obj)
:返回obj在当前集合中末次出现的位置,如果不存在,返回-1Object remove(int index)
:移除指定index位置的元素,并返回此元素Object set(int index, Object ele)
:设置指定index位置的元素为eleList subList(int fromIndex, int toIndex)
:返回从fromIndex到toIndex(左闭右开)的子集合
- ArrayList解析
- JKD7:
ArrayList()
:创建一个长度为10的Object数组ArrayList(int capacity)
:创建一个长度为capacity的Object数组add(Object o)
:先检测长度是否够用,不够用则会扩容,如果扩容超过了int类型的表达范围,则会报错,扩容公式:length + (length >> 2)
(1.5倍),扩容后把原数组的内容赋值给扩容后的数组
- JDK8:
ArrayList()
:会把一个长度为空的全局常量数组赋给Object数组ArrayList(int capacity)
:创建一个长度为capacity的Object数组add(Object o)
:调用空参构造器创建实例后,第一次调用add会把数组长度扩充为10,之后扩容机制与JDK7一致
- JDK7类似饿汉式,JDK8类似懒汉式
- JKD7:
- LinkedList解析
- 内部定义了头结点Node first和尾节点Node last,调用空参构造器时,两者都为null
add(Object e)
:从尾节点插入final Node<E> l = last; //将存储的数据e,前一个结点l(原来的last)传入 final Node<E> newNode = new Node<>(l, e, null); //新的节点就是新的last last = newNode; if (l == null) //如果last为空,说明是第一次添加,所以first也是新节点 first = newNode; else //如果不是第一次添加,则需要把原来last指向新节点 l.next = newNode; size++; modCount++;
- Vector解析
- Vector与JDK7的ArrayList类似,如调用空参构造器都会造一个长度为10的数组
- Vector与ArrayList的主要区别在于Vector是线程安全的,Vector的扩容机制不同,会默认扩成原来2倍
- List的遍历
Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } for(Object o : list){ System.out.println(o); } for(int i = 0;i<list.size();i++){ System.out.println(list.get(i)); }
- 例题
- ArrayList和LinkedList的异同:
- 二者都线程不安全,相对线程安全的Vector,执行效率高。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
- ArrayList和Vector的区别:
- Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack
- 关于remove方法
List list = new ArrayList(); list.add(1); list.add(2); list.add(3); list.remove(2);//删除索引为2的元素(即使用了整形泛型也是如此) list.remove(new Integer(2));//删除值为2的元素
- ArrayList和LinkedList的异同:
Collection子接口之二:Set接口
-
概述:
- Set接口是Collection的子接口,set接口没有提供额外的方法
- Set存储的是无序,不重复(根据equals方法判断)的元素
-
实现类:HashSet、LinkedHashSet(HashSet的子类)、TreeSet
-
理解Set的无序性和不可重复性
- 无序性:它不等同于随机性,以HashSet为例,存储的数据在底层数组中不以数组索引顺序添加,以数据的哈希值添加,
- 不可重复性:保证添加的元素按照equals()判断时,不能返回true
-
Set实现类之一:HashSet
- 特点:是Set接口的主要实现类,线程不安全,可以存null值
- HashSet添加元素的过程:
- 当向 HashSet 集合中存入一个元素a时,HashSet 会调用a的 hashCode()方法来得到哈希值,然后根据值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
- 如果得到的下标处没有元素,则添加a成功,如果有元素,则比较两个元素的哈希值是否相同,如果不相同,则a能添加到该位置,如果相同则调用equals方法,如果返回false,则a能添加到该位置,如果返回true,则a添加失败
- 对于已有元素的位置再次添加元素会以链表的形式来存储。在JDK7中,新的元素放进数组,指向原来的元素,即链表顺序以添加顺序的逆序排列;JDK8中原来的元素在数组中,指向新的元素,即链表顺序以添加顺序排列
- HashSet扩容机制:
- 调用空参构造器时,JDK7会生成长度为16的底层数组,JKD8会生成长度0的数组,当第一次添加时扩容成16长度
- 当元素个数达到数组长度的75%以上时,扩容成原来的两倍
- HashSet底层实际上new了一个HashMap,HashSet中的元素实际上是HashMap结构中的key,value上是一个实例化后的Object对象
public HashSet(){ map = new HashMap<>(); } private static final Object PERSENT = new Object() public boolean add(E e){ return map.put(e,PERSENT)==null; }
- 总结:在HashSet中判断元素是否相等,除了equals()外,还有hashCode()方法,但我们并不希望equals()返回true而hashCode()返回false的情况发生,这就涉及hashCode()重写规则了
-
hashCode()重写
- Object类中定义了hashCode()方法,这意味着所有对象都能调用该方法,Object类中的该方法返回的哈希值表示地址值,这相当于意味不重写的话,新建对象的哈希值都不一致
- 重写的基本原则
- 重写equals方法的时候一般都需要同时重写hashCode方法
- 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值
- 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等(相等的对象必须具有相等的散列码)
- 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值
- 为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
- 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)过大也有可能造成溢出
- 31只占用5bits,相乘造成数据溢出的概率较小
- 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
- 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)
-
Set实现类之二:LinkedHashSet
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得能以存储顺序来遍历LinkedHashSet
- LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能
- LinkedHashSet同样具有无序性和不可重复性
LinkedHashSet底层存储结构 -
Set实现类之三:TreeSet
- SortedSet 接口的实现类,内部只能装同一类型(可以比较)的数据,确保集合元素处于排序状态
- TreeSet底层是红黑树
- TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序
- 自然排序:调用空参构造器就会默认使用传入对象的compareTo()方法,这也要求传入对象所处的类必须实现Comparable接口
- 定制排序:将实现了Comparator接口的实例对象传入构造器,则会用该实例对象中实现的int compare(Object o1,Object o2)方法对传入Set的元素定制排序
- 新增的方法如下:
Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)
-
例题
//去除list中的重复数字 public static List duplicateList(List list) { HashSet set = new HashSet(); set.addAll(list); return new ArrayList(set); } public static void main(String[] args) { List list = new ArrayList(); list.add(new Integer(1)); list.add(new Integer(2)); list.add(new Integer(2)); list.add(new Integer(4)); list.add(new Integer(4)); List list2 = duplicateList(list); for (Object integer : list2) { System.out.println(integer); } }
//Person重写了equals和hashCode方法 HashSet set = new HashSet(); Person p1 = new Person(1001,"AA"); Person p2 = new Person(1002,"BB"); set.add(p1); set.add(p2); p1.name = "CC"; set.remove(p1); //p1在list中的位置是AA+1001决定,p1的name改变后,位置不变,此时remove //p1,由1001+CC决定位置,故无法删除p1 System.out.println(set); set.add(new Person(1001,"CC")); //1001+CC的位置此时应该是空的,故添加成功 System.out.println(set); set.add(new Person(1001,"AA")); //1001+AA的位置不为空,但p1的name成了CC,故hashCode相同equals不同,添加成功 System.out.println(set);
Map接口
Map继承树
-
Map接口概述:
- Map与Collection并列存在,Map用于保存具有映射关系的数据:key-value
- Map 中的 key 和 value 都可以是任何引用类型的数据,key常用String
- Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
Map结构 -
实现类概述:
- HashMap:Map的主要实现类,线程不安全,效率高,可以存null的key和value,JDK7底层用数组+链表,JDK8底层用数组+链表+红黑树
- LinkedHashMap:HashMap的子类,在原有的底层结构基础上,添加了一对指针,指向前一个和后一个元素。在遍历的时候可以按照添加顺序遍历,对于频繁的遍历操作效率更高
- Hashtable:古老实现类,线程安全效率低,不能存null的key和value
- Properties:Hashtable的子类,key和value都是String类型,常用来处理配置文件
- TreeMap:添加的元素按照key进行排序,分为自然排序和定制排序,底层是红黑树
-
Map常用方法:
- 添加、删除、修改操作:
Object put(Object key,Object value)
:将指定key-value添加到(或修改)当前map对象中void putAll(Map m)
:将m中的所有key-value对存放到当前map中Object remove(Object key)
:移除指定key的key-value对,并返回valuevoid clear()
:清空当前map中的所有数据,map为空但不为null
- 元素查询的操作:
Object get(Object key)
:获取指定key对应的valueboolean containsKey(Object key)
:是否包含指定的keyboolean containsValue(Object value)
:是否包含指定的valueint size()
:返回map中key-value对的个数boolean isEmpty()
:判断当前map是否为空boolean equals(Object obj)
:判断当前map和参数对象obj是否相等
- 元视图操作的方法:
Set keySet()
:返回所有key构成的Set集合Collection values()
:返回所有value构成的Collection集合,顺序与keySet一致Set entrySet()
:返回所有key-value(Entry对象)对构成的Set集合Set entrySet = map.entrySet(); Iterator iterator = entrySet.iterator(); while(iterator.hasNext()){ Map.Entry entry = (Map.Entry)iterator.next(); System.out.ptintln(entry.getKey()+"----"+entry.getValue()); }
- 添加、删除、修改操作:
-
Map实现类之一:HashMap
- HashMap概述:
- HashMap是 Map 接口主要实现类,允许使用null键和null值,与HashSet一样,不保证映射的顺序
- 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
- 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
- 一个key-value构成一个entry,所有的entry构成的集合是Set:无序的、不可重复的
- 判断两个 key 相等的标准是:equals()+hashCode();判断两个 value相等的标准是:equals()
- HashMap源码中的重要常量:
DEFAULT_INITIAL_CAPACITY= 1 << 4
:HashMap的默认容量,16MAXIMUM_CAPACITY= 1 << 30
:HashMap的最大支持容量,2^30DEFAULT_LOAD_FACTOR= 0.75f
:HashMap的默认负载因子TREEIFY_THRESHOLD= 8
:Bucket(桶,数组中的每个位置都是一个桶)中链表长度大于该默认值,转化为红黑树UNTREEIFY_THRESHOLD= 6
:Bucket中红黑树存储的Node小于该默认值,转化为链表MIN_TREEIFY_CAPACITY= 64
:桶中的Node被树化时数组最小容量(当桶中Node的数量大到需要变红黑树时,若数组容量小于MIN_TREEIFY_CAPACITY
时,此时应执行resize扩容操作这MIN_TREEIFY_CAPACITY
的值至少是TREEIFY_THRESHOLD
的4倍)table
:存储元素的数组,长度总是2的n次幂entrySet
:存储具体元素的集size
:HashMap中存储的键值对的数量modCount
:HashMap扩容和结构改变的次数threshold
:扩容的临界值,=容量*负载因子loadFactor
:负载因子,默认是0.75,可以通过构造器传参改变
- 底层原理:
- JDK7:
new HashMap()
:创建一个长度为16的一位数组Entry[] tableput(Object key,Object value)
:用hashCode()得到key的哈希值,经过某种算法后得到在数组中的存放位置,如果此位置为空则添加成功,如果不为空,此时位置上有一个或多个元素,多个则以链表形式存储(元素在链表中的先后位置参考HashSet),利用key的哈希值与位置上的所有元素比较,若都不相同则继续以链表形式添加进去,若某个元素(key1-value1)哈希值与key相同,则比较key的key1的equals,如果是false则以链表形式继续添加,如果是true,则使用value替换value1- 在不断添加的过程中,涉及到扩容问题,当元素个数超过临界值(临界值=数组总长度 * 负载因子)时,默认的扩大为原来的2倍,将原来的元素重新一一添加(此时元素的位置很可能发生改变)
- JDK8的不同之处:
- 调用空参构造器不会对数组进行操作(底层数组时Node[]不是Entry了)
- 首次调用put方法后会创建一个长度为16的数组
- 当数组某个位置的元素以链表形式存在的个数超过了8个,且当前数组长度大于64时,此时此位置上的所有数据改用红黑树存储
- JDK7:
- 源码分析:
- JDK7
//Entry结构 transient Entry[] table; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; …… }
//put方法 public V put(K key, V value) { // HashMap 允许存放 null 键和 null 值 if (key == null) // 当 key 为 null 时,调用 putForNullKey 方法,将 value 放置在数组第一个位置 return putForNullKey(value); // 根据 key 的 keyCode 重新计算 hash 值 int hash = hash(key.hashCode()); // 搜索指定 hash 值在对应 table 中的索引 int i = indexFor(hash, table.length); // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果有hashCode和equals均相等的,则把新的value赋给key,原来的value返回 V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry modCount++; // 将 key、value 添加到 i 索引处 addEntry(hash, key, value, i); return null; }
//向指定位置添加元素 void addEntry(int hash, K key, V value, int bucketIndex) { // 获取指定 bucketIndex 索引处的 Entry Entry<K,V> e = table[bucketIndex]; // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 如果 Map 中的 key-value 对的数量超过了临界值(数组总长度*填充因子) if (size++ >= threshold) // 把 table 对象的长度扩充到原来的 2 倍。 resize(2 * table.length); }
//构造器中数组长度的初始化 //initialCapacity是构造器的形参,空参时传入值为16 int capacity = 1; while (capacity < initialCapacity) //最终作为数组长度的参数是capacity,不是形参initialCapacity //这意味着数组的长度总是2的n次方,无论指定的长度是多少 capacity <<= 1;
//由哈希值得到索引 static int indexFor(int h, int length) { return h & (length-1); } //构造器和索引步骤都是精心设计的,两者的结合能够有效减少碰撞, //数据能够在数组上尽可能的均匀分布
//读取 public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
- JDK8
//put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 putVal 方法 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果哈希表为空,则先创建一个哈希表 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果当前桶没有碰撞冲突,则直接把键值对插入,完事 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //如果桶上节点的 key 与当前 key 重复,那你就是我要找的节点 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果是采用红黑树的方式处理冲突,则通过红黑树的 putTreeVal 方法去插入这个键值对 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //否则就是传统的链式结构 else { //采用循环遍历的方式,判断链中是否有重复的 key for (int binCount = 0; ; ++binCount) { //到了链尾还没找到重复的 key,则说明 HashMap 没有包含该键 if ((e = p.next) == null) { //创建一个新节点插入到尾部 p.next = newNode(hash, key, value, null); //如果链的长度大于 TREEIFY_THRESHOLD 这个临界值,则把链变为红黑树尚硅谷 Java 高级编程 宋红康 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //找到了重复的 key if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //这里表示在上面的操作中找到了重复的键,所以这里把该键的值替换为新值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //判断是否需要进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } //put 方法比较复杂,实现步骤大致如下: //1、先通过 hash 值计算出 key 映射到哪个桶。 //2、如果桶上没有碰撞冲突,则直接插入。 //3、如果出现碰撞冲突了,则需要处理冲突: //(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。 //(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红黑树。 //4、如果桶中存在重复的键,则为该键替换新值。 //5、如果 size 大于阈值,则进行扩容。
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null:e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果哈希表不为空 && key 对应的桶上不为空 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) { //如果当前的桶是采用红黑树处理冲突,则调用红黑树的 get 方法去获取节点 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //不是红黑树的话,那就是传统的链式结构了,通过循环的方法判断链中是否存在该 key do { if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } //实现步骤大致如下: //1、通过 hash 值获取该 key 映射到的桶。 //2、桶上的 key 就是要查找的 key,则直接命中。 //3、桶上的 key 不是要查找的 key,则查看后续节点: //(1)如果后续节点是树节点,通过调用树的方法查找该 key。 //(2)如果后续节点是链式节点,则通过循环遍历链查找该 key。
//在 get 方法和 put 方法中都需要先计算 key 映射到哪个桶上,然后才进行之后的 //操作,计算的主要代码如下: (n - 1) & hash //上面代码中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通过 //下面这个方法计算出来的,采用了二次哈希的方式,其中 key 的 hashCode 方法是 //一个native 方法: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16); //这个 hash 方法先通过 key 的 hashCode 方法获取一个哈希值,再拿这个哈希值 //与它的高 16 位的哈希值做一个异或操作来得到最后的哈希值,为啥要这样做呢? //注释中是这样解释的:如果当 n 很小,假设为 64 的话,那么 n-1即为 63(0x111111), //这样的值跟 hashCode()直接做与操作,实际上只使用了哈希值的后 6 位。如果当 //哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低 //位都利用起来,从而解决了这个问题。
- JDK7
- 面试题:负载因子值的大小,对HashMap有什么影响:
- 负载因子的大小决定了HashMap的数据密度
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数
- HashMap概述:
-
Map实现类之二:LinkedHashMap
- 概述:
- LinkedHashMap 是 HashMap 的子类
- 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
- 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致
- 结构的不同
- HashMap中的内部类:Node
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
- LinkedHashMap中的内部类:Entry
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
- HashMap中的内部类:Node
- 概述:
-
Map实现类之三:TreeMap
- TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态
- TreeSet底层使用红黑树结构存储数据
- TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 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)方法
//jdbc.properties内容 //user=properties Properties pros = new Properties(); pros.load(new FileInputStream("jdbc.properties")); String user = pros.getProperty("user"); System.out.println(user);
Collections工具类
- Collections 是一个操作 Set、List 和 Map 等集合的工具类,它提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
- 排序操作:(均为static方法):
reverse(List)
:反转 List 中元素的顺序shuffle(List)
:对 List 集合元素进行随机排序sort(List)
:根据元素的自然顺序对指定 List 集合元素按升序排序sort(List,Comparator)
:根据指定的 Comparator 产生的顺序对 List 集合元素进行排序swap(List,int, int)
:将指定 list 集合中的 i 处元素和 j 处元素进行交换
- 查找、替换:
Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大元素Object max(Collection,Comparator)
:根据 Comparator 指定的顺序,返回给定集合中的最大元素Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object)
:返回指定集合中指定元素的出现次数void copy(List dest,List src)
:将src中的内容复制到dest中boolean replaceAll(List list,Object oldVal,Object newVal)
:使用新值替换List 对象的所有旧值
- 同步控制
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,并返回该线程安全集合
Collections的同步控制