当前位置: 首页 > 科技观察

Java集合框架系统概述

时间:2023-03-12 00:49:12 科技观察

本文转载自微信公众号《飞天小牛》,作者飞天小牛。转载本文请联系飞天小牛公众号。采集这块知识的重要性不用多说,再加上多线程和妥妥的面试一定要请教霸王楼主,非常有必要深入了解采集框架的整体结构和各集合类的实现原理。由于不同的集合在实现中使用了不同的数据结构,因此每个集合在性能、底层实现和使用上都存在一定的差异。因此,关于集合的知识点很多,但好在其整体的学习框架还是比较清晰的。本文只是对集合框架的知识体系进行大体介绍,帮助大家理清思路。对重点集合类进行详细分析后,将分几篇文章分别介绍。全文脉络思维导图如下:1.为什么要用集合我们在学习一个东西的时候,最好理解为什么要用,不是为了用而用,而是知道为什么用.集合,因此得名,是用来存储元素的,数组也有这个功能,所以既然集合出现了,那一定是因为“数组的使用有一定的缺陷”。正如上一篇文章中简要提到的,一旦定义了一个数组,它的存储大小就不能再改变了。例如,假设这个班有一个班级,有50个学生,那么我们定义一个数组,可以存储50个学生的信息:1)如果这个班级有10个转学生,由于长度是固定的,那么显然这个阵法的存储容量不能支持60个学生;再比如,如果这个班有20个学生退学,那么数组实际上只存储了30个学生,造成内存空间的浪费。总结一下,“由于数组的长度一旦定义就不能改变,所以数组不能动态适应元素个数的变化”。2)数组有一个length属性,通过这个属性可以知道数组的存储容量,即数组的长度,但是数组实际存储的元素个数不能通过一个属性直接得到。3)由于“数组在内存中采用了连续分配空间的存储方式”,我们可以根据下标快速获取对应的学生信息。比如我们把一个学生的学号111存放在数组的下标2处,显然可以直接通过下标2获取到学号111。但是“如果我们要找的下标呢?学号111?”?数组不是原生可用的,这需要使用各种搜索算法。4)另外,如果我们要存储学生姓名和家庭住址的一一对应关系,数组显然是不可能的。5)如果我们想在用来存放学生信息的数组中存放一些教师信息,数组不能满足这个要求,它只能存放相同类型的元素。为了解决这些数组的使用痛点,集合框架的应用诞生了。简单来说,集合的主要作用就是两点:存储不定数量的数据(集合的长度可以动态改变)存储具有映射关系的数据存储不同类型的数据但是需要注意的是“集合只能存储引用类型(对象),如果存储int类型数据(基本类型),它会自动装箱成Integer类型。而数组既可以存储基本数据类型,也可以存储引用类型。”2.CollectionsFramework快速浏览与现代数据结构库中常见的一样,Javacollections类也将接口和实现分离,两者都位于java.util包中。根据其存储结构,collections可以分为两类:单列集合Collection双列集合MapCollection接口“单列集合”java.util.Collection:元素是孤立存在的,元素一个一个存储在集合中。看继承体系Collection接口示意图:Collection接口定义了一些单列集合的常用方法:publicbooleanadd(Ee);//将给定的对象添加到当前集合中publicvoidclear();//清除集合中的所有元素publicbooleanremove(Ee);//删除当前集合中的给定对象publicbooleancontains(Ee);//判断当前集合是否包含给定对象publicbooleanisEmpty();//判断当前集合是否为空publicintsize();//返回集合中的元素个数publicObject[]toArray();//将集合中的元素存储到一个数组中Collection有两个重要的子接口List和Set,分别代表有序集合和无序集合:1)列表的特点是“有序且可重复的元素”。这里所谓有序的意思是:“元素入库的顺序和取出的顺序是一样的”。比如存储元素的顺序是11、22、33,那么我们从List中取出这些元素的时候也是按照11、22、33的顺序。List接口常用的实现类有:“ArrayList”:底层数据结构为数组,非线程安全“LinkedList”:底层数据结构为链表,非线程安全另外在Collection接口的所有方法的基础上,List接口还增加了一些根据元素索引操作集合的特有方法:publicvoidadd(intindex,Eelement);//将指定元素添加到集合中的指定位置publicEget(intindex);//返回集合中指定位置的元素publicEremove(intindex);//移除列表中指定位置的元素,并返回移除的元素publicEset(intindex,Eelement);//用指定元素替换set指定位置的元素2)Set接口的方法签名与Collection接口的方法签名完全一样,只是在方法的描述中有更严格的定义。最大的特点是“拒绝添加重复元素,不能通过整型索引访问”,“元素乱序”。所谓无序,就是元素入库的顺序和取出的顺序不一致。其常用的实现类有:“HashSet”:底层基于HashMap实现,HashMap用于存储元素。《LinkedHashSet》:LinkedHashSet是HashSet的子类,其底层是通过LinkedHashMap实现的。至于为什么要定义一个方法签名完全一样的接口,我的理解是为了让集合框架的结构更加清晰,从以下两点来区分单列集合:可以添加重复的元素(List)和不能添加重复元素(Set)可以通过整型索引(List)访问,不能通过整型索引(Set)访问,这样我们在声明单列集合时,可以更准确的继承对应的接口。Map接口“双列集合”java.util.Map:元素成对存在。每个元素由两部分组成:键(key)和值(value)。通过key可以找到对应的value。显然,这个双列集合解决了数组无法存储映射关系的痛点。另外需要注意的是“Map不能包含重复的键,值可以重复;并且每个键只能对应一个值”。看Map接口的继承体系图:Map接口定义了一些双列集合的常用方法:publicVput(Kkey,Vvalue);//将指定的key和指定的value添加到Map集合中。publicVremove(Objectkey);//从Map集合中删除指定key对应的键值对元素,并返回删除元素的值。publicVget(Objectkey);//根据指定的key,获取Map集合中对应的value。booleancontainsKey(Objectkey);//判断集合中是否包含指定的key。publicSetkeySet();//获取Map集合中的所有key,存入Set集合中。Map有两个重要的实现类,HashMap和LinkedHashMap:①《HashMap》:可以说对HashMap不熟悉,去面试。这里简单介绍一下它的底层结构,后面会详细解释。在JDK1.8之前,HashMap的底层是通过数组加链表实现的。数组是HashMap的主体,链表的存在主要是为了解决哈希冲突(“拉链法”解决冲突)。JDK1.8之后,在解决hash冲突方面有了较大的变化。当链表长度大于阈值(默认为8)时,将链表转为红黑树,以减少查找时间(注:链表转为红黑树前树,它会判断如果当前数组的长度小于64,那么它会选择先扩充数组而不是转成红黑树)。②“LinkedHashMap”:HashMap的子类,可以保证元素按相同的顺序访问(存储的顺序就是取出的顺序,不会因为元素的大小而改变)钥匙)。LinkedHashMap继承自HashMap,所以它的底层还是基于zipperhash结构,由数组和链表或红黑树组成。另外,LinkedHashMap在上述结构的基础上增加了一个双向链表,使得上述结构可以维护键值对的插入顺序。同时,通过对链表进行相应的操作,实现访问顺序相关的逻辑。OK,我们已经知道Map中存储的对象有两种,一种叫做键(key),另一种叫做值(value)。在地图中创建一个“条目”(项目)。Entry将键值对的对应关系封装成一个对象,即键值对对象。Map还提供了获取所有Entry对象的方法:publicSet>entrySet();//获取Map中所有Entry对象的集合。同样的,Map也提供了获取每个Entry对象中对应的key和对应的value的方法,这样我们在遍历Map集合时,就可以从每个键值对(Entry)对象中获取对应的key和对应的value了:publicKgetKey();//获取一个Entry对象中的key。publicVgetValue();//获取一个Entry对象中的值。下面我们根据上面所学的内容来看一下Map的两种遍历方式:1)“遍历方式一:根据key查找值”获取Map中所有的key。由于key是唯一的,返回一个Set集合来存放所有的key。方法提示:keyset()遍历key的Set集合,获取每一个key。根据key,得到key对应的value。方法提示:get(Kkey)publicstaticvoidmain(String[]args){//创建Map集合对象HashMapma??p=newHashMap();//向集合map中添加元素。put(1,"小五");map.put(2,"小红");map.put(3,"小张");//获取所有key得到key集合Setkeys=map.keySet();//遍历key集合得到每个keyfor(Integerkey:keys){//得到对应的valueStringvalue=map.get(key);System.out.println(key+":"+value);}}这里不知道大家有没有注意一个细节,keySet方法的返回结果是Set。由于Map没有实现Iterable接口,所以不能直接用迭代器遍历,也不能foreach循环遍历,但是转成Set后就可以使用了。至于什么是迭代器,请继续往下看。2)《遍历方法二:键值对法》获取Map集合中的所有键值对(Entry)对象,以Set集合的形式返回。方法提示:entrySet()。遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。获取每个Entry对象中的键和值。方法提示:getkey(),getValue()//获取所有入口对象Set>entrySet=map.entrySet();//遍历获取每个入口对象for(Entryentry:entrySet){Integerkey=entry.getKey();Stringvalue=entry.getValue();System.out.println(key+":"+value);}3.IteratorIterator什么是Iterator在上一章数组中讲过foreach循环:for(variable:collection){//todo}collection这个表达式必须是一个数组或者一个实现了Iterable接口的类对象。可以看到Collection接口继承了Itreable接口,所以所有实现了Collection接口的集合都可以使用foreach循环。我们点进Iterable看一下:它有一个iterator方法,返回类型是Iterator,这是什么东西,我们点进去看看:有3个接口,但是我们不能再往下看了,我们去Collection的实现在类中查看是否实现了Itreator接口,随便开一个,比如ArrayList:从源码可以看出Iterator接口是以“内部类”的形式实现的在数组列表中。并且,Iterator实际上是在遍历集合。所以综上所述:我们可以通过Iterator接口来遍历Collection的元素。该接口的具体实现是作为内部类在具体的子类中实现的。这里有个问题,“为什么迭代器不封装成类,而是做成接口”?假设迭代器是一个类,这样我们就可以创建一个这个类的对象,调用这个类的方法来实现对Collection的遍历。但实际上,Collection接口有很多不同的实现类。正如我们在文章开头所说的,这些类的底层数据结构大多是不同的。所以它们各自的存储方式和遍历方式也是不同的,所以我们不能用一个类来指定死遍历的方式。我们把遍历需要的通用方法抽取出来,封装到接口中,让Collection的子类根据自己的特点来实现。看完上面的分析,我们来验证一下,看看LinkedList实现的Itreator接口和ArrayList实现的Itreator接口有没有区别:很明显,这两者虽然都是Collection的实现类,但是具体实现Itreator接口的内部过程是不一样的。迭代器基本都用OK了。我们已经知道Iterator是用来遍历Collection集合的,那么它具体是怎么遍历的呢?答:“迭代遍历”!解释一下迭代的概念:在取元素之前,先判断集合中是否有元素,如果有就取出这个元素,然后继续判断,如果还有就继续取出。直到取出集合中的所有元素。这种取出的方式叫做迭代。因此,Iterator对象也称为“迭代器”。也就是说,如果要遍历Collection,就需要获取集合对应的迭代器。如何获得?其实上面已经出现了。Collection实现的Iterable中有这样一个方法:iterator下面介绍一下Iterator接口中的常用方法:publicEnext();//返回迭代的下一个元素。publicbooleanhasNext();//如果还有元素可以迭代,则返回true例如:publicstaticvoidmain(String[]args){Collectioncoll=newArrayList();//向集合中添加元素coll.add("A");coll.add("B");coll.add("C");//获取coll的迭代器Iteratorit=coll.iterator();while(it.hasNext()){//判断是否有迭代元素Strings=it.next();//获取迭代元素System.out.println(s);}}当然同样的循环操作可以用更简单的表达foreach循环:Collectioncoll=newArrayList();...for(Stringelement:coll){System.out.println(element);}参考?Java3y-CollectionCollectionoverview:https://觉进。cn/post/6844903587441541127#heading-1