Java语言基础(4)
1. 集合类
1.1 Set
在 Java 的 Set 体系中, 根据实现方式不同主要分为两大类。 HashSet 和 TreeSet.
- TreeSet 是二叉树实现的,Treeset 中的数据是自动排好序的, 不允许放入 null 值
- HashSet 是哈希表实现的,HashSet 中的数据是无序的,可以放入 null,但只能放入一个 null.
HashSet 中, 基本的操作都是有 HashMap 底层实现的, 因为 HashSet 底层是用HashMap 存储数据的。
当向 HashSet 中添加元素的时候, 首先计算元素的 hashcode值, 然后通过扰动计算和按位与的方式计算出这个元素的存储位置, 如果这个位置位空, 就将元素添加进去; 如果不为空, 则用 equals 方法比较元素是否相等, 相等就不添加, 否则找一个空位添加。
TreeSet 的底层是 TreeMap 的 keySet(), 而 TreeMap 是基于红黑树实现的, 红黑树是一种平衡二叉查找树, 它能保证任何一个节点的左右子树的高度差不会超过较矮的那棵的一倍。TreeMap 是按 key 排序的, 元素在插入 TreeSet 时 compareTo()方法要被调用,所以 TreeSet 中的元素要实现 Comparable 接口。 TreeSet 作为一种 Set, 它不允许出现重复元素。 TreeSet 是用 compareTo()来判断重复元素的.
1.2 HashMap、 HashTable、 ConcurrentHashMap 区别
HashMap 和 HashTable 有何不同?
- 线程安全: HashTable 中的方法是同步的, 而 HashMap 中的方法在默认情况下是非同步的。 在多线程并发的环境下, 可以直接使用 HashTable, 但是要使用 HashMap 的话就要自己增加同步处理了。
- 继承关系: HashTable 是基于陈旧的 Dictionary 类继承来的。 HashMap 继承的抽象类 AbstractMap 实现了 Map 接口。
- 允不允许 null 值: HashTable 中, key 和 value 都不允许出现 null 值, 否则会抛出NullPointerException 异常。 HashMap 中, null 可以作为键, 这样的键只有一个; 可以有一个或多个键所对应的值为 null。
- 默认初始容量和扩容机制: HashTable 中的 hash 数组初始大小是 11, 增加的方式是 old*2+1。 HashMap 中 hash 数组的默认大小是 16, 而且一定是 2 的指数。
- 哈希值的使用不同 : HashTable 直接使用对象的 hashCode。 HashMap 重新计算 hash 值。
- 遍历方式的内部实现上不同 : Hashtable、 HashMap 都使用了 Iterator。 而由于历史原因, Hashtable 还使用了 Enumeration 的方式 。 HashMap 实现 Iterator, 支持 fast-fail, Hashtable 的 Iterator 遍历支持 fast-fail, 用 Enumeration 不支持 fast-fail。
HashMap 和 ConcurrentHashMap 的区别?
ConcurrentHashMap 和 HashMap 的实现方式不一样, 虽然都是使用桶数组实现的, 但是还是有区别, ConcurrentHashMap 对桶数组进行了分段, 而 HashMap 并没有。
ConcurrentHashMap 在每一个分段上都用锁进行了保护。 HashMap 没有锁机制。
所以, 前者线程安全的, 后者不是线程安全的。
PS: 以上区别基于 jdk1.8 以前的版本
1.3 HashMap 的容量、 扩容
见文档 P139 页
1.4 HashMap 中 hash 方法的原理
文档 P144
《 全网把 Map 中的 hash()分析的最透彻的文章, 别无二家。 》
indexFor 方法其实主要是将 hash 生成的整型转换成链表数组中的下标。
那么 return h & (length-1);是什么意思呢?
其实, 他就是取模。 Java 之所有使用位运算(&)来代替取模运算(%), 最主要的考虑就是效率。 位运算(&)效率要比代替取模运算(%)高很多, 主要原因是位运算直接对内存数据进行操作, 不需要转成十进制, 因此处理速度非常快.
所以, return h & (length-1);只要保证 length 的长度是 2^n 的话, 就可以实现取模运算了。 而 HashMap 中的 length 也确实是 2 的倍数, 初始值是 16, 之后每次扩充为原来的 2 倍。
先做个总结。
HashMap 的数据是存储在链表数组里面的。在对 HashMap 进行插入/删除等操作时,都需要根据 K-V 对的键值定位到他应该保存在数组的哪个下标中。 而这个通过键值求取下标的操作就叫做哈希。 HashMap 的数组是有长度的, Java 中规定这个长度只能是 2 的倍数, 初始值为 16。 简单的做法是先求取出键值的 hashcode, 然后在将 hashcode 得到的int 值对数组长度进行取模。 为了考虑性能, Java 总采用按位与操作实现取模操作。
1.5 为什么 HashMap 的默认容量设置成 16
根据作者的推断, 这应该就是个经验值( Experience Value) , 既然一定要设置一个默认的 2^n 作为初始值, 那么就需要在效率和内存使用上做一个权衡。 这个值既不能太小,也不能太大.
1.6 Java 8 中 stream 相关用法
Java 8 API 添加了一个新的抽象称为流Stream, 可以让你以一种声明的方式处理数据。 Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对Java 集合运算和表达的高阶抽象。
Stream 有以下特性及优点:
- 无存储。 Stream 不是一种数据结构, 它只是某种数据源的一个视图, 数据源可以是一个数组, Java 容器或 I/O channel 等。
- 为函数式编程而生。 对 Stream 的任何修改都不会修改背后的数据源, 比如对Stream 执行过滤操作并不会删除被过滤的元素, 而是会产生一个不包含被过滤元素的新 Stream。
- 惰式执行。 Stream 上的操作并不会立即执行, 只有等到用户真正需要结果的时候才会执行。
- 可消费性(一次性)。 Stream 只能被“ 消费” 一次, 一旦遍历过就会失效, 就像容器的迭代器那样, 想要再次遍历必须重新生成。
我们举一个例子, 来看一下到底 Stream 可以做什么事情
上面的例子中, 获取一些带颜色塑料球作为数据源, 首先过滤掉红色的、 把它们融化成随机的三角形。 再过滤器并删除小的三角形。 最后计算出剩余图形的周长。
如上图, 对于流的处理, 主要有三种关键性操作: 分别是流的创建、 中间操作 以及最终操作。
1.6.1 Stream 的创建
1、 通过已有的集合来创建流
在 Java 8 中, 除了增加了很多 Stream 相关的类以外, 还对集合类自身做了增强,在其中增加了 stream 方法, 可以将一个集合类转换成流。
2、 通过 Stream 创建流
可以使用 Stream 类提供的方法, 直接返回一个由指定元素组成的流。
1.6.2 Stream 的中间操作
以下是常用的中间操作列表:
见 P170
1.6.3 stream最终操作
Stream 的中间操作得到的结果还是一个 Stream, 那么如何把一个 Stream 转换成我们需要的类型呢? 比如计算出流中元素的个数、 将流装换成集合等。 这就需要最终操作。
最终操作会消耗流, 产生一个最终结果。 也就是说, 在最终操作之后, 不能再次使用流,也不能在使用任何中间操作, 否则将抛出异常。
见P173
1.7 fail-fast 和 fail-safe
1.7.1 fail-fast:
fail-fast 是一种比较好的机制, 为什么说 fail-fast 会有坑呢?
原因是 Java 的集合类中运用了 fail-fast 机制进行设计, 一旦使用不当, 触发 fail-fast 机制设计的代码, 就会发生非预期情况。
我们通常说的 Java 中的 fail-fast 机制, 默认指的是 Java 集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变的操作时, 有可能会产生 fail-fast 机制, 这个时候就会抛出 ConcurrentModificationException( 后文用 CME 代替) 。
简单总结一下, 之所以会抛出 CMException 异常, 是因为我们的代码中使用了增强for循环(如:for (int num : nums)) , 而 在 增 强 fo r 循 环 中 , 集 合 遍 历 是 通 过 it e r a t o r 进 行 的 , 但 是 元 素 的add/remove 却是直接使用的集合类自己的方法。 这就导致 iterator 在遍历的时候, 会发现有一个元素在自己不知不觉的情况下就被删除/添加了, 就会抛出一个异常, 用来提示用户, 可能发生了并发修改。
这也是为什么增强for循环不能修改原有集合的原因。
1.7.2 fail-safe
为了避免触发 fail-fast 机制, 导致异常, 我们可以使用 Java 中提供的一些采用了fail-safe 机制的集合类。
这样的集合容器在遍历时不是直接在集合内容上访问的, 而是先复制原有集合内容, 在拷贝的集合上进行遍历。
java.util.concurrent 包下的容器都是 fail-safe 的, 可以在多线程下并发使用, 并发修改。 同时也可以在 foreach 中进行 add/remove 。
如何在遍历的同时删除 ArrayList 中的元素?
- 直接使用普通 for 循环进行操作: 普通 for循环并没有用到 Iterator 的遍历, 所以压根就没有进行 fail-fast 的检验
- 直接使用 Iterator 进行操作
- 使用 Java 8 中提供的
filter过滤: Java 8 中可以把集合转换成流, 对于流有一种 filter 操作, 可以对原始 Stream 进行某项测试, 通过测试的元素被留下来生成一个新 Stream。 - 直接使用 fail-safe 的集合类