Java集合【重点】
集合存储的是对象的引用、内存 、集合体系结构图
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分析 *
ArrayList
是List
的主要实现类,底层使用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 指向后一个节点。
源码分析:
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);
}
双向链表和双向循环链表的区别
JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别
添加元素
LinkedList在中间添加元素的方法实现原理就是,典型的双链表在中间添加元素的流程!
- 在队列首尾添加元素很高效,时间复杂度为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) 删除某个位置的节点
总结:
- LinkedList是一个以双链表实现的List;
- LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
- LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
- LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
- LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
3.3、Vector
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用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获取对应的valueremove(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的化会再自动转化为单链表。
扩展:所谓扰动函数(哈希算法)指的就是 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特点:
- 存储无序
- 键和值位置都可以是 null,但是键位置只能存在一个 null;
- 容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化。
- 装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
- 树化:树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。
- 键位置是唯一的,是由底层的数据结构控制的;
- jdk1.8 前数据结构是链表+数组,jdk1.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(已覆盖)") ; //覆盖
}
执行流程分析:
- 首先,HashMap<String, Integer> hashMap = new HashMap();当创建 HashMap 集合对象的时候,HashMap 的构造方法并没有创建数组,而是在第一次调用 put 方法时创建一个长度是16 的数组(即,16个桶) ,Node[] table (jdk1.8 之前是 Entry[] table)用来存储键值对数据 ;
- 将<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的区别:
- 线程是否安全:
HashMap
是非线程安全的,Hashtable
是线程安全的,因为Hashtable
内部的方法基本都经过synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); - 效率: 因为线程安全的问题,
HashMap
要比Hashtable
效率高一点。另外,Hashtable
基本被淘汰,不要在代码中使用它; - 对 Null key 和 Null value 的支持:
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException
。 - 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: 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是会给其中的元素进行排序的,然而排序方式需要我们自己定义!
-
方式一:让类实现compareable接口重写compareTo方法,让类本身可比较!
-
方式二:创建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树 - > 红黑树
详情见有道云笔记 ;
左旋
右旋