本章重点:

Java集合类是一种特别有用的工具类,可用于存储数量不等的对象,并可以实现常用的数据结构,如栈、队列等。除此之外,Java 集合还可用于保存具有映射关系的关联数组。Java集合大致可分为Set、List、Queue和 Map四种体系,其中Set 代表无序、不可重复的集合;List 代表有序、重复的集合;而Map则代表具有映射关系的集合,Java 5又增加了Queue体系集合,代表一种队列集合实现
        Java集合就像一种容器,可以把多个对象(实际上是对象的引用,但习惯上都称对象)“丢进”该容器中。在Java 5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理;从Java 5增加了泛型以后,Java集合可以记住容器中对象的数据类型,从而可以编写出更简洁、健壮的代码。本章不会介绍泛型的知识,本章重点介绍Java的4种集合体系的功能和用法。本章将详细介绍Java的4种集合体系的常规功能,深入介绍各集合实现类所提供的独特功能,深入分析各实现类的实现机制,以及用法上的细微差别,并给出不同应用场景选择哪种集合实现类的建议。

1. Java集合概述

在编程时,常常需要集中存放多个数据,例如第6章练习题中梭哈游戏里剩下的牌。可以使用数组来保存多个对象,但数组长度不可变化,一旦在初始化数组时指定了数组长度,这个数组长度就是不可变的,如果需要保存数量变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:语文—79,数学—80,这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。
        为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组)Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent 包下提供了一些多线程支持的集合类
        集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)
        Java的集合类主要由两个接口派生而出:Collection和 Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。如图8.1所示是Collection接口、子接口及其实现类的继承树。

图8.1显示了Collection体系里的集合,其中粗线圈出的Set和 List接口是Collection接口派生的两个子接口,它们分别代表了无序集合和有序集合;Queue是Java提供的队列实现,有点类似于List。

如图8.2所示是Map体系的继承树,所有的Map实现类用于保存具有映射关系的数据(也就是前面介绍的关联数组)。
        图8.2显示了Map接口的众多实现类,这些实现类在功能、用法上存在一定的差异,但它们都有一个功能特征:Map保存的每项数据都是key-value对,也就是由key和 value两个值组成。就像前面介绍的成绩单:语文一79,数学―80,每项成绩都由两个值组成,即科目名和成绩。对于一张成绩表而言,科目通常不会重复,而成绩是可重复的,通常习惯根据科目来查阅成绩,而不会根据成绩来查阅科目。Map与此类似,Map里的 key是不可重复的,key用于标识集合里的每项数据,如果需要查阅Map中的数据时,总是根据Map 的key来获取
        对于图8.1和图8.2中粗线标识的4个接口,可以把Java所有集合分成三大类,其中 Set集合类似于一个罐子,把一个对象添加到Set集合时,Set集合无法记住添加这个元素的顺序,所以Set里的元素不能重复(否则系统无法准确识别这个元素);List集合非常像一个数组,它可以记住每次添加元素的顺序、且List 的长度可变Map集合也像一个罐子,只是它里面的每项数据都由两个值组成。图8.3显示了这三种集合的示意图。

从图8.3中可以看出,如果访问List集合中的元素,可以直接根据元素的索引来访问;如果访问Map集合中的元素,可以根据每项元素的key来访问其 value;如果访问Set集合中的元素,则只能根据元素本身来访问(这也是Set集合里元素不允许重复的原因)
        对于Set、List、Queue和 Map四种集合,最常用的实现类在图8.1、图8.2中以灰色背景色覆盖,分别是HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList和HashMap、TreeMap等实现类。

2. Collection和 Iterator接口

Collection接口是List、Set和 Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和 Queue集合。Collection接口里定义了如下操作集合元素的方法。

  • boolean add(Object o):该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回true。
  • boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回true。
  • void clear():清除集合里的所有元素,将集合长度变为0
  • boolean contains(Object o):返回集合里是否包含指定元素
  • boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素
  • boolean isEmpty():返回集合是否为空。当集合长度为0时返回true,否则返回false。
  • Iterator iterator():返回一个 Iterator对象,用于遍历集合里的元素
  • boolean remove(Object o):删除集合中的指定元素o当集合中包含了一个或多个元素o时,该方法只删除第一个符合条件的元素,该方法将返回true。
  • boolean removeAll(Collection c):从集合中删除集合c里包含的所有元素(相当于用调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true。
  • boolean retainAll(Collection c):从集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合和集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。
  • int size():该方法返回集合里元素的个数
  • Object[] toArray():该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素

下面程序示范了如何通过上面方法来操作Collection集合里的元素。

上面程序中创建了两个Collection对象,一个是c集合,一个是books集合,其中c集合是ArrayList,而 books集合是HashSet虽然它们使用的实现类不同,但当把它们当成Collection来使用时,使用add、remove、clear等方法来操作集合元素时没有任何区别
        编译和运行上面程序,看到如下运行结果:

把运行结果和粗体字标识的代码结合在一起看,可以看出Collection 的用法有:添加元素、删除元素、返回Collection集合的元素个数以及清空整个集合等

当使用System.out的 println()方法来输出集合对象时,将输出[ele1,ele2,...]的形式,这显然是因为所有的Collection实现类都重写了toString()方法,该方法可以一次性地输出集合中的所有元素。
        如果想依次访问集合里的每一个元素,则需要使用某种方式来遍历集合元素,下面介绍遍历集合元素的两种方法。

3. 使用Lambda表达式遍历集合

Java 8为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口,而Iterable 接口是Collection接口的父接口,因此Collection集合也可直接调用该方法(下面的程序就是集合调用的)
        当程序调用Iterable 的forEach(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumer的 accept(T t)方法(该接口中唯一的抽象方法)。正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素
如下程序示范了使用Lambda表达式来遍历集合元素。

上面程序中粗体字代码调用了Iterable的 forEach()默认方法来遍历集合元素,传给该方法的参数是一个Lambda表达式,该Lambda表达式的目标类型是Comsumer。forEach()方法会自动将集合元素逐个地传给Lambda表达式的形参,这样 Lambda表达式的代码体即可遍历到集合元素了。

4. 使用Java8增强的lterator遍历集合元素

Iterator接口也是Java集合框架的成员,但它与Collection系列、Map系列的集合不一样:Collection系列集合、Map系列集合主要用于盛装其他对象而Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。
        Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口。Iterator接口里定义了如下4个方法。

  • boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true
  • Object next():返回集合里的下一个元素
  • void remove():删除集合里上一次next方法返回的元素。
  • void forEachRemaining(Consumer action),这是Java 8为lterator新增的默认方法,该方法可使用Lambda表达式来遍历集合元素。

下面程序示范了通过Iterator接口来遍历集合元素。

从上面代码中可以看出,Iterator仅用于遍历集合,Iterator本身并不提供盛装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。没有集合的Iterator仿佛无本之木,没有存在的价值。

上面程序中①行代码对迭代变量 book进行赋值,但当再次输出 books集合时,会看到集合里的元素没有任何改变。这就可以得到一个结论:当使用lterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响
        当使用Iterator迭代访问Collection集合元素时,Collection集合里的元素不能被改变只有通过lterator的remove()方法删除上一次next()方法返回的集合元素才可以;否则将会引发java.util.ConcurrentModificationException异常。下面程序示范了这一点。

上面程序中粗体字标识的代码位于Iterator迭代块内,也就是在Iterator迭代Collection集合过程中修改了Collection集合,所以程序将在运行时引发异常(这里是直接使用Collection集合调用的remove()方法,上个程序是使用迭代器调用的,不要搞混了)
        lterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

5. 使用Lambda表达式遍历lterator

Java 8起为Iterator新增了一个forEachRemaining(Consumer action)方法,该方法所需的Consumer参数同样也是函数式接口。当程序调用lterator的 forEachRemaining(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumer的 accept(T t)方法(该接口中唯一的抽象方法)。

!!!注意:别分上面的Iterable提供的forEach()方法搞混了

  • Iterable提供的forEach()方法Collection对象使用。
  • Iterator新增了一个forEachRemaining(Consumer action)方法,供Iterator使用。

如下程序示范了使用Lambda表达式来遍历集合元素。

上面程序中粗体字代码调用了lterator的forEachRemaining()方法来遍历集合元素,传给该方法的参数是一个Lambda表达式,该Lambda表达式的目标类型是Comsumer,因此上面代码也可用于遍历集合元素。

6. 使用foreach循环遍历集合元素

除可以使用Iterator接口迭代访问Collection集合里的元素之外,使用Java 5提供的foreach循环迭代访问集合元素更加便捷。如下程序示范了使用foreach循环来迭代访问集合元素。

上面代码使用foreach循环来迭代访问Collection集合里的元素更加简洁,这正是JDK 1.5的 foreach循环带来的优势。与使用Iterator接口迭代访问集合元素类似的是,foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在 foreach循环中修改迭代变量的值也没有任何实际意义。
        同样,当使用foreach循环迭代访问集合元素时,该集合也不能被改变,否则将引发ConcurrentModificationException异常。所以上面程序中①行代码处将引发该异常。

7. 使用Java 8新增的Predicate 操作集合

Java8起为Collection集合新增了一个removelf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,因此可使用Lambda表达式作为参数。
        如下程序示范了使用Predicate来过滤集合。

上面程序中粗体字代码调用了Collection集合的removelf()方法批量删除集合中符合条件的元素,程序传入一个Lambda表达式作为过滤条件:所有长度小于10的字符串元素都会被删除。编译、运行这段代码,可以看到如下输出:

 使用Predicate可以充分简化集合的运算,假设依然有上面程序所示的 books集合,如果程序有如下三个统计需求:

  • 统计书名中出现“疯狂”字符串的图书数量。
  • 统计书名中出现“Java”字符串的图书数量。
  • 统计书名长度大于10的图书数量。

此处只是一个假设,实际上还可能有更多的统计需求。如果采用传统的编程方式来完成这些需求,则需要执行三次循环但采用Predicate只需要一个方法即可。如下程示范了这种用法

上面程序先定义了一个calAll()方法,该方法将会使用Predicate判断每个集合元素是否符合特定条件——该条件将通过Predicate参数动态传入。从上面程序中三行粗体字代码可以看到,程序传入了三个Lambda表达式(其目标类型都是Predicate),这样calAll()方法就只会统计满足Predicate条件的图书。

8. 使用Java 8新增的Stream操作集合

Java8还新增了Stream、IntStream、LongStream、DoubleStream等流式API这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中, Stream是一个通用的流接口,而IntStream、LongStream,DoubleStream 则代表元素类型为int、long、double的流。
        Java 8还为上面每个流式API 提供了对应的 Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder 来创建对应的流。
        独立使用Stream的步骤如下:

  1. 使用Stream或XxxStream的 builder()类方法创建该Stream对应的 Builder
  2. 重复调用Builder的add()方法向该流中添加多个元素
  3. 调用Builder的 build()方法获取对应的Stream
  4. 调用Stream的聚集方法

在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream 提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。例如如下程序。

上面程序先创建了一个IntStream,接下来分别多次调用IntStream的聚集方法执行操作,这样即可获取该流的相关信息。注意:上面粗体字代码每次只能执行一行,因此需要把其他粗体字代码注释掉。
        Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”( intermediate),也可以是“末端的”( terminal)。

  • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流
  • 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法

除此之外,关于流的方法还有如下两个特征。

  • 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
  • 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素

下面简单介绍一下Stream常用的中间方法

  • filter(Predicate predicate):过滤 Stream中所有不符合predicate的元素
  • mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
  • peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
  • distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true)。这是一个有状态的方法。
  • sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
  • limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。

下面简单介绍一下Stream常用的末端方法。

  • forEach(Consumer action):遍历流中所有元素,对每个元素执行action。
  • toArray():将流中所有元素转换为一个数组。
  • reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素
  • min():返回流中所有元素的最小值。
  • max():返回流中所有元素的最大值。
  • count():返回流中所有元素的数量。
  • anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
  • allMatch(Predicate predicate):判断流中是否每个元素都符合Predicate条件。
  • noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。
  • findFirst():返回流中的第一个元素。
  • findAny():返回流中的任意一个元素。

除此之外,Java 8允许使用流式API来操作集合Collection接口提供了一个stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。
        例如,对于8.2.5节介绍的示例程序,该程序需要额外定义一个calAll()方法来遍历集合元素,然后依次对每个集合元素进行判断——这太麻烦了。如果使用Stream,即可直接对集合中所有元素进行批量操作。下面使用Stream来改写这个程序。

从上面程序中粗体字代码可以看出,程序只要调用Collection的 stream()方法即可返回该集合对应的Stream,接下来就可通过Stream提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码,这也是Stream编程带来的优势。
        上面程序中最后一段粗体字代码先调用Collection对象的 stream()方法将集合转换为Stream对象,然后调用Stream对象的mapTolnt()方法将其转换为IntStream——这个mapToInt()方法就是一个中间方法,因此程序可继续调用IntStream的forEach()方法来遍历流中的元素。

双冒号运算操作符是类方法的句柄,lambda 表达式的一种简写,这种简写的学名叫 eta-conversion 或者叫 η-conversion。

把 x -> System.out.println(x) 简化为 System.out::println 的过程称之为 eta-conversion

把 System.out::println 简化为 x -> System.out.println(x) 的过程称之为 eta-expansion

疯狂Java讲义(八)----第一部分相关推荐

  1. 疯狂Java讲义(六)----第四部分

    1.枚举类 在某些情况下,一个类的对象是有限而且固定的,比如季节类,它只有4个对象;再比如行星类,目前只有8个对象.这种实例有限而且固定的类,在Java里被称为枚举类. 在早期代码中,可能会直接使用简 ...

  2. 《疯狂Java讲义》学习笔记 第六章 面向对象(下)

    <疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...

  3. 《疯狂Java讲义》读书笔记5

    目录 Java的集合类 Inerator 接口 Set 集合 HashSet 类 LinkedHashSet 类 TreeSet 类 EnumSet 类 List 集合 ArrayList 和 Vec ...

  4. 疯狂java讲义第五章课后习题答案

    1.编写一个学生类,提供name.age.gender.phone.address.cmail成员变量,且为每个成员变量提供 seter.geter方法.为学生类提供默认的构造器和带所有成员变量的构造 ...

  5. 学习《疯狂Java讲义第4版》PDF+代码+课件+面试题分析

    学习java选择了<疯狂java讲义>最新的第四版,用了两个多月时间看完,一边阅读,一边敲代码.对Java有了不少新的认识.书中实例清晰明了,讲解也是娓娓道来.Java可以编写图形桌面系统 ...

  6. 《疯狂Java讲义》学习笔记 第六章 面向对象(下续)

    这里## 6.6 Java 9改进的接口 6.6.1 接口的概念 6.6.2 Java 9中接口的定义 6.6.3接口的继承 ==以下代码中纯在自己的很多错误== 6.6.4使用接口 6.6.5接口和 ...

  7. 疯狂java讲义 光盘_书单丨24岁的Java,到底有多美?

    Java诞生二十多年来尽管有坎坷有颠簸 但仍在计算核心层面占有重要地位 且多年来达成的辉煌仍无人能及 跟随本书单走进Java,一同领略Java之美 NO. 1 <码出高效:Java开发手册> ...

  8. 疯狂java讲义epub_为讲义创建EPUB

    疯狂java讲义epub I enjoy teaching people about CSS and in particular CSS Layout, over the last year I've ...

  9. 疯狂JAVA讲义---第十二章:Swing编程(五)进度条和滑动条

    http://blog.csdn.net/terryzero/article/details/3797782 疯狂JAVA讲义---第十二章:Swing编程(五)进度条和滑动条 标签: swing编程 ...

  10. java下载pdf6_疯狂java讲义第6版 电子版(pdf格式)

    疯狂java讲义第6版pdf是李刚打造的经典java编程图书的最新版,为了帮助更多的读者能真正掌握Java编程,感受到Java语言的魅力,领会到Java编程的快乐,作者根据多年来的Java授课经验,精 ...

最新文章

  1. IAAS、SAAS、PAAS
  2. 软件项目管理0820:项目经理的困境
  3. 11.28 限定某个目录禁止解析php 11.29 限制user_agent 11.30/11.31 php相关配置
  4. VisualSVN Server安装过程
  5. android studio不能更新代码,解决Android Studio 和 Android SDK Manager 无法在线更新的问题....
  6. C#中 ??、 ?、 ?: 、?.、?[ ]、:
  7. MySQL如何创建沙箱,沙箱环境搭建 - osc_y8w65yuq的个人空间 - OSCHINA - 中文开源技术交流社区...
  8. 让你的Android应用与外部元素互动起来
  9. 【面试】基于二叉树层次遍历相关问题的求解
  10. ApacheBench~网站性能测试工具
  11. poj2373 Dividing the Path (单调队列+dp)
  12. window10系统下载软件教程
  13. 用防火墙可以防御DDoS吗?
  14. 智能制造,从smart到intelligent
  15. 联想System X 3650M5 服务器装机问题记录
  16. 数据分析模型篇—波士顿矩阵
  17. @vaild权限校验框架配合java正则表达式
  18. php自动上传到onedrive,让iPhone里的照片自动上传备份到OneDrive
  19. kindle特性和硬件介绍(不介绍操作和使用方法)
  20. 【JavaWEB】项目实战-黑马面面

热门文章

  1. linux四剑客(核心基础)与正则表达式
  2. python pywin32 相同句柄_如何利用Python和win32编程避免重复性体力劳动(一)——开始、FindWindow和FindWindowEx...
  3. 探访广州黑人村,我好像来到非洲
  4. java 正则 g_Java中的正则表达式“ \ G”元字符
  5. WebRTC Native M96 SDK接口封装--startPreview开启视频预览
  6. Linux------磁盘与文件系统管理(3)
  7. 【java】之3种方式实现Object和Map之间的转换
  8. 全新C/C++发展路线,看这里
  9. Tenda U9无线网卡在ubuntu16.04下面的使用方法
  10. 正则表达式的替换技巧