一、2、JAVA--集合

一、理解部分

 

    (一)集合基础

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、集合的优点:

        (1)可以动态的存储多个类型任意个对象

        (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循环

        List接口的实现类都可以用以上三种遍历方式,但具体情况下有所利弊

普通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举例),可能依次执行--add--ensureCapacityInteral--ensureExplicitCapacity--grow四个方法,首先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数组

        (1)无参构造器:默认创建一个容量为10的数组,扩容机制和ArraList大同小异,public synchronized boolean add(E e),添加数据时synchronized修饰线程安全;grow里面,计算newCapacity有所不同,此时默认capacityIncrement是0,也就是按2倍扩容

         (2)一个参数构造器

        public Vector(int initialCapacity) {...}

        (3)两个参数构造器:这时候capacityIncrement就不是0而是参数值 

        public Vector(int initialCapacity, int capacityIncrement) {...}

具体:(28条消息) java 集合Vector 的扩容机制_谭永鹏的博客-CSDN博客_vector如何扩容

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、哈希的相关知识(33条消息) java hashcode 位数_面试官问我:hashcode 是什么?和equals是兄弟吗?_哆魚剪辑的博客-CSDN博客

        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)特点:

        是Map接口最常见的实现类;1内部采用键值对的方式存储具有映射关系的数据,存储的类型是HashMap$Node类型(继承了Map$Entry接口);2存入的元素的键不能重复,如果重复会将值覆盖;3允许键值为空但键只能有一个为空;4不能保证映射的顺序,这是由哈希表决定的,5JDK8以后底层维护的是数组+链表+红黑树的结构;6没有实现同步,线程不安全

        (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源码分析 - 平凡希 - 博客园 (cnblogs.com)

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进行关联的。(25条消息) 链表(next)和节点(node)_Angee_sam的博客-CSDN博客_node.next

使用next确定插入位置见上边连接

4.hashmap源码学习:

(1)成员属性

 // 序列号
    private static final long serialVersionUID = 362498820763181265L;  

序列号就是来验证版本一致性的,比如在java中如果想要将对象存储到本地,那是需要将这个对象进行序列化的,序列化成字节存起来,而在需要用的时候,就需要将这个对象进行反序列化,反序列化成对象,那么问题就在这个转换的过程中,比如序列化和反序列化的不是同一个东西,就出错了,有了SUID之后,那么如果序列化的类已经保存了在本地中,中途你更改了类后,SUID变了,那么反序列化的时候就不会变成原始的类了,主要就是用于版本控制的。总结就是这玩意就是比较版本的,没有啥特殊意义。

        其余还有 static final的默认填充因子、初始容量、最大容量、数链转换上下阈值、转数时桶结构的表的最小默认值64;transient不序列化的node数组、具体存放的entryset、存放元素的个数、更改或扩容的计数器、需要初始化的临界值threshold到达这里会扩容、填充因子loadFactor

(2)构造器

1、HashMap(int, float)型构造函数:传入初始容量和填充因子,if判断初始容量大于0小于等于最大值,填充因子不能小于0不能是非数字

初始化填充因子和threshold

 this.loadFactor = loadFactor;
    // 初始化threshold大小
    this.threshold = tableSizeFor(initialCapacity);  

tableSizeFor(initialCapacity)返回大于initialCapacity的最小的二次幂数值。该算法具体:Java8 HashMap之tableSizeFor - 兴趣使然~ - 博客园 (cnblogs.com)

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)重要方法 

1、

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值