- 存储数组节点用
ArrayList
,可以使用add(element)
方法添加元素。 - 存储
Key-Value
用HashMap
,可以使用put(key,value)
方法存入一个Entry
。 - 使用
Set
存储不重复的元素,可以使用add(element)
方法添加元素。
如果你对 Java Collection 体系的了解仅仅到这里就结束了,那么本系列文章将非常适合你。它将带领你深入了解一些你并不知道的 Collection 集合,如 LinkedList
、SortedMap
、TreeMap
、TreeSet
等。并且深入讲解每一种集合类型源代码的实现及其所存在的坑点
。
如何系统了解 Java Collection 体系
使用 IntellJ IDEA
打开我们的 IntellJ IDEA
使用它的 Diagrams
功能,你将会看到整个 Collection
体系的架构。
查看 Collection 接口源代码
打开 Collection
接口,我们将会看到源码文档中所示我们需要了解的一些类
,当然,该文档是 JDK1.2
编写的,但这些类是构成我们现在多种 Collection
集合的基础。
预备知识
当我们在学习一些东西的时候,最好能拥有一些预备知识。如我们以前读书时,每一课老师都让我们先预习一下,为的是在学习的时候起到事倍功半的功效。当然,如果你想要系统的学习 Collection
集合,也是需要很多的预备知识的。下面就跟我一起来了解一下。
计算机基础知识
九层之台,起于垒土
无论多么高度的抽象,想要深入的了解,相关基础知识是一定要掌握的,接下来我们将要介绍一些计算机相关的基础知识,来为我们深入了解 Collection
体系助力。
数据结构
计算机世界中有多种数据结构,用来存储和组织数据。Java Collection
体系所做的事就是将基本所有的数据结构都进行了封装。下面我们将列出一些我们将会用到的数据结构,并在后续文章中,结合相关实践进行讲解。
- 堆栈(Stack)
- 队列(Queue)
- 数组(Array)
- 链表(Linked List)
- 树(Tree)
- 散列表(Hash table)
时间复杂度
正确的数据结构选择可以提高算法的效率
,如何计算该效率则通过算法所占用的「时间」
和「空间」
两个维度去考量。由于现代科技的内存等存储设备的容量都很大,对于空间的消耗我们也不是太过在乎。所以在这里我们仅仅讲解一下时间复杂度
。
如果你对
空间复杂度
也感兴趣,可以查看本文最后的参考进行学习。
什么是时间复杂度?
时间复杂度是执行当前算法所消耗的时间。
那么,如何测量时间复杂度呢?
我们可以简单的把这个算法运行一遍并得出该算法所花费的时间。然后把两个算法执行所需要的时间进行比较。最后就可以得出哪一个效率最高。
但是,这种方法的局限性太高。机器性能、环境等都会影响算法的运行时间。这时,一个新的方法就出现了。
算法的渐进时间复杂度
这时,大O符号
出现了。其数学表示为 T(n) = O(f(n))
。其中的f(n)
表示的是每行代码执行次数之和,而 O
表示正比例关系。该方法通过计算算法的渐进时间复杂度来得出哪个算法所消耗的时间更短。
科普小知识 ? :
大O符号(英语:Big O notation),又称为渐进符号,是用于描述函数渐近行为的数学符号。更确切地说,它是用另一个(通常更简单的)函数来描述一个函数数量级的渐近上界。在数学中,它一般用来刻画被截断的无穷级数尤其是渐近级数的剩余项;在计算机科学中,它在分析算法复杂性的方面非常有用。
其常用的函数阶如下:
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O(n²)
- 立方阶O(n³)
- K次方阶O(n^k)
- 指数阶(2^n)
如果你感觉上面的理论太过于抽象,那我们就 “举个栗子” 来帮助你理解。参见下列代码:
for(int i=1; i<=n; ++i) { (A)
System.out.println(i); (B)
} (C)
复制代码
代码很简单,就三行。由此代码我们可以看出,(A)将会运行 1 次,(B)将会运行 n 次,(C)是符号,忽略不计。这时我们就可以来计算总时间为 T(n)=O(1+n)
。由于公式中的 O(1+n)
是跟随着 n
在变化,我们可以将其简化为线型阶 O(n)
。到此,该算法的时间复杂度为 T(n)=O(n)
。
Java 世界内的一些重要约定
在 Java 世界内有很多约定,如 getter/setter
方法,目录结构
等。接下来我们讲解的是一些比较重要的约定。如果你不遵守,可能会导致一些奇奇怪怪得现象。
一、equals 方法
这是大家熟悉且陌生的一个方法。熟悉是指它是来自 Java “对象之祖” 的 Object
。 陌生是指你并不经常用到它。
但是它却是一个很重要的方法。重要到 JDK 列举了四条特性来讲解该方法。并且这些特性和数学上对于等于的定义基本是一致的。
特性一 自反性 ( reflexive )
一个非空的对象,它一定和自己是相等的 (
x.equals(x) == true
)
特性二 对称性 ( symmetric )
如果对象 A 等于对象 B,那么对象 B 也一定要等于对象 A。(
( a.equals(b) && b.euals(a) ) == true
)
特性三 传递性 ( transitive )
如果对象 A 等于对象 B,对象 B 等于对象 C,那么对象 C 一定等于对象 A。 (
( a.equals(b) && b.equals(c) && c.equals(a) ) == true
)
特性四 一致性 ( consistent )
如果对象 A 等于对象 B,那么无论做了多少操作。A 都要等于 B。
二、hashCode
来自 Object
的第二个方法约定。不遵守它会产生一些意想不到的事情。我们将在后面讲解单个集合
的时候举例描述。
- 一个对象无论经过多少操作其
hashcode
值必须始终唯一。 - 如果对象 A 和对象 B 调用
equals
方法相等,那么它们调用hashcode
方法也一定返回相同值。 - 如果对象 A 和对象 B 调用
equals
方法不相等,允许它们调用hashcode
方法返回的值相等。但是不推荐这样做,这样会影响内部 Hash 表的查找速度。
该方法约定就会产生了一个 De facto standard ( 业界标准 ),那就是当你重写
equals
方法时也需要同时重写hashcode
方法。( 根据 2 原则 )。这也解释了 IntelliJ IDEA 快捷方法中要把这两个方法的重写放在一起。
三、Comparable/Comparator
Java 世界中的一个接口,用来声明该类可以用来比较。在调用 Collections.sort()
方法或者在对集合内的对象进行排序时,就需要使得被排序的类实现该接口。 其默认按照自然顺序从小到大排序。
X < Y => -1
X = Y => 0
X > Y => 1
复制代码
现在的对象中基本已经实现基本类型的 Comparable 接口,所以大部分已经不需要手写了。例如:Long.compare()/Long.compareTo()
方法等。
对于实现了 Comparable 接口的类。如果它们的
compareTo
方法返回相等,那么它们的equals
方法也必须要相等。
迭代器 (Iterator)
迭代器是 Java 世界内的一个接口,用来为 Collection
集合的遍历提供一个方法。它仅仅有 4 个方法:
hasNext
: 判断是否拥有下一个元素。next
: 获取下一个元素。remove
: 删除上一个元素,并可以避免触发fail-fast
机制。必须调用过next
方法后,才可以使用。如果未调用next
直接调用remove
,将会触发IllegalStateException
异常。
fail-fast
机制
fail-fast
机制是在使用一个迭代器进行迭代的时候对该迭代器的集合进行操作,例如删除元素等。该机制出于安全考虑,会抛出ConcurrentModificationException
异常,其目的是保证在你做一个错误的操作时快速的抛出异常终止操作。 而不是带着错误的结果继续进行运算。
forEachRemaining
:JDK 1.8
新增的 default 方法,语法糖。帮助你遍历剩余的元素。
语法糖
简单来说,语法糖就是一种便捷写法。将一些常用的多步骤操作封装为一个简单的操作。如
forEachRemaining
方法:
default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } 复制代码
其主要是将
hasNext
方法和next
方法进行了封装。
总结
本篇文章对一些深入理解 Collection
集合所需要的前置知识进行了讲解,一些约定和规则将会在后续文章中通过一些案例来证实。希望读者记住该基础知识,后续文章中我们将会经常用到。
- 如本章有错误讲解或未涉及到的地方欢迎指正。
- 如果需要后面参考书籍和资料的可在下方留言提供邮箱。
参考
-
Effective Java Third Edition: Joshua Bloch
-
JDK8 官方文档:Oracle
-
算法的时间与空间复杂度 : 作者:不止思考(奎哥)