一、理解部分
(一)集合基础
1、使用集合的原因:数组长度固定不能更改,数组保存的是同一类的元素,数组增删麻烦
数组扩容(每次增加新数据都要这样做):创立新数组、复制原来数据、(增加新数据)
People[] per = new People[2];
per[0] = new People();//这里括号里可以传入参数,new关键字的用法见前页。不像基本数据类型或者String类型,People类型还得再new一次
扩容:
People[] per2 = new People[2*(per.length - 1)];
for(int i = 0; i < per.length; i++){
per2[i] = per[i];
}
per2[2] = new People(); ……
此时体现集合的好处——add方法即可完成。
2、集合的优点:
动态的存储多个类型的任意个对象
(2)提供了方便操作对象的方法,增删存取:add、remove、set、get
(3)代码更加简洁,直接调用方法,不必反复重写底层代码
3、集合框架体系(背)
绿色是接口,蓝色是实现类
(二)Collection接口
1、Collection接口特点:
public interface Collection<E> extends Iterable<E> [接口继承用extends]
(1)可以存放多个元素,元素类型是Object
(2)存放元素Set无序不可重复;List有序可重复
(3)没有直接的实现类,有两个子接口:Set List
2、Collection接口方法:
【接口不能直接实例化对象,所以以其子接口的ArrayList实现类来举例说明】
【先暂时知道这些,具体细节在不同的实现类中也有所不同】
package com.hl.collection;
import java.util.ArrayList;
public class Collection1 {
public static void main(String[] args) {//肯定要先创立main方法
ArrayList list = new ArrayList();
//1、add();插入单个数据 重载2
list.add("hl");
list.add(23);//这里是自动装箱add(new Integer(23))
list.add(true);//也是自动装箱
list.add(0,"你好");//是重载的add可以按照索引插入单个数据
System.out.println("List1:" + list);
//2、remove();删除单个数据 重载2 可以按照索引或者元素删除
list.remove(0);
System.out.println("List2:" + list);
System.out.println(list.remove("hl"));
list.contains(23);
list.isEmpty();
list.size();
ArrayList list1 = new ArrayList();
list1.add("电");
list1.add("子");
list1.add("信");
list1.add("息");
list.addAll(list1);
list.removeAll(list1);
list.containsAll(list1);
list.clear();
}
【小心set(i,e)方法】
3、遍历方法
遍历是你写好了集合以后,不是用于写集合,写集合想用循环用普通for(int i; i <= 10; i++){ }
(1)迭代器——用于遍历Collection集合中的元素
方法:先创建迭代器(集合对象调用iterater()方法,创建iterator对象);while循环:先用hasNext() 判断有没有下一个元素,再用next() 方法用于指针下移并且将下移后的集合位置的元素返回。
ArrayList list = new ArrayList();
list.add(...)...
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Object obj = iterator.next();
//System.out.println(iterator);
System.out.println(obj);
}
//下一次遍历要重置迭代器 iterator = list.iterator();
原理:
所有继承了Iterator接口的实现类都有iterator() 方法,用于返回一个实现了Iterator接口的对象,他只用于遍历集合本身并不存放对象。
必须先判断hasNext(),如果next()的下一个元素无效,会报错:NoSuchElementException,
所以再次遍历时也要重置迭代器。
【注意:要Iterator iterator = list.iterator();不要直接while( list.iterator().hasNext()) 这样会一直一直输出第一个元素】
(2)增强for循环:本质上是简化的iterator,底层还是调用了iterator();hasNext();next();三种方法;可应用于集合、数组遍历
for(Object obj : list){
system.out.println(obj);
}
练习:创建有三个狗对象的集合,有名字和年龄,两种方法遍历
(三)List接口
1、特点:
【接口特点的叙述模板:他继承于谁;实现类的特点; 他里面放的东西有什么特点】
有序,有索引,允许元素重复;添加顺序与取出顺序一致,当然add存入时也可以指定索引位置
2、方法:
【只是常用的方法,还有其他Collection接口公用的方法和其他不常用的方法】
增:add(); addAll(); 都是可以指定索引位置插入的【不指定也行啊就是不含参数的add(); 方法,不是list特有的】
删:remove();
改:set(); 指定索引必须存在,否则报错
查:indexOf(); lastIndexOf(); subList();get(); subList方法是一个前闭后开区间
3、遍历:
三种:迭代器;增强for循环;普通for循环
具体情况下有所利弊
普通for:
for(int i = 0; i <= list.size() - 1; i++){
system.out.println(list.get(i));
}
【为什么List多了一种普通for循环:】
4、练习:创建书对象,按照书价格从低到高排序
冒泡排序:两层循环:内层循环只是将最大的那个数排到了最后,那还需要将倒数第二大的排在倒数第二位...依次类推所以n个数排序还需要在将这样的循环经历n次,也就是还要一个外层循环。比较1、2位,再比较2、3位,若2、3交换了位置,再比较3、4位,那么1和3就没有进行比较!
package com.hl.collection;
import java.util.ArrayList;
public class List2 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(new Book("三国",16.6,"罗贯中"));
list.add(new Book("JAVA核心技术卷",12.6,"凯"));
list.add(new Book("红楼梦",19.6,"曹雪芹"));
for(Object o : list){
System.out.println(o);
}
sort(list);
System.out.println("===价格升序===");
for(Object o : list){
System.out.println(o);
}
}
//想在main方法里调用,要写成静态的
public static void sort(ArrayList list){
int size = list.size();
for(int i = 0; i < size - 1; i++){
for(int j = 0; j < size - 1 - i; j++){//内层循环记得是j < size - 1 - i,多了没有必要
//取出list中的元素的价格
Book b1 = (Book)list.get(j);//因为get出来的是Object类型的
Book b2 = (Book)list.get(j + 1);
double pr1 = b1.getPrice();
double pr2 = b2.getPrice();
if(pr1 > pr2){ //严格按照前一个>后一个价格就交换位置的比较方式,否则pr1<pr2相当于没变,else也没变啊
list.set(j, b2);
list.set(j + 1, b1);
}
}
}
}
}
class Book{
String name;
double price;
String writer;
public Book(String name, double price, String writer) {
super();
this.name = name;
this.price = price;
this.writer = writer;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the price
*/
public double getPrice() {
return price;
}
/**
* @param price the price to set
*/
public void setPrice(double price) {
this.price = price;
}
/**
* @return the writer
*/
public String getWriter() {
return writer;
}
/**
* @param writer the writer to set
*/
public void setWriter(String writer) {
this.writer = writer;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Book [name=" + name + ", price=" + price + ", writer=" + writer + "]";
}
}
5、ArrayList底层结构
(1)ArrayList底层维护的是一个Object数组elementData,transient Object[] elementData;
transient : 瞬间,短暂的; 表示不会被序列化
(2)ArrayList有两种构造方法:无参构造时,会创建一个初始容量为0的elementData数组,第一次存入数据,扩容为10,以后每次扩容为当前的1.5倍;传入int类型参数构造时,初始容量就是传入的参数值,以后每次都按当前的1.5倍扩容。
6、ArrayList扩容机制:
(1)无参构造器:直接建一个空数组
add方法【整个扩容机制先确定是否需要扩容,再进行赋值】中会将(size + 1)当成minCapacity(数组真实长度)传给ensureCapacityInteral方法【确定当前数组真实需要的容量minCapacity】,这里会先判断一下elementData数组是不是空,空值的话会把默认容量DEFAULT_CAPACITY(10)和传进来的minCapacity中的最大值赋予新的minCapacity并且传给ensureExplicitCapacity方法【判断elementData数组够不够用,是否真的进行扩容 】,这时会先记录一下集合更改次数modcount++(防治多线层修改),再判断一下,如果此时的minCapacity比数组长度elementData.length大,就调用grow方法真的扩容;在grow方法里会先定义oldCapacity为原来数组的长度,再定义新的newCapacity为原来长度的1.5倍左右,再次判断,新容量如果比传进来的minCapacity小,就把新容量的值更新为minCapacity,如果新容量比规定最大值MAX_ARRAY_SIZE还大 ,就更新为hugeCapacity(minCapacity)方法的返回值,最后调用Arrays.copyOf(elementData,newCapacity)方法更新得到最终的elementData.这时会一步步退回到add方法,自行elementData[size++] = e;语句,最后return ture;结束add方法。
(2)有参构造器:传进来一个int参数,如果为0,还是定义一个空数组;大于0定义一个规定的数组;小于0报错。
扩容过程中,就是第一次扩容grow方法中就是按照数组的1.5倍定义newCapacity,去和minCapcity作比较,注意:这里的minCapacity就是扎扎实实的真实需要的容量,从来没和10有过关系。
7、Vector底层结构与扩容机制
底层也是维护了个Object数组
添加数据时synchronized修饰线程安全;grow里面,计算newCapacity有所不同,此时默认capacityIncrement是0,也就是按2倍扩容
(2)一个参数构造器
public Vector(int initialCapacity) {...}
(3)两个参数构造器:这时候capacityIncrement就不是0而是参数值
public Vector(int initialCapacity, int capacityIncrement) {...}
8、LinkedList底层结构
1特点:
(1)底层实现了双向链表和双端队列特点
(2)可以添加任意元素,包括null,元素可以重复
(3)线程不安全,没有实现同步
2底层:
(1)LinkedList底层维护的是一个双向链表
(2)还维护了first和last两个属性,分别指向首尾两个对象
(3)每个节点(每个 Node 对象)中同时维护 prev、item、next三个属性,pre指向前一个对象,next指向后一个对象,最终实现双向链表
(4)LinkedList元素的添加删除走的不是数组而是双向链表,效率很高
3扩容机制
(1)无参构造器:创建了个空 public LinkedList() {}; 此时first、last属性都是空
(2)添加add(); 这是一个返回类型为boolean的方法,执行成功返回true。
传进来一个对象e,进入到linkLast(e); 方法中,第一次last为空,定义了一个node对象l 指向last也就是空,再创建了一个新的node对象newNode,pre是l也就是空,item是e,next是null;更新last属性为newNode,这时候判断一下l是不是空,因为是第一次添加元素所以是空,那么让first属性指向newNode节点对象;如果不是第一次扩容最开始的l不是空,那么会把l 也就是之前的last 赋给新节点的pre属性,之后再更新之前last指向的节点对象的next属性更新为newNode,这样就完成了再链表最后添加新元素的操作。
4删除remove
按删除第一个节点为例:先判断一下第一个节点是不是空,空就报错,不是空就进入unlinkFirst(f)方法:这里会先用element和next记录原有的f.item和f.next;之后更新f.item元素为空,f.next为空(第一个节点的pre本来就是空),令集合的first为记录原来f.next的next对象,这里还要再判断一下next是不是空(万一原来链表就一个节点呢),如果时空就让last为空,不是空就不用改last,让next.prev为空;最后再让集合size减一,记录modcount, 返回删除的那个item值。
9、ArrayList和LinkedList比较
(1)数组:增删效率低,每次改变都要创立新数组,copy旧数组; 改查效率高,可以通过索引定位
(2)双向链表:增删效率高:不用动以前的元素,创建或删除节点,改变属性指向就可以;改查效率低,每次查一个元素都要遍历
(四)Set接口
1、Set接口特点:
继承于Collection接口,存放元素无序且不可以重复,可以有null但最多只能有一个,常见实现类HashSet、TreeSet。
所谓无序:存入和取出的顺序不一致,但取出时自有一套固定顺序。
2、常用方法
3、遍历方式:迭代器、增强for循环。
4、HashSet实现类
(1)全局说明
(2)底层结构
底层就是HashMap,是数组+链表+红黑树的结构
(3)扩容机制
1构造器,就是创建了一个hashmap:
2第一次add("java") 扩容过程
首先是add(e)调用map.put(e,PRESENT); 方法,其中e是HashSet集合中真的存入的元素,放在了底层HashMap的K上,PRESENT是一个object对象,为空,是静态不可变的 static final修饰的,其实就是占个V的位置。
这里先说一下hash(key): 这是一个方法 ,计算的结果并不是key的hashcode,而是在与无符号右移16位的结果按位异或出来的,右移是为了避免碰撞。
public hash(Object key){
int h;
return (key == null) ? 0 : (h = key.hashcode()) ^ (h >>> 16);
}
将5个参数传入putValue方法中:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
1、先设置辅助参数
Node<K,V>[] tab; Node<K,V> p; int n, i;
2、第一次判断:table是不是空或者长度为0,是的话就调用resize()方法,并将返回值:Node数组newTab赋给局部变量tab,并取其数组长度length 赋给局部变量n.(table是hashmap的全局变量,在resize方法里会把它更新为newTab)【也就是第一次扩容,扩到16】
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
3、第二次判断:计算下标值:(n-1)和K的hash值按位与得到,并把table表上改索引位置的节点对象赋给辅助变量P,判断p是不是空.
3.1 如果p是空,那么就创建一个新的Node给该节点对象,放的东西就是hash:传进来的K的hash; 传进来的K,即set要存放的内容;V就是PRESENT为空;next就是null。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
3.2如果p不是空,说明这个索引位置本来就存放了内容,那么就要把现在这个元素挂在原有对象的后面:
else {
//这里又做了一些辅助变量
Node<K,V> e; K k;
3.2.1 针对p的类型不同,又分了三种情况,这里是第一种:p是个节点对象:准备添加的key的hash和p指向的对象的key的hash 相同,并且满足准备加入的k和p指向的对象是同一个,或者说准备添加的key和P指向的对象的Key的equals() 方法作比较是相同的--这时就不能添加元素!!
【就是说不是一个对象,但内容一样也行,比如new了两只猫都叫TOM,不过实际上equals具体还是要看重写时候是怎么定义的了】
【问题:P到底是什么,是当前的Node,可以只有一个节点,也可以是链表或者红黑树】
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
3.2.2 p是个红黑树,调用putTreeVal方法进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
3.2.3 p不是对象也不是红黑树--链表:所以是for循环,循环比较机制 ,在这个循环比较机制当中,没有设置何时跳出循环,只有满足条件后break才能跳出。判断条件:依次和该链表的每个元素比较后,中间发现有相同元素,就直接break;如果都不相同就加到链表的最后,这时还要再判断一下链表的长度是不是达到阈值(8),如果达到了就调用treeifyBin把当前链表树化为红黑树,在树化时如果table为空或长度小于64.会先进行扩容
【上来我就让e=p.next了,虽然if语句不是真,但比较过即执行过,在加上下边的p=e,就是说每循环一次,e和p都往下走一个】
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//这句话就是把新对象挂到p后面去
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
}
}
4、上述流程走完,元素已经添加到集合当中,那么就会记录一次集合更改的记录
++modCount;
5、第三次判断:增加元素后是否需要扩容,这里是增加一个元素所以++size,增加之后和threshold临界值相比,这里是16*0.75=12.
if (++size > threshold)
resize();
6、不需要扩容,进入一个 afterNodeInsertion方法,他在HashMap里其实什么都没干,是个空方法,是留给子类进行操作的。
afterNodeInsertion(evict);
7、成功,返回空
return null;
}
树化:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
1.这是在满足某索引处链表长度达到8时进来的,这里要判断一下,传进来的tab(就是这时的数组table)是不是空或者tab的长度是不是小于MIN_TREEIFY_CAPACITY=64,二者满足其一就先扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
执行完putVal方法会退回put和add,add的返回值就是判断put返回回来的是不是空,空代表true就是添加成功。
3第二次add("php")
还是add-put-putVal,第一次判断不为空,第二次判断索引为9 的Node是空,走到++modCount ,第三次判断不需要扩容,结束方法。
4第三次add("java")
还是add-put-putVal,进入到第二次判断索引为3的地方不为空
【综上所述】:
这里面的equals方法是程序员重写来确定的,不一定是内容相同的!
5、LinkedHashSet实现类
(1)特点:
是HashSet的子类,底层其实是LinkedHashMap,维护的是数组+双向链表, 是这让他看起来是个有序的集合,但依然不能放重复元素。
(2)底层实现:是数组+双向链表。就是说他存放元素还是和HashSet一样,算哈希算索引,看情况链到链表尾部,只不过每个节点里多了before和after指向先后的对象,形成了双向链表的形式,读取时可以根据链表按存入顺序读取,整个集合还维护了head和tail两个属性指向第一个和最后一个节点对象,注意存放的节点类型LinkedHashMap$Entry,是说 数组是HashMap.Node这个内部类类型 但实际存放的是LinkedHashMap$Entry,这里体现的是数组的多态,Entry是继承了HashMap.Noded ,用Entry是因为它扩展了before\after两个属性可以实现双向链表结构。
注:
1、【计算的索引一定在哈希桶的长度之内】:因为n是哈系桶的长度,是2的正整数次幂,二进制表示肯定是1后面跟多少个0,而n-1就得到了一个全是1 的二进制数,大小比n小1,任意一个数和他按位与,最多是(n-1)的第一位是1,肯定比n要小
而且,hash和1111相与,得到的就是他自己的末尾几位数(为1为真,为0为假),这也就是解释了注释2中的内容,和n取模得到的数肯定就是末尾剩的这几位。
2、【hash % n = hash & (n - 1)】
酷工作 - SegmentFault 思否
3、开发技巧提示:定义辅助变量没有必要一次性定好,在什么地方需要再定义
4、哈希的相关知识
5、【equals和hashcode耦合】吸取hashset作业经验,深刻理解
不可能只改equals,因为你必须满足他俩的关系,只改了equals可能出现equals判断相等但hashcode不等的尴尬局面,hashcode不等还是会正常存入集合的。
(五)Map接口——存放双列元素 【以下基于JDK8版本】
1、特点:
(1)存放具有映射关系的数据:K-V
(2)Key/Value 都可以是任意引用类型的数据。都存放在HashMap&Node对象中,虽然平时常用的是字符串作为Key, 但实际上任意Object类型的对象都可以
HashMap map = new HashMap();
map.put(new Object() , "你好");
(3)Key值不能重复——Value会替换; Value值可以重复的,就是说多个Key的值可以是一样的(我们通过计算K的hash,从而确定数据存放在Node数组中的下标,如果K值相同,那数据一定会被替换掉,但value不同,我们根据K值确定存放的具体节点,即便value相同,但数据存放的节点不同,也不会互相影响)
(4)K-V值可以是null,但k值只有一个可以是null,这是因为K不能重复
(5)K-V是一对一的关系,可以通过指定的K值找到对应的Value值,比如可以应用到身份证号与个人的对应关系中
(6)Map存放的K-V是在HashMap$Node(HashMap是Map的一个实现类,它里面有个Node<K,V>内部类)中的,为了方便程序员遍历数组,Node继承了Entry接口,所以也有人说一个KV就是一个Entry。【为什么引用到Entry,引用的过程,可以引用的理由,方便遍历 的理由】
注意,这里右边的Entry里面也是和Node一样每个对象都放了一对键值对,每个node封装成entry,如果愿意还可以用map.keyset()方法把他的K放到一个Set集合里面,map.values()把他的V放到一个Collection接口实现类里面。(上述两个方法的编译类型是你写的,运行类型是HashMap的两个内部类,KeySet和Values)
更要注意的是,右边Entry不是真的放进去了而是左边的一个引用。就是说,真正的键值数据是存放在HashMap$Node这个类的对象里面的,而Set集合和Collection实现类只是指向了它,只是建立了个引用,没有重新放一组数据。
node里,为了方便遍历,又创建 Set集合 ,里面放的是 Entry类型的元素, 即 transient Set<Map.Entry<K,V>> entrySet; 一个Entry 对象里面放的也是KV。 ( 一个Node节点里存放了一对KV,(因为HashMap$Node继承了Map.Entry接口,所以向上)转型成了Entry的对象,又放到了EntrySet集合中,但实际上只是引用过去的,实际存放的还是HashMap$Node )体现的是接口的多态。
转型成的Entry的对象调用getClass()方法返回的编译类型是我写的Set类型,而直接运行得到的是Node类型。
为什么可以引用:因为HashMap$Node实现了Map.Entry接口,接口的多态:一个类实现了接口,这个实现类的实例 就可以赋给这个接口类
Map map = new HashMap();
map.put("01","111");
map.put("02","222");
Set set=map.entrySet();
System.out.println(set.getClass());//HashMap$EntrySet
for(Object entry:set){
System.out.println(entry.getClass());//HashMap$Node
//为了从HashMap$Node中取出k-v
//先做一个向下转型Object->Map.Entry
Map.Entry mentry=(Map.Entry) entry;
System.out.println(mentry.getKey()+mentry.getValur());
}
方便遍历的原因:当把HashMap $ Node对象存放到entrySet就方便我们的遍历,因为Map.Entry提供了重要的方法:K getKey()和V getValue()
Map.Entry中提供的getKey()、getValue()方法,返回的类型分别是Set、Collection
2、重要方法
Map map = new HashMap();
map.put(k,v);//都是实例方法
map.get(k);
map.remove(k);
map.size();
map.isEmpty();判断是不是空集合
map.containsKey(k);判断有没有目标键值
map.clear();
map.keySet(); 返回Set类型
map.values(); 返回Collection类型
3、六大遍历方式
见PDF笔记
4、HashMap实现类
(1)特点:
没有实现同步,线程不安全。
(2)底层机制:
HashMap实现类底层维护了Node数组table,默认为空,table的长度称为容量,存储在table数组中的对象有一个引用变量只想下一个元素;
创建对象时,将加载因子loadfactor初始化为0.75;
当添加键值对时,会先计算K的哈希,从而计算索引位置,继而判断该索引位置是否为空,如果是空,就把键值对添加到这里;如果不是空,就判断此处是否有和当前键值对的键相通的元素,如果有,就覆盖他的Value值,如果没有就判断此处是树结构还是链表结构,继而做出相应的处理,添加时如果到达容量临界值就会触发扩容机制;如果数上的元素又被删除小于等于6了,数组为6实际7个元素,就会调用untreeify方法树就会退化到链表;要小心的是,table扩容之后,计算出来的索引会发生变化;
第一次添加元素需要扩容table为16,临界值为16*0.75=12;
此后如果元素达到临界值就把table扩容到原来的两倍,临界值也相应变为两倍;
JKD8中默认链表元素个数超过TREEIFY_THRESHOLD,默认是8,同时数组table容量大于等于MIN_TREEIFY_CAPACITY默认64就会进行树化。
(3)源码分析
public class HM{
public static void main(String[] args){
HashMap map = new HashMap();
map.put("java",10);
map.put("php",20);
map.put("java",20);
}
1.执行构造器:无参构造器 即 初始化加载因子为默认值0.75;创建空的HashMap$Node类型的空数组table;
2.执行put方法!!!【注意!!!map接口添加元素用put】
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
传进来两个参数,键和值;执行putVal方法,该方法5个参数:键的hash,键,值,false,true
3.执行putVal方法--和set那里一样
注:
1、接口是不能实现,不能实例化的,所谓接口的特点也是讲他实现类的特点。
2、Set的底层实际也是用的双列,只不过它是元素都存到K里,V里都是存放的present
(二)hashmap
1、源码参考
JDK1.8 HashMap源码分析
2、hashmap的原理,结构等见记忆部分
3、JDK1.8之后采用数组+链表+红黑树来实现hashmap,底层采用的是node数组。
Node是HashMap的一个内部类,其源码学习:
(1)实现了Map.Entry接口,有hash,key,value,next属性,全参构造器,getkey,getvalue,tostring,setvalue方法,以及equals方法:判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true。
可以看到,node中包含一个next变量,这个就是链表的关键点,hash结果相同的元素就是通过这个next进行关联的。
使用next确定插入位置见上边连接
4.hashmap源码学习:
(1)成员属性
// 序列号 private static final long serialVersionUID = 362498820763181265L;
序列号就是来验证版本一致性的,比如在java中如果想要将对象存储到本地,那是需要将这个对象进行序列化的,序列化成字节存起来,而在需要用的时候,就需要将这个对象进行反序列化,反序列化成对象,那么问题就在这个转换的过程中,比如序列化和反序列化的不是同一个东西,就出错了,有了SUID之后,那么如果序列化的类已经保存了在本地中,中途你更改了类后,SUID变了,那么反序列化的时候就不会变成原始的类了,主要就是用于版本控制的。总结就是这玩意就是比较版本的,没有啥特殊意义。
需要初始化的临界值threshold到达这里会扩容、填充因子loadFactor。
(2)构造器
1、HashMap(int, float)型构造函数:传入初始容量和填充因子,if判断初始容量大于0小于等于最大值,填充因子不能小于0不能是非数字
初始化填充因子和threshold
this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity);
tableSizeFor(initialCapacity)返回大于initialCapacity的最小的二次幂数值。
2、HashMap(int)型构造函数,调用1
3、HashMap()型构造函数,初始化loadFactor为默认
4、HashMap(Map<? extends K>)型构造函数,初始化loadFacor为默认,并把参数存到hashmap中,即putMapEntries(m, false);
(3)hash算法
右移16位:在putValue()方法中,会用到(n-1)&h,这个n:hash桶数据的长度为2^n次幂,n不会太大,但h=key.hashcode();的高位有值,右移16位会使得h的高位也参与运算,减小碰撞几率。
这样我们最后计算出来的就是数组下标
(4)重要方法