Java基础总结—容器篇

Java集合【重点】

集合存储的是对象的引用、内存 、集合体系结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A2oFEB6c-1633793386139)(JavaSE.assets/image-20210922132408791.png)]

1、Iterable接口:

  • Iterator方法 : 调用iterator方法,返回一个Iterator类型的迭代器

    public class IteratorTest {
        public static void main(String[] args) {
            Collection c = new HashSet();
            c.add(100) ;        //自动装箱
            c.add(20)  ;
            Iterator it = c.iterator();   //返回集合迭代器对象,用于迭代(遍历)集合!
            							  //迭代器类似于快照,记录集合当前的状态(结构),一旦状态改变需要重新获取迭代器,
            while(it.hasNext()){
               Object obj = it.next() ;
                System.out.println(obj);
                if (obj instanceof Integer)
                    System.out.println("返回得是Integer类型");
            }
        }
    }
    //返回的迭代器,it可以看作一个指针,指向迭代器第一个元素的前一个位置!
    it.hasnext()就是下一个元素
    it.next()无论元素是什么类型,都会返回一个object类型对象  
    it.remove()迭代器删除,即删除快照中的元素,同样集合中的元素也会被删除!
    

2、Collection 接口:

常用方法:

  • add(Object obj) :向集合中添加元素

  • size() : 返回集合中元素的个数,而非集合的容量

  • contains:底层调用的是euqals方法,本质是集合中的元素与其进行比较,如果重写equals,则比较内容,否则通过内存地址判断是否相等

  • remove:底层调用equals方法,本质与contains方法同理!

总结:equals方法是需要我们重写的!

其余接口中方法参考文档API

3、List 接口

3.1、ArrayList分析 *

ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全

ArrayList 又称动态数组,底层是基于数组实现的List,与数组的区别在于,其具备动态扩展能力。从继承体系图中可看出ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	...
}
  • 实现了List, RandomAccess, Cloneable, java.io.Serializable等接口
  • 实现了List,具备基础的添加、删除、遍历等操作
  • 实现了RandomAccess,具备随机访问的能力
  • 实现了Cloneable,可以被克隆(浅拷贝) list.clone()
  • 实现了Serializable,可以被序列化

ArrayList初始化

JDK8以后 执行无参构造,底层先创建一个初始化容量为0的数组,当添加第一个元素的时候,初始化容量为10 !

补充:JDK6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData 。

   /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;


    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//初始容量大于0
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {//初始容量等于0
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10

ArrayList扩容机制

  int newCapacity = oldCapacity + (oldCapacity >> 1);
//所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)


 /**
  * 	ArrayList扩容的核心方法grows()方法。
  */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //再检查新容量是否超出了ArrayList所定义的最大容量,
        //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    //比较minCapacity和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

扩容总结:当我们ArrayList的容量不够时,按照规则扩容至原来的1.5倍,如果扩容后仍然不满足需求的最小容量,则容量更新为要求的容量,此时检查当前容量是否超出ArrayList所定义的最大容量,若超出则更新为ArrayList所定义的最大容量MAX_ARRAY_SIZE

3.2、LinkedList

LinkedList是一个以双向链表实现的List,它除了作为List使用,还可以作为队列或者栈来使用

双向链表: 包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j5ZztjKB-1633793386141)(JavaSE.assets/image-20210924213446761.png)]

源码分析:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

可以看出LinkedList实现了Cloneable和Serializable接口,说明其可以被克隆,也可以被序列化!同样的,LinkedList被克隆的时候,和ArrayList一样二者均是浅拷贝。

1、LinkedList的基本属性

// 链表中元素的个数
transient int size = 0;
// 链表的头节点
transient Node<E> first;
// 链表的尾节点
transient Node<E> last;

三个基本属性通过关键字transient修饰,使其不被序列化。

2、Node类(节点)

//单个节点分析    
private static class Node<E> {
        E item;			//存放元素
        Node<E> next;	//下一个节点的地址
        Node<E> prev;	//上一个节点的地址

     Node(Node<E> prev, E element, Node<E> next) {
           this.item = element;
           this.next = next;
           this.prev = prev;
     }
 }

从代码中就可以看出,这是双向链表结构。

3、构造方法

	public LinkedList() {   //无参构造  
    }

    public LinkedList(Collection<? extends E> c) {			// 将指定集合中的所有元素追加到此列表的末尾
        this();
        addAll(c);
    }

双向链表和双向循环链表的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C81V8mWq-1633793386142)(JavaSE.assets/image-20210924215518947.png)]

JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别

添加元素

LinkedList在中间添加元素的方法实现原理就是,典型的双链表在中间添加元素的流程!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-acmRquIZ-1633793386143)(JavaSE.assets/image-20210924215748986.png)]

  • 在队列首尾添加元素很高效,时间复杂度为O(1)
  • 在中间添加元素比较低效,首先要先找到插入位置的节点,再修改前后节点的指针,时间复杂度为O(n)
add(E e) 添加一个元素 、addFirst(E e) 头部添加元素 、addLast(E e) 尾节点添加元素 、removeFirst() 删除头节点 、 removeLast() 删除尾节点
    

删除元素

  • 在队列首尾删除元素很高效,时间复杂度为O(1)
  • 在中间通过指定下标删除元素比较低效,首先要先找到要删除节点的位置,再进行删除,时间复杂度为O(n)
add(int index, E element) 指定位置插入节点 , remove(Object o) 删除节点、 removeFirst() 删除头节点 、removeLast()删除尾节点 、remove(int index) 删除某个位置的节点 

总结:

  1. LinkedList是一个以双链表实现的List;
  2. LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
  3. LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
  4. LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
  5. LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
3.3、Vector
  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的。 几乎等于ArrayList

从图中我们可以看出:Vector继承了AbstractList,实现了List,RandomAccess,Cloneable,Serializable接口,因此Vector支持快速随机访问,可以被克隆,支持序列化。

Vector扩容机制

   int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                capacityIncrement : oldCapacity);
//扩容增量:原容量的 1倍  如 Vector的容量为10,一次扩容后是容量为20    


// 确定数组当前的容量大小
public synchronized void ensureCapacity(int minCapacity) {
    if (minCapacity > 0) {
        modCount++;
        ensureCapacityHelper(minCapacity);
    }
}

// 如果:当前容量 > 当前数组长度,就调用grow(minCapacity)方法进行扩容
// 由于该方法是在ensureCapacity()中被调用的,而ensureCapacity()方法中已经加上了synchronized锁,所以
// 该方法不需要再加锁
private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 最大上限的数组容量大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE 
    
// Vector集合中的核心扩容方法
private void grow(int minCapacity) {
    // overflow-conscious code
    // 获取旧数组的容量
    int oldCapacity = elementData.length;
    // 得到扩容后(如果需要扩容的话)的新数组容量
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                      capacityIncrement : oldCapacity);
    // 如果新容量 < 数组实际所需容量,则令newCapacity = minCapacity
    if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
    // 如果当前所需容量 > MAX_ARRAY_SIZE,则新容量设为 Integer.MAX_VALUE,否则设为 MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
         
// 最大容量
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

4、Set 接口

特点:无序不可重复

原因:主要是由于底层的HashMap,采用的是Hash表(数组+链表+红黑树)的数据结构,我们的元素不一定放在哪里,所以说是无序的

4.1、HashSet

底层是一个HashMap,也是一个Hash表的数据结构

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

同样HashSet是可克隆,支持序列化操作

HashSet源码分析

构造方法

new HashSet的时候,底层new了一个HashMap

public HashSet() {
        map = new HashMap<>();
    }   
public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));   
        addAll(c);
    }

插入元素

    public boolean add(E e) {		// 把元素本身作为key,把PRESENT作为value,也就是这个map中所有的value都是一样的。
        return map.put(e, PRESENT)==null;	// HashSet添加元素的时候,直接调用的是HashMap中的put()方法,
    }

向HashSet中添加元素,本质是向底层中的HashMap的key位置添加元素

删除元素

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;		//移除元素,本质还是移除hashmap中的key
}

/**
 * Removes all of the elements from this set.
 * The set will be empty after this call returns.
 */
public void clear() {				
    map.clear();					//清空set中的元素
}
HashSet总结:
  • HashSet内部使用HashMap的key存储元素,以此来保证元素不重复;
  • HashSet是无序的,因为HashMap的key是无序的;
  • HashSet中允许有一个null元素,因为HashMap允许key为null;
  • HashSet是非线程安全的;
  • HashSet是没有get()方法的;
4.2、TreeSet

底层是一个TreeMap,本质也是红黑树的数据结构

5、Map 接口:

  • clear() : 清空Map集合
  • size():统计Map集合当中的键值对的个数
  • put(Object key ,Object value) :添加一个键值对<key,value>
  • get(Object key):通过一个key获取对应的value
  • remove(Object key):通过key删除一对键值对
  • values():返回Map集合中的所有value,返回值类型为Collection类型
  • keyset( ):返回Map集合当中所有key,返回值类型为Set类型,因为key本身就是一个Set集合!
  • entrySet<Map.Entry<key,value>> :将Map集合中的每对键值对整合为整体,放入一个Set集合中,泛型类型为Map.Entry,本质是Map集合中的一个静态内部类

Map遍历

		HashMap<Object, Object> map = new HashMap<>();
        map.put(1,"张三");
        map.put(2,"张三2");
        map.put(3,"张三3");
方式一:先拿到所有key间接获取对应的所有value    
        Set<Object> sets = map.keySet();
        for (Object key : sets) {
            System.out.println("key="+key+ " " +"value="+map.get(key));
        }
方式二:直接获取到每一个符合键值对整体,然后get属性即可!
       Set<Map.Entry<Object, Object>> entries = map.entrySet();
        for (Map.Entry<Object, Object> entry : entries) {
            System.out.println("key="+entry.getKey()+ " " +"value="+entry.getValue());
        }
5.1、HashMap *

HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列(哈希表)。

  • HashMap 实现了Cloneable接口,可以被克隆。
  • HashMap 实现了Serializable接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。
  • HashMap 继承了AbstractMap,父类提供了 Map 实现接口,具有Map的所有功能,以最大限度地减少实现此接口所需的工作。
一、HashMap的底层实现

JDK1.8 之前

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过路由寻址法: (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

JDK 1.8 之后

jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,同样的是如果红黑树上的节点数小于6的化会再自动转化为单链表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUNnN9iC-1633793386144)(JavaSE.assets/image-20210924222710613.png)]

扩展:所谓扰动函数(哈希算法)指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

//扰动函数
	static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

二、HashMap的扩容机制

HashMap == 数组+散链表+红黑树

  • HashMap 默认初始桶位数16(数组位),如果某个桶中的链表长度大于8,则先进行判断:
  • 如果桶位数小于64,则先进行扩容(2倍),扩容之后重新计算哈希值,这样桶中的链表长度就变短了(之所以链表长度变短与桶的定位方式有关,请接着往下看)。
  • 如果桶位数大于64,且某个桶中的链表长度大于8,则对链表进行树化(红黑树,即自平衡的二叉树)
  • 如果红黑树的节点数小于6,树也会重新变会链表。

结论:所以得出树化条件链表阈值大于8,且桶位数大于64(数组长度),才进行树化

元素放入桶(数组)中,定位桶的方式(数组定位方式):通过数组下标 i 定位,添加元素时,目标桶位置 i 的计算公式,i = hash & (cap - 1),cap为容量

为什么优先扩容桶位数(数组长度),而不是直接树化?

  • 这样做的目的是因为,当桶位数(数组长度)比较小时,应尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率。因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树
  • 而当阈值大于 8 并且数组长度大于 64 时,虽然增了红黑树作为底层数据结构,结构变得复杂了,但是,长度较长的链表转换为红黑树时,效率也变高了。
三、HashMap特点:
  1. 存储无序
  2. 键和值位置都可以是 null,但是键位置只能存在一个 null;
  3. 容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化
  4. 装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
  5. 树化:树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化
  6. 键位置是唯一的,是由底层的数据结构控制的;
  7. jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树;
  8. 阈值(边界值)> 8 并且桶位数(数组长度)大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询;
四、HashMap存储数据的过程

即向hash表中添加元素的执行过程

//测试代码
HashMap<Object, Object> map = new HashMap<>();
        map.put("1","宋淇祥1") ;
        map.put("2","宋淇祥2") ;
        map.put("3","宋淇祥3") ;
        map.put("3","宋淇祥3(已覆盖)") ;  //覆盖
    }

执行流程分析:

  1. 首先,HashMap<String, Integer> hashMap = new HashMap();当创建 HashMap 集合对象的时候,HashMap 的构造方法并没有创建数组,而是在第一次调用 put 方法时创建一个长度是16 的数组(即,16个桶) ,Node[] table (jdk1.8 之前是 Entry[] table)用来存储键值对数据 ;
  2. 将<K , V>封装成为一个Node(节点)对象,底层会调用key的hashCode方法得出hash值,然后通过哈希算法(哈希函数),将hash值转化为数组的下标,如果下标位置(桶位置)如果没有任何元素,就把Node节点添加到这个位置上,如果桶位置上有链表,此时,会拿着key和链表中每个节点中的key进行equals比较,如果返回false,那么这个节点添加到链表末尾,如果其中一个返回true,那么这个节点的value就会被当前Node覆盖 ;

注意:put的执行流程中必须重写实例的HashCode方法和equals方法 ;

五、HashMap相关面试题

具体原理我们下文会具体分析,这里先大概了解下面试的时候会问什么,带着问题去读源码,便于理解

1、HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?

答:对 key 的 hashCode 做 hash 操作,如果key为null则直接赋哈希值为0,否则,无符号右移 16 位然后做异或位运算,如,代码所示(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

除上面的方法外,还有平方取中法伪随机数法取余数法。这三种效率都比较低,而无符号右移 16 位异或运算效率是最高的。

2、当两个对象的 hashCode 相等时会怎么样?

答:会产生哈希碰撞(hash冲突)。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储

3、什么是哈希碰撞,如何解决哈希碰撞?

答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

4、如果两个键的 hashCode 相同,如何存储键值对?

答:通过 equals 比较内容是否相同。

  • 相同:则新的 value 覆盖之前的 value。
  • 不相同:遍历该桶位的链表(或者树):如果找不到,则将新的键值对添加到链表(或者树)中

5、容量为什么必须是 2 的 n 次幂?如果输入值不是 2 的幂比如 10 会怎么样?

答:为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

6、如果创建HashMap对象时,输入的数组长度length是10,而不是2的n次幂会怎么样呢

HashMap<String, Integer> hashMap = new HashMap(10);

HashMap双参构造函数会通过tableSizeFor(initialCapacity)方法,得到一个最接近length且大于length的2的n次幂数(比如最接近10且大于10的2的n次幂数是16)

六、总结:

(1)HashMap是一种散列表,采用(数组 + 链表 + 红黑树)的存储结构;

(2)HashMap的默认初始容量为16(1<<4),默认装载因子为0.75f,容量总是2的n次方;

(3)HashMap扩容时每次容量变为原来的两倍;

(4)当桶的数量小于64时不会进行树化,只会扩容;

(5)当桶的数量大于64且单个桶中元素的数量大于8时,进行树化;

(6)当单个桶中元素数量小于6时,进行反树化;

(7)HashMap是非线程安全的容器;

(8)HashMap查找添加元素的时间复杂度都为O(1);

5.2、HashTable

因为线程安全的问题,HashMap(非线程安全) 要比 Hashtable(线程安全的) 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它。

Dictionary 类是一个已经被废弃的类(见其源码中的注释)。父类被废弃,自然其子类Hashtable也用的比较少了。

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
HashTable和HashMap的区别:
  1. 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
Properties

HashTable的子类,key和value都是只支持字符串!<String , String >

public
class Properties extends Hashtable<Object,Object> {

setProperty :添加元素

public synchronized Object setProperty(String key, String value) {
    return put(key, value);			//底层调用map接口的put方法
}

getProperty :通过key删除

public String getProperty(String key) {
    Object oval = super.get(key);
    String sval = (oval instanceof String) ? (String)oval : null;
    return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}
5.3、TreeMap

TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历。

TreeMap实现了Map、SortedMap、NavigableMap、Cloneable、Serializable等接口。

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

存储结构

TreeMap只使用到了红黑树(特殊的AVL(自平衡)二叉树),所以它的时间复杂度为O(log n),我们再来回顾一下红黑树的特性

源码分析

TreeMap的基本属性:

   private final Comparator<? super K> comparator;

    private transient Entry<K,V> root;   //根节点
    /**
     * The number of entries in the tree
     */
    private transient int size = 0; 	//元素个数

    /**
     * The number of structural modifications to the tree.
     */
    private transient int modCount = 0;		//修改次数
TreeMap的构造方法
/**
 * 默认构造方法,key必须实现Comparable接口 
 */
public TreeMap() {
    comparator = null;
}

/**
 * 使用传入的comparator比较两个key的大小
 */
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}
    
/**
 * key必须实现Comparable接口,把传入map中的所有元素保存到新的TreeMap中 
 */
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

/**
 * 使用传入map的比较器,并把传入map中的所有元素保存到新的TreeMap中 
 */
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}
TreeMap比较方法:

由于TreeMap是会给其中的元素进行排序的,然而排序方式需要我们自己定义!

  1. 方式一:让类实现compareable接口重写compareTo方法,让类本身可比较!

  2. 方式二:创建TreeMap的时候传入一个比较器Comaparator(自定义一个类,重写compare方法),按照比较器的规则比较!

TreeMap的遍历

当我们呢打印输出TreeMap集合的时候,是将自平衡二叉树进行了中序遍历

Treemap插入元素

即向二叉树中插入元素

 public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check	// 如果没有根节点,直接插入到根节点

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;      //获取器集合中的比较器
        if (cpr != null) {		//如果初始化传入比较器comparator,重写compare方法
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);   // 将调用比较器得compare方法,使得插入节点与每个节点进行做差比较
                if (cmp < 0)					 // 通过差值判断节点得大小,由此确定插入节点的位置
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {				  // 如果初始化没有传入比较器,而是让key实现了compareable接口,重写了compareTo方法
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;  //将我们的key转化为可比较的对象
            do {
                parent = t;
                cmp = k.compareTo(t.key);			//然后调用每个节点中key得compareTo方法,
                									//同样通过做差确定插入节点位置
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;    //< 0 插入到节点得左子树
        else
            parent.right = e; 	//> 0 插入到节点得右子树
        fixAfterInsertion(e);  
        size++;
        modCount++;
        return null;
    }

6、红黑树

树 -> 二叉树 -> 二叉搜索树 -> AVL树 - > 红黑树

详情见有道云笔记 ;

左旋

在这里插入图片描述

右旋

在这里插入图片描述

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Just_Goer~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值