集合和数组的区别
使用集合的优点
1、数组能存放基本数据类型和对象,而集合类存放的都是对象,集合类不能存放基本数据类型(你存储的是简单的int,它会自动装箱成Integer)。数组和集合存放的对象皆为对象的引用地址
数组容易固定无法动态改变,集合类容量动态改变。
2、数组无法判断其中实际存有多少元素,length只告诉了数组的容量,而集合的size()可以确切知道元素的个数
3、集合有多种实现方式和不同适用场合,不像数组仅采用顺序表方式
4、集合以类的形式存在,具有封装、继承、多态等类的特性,通过简单的方法和属性即可实现各种复杂操作,大大提高了软件的开发效率
集合体系概述
Collection集合
数据结构:只是定义了他们的存储方式,具体的值List以对象为值存,Map以KV为值存。
List集合的特点就是:有序(存储顺序和取出顺序一致),可重复
• ArrayList:数组实现,查询快,增删慢,轻量级;(线程不安全)
• LinkedList:双向链表实现,增删快,查询慢 (线程不安全)经常用在增删操作较多而查询操作很少的情况下:
• Vector:数组实现,重量级 (线程安全、使用少)
现在要想在多线程的环境下,使用一个线程安全的集合怎么实现?
1、CopyOnWriteArrayList
特性:CopyOnWriteArrayList是线程安全的List实现,通过对底层数组进行复制来实现线程安全。读操作不会阻塞,而写操作会创建一个新的数组进行修改,确保写操作不影响读操作。
用法:CopyOnWriteArrayList适用于读操作频繁、写操作较少的场景,例如缓存或只读数据。它提供了线程安全的遍历,但写操作的开销较高。
public static void testSynchronizedList(){
List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
long time1 = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
list.add(i);
}
long time2 = System.currentTimeMillis();
System.out.println("synchronizedList: "+(time2-time1));
}
public static void testCopyOnWriteArrayList(){
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
long time1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long time2 = System.currentTimeMillis();
System.out.println("copyOnWriteArrayList: "+(time2-time1));
}
Set集合的特点是:元素不可重复
• HashSet:底层是操作HashMap,数据结构是哈希表 (数组+链表 / 红黑树)
• TreeSet(Set的子接口SortedSet接口的实现类): 底层是操作TreeMap,数据结构是红黑树(是一个自平衡的二叉树),保证元素的排序方式
• LinkedHashSet:底层数据结构由哈希表 (数组+链表 / 红黑树),并加了一个链表组成。
Map集合
• HashMap:键值对,key不能重复,但是value可以重复;key的实现就是HashSet;value对应着放;允许null的键或值;
• Hashtable:线程安全的,不允许null的键或值; Properties::key和value都是String类型,用来读配置文件;
• TreeMap:对key排好序的Map; key 就是TreeSet, value对应每个key;key要实现Comparable接口或TreeMap有自己的构造器;
• LinkedHashMap:此实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。存储的数据是有序的。
Collection——>List——>ArrayList
1、ArrayList的add方法:
1)先检查是否需要扩容
2)得到最小容量
3)足够:直接添加,不足够:扩容
4)如果最小容量比数组长度要大,就调用grow()扩容
5)grow()中调用Arrays.copyOf()【向新数组重新拷贝,并扩容为1.5倍】
6)第一次扩容后,如果容量还是小于最小容量(minCapacity),就将容量扩充为最小容量
2、ArrayList必知细节:
• ArrayList是基于动态数组实现的,在增删时候,需要数组的拷贝复制。
• ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
• 删除元素时不会减少容量,若希望减少容量则调用trimToSize()
• 它不是线程安全的。它能存放null值。
3、ArrayList扩容机制?
1)调用ArrayList.ensureCapacity()下面的calculateCapacity(elementData, minCapacity)方法:
2)首先得到当前元素数据(elementData)和属性的长度(oldCapacity)。
3)然后通过判断属性长度(oldCapacity)和最小长度(minCapacity)参数谁大来决定是否需要扩容, 如果最小长度(minCapacity)大于属性长度(oldCapacity),那么我们就对当前的List对象进行扩容。 扩容为原来的1.5倍。然后使用copyOf()数组拷贝的方法,把以前存放的数据转移到新的数组对象中
4)如果最小长度(minCapacity)不大于属性长度(oldCapacity)那么就不进行扩容。
从此方法中我们可以清晰的看出其实ArrayList扩容的本质就是计算出新的扩容数组的size后实例化, 并将原有数组内容复制到新数组中去。
4、 Arraylist 与 LinkedList 区别?
1)底层数据结构不同:
ArrayList底层是Object动态数组,所以ArrayList具有数组的查询速度快的优点以及增删速度慢的缺点(插入和删除元素会移动后面全部的元素向前或者向后一位)。
而在LinkedList的底层是一种双向链表(JDK1.6 之前为循环链表,JDK1.7取消了循环)。在此链表上每一个数据节点都由三部分组成:前指针(指向前面的节点的位置),数据,后指针(指向后面的节点的位置)。
2)因为底层数据结构不同,所以内容空间占用也是不同的:
ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
3)因为底层数据结构不同,插入和删除是否受元素位置的影响也是不同的:
ArrayList插入和删除元素会移动后面全部的元素向前或者向后一位,增加则是列表的末尾。LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响,如果是要在指定位置 i 插入和删除元素的话需要先移动到指定位置再插入。
相同点:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
5、Arraylist 与 Vector区别?
1)Vector底层也是数组,与ArrayList最大的区别就是:同步(线程安全)
2)Vector是同步的,在要求非同步的情况下,我们一般都是使用ArrayList来替代Vector的了
【如果想要ArrayList实现同步,可以使用Collections的方法:
List list = Collections.synchronizedList(new ArrayList(…));就可以实现同步了】
3) ArrayList在底层数组不够用时在原来的基础上扩展0.5倍成为原来的1.5倍,Vector是扩展1倍成为原来的2倍。
Collection——>List——>LinkedList
1、LinkedList必知细节:
1)LinkedList底层是双向链表
2)LinkedList还实现了Deque接口,因此,除了链表我们还可以操作LinkedList像操作队列和栈一样。
3)LinkedList变量就这么几个,因为我们操作单向链表的时候也发现了:有了头结点,其他的数据我们都可以获取得到了。(双向链表也同理)
Collection——>List小结
ArrayList:
• 底层实现是数组
• ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
• 在增删时候,需要数组的拷贝复制(navite 方法由C/C++实现)
LinkedList:
• 底层实现是双向链表[双向链表方便实现往前遍历]
Vector:
• 底层是数组,现在已少用,被ArrayList替代,原因有两个:
– Vector所有方法都是同步,有性能损失。
– Vector初始length是10 超过length时 以100%比率增长,相比于ArrayList更多消耗内存。
总的来说:查询多用ArrayList,增删多用LinkedList。
ArrayList增删慢不是绝对的(在数量大的情况下,已测试):
• 如果增加元素一直是使用add()(增加到末尾)的话,那是ArrayList要快
• 一直删除末尾的元素也是ArrayList要快【不用复制移动位置】
• 至于如果删除的是中间的位置的话,还是ArrayList要快!
但一般来说:增删多还是用LinkedList,因为上面的情况是极端的~
Collection——>Set——>HashSet
底层数据结构是HashMap,即哈希表(数组+链表/红黑树),值是一个一个对象。而不是HashMap的KV值
1、HashSet必知细节
• 实现Set接口
• 不保证迭代顺序
• 允许元素为null
• 底层实际上是一个HashMap实例
• 非同步
• 初始容量非常影响迭代性能
• HashSet是线程非安全的
所以可以直接总结出:HashSet实际上就是封装了HashMap,操作HashSet元素实际上就是操作HashMap。这也是面向对象的一种体现,重用性贼高!
2、HashSet的equals和HashCode(HashSet如何判断元素重复)
前面说过,Set集合是不允许重复元素的,否则将会引发各种奇怪的问题。
HashSet需要同时通过equals和HashCode来判断两个元素是否相等,具体规则是,先判断HashCode是否相等,再判断equals( )是否相等。相等则这两个元素相等(即重复)。
试想如果重写了equals方法但不重写hashCode方法,即相同equals结果的两个对象将会被HashSet当作两个元素保存起来(第一步就是调用hashCode方法,结果不同,所以当成了两个元素),这与我们设计HashSet的初衷不符(元素不重复,但他俩实际上是重复的相同元素)。equals判断相等的元素,hashCode必定相等,所以重写equals,hashCode也一定要重写。重写前后hashCode返回的结果相等(即保证保存在同一个位置)。
另外如果两个元素HashCode相等但equals结果不为true,HashSet会将这两个元素保存在同一个位置,并将超过一个的元素以链表方式保存,这将影响HashSet的效率。
如果重写了equals方法但没有重写hashCode方法,则HashSet可能无法正常工作
3、HashSet如何达到不存在重复元素的目的(上一个的问题总结)
“键”就是我们要存入的对象,“值”则是一个常量。这样可以确保,我们所需要的存储的信息是“键”。而“键”在Map中是不能重复的,这就保证了我们存入Set中的所有的元素都不重复。
HashSet如何过滤重复元素()?
调用元素HashCode获得哈希码–》判断哈希码是否相等,不相等则录入 —》相等则判断equals()后是否相等,不相等在进行 hashcode录入,相等不录入
4、HashMap和HashSet区别
Collection——>Set——>TreeSet
存储和读数据方式:
1、TreeSet必知细节
• 实现NavigableSet接口
• 可以实现排序功能
• 底层实际上是一个TreeMap实例
• 非同步
2、TreeSet为什么可以排序
TreeSet实现了SortedSet接口,顾名思义这是一种排序的Set集合,查看jdk源码发现底层是用TreeMap实现的,本质上是一个红黑树原理。 正因为它是排序了的,所以相对HashSet来说,TreeSet提供了一些额外的按排序位置访问元素的方法,例如first(), last(), lower(), higher(), subSet(), headSet(), tailSet().
TreeSet的排序分两种类型,一种是自然排序,另一种是定制排序。
3、自然排序(在元素中写排序规则)
TreeSet 会调用compareTo方法比较元素大小,然后按升序排序。所以自然排序中的元素对象,都必须实现了Comparable接口,否则会抛出异常。对于TreeSet判断元素是否重复的标准,也是调用元素从Comparable接口继承而来额compareTo方法,如果返回0则是重复元素(两个元素相等)。Java的常见类都已经实现了Comparable接口。
因为TreeSet会调用元素的compareTo方法,这就要求所有元素的类型都相同,否则也会发生异常。也就是说,TreeSet只允许存入同一类的元素。
4、定制排序(在集合中写排序规则)
TreeSet还有一种排序就是定制排序,定制排序时候,需要关联一个Comparator对象,由Comparator提供排序逻辑。
5、其他知识点
TreeSet是依靠TreeMap来实现的。
TreeSet是一个有序集合,TreeSet中元素将按照升序排列,缺省是按照自然顺序进行排列,意味着TreeSet中元素要实现Comparable接口。
我们可以在构造TreeSet对象时,传递实现了Comparator接口的比较器对象。
6、Comparable和Comparator
Comparable 接口提供自然排序顺序。
对于那些没有自然顺序的类、或者当您想要一个不同于自然顺序的顺序时,您可以实现Comparator 接口来定义您自己的排序函数。可以将Comparator传递给Collections.sort或Arrays.sort。
Comparator接口
当一个类并未实现Comparable,或者不喜欢缺省的Comaparable行为。可以实现Comparator接口
直接实现Comparator的compare接口完成自定义比较类。
例:Arrays.sort(results, new Comparator() 数组排序 RepDataQueryExecutor
例:Collections.sort(lst,new Comparator()
Collection——>Set——>LinkedHashSet
1、LinkedHashSet必知细节
• 迭代是有序的
• 允许为null
• 底层实际上是一个HashMap+双向链表实例(其实就是LinkedHashMap)…
• 非同步
• 性能比HashSet差一丢丢,因为要维护一个双向链表
• 初始容量与迭代无关,LinkedHashSet迭代的是双向链表
LinkedHashSet是HashSet的一个子类,LinkedHashSet也根据HashCode的值来决定元素的存储位置,但同时它还用一个链表来维护元素的插入顺序,插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。
LinkedHashSet本质上也是从LinkedHashMap而来,LinkedHashSet的所有方法都继承自HashSet, 而它能维持元素的插入顺序的性质则继承自LinkedHashMap
Collection——>Set小结
可以很明显地看到,Set集合的底层就是Map。
下面总结一下Set集合常用的三个子类吧:set集合的特点是元素不可重复
1)HashSet:无序,允许为null,底层是HashMap,即哈希表 (数组+链表 / 红黑树),非线程同步
2)TreeSet:有序,不允许为null,底层是TreeMap(红黑树(是一个自平衡的二叉树)),非线程同步
3)LinkedHashSet:迭代有序,允许为null,底层是HashMap+双向链表(哈希表 (数组+链表 / 红黑树),并加了一个链表组成),非线程同步
从结论而言我们就可以根据自己的实际情况来使用了。
几种Set的比较
1)HashSet外部无序地遍历成员。
成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。
2)TreeSet外部有序地遍历成员;
附加实现了SortedSet, 支持子集等要求顺序的操作
成员要求实现Comparable接口,或者使用Comparator构造TreeSet。成员一般为同一类型。
3)LinkedHashSet外部按成员的插入顺序遍历,成员与HashSet成员类似
HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
HashSet的元素存放顺序和我们添加进去时候的顺序没有任何关系,而LinkedHashSet 则保持元素的添加顺序。TreeSet则是对我们的Set中的元素进行排序存放。
一般来说,当您要从集合中以有序的方式抽取元素时,TreeSet实现就会有用处。为了能顺利进行,添加到 TreeSet 的元素必须是可排序的。 而您同样需要对添加到TreeSet中的类对象实现 Comparable 接口的支持。一般说来,先把元素添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快。
各种Set集合性能分析
HashSet和TreeSet是Set集合中用得最多的I集合。HashSet总是比TreeSet集合性能好,因为HashSet不需要额维护元素的顺序。
LinkedHashSet需要用额外的链表维护元素的插入顺序,因此在插入时性能比HashSet低,但在迭代访问(遍历)时性能更高。因为插入的时候即要计算hashCode又要维护链表,而遍历的时候只需要按链表来访问元素。
EnumSet元素是所有Set元素中性能最好的,但是它只能保存Enum类型的元素
Map集合与Collection的区别
- Map集合存储元素是成对出现的,Map的键是唯一的,值是可以复制的
- Collection集合存储元素是单独出现的,Collection的儿子Set是唯一的,List是可重复的
散列表
1、介绍
无论是Set还是Map,我们会发现都会有对应的–>HashSet,HashMap
首先我们也先得回顾一下数组和链表:
• 链表和数组都可以按照人们的意愿来排列元素的次序,他们可以说是有序的(存储的顺序和取出的顺序是一致的)
• 但同时,这会带来缺点:想要获取某个元素,就要访问所有的元素,直到找到为止。
• 这会让我们消耗很多的时间在里边,遍历访问元素~
而还有另外的一些存储结构:不在意元素的顺序,能够快速的查找元素的数据,其中就有一种非常常见的:散列表
2、工作原理
散列表为每个对象计算出一个整数,称为散列码。根据这些计算出来的整数(散列码)保存在对应的位置上!在Java中,散列表用的是链表数组实现的,每个列表称之为桶。
一个桶上可能会遇到被占用的情况(hashCode散列码相同,就存储在同一个位置上),这种情况是无法避免的,这种现象称之为:散列冲突
- 此时需要用该对象与桶上的对象进行比较,看看该对象是否存在桶子上了~如果存在,就不添加了,如果不存在则添加到桶子上
- 当然了,如果hashcode函数设计得足够好,桶的数目也足够,这种比较是很少的~
- 在JDK1.8中,桶满时会从链表变成平衡二叉树(红黑树,是弱平衡二叉树)
- 如果散列表太满,是需要对散列表再散列,创建一个桶数更多的散列表,并将原有的元素插入到新表中,丢弃原来的表~
- 装填因子(load factor)决定了何时对散列表再散列~
- 装填因子默认为0.75,如果表中超过了75%的位置已经填入了元素,那么这个表就会用双倍的桶数自动进行再散列
Collection——>Map——>HashMap
1、HashMap必知细节
Hash的底层是散列表,而在Java中散列表的实现是通过数组+链表的:数组+链表–>散列表
我们可以简单总结出HashMap:
1)键不允许重复,值允许重复
2)HashMap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
3)无序
4)HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;
5)HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力。(非同步)
6)底层由散列表(哈希表)实现
7)初始容量和装载因子对HashMap影响挺大的,设置小了不好,设置大了也不好
2、HashMap核心方法——put
1)计算了哈希值
2)并对哈希值执行了异或运算
直接将key作为哈希值不就好了吗,做异或运算是干嘛用的?
我们是根据key的哈希值来保存在散列表中的,我们表默认的初始容量是16,要放到散列表中,就是0-15的位置上。也就是tab[i = (n - 1) & hash]。可以发现的是:在做&运算的时候,仅仅是后4位有效~那如果我们key的哈希值高位变化很大,低位变化很小。直接拿过去做&运算,这就会导致计算出来的Hash值相同的很多。
而设计者将key的哈希值的高位也做了运算(与高16位做异或运算,使得在做&运算时,此时的低位实际上是高位与低位的结合),这就增加了随机性,减少了碰撞冲突的可能性!
3、HashMap的实现原理
散列:
Hash哈希算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系。散列表又称为哈希表。散列表算法的基本思想是:以结点的关键字为自变量,通过一定的函数关系(散列函数)计算出对应的函数值,以这个值作为该结点存储在散列表中地址。
当散列表中的元素存放太满,就必须进行再散列,将产生一个新的散列表,所有元素存放到新的散列表中,原先的散列表将被删除。在Java语言中,通过负载因子(load factor)来决定何时对散列表进行再散列。例如:如果负载因子0.75,当散列表中已经有75%位置已经放满,那么将进行再散列。
负载因子越高(越接近1.0),内存的使用效率越高,元素的寻找时间越长。负载因子越低(越接近0.0),元素的寻找时间越短,内存浪费越多。
何时需重写equals?
当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念);
Object类仅仅提供了一个对引用的比较,如果两个引用不是同一个那就返回false,这是无法满足大多数对象比较的需要的,所以要覆盖;
使用 == 操作符检查实参是否为指向对象的引用” 用instanceof操作符检查实参是否为正确的类型,把实参转换到正确的类型;对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹配。
对于既不是float也不是double类型的基本类型的域,可以使用 == 操作符进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法,对于float和double类型的域,先转换成int或long类型的值,然后使用==操作符比较;
当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的? 如果答案是否定的,那么请找到 这些特性未能满足的原因,再修改equals方法的代码
equals()和hashCode()同时覆写,尤其强调当一个对象被当作键值(或索引)来使用的时候要重写这两个方法;
覆写equals后,两个不同实例可能在逻辑上相等,但是根据Object.hashCode方法却产生不同的散列码,违反“相等的对象必须具有相等的散列码”。
导致,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另 一个作为键值去查找他们的时候,则根本找不到
不同类型的hashCode取值
如果该域是布尔型的,计算(f?0:1)
如果是char,short,byte或int,计算(int)f
如果是long类型,计算(int)(f^(f>>>32))
如果是float类型,计算Float.floatToIntBits(f)
如果是double类型,计算Dobule.doubleToLongBits(f)
如果该域是一个对象引用,递归调用hashCode
如果该域是一个数组,则把每个元素当做单独的域来处理,对每个重要的元素计算一个散列码
4、HashMap和HashTable的区别
从存储结构和实现来讲基本上都是相同的。它和HashMap的最大的不同是它是线程安全的,另外它不允许key和value为null。Hashtable是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
不同点 | HashMap | HashTable |
数据结构 | 数组+链表+红黑树 | 数组+链表 |
基础的类 | 基础AbstractMap | 基础Dictionary |
是否线程安全 | 否 | 是 |
性能高低 | 高 | 低 |
默认初始化容量 | 16 | 11 |
扩容的方式 | 原始容量×2 | 原始容量×2+1 |
遍历方式 | Iterator(迭代器) | Iterator(迭代器)和Enumeration()枚举器 |
Iterator(迭代器)遍历数组顺序 | 索引从小到大 | 索引从大到小 |
5、hashMap总结(重点)
在JDK8中HashMap的底层是:数组+链表(散列表)+红黑树
在散列表中有装载因子这么一个属性,当装载因子*初始容量小于散列表元素时,该散列表会再散列,扩容2倍!
装载因子的默认值是0.75,无论是初始大了还是初始小了对我们HashMap的性能都不好
• 装载因子初始值大了,可以减少散列表再散列(扩容的次数),但同时会导致散列冲突的可能性变大(散列冲突也是耗性能的一个操作,要得操作链表(红黑树)!
• 装载因子初始值小了,可以减小散列冲突的可能性,但同时扩容的次数可能就会变多!
初始容量的默认值是16,它也一样,无论初始大了还是小了,对我们的HashMap都是有影响的:
• 初始容量过大,那么遍历时我们的速度就会受影响~
• 初始容量过小,散列表再散列(扩容的次数)可能就变得多,扩容也是一件非常耗费性能的一件事~
从源码上我们可以发现:HashMap并不是直接拿key的哈希值来用的,它会将key的哈希值的高16位进行异或操作,使得我们将元素放入哈希表的时候增加了一定的随机性。
还要值得注意的是:并不是桶子上有8位元素的时候它就能变成红黑树,它得同时满足我们的散列表容量大于64才行的~
Collection——>Map——>LinkedHashMap
1、LindkedHashMap必备知识点
LinkedHashMap: 此实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。存储的数据是有序的。
调用的是HashMap构造方法
LinkedHashMap没有put方法,原来LinkedHashMap和HashMap的put方法是一样的!LinkedHashMap继承着HashMap,LinkedHashMap没有重写HashMap的put方法
所以,LinkedHashMap的put方法和HashMap是一样的。
当然了,在创建节点的时候,调用的是LinkedHashMap重写的方法
根据源码注释,我们可以分析出:
• 底层是散列表和双向链表
• 允许为null,不同步
• 插入的顺序是有序的(底层链表致使有序)
• 装载因子和初始容量对LinkedHashMap影响是很大的~
• 初始容量对遍历没有影响