Java基础9:Java集合

集合概述

  • 集合、数组都是对多个数据进行存储操作的结构,简称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;如果所有元素均没有,则返回false
      • boolean 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); 
      }
      

Collection子接口之一:List接口

  • 概述:
    • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。可以根据索引取出对应元素
    • JDK API中List接口的实现类常用的有:ArrayListLinkedListVector
  • 三个实现类的异同:
    • 同:三者都实现了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在集合中首次出现的位置,如果不存在,返回-1
    • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,如果不存在,返回-1
    • Object remove(int index):移除指定index位置的元素,并返回此元素
    • Object set(int index, Object ele):设置指定index位置的元素为ele
    • List 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类似懒汉式
  • 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的元素
      

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对,并返回value
      • void clear():清空当前map中的所有数据,map为空但不为null
    • 元素查询的操作:
      • 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集合,顺序与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的默认容量,16
      • MAXIMUM_CAPACITY= 1 << 30:HashMap的最大支持容量,2^30
      • DEFAULT_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[] table
        • put(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
        //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 位。如果当
        //哈希值的高位变化很大,低位变化很小,这样就很容易造成冲突了,所以这里把高低
        //位都利用起来,从而解决了这个问题。
        
    • 面试题:负载因子值的大小,对HashMap有什么影响:
      • 负载因子的大小决定了HashMap的数据密度
      • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降
      • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间
      • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数
  • 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);
        	}
        }
        
  • 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的同步控制
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值