- 基本的数据结构
- JAVA集合概述
- JAVA集合框架的四个主要体系:Set,List,Queue和Map
1. 基本数据结构
数据结构是指计算机存储、组织数据的方式。这里面有两个内涵,一是数据的逻辑结构;二十数据的物理结构。
数据的逻辑结构反映了元素之间的逻辑关系,逻辑结构主要包括:
(1).线性结构
元素存在着一对一的相互关系。数组就是最简单暴力的线性结构,此外还有链表,再增加一些操作限制就是栈,队列等。
(2)树形结构
元素中存在着一对多的相互关系。常见的如二叉树。
(3)图形结构
元素中存在着多对多的相互关系。
2.JAVA集合概述
Java的集合类是一种位于java.util 下,可以用于存储数量不等的对象,并可以实现常用的数据结构,如栈、队列等。大致可以分为四个主要体系:Set,List,Queue和Map。其中Set就是数学意义上的“集合”,其中的元素无序且不可重复;List中的元素有序且可以重复;Queue是Java5之后新增的Collection的子接口,代表一种队列集合的实现;Map则代表有映射关系的集合。
在现实中,常常有保存多个数据的需求。面对这种需求,通常有两种解决方案,一是使用数组,二是使用集合类。前者的主要局限在于:数组的长度不可变化,一旦在数组初始化时指定了数组的长度,在保存数量可变化的数据情境下,数组就显得无能为力了;此外,数组也无法有效保存具有映射关系的数据(key-value,也被称为关联数组)。
集合类和数组的区别还在于,数组元素既可以是基本类型的值,也可以是对象(实质上还是对象的引用,在此处一般不做区分);而集合类只能保存对象(实质上也还是对象的引用,同上)。
Java的集合类主要由两个接口派生而出:Collection和Map。
API中关于Collection接口的相关信息如下(JavaTM 2 Platform Standard Ed. 6):
进一步做出Collection、Map集合体系的继承树如下:
在访问操作上,对于List集合的元素,可以通过元素的索引来访问;Map元素可以通过没想元素的key来访问;由于Set中元素的特殊性,对于Set只能通过元素本身来访问了。
所有通用的 Collection 实现类(通常通过它的一个子接口间接实现 Collection)应该提供两个“标准”构造方法:一个是 void(无参数)构造方法,用于创建空 collection;另一个是带有 Collection 类型单参数的构造方法,用于创建一个具有与其参数相同元素新的 collection。实际上,后者允许用户复制任何 collection,以生成所需实现类型的一个等效 collection。尽管无法强制执行此约定(因为接口不能包含构造方法),但是 Java 平台库中所有通用的 Collection 实现都遵从它。
Collection是List、Queue、Set的父接口,该接口定义的操作方法可以用于操作List集合、Queue集合和Set集合。
API中,Collection中定义了如下的集合操作方法:
遍历集合的几种方法
(1)使用Lambda表达式遍历集合
java8为Iterable接口新增了一个forEach的默认方法,该方法所需的参数是一个函数式的接口。而Iterable是Collection的父接口,因此Collection集合也可以直接调用改方法。
当程序调用Iterable的forEach(Consumer action)遍历集合函数时,程序会依次将集合元素传给Consumer的accpet(T t)方法。正因为Consumer是函数式的接口,因此可以使用Lambda表达式来遍历集合元素。
public class CollectionEach{
public static void main(String[] args){
Collection books = new HashSet();
books.add("book_one");
books.add("book_two");
// 调用forEach()遍历集合
books.forEach(obj -> System.out.println("迭代集合元素:"+obj));
}
}
(2)使用java8增强的Iterator遍历集合元素
Iterator接口也是集合框架的成员,主要用于遍历Collection集合中的元素,因此Iterstor也被称为迭代器。
除了上述的方法之外,java8还新增了一个默认方法* forEachRemaining(Consumer action)*可以使用Lambda表达式来遍历集合元素。
**注意:**Iterator必须依附于Collection对象,其本身不提供盛装对象的能力。Iterator提供了两个方法来迭代访问Collection集合中的元素,并提供了一个remove()来删除上一个next()方法返回的集合元素。
(3)使用Lambda表达式遍历Iterator
(4)使用foreach()循环遍历集合元素
foreach循环可以使迭代访问元素更加敏捷,类似于Python的for i in range(100)。需要注意的是,foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在foreach循环最终修改迭代变量的值也没有实际意义。
JAVA集合框架的主要体系:Set
Set在概念上类似于数学意义上的“集合”,关于Set接口定义的具体方法如下:
类型 | 方法 | 说明 |
---|---|---|
boolean | add(E e) | 如果 set 中尚未存在指定的元素,则添加此元素(可选操作)。 |
boolean | addAll(Collection c) | 如果 set 中没有指定 collection 中的所有元素,则将其添加到此 set 中(可选操作)。 |
void | clear() | 移除此 set 中的所有元素(可选操作)。 |
boolean | contains(Object o) | 如果 set 包含指定的元素,则返回 true。 |
boolean | containsAll(Collection c) | 如果此 set 包含指定 collection 的所有元素,则返回 true。 |
boolean | equals(Object o) | 比较指定对象与此 set 的相等性。 |
int | hashCode() | 返回 set 的哈希码值。 |
boolean | isEmpty() | 如果 set 不包含元素,则返回 true。 |
Iterator | iterator() | 返回在此 set 中的元素上进行迭代的迭代器。 |
boolean | remove(Object o) | 如果 set 中存在指定的元素,则将其移除(可选操作)。 |
boolean | removeAll(Collection c) | 移除 set 中那些包含在指定 collection 中的元素(可选操作)。 |
boolean | retainAll(Collection c) | 仅保留 set 中那些包含在指定 collection 中的元素(可选操作)。 |
int | size() | 返回 set 中的元素数(其容量)。 |
Object[] | toArray() | 返回一个包含 set 中所有元素的数组。 |
T[] | toArray(T[] a) | 返回一个包含此 set 中所有元素的数组;返回数组的运行时类型是指定数组的类型。 |
HashSet
HashSet是Set的典型实现,大多数情况下使用Set集合就是使用这个实现类。HashSet使用Hash算法来存储集合中的元素,因此具有良好的存取和查找性能。
HashSet具有以下特点:
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化。
- HashSet不是同步的,如果有多个线程访问并修改同一个HashSet,则必须通过代码来保证其同步
- 集合的元素可以是null
当向HashSet集合中add一个元素的时候,HashSet会调用该对象的hashCode()方法得到对象的hashCode值,再根据hashCode的值决定该对象的存储位置。也就是说,如果两个元素通过equals()返回的值是相等的,但是hashCode()的值是不等的话,那么仍然可以add成功。这就与Set的规则冲突。
因此,要避免上述的情况出现。将一个对象放入到HashSet中时,如果需要重写对象对应类的equals()方法时,必须要也要同时重写其hashCode()方法。规则是:如果两个对象的equels()的值返回的是true,则hashCode()的返回值必须相同。
HashSet存储元素的位置称之为bucket,有人翻译成“桶”。如果多个元素的hashCode值相同,但是它们的equels()返回的是false,那么实际上会在这个位置上用链式结构来保存多个对象,也就是说,在一个“桶”里面放置了多个元素,这样会导致性能的下降。
下面给出重写hashCode()的基本规则
- 同一对象多次调用hashCode()的返回值应该相同;
- 当两个对象的equals()的返回值为true时,这个两个对象的hashCode()的返回值应该相同;
- 对象中用作equals()方法比较标准的实例变量,都应该用于计算hashCode()的值。
LinkedHashSet
HashSet还有一个子类就是LinkedHashSet。它也是根据的元素的hashCode的值来决定元素的存取位置,但是塔同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序来保存的。也就是说,当遍历LinkedHashSet集合时,会以元素的添加顺序来访问集合中的元素。
这样做的好处是在迭代访问集合中的全部元素时,具有良好的性能。但同时也增加了额外的开销。
需要注意的是,虽然LinkedHashSet会维护元素的添加顺序,但是其还是Set,因此不允许有重复的元素。
TreeSet类
TreeSet是SortSet接口的实现类,因此可以确保集合处于排序状态。也就多了以下的一些方法:如comparator();last()(返回最后一个元素);first()(返回第一个元素);lower()(返回指定元素之前的元素);higher()(返回指定元素之后的元素)等。
Set小结
HashSet和TreeSet是Set的两个经典实现,那么在实际中应该如何选择呢?应该清楚的是,HashSet的性能总是要比TreeSet(特别是最常用的添加,查询元素等操作)。这是因为TreeSet需要额外的红黑树来维护集合元素的次序。因此,只有当需要保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
JAVA集合框架的主要体系:List
List集合中的元素是有序且可重复的,并且可以通过索引来访问指定元素。List比较熟悉,一般方法也就不在此再次说明了。且说一下java8对于List集合增加的sort()和replaceAll()方法。其中sort()需要一个Comparator对象来控制元素排序,同样可以使用Lambda表达式来作为参数。
ArrayList和Vector
ArrayList和Vector是List的两个典型实现。ArrayList和Vector类都是基于数组实现的List类,所以都封装了一个动态的、允许再分配的Object[]数组。ArrayList和Vector的对象使用initialCapacity参数的来设置数组的长度,并且initialCapacity会在“不够用”的时候自动增加。
一般情况下,无需去主动关心initialCapacity的值。但是如果需要添加大量元素时,可使用ensureCapacity(int minCapacity)的方法一步到位,以减少重分配的次数,增加性能。
关于两者的区别,可能要历史的沿革上面说起来了:d。Vector和ArrayList在用法上几乎相同,但是,Vector比ArrayList要“古老”得多。早在上古时期的KDK 1.0时代,彼此 Java 尚没有提供完整的集合框架,Vector就存在了。也因此,Vector有一些名字很长的方法。1.2 以后,Java提供了系统的集合框架,Vector就作为List的实现之一,但是仍然保留了一些“名字很长的方法”,这导致了Vector里面有一些功能重复的方法。
ArrayList,Vector主要区别为以下几点:
(1):Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比。当多个线程访问一个ArrayList时,如果有超过一个线程修改了ArrayList,则必须手动保证该集合的同步性。在《疯狂 Java 》,更是提出了如下建议:“Vector的缺点很多”,“即使是为了保证List的线程安全,也不推荐使用Vector”。Collections工具类可以将ArrayList变成是线性安全的。
(2):ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,ArrayList默认增加为原来的50%,Vector默认增加为原来的一倍;
(3):Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。
(4):Vector有一个Stack(见Java的集合框架体系图) 子类。由于Stack是继承了Vector,所以同样Stack也是形成安全+性能较差的。如果需要使用“栈”这种数据结构,推荐使用ArrayQeque。
Arrays.ArraysList:固定长度的List
在Arrays的工具类中,提供了asList(Object…a)的方法。需要特别注意的是,这个asList(Object…a)返回的集合既不是ArraysList的实例,也不是Vector的实例。它是Arrays内部的ArraysList的实例。ArraysList是是一个固定长度的List,可以对其进行遍历访问,但是不可以进行修改操作,否则会报异常。
JAVA集合框架的主要体系:Queue
Queue用于模拟队列/栈此类的数据结构。队列的通俗定义是“先进先出”(FIFO)。对于一个队列而言,头部的元素存放的时间最久,尾部的元素存放的时间最短。offer操作可以将一个元素插入到队列的尾部,而通过poll操作可以返回首部的元素。
此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。插入操作的后一种形式是专为使用有容量限制的 Deque 实现设计的;在大多数实现中,插入操作不能失败。