java基础(集合框架)

1.Java 集合框架

1.1 什么是集合框架

通常,程序总是根据运行时才知道的某些条件去创建新对象。
在此之前,不会知道你所需要对象的数量,甚至不知道确切的类型。
为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。
所以,就不能依靠创建命名的引用来持有每一个对象,
因为你不知道实际上会需要多少这样的引用 ——Thinking in Java

Java 提供的集合类是我们可以通过容器类型的变量保存对象,并达到动态扩容,查询、增删改查的操作。

1.1.1 数组和集合的区别

  • 数组是长度不可变的,而集合是长度可变的
  • 数组存储的是同一类型的元素,而集合可以存储不同类型的元素
  • 数组可以存储基本数据类型的元素,也可以存储引用类型的元素

1.2 集合框架的体系结构

常见的集合框架就是下图所示,还有一些特殊的没有列举出来,例如ConcurrentHashMap 等等
在这里插入图片描述
Collection 和 Map 的体系结构

Collection 的体系结构

在这里插入图片描述
Map 的体系结构
在这里插入图片描述

1.3 Java集合类框架的接口

首先集合类操作的对象,我们称为元素,而集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序。有的集合类允许重复的键,有的则不允许。

  • Collection: 代表一组对象,每一个对象都是他的子元素
  • List: 有顺序的Collection,并且可以包含重复的元素
  • Set: 不保证有序,同时不包含重复元素的Collection(唯一)
  • Map: 可以把 键(key) 映射到 值(value) 的对象,键不能重复(键值对)

1.4Java常见集合的数据结构及其特点

1.4.1 List

  • ArrayList : Object数组(查询快,增删慢,线程不安全,效率高)
  • Vector : Object 数组(查询快,增删慢,线程安全,效率低)
  • LinkList : 双向链表 (查询慢,增删块,线程不安全,效率高)

1.4.2 Map

  • HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 存储元素的主体,链表则是主要为了解决哈希冲突而存在的,即 “拉链法” 解决冲突。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(哈希表对键进行散列,Map结构即映射表存放键值对)
  • LinkHashMap : LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得键值对的插入顺序以及访问顺序等逻辑可以得以实现。
  • HashTable : 数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap : 红黑树(平衡二叉排序)

1.4.3 Set

  • HashSet: 基于 HashMap 实现的,底层采用 HashMap 来保存元素(不保证有序,唯一)
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet: 红黑树,自平衡的排序二叉树(可实现自然排序,例如 a-z)

1.5 Collection和Collections的区别

  • Collection是集合的上级接口,继承它的有 Set 和 List 接口
  • Collections是集合的工具类,提供了一系列的静态方法对集合的搜索、查找、同步等操作

1.6 迭代器

Iterator 提供了遍历即操作集合元素的接口。而Collection 接口实现了Iterable 接口,也就是说,每个集合都通过实现Iterable 接口中的 iterator()方法返回Iterator 接口实例,然后对集合元素进行迭代操作。

有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过 Iterator接口中的 remove() 方法进行删除,同理想要增加元素,就可以使用 ListIterator 的 add 方法 (ListIterator 拥有 add、set、remove 方法,Iterator 拥有 remove 方法)

1.7 Iterator和ListIterator的区别

  • Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历List。
  • Iterator只能 remove() 元素,而ListIterator可以 add()、set()、remove()
  • Iterator只能使用 next() 顺序的向后遍历,ListIterator则向前 previous()和向后 next() 遍历都可以
  • ListIterator可以使用 nextIndex() 和 previousIndex() 取得当前游标位置的前后index位置,Iterator没有此功能

2 List 接口

2.1 ArrayList 分别与 Vector、LinkedList 的异同点

2.1.1 ArrayList 与 Vector

ArrayList 是现在 List 的一种主要实现类,而 Vector 已经是过时的 Java 遗留容器

  • 同:两者都是使用 Object 数组方式存储数据,均可以实现扩容且允许直接按序号查询(索引)元素,但是插入元素要涉及数组元素移动等内存操作,所以两者查询数据快而插入数据慢

  • 异:Vector中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上较 ArrayList 差

2.1.2 ArrayList 与 LinkedList

  • 数据结构:ArrayList 是 Object 数组,LinkedList 是双向链表(JDK1.6 之前是循环链表,JDK1.7 取消了循环)

  • 查询效率:ArrayList 支持高效的随机元素访问,即通过下标快速获取元素对象。而 LinkedList 不支持,所以 ArrayList 的查询效率更高

  • 增删效率:ArrayList 底层是数组存储,所以插入和删除元素的时间复杂度与元素插入的位置有关,因为会涉及到元素的移动问题,例如追加在末尾,则时间复杂度为 O(1) ,若在首部插入,则时间复杂度为 O(n),中间任意位置插入,时间复杂度为,为 O((n - 1) / 2) ,平均时间复杂度还是 O(n) 而 LinkedList采用的是链表存储,所以增删不会涉及到元素的移动,只需要修改指针即可,时间复杂度可以简单看为 O(1),但是要是在指定位置增删元素的话,需要先移动到指定位置再插入,以这个角度看时间复杂度为 O(n)

  • 线程安全:ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。

  • 内存消耗:LinkedListed 每一个元素都需要存放前驱和后继节点的地址,所以每一个元素都更加消耗空间,而 ArrayList 只要是在结尾会预留一定的容量空间,这是扩容所导致的不能充分填满数组的情况(除非使用方法瘦身)

3 Set 接口

3.1 Set 无序性

无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
集合所说的序,是指元素存入集合的顺序,当元素存储的顺序和取出的顺序一致时就是有序,否则就是无序。

3.1.1 HashSet 如何检查重复

当你把对象加入 HashSet时,HashSet 会先计算对象的 hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode ,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。—— 《Head fist java》第二版

4.Map 接口

4.1 HashMap 和 HashTable

  • 数据结构:HashMap JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间,不过在转为红黑树前会判断,如果数组长度小于 64,还是会优先进行数组扩容(哈希表对键进行散列,Map结构即映射表存放键值对),而 HashTable 没有这种特殊的机构。

  • 线程安全:HashMap 是非线程安全的,而 HashTable 属于线程安全的(方法添加了 synchronized 修饰 ),因此,HashMap 效率也会略高,通常认为,HashTable 类似 Vector 是一个Java遗留类,基本不做使用。想保证线程安全,可以考虑使用 ConcurrentHashMap。

  • Null 的处理:HashMap 的键和值都可以存储为 null 的类型,但是只能有一个 null 类型的键,但是 null 类型的值可以有多个。HashTable 的键和值都不允许有 null 类型出现,否则会抛出空指针异常。

  • 扩容机制:不指定初始值的时候,HashMap 初始值为 16,之后每次扩容,容量会成为原先的两倍,HashTable 初始值为 11,扩容会使得容量成为原先的 2n + 1。若指定了初始值,HashMap 会将其扩充为 2 的幂次方大小,而 HashTable 会直接使用你给的初始值。

4.2 HashMap 与 HashSet

  • HashMap 实现了 Map 接口,HashSet实现了 Set 接口。

  • HashMap 储存键值对,HashSet仅仅存储对象。

  • 使用 put() 方法将元素放入 map 中 使用 add() 方法将元素放入 set 中,但 add() 方法实际调用的还是 HashMap 中的 put() 方法。

  • HashMap 中使用键对象来计算 hashcode 值 HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说hashcode 可能相同,所以 equals() 方法用来判断对象的相等性,如果两个对象不同的话,那么返回 false。

  • HashMap 比较快,因为是使用唯一的键来获取对象,HashSet 较 HashMap 来说比较慢。

4.3 HashMap 与 TreeMap

  • 顺序问题:HashMap 中的元素是没有顺序的,TreeMap 中所有的元素都是有某一固定顺序的。

  • 线程安全:HashMap 和 TreeMap 都不是线程安全的

  • 继承问题:HashMap 继承 AbstractMap 类;覆盖了 hashcode() 和 equals() 方法,以确保两个相等的映射返回相同的哈希值。TreeMap 继承 SortedMap 类,保持键的有序顺序。

  • 调优问题:HashMap 基于哈希表实现的,使用HashMap要求添加的键类明确定义了 hashcode() 和 equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子。而 TreeMap 基于红黑树实现,所以TreeMap 就没有调优选项,因为红黑树总是处于平衡的状态。

适用场景: HashMap 适用于 Map 插入,删除,定位元素,TreeMap适用于按自然顺序或自定义顺序遍历键(key)

4.4HashMap 的长度为什么是 2 的幂次方

HashSet因为底层使用哈希表(链表结合数组)实现,存储时key通过一些运算后得出自己在数组中所处的位置。我们在hashCoe方法中返回到了一个等同于本身值的散列值,但是考虑到int类型数据的范围:-2147483648~2147483647 ,着很显然,这些散列值不能直接使用,因为内存是没有办法放得下,一个40亿长度的数组的。所以它使用了对数组长度进行取模运算,得余后再作为其数组下标,indexFor( ) ——JDK7中,就这样出现了,在JDK8中 indexFor()就消失了,而全部使用下面的语句代替,原理是一样的。

// JDK8中
(tab.length - 1) & hash;

/

/ JDK7中
bucketIndex = indexFor(hash, table.length);

static int indexFor(int h, int length) {
    return h & (length - 1);
}

可以看到其本质计算方法都是 (length - 1) & hash 提一句,为什么取模运算时我们用 & 而不用 % 呢,因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,这样就导致位运算 & 效率要比取模运算 % 高很多。

最关键的内容来了,如果我们用更容易理解的取余(%), length % hash == (length - 1) & hash 这个公式想要成立的前提,就必须满足 length 是 2 的 n 次方

简单的说:HashMap 的长度为什么是 2 的幂次方的原因就是,我们为了使用更加高效的 & 运算而不是 % 运算,但又为了保证运算的结果,仍然是取余操作。

4.5 简单谈谈 HashMap 中的底层原理

4.5.1 JDK 1.8 之前

JDK1.8 之前 HashMap 底层是数组 + 链表,HashMap 会使用 hashCode 以及扰动函数处理 key ,然后获取一个hash 值,然后通过 (length- 1) & hash 判断当前元素应该存放的位置,如果这个位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

扰动函数在 4.3 中讲述的应该很清楚了

拉链法的解释,同样可以参考 003-HashMap源码分析(含散列表和红黑树介绍)

4.5.2 JDK 1.8

JDK 8 做了一些较大的调整,当数组中每个格子里的链表,长度大于阈值(默认为8)时,将链表转化为红黑树,就可以大大的减少搜索时间,不过在转为红黑树前会判断,如果数组长度小于 64,还是会优先进行数组扩容。

4.6 HashMap 中加载因子的理解

加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作rehashing(再哈希)。
加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了0.75f。
4.6 ConcurrentHashMap 和 Hashtable 的区别
HashTable 虽然也满足线程安全,但是类似 Vector, 是一个Java遗留类,基本不做使用。想保证线程安全,可以考虑使用 ConcurrentHashMap。

数据结构:JDK 1.7 中,ConcurrentHashMap 底层采用分段数组 + 链表实现,在 JDK 1.8 中,ConcurrentHashMap 中的数据结构与 HashMap 一致,都是数组 + 链表或红黑树。而 Hashtable 采用的是数组 + 链表的形式(数组为主体,链表用来解决哈希冲突)

线程安全:ConcurrentHashMap 在 JDK 1.7 的时候,有一个分段锁的概念,也就是对整个数组进行分割开来(这就是 Segment 的概念),每一把锁,只负责整个锁分段中的一部分,而如果多线程访问不同数据段的数据,锁的竞争也就不存在了,访问并法律也因此提高。而在 JDK 1.8 的时候,直接用 Node 数组 + 链表或红黑树实现,通过 synchronized(JDK 1.6 后优化了很多) 和 CAS 进行并发的控制。Hashtable 就是用一把锁 synchronized 来保证线程安全,效率不是很高,多线程下,很可能会陷入阻塞轮询状态。

注:虽然 JDK 1.8 的源码中还能看到 Segment ,但是主要也只是为了兼容旧版本了

5 二叉树相关

特点:

1.所有非叶子结点至多拥有两个儿子(Left和Right);
2.所有结点存储一个关键字;
3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;

概念:
二叉树又名二叉查找/搜索/排序树
或者是一棵空树;
或者是具有下列性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于它的父结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的父结点的值;
(3)它的左、右子树也分别为二叉排序树。
遍历:分为前序遍历、中序遍历(得到有序集合)、后续遍历
在这里插入图片描述
上图 右边也是一棵 BST树,但是它的搜索性能已经是线性过的了。

所以,使用BST 树 还要考虑尽可能让BST 树保持左图的结构,也就是所谓的“平衡”问题

5.1 平衡二叉树

自平衡二叉查找树 又被称为AVL树(有别于AVL算法)
它是一棵空树或它的左右两个子树的高度差(平衡因子)的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,平衡二叉树必定是二叉搜索树,反之则不一定

平衡二叉树的目的是为了减少二叉查找树层次,提高查找速度。
平衡二叉树的常用实现方法有AA树、AVL树、红黑树、树堆Treap、伸展树等

在这里插入图片描述

5.2 红黑树 -R-B Tree,全称是Red-Black Tree

又称为“红黑树”,它是一种平衡二叉树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点

注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树

在这里插入图片描述

5.3 B- 树

B- 树是一种多路搜索树,一颗m阶B树()是一棵平衡的m路搜索树。他或者是空树,或者是满足下列性质的树:

  • 根结点至少有两个子女;

  • 每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;

  • 除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;

  • 所有的叶子结点都位于同一层。

特点:
是一种多路搜索树(并不是二叉的):

1.定义任意非叶子结点最多只有M个儿子;且M>2;

2.根结点的儿子数为[2, M];

3.除根结点以外的非叶子结点的儿子数为[M/2, M];

4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)

5.非叶子结点的关键字个数=指向儿子的指针个数-1;

6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];

7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的

子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;

8.所有叶子结点位于同一层;

如:(M=3)
在这里插入图片描述
B-树的特性:
1.关键字集合分布在整颗树中;

2.任何一个关键字出现且只出现在一个结点中;

3.搜索有可能在非叶子结点结束;

4.其搜索性能等价于在关键字全集内做一次二分查找;

5.自动层次控制;

5.4 B+ 树

B+ 树 也是一种多路搜索的树,他是B- 树的变体;
1.其定义基本与B-树同,除了:

2.非叶子结点的子树指针与关键字个数相同;

3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树

(B-树是开区间);

5.为所有叶子结点增加一个链指针;

6.所有关键字都在叶子结点出现;

在这里插入图片描述
B+的特性:

1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;

2.不可能在非叶子结点命中;

3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;

4.更适合文件索引系统;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值