Java集合框架不看后悔系列

前言

2.1 数组在存储多个数据方面的特点:

  1. 一旦初始化以后,其长度就确定了。

  2. 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。

比如:String[] arr;int[] arr1;Object[] arr2;
2.2 数组在存储多个数据方面的缺点:

  1. 一旦初始化以后,其长度就不可修改。

  2. 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。

  3. 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用

  4. 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

因此:需要其他存储(集合框架)

集合框架(JCF)

简介:Java集合框架(Java Collections Framework,JCF)

JCF:

  1. • 容器:能够存放数据的空间结构 – 数组/多维数组,只能线性存放 –列表/散列集/树/……
    • 容器框架:为表示和操作容器而规定的一种标准体系结构 – 对外的接口:容器中所能存放的抽象数据类型 – 接口的实现:可复用的数据结构
    – 算法: 对数据的查找和排序 • 容器框架优点:提高数据存取效率,避免程序员重复劳动 • C++的STL,Java的JCF
  2. • Java 1.1和以前的数据结构 – Vector, Stack, Hashtable,Enumeration等
    • Java1.2和以后,JCF集合框架 – 功能更强大 – 易于学习 – 接口和实现分离,多种设计模式设计更灵活 – 泛型设计
  3. 概况

在这里插入图片描述
工具类 Arrays Collections

4 • 早期接口Enumeration • JCF的集合接口是Collection – add,contains,remove,size
– iterator • JCF的迭代器接口Iterator – hasNext – next – remove

• JCF主要的数据结构实现类
– 列表(List, ArrayList, LinkedList)
– 集合(Set, HashSet, TreeSet, LinkedHashSet)
– 映射(Map, HashMap, TreeMap, LinkedHashMap)

• JCF主要的算法类
– Arrays: 对数组进行查找和排序等操作
– Collections:对Collection及其子类进行排序和查找操作

• 总结

– 容器框架的作用
– JCF主要数据结构
• 列表
• 集合
• 映射
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)
一般分为Collection和Map

Collection

Collection接口继承树

JDK提供的集合API位于java.util包内
Collection接口:单列集合,用来存储一个一个的对象
在这里插入图片描述

Collection 接口

  • Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。

  • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。

  • 在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。

Collection 接口方法

  1. 添加
    add(Object obj)
    addAll(Collection coll)

  2. 获取有效元素的个数
    int size()

  3. 清空集合
    void clear()

  4. 是否是空集合
    boolean isEmpty()

  5. 是否包含某个元素
    boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
    boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。

  6. 删除
    boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
    boolean removeAll(Collection coll):取当前集合的差集

  7. 取两个集合的交集
    boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c

  8. 集合是否相等
    boolean equals(Object obj)

  9. 转成对象数组
    Object[] toArray()

  10. 获取集合对象的哈希值
    hashCode()

  11. 遍历
    iterator():返回迭代器对象,用于集合遍历

LIst接口

LIst接口:存储有序的、可重复的数据。 -->“动态”数组

有序的Collection
元素以线性方式存储存储的顺序与添加的顺序相同允许空值
允许重复的元素

允许null值
元素在集合中的顺序与添加顺序一致

List接口方法

  • List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
方法功能
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
Object get(int index):获取指定index位置的元素
int indexOf(Object obj):返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
Object remove(int index):移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

常用的实现该接口的类:
ArrayList – 动态数组(非同步的)
Vector – 向量(同步)
LinkedList – 链表(非同步)
Stack – 堆栈(LIFO)
ArrayList/Vector/LinkedList/Stack的区别:

List实现类之一:ArrayList:

– 以数组实现的列表,不支持同步
• List list = Collections.synchronizedList(new ArrayList(…));
• 本质上,ArrayList是对象引用的一个”变长”数组
– 利用索引位置可以快速定位访问
– 不适合指定位置的插入、删除操作
– 适合变动不大,主要用于查询的数据
– 和Java数组相比,其容量是可动态调整的
– ArrayList在元素填满容器时会自动扩充容器大小的50%

ArrayList的JDK1.8之前与之后的实现区别?
– JDK1.7:ArrayList像饿汉式,直接创建一个初始容量为10的数组
JDK1.8:ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组

结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)

List实现类之二:LinkedList:

以双向链表实现的列表,不支持同步
– LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:记录前一个元素的位置,记录下一个元素的位置
• List list = Collections.synchronizedList(new LinkedList(…));
– 可被当作堆栈、队列和双端队列进行操作
顺序访问高效,随机访问较差,中间插入和删除高效
适用于经常变化的数据

  • 新增方法:

  • void addFirst(Object obj)

  • void addLast(Object obj)

  • Object getFirst()

  • Object getLast()

  • Object removeFirst()

  • Object removeLast()

在这里插入图片描述

List 实现类之三:Vector(同步)

– 和ArrayList类似,可变数组实现的列表
– Vector同步,适合在多线程下使用
– 原先不属于JCF框架,属于Java最早的数据结构,性能较差
– 从JDK1.2开始,Vector被重写,并纳入到JCF
– 官方文档建议在非同步情况下,优先采用ArrayList

  • 新增方法:

  • void addElement(Object obj)

  • void insertElementAt(Object obj,int index)

  • void setElementAt(Object obj,int index)

  • void removeElement(Object obj)

  • void removeAllElements()

Stack
import java.util.*;

public class StackDemo {

	public static void main(String[] args) {
		//堆栈
		Stack sc=new Stack();
		sc.add("Jack"); //add方法返回的是boolean, true表示添加成功,false表示添加失败
		sc.add(0, "Annie");//在索引为0的位置上添加元素
		sc.push("Frank"); // 添加元素,采用压入栈内方式,push方法返回的是添加的元素
		sc.addElement("William");// 添加元素,没有返回值
		
		System.out.println(sc);
		
		//取出元素
		System.out.println("================");
		System.out.println(sc.size());
		for(int i=0;i<sc.size();i++){ //当 i=2,sc.size()==>2   2<2不成立
			System.out.println(sc.pop()); //最后压进去的,最先取出来,取出最后一个元素
		
		}
	}
}

总结

ArrayList 和Vector是采用数组方式存储数据,允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,
所以索引数据快插入数据慢,Vector由于使用了synchronized(线程安全)方法,但是性能上比ArrayList要差
LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,
所以插入数度较快!
Stack是按照堆栈的原理来存储数据,先进后出

面试题:
请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?

  • 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。

面试题

@Test
public void testListRemove() {
List list = new ArrayList();
list.add(1); 
list.add(2); 
list.add(3);
updateList(list);
System.out.println(list);//
}
private static void updateList(List list) {
list.remove(2);//删除的是3这个元素,里面3会自动装箱成Integer
list.remove(new Integer(2));//如果需要删除2这个元素
}

Set接口

Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”

– 确定性:对任意对象都能判定其是否属于某一个集合
– 互异性:集合内每个元素都是无差异的,注意是内容差异
– 无序性:集合内的顺序无关
不保证元素的顺序不允许重复元素
Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法

实现Set接口的类:HashSet、TreeSet,LinkedHashSet

  • HashSet类不允许出现重复元素,不保证集合中元素的顺序,只允许一个null元素。(基于散列函数的集合,无序,不支持同步)

  • TreeSet是Set的一种变体,可以实现按照自然顺序排序。在添加元素时会自动将其插入到已经有序的元素序列中。(基于树结构的集合,可排序的,不支持同步)

  • LinkedHashSet(基于散列函数和双向链表的集合,可排序的,不支持同步)

Set实现类之一:HashSet

基于HashMap实现的,可以容纳null元素, 不支持同步

  • HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

  • HashSet 具有以下特点

  • 不能保证元素的排列顺序

  • HashSet 不是线程安全的

  • 集合元素可以是 null

  • HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

  • 对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

• Set s = Collections.synchronizedSet(new HashSet(…));
set方法Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。

– add 添加一个元素
– clear 清除整个HashSet
– contains 判定是否包含一个元素
– remove 删除一个元素 size 大小
– retainAll 计算两个集合交集

注意(添加过程,):

  • 向HashSet中添加元素的过程:
  • 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
  • 如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。
  • 如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet将会把它们存储在不同的位置,但依然可以添加成功。
    对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
    jdk 7 :元素a放到数组中,指向原来的元素。
    jdk 8 :原来的元素在数组中,指向元素a
    总结:七上八下

底层也是数组,初始容量为16,当如果使用率超过0.75,(16*0.75=12)就会扩大容量为原来的2倍。(16扩容为32,依次为64,128…等)

  • 重写 hashCode() 方法的基本原则

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。

  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。

  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

  • 重写 equals() 方法的基本原则

  • 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。

  • 因此,违反了“相等的对象必须具有相等的散列码”。

  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

  • 以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

  • 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)

  • 并且31只占用5bits,相乘造成数据溢出的概率较小。

  • 31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)

  • 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

Set实现类之二:LinkedHashSet

继承HashSet,也是基于HashMap实现的,可以容纳null元素
– 不支持同步
• Set s = Collections.synchronizedSet(new LinkedHashSet(…));
通过一个双向链表维护插入顺序
– LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
– 方法和HashSet基本一致
• add, clear, contains, remove, size

Set实现类之三:TreeSet

– 基于TreeMap实现的,不可以容纳null元素,不支持同步
• SortedSet s = Collections.synchronizedSortedSet(new TreeSet(…));
• TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
TreeSet底层使用红黑树结构存储数据
• TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。

– add 添加一个元素
– clear 清除整个TreeSet
– contains 判定是否包含一个元素
– remove 删除一个元素 size 大小
– 根据compareTo方法或指定Comparator排序

区别:
• HashSet, LinkedHashSet, TreeSet的元素都只能是对象
• HashSet和LinkedHashSet判定元素重复的原则
– 判定两个元素的hashCode返回值是否相同,若不同,返回false
– 若两者hashCode相同,判定equals方法,若不同,返回false;否则返回true。
– hashCode和equals方法是所有类都有的,因为Object类有
• TreeSet判定元素重复的原则
– 需要元素继承自Comparable接口
– 比较两个元素的compareTo方法

排 序—自然排序
  • 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj)
    方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列

  • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable接口。

  • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。

  • Comparable 的典型实现:

  • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较

  • Character:按字符的 unicode值来进行比较

  • Boolean:true 对应的包装类实例大于 false 对应的包装类实例

  • String:按字符串中字符的 unicode 值进行比较

  • Date、Time:后边的时间、日期比前面的时间、日期大

  • 向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。

  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象。

  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较返回值。

  • 当需要把一个对象放入 TreeSet 中,重写该对象对应的 equals() 方法时,应保证该方法与 compareTo(Object obj) 方法有一致的结果:如果两个对象通过equals() 方法比较返回 true,则通过 compareTo(Object obj) 方法比较应返回 0。否则,让人难以理解。

排 序—定制排序
  • TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
  • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。

• 总结

– HashSet、LinkedHashSet、TreeSet
– 注意不同Set判定元素重复的原则

字符串按照“字典”的顺序
整数按照数字大小的顺序

Map

Map接口继承树

在这里插入图片描述

Map接口

Map接口:双列集合,用来存储一对(key - value)一对的数据

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value

  • Map 中的 key 和 value 都可以是任何引用类型的数据

  • Map 中的key 用Set来存放,不允许重复,即同一个Map 对象所对应的类,须重写hashCode()和equals()方法

  • 常用String类作为Map的“键”

  • key 和 value 之间存在单向一对一关系,即通过指定的key 总能找到唯一的、确定的 value

  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类

常用实现Map接口的类:
– Hashtable(同步,慢,数据量小)
– HashMap(不支持同步,快,数据量大)
– Properties (同步,文件形式,数据量小)

HashMapHashtableTreeMap
允许null键和值不允许有null键和值不允许有null键
不保证元素的顺序不保证元素的顺序可以按键的自然顺序将值排序
线程不安全线程安全线程不安全

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对,并返回value
void clear()清空当前map中的所有数据

元素查询的操作:

方法功能
Object get(Object key)获取指定key对应的value
boolean containsKey(Object key)是否包含指定的key
boolean containsValue(Object value)是否包含指定的value
int size()返回map中key-value对的个数
boolean isEmpty()判断当前map是否为空
boolean equals(Object obj)判断当前map和参数对象obj是否相等

元视图操作的方法:

方法功能
Set keySet()返回所有key构成的Set集合
Collection values()返回所有value构成的Collection集合
Set entrySet()返回所有key-value对构成的Set集合

*总结:常用方法:

  • 添加:put(Object key,Object value)

  • 删除:remove(Object key)

  • 修改:put(Object key,Object value)

  • 查询:get(Object key)

  • 长度:size()

  • 遍历:keySet() / values() / entrySet()

Map实现类之一:HashMap

– K-V对,K和V都允许为null
– 不同步,多线程不安全
• Map m = Collections.synchronizedMap(new HashMap(…));
– 无序的
– 主要方法:clear, containsValue, containsKey, get, put,remove, size
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。

HashMap的存储结构

JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
在这里插入图片描述

  • HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为
    “桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

  • 每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

那么HashMap什么时候进行扩容和树形化呢?

  • 当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor 时 , 就会进行数组扩容, loadFactor 的默认 值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

  • 当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,
    下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

关于映射关系的key是否可以修改?answer:不要修改

映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

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倍,并将原有的数据复制过来。

总结:JDK1.8相较于之前的变化

1. HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组

2. 当首次调用map.put()时,再创建长度为16的数组
3. 数组为Node类型,在jdk7中称为Entry类型
4. 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
5. 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

JDK1.7:

  • 底层数据结构是数组+链表;
  • 在JDK1.7中HashMap是以Entry数组来存储数据;
  • 用key的hashcode取模来决定Key会被放在数组里的位置;
  • 如果hashcode相同,或者hashcode取模结果相同,那么这些Key会被定义到 - Entry数组的同一个格子里,这些Key会形成一个链表;
  • 新增节点采用头插法
  • 插入数据之前扩容。

JDK1.8:

  • 底层数据结构是数组+链表+红黑树结构(当链表长度大于8,转为红黑树);
  • 在JDK1.8中HashMap是以Node数组来存储数据;
  • Node可能是链表结构,也可能是红黑树结构,如果同一个格子里的Key不超过8个,会用链表结构储存如果超过了8个,将链表转换为红黑树;
  • 如果插入的key和hashcode相同,会被定义到node数组的格子里
    即使hashcode相同,由于红黑树的特点查找某个特定元素,只需要O(log n)的开销,也就是说get/put的操作时间复杂度最差就只有o(log n);
  • 新增节点采用尾插法,这也是为什么1.8不容易出现环型链表的原因;
  • 插入数据成功之后扩容。

HashMap源码中的重要常量

DEFAULT_INITIAL_CAPACITY: HashMap的默认容量,16
MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子 0.75
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
table:存储元素的数组,总是2的n次幂
entrySet:存储具体元素的集
size:HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数。
threshold:扩容的临界值,=容量*填充因子
loadFactor:填充因子

HashMap默认的加载因子是0.75,最大容量是16,因此可以得出HashMap的默认容量是:0.75*16=12。

==============================================
hashmap为什么不安全

Map实现类之:ConcurrentHashMap实现原理

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
    实现线程安全的方式(重要)
    在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,
效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用
put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

https://www.cnblogs.com/chengxiao/p/6842045.html

Map实现类之二:LinkedHashMap

– 基于双向链表的维持插入顺序的HashMap
LinkedHashMap 是 HashMap 的子类
– 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
– 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致

LinkedHashMap的底层实现原理(了解)

源码中:

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);
             }
         }

Map实现类之三:TreeMap

– 基于红黑树的Map,可以根据key的自然排序或者compareTo方法进行排序输出
– 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

– K-V对,K和V都不允许为null
– Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap
– Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。

– 同步,多线程安全
– 无序的
– 适合小数据量
– 主要方法:clear, contains/containsValue, containsKey, get, put,remove, size

Map实现类之五:Properties

– 继承于Hashtable
– 可以将K-V对保存在文件中
– 适用于数据量少的配置文件
– 继承自Hashtable的方法:clear, contains/containsValue, containsKey, get, put,remove, size
– 从文件加载的load方法, 写入到文件中的store方法
– 获取属性 getProperty ,设置属性setProperty
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

map遍历

@Test
public void test5(){
    Map map = new HashMap();
    map.put("AA",123);
    map.put(45,1234);
    map.put("BB",56);

    //遍历所有的key集:keySet()
    Set set = map.keySet();
        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
    }
    System.out.println();
    //遍历所有的value集:values()
    Collection values = map.values();
    for(Object obj : values){
        System.out.println(obj);
    }
    System.out.println();
    //遍历所有的key-value
    //方式一:entrySet()
    Set entrySet = map.entrySet();
    Iterator iterator1 = entrySet.iterator();
    while (iterator1.hasNext()){
        Object obj = iterator1.next();
        //entrySet集合中的元素都是entry
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "---->" + entry.getValue());

    }
    System.out.println();
    //方式二:
    Set keySet = map.keySet();
    Iterator iterator2 = keySet.iterator();
    while(iterator2.hasNext()){
        Object key = iterator2.next();
        Object value = map.get(key);
        System.out.println(key + "=====" + value);

    }

}

总结

– HashMap是最常用的映射结构
– 如需要排序,考虑LinkedHashMap和TreeMap
– 如需要将K-V存储为文件,可采用Properties类

面试题:负载因子值的大小,对HashMap有什么影响

  • 负载因子的大小决定了HashMap的数据密度。

  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。

  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。

  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

总结

set: Hashset LinkedHashSet hashcod() equals()如果没有定义,调用object的,像指针什么的
TreeSet 需要元素继承Comparable接口 比较两个元素compareTo方法

下面用到的是lambda表达式写法
最简单的例子是 Collections.sort(list, (x, y) -> y - x); 其中
(x, y) -> y - x

由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。
Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value
Map.Entry里面包含getKey()和getValue()方法

Set<Entry<T,V>> entrySet()
该方法返回值就是这个map中各个键值对映射关系的集合。

可使用它对map进行遍历。

add和put

add()和put()方法都是集合框架中的添加元素的方法。
但是put()方法应用于map集合中,add()方法应用于collection集合中。
二者的主要区别是:返回值类型不一样。
add()返回布尔(boolean)类型。因为像Set集合中不允许添加重复的元素。当HashSet调用add()方法时,如果返回false,表示添加不成功。
put()的使用是:添加时出现相同的键,那么后添加的值会替换(覆盖)掉此键对应的原来的值。并返回此键对应的原来的值

一般遍历方式

Iterator接口(第一种方式)

  • Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。

  • GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。

  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。

  • Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。

  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

  • 指针原理

remove()方法删除元素
hasNext()方法检测集合中是否还有下一个元素。
next()方法返回集合中的下一个元素。
Iterator遍历集合元素的语法:

Iterator it = 集合对象名.iterator();
while(it.hasNext()){
	Object o=it.next();
	System.out.println (o);`在这里插入代码片`
}

第一步:创建迭代器(一般会加个泛型上去)
第二步:遍历

map的迭代器与其他的不同
通过map.keySet()获取值
KeySet迭代器遍历

HashMap<String, String> hm=new HashMap();
Iterator it=hm.keySet().iterator(); //获得键的集合的迭代器
		while(it.hasNext()){ //是否还有下一个键
			String key=(String)it.next();//获得下一个键
			String value=(String)hm.get(key); //根据键获取值
			System.out.println(key+"<==>"+value);
		}
 Map<Integer, String> map = new HashMap<Integer, String>();
 
for (Integer in : map.keySet()) {
            //map.keySet()返回的是所有key的值
            String str = map.get(in);//得到每个key多对用value的值
             System.out.println(in + "     " + str);
        }

通过Map.entrySet可以同时获取key和value

  • Set entrySet():返回所有key-value对构成的Set集合)
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • 一个key-value构成一个entry

Entry迭代器遍历

Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
     while (it.hasNext()) {
          Map.Entry<Integer, String> entry = it.next();
           System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }

推荐,尤其是容量大时

for (Map.Entry<Integer, String> entry : map.entrySet()) {
         //Map.entry<Integer,String> 映射项(键-值对)  有几个方法:用上面的名字entry
         //entry.getKey() ;entry.getValue(); entry.setValue();
         //map.entrySet()  返回此映射中包含的映射关系的 Set视图。
          System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }

通过Map.values()遍历所有的value,但不能遍历key

for (String v : map.values()) {
           System.out.println("value= " + v);
    }

foreach循环(第二种方式)

  • 一般用这个比较快一点

  • Java 5.0 提供了 foreach 循环迭代访问 Collection和数组。

  • 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。

  • 遍历集合的底层调用Iterator完成操作。

  • foreach还可以用来遍历数组。

for(类型 自己创建的对象k:集合对象)  {
                                	   System.out.println(k);
                            	   
                                   }

JCF的工具类

• JCF中工具类
– 不存储数据,而是在数据容器上,实现高效操作
• 排序
• 搜索
– Arrays类
– Collections类

Collections工具类

操作数组的工具类:Arrays

  • Collections 是一个操作 Set、List 和 Map 等集合的工具类

  • Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

  • 排序操作:(均为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 对象的所有旧值
@Test
 public void test1(){
        List list = new ArrayList();
        list.add(123);
        list.add(43);
        list.add(765);
        list.add(765);
        list.add(765);
        list.add(-97);
        list.add(0);

        System.out.println(list);

//        Collections.reverse(list);
//        Collections.shuffle(list);
//        Collections.sort(list);
//        Collections.swap(list,1,2);
        int frequency = Collections.frequency(list, 123);

        System.out.println(list);
        System.out.println(frequency);

    }

Collections常用方法:同步控制

Collections 类中提供了多个synchronizedXxx()方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
例如:把ArryList,HashMap变成线程安全的
代码案例:

//返回的list1即为线程安全的List
List list1 = Collections.synchronizedList(list);

Arrays

• Arrays:处理对象是数组
– 排序:对数组排序, sort/parallelSort。
– 查找:从数组中查找一个元素, binarySearch。
– 批量拷贝:从源数组批量复制元素到目标数组, copyOf。
– 批量赋值:对数组进行批量赋值, fill。
– 等价性比较:判定两个数组内容是否相同, equals。

总结

• Arrays和Collections功能强大,不需要重复造轮子
• 对象比较方法Comparable/Comparator

比较接口

Comparable 接口–1(自然排序)

Java.lang包中定义的一个比较对象的接口
提供抽象方法compareTo(),可以对对象进行整体排序。
使用方法:对要进行比较对象的类实现Comparable接口,重写compareTo()方法
Comparable接口不仅仅用于集合框架,可用于任何需要进行对象比较的场合。

  • 重写compareTo(obj)的规则:

  • 如果当前对象this大于形参对象obj,则返回正整数,

  • 如果当前对象this小于形参对象obj,则返回负整数,

  • 如果当前对象this等于形参对象obj,则返回零。

  • 对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法。

    • 在compareTo(obj)方法中指明如何排序

怎么写:
一个类实现Comparable 接口,重写public int compareTo(Object obj){}
比较此对象与指定对象的顺序,如果该对象小 于、等于或大于指定对象,则分别返回负整数、零或正整数。

class Student implements Comparable {             
//实现Comparable接口

	int id;
	String name; 
   ……
	public int compareTo(Object obj){              
	 //   重写Comparable接口的compareTo方法

		Student s=(Student)obj;
		return this.id-s.id;   //排序
	}
   ……
}


第二个案例

  //指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从高到低排序
    @Override
    public int compareTo(Object o) {
//        System.out.println("**************");
        if(o instanceof Goods){
            Goods goods = (Goods)o;
            //方式一:
            if(this.price > goods.price){
                return 1;
            }else if(this.price < goods.price){
                return -1;
            }else{
//                return 0;
               return -this.name.compareTo(goods.name);//一样的话就按照名字排序
            }
            //方式二:
//           return Double.compare(this.price,goods.price);
        }
//        return 0;
        throw new RuntimeException("传入的数据类型不一致!");
    }

Comparator 接口–2(定制排序)

Java.util包中提供的一个对某个对象集合进行整体排序的比较接口
提供了compare的比较方法来比较对象
使用方法:
定义实现该接口的类,重写compare方法
Comparator接口的使用:定制排序

  • 背景:

当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码,
或者实现java.lang.Comparable接口的排序规则不适合当前的操作,
那么可以考虑使用 Comparator 的对象来排序

  • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:

如果方法返回正整数,则表示o1大于o2;
如果返回0,表示相等;
返回负整数,表示o1小于o2。

先知道

  • 接口可以进行实例化,只需要在new后将接口中未实现的方法进行实现就好,这种做法其实是匿名内部类的做法,本质上讲还是类的实例化。

怎么写?
实例化接口并且重写未实现方法

 Comparator com = new Comparator() {
            //按照年龄从小到大排列
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User)o1;
                    User u2 = (User)o2;
                    return Integer.compare(u1.getAge(),u2.getAge());//Integer的compare已经写好的
                }else{
                    throw new RuntimeException("输入的数据类型不匹配");
                }
            }
        };

怎么用?
放入数据结构
TreeSet set = new TreeSet(com);

    @Test
    public void test3(){
        String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
        Arrays.sort(arr,new Comparator(){

            //按照字符串从大到小的顺序排列
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof String && o2 instanceof  String){
                    String s1 = (String) o1;
                    String s2 = (String) o2;
                    return -s1.compareTo(s2);
                }
//                return 0;
                throw new RuntimeException("输入的数据类型不一致");
            }
        });
        System.out.println(Arrays.toString(arr));
    }

    @Test
    public void test4(){
        Goods[] arr = new Goods[6];
        arr[0] = new Goods("lenovoMouse",34);
        arr[1] = new Goods("dellMouse",43);
        arr[2] = new Goods("xiaomiMouse",12);
        arr[3] = new Goods("huaweiMouse",65);
        arr[4] = new Goods("huaweiMouse",224);
        arr[5] = new Goods("microsoftMouse",43);

        Arrays.sort(arr, new Comparator() {
            //指明商品比较大小的方式:按照产品名称从低到高排序,再按照价格从高到低排序
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof Goods && o2 instanceof Goods){
                    Goods g1 = (Goods)o1;
                    Goods g2 = (Goods)o2;
                    if(g1.getName().equals(g2.getName())){
                        return -Double.compare(g1.getPrice(),g2.getPrice());
                    }else{
                        return g1.getName().compareTo(g2.getName());
                    }
                }
                throw new RuntimeException("输入的数据类型不一致");
            }
        });

        System.out.println(Arrays.toString(arr));
    }

总结

一、说明:Java中的对象,正常情况下,只能进行比较:== 或 != 。不能使用 > 或 < 的
但是在开发场景中,我们需要对多个对象进行排序,言外之意,就需要比较对象的大小。
如何实现?

  • 使用两个接口中的任何一个:Comparable 或 Comparator

二、Comparable接口与Comparator的使用的对比:

  • Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小。

  • Comparator接口属于临时性的比较。

结果:大于0则交换(true)
例如:s1.compareTo(s2) :s1-s2>0则交换,s1不能大于s2,既,从小到大排序

快速判断
1为小
2为大

s1.compareTo(s2) 小到大
s2.compareTo(s1) 大到小

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值