Java SE基础——Java集合
Java SE基础——Java集合
Java集合框架概述
Java容器:集合与数组
(1) 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。 集合,数组都是对多个数据进行存储操作的结构
- 说明:此时的存储,主要指的是内存的存储,不涉及到持久化的存储( .txt, .jpg, .avi, 数据库中)
(2) 数组在存储多个数据方面的特点:
- 一旦初始化以后,其长度就确定了。
- 数组一旦定义好,其元素的类型也就确定了。只能操作指定类型的数据。比如:String[] arr;等,但是对于Object[] arr,可以利用多态性,存放Object类的子类。
(3)数组在存储多个数据方面的缺点:
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。
- 获取数组中实际元素的个数的需求,数组中没有现成的属性或方法可用
- 数组存储的数据是有序的、可以重复的。---->存储数据的特点单一
集合的使用
Java集合两种体系:Collection和Map(这两者是接口)
Collection接口(该接口没有提供具体实现类):单列数据,定义了存取一组对象的方法的集合。
- List:元素有序、可重复的集合
- Set:元素无序、不可重复的集合
Collection接口继承树:(实线:代表继承关系 , 虚线:代表实现关系 )
Map 接口:双列数据,保存具有映射关系“key-value对”的集合
Map接口继承树 :
Collection接口 方法
Collection接口:
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals( )
单列集合,用来存储一个一个的对象**(如果输入的是int,double等基本类型数据,则存储的是对应转换的包装类)**,是 List、Set 和 Queue 接口的父接口,该接口里定义的方法,既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
List接口:(重心)
- 单列数据,也可以看作键值对(key-value)特点,不过这里的key对应的是索引(index)
从0开始,key自动通过索引方式去维护 - 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组,”“动态”数组。
- 存储有序的,可重复的数据,集合中的每个元素都有其对应的顺序索引。
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据
序号存取容器中的元素。
List接口方法
- 使用的是equals( )
- void add(int index, Object ele): 在index 位置插入ele元素
- boolean addAll(int index, Collection eles): 从index 位置开始将eles中的所有元素添加进来
- Object get(int index): 获取指定index位置的元素
- int indexOf(Object obj): 返回obj在集合中首次出现的位置,如果找不到,就返回-1。
- int lastIndexOf(Object obj): 返回obj 在当前集合中末次出现的位置
- Object remove(int index): 移除指定index位置的元素,并返回此元素
- Object set(int index, Object ele): 设置指定index 位置的元素为ele
- List subList(int fromIndex, int toIndex): 返回从fromIndex 到toIndex位置的左闭右开区间的子集合, 这里只是将返回值赋给变量,不会修改原来的值
总结:常用的方法
- 增:add(Object obj)往末尾添加
- 删:remove(int index) / remove(Object obj)
- 改: set(int index, Object ele)
- 查: get(int index)
- 插; add(int index, Object obj)
- 长度: size( ) 返回的不是底层数组的长度,是元素的个数
- 遍历: (1) Interator迭代器方式 (2) 增强for循环 (3) 普通的循环
//用来测试遍历
@Test
public void test3(){ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
System.out.println(list) //输出[123, 456, AA]//方式一:Iterator迭代器方式
Iterator iterator = list.iterator();
while(iterator.hasNext()){System.out.println(iterator.next());
}//方式二:增强for循环
for(Object obj : list){System.out.println(obj);
}//方式三:普通for循环
for(int i =0;i < list.size(); i++){System.out.println(list.get(i));
}
}
public class ListTest{@Test
public void test1(){ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
list.add(new Person("TOM",12));
list.add(456);System.out.println(list);//void add(int index, Object ele):在index位置插入ele元素
List list1 = Arrays.asList(1,2,3);
list.addAll(list1);
//list.add(list1);//这里如果用add,相当于是把1,2,3当作一个整体。
System.out.println(list.size());
}
}
面试题
- 区分List中remove(int index) 和remove(Object obj )
@Test
public void testListRemove() {List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);//add( )方法中,没有重载,没有选择,因为形参是Object,所以自动装箱。
updateList(list);
System.out.println(list);
}
private static void updateList(List list) {list.remove(2); //这里输出的结果是[1,2].这里remove()方法有选择,装箱麻烦,可以直接用index,所以这里remove中的2是代表索引
list.remove(new Integer(2));//这里就是删除2这个元素,结果为[1,3].
}
JDK API中List接口的实现类常用的有:
ArrayList
- ArrayList 是 List 接口的典型实现类、主要实现类
- 作为List接口的主要实现类 ;线程不安全的,效率高;底层使用Object[] elementData存储.
- 本质上,ArrayList是对象引用的一个”变长”数组
- Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是
Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合
ArrayList的源码分析
- jdk 7情况下:
- ArrayList list = new ArrayList();底层创建了长度是10的Object[]数组elementData,
list.add(123);elementData[0] = new Integer(123);… 如果此次添导致底层elementData数组容量不够,则扩容。默认情况下。扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
- ArrayList list = new ArrayList();底层创建了长度是10的Object[]数组elementData,
- jdk 8情况下:
- ArrayList list = new ArrayList();底层Object[ ] elementData初始化为{ },并没有创建长度为10的数组
- List.add(123);第一次调用add( )时,底层才创建了长度10的数组,并将数组123添加到 elementData[0]中,后续的添加和扩容操作与jdk 7 无异。
- 小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
LinkedList
- 对于频繁的插入,删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
- 没有角标的说法,双向链表,内部没有声明数组,而是定义了Node类型的first和last,
用于记录首末元素。同时,定义内部类Node:数据存储的基本单位,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:(1) prev变量记录前一个元素的位置,(2) next变量记录下一个元素的位置
LinkedList的源码分析
- LinkedList list = new LinkedList( );内部声明了Node类型的first和last属性,默认值为null
- list.add(123); 将123封装到Node中,创建了Node对象
- 其中,Node定义为:体现了LinkedList的双向链表的说法
Vector(不重要,不常用)
- 作为List接口的古老实现类;线程安全的,效率低
Vector的源码分析:
- jdk7和jdk8中通过Vector( )构造器创建对象时,底层都创建了长度为10的数组,在扩容方面,默认扩容为原来的数组长度的2倍。
面试题:ArrayList,LinkedList,Vector三者的异同?
(1)同:三个类都是实现了List接口,存储数据的特点相同:存储有序的,可重复的数据
(2)异:ArrayList: 作为List接口的主要实现类 ;线程不安全的,效率高;底层使用Object[] elementData存储.
LinkedList:对于频繁的插入,删除操作,使用此类效率比ArrayList高;底层使用双向链表存储。
Vector:作为List接口的古老实现类;线程安全的,效率低,底层是数组
面试题:请问ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
ArrayList和LinkedList的异同
- 二者都线程不安全,相对线程安全的Vector,执行效率高。此外,ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
ArrayList和Vector的区别
- Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类synchronized,属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
Set接口:
- 存储无序的(即:无索引),这里无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序来添加,而是根据数据的哈希值决定的。
- 不可重复的数据:保证添加的元素按照equals( )判断时,不能返回true. 即:相同的元素只能添加一个。
- Set接口是Collection的子接口,set接口没有提供额外的方法,使用的都是Collection中声明过的方法
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
- Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
HashSet
作为Set接口的主要实现类;线程不安全的;可以存储null值,不能保证元素的排列顺序
底层:数组+链表的结构(前提:jdk7)
对于存放在Set容器(主要指:HashSet , LinkedHashSet)中的对象, 对应的类一定要重写equals() 和hashCode(Object obj) 方法,以实现对象相等规则 。即:“相等的对象必须具有相等的散列码(即:哈希值)” 。
HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
添加元素过程
- 向HashSet中添加元素a,首先调用元素a所在类的hashCode( )方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
- 情况一 : 如果此位置没有其他元素,则元素a添加成功
- 如果此位置上有其他元素( 或以链表形式存在的多个元素),则比较元素a与元素b的hash值:情况二:(1) 如果hash值不相同,则元素a添加成功。 (2)如果hash值相同,进而需要调用元素a所在类的equals( )方法: equals( )返回true,元素a添加失败
情况三:equals( )返回false,则元素a添加成功。
- 如果此位置上有其他元素( 或以链表形式存在的多个元素),则比较元素a与元素b的hash值:情况二:(1) 如果hash值不相同,则元素a添加成功。 (2)如果hash值相同,进而需要调用元素a所在类的equals( )方法: equals( )返回true,元素a添加失败
对于添加成功的情况二和情况三而言:元素 a 与已经存在指定索引位置上数据以链表的方式存储。
- jdk 7:元素a放到数组中,指向原来的元素。
- jdk8 :原来的元素在数组中,指向元素a
- 总结:七上八下
HashSet经典面试题
package com.xsl.java;import org.junit.Test;import java.util.HashSet;/*** @author xsl* @create 2022-04-13 16:05*/
public class CollecTest {@Testpublic void test(){HashSet set = new HashSet();//这里Person类中重写了HashCode()和equals()方法Person p1 = new Person(1001,"AA");Person p2 = new Person(1002,"BB");set.add(p1);set.add(p2);//输出[Person{id=1001,name='AA'}, Person{id=1002,name='BB'}]System.out.println(set);p1.name = "CC";set.remove(p1); //输出[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]System.out.println(set); // [1]set.add(new Person(1001,"CC"));//输出[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}] System.out.println(set); // [2]set.add(new Person(1001,"AA"));//输出[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]System.out.println(set); // [3]}
- 代码结果分析
- 针对结果[1],这里将p1的name改成CC,但是原来是由1001,AA计算的哈希值,由底层原理可知,此时remove移除的是(1001,CC)的哈希值,此时,该哈希值对应的数组没有元素,所以没有元素可删除,故输出两个。
- 针对结果[2],虽然这里添加的(1001,CC),但是原来的(1001,CC)是通过修改前(1001,AA)的哈希值添加的,所以两者的哈希值不一样,可以添加,所以输出三个。
- 针对结果[3],这里添加的是(1001,AA),其哈希值对应的数组上的位置被占据,但是该值是(1001,CC),哈希值相等,但是equals值不一样,所以可以添加,通过链表的方式继续链接,故输出四个。
重写 hashCode() 方法的基本原则
在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
效果图:
LinkedHashSet
作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
在添加数据的同时,每个数据还维护了两个引用(指针),记录此数据前一个数据和后一个数据
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
LinkedHashSet 不允许集合元素重复。
对于频繁的遍历操作,LinkedHashSet效率高于HashSet
LinkedHashSet底层结构
TreeSet
- TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态
- 可以按照添加对象的指定属性,进行排序。
- 向TreeSet中添加的数据,要求是相同类的对象
- 不能放相同的数据
- TreeSet底层使用 红黑树(特点:有序,查询速度比List快) 结构存储数据
- 两种排序方式:(1) 自然排序(默认情况下,TreeSet 采用自然排序) (2)定制排序
自然排序
比较两个对象是否相同的标准为:compareTo(Object obj )返回0,不再是equals( )方法。
TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列
实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通compareTo(Object obj) 方法的返回值来比较大小。
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类 的 对象
定制排序
- TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
- 比较两个对象是否相同的标准为: compare(Object o1,Object o2 )返回0,不再是equals( )方法。
- 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
- 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
- 此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。
Collection接口中的方法的使用
- 添加
- add(Object obj)
- addAll(Collection coll)
- 获取有效元素的个数
- int size()
- 清空集合
- void clear()
- 是否是空集合
- boolean isEmpty()
- 是否包含某个元素
- boolean contains(Object obj):是通过元素的equals方法来判断是否
是同一个对象 - boolean containsAll(Collection c):也是调用元素的equals方法来比
较的。拿两个集合的元素挨个比较。
- boolean contains(Object obj):是通过元素的equals方法来判断是否
- 删除
- boolean remove(Object obj) :通过元素的equals方法判断是否是
要删除的那个元素。只会删除找到的第一个元素 - boolean removeAll(Collection coll):取当前集合的差集
- boolean remove(Object obj) :通过元素的equals方法判断是否是
- 取两个集合的交集
- boolean retainAll(Collection c):把交集的结果存在当前集合中,不
影响c
- boolean retainAll(Collection c):把交集的结果存在当前集合中,不
- 集合是否相等
- boolean equals(Object obj)
- 这里如果是List或者ArrayList,这里不仅要求元素一样,顺序也要一样,如果是set,只需要元素一样,因为本身是无序的。
- 转成对象数组
- Object[] toArray()
- 数组 -->集合:
- 调用Arrays类的静态方法asList(可变形参)
- 获取集合对象的哈希值
- hashCode()
- 遍历
- iterator():返回迭代器对象,用于集合遍历
//向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().
public class CollectionTest{@Testpublic void test1(){Collection coll = new ArrayList();//1.add(Object e):将元素e添加到集合coll中coll.add("AA");coll.add("BB");coll.add(123)//自动装箱coll.add(new Date());coll.add(new String("TOM"));Person p = new Person("Jerry","20");coll.add(p);//2.size():获取添加的元素的个数System.out.println(coll.size()); //4//3.addAll():将coll1集合中的元素添加到当前的集合中Collection coll1 = new ArrayList();coll1.add(456);coll1.add("CC");coll.addAll(coll1);System.out.println(coll.size());//6//4.clear():清空集合元素(是指清理里面的数据,不是将其变成null)coll.clear();//5.isEmpty():判断当前集合是否为空(是判断集合当中是否有元素)System.out.println(coll.isEmpty());//6.contains(Object obj):判断当前集合中是否包含obj//我们在判断时会调用obj对象所在类的equalsboolean contains = coll.contains(123);System.out.println(coll.contains(new String("Tom")));//true//这里判断的是里面的内容,不是地址System.out.println(coll.contains(p));//true,因为是同一个引用//7.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在与当前集合中Collection coll1 = Arrays.asList(123,456);//多态形式System.out.println(coll.containsAll(coll1));//8.remove(Object obj):从当前集合中移除obj元素coll.remove(123); //9.removeAll(Collection coll1):从当前集合中移除coll1中所有的元素Collection coll1 = Array.asList(123,4567);coll.removeAll(coll1);System.out.println(coll);//10.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合Collcetion coll1 = Array.asList(123,456,789);coll.retainAll(coll1);System.out.println(coll);//11.equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同Collection coll2 = new ArrayList();coll2.add(456);coll2.add("CC");System.out.println(coll1.equals(coll2));//12.hashCode():返回当前对象的哈希值System.out.println(coll.hashCode());//13.集合-->数组:toArray()Object[] arr = coll.toArray();for(int i =0;i < arr.length;i++){System.out,println(arr[i]);//扩展:数组 -->集合:调用Arrays类的静态方法asList()//这里asList(可变形参),这里可变形参等同于数组。List<String> list = Arrays.asList(new String[]{"AA","BB","CC"});List<String> list = Arrays.asList({"AA","BB","CC"});//new String[]可以不用写,因为asList(可变形参),这里可变形参等同于数组。System.out.println(list);//这里注意//错误写法List arr1 = Arrays.asList(new int[]{123,456});System.out.println(arr1.size());//这里输出是1,因为把123,456看成整体//正确写法List arr1 = Arrays.asList(new Integer[]{123,456});System.out.println(arr2.size()); //2//iterator():返回Iterator接口的实例,用于遍历集合元素。}}
}
Map接口: (重心)
双列集合,用来存储一对(key-value)一对的数据 ,这里key可以是任何指定的类型, 类似于高中的函数: y=f(x)
-Map与Collection并列存在。用于保存具有 映射关系的数据:key-value
Map 中的 key 和 value 都可以是任何引用类型的数据
Map 中的 key 用Set来存放,无序的, 不允许重复,即同一个 Map 对象(key所在的类)所对应的类,须重写hashCode()和equals()方法
Map中的value: 无序的,可重复的,使用Collection存储所有的value
Map中的entry:无序的,不可重复的,使用set存储所有的entry
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
方法
- 添加 、 删除、修改操作
- 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
- boolean containsValue(Object value):是否包含指定的value
- 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集合
代码显示:
@Test
public void test(){Map map = new HashMap();map.put("AA",123);map.put(45,123);map.put("BB",56);//遍历用的iterator适用于Collection
//遍历所有的key集:keySet()Set set = map.keySet();Iterator iterator = set.iterator();while(iterator.hasNext()){System.out.println(iterator.next());
}
//遍历所有的value集:values()
Collection values = map.values();
for(Object obj : values){System.out.println(obj);
}
//遍历所有的key-value
//方式一:entrySet():Set entrySet = map.entrySet();Iterator iterator1 = entrySet.iterator();while(iterator1.hasNext()){Object obj = iterator1.next();
//entrySet集合中的元素都是entryMap.Enter entry = (Map.Enter) obj;System.out.println(entry.getKey() + "------" + entry.getValue());
}//方式二:Set keyset = map.keySet();Iterator iterator2 = keySet.iterator();while(iterator2.hasNext()){Object key = iterator2.next();Object value = map.get(key);System.out.println(key + "====="m + value);
}}
常用方法
- 添加:Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
- 删除:Object remove(Object key):移除指定key的key-value对,并返回value
- 修改:Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
- 查询:Object get(Object key):获取指定key对应的value
- 长度:size()
- 遍历:Set keySet( ) / Collection values( ) / Set entrySet( )
HashMap (Map接口使用频率最高的实现类)
线程不安全的,效率高,存储null的key和value
允许使用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时,会初始化initialCapacity和loadFactor,在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
HashMap的底层实现原理?
以jdk7为例说明:
HashMap的内部存储结构其实是 数组和链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。
添加元素的过程
HashMap map = new HashMap( );
在实例化以后,底层创建了长度是16的一维数组Entry[ ] table。
map.put(key1,value1):
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素,则entry1直接添加成功。 【情况一】
如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。【情况二】
如果hash值相同,继续比较二者是否equals(比较的是key的属性)。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。 【情况三】 entry1指向原有的entry元素。
补充: 关于情况二和情况三:此时key-value 和原来的数据以链表的方式存储
HashMap 的扩容
-默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来- 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的
长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
- 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的
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中的元素个数超过数组大小(数组总大小length,不是数组中个数size)×
以jdk8为例说明:
- jdk8相较于jdk7在底层实现方面的不同:
- new HashMap( ):底层没有创建一个长度为16的数组
- jdk8 底层的数组是: Node[ ] , 而非Entry[ ]
- 首次调用put( )方法时,底层创建长度为16的数组
- jdk 7 底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。 当前数组中的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所有数据改为使用红黑树存储。
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)–>[ jdk7:新的元素指向旧的元素。 jik8:旧的元素指向新的元素 ]
HashMap源码中的重要常量
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- DEFAULT_LOAD_FACTOR :HashMap的默认加载因子:0.75
- threshold :扩容的临界值,=容量*加载因子 ,既: 16×0.75 =12
- TREEIFY_THRESHOLD :Bucket中链表长度大于该默认值,转化为红黑树 : 8
- MIN_TREEIFY_CAPACITY :桶中的Node被树化时最小的hash表容量。64
面试题:负载对 因子值的大小,对HashMap 有什么影响
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞(碰撞:两个key不一样,因为数组比较小,放在数组同一个位置上,以链表的方式存在)的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
LinkedHashMap
- LinkedHashMap 是 HashMap 的子类,底层使用的结构与HashMap相同。
- 区别在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node
- 保证在遍历map元素时,可以按照添加的顺序实现遍历。
- 原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素,对于频繁的遍历操作,此类执行效率高于HashMap
LinkedHashMap 的底层实现原理(了解)
TreeMap
保证按照添加的key-value对 进行排序,实现排序遍历。TreeMap 可以保证所有的 Key-Value 对处于 有序状态。 此时考虑key的自然排序或定制排序,底层使用红黑树
要求key必须是同一个类创建的对象
TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable接口
TreeMap判断 两个key 相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
Hashtable (不重要)
- 作为古老的实现类,线程安全的,效率低。不能 存储null的key和value
- 即使涉及到多线程,也不用Hashtable
Properties
- 常用来处理配置文件。 key和value都是String类型。
- Properties 类是 Hashtable 的子类,该对象用于处理属性文件
- 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
- 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法
- 关键代码部分
//properties:常用来处理配置文件。key和value都是String类型 FileInputStream fis = null;Properties pros = new Properties(); //这里要处理异常fis = new FileInputStream("jdbc.properties");pros.load(fis); //加载流对应的文件String name = pros.getProperty("name");String password = pros.getProperty("password");System.out.println("name "+name +", password = " + password);
Iterator迭代器接口
- Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
- GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元
素,而又不需暴露该对象的内部细节。 迭代器模式,就是为容器而生。类似于“公
交车上的售票员”、“火车上的乘务员”、“空姐”。 - Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合
的第一个元素之前。
Iterator 接口的方法
- hasNext()–boolean
- next() – E
- remove()–void:可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove()
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且
下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。
迭代器的执行原理
注意:1.执行next()时候,指针才下移; 执行hasNext()指针没有下移
2.Iterator是迭代器,不是容器,其遍历的数据还是原来容器Collection里面的数据。
3. 如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
public class IteratorTest{@Testpublic void test1(){Collection coll = new ArrayList();coll.add(123);coll.add(456);Person p = new Person("Jerry","20");coll.add(new String("TOM"));coll.add(false);Iterator interator = coll.iterator();System.out.printin(interator.next());//123//推荐://hasNext():判断是否还有下一个元素while(iterator.hasNext()){//next():1.指针下移 2.将下移以后集合位置上的元素返回System.out.println(iterator.next());//错误方式//原因:集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。while(coll.iterator().hasNext()){System.out,println(coll.iterator().next());//循环输出123}}//测试Iterator中的remove()@Testpublic void test2(){Collection coll = new ArrayList();coll.add(123);coll.add(456);Person p = new Person("Jerry","20");coll.add(new String("TOM"));coll.add(false);//删除集合中"TOM"Interator interator = coll.iterator();while(iterator.hasNext()){Object obj = iterator.next();if("TOM".equals(obj)){iterator.remove();//遍历集合iterator = coll.iterator();while(iterator.hasNext()){System.out.println(iterator.next());}}}}}
}
使用 foreach 循环遍历集合元素(jdk 5.0 新增)
- foreach 循环迭代访问 Collection和数组。
- 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
- 遍历集合的底层调用Iterator完成操作。
- for(集合元素的类型 局部变量 : 集合对象)
public class ForTest{@Testpublic void test1(){Collection coll = new ArrayList();coll.add(123);coll.add(456);Person p = new Person("Jerry","20");coll.add(new String("TOM"));coll.add(false);//for(集合元素的类型 局部变量 : 集合对象)//内部仍然调用了迭代器for(Object obj : coll){System.out.println(obj);}
}@Testpublic void test2(){int[] arr = new int[]}{1,2,3,4,5,6};//for(数组元素的类型 局部变量 : 数组对象)for(int i : arr){System.out.println(i)}}//面试题:@Testpublic void test3(){String[] arr = new String[]}{"MM","MM","MM"};for(String s : arr){s = "GG";}for(int i = 0;i < arr.length; i++){System.out.println(arr[i]);//"MM","MM","MM"}
//(解析:因为对于增强for循环,这里只是把arr数组中值取出来,赋给新的变量s,并不会改变原来的arr数组中的值)}
Collections工具类
- Collections 是一个操作 Set、List 和 Map 等集合的工具类
-排序操作:(均为static方法)
- reverse(List):反转 List 中元素的顺序
- shuffle(List):对 List 集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
- sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
- swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
查找,替换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中
---- — 注意:
//报异常 List dest = new ArrayList(); Collection.copy( dest,list); System.out.println(dest); //这里会报异常,由底层源码可知,要让list.size() < = dest.size()//正确写法 List dest = Arrays.asList(new Object[list.size( )]); System.out.printin(dest.size()); //输出就是list.size() Collections.copy(dest,list); System.out.println(dest);
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
Collections常用方法:同步控制
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
Collection 和 Collections 的区别
Collection是集合类的上级接口,继承于他的接口主要有Set 和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中
---- — 注意:
//报异常 List dest = new ArrayList(); Collection.copy( dest,list); System.out.println(dest); //这里会报异常,由底层源码可知,要让list.size() < = dest.size()//正确写法 List dest = Arrays.asList(new Object[list.size( )]); System.out.printin(dest.size()); //输出就是list.size() Collections.copy(dest,list); System.out.println(dest);
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值
Collections常用方法:同步控制
- Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
Collection 和 Collections 的区别
- Collection是集合类的上级接口,继承于他的接口主要有Set 和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作
Java SE基础——Java集合相关推荐
- Java SE基础(十六)集合
Java SE基础(十六)集合 集合 集合体系 数据结构简述 栈 队列 数组 链表 Collection集合 List集合 ArrayList集合 LinkedList集合 Set集合 HashSet ...
- 面试必会系列 - 1.1 Java SE 基础
本文已收录至 github,完整图文:https://github.com/HanquanHq/MD-Notes Java SE 基础 面向对象 Java 按值调用还是引用调用? 按值调用指方法接收调 ...
- Java复习总结(二)Java SE基础知识
Java SE面试题 目录 Java SE基础 基本语法 数据类型 关键字 面向对象 集合 集合类概述 Collection接口 进阶 线程 锁 JDK 反射 JVM GC io操作和NIO,AIO ...
- Java SE 基础知识
Java SE 基础知识 1 2 @(Notes)[J2SE, Notes] VICTORY LOVES PREPARATION. 特别说明: 该文档在马克飞象查阅最佳: 本部分知识还在迭代中,欢迎补 ...
- Java SE基础(更新中)
Java的运行机制 Java SE基础(更新中) 基本语法 大小写敏感:Java 是大小写敏感的,这就意味着标识符 Hello 与 hello 是不同的. 类名:对于所有的类来说,类名的首字母应该大写 ...
- JAVA SE基础笔记
第一天 JDK安装.快捷键.JAVA语言特点 1.Java语言的概述 1.1 Java语言的背景 Java语言诞生于1995年,在编程语言排行榜占据重要的地位. Java语言之父是高斯林,以前隶属于 ...
- Java SE 基础知识汇总
目录 一.Java概述 1.1 Java语言背景 1.2 Java语言的三个版本 1.3 Java语言的跨平台原理 1.4 JRE和JDK 1.5 DOS命令 二.Java环境搭建 2.1 环境变量的 ...
- JAVA SE基础知识总结
JAVA基础篇 1_JAVA语言概述 1.1 JAVA的总体概述 1.2 JAVA语言概述 1.2.1 基础常识 1.2.2 计算机语言的发展迭代史 1.2.3 Java语言版本迭代概述 1.2.4 ...
- 零基础学JAVA]Java SE基础部分-01. Java发展及JDK配置
1.课程名称:Java发展及JDK配置 本季介绍了JAVA的发展过程,包括JDK的发展历程,path路径的配置和classpath的配置及作用.并简单讲解了一个简单的JAVA程序,并通过此程序讲解了J ...
- java实现linkstring,【JAVA SE基础篇】32.String类入门
[JAVA SE基础篇]32.String类入门 1.字符串 1.String类又称作不可变字符序列 2.String位于java.lang包中,java程序默认导入java.lang包下所有的类 3 ...
最新文章
- 千万级并发HAproxy均衡负载系统介绍
- 微信小程序的数字有部分会自动加粗的解决方法
- Java连MySQL性能调优(batch insert和连续left join筛选)
- java简单通讯录的实现02person类_Java中Math类的简单介绍
- 飘逸的python - 鲜为人知的参数
- JavaScript事件与jQuery方法
- 大数据时代的“找油利器”
- boost::math模块非有限信号 NaN 环回测试
- 12.04 深圳站 | Serverless Developer Meetup 开放报名
- Linux——curl(转)
- 阿里巴巴为什么禁止使用Apache Beanutils进行属性复制?
- double+float
- siesta在Linux运行,siesta-3.0-b
- Linux之JDK安装
- Kotlin学习笔记21 协程part1 基本概念
- 如何制作自己的网课网站 需网课查课插件
- 计算机软件系统测试报告模板,测试报告模板
- HTML5+CSS3+Bootstrap开发静态页面嵌入android webview中
- unrealengine(UE5)虚幻引擎下载安装
- 制作歌词录入系统php,如何制作歌词字幕 制作字幕的软件
热门文章
- android 剪贴板增强工具,剪切板增强工具(ClipboardFusion Pro)
- 恋与抽卡模拟器网页_恋与制作人抽卡模拟器-恋与制作人抽卡模拟器软件下载v1.14.1202-k73游戏之家...
- 计算机表格填充,excel自动填充怎样快速填充一列?/excle填充工具
- nachos交叉编译器java_ubuntu - 编译Nachos源代码时出错“gnu / stubs-32.h:没有这样的文件或目录”...
- JavaScript中的this指向以及bind()函数
- 按键精灵定时后台点击
- 为什么NIO比BIO效率高
- Flutter热重载原理探索调试
- linux中分号转义字符,Linux职场技术篇-Linux shell中元字符、转义符、通配符的使用方法...
- brandon公司_开发人员聚焦:布兰登·里德(Brandon Reid)