集合框架(超详解)

目录

集合框架的作用:用于存储数据的容器

概要:

集合框架的优点:

集合框架的关系图:  

list集合 

list的常用方法:

 ArrayList、linkedlist与vector的区别        

list集合的增删改查的操作:

增加:

修改:

查询:

删除:

list集合的扩容原理:

set集合

如何对list容器中的元素去重?

遍历:

扩容:

hashset的实现

treeSet特性

map集合

map集合概念:

Map接口的常用方法:

Map实现类之一:HashMap

HashMap的存储结构

JDK8之前的源码:

Map实现类之二:LinkedHashMap

LinkedHashMap中的内部类:Entry

Map实现类之三:TreeMap

Map实现类之四:Hashtable

Map实现类之五:Properties


集合框架的作用用于存储数据的容器

集合框架就是为了表示和操作集合而规定的一种统一的标准的体系结构。

概要:

任何的集合框架都会包含三大快内容

1.对外的接口

2.接口的实现

3.集合运算的算法

集合框架的优点:

  • 容器能够自增长
  • 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量、
  • 允许不同API之间的互操作,API之间可以来回传递集合
  • 可以方便地扩展或改写集合,提高代码复用性和可操作性
  • 通过使用JDK自带的集合类,可以降低代码维护和学习新API版本。

集合框架的关系图:  

        

        这次我们先来讲解一下list集合:

list集合 

list的常用方法

 

 ArrayList、linkedlist与vector的区别        


 
 ArrayListLinkedListVector
底层实现 数组双向链表数组
同步性及效率不同步 ,非线程安全,效率高,支持随机访问不同步,非线程安全,效率高同步,线程安全,效率低
特点查询快,增删慢查询慢,增删快查询快,增删慢
默认容量10/10
扩容机制int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5 倍/2倍

总结:

  • ArrayList 和 Vector 基于数组实现,对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
  • LinkedList 不会出现扩容的问题,所以比较适合随机位置增、删。但是其基于链表实现,所以在定位时需要线性扫描,效率比较低。
  • 当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;
  • 当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
     

list集合的增删改查的操作:

增加:

private List<Integer> list = new ArrayList<Integer>();

	// 在每个测试方法之前都会执行
	@Before
	public void setup() {
		list.add(1);
		list.add(2);
		list.add(3);	
		list.add(3);
		list.add(4);
	}

注意:@Test、@Before、@After注释是需要导入单元测试包吗,导入顺序如下:

1.选中项目然后alt+enter

2.选中classpath,然后点击 Add  Library

3.选中JUnit,然后点击 next

 

4.这里建议选择版本4,因为版本5可能会存在注释失效的情况 。最后点击finish即可。

  1.  @Test:目的是为了测试单个 方法,我们之前的代码都是在main方法中进行测试的,我们通过@Test注释就能够使相应的方法不用main方法就可以直接运行了。对于测试而言也是相当方便的。就像我们上面的增加值的方法就是用到了@before注释,就是为了下面每次测试的时候都会自动往集合里面增加值。方便了测试。
  2. @before:目的是将@before作为首先执行的代码,用于资源的申请
  3. @after:目的是为了让 每次释放资源后都会执行注释下的代码。即使出现了异常任然会执行对应的代码,用于资源的释放

修改:

将下标为3的值修为5

	@Test
	public void update() {
		list.set(3, 5);
		System.out.println(list);
	}

然后双击方法名,然后右键:

结果如下:

查询:

查询主要有三种方式:

三种的运行结果都是一样的:

1.for循环遍历

@Test
	public void query01() {
		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i));
		}
	}

2.foreach遍历

@Test
	public void query02() {
		for (Integer i : list) {
			System.out.println(i);
		}
	}

3.使用迭代器遍历

@Test
	public void query03() {
		Iterator<Integer> i = list.iterator();
		while(i.hasNext()) {
			System.out.println(i.next());
		}

删除:

为了方便测试我们准备了一组数据:

12334

要求:将数组中的3全部删除

1.for循环顺遍历根据下标删除:

	@Test
	public void remove01() {
		for (int i = 0; i < list.size(); i++) {
			if (list.get(i) == 3)
				list.remove(i);
		}
		System.out.println(list);
	}

 我们会发现任然还有一个3,为什么会出现这种问题呢?

原因其实很简单,就是因为当数组的第一个3被删除的时候,它的下标为2,第一个3被删除的时候后面的数的下标就会向前面进一位,也就是说第二个3下标会变成2,但是刚刚已经遍历过下标为2的了,只会继续往后面遍历了,所以这就是list集合中的移位。为了防止这个bug,下面有对这个方法的改进。

2.for循环顺遍历(根据下标删除)2:

	@Test
	public void remove02() {
		for (int i = 0; i < list.size(); i++) {
			if (list.get(i) == 3)
				list.remove(i--);

		}
		System.out.println(list);

	}

控制台结果:

         

我们会发现我们的程序没有问题,3也全部都删除了,这又是什么原理呢?

我们发现改进后的代码只有一处变动:

list.remove(i--),这其中的算法其实很简单的:当第一个3被删除的时候此时遍历的下标是2,按道理来说,循环接下来会遍历下标为3的值,这种情况就是上面那种方法,但是这种方式是会存在bug的,所以这个地方在i的后面加上了一个--,就巧妙的避免了这个问题。当删除的操作执行成功之后,后面的值都会进行移位的操作(下标都-1),那么第二个3下标就会为2,所以当i进行了--的操作,循环就会回到下标为2的地方,这个时候第二个3就不会“逃之夭夭”了,还是会进行删除的操作。 

3.for循环逆遍历(根据下标删除):

@Test
	public void remove03() {
		for (int i = list.size() - 1; i >= 0; i--) {
			if (list.get(i) == 3) {
				list.remove(i);
			}
		}
		System.out.println(list);
	}

运行结果:

这种方法与第一种方法其实就是遍历的顺序变了。

那么这种方式为什么变了一下遍历顺序就不会出现bug呢?

如上图:当我们第一次进行删除的操作应该是下标为3的那个3,当这个3被删除的时候,后面的4移位上来,但是你会发现,这个时候4移位上来并不会影响我们后面的操作,是吧,因为程序是从后面开始执行的,也就是从4开始的,那既然4已经执行过了,它移位上来又什么意义呢?我们仔细想一下这就是与方法一的高明之处。

4.使用foreach遍历删除:

@Test
	public void remove04() {
		for (Integer i : list) {
			if (i == 3)
				list.remove(i);
		}
		System.out.println(list);
	}

当我们测试这个代码的时候,会发现,完了芭比q了报错了 

 这个时候我硬是不信邪了,又重新创建了一个集合,为[1,2,3,4],删除倒数第二个数:

如果不是倒数第二个数就会报一样的错! 

@Test
	public void remove04() {
		ArrayList<Integer> list2 = new ArrayList<Integer>();
		list2.add(1);
		list2.add(2);
		list2.add(3);
		list2.add(4);
		
		for (Integer i : list2) {
			if (i == 3)
				list2.remove(i);
		}
		System.out.println(list2);
	}

 这个时候,我们发现它又能够正常运行了,这是为什么,电脑出错了?

其实不是,我们在讨论之前先来看看foreach的遍历原理:

foreach的本质上就是创建了一个迭代器,利用iterator实现遍历,即:

Iterator<Integer> it = list.iterator();
		while (it.hasNext()) {
			if (it.next() == 3) {
				it.remove();
			}
		}

 瞧一瞧,

但是我现在要知道我为什么报错,跟这个有时候关系呢?不急,我们先来看看源码:

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}
  
  //1.
        public boolean hasNext() {
            return cursor != size;
        }

  //2.
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

  //3.
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
        
        //4.
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

好家伙,好像还不是特别清楚

那我们先拆开来看问题!

首先想:为什么遍历list的时候,删除元素会报错?

仔细看下报错的异常:

 我们发现抛出的异常为:checkForComodification()

源码如下:

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

我发现里面判断有一个值是modCount,它就是修改次数

就是说当list调用add()方法时,add方法会对modCount实现++的操作,如上例,共调用了add()4次

则modCount=4

而在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,

而此时:modCount=expectedModCount。

然而,当进行一次删除的操作后,即对list进行了一次修改,则modCount+1,此时的modCount=5,

而expectedmodCount依然为4,因为modCount !=expectedmodCount,所以就会抛出异常。

那既然我们知道了为什么报错,那咱再看看为啥倒数第二个数不抛出异常了!

迭代器实际上调用了两个方法,即:hashNext()和next()

而且我们可以看到抛异常的处理是在next()的方法中

哦可啊

再来看看源码:

//1.
        public boolean hasNext() {
            return cursor != size;
        }

  //2.
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

所以,不抛出异常的2最佳方法,就是不进hashNext()方法,前提是:删除的元素必须为倒数第二个元素。

cursor:保存当前iterator的位置信息,从0开始

其实很简单的,让cursor==size即可,当cursor==size时,返回false。也就是说:当next()方法接收到hashNext()发来的信息,得知,没有下一个元素了,就不会再进行处理了。

可能说到这,还不是很清晰,那就再举个例子:

删除倒数第二个元素,cursor=3,而size-1,即size=3.,当再调用hasNext()判断的时候,cursor==size,return false,自然就不会执行next的方法了,而是执行remove操作删除元素后,直接退出循环,也就不会走到最后抛异常的环节了。

所以,经验所得学会看源码是个好东西!!!

5.使用迭代器遍历删除:

@Test
	public void remove05() {
		Iterator<Integer> it = list.iterator();
		while (it.hasNext()) {
			if (it.next() == 3) {
				it.remove();
			}
		}
		System.out.println(list);
	}

运行结果:

这个就要说到迭代器的删除原理了,这里我们看到源码:

 public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

 我们会发现当我们执行了删除的操作之后会重新赋值给cursor和expectModCount,那么关于这两值,咱在已经详细的提到,讲到这,大家应该知道原理了吧,使用迭代器删除与foreach遍历删除的两则的区别就是在于这两个值变不变 。

6.迭代器的稍微变动:

@Test
	public void remove06() {
		Iterator<Integer> it = list.iterator();
		while (it.hasNext()) {
			Integer value = it.next();
			if (value == 3) {
				list.remove(value);
			}
		}
	}

呕吼,又报错了:

 这个错误大家熟悉吧,没错就是我们在使用foreach遍历删除的时候抛出的异常。

那么问题又来了,这个为什么又报错呢?不是已经使用了迭代器的删除的方法吗

大家仔细看代码会发现

我们的删除方法并不是迭代器的删除方法

查看源码:

boolean remove(Object o);

调用的list删除方法,里面的参数是一个object类型,说明这个删除的方法是根据对象删除的

我们再使用另外一个集合测试:

@Test
	public void remove06() {
		ArrayList<Integer> list2 = new ArrayList<Integer>();
		list2.add(1);
		list2.add(2);
		list2.add(3);
		list2.add(4);
		Iterator<Integer> it = list2.iterator();
		while (it.hasNext()) {
			Integer value = it.next();
			if (value == 3) {
				list2.remove(value);
			}
		}
		System.out.println(list2);
	}

输出的结果为:

这个问题又出现了,为什么呢?其实跟上面是一样的原理,因为调用的不是迭代器的删除方法,所以每次遍历后并没有对cursor和expectModCount进行重新设值,才会出现这个问题的。

7.根据下标删除:

@Test
	public void remove07() {
		list.remove(2);
		System.out.println(list);
	}

查询结果:

1234

8.根据对象删除:

@Test
	public void remove08() {
		list.remove(Integer.valueOf(2));
		System.out.println(list);
	}

结果:

list集合的扩容原理:

@Test
	public void listKR() throws Exception {
		List<Integer> list = new ArrayList<Integer>();
		for (int i = 0; i <= 100; i++) {
			list.add(i);
			System.out.println("i :" + i);
			System.out.println("length :" + getListElsSize(list));
		}
	}

运行结果:

 我们可以看到,当下标为9的时候容量为10,这个时候刚好满了 ,此时还不会进行扩容,当下一个元素进来的时候 ,才进行扩容、

扩容原理:新容量=旧容量*扩容因子

从图中我们不难发现list集合的默认的扩容因子是0.5,当然这个也是能够设置的。

set集合

特点:无序,不重复

如何对list容器中的元素去重?

private List<Integer>list=new ArrayList<>();
	
	@Before
	public void setup() {
		list.add(1);
		list.add(1);
		list.add(2);
		list.add(2);
		list.add(3);
		list.add(3);
	}
	
	@Test
	public void test01() {
		List<Integer> tmp=new ArrayList<>(new HashSet<Integer>(list));
	    System.out.println(tmp);
	}

实现原理:

如代码所示,先将arraylist集合强转为hashset集合,再强转为arraylist集合,这样一来就是arraylist保持了hashset不重复的特点了。

遍历:

与list集合的唯一不同之处就是set集合没有下标,不能根据下标遍历,其他的特性均与list一致

删除修改都会出现list的相关问题。

 
	private Set<Integer>set=new HashSet<>();
	
	@Before
	public void setup() {
		set.add(1);
		set.add(2);
		set.add(2);
		set.add(3);
		set.add(4);
		set.add(5);
		set.add(6);
		set.add(7);
		
	}
	
 
    /**
	 * foreach遍历
	 */
	@Test
	public void test02() {
		for (Integer e : set) {
			System.out.println(e);
		}
	}
	
	/**
	 * 迭代器
	 */
	@Test
	public void test03() {
		Iterator<Integer> it = set.iterator();
		while(it.hasNext()) {
			System.out.println(it.next());
		}
	}

输出结果:

扩容

初始容量16,负载因子0.75,扩容增量1倍

hashset的实现

它存储唯一元素并允许空值,依据对象的hashcode来确定该元素是否存在

示例(检测去重特性):

先创建一个实体类(Student):

package com.ljq.text;

import java.io.Serializable;

/**
 * 
 * @author 一麟
 *
 */
@SuppressWarnings("all")
public class Student {

	private Integer sid;

	private String sname;

	private int age;

	public Student(Integer sid, String sname, int age) {
		super();
		this.sid = sid;
		this.sname = sname;
		this.age = age;
	}

	public Integer getSid() {
		return sid;
	}

	public void setSid(Integer sid) {
		this.sid = sid;
	}

	public String getSname() {
		return sname;
	}

	public void setSname(String sname) {
		this.sname = sname;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}


	@Override
	public String toString() {
		return "Student [sid=" + sid + ", sname=" + sname + ", age=" + age + "]";
	}

	
}

测试代码:

@Test
	public void test04() {
		HashSet<Student> stu = new HashSet<>();
		stu.add(new Student(0, "小明", 18));
		stu.add(new Student(1, "小芳", 18));
		stu.add(new Student(1, "小芳", 18));
		stu.add(new Student(4, "古德", 10));
		stu.add(new Student(7, "拉稀", 18));
		stu.add(new Student(5, "good", 20));
		stu.add(new Student(3, "dis", 30));

		for (Student s : stu) {
			System.out.println(s);
		}
	}

输出结果为:

可以看到它并没有去除重复。

然后我们再在实体类中编写hashcode和equals方法

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((sid == null) ? 0 : sid.hashCode());
		result = prime * result + ((sname == null) ? 0 : sname.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Student other = (Student) obj;
		if (age != other.age)
			return false;
		if (sid == null) {
			if (other.sid != null)
				return false;
		} else if (!sid.equals(other.sid))
			return false;
		if (sname == null) {
			if (other.sname != null)
				return false;
		} else if (!sname.equals(other.sname))
			return false;
		return true;
	}

 输出结果为:

treeSet特性

 是一个包含有序的且没有重复元素的集合,作用是提供有序的Set集合,自然排序或者根据提供的Comparator进行排序。TreeSet是基于TreeMap实现的

我们继续用上面那个例子进行测试:

将hashSet改为treeSet

测试结果:

 我们可以看到报了一个错,其实原因很简单,就是因为treeset内部必须要传入一个比较器compare

一般情况下,有两种比较器的写法,第一种就是在定义集合时充当参数植入集合(自定义比较器),另一种就是在实体类中实现Comparable接口

自定义比较器:

TreeSet<Student>stu=new TreeSet<>(new Comparator<Student>() {
 
			@Override
			public int compare(Student o1, Student o2) {
				
				return o2.getSage()-o1.getSage();
			}
 
			
		});

 我们实现接口之后重写方法:

编辑比较器(按年龄从大到小比较)

@Override
	public int compareTo(Student o) {
		if (this.getAge() - o.getAge() == 0) {
			return this.getSid() - o.getSid();
		}
		return this.getAge() - o.getAge();
	}

就不会报错了。

map集合

map集合概念:

  • 在集合框架的关系图中我们可以看到connection和map是并列存在的,用户保存具有映射关系的数据结构:key-value
  • 其中的key和value都是可以为任意引用类型的数据。
  • Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 valueMap接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类

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中的所有数据
Object get(Object key)获取指定key对应的value
boolean containsKey(Object key)是否包含指定的key
int size()返回map中key-value对的个数
boolean isEmpty()判断当前map是否为空
boolean equals(Object obj) 判断当前map和参数对象obj是否相等
Set keySet() 返回所有key构成的Set集合
Collection values() 返回所有value构成的Collection集合
  • Set entrySet()
返回所有key-value对构成的Set集合

Map实现类之一:HashMap

  • HashMap是 Map 接口使用频率最高的实现类。
  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  • 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
  • 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  • 一个key-value构成一个entry
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
  • HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap的存储结构

HashMap的存储结构在jdk8之后就加上了红黑树实现原理。


   
   
    

JDK8之前的源码:

HashMap中的内部类:Node

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    ...
}

HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

添加元素的过程: 

向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果
hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

HashMap的扩容 

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)* loadFactor时,就会进行数组扩容,loadFactor 的 默 认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16 * 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2 * 16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

JDK1.8之后源码解析

HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。

每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

那么HashMap什么时候进行扩容和树形化呢?

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)* loadFactor时,就会进行数组扩容,loadFactor 的 默 认 值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16 * 0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

关于映射关系的key是否可以修改?answer:不要修改
映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

总结:JDK1.8相较于之前的变化:

HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
当首次调用map.put()时,再创建长度为16的数组
数组为Node类型,在jdk7中称为Entry类型
形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

jdk8 相较于jdk7在底层实现方面的不同: 

new HashMap():底层没有创建一个长度为16的数组
jdk 8底层的数组是:Node[],而非Entry[]
首次调用put()方法时,底层创建长度为16的数组
jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

Map实现类之二:LinkedHashMap

  • LinkedHashMap 是 HashMap 的子类
  • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
  • 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代
  • 顺序:迭代顺序与 Key-Value 对的插入顺序一致

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)方法
     
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);

 


这期的讲解就到这里了,主要是关于集合框架的透析,有啥补充的欢迎私信或者评论我!😄

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一麟yl

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

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

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

打赏作者

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

抵扣说明:

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

余额充值