Java核心教程3: 集合

如果一个程序只包含固定数量的对象且对象的生命周期都是已知的,那么这是一个非常简单的程序。

通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。

大多数编程语言都提供了某种方法来解决这个基本问题。例如之前学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果想要保存一组基本类型数据,也推荐使用数组。但是数组具有固定的大小尺寸,而且在更一般的情况下,在写程序的时候并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组长度固定这一限制就显得太不方便了。

因此集合框架就应运而生了,让我们先来看一张整个 Java 集合框架的结构图:


是不是让人看得眼花缭乱?确实如此,我第一次见到这张图的时候也被吓了一跳,没想到Java的集合框架比C++ 的 STL 要复杂这么多。不过没事,让我们慢慢来。

而在这次课程中我们主要介绍图中用红框标记的接口及其主要子类


基本概念

首先Java集合框架分为两大类,如上图所示,分别是 CollectionMap

  • Collection 顾名思义,就是集合的意思,不过和数学上的集合不太一样,根据官方文档,这里的集合既可以有重复元素,也可以没有重复元素,既可以有序,也可以无序,是用来实现多态的一个接口。我们可以把它简单理解成长度可变的数组。List 会以插入顺序存储元素,而 Set 则不能包含重复元素,Queue 则按照指定的排队规则来确定元素存储的顺序(Queue的部分就留给你们自学啦)。

  • Map 则是指一个一个的键值对,可以把它简单的理解成元素类型是键值对的 Collection(但不允许一个键对应多个值)。通过给定 Map 一个键,就可以得到其对应的值,因此它又叫做字典(就像我们查字典的时候,根据单词拼写可以找到对应的单词意思一样)。

在大多数情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定具体类型的地方就是在创建它们的时候。

因此,可以像下面这样创建一个 List ,尖括号里面是集合里面元素的类型,同样右边尖括号里的范型类型可以省略。

List<Apple> apples = new ArrayList<>();

请注意, ArrayList 已经被向上转型为了 List 。使用这些接口的目的是:如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:

List<Apple> apples = new LinkedList<>();

因此,应该创建一个具体类的对象,将其向上转型为对应的接口,然后在其余代码中都是用这个接口,这是面向对象的一种重要思想。

这种方式并非总是有效的,因为某些具体类有额外的功能。例如, LinkedList 具有 List 接口中未包含的额外方法,而 TreeMap 也具有在 Map 接口中未包含的方法。如果需要使用这些方法,就不能将它们向上转型为更通用的接口。


Collection

作为所有集合的根接口,它定义了所有集合都能进行的操作,这里就只介绍几个比较重要的方法:

  • int size() 获取集合大小
  • boolean isEmpty() 集合是否为空
  • boolean contains(Object) 集合是否包含某个元素
  • boolean add(E) 将元素加入集合,成功则返回true
  • boolean remove(E) 将指定元素移除集合,成功则返回true
  • clear() 清空整个集合
  • boolean addAll(Collection<?>)
    将指定集合中所有元素加入此集合,如果集合改变了则返回true
  • boolean removeAll(Collection<?>)
    删除所有指定集合中有的元素,如果集合改变了则返回true
  • <T> T[] toArray(T[] a)
    返回一个包含集合中所有元素的数组,运行时根据集合元素的类型指定数组的类型

值得注意的最后这个方法的参数数组a,如果数组a有足够空间可以放下所有的元素,就会把生成的数组放入a里面,反之则会生成一个新的数组,所以我一般的用法是这样的:

String[] array=list.toArray(new String[0]);



列表 List

List 将元素保存在特定的序列(Sequence)中。 List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。

有下面两种类型的 List

  • ArrayList :实现原理是数组,随机访问元素(就是不按照元素的顺序进行依次访问)速度较快,但在 List 中间插入和删除元素时速度较慢。
  • LinkedList :实现原理是链表,它在 List 中间进行的插入和删除操作速度比较快,但对于随机访问来说相对较慢。由于是用链表实现的,提供了很方便获取开头和末尾元素的方法。


集合 Set

Set 是不允许重复元素的集合,也就是数学意义上真正的集合, 如果试图将相同对象的多个实例添加到 Set 中,它会阻止这种行为。Set 具有与 Collection 一模一样的接口,因此没有任何额外的功能,不像前面两种不同类型的 List 那样。所以实际上, Set 就是一个 Collection ,只是行为不同。

由于 Set 和 Collection 的接口完全相同,所以没有方便的获取中间元素的方法,一般访问元素采用的是 for-in 语句(或者是迭代器),如下所示:

public class SetOfString {
  public static void main(String[] args) {
    Set<String> colors = new HashSet<>();
    for(int i = 0; i < 100; i++) {
      colors.add("Yellow");
      colors.add("Blue");
      colors.add("Red");
      colors.add("Red");
      colors.add("Orange");
      colors.add("Yellow");
      colors.add("Blue");
      colors.add("Purple");
    }
    for(String color: colors){
      System.out.println(color);
    }
  }
}

细心的小伙伴们运行完上面的代码以后可能会发现不对的地方,怎么输出的顺序和添加的顺序不同呢?

对,这就是 HashSet 的一个特性之一,由于它是用散列函数决定元素存储的顺序,所以无法保证输出顺序和添加的顺序保持一致,它内部是用后面要讲到的 HashMap 实现的。

那如果就是要保持一致怎么办呢?那就要用到 LinkedHashSet 了,可以试试把上面代码中的 HashSet 换成 LinkedHashSet 然后输出结果。

最后就是 TreeSet,可以将元素按照给定的 Comparator 进行排序,而它内部也是用 TreeMap 实现的。


迭代器 Iterator

Iterator 的中文名称是迭代器,在 C++ 的 STL 里也有类似的东西。

迭代器是什么?试想一下,我们在所有集合中是不是都会需要访问里面的元素,而迭代器则是将对元素的访问和集合本身隔离开来,也就是说迭代器就是专门用来对集合中的元素进行访问的类。

为什么要有迭代器?想象一下下面的场景:你有一家餐厅,有一个存储菜单项的集合,需要对集合的元素进行访问从而显示菜单,刚开始你可以打算用 List,于是用了 get() 函数访问元素;但过几天以后老板觉得不满意,说要你改用Set,之前所有的代码都需要改,这可麻烦了。

而一开始就用迭代器就不一样了,所有的 Collection 都有一个方法 iterator(),可以返回一个迭代器对象,不管你集合的底层怎么变化,你都不需要改很多的代码。

迭代器主要有如下的方法:

  • boolean hasNext() 判断是否有下一个元素
  • T next() 返回下一个元素
  • void remove() 删除上一次调用next()方法所返回的对象,如果之前没有执行过next()则会抛出错误

值得注意的是remove()这个方法并不是抽象方法,而是有默认实现的:

default void remove() {
    throw new UnsupportedOperationException("remove");
}

也就是说不是所有的集合返回的迭代器对象都可以在中途删除元素,只要子类不重写这个方法就可以了。


映射 Map

Map用起来比较简单,它有两个范型参数,分别表示键的类型和值的类型。

它的几个核心方法如下:

  • V put(K key, V value) 将一个键和一个值进行对应,返回值是这个键之前对应的值,如果这个键之前没有对应的值则返回null
  • V get(K key) 返回一个键对应的值
  • V remove(Object key) 删除一个键对应的值,返回值是这个键之前对应的值
  • void clear() 清除所有键值对的对应关系
  • boolean containsKey(Object key)/containsValue(Object value) 是否存在某个键/值
  • Set<Map.Entry<K,V>> entrySet() 返回包含所有键值对的 Set

然后就是几个主要子类:
HashMap 通过散列函数使得查找速度非常快,但不保存键的顺序(和 HashSet 差不多)。
TreeMap 始终按照指定排序规则的升序来保存键,所以查找速度比 HashMap 要慢。
LinkedHashMap 在保持 HashMap 的查找速度的同时按照键的插入顺序保存键。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343