Java——容器类库框架浅析
前言
通常,我们总是在程序运行过程中才获得一些条件去创建对象,这些动态创建的对象就需要使用一些方式去保存。我们可以使用数组去存储,但是需要注意数组的尺寸一旦定义便不可修改,而我们并不知道程序在运行过程中会产生多少对象,于是数组的尺寸便成了限制。Java实用类库还提供了一套的容器类来解决这个问题,基本类型为:List 、Set、Queue和Map。这些对象类型也称为集合类,但是由于Java类库使用了Collection这个名字来指代该类库中的一个特殊子集,所以使用术语“容器”来称呼它们。下面将简单介绍Java容器类库的基本概念和功能、每个容器的不同版本实现和和从整体分析容器之间的联系。
Java容器类库概览
Java容器类的框架图
Java容器类库的主要作用是“保存对象”,我们将其划分成以下两个不同的概念:
Collection
一个独立的元素序列(一种存放一组对象的方式),这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素;Set中不能有重复的元素;Queue按排队规则来确定对象产生的顺序。
Collection是一个高度抽象的容器接口,其中包含了容器的基本操作和属性。
Map
一组成对的“键值对”对象,允许你使用键来查找值。
框架类图中还包含了许多Abstract类,主要便于我们创建容器的实例,Abstract类中已基本实现了接口中的方法,我们只需要选择我们需要的方法进行覆盖即可。
Iterator
我们再来看Iterator,我们通常是使用Iterator迭代器来遍历容器。上图存在的Collection依赖于Iterator是指:实现Collection需要实现iterator()函数,可以返回一个Iterator对象。ListIterator是专门用于遍历List的迭代器。
工具类Arrays和Collections为容器添加元素
java.util包中的Arrays和Collections类中包含了很多的实用方法。Arrays类中包含操作数组的各种方法,还包含一个静态的Arrays.asList()方法接受一个数组或是用逗号分隔的元素列表,将其转换成一个列表对象。Collection类包含对集合操作的各种方法。我们也可以使用Collections.addAll()向容器中添加一组元素。Collections.addAll()接受一个Collection对象以及一个数组或者用逗号分隔的元素列表,将元素添加到Collection对象中。
Arrays.asList()的底层实现是一个数组,即使用Arrays.asList()生成的List的尺寸是不可以修改的(添加或删除元素),否则将会抛出UnsupportedOperationException异常。
List
List接口继承自Collection接口,用于Collection中的所有方法,在Collection的基础上也添加了许多方法,使得可以在List中插入和删除元素。List有两种基本的实现:ArrayList和LinkedList
- 基本的ArrayList,它适合于随机访问元素,但是在插入和删除元素时就比较慢
- LinkedList适合于在元素插入和删除较频繁时使用,随机访问的速度比较慢
Set
Set中不保存重复的元素,含义同数学概念上的集合。 Set常用于测试归属性,即查询某个元素是否在某个Set中。正因为如此查找也就成了Set中重要的操作。通常会选择HashSet的实现,它对快速查找进行了优化。Set也有多种不同的实现,不同的Set实现不仅具有不同的行为,而且它们对于可以在特定的Set中放置元素的类型也有不同的要求。
Set(interface)
存入Set的每个元素必须是唯一对的。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set接口不保证维护元素的次序。
HashSet
为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()。使用HashMap实现。
TreeSet
保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口。使用TreeSet实现。
LinkedHashSet
具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。在使用迭代器遍历该Set时,结果会按照元素插入的次序显示。元素也必须定义hashCode()方法
Map
Map有以下特点:
Map是将键映射到值的键值对(key-value)接口
映射中不能包含重复的键,每个键最多可以映射到一个值,但是一个值可以被多个键映射
Map提供了三个Set视图供我们访问:键的Set、值的Set和键值对的Set
映射的顺序定义为访问的映射Set上的迭代器返回元素的顺序。TreeMa类,可以对映射的顺序做出特定保证;其他的,则不能保证
可变对象作为映射键需要非常小心
Map的实现类应该提供两个“标准“构造函数
第一个,void(无参数)构造方法,用于创建空映射
第二个,带有单个 Map 类型参数的构造方法,用于创建一个与其参数具有相同键-值映射关系的新映射。带有单个 Map 类型参数的构造方法,用于创建一个与其参数具有相同键-值映射关系的新映射
Map的几种基本实现:
HashMap
Map是基于散列表的实现(取代了HashTable)。HashMap使用散列码(对象hashCode()生成的值)来进行快速搜索。
LinkedHashMap
类似于HashMap,但是迭代的时候,取得键值对的顺序是起插入的顺序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点,而迭代访问的时候更快,因为使用链表维护了内部次序。
TreeMap
基于红黑树的实现。查看“键”或者“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于所得到的结果是经过排序的。TreeMap是唯一的带有subMap()的Map,可以返回一个子树。
WeakHashMap
弱键(weak key)映射,允许释放映射所指向的对象,这是为了解决某类特殊问题而设计的。如果映射之外没有引用指向某个“键”,则此“键”可以被垃圾回收。
ConcurrentHashMap
一种线程安全的Map,不涉及同步加锁。在并发中还会介绍。
Stack和Queue
Stack是一个先进后出(LIFO)的容器。往盒子中放书,先放进去的最后才拿得出来,最后放进去的第一个就可以取出,这种模型就是栈(Stack)可以描述的。LinkedList中有可以实现栈所有功能的方法,有时也可以直接将LinkedList作为栈使用。
队列是一个典型的先进先出(FIFO)的容器。事物放进容器的顺序和取出的顺序是相同的(优先级队列根据事物优先级出队事物)。队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要。同样,LinkedList也提供了方法支持队列的行为,并且它实现了Queue接口。
优先级队列PriorityQueue
先进先出描述了典型的队列规则。队列规则是指在给定一组队列的元素情况下,确定下一个弹出队列的元素的规则。优先级队列声明的下一个弹出的元素是最需要的元素(具有最高优先级的元素)。
我们可以在PriorityQueue上调用offer()方法来插入一个对象,这个对象就会在队列中被排序,默认排序为自然排序,即按插入的先后进行排序,但是我们可以通过提供自己的Comparator来修改这个排序。当调用peek()、poll()和remove()方法时,将获取队列优先级最高的元素。
优先级队列算法实现的数据结构通常是一个堆。
迭代器
对于访问容器而言,有没有一种方式使得同一份遍历的代码可以适用于不同类型的容器?要实现这样的目的就可以使用迭代器。使用迭代器对象,遍历并选择序列中的对象,而客户端不必知道或关心该序列底层的结构。Java中对迭代器有一些限制,比如Java的Iterator只能单向移动,这个Iterator只能用来:
- 使用next()方法获得序列的下一个元素
- 使用hasNext()方法检查序列中是否还有元素
- 使用remove()方法将迭代器新近返回的元素删除,意味着在调用remove()之前必须先调用next()
API中的Iterator接口中方法如上,实现Iterator对象需要实现hashNext()方法和next()方法,remove方法是一个可选操作。forEachRemaining是Java 1.8(Java SE8)中加入的方法,用于Lambda表达式。
举一个简单的使用迭代器访问容器的例子:
class Cat{
private static int counter = 0;
private int id = counter++;
@Override
public String toString() {
return "Cat: " + id;
}
}
public class IteratorAccessContainer {
//不包含任何容器类型信息的遍历容器方法
public static void showElement(Iterator<Cat> it) {
while (it.hasNext()) { //hasNext()检查序列中是否还有元素
Cat cat = it.next(); //next()返回序列中的元素
System.out.print(cat + "\t");
}
System.out.println();
}
public static void main(String[] args) {
ArrayList<Cat> cats1 = new ArrayList<Cat>();
LinkedList<Cat> cats2 = new LinkedList<>(); //可以省略类型参数 编译器可自动推断出
HashSet<Cat> cats3 = new HashSet<>();
for(int i=0;i<3; i++) {
cats1.add(new Cat());
cats2.add(new Cat());
cats3.add(new Cat());
}
showElement(cats1.iterator());
showElement(cats2.iterator());
showElement(cats3.iterator());
}
}
/*
output:
Cat: 0 Cat: 3 Cat: 6
Cat: 1 Cat: 4 Cat: 7
Cat: 2 Cat: 8 Cat: 5
*/
showElement()方法不包含任何有关它遍历的序列类型信息,这就展示了Iterator的好处:能够将遍历序列的操作与序列底层结构分离。也可以说,迭代器统一了对容器的访问方式。
从容器框架图中我们可以看出,Collection是描述所有序列容器的共性的根接口。但是在C++中,标准的C++类库中没有其他容器的任何公共基类,容器之间的共性都是通过迭代器达成的。在Java中,则将两种方法绑定到了一起,实现Collection的同时也要实现iterator()方法(返回该容器的迭代器)。
ListIterator
ListIterator是一个更加强大的Iterator子类型,但是它只能用于各种List的访问。Iterator只能前向移动,但ListIterator允许我们可以前后移动。它还可以产生相对于迭代器在列表中指向当前位置的前一个和后一个索引,并且可以使用set()方法替换它访问过的最后一个元素。remove()方法可以删除它访问过的最后一个元素。需要注意,这两处的最后一个元素只的都是调用next()或者previous返回的元素,也就意味着调用set()、remove()这两个方法之前,要先调用next()或者previous()。
需要注意ListIterator在序列中的游标位置与Iterator不同,Iterator的游标位置始终位于调用previous()将返回的元素和调用next()将返回的元素之间。长度为n的列表的迭代器的游标位置有n+1个。
使用ListIterator对列表进行正向和返回迭代,以及使用set()替换列表元素的例子:
public class ListIteration {
public static void main(String[] args) {
List<Cat> catList = new ArrayList<>();
for(int i=0; i<5; i++) {
catList.add(new Cat());
}
ListIterator<Cat> it = catList.listIterator();
System.out.println("CatNo.\t nextIndex\t previousIndex");
//正向遍历
System.out.println("正向遍历:");
while (it.hasNext()) {
Cat cat = it.next();
System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
}
System.out.println();
System.out.println("当迭代器游标处于最后一个元素末尾时:");
ListIterator<Cat> it2 = catList.listIterator();
while (it2.hasNext()) {
Cat cat = it2.next();
System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
}
System.out.println();
//反向遍历
System.out.println("反向遍历");
while(it.hasPrevious()) {
Cat cat = it.previous();
System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
}
System.out.println();
//产生指定游标位置的迭代器 从第二个位置开始向前替换列表中的Cat对象
System.out.println("从第二个位置开始向前替换列表中的Cat对象");
it = catList.listIterator(2);
while(it.hasNext()) {
it.next();
it.set(new Cat());
}
System.out.println(catList);
}
}
/*
CatNo. nextIndex previousIndex
正向遍历:
Cat: 0 1 0
Cat: 1 2 1
Cat: 2 3 2
Cat: 3 4 3
Cat: 4 5 4
当迭代器游标处于最后一个元素末尾时:
Cat: 0 5 4
Cat: 1 5 4
Cat: 2 5 4
Cat: 3 5 4
Cat: 4 5 4
反向遍历
Cat: 4 4 3
Cat: 3 3 2
Cat: 2 2 1
Cat: 1 1 0
Cat: 0 0 -1
从第二个位置开始向前替换列表中的Cat对象
[Cat: 0, Cat: 1, Cat: 5, Cat: 6, Cat: 7]
*/
foreach与迭代器
foreach语法不仅可以用在数组,也可以用在任何Collection对象。之所以可以用在Collection对象,是因为Java SE5引入了Iterable
接口,该接口包含一个能够产生Iterator的iterator()方法,并且Iterable接口被foreach用来在序列中移动。因此,如果创建了任何实现Iterable的类,都可以将它用于foreach当中。需要注意,数组虽然可以使用foreach语法遍历,但不意味着数组是Iterable的。
实现一个可迭代的类,使用foreach方法遍历
public class IterableClass implements Iterable<String>{
private String[] words = ("This is happy day.").split(" ");
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {
private int index = 0;
//判断是否存在下一个元素
public boolean hasNext() {
return index < words.length;
}
//返回下一个元素
public String next() {
return words[index++];
}
public void remove() { //remove可以不用实现
throw new UnsupportedOperationException();
}
};
}
public static void main(String[] args) {
//foreach语法遍历实现了Iterable接口的类
for(String s : new IterableClass()) {
System.out.println(s);
}
}
}
/*
This
is
happy
day.
*/
小结
对Java容器类库做了大致的介绍,具体的容器使用方法以及实现会在后面的博客中继续介绍。本文重点介绍了Iterator,它统一了对容器的访问方式,但是仍有一点心存疑惑:foreach语法可以遍历容器是因为容器实现了Iterable的原因,但是也可以遍历数组,数组并不是Iterable的。那么foreach可以遍历数组的依据是什么呢?这个问题暂时还没有看到合适的解答,各位看官若有想法可留言告知,感激不尽!
参考:
Java 集合系列目录(Catedory): https://www.cnblogs.com/skywang12345/p/3323085.html
《Java编程思想》第四版
Java——容器类库框架浅析的更多相关文章
- java容器---集合总结
思考为什么要引入容器这个概念? Java有多种方式保存对象(应该是对象的引用),例如使用数组时保存一组对象中的最有效的方式,如果你想保存一组基本类型的数据,也推荐使用这种方式,但大家知道数组是具有固定 ...
- 【Java心得总结六】Java容器中——Collection
在[Java心得总结五]Java容器上——容器初探这篇博文中,我对Java容器类库从一个整体的偏向于宏观的角度初步认识了Java容器类库.而在这篇博文中,我想着重对容器类库中的Collection容器 ...
- 【Java心得总结五】Java容器上——容器初探
在数学中我们有集合的概念,所谓的一个集合,就是将数个对象归类而分成为一个或数个形态各异的大小整体. 一般来讲,集合是具有某种特性的事物的整体,或是一些确认对象的汇集.构成集合的事物或对象称作元素或是成 ...
- Java 容器(list, set, map)
java容器类库的简化图: (虚线框表示接口, 实线框表示普通的类, 空心箭头表示特定的类实现了接口, 实心箭头表示某个类可以生成箭头所指的类对象) 继承Collection的主要有Set 和 Lis ...
- Java 容器相关知识全面总结
Java实用类库提供了一套相当完整的容器来帮助我们解决很多具体问题.因为我本身是一名Android开发者,包括我在内很多安卓开发,最拿手的就是ListView(RecycleView)+BaseAda ...
- Java 容器的打印
Java容器类库中的两种主要类型,它们的区别在于容器中每个"槽"保存的元素个数 Clollection容器只能在保存一个元素,此类容器包括: List,它以特定顺序保存一组元素 S ...
- Java容器总结
容器总结 Java容器工具包框架图 List,Set,Map三者的区别 List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一无二的性 ...
- 013-并发编程-java.util.concurrent.locks之-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放
一.概述 AbstractQueuedSynchronizer (简称AQS),位于java.util.concurrent.locks.AbstractQueuedSynchronizer包下, A ...
- JAVA学习笔记--初识容器类库
一.前言 JAVA中一切皆为对象,因而,持有对象显得尤为重要. 在JAVA中,我们可以通过创建一个对象的引用的方式来持有对象: HoldingObject holding; 也可以创建一个对象数组来持 ...
随机推荐
- BZOJ_1180_[CROATIAN2009]OTOCI_LCT
BZOJ_1180_[CROATIAN2009]OTOCI_LCT Description 给出n个结点以及每个点初始时对应的权值wi.起始时点与点之间没有连边.有3类操作: 1.bridge A ...
- Dubbo原理和源码解析之服务引用
一.框架设计 在官方<Dubbo 开发指南>框架设计部分,给出了引用服务时序图: 另外,在官方<Dubbo 用户指南>集群容错部分,给出了服务引用的各功能组件关系图: 本文将根 ...
- Linux下网站根目录权限
网站根目录权限遵循: 文件644 文件夹755 权限用户和用户组www-data 如出现文件权限问题时,请执行下面3条命令: chown -R www-data.www-data /usr/local ...
- 使用 NLog 给 Asp.Net Core 做请求监控
为了减少由于单个请求挂掉而拖垮整站的情况发生,给所有请求做统计是一个不错的解决方法,通过观察哪些请求的耗时比较长,我们就可以找到对应的接口.代码.数据表,做有针对性的优化可以提高效率.在 asp.ne ...
- AMBA总线协议AHB、APB
一.什么是AMBA总线 AMBA总线规范是ARM公司提出的总线规范,被大多数SoC设计采用,它规定了AHB (Advanced High-performance Bus).ASB (Advanced ...
- Twitter分布式自增ID算法snowflake原理解析
以JAVA为例 Twitter分布式自增ID算法snowflake,生成的是Long类型的id,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特(0和1). 那么一个 ...
- javascript && php &&java 轰炸!!!
java && javascript && php 轰炸!!!恢复 1.javascript简介 *是基于对象和时间的驱动语言,应用于客户端. -----基于对象: * ...
- HTML 基本语法速查
HTML 基本文档 <!DOCTYPE html> <html> <head> <title>文档标题</title> </head& ...
- 『开源』扩展 JS 的 Date 处理函数
背景: JS 有自己的 时间类型 Date —— 但是,在某些情况下 这个对象似乎 不太好用. 本文 基于 JQuery 扩展了一些 JS日期函数,包括: > 字符串 转 Date 对象 万 ...
- .NETCore 下支持分表分库、读写分离的通用 Repository
首先声明这篇文章不是标题党,我说的这个类库是 FreeSql.Repository,它作为扩展库现实了通用仓储层功能,接口规范参考 abp vnext 定义,实现了基础的仓储层(CURD). 安装 d ...