Scala-和-Spark-大数据分析-一-

66 阅读47分钟

Scala 和 Spark 大数据分析(一)

原文:annas-archive.org/md5/39eecc62e023387ee8c22ca10d1a221a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据的持续增长与需要在这些数据上做出日益复杂决策的需求正在带来巨大的挑战,阻碍了组织通过传统分析方法及时获取洞察力。大数据领域已与这些框架紧密相关,其范围由这些框架能处理的内容来定义。无论你是在分析数百万访客的点击流以优化在线广告投放,还是在筛查数十亿笔交易以识别欺诈迹象,对于高级分析(如机器学习和图形处理)的需求——从大量数据中自动获取洞察力——比以往任何时候都更加迫切。

Apache Spark,作为大数据处理、分析和数据科学的事实标准,已被广泛应用于所有学术界和行业,它提供了机器学习和图形处理库,帮助企业利用高度可扩展的集群计算轻松解决复杂问题。Spark 的承诺是将这一过程进一步推进,让使用 Scala 编写分布式程序的感觉,像是为 Spark 编写常规程序一样。Spark 将在大幅提升 ETL 管道性能方面发挥巨大作用,减轻 MapReduce 程序员每天对 Hadoop 神明的哀鸣。

在本书中,我们使用 Spark 和 Scala 的组合,致力于将最先进的机器学习、图形处理、流处理和 SQL 等大数据分析技术带到 Spark,并探讨它们在 MLlib、ML、SQL、GraphX 等库中的应用。

我们从 Scala 开始,然后转向 Spark 部分,最后覆盖了 Spark 和 Scala 的大数据分析高级主题。在附录中,我们将看到如何将你的 Scala 知识扩展到 SparkR、PySpark、Apache Zeppelin 以及内存中的 Alluxio。本书并非按从头到尾的顺序阅读,跳到你正在尝试实现的目标或激发你兴趣的章节即可。

祝你阅读愉快!

本书内容

第一章,Scala 简介,将教授使用基于 Scala 的 Spark API 进行大数据分析。Spark 本身是用 Scala 编写的,因此作为起点,我们将简要介绍 Scala 的历史、用途以及如何在 Windows、Linux 和 Mac OS 上安装 Scala。接下来,我们将简要讨论 Scala 的 Web 框架。然后,我们将进行 Java 和 Scala 的对比分析。最后,我们将深入 Scala 编程,开始学习 Scala。

第二章,面向对象的 Scala,说明面向对象编程(OOP)范式提供了一种全新的抽象层。简而言之,本章讨论了 OOP 语言的一些最大优势:可发现性、模块化和可扩展性。特别地,我们将看到如何在 Scala 中处理变量;Scala 中的方法、类和对象;包和包对象;特质和特质线性化;以及 Java 互操作性。

第三章,函数式编程概念,展示了 Scala 中的函数式编程概念。更具体地,我们将学习几个主题,如为什么 Scala 是数据科学家的武器库,为什么学习 Spark 范式很重要,纯函数和高阶函数(HOFs)。还将展示一个使用高阶函数的实际用例。接着,我们将看到如何在不使用集合的情况下,通过 Scala 的标准库来处理高阶函数中的异常。最后,我们将了解函数式 Scala 如何影响对象的可变性。

第四章,集合 API,介绍了吸引大多数 Scala 用户的一个特性——集合 API。它功能强大且灵活,具有丰富的操作组合。我们还将展示 Scala 集合 API 的功能以及如何使用它来处理不同类型的数据,并解决各种各样的问题。在这一章中,我们将涵盖 Scala 集合 API、类型和层次结构、一些性能特性、Java 互操作性和 Scala 隐式转换。

第五章,应对大数据 - Spark 登场,概述了数据分析和大数据;我们看到了大数据所带来的挑战,分布式计算如何应对这些挑战,以及函数式编程提出的方法。我们介绍了 Google 的 MapReduce、Apache Hadoop,最后是 Apache Spark,看看它们是如何采用这一方法和技术的。我们将探讨 Apache Spark 的演变:为什么最初创建了 Apache Spark,它能为大数据分析和处理的挑战带来什么价值。

第六章,开始使用 Spark - REPL 和 RDDs,介绍了 Spark 的工作原理;接着,我们介绍了 RDDs,它是 Apache Spark 背后的基本抽象,并看到它们只是暴露类似 Scala 的 API 的分布式集合。我们将探讨 Apache Spark 的部署选项,并作为 Spark shell 在本地运行它。我们将学习 Apache Spark 的内部结构,RDD 是什么,RDD 的 DAG 和谱系,转换和操作。

第七章,特殊的 RDD 操作,重点介绍了如何根据不同需求定制 RDD,以及这些 RDD 如何提供新的功能(以及潜在的风险!)。此外,我们还将探讨 Spark 提供的其他有用对象,如广播变量和累加器。我们将学习聚合技术和数据洗牌。

第八章,引入一些结构 - SparkSQL,讲解了如何使用 Spark 作为 RDD 的高级抽象来分析结构化数据,以及如何通过 Spark SQL 的 API 简单且强大地查询结构化数据。此外,我们介绍了数据集,并对数据集、DataFrame 和 RDD 之间的差异进行了比较。我们还将学习如何通过 DataFrame API 进行连接操作和窗口函数,来进行复杂的数据分析。

第九章,流式处理 - Spark Streaming,带领你了解 Spark Streaming,以及如何利用 Spark API 处理数据流。此外,本章中,读者将学习如何通过实践示例,使用 Twitter 上的推文进行实时数据流处理。我们还将探讨与 Apache Kafka 的集成,实现实时处理。我们还会了解结构化流处理,能够为你的应用提供实时查询。

第十章,万物互联 - GraphX,本章中,我们将学习如何使用图模型来解决许多现实世界的问题。我们将通过 Facebook 举例,学习图论、Apache Spark 的图处理库 GraphX、VertexRDD 和 EdgeRDD、图操作符、aggregateMessages、TriangleCounting、Pregel API 以及 PageRank 算法等应用场景。

第十一章,学习机器学习 - Spark MLlib 和 ML,本章的目的是提供统计机器学习的概念性介绍。我们将重点介绍 Spark 的机器学习 API,称为 Spark MLlib 和 ML。接着,我们将讨论如何使用决策树和随机森林算法解决分类任务,以及使用线性回归算法解决回归问题。我们还将展示如何通过使用独热编码和降维算法,在训练分类模型前进行特征提取。此外,在后续部分,我们将通过一个逐步示例,展示如何开发基于协同过滤的电影推荐系统。

第十二章,高级机器学习最佳实践,提供了关于 Spark 机器学习的一些高级主题的理论和实践方面的内容。我们将了解如何使用网格搜索、交叉验证和超参数调优来优化机器学习模型的性能。在后续部分,我们将讨论如何使用 ALS 开发可扩展的推荐系统,ALS 是基于模型的推荐算法的一个例子。最后,我们还将展示一个主题建模应用,这是文本聚类技术的一个实例。

第十三章,我的名字是贝叶斯,朴素贝叶斯,指出大数据中的机器学习是一种激进的结合,已经在学术界和工业界的研究领域产生了巨大影响。大数据对机器学习、数据分析工具和算法带来了巨大的挑战,以帮助我们找到真正的价值。然而,基于这些庞大的数据集进行未来预测从未如此简单。考虑到这一挑战,本章将深入探讨机器学习,并研究如何使用一种简单而强大的方法构建可扩展的分类模型,涉及多项式分类、贝叶斯推理、朴素贝叶斯、决策树等概念,并对朴素贝叶斯与决策树进行比较分析。

第十四章,是时候整理一些秩序——使用 Spark MLlib 对数据进行聚类,帮助你了解 Spark 如何在集群模式下工作及其底层架构。在前几章中,我们已经看到如何使用不同的 Spark API 开发实际应用。最后,我们将看到如何在集群上部署一个完整的 Spark 应用,无论是使用预先存在的 Hadoop 安装还是没有。

第十五章,使用 Spark ML 进行文本分析,概述了使用 Spark ML 进行文本分析这一美妙领域。文本分析是机器学习中的一个广泛领域,应用场景非常广泛,如情感分析、聊天机器人、电子邮件垃圾邮件检测、自然语言处理等。我们将学习如何使用 Spark 进行文本分析,重点讨论文本分类的应用,使用一万条 Twitter 数据样本集进行分析。我们还将研究 LDA,这是一种流行的技术,用于从文档中生成主题,而无需深入了解实际文本,并将实现基于 Twitter 数据的文本分类,看看如何将这些内容结合起来。

第十六章,Spark 调优,深入探讨了 Apache Spark 的内部机制,指出尽管 Spark 在使用时让我们感觉就像在使用另一个 Scala 集合,但我们不应忘记 Spark 实际上运行在分布式系统中。因此,在本章中,我们将介绍如何监控 Spark 作业、Spark 配置、Spark 应用开发中的常见错误,以及一些优化技术。

第十七章,进入 ClusterLand - 在集群上部署 Spark,探讨了 Spark 在集群模式下的工作原理及其底层架构。我们将了解 Spark 在集群中的架构、Spark 生态系统和集群管理,以及如何在独立集群、Mesos、Yarn 和 AWS 集群上部署 Spark。我们还将了解如何在基于云的 AWS 集群上部署应用程序。

第十八章,测试和调试 Spark,解释了在分布式应用程序中进行测试的难度;然后,我们将介绍一些解决方法。我们将讲解如何在分布式环境中进行测试,以及如何测试和调试 Spark 应用程序。

第十九章,PySpark & SparkR,介绍了使用 R 和 Python 编写 Spark 代码的另外两个流行 API,即 PySpark 和 SparkR。特别是,我们将介绍如何开始使用 PySpark,并与 DataFrame API 和 UDF 进行交互,然后我们将使用 PySpark 进行一些数据分析。本章的第二部分介绍了如何开始使用 SparkR。我们还将了解如何使用 SparkR 进行数据处理和操作,如何使用 SparkR 处理 RDD 和 DataFrame,最后是使用 SparkR 进行一些数据可视化。

附录 A,通过 Alluxio 加速 Spark,展示了如何将 Alluxio 与 Spark 结合使用,以提高处理速度。Alluxio 是一个开源的分布式内存存储系统,对于提高跨平台应用程序的速度非常有用,包括 Apache Spark。在本章中,我们将探讨使用 Alluxio 的可能性,以及 Alluxio 的集成如何提供更高的性能,而不需要每次运行 Spark 任务时都将数据缓存到内存中。

附录 B,使用 Apache Zeppelin 进行互动数据分析,指出从数据科学的角度来看,数据分析的交互式可视化也非常重要。Apache Zeppelin 是一个基于 Web 的笔记本,用于交互式和大规模数据分析,支持多种后端和解释器。在本章中,我们将讨论如何使用 Apache Zeppelin 进行大规模数据分析,使用 Spark 作为后端的解释器。

本书所需的工具

所有示例均使用 Python 版本 2.7 和 3.5 在 Ubuntu Linux 64 位系统上实现,包括 TensorFlow 库版本 1.0.1。然而,在书中,我们仅展示了兼容 Python 2.7 的源代码。兼容 Python 3.5+的源代码可以从 Packt 仓库下载。您还需要以下 Python 模块(最好是最新版本):

  • Spark 2.0.0(或更高版本)

  • Hadoop 2.7(或更高版本)

  • Java(JDK 和 JRE)1.7+/1.8+

  • Scala 2.11.x(或更高版本)

  • Python 2.7+/3.4+

  • R 3.1+ 和 RStudio 1.0.143(或更高版本)

  • Eclipse Mars、Oxygen 或 Luna(最新版本)

  • Maven Eclipse 插件(2.9 或更高版本)

  • 用于 Eclipse 的 Maven 编译插件(2.3.2 或更高版本)

  • Maven assembly 插件用于 Eclipse(2.4.1 或更高版本)

操作系统: 推荐使用 Linux 发行版(包括 Debian、Ubuntu、Fedora、RHEL 和 CentOS),具体来说,推荐在 Ubuntu 上安装完整的 14.04(LTS)64 位(或更高版本)系统,VMWare Player 12 或 VirtualBox。你可以在 Windows(XP/7/8/10)或 Mac OS X(10.4.7 及更高版本)上运行 Spark 作业。

硬件配置: 推荐使用 Core i3、Core i5(推荐)或 Core i7 处理器(以获得最佳效果)。不过,多核处理器将提供更快的数据处理速度和更好的扩展性。你至少需要 8-16 GB 的内存(推荐)用于独立模式,至少需要 32 GB 内存用于单个虚拟机——集群模式则需要更高的内存。你还需要足够的存储空间来运行大型作业(具体取决于你处理的数据集大小),并且最好至少有 50 GB 的可用磁盘存储(用于独立模式的缺失和 SQL 数据仓库)。

本书适合谁阅读

任何希望通过利用 Spark 的强大功能来进行数据分析的人,都会发现本书极为有用。本书假设读者没有 Spark 或 Scala 的基础,虽然具备一定的编程经验(特别是其他 JVM 语言的经验)将有助于更快地掌握概念。Scala 在过去几年中一直在稳步增长,特别是在数据科学和分析领域。与 Scala 密切相关的是 Apache Spark,它是用 Scala 编写的,并且广泛应用于分析领域。本书将帮助你充分利用这两种工具的力量,理解大数据。

约定

本书中,你会发现一些文本样式,用于区分不同类型的信息。以下是这些样式的示例和它们的含义解释。文中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名的显示方式如下:“下一行代码读取链接并将其传递给 BeautifulSoup 函数。”

代码块设置如下:

package com.chapter11.SparkMachineLearning
import org.apache.spark.mllib.feature.StandardScalerModel
import org.apache.spark.mllib.linalg.{ Vector, Vectors }
import org.apache.spark.sql.{ DataFrame }
import org.apache.spark.sql.SparkSession

当我们希望将你的注意力引导到代码块的某一部分时,相关的行或项将以粗体显示:

val spark = SparkSession
                 .builder
                 .master("local[*]")
                 .config("spark.sql.warehouse.dir", "E:/Exp/")
                 .config("spark.kryoserializer.buffer.max", "1024m")
                 .appName("OneVsRestExample")        
           .getOrCreate()

任何命令行输入或输出将以以下方式呈现:

$./bin/spark-submit --class com.chapter11.RandomForestDemo \
--master spark://ip-172-31-21-153.us-west-2.compute:7077 \
--executor-memory 2G \
--total-executor-cores 2 \
file:///home/KMeans-0.0.1-SNAPSHOT.jar \
file:///home/mnist.bz2

新术语重要词汇 以粗体显示。你在屏幕上看到的词汇,例如在菜单或对话框中,文本中会以这种方式呈现:“点击下一步按钮会将你带到下一个界面。”

警告或重要注意事项将以这样的方式出现。

提示和技巧将以这种方式出现。

读者反馈

我们始终欢迎读者反馈。请告诉我们你对这本书的看法——你喜欢或不喜欢的部分。读者的反馈对我们非常重要,它帮助我们开发出你真正能从中受益的书籍。如果你有任何建议,请通过电子邮件feedback@packtpub.com联系我们,并在邮件主题中注明书名。如果你在某个领域有专业知识,并且有兴趣为书籍写作或贡献内容,请查看我们的作者指南:www.packtpub.com/authors

客户支持

既然你已经拥有了一本 Packt 书籍,我们为你准备了多项内容,帮助你最大化地利用这次购买。

下载示例代码

你可以从你在www.packtpub.com的账户中下载本书的示例代码文件。如果你是在其他地方购买的此书,你可以访问www.packtpub.com/support并注册以直接通过电子邮件接收文件。你可以按照以下步骤下载代码文件:

  1. 使用你的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名。

  5. 选择你希望下载代码文件的书籍。

  6. 从下拉菜单中选择你购买这本书的来源。

  7. 点击“代码下载”。

下载文件后,请确保使用最新版本的工具解压或提取文件夹:

  • 适用于 Windows 的 WinRAR / 7-Zip

  • 适用于 Mac 的 Zipeg / iZip / UnRarX

  • 适用于 Linux 的 7-Zip / PeaZip

本书的代码包也托管在 GitHub 上,地址为:github.com/PacktPublishing/Scala-and-Spark-for-Big-Data-Analytics。我们还有其他来自我们丰富书籍和视频目录的代码包,地址为:github.com/PacktPublishing/。快去看看吧!

下载本书的彩色图片

我们还为你提供了一份包含本书中截图/图表彩色图片的 PDF 文件。这些彩色图片将帮助你更好地理解输出结果中的变化。你可以从www.packtpub.com/sites/default/files/downloads/ScalaandSparkforBigDataAnalytics_ColorImages.pdf下载此文件。

勘误

尽管我们已尽一切努力确保内容的准确性,但错误还是可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——我们将非常感激你能向我们报告。这样做,你不仅可以帮助其他读者避免困扰,还能帮助我们改进本书的后续版本。如果你发现任何勘误,请访问www.packtpub.com/submit-errata报告,选择你的书籍,点击“勘误提交表单”链接,填写勘误详情。一旦你的勘误得到验证,提交将被接受,并且勘误将被上传到我们的网站或加入该书籍的现有勘误列表中。要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,在搜索框中输入书名,所需信息将显示在勘误部分。

盗版

互联网版权材料的盗版问题在所有媒体中都是一个持续存在的问题。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上发现我们作品的任何非法复制品,请立即向我们提供该位置地址或网站名称,以便我们采取相应措施。请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。感谢你在保护我们的作者和我们提供有价值内容的能力方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:Scala 简介

"我是 Scala。我是一种可扩展的、函数式的、面向对象的编程语言。我可以随你一起成长,你可以通过输入一行表达式并立即观察结果来与我互动"

  • Scala 引用

在过去几年中,Scala 逐渐崭露头角,得到了开发者和从业人员的广泛采用,特别是在数据科学和分析领域。另一方面,Apache Spark 是用 Scala 编写的,它是一个快速且通用的大规模数据处理引擎。Spark 的成功归因于多个因素:易用的 API、清晰的编程模型、优越的性能等。因此,自然地,Spark 对 Scala 提供了更多支持:相比于 Python 或 Java,Scala 有更多的 API;此外,新的 Scala API 在发布时通常会先于 Java、Python 和 R 的 API。

在我们开始使用 Spark 和 Scala 编写数据分析程序(第二部分)之前,我们首先会详细了解 Scala 的函数式编程概念、面向对象特性和 Scala 集合 API(第一部分)。作为起点,我们将在本章提供对 Scala 的简要介绍。我们将涵盖 Scala 的一些基本方面,包括它的历史和目的。然后,我们将了解如何在不同的平台上安装 Scala,包括 Windows、Linux 和 macOS,以便你可以在喜欢的编辑器和 IDE 中编写数据分析程序。本章后面,我们将对 Java 和 Scala 进行比较分析。最后,我们将通过一些示例深入探讨 Scala 编程。

简而言之,以下主题将被涵盖:

  • Scala 的历史与目的

  • 平台和编辑器

  • 安装和设置 Scala

  • Scala:可扩展的语言

  • 面向 Java 程序员的 Scala

  • 面向初学者的 Scala

  • 总结

Scala 的历史与目的

Scala 是一种通用编程语言,支持 函数式编程 和强大的 静态类型 系统。Scala 的源代码被编译为 Java 字节码,以便生成的可执行代码可以在 Java 虚拟机(JVM)上运行。

Martin Odersky 于 2001 年在洛桑联邦理工学院EPFL)开始设计 Scala。这是他在 Funnel 编程语言上的工作的扩展,Funnel 是一种使用函数式编程和 Petri 网的编程语言。Scala 的首次公开发布出现在 2004 年,但只支持 Java 平台。随后,在 2004 年 6 月,它也支持了 .NET 框架。

Scala 已经变得非常流行,并且得到了广泛的采用,因为它不仅支持面向对象编程范式,还融合了函数式编程的概念。此外,尽管 Scala 的符号运算符相较于 Java 来说不容易阅读,但大多数 Scala 代码相对简洁且易于阅读——例如,Java 的代码过于冗长。

就像其他编程语言一样,Scala 是为了特定的目的而提出和开发的。那么,问题是,Scala 为什么会被创造出来,它解决了哪些问题呢?为了回答这些问题,Odersky 在他的博客中说:

“Scala 的工作源自一项研究,旨在为组件软件开发更好的语言支持。我们希望通过 Scala 实验验证两个假设。首先,我们假设面向组件软件的编程语言需要具备可扩展性,意味着相同的概念能够描述从小到大的各个部分。因此,我们将重点放在抽象、组合和分解机制上,而不是添加一大堆原语,这些原语在某一层级上可能对组件有用,但在其他层级上则可能无效。第二,我们假设通过统一和泛化面向对象编程与函数式编程,编程语言可以为组件提供可扩展的支持。对于静态类型语言(如 Scala),这两种范式直到现在仍然大多是分开的。”

然而,Scala 还提供了模式匹配、高阶函数等特性,这些特性并非为了填补函数式编程(FP)与面向对象编程(OOP)之间的空白,而是因为它们是函数式编程的典型特征。为此,Scala 具有一些强大的模式匹配功能,这是一个基于演员模型的并发框架。此外,它还支持一阶和高阶函数。总的来说,“Scala”这个名字是“可扩展语言”(scalable language)的合成词,意味着它被设计成能够随着用户需求的增长而扩展。

平台与编辑器

Scala 运行在Java 虚拟机JVM)上,这使得 Scala 对于希望在代码中加入函数式编程风格的 Java 程序员来说,也是一个不错的选择。在选择编辑器时有很多选项。建议你花一些时间进行对比研究,因为对 IDE 的舒适使用是成功编程体验的关键因素之一。以下是一些可供选择的选项:

  • Scala IDE

  • Scala 插件用于 Eclipse

  • IntelliJ IDEA

  • Emacs

  • VIM

在 Eclipse 上支持 Scala 编程有多个优点,借助众多的 beta 插件。Eclipse 提供了一些令人兴奋的功能,如本地、远程以及高级调试功能,结合语义高亮和代码自动完成,适用于 Scala。你可以同样轻松地使用 Eclipse 进行 Java 和 Scala 应用程序的开发。然而,我也建议使用 Scala IDE(scala-ide.org/)——它是一个基于 Eclipse 的完整 Scala 编辑器,并通过一系列有趣的功能进行定制(例如,Scala 工作表、ScalaTest 支持、Scala 重构等)。

在我看来,第二个最佳选择是 IntelliJ IDEA。它的首个版本发布于 2001 年,是首批集成了高级代码导航和重构功能的 Java IDE 之一。根据 InfoWorld 的报告(见www.infoworld.com/article/2683534/development-environments/infoworld-review--top-java-programming-tools.html),在四大 Java 编程 IDE 中(即 Eclipse、IntelliJ IDEA、NetBeans 和 JDeveloper),IntelliJ 的测试得分为 8.5 分(满分 10 分),是最高的。

对应的评分在下图中展示:

图 1: 最佳的 Scala/Java 开发者 IDE

从前面的图示来看,你可能也有兴趣使用其他 IDE,如 NetBeans 和 JDeveloper。最终,选择权是开发者之间的一个永恒争论话题,这意味着最终的决定是你的。

安装并设置 Scala

如我们已经提到过,Scala 使用 JVM,因此请确保你的计算机上已经安装了 Java。如果没有,请参考下一节,介绍如何在 Ubuntu 上安装 Java。在本节中,首先我们将展示如何在 Ubuntu 上安装 Java 8。然后,我们将展示如何在 Windows、Mac OS 和 Linux 上安装 Scala。

安装 Java

为了简便起见,我们将展示如何在 Ubuntu 14.04 LTS 64 位机器上安装 Java 8。但对于 Windows 和 Mac OS,最好花些时间在 Google 上搜索相关安装方法。对于 Windows 用户的最低提示:请参阅这个链接了解详细信息:java.com/en/download/help/windows_manual_download.xml.

现在,让我们看看如何通过一步步的命令和说明在 Ubuntu 上安装 Java 8。首先,检查 Java 是否已经安装:

$ java -version 

如果返回无法在以下软件包中找到程序 java,则说明 Java 尚未安装。接下来,你需要执行以下命令来删除:

 $ sudo apt-get install default-jre 

这将安装Java 运行时环境JRE)。然而,如果你需要的是Java 开发工具包JDK),通常这是编译 Java 应用程序时在 Apache Ant、Apache Maven、Eclipse 和 IntelliJ IDEA 中所需要的。

Oracle JDK 是官方 JDK,但现在 Oracle 已不再为 Ubuntu 提供默认安装。你仍然可以通过 apt-get 进行安装。要安装任何版本,首先执行以下命令:

$ sudo apt-get install python-software-properties
$ sudo apt-get update
$ sudo add-apt-repository ppa:webupd8team/java
$ sudo apt-get update 

然后,根据你想安装的版本,执行以下命令之一:

$ sudo apt-get install oracle-java8-installer

安装完成后,别忘了设置 Java 的环境变量。只需应用以下命令(为了简便起见,我们假设 Java 安装在/usr/lib/jvm/java-8-oracle):

$ echo "export JAVA_HOME=/usr/lib/jvm/java-8-oracle" >> ~/.bashrc  
$ echo "export PATH=$PATH:$JAVA_HOME/bin" >> ~/.bashrc
$ source ~/.bashrc 

现在,让我们来看一下Java_HOME,如下所示:

$ echo $JAVA_HOME

你应该在终端上观察到以下结果:

 /usr/lib/jvm/java-8-oracle

现在,让我们通过执行以下命令来检查 Java 是否已成功安装(你可能会看到最新版本!):

$ java -version

你将看到以下输出:

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

太棒了!现在你已经在计算机上安装了 Java,一旦安装 Scala,你就可以开始编写 Scala 代码了。接下来的几节课将展示如何实现这一点。

Windows

这一部分将重点介绍如何在 Windows 7 上安装 Scala,但最终,无论你当前运行的是哪个版本的 Windows,都没有关系:

  1. 第一步是从官方网站下载 Scala 的压缩文件。你可以在 www.Scala-lang.org/download/all.html 找到该文件。在该页面的“其他资源”部分,你会找到可以用来安装 Scala 的归档文件列表。我们选择下载 Scala 2.11.8 的压缩文件,如下图所示:

图 2: Windows 上的 Scala 安装程序

  1. 下载完成后,解压文件并将其放入你喜欢的文件夹中。你还可以将文件重命名为 Scala,以便于导航。最后,需要为 Scala 创建一个 PATH 变量,以便在你的操作系统中全局识别 Scala。为此,请导航到计算机 | 属性,如下图所示:

图 3: Windows 上的环境变量标签

  1. 从那里选择环境变量,并获取 Scala 的 bin 文件夹的路径;然后,将其添加到 PATH 环境变量中。应用更改并点击确定,如下截图所示:

图 4: 为 Scala 添加环境变量

  1. 现在,你可以开始进行 Windows 安装了。打开命令提示符(CMD),然后输入 scala。如果安装成功,你应该会看到类似以下截图的输出:

图 5: 从“Scala shell”访问 Scala

Mac OS

现在是时候在你的 Mac 上安装 Scala 了。你可以通过多种方式在 Mac 上安装 Scala,这里我们将介绍其中两种方法:

使用 Homebrew 安装器

  1. 首先,检查你的系统是否已安装 Xcode,因为这一步骤需要它。你可以通过 Apple App Store 免费安装它。

  2. 接下来,你需要通过在终端中运行以下命令来安装 Homebrew

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

注意:前面的命令有时会被 Homebrew 开发者修改。如果命令无法正常工作,请访问 Homebrew 网站查找最新的命令:brew.sh/

  1. 现在,你准备好通过在终端中输入命令 brew install scala 来安装 Scala 了。

  2. 最后,你只需在终端中输入 Scala(第二行),就可以开始使用 Scala,并在终端中看到以下内容:

图 6: macOS 上的 Scala shell

手动安装

在手动安装 Scala 之前,选择您喜欢的 Scala 版本,并从 www.Scala-lang.org/download/ 下载该版本的 .tgz 文件 Scala-verion.tgz。下载完您喜欢的 Scala 版本后,按以下方式提取:

$ tar xvf scala-2.11.8.tgz

然后,按照如下方式将其移动到 /usr/local/share

$ sudo mv scala-2.11.8 /usr/local/share

现在,为了使安装永久生效,请执行以下命令:

$ echo "export SCALA_HOME=/usr/local/share/scala-2.11.8" >> ~/.bash_profile
$ echo "export PATH=$PATH: $SCALA_HOME/bin" >> ~/.bash_profile 

就这样。现在,让我们看看如何在像 Ubuntu 这样的 Linux 发行版上执行此操作,在下一小节中讲解。

Linux

在这一小节中,我们将向您展示在 Linux 的 Ubuntu 发行版上安装 Scala 的过程。开始之前,让我们检查 Scala 是否已正确安装。使用以下命令检查非常简单:

$ scala -version

如果 Scala 已安装在您的系统上,您应该会在终端看到以下消息:

Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL

请注意,在编写本安装过程时,我们使用的是 Scala 的最新版本,即 2.11.8。如果您的系统上没有安装 Scala,请确保在进行下一步之前安装它;您可以从 Scala 官网 www.scala-lang.org/download/ 下载最新版本的 Scala(为更清晰的查看,参考 图 2)。为了方便起见,让我们下载 Scala 2.11.8,方法如下:

$ cd Downloads/
$ wget https://downloads.lightbend.com/scala/2.11.8/scala-2.11.8.tgz

下载完成后,您应该能在下载文件夹中找到 Scala 的 tar 文件。

用户应首先通过以下命令进入 Download 目录:$ cd /Downloads/。请注意,下载文件夹的名称可能会根据系统所选语言而有所不同。

要从其位置提取 Scala tar 文件或更多内容,请输入以下命令。使用此命令,可以从终端提取 Scala tar 文件:

$ tar -xvzf scala-2.11.8.tgz

现在,通过输入以下命令或手动操作,将 Scala 分发包移动到用户的视角(例如,/usr/local/scala/share):

 $ sudo mv scala-2.11.8 /usr/local/share/

使用以下命令转到您的主目录:

$ cd ~

然后,使用以下命令设置 Scala home:

$ echo "export SCALA_HOME=/usr/local/share/scala-2.11.8" >> ~/.bashrc 
$ echo "export PATH=$PATH:$SCALA_HOME/bin" >> ~/.bashrc

然后,通过使用以下命令,使会话的更改永久生效:

$ source ~/.bashrc

安装完成后,您最好使用以下命令验证安装情况:

$ scala -version

如果 Scala 已成功配置在您的系统上,您应该会在终端看到以下消息:

Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL

做得好!现在,让我们通过在终端输入 scala 命令进入 Scala shell,如下图所示:

****图 7: Linux 上的 Scala shell(Ubuntu 发行版)

最后,您还可以使用 apt-get 命令安装 Scala,方法如下:

$ sudo apt-get install scala

此命令将下载最新版本的 Scala(即 2.12.x)。然而,Spark 尚不支持 Scala 2.12(至少在我们编写本章时是这样)。因此,我们建议使用之前描述的手动安装方法。

Scala:可扩展的语言

Scala 的名字来源于“可扩展语言”,因为 Scala 的概念能够很好地扩展到大规模的程序中。用其他语言编写的某些程序可能需要几十行代码,而在 Scala 中,你可以通过简洁有效的方式表达编程中的一般模式和概念。在本节中,我们将描述一些由 Odersky 为我们创建的 Scala 的激动人心的特性:

Scala 是面向对象的

Scala 是面向对象语言的一个很好的例子。要为你的对象定义类型或行为,你需要使用类和特征的概念,这将在下一章中详细解释。Scala 不支持直接的多重继承,但要实现这种结构,你需要使用 Scala 扩展的子类化基于混入的组合。这一点将在后续章节中讨论。

Scala 是函数式的

函数式编程将函数视为一等公民。在 Scala 中,这通过语法糖和扩展特征的对象(如Function2)来实现,但这就是 Scala 中实现函数式编程的方式。此外,Scala 定义了一种简单易用的方式来定义匿名 函数(没有名称的函数)。它还支持高阶函数,并允许嵌套函数**。**这些概念的语法将在后续章节中详细解释。

它还帮助你以不可变的方式编写代码,通过这种方式,你可以轻松地将其应用于带有同步和并发的并行编程。

Scala 是静态类型的

与其他静态类型语言(如 Pascal、Rust 等)不同,Scala 并不要求你提供冗余的类型信息。在大多数情况下,你不需要指定类型。最重要的是,你甚至不需要重复指定它们。

如果一个编程语言在编译时已知变量的类型,则称其为静态类型语言:这也意味着,作为程序员,你必须指定每个变量的类型。例如,Scala、Java、C、OCaml、Haskell、C++等都是静态类型语言。另一方面,Perl、Ruby、Python 等是动态类型语言,它们的类型与变量或字段无关,而是与运行时的值相关。

Scala 的静态类型特性确保所有类型检查都由编译器完成。Scala 这一极其强大的特性有助于你在代码执行之前,在非常早的阶段就能发现和捕捉大多数细微的 bug 和错误。

Scala 运行在 JVM 上

与 Java 一样,Scala 也编译成字节码,可以很容易地由 JVM 执行。这意味着 Scala 和 Java 的运行时平台是相同的,因为两者都会生成字节码作为编译输出。因此,你可以轻松地从 Java 切换到 Scala,并且也可以轻松地将两者集成,甚至在你的 Android 应用中使用 Scala 来增加函数式的风味;

请注意,虽然在 Scala 程序中使用 Java 代码非常容易,但反过来则非常困难,主要是因为 Scala 的语法糖。

就像 javac 命令将 Java 代码编译为字节码一样,Scala 也有 scalas 命令,它将 Scala 代码编译为字节码。

Scala 可以执行 Java 代码

如前所述,Scala 也可以用来执行你的 Java 代码。它不仅可以安装你的 Java 代码;它还允许你在 Scala 环境中使用所有可用的 Java SDK 类,甚至是你自己定义的类、项目和包。

Scala 可以进行并发和同步处理

其他语言中的一些程序可能需要数十行代码,而在 Scala 中,你能够以简洁有效的方式表达编程中的一般模式和概念。此外,它还帮助你以不可变的方式编写代码,进而轻松应用于并行性、同步和并发性。

针对 Java 程序员的 Scala

Scala 具有一组完全不同于 Java 的特性。在这一节中,我们将讨论其中的一些特性。本节内容将对那些来自 Java 背景或至少熟悉基本 Java 语法和语义的人有所帮助。

所有类型都是对象

如前所述,Scala 中的每个值都看起来像一个对象。这句话的意思是,一切看起来都像对象,但有些实际上并不是对象,你将在接下来的章节中看到对此的解释(例如,Scala 中引用类型和原始类型之间的区别依然存在,但它大部分被隐藏了)。举个例子,在 Scala 中,字符串会被隐式转换为字符集合,但在 Java 中则不是这样!

类型推导

如果你不熟悉这个术语,它其实就是编译时的类型推导。等等,这不是动态类型的意思吗?嗯,不是。注意我说的是类型推导;这与动态类型语言的做法截然不同,另外,它是在编译时完成的,而不是运行时。许多语言都内建了这一功能,但其实现因语言而异。刚开始时这可能会让人困惑,但通过代码示例会变得更清晰。让我们进入 Scala REPL 做些实验。

Scala REPL

Scala REPL 是一个强大的功能,它使得在 Scala shell 中编写 Scala 代码更加直接和简洁。REPL 代表 Read-Eval-Print-Loop,也叫做 交互式解释器。这意味着它是一个用于:

  1. 阅读你输入的表达式。

  2. 使用 Scala 编译器评估步骤 1 中的表达式。

  3. 输出步骤 2 中计算结果。

  4. 等待(循环)你输入更多表达式。

图 8: Scala REPL 示例 1

从图中可以明显看出,没有魔法,变量会在编译时自动推断为它们认为最合适的类型。如果你再仔细看,当我尝试声明时:

 i:Int = "hello"

然后,Scala shell 抛出一个错误,显示如下内容:

<console>:11: error: type mismatch;
  found   : String("hello")
  required: Int
        val i:Int = "hello"
                    ^

根据奥德斯基的说法,“将字符映射到 RichString 上的字符图应当返回一个 RichString,如 Scala REPL 中的以下交互所示”。前述语句可以通过以下代码行进行验证:

scala> "abc" map (x => (x + 1).toChar) 
res0: String = bcd

然而,如果有人将 Char 的方法应用于 Int 再应用于 String,会发生什么呢?在这种情况下,Scala 会进行转换,因为向量整数(也称为不可变集合)是 Scala 集合的特性,正如 图 9 所示。我们将在 第四章 中详细了解 Scala 集合 API,集合 API

"abc" map (x => (x + 1)) 
res1: scala.collection.immutable.IndexedSeq[Int] = Vector(98, 99, 100)

对象的静态方法和实例方法也可以使用。例如,如果你声明 x 为字符串 hello,然后尝试访问对象 x 的静态和实例方法,它们是可用的。在 Scala shell 中,输入 x 然后 .<tab>,你就会看到可用的方法:

scala> val x = "hello"
x: java.lang.String = hello
scala> x.re<tab>
reduce             reduceRight         replaceAll            reverse
reduceLeft         reduceRightOption   replaceAllLiterally   reverseIterator
reduceLeftOption   regionMatches       replaceFirst          reverseMap
reduceOption       replace             repr
scala> 

由于这一切都是通过反射动态完成的,即使是你刚刚定义的匿名类,也同样可以访问:

scala> val x = new AnyRef{def helloWord = "Hello, world!"}
x: AnyRef{def helloWord: String} = $anon$1@58065f0c
 scala> x.helloWord
 def helloWord: String
 scala> x.helloWord
 warning: there was one feature warning; re-run with -feature for details
 res0: String = Hello, world!

前面两个示例可以在 Scala shell 中展示,方法如下:

图 9: Scala REPL 示例 2

“所以结果是,map 会根据传递的函数参数的返回类型不同,产生不同的类型!”

  • 奥德斯基

嵌套函数

为什么你的编程语言需要支持嵌套函数?大多数时候,我们希望保持方法简洁,避免过大的函数。在 Java 中,典型的解决方案是将所有这些小函数定义在类级别,但任何其他方法都可以轻松引用和访问它们,即使它们是辅助方法。而在 Scala 中,情况不同,你可以在方法内部定义函数,从而防止外部访问这些函数:

def sum(vector: List[Int]): Int = {
  // Nested helper method (won't be accessed from outside this function
  def helper(acc: Int, remaining: List[Int]): Int = remaining match {
    case Nil => acc
    case _   => helper(acc + remaining.head, remaining.tail)
  }
  // Call the nested method
  helper(0, vector)
}

我们并不指望你理解这些代码片段,它们展示了 Scala 和 Java 之间的区别。

导入语句

在 Java 中,你只能在代码文件的顶部导入包,即在 package 声明之后。但在 Scala 中,情况不同;你几乎可以在源文件中的任何位置编写导入语句(例如,你甚至可以在类或方法内部写导入语句)。你只需要注意导入语句的作用域,因为它继承了类的成员或方法中局部变量的作用域。Scala 中的 _(下划线)用于通配符导入,类似于 Java 中使用的 *(星号):

// Import everything from the package math 
import math._

你还可以使用 { } 来表示来自同一父包的一组导入,只需一行代码。在 Java 中,你需要使用多行代码来实现:

// Import math.sin and math.cos
import math.{sin, cos}

与 Java 不同,Scala 没有静态导入的概念。换句话说,Scala 中不存在静态的概念。然而,作为开发者,显然,你可以使用常规导入语句导入对象的一个或多个成员。前面的示例已经展示了这一点,我们从名为 math 的包对象中导入了方法 sin 和 cos。为了举例说明,从 Java 程序员的角度来看,前面的代码片段可以定义如下:

import static java.lang.Math.sin;
import static java.lang.Math.cos;

Scala 另一个美妙之处在于,你可以重新命名导入的包。或者,你可以重新命名导入的包,以避免与具有相似成员的包发生类型冲突。以下语句在 Scala 中是有效的:

// Import Scala.collection.mutable.Map as MutableMap 
import Scala.collection.mutable.{Map => MutableMap}

最后,你可能希望排除某个包的成员,以避免冲突或出于其他目的。为此,你可以使用通配符来实现:

// Import everything from math, but hide cos 
import math.{cos => _, _}

运算符作为方法

值得一提的是,Scala 不支持运算符重载。你可能会认为,Scala 中根本没有运算符。

调用接受单个参数的方法的另一种语法是使用中缀语法。中缀语法就像你在 C++ 中做的那样,给你一种类似运算符重载的感觉。例如:

val x = 45
val y = 75

在以下情况下,+ 表示 Int 类中的一个方法。以下代码是非传统的调用方法语法:

val add1 = x.+(y)

更正式地说,可以使用中缀语法来做到这一点,如下所示:

val add2 = x + y

此外,你还可以利用中缀语法。然而,该方法只有一个参数,如下所示:

val my_result = List(3, 6, 15, 34, 76) contains 5

使用中缀语法时有一个特殊情况。也就是说,如果方法名以 :(冒号)结尾,那么调用或调用将是右关联的。这意味着方法在右侧参数上被调用,左侧的表达式作为参数,而不是反过来。例如,以下在 Scala 中是有效的:

val my_list = List(3, 6, 15, 34, 76)

前面的语句表示:my_list.+:(5) 而不是 5.+:(my_list),更正式地说是:;

val my_result = 5 +: my_list

现在,让我们看看前面的 Scala REPL 示例:

scala> val my_list = 5 +: List(3, 6, 15, 34, 76)
 my_list: List[Int] = List(5, 3, 6, 15, 34, 76)
scala> val my_result2 = 5+:my_list
 my_result2: List[Int] = List(5, 5, 3, 6, 15, 34, 76)
scala> println(my_result2)
 List(5, 5, 3, 6, 15, 34, 76)
scala>

除了上述内容之外,这里的运算符其实就是方法,因此它们可以像方法一样简单地被重写。

方法和参数列表

在 Scala 中,一个方法可以有多个参数列表,甚至可以没有参数列表。而在 Java 中,一个方法总是有一个参数列表,且可以有零个或多个参数。例如,在 Scala 中,以下是有效的方法定义(用 currie notation 编写),其中一个方法有两个参数列表:

def sum(x: Int)(y: Int) = x + y     

前面的该方法不能这样写:

def sum(x: Int, y: Int) = x + y

一个方法,比如 sum2,可以没有参数列表,如下所示:

def sum2 = sum(2) _

现在,你可以调用方法 add2,它返回一个接受一个参数的函数。然后,它用参数 5 调用该函数,如下所示:

val result = add2(5)

方法内的方法

有时候,你可能希望通过避免过长和复杂的方法来使你的应用程序和代码模块化。Scala 提供了这个功能,帮助你避免方法变得过于庞大,从而将它们拆分成几个更小的方法。

另一方面,Java 只允许你在类级别定义方法。例如,假设你有以下方法定义:

def main_method(xs: List[Int]): Int = {
  // This is the nested helper/auxiliary method
  def auxiliary_method(accu: Int, rest: List[Int]): Int = rest match {
    case Nil => accu
    case _   => auxiliary_method(accu + rest.head, rest.tail)
  }
}

现在,你可以按照如下方式调用嵌套的辅助方法:

auxiliary_method(0, xs)

考虑到以上内容,下面是一个完整的有效代码段:

def main_method(xs: List[Int]): Int = {
  // This is the nested helper/auxiliary method
  def auxiliary_method(accu: Int, rest: List[Int]): Int = rest match {
    case Nil => accu
    case _   => auxiliary_method(accu + rest.head, rest.tail)
  }
   auxiliary_method(0, xs)
}

Scala 中的构造函数

关于 Scala 有一个令人惊讶的地方是,Scala 类的主体本身就是一个构造函数。;然而,Scala 是这样做的;事实上,它的方式更加显式。之后,该类的一个新实例会被创建并执行。此外,你可以在类声明行中指定构造函数的参数。

因此,构造函数的参数可以从该类中定义的所有方法中访问。例如,以下类和构造函数定义在 Scala 中是有效的:

class Hello(name: String) {
  // Statement executed as part of the constructor
  println("New instance with name: " + name)
  // Method which accesses the constructor argument
  def sayHello = println("Hello, " + name + "!")
}

相应的 Java 类将是这样的:

public class Hello {
  private final String name;
  public Hello(String name) {
    System.out.println("New instance with name: " + name);
    this.name = name;
  }
  public void sayHello() {
    System.out.println("Hello, " + name + "!");
  }
}

对象代替静态方法

如前所述,Scala 中没有 static。你不能进行静态导入,也不能将静态方法添加到类中。在 Scala 中,当你定义一个与类同名且在同一源文件中的对象时,这个对象被称为该类的伴生对象*。*你在这个伴生对象中定义的函数,就像是 Java 中类的静态方法:

class HelloCity(CityName: String) {
  def sayHelloToCity = println("Hello, " + CityName + "!") 
}

这就是你如何为类 hello 定义伴生对象的方式:

object HelloCity { 
  // Factory method 
  def apply(CityName: String) = new Hello(CityName) 
}

相应的 Java 类将是这样的:

public class HelloCity { 
  private final String CityName; 
  public HelloCity(String CityName) { 
    this.CityName = CityName; 
  }
  public void sayHello() {
    System.out.println("Hello, " + CityName + "!"); 
  }
  public static HelloCity apply(String CityName) { 
    return new Hello(CityName); 
  } 
}

所以,在这个简单的类中,有很多冗余的内容,是不是?;在 Scala 中,apply 方法的处理方式不同,你可以找到一种特殊的快捷语法来调用它。这是调用该方法的常见方式:

val hello1 = Hello.apply("Dublin")

这是与之前相等的快捷语法:

 val hello2 = Hello("Dublin")

请注意,只有当你在代码中使用 apply 方法时,这才有效,因为 Scala 以不同的方式处理名为 apply 的方法。

特征

Scala 为你提供了一个很棒的功能,可以扩展和丰富类的行为。这些特征类似于接口,其中定义了函数原型或签名。因此,借助这些,你可以将来自不同特征的功能混合到类中,从而丰富了类的行为。那么,Scala 中的特征有什么好处呢?它们支持从这些特征中组成类,特征就像是构建块。像往常一样,让我们通过一个例子来看一下。下面是在 Java 中设置常规日志记录例程的方式:

请注意,尽管你可以混入任何数量的特征。此外,像 Java 一样,Scala 不支持多重继承。然而,在 Java 和 Scala 中,子类只能扩展一个父类。例如,在 Java 中:

class SomeClass {
  //First, to have to log for a class, you must initialize it
  final static Logger log = LoggerFactory.getLogger(this.getClass());
  ...
  //For logging to be efficient, you must always check, if logging level for current message is enabled                
  //BAD, you will waste execution time if the log level is an error, fatal, etc.
  log.debug("Some debug message");
  ...
  //GOOD, it saves execution time for something more useful
  if (log.isDebugEnabled()) { log.debug("Some debug message"); }
  //BUT looks clunky, and it's tiresome to write this construct every time you want to log something.
}

如需更详细的讨论,请参考这个网址:stackoverflow.com/questions/963492/in-log4j-does-checking-isdebugenabled-before-logging-improve-performance/963681#963681

然而,特质有所不同。始终检查日志级别是否启用是非常麻烦的。如果你能一次性写好这个例程,并且可以随时在任何类中重用,那该有多好。Scala 中的特质使这一切成为可能。例如:

trait Logging {
  lazy val log = LoggerFactory.getLogger(this.getClass.getName)     
  //Let's start with info level...
  ...
  //Debug level here...
  def debug() {
    if (log.isDebugEnabled) log.info(s"${msg}")
  }
  def debug(msg: => Any, throwable: => Throwable) {
    if (log.isDebugEnabled) log.info(s"${msg}", throwable)
  }
  ...
  //Repeat it for all log levels you want to use
}

如果你查看上面的代码,你会看到一个以 s 开头的字符串示例。这样,Scala 提供了一种机制,通过你的数据来创建字符串,这个机制叫做字符串插值

字符串插值允许你将变量引用直接嵌入到处理过的字符串字面量中。例如:

; ; ;scala> val name = "John Breslin"`

; ;scala> println(s"Hello, $name") ; // Hello, John Breslin

现在,我们可以用更传统的方式将高效的日志记录例程作为可重用的模块。为了在任何类中启用日志记录,我们只需要将 Logging 特质混入类中!太棒了!现在,只需这么做就能为你的类添加日志功能:

class SomeClass extends Logging {
  ...
  //With logging trait, no need for declaring a logger manually for every class
  //And now, your logging routine is either efficient and doesn't litter the code!

  log.debug("Some debug message")
  ...
}

甚至可以混合多个特质。例如,对于前面的 trait(即 Logging),你可以按以下顺序继续扩展:

trait Logging  {
  override def toString = "Logging "
}
class A extends Logging  {
  override def toString = "A->" + super.toString
}
trait B extends Logging  {
  override def toString = "B->" + super.toString
}
trait C extends Logging  {
  override def toString = "C->" + super.toString
}
class D extends A with B with C {
  override def toString = "D->" + super.toString
}

然而,需要注意的是,Scala 类可以一次性扩展多个特质,但 JVM 类只能扩展一个父类。

现在,要调用上述特质和类,只需在 Scala REPL 中使用 new D(),如下面的图示所示:

图 10:混合多个特质

到目前为止,本章内容进展顺利。接下来,我们将进入新的一部分,讨论一些初学者希望深入了解 Scala 编程领域的主题。

Scala 入门

在这一部分,你会发现我们假设你对任何以前的编程语言有基本的了解。如果 Scala 是你进入编程世界的第一步,那么你会发现网上有大量的材料,甚至有针对初学者的课程来解释 Scala。如前所述,网上有很多教程、视频和课程。

Coursera 上有一个专门的课程系列,包含这门课程:www.coursera.org/specializations/scala。这门课程由 Scala 的创建者马丁·奥德斯基(Martin Odersky)教授,采用一种较为学术的方式来教授函数式编程的基础。你将在通过解决编程作业的过程中学到很多 Scala 的知识。此外,这个课程系列还包括关于 Apache Spark 的课程。此外,Kojo(www.kogics.net/sf:kojo)是一个交互式学习环境,使用 Scala 编程来探索和玩转数学、艺术、音乐、动画和游戏。

你的第一行代码

作为第一个示例,我们将使用一个非常常见的Hello, world!程序,向你展示如何在不需要了解太多的情况下使用 Scala 及其工具。打开你喜欢的编辑器(本示例在 Windows 7 上运行,但在 Ubuntu 或 macOS 上也可以类似地运行),例如 Notepad++,然后输入以下代码行:

object HelloWorld {
  def main(args: Array[String]){ 
    println("Hello, world!")  
  } 
}

现在,将代码保存为一个名字,例如HelloWorld.scala,如下图所示:

**图 11:**使用 Notepad++保存你的第一个 Scala 源代码

让我们按如下方式编译源文件:

C:\>scalac HelloWorld.scala
 C:\>scala HelloWorld
 Hello, world!
 C:\>

我是 Hello world 程序,请好好解释我!

这个程序应该对任何有一些编程经验的人来说都很熟悉。它有一个主方法,打印字符串Hello, world!到你的控制台。接下来,为了查看我们是如何定义main函数的,我们使用了def main()这种奇怪的语法来定义它。def是 Scala 中的关键字,用于声明/定义方法,接下来我们将在下一章讲解更多关于方法和不同写法的内容。所以,我们为这个方法提供了一个Array[String]作为参数,它是一个字符串数组,可以用于程序的初始配置,并且可以省略。然后,我们使用了常见的println()方法,它接收一个字符串(或格式化字符串)并将其打印到控制台。一个简单的 Hello world 程序引出了许多学习话题,特别是三个:

● ; ; ;方法(将在后续章节中讲解)

● ; ; ;对象和类(将在后面的章节中讲解)

● ; ; ;类型推断——这就是 Scala 为什么是静态类型语言的原因——已在前面解释过。

交互式运行 Scala!

scala命令为你启动交互式 Shell,在这里你可以交互式地解释 Scala 表达式:

> scala
Welcome to Scala 2.11.8 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_121).
Type in expressions for evaluation. Or try :help.
scala>
scala> object HelloWorld {
 |   def main(args: Array[String]){
 |     println("Hello, world!")
 |   }
 | }
defined object HelloWorld
scala> HelloWorld.main(Array())
Hello, world!
scala>

快捷键:q代表内部 Shell 命令:quit,用于退出解释器。

编译它!

scalac命令类似于javac命令,编译一个或多个 Scala 源文件并生成字节码输出,然后可以在任何 Java 虚拟机上执行。要编译你的 Hello world 对象,请使用以下命令:

> scalac HelloWorld.scala

默认情况下,scalac会将类文件生成到当前工作目录。你可以使用-d选项指定一个不同的输出目录:

> scalac -d classes HelloWorld.scala

然而,请注意,在执行此命令之前,必须创建名为classes的目录。

使用 Scala 命令执行它

scala命令执行由解释器生成的字节码:

$ scala HelloWorld

Scala 允许我们指定命令选项,比如-classpath(别名-cp)选项:

$ scala -cp classes HelloWorld

在使用scala命令执行你的源文件之前,你应该有一个主方法作为应用程序的入口点。否则,你应该有一个扩展Trait Scala.AppObject,然后这个对象中的所有代码将由命令执行。以下是相同的Hello, world!示例,但使用了App特质:

#!/usr/bin/env Scala 
object HelloWorld extends App {  
  println("Hello, world!") 
}
HelloWorld.main(args)

前面的脚本可以直接从命令 Shell 中运行:

./script.sh

注意:我们假设此处文件 script.sh 已具有执行权限;

$ sudo chmod +x script.sh

然后,scala 命令的搜索路径在 $PATH 环境变量中指定。

摘要

本章中,您已经学习了 Scala 编程语言的基础知识、特性以及可用的编辑器。我们还简要讨论了 Scala 及其语法。我们展示了为 Scala 编程初学者提供的安装和设置指南。在本章后期,您学习了如何编写、编译和执行示例 Scala 代码。此外,针对来自 Java 背景的读者,我们还进行了 Scala 和 Java 的比较讨论。下面是 Scala 与 Python 的简短对比:

Scala 是静态类型的,而 Python 是动态类型的。Scala(大多数情况下)采用函数式编程范式,而 Python 则没有。Python 有独特的语法,缺少大部分括号,而 Scala(几乎总是)要求使用括号。在 Scala 中,几乎所有的东西都是表达式,而 Python 并非如此。然而,也有一些看似复杂的优点。类型的复杂性大多是可选的。其次,根据 stackoverflow.com/questions/1065720/what-is-the-purpose-of-scala-programming-language/5828684#5828684 提供的文档;Scala 编译器就像是免费的测试和文档,因为环路复杂度和代码行数不断增加。只要正确实现,Scala 就能在一致且连贯的 API 后面执行那些几乎不可能完成的操作。

在下一章中,我们将讨论如何通过了解 Scala 实现面向对象范式的基本知识,从而提升我们在构建模块化软件系统方面的经验。

第二章:Scala 的面向对象

"面向对象模型使得通过积累来构建程序变得容易。在实践中,这经常意味着它提供了一种结构化的方式来编写意大利面代码。"

  • 保罗·格雷厄姆

在上一章中,我们看到如何开始使用 Scala 进行编程。如果你正在编写我们在前一章中遵循的过程式程序,你可以通过创建过程或函数来强化代码的可重用性。然而,随着工作的继续,你的程序变得越来越长、越来越大、越来越复杂。在某一点上,你甚至可能没有其他更简单的方法在生产之前组织整个代码。

相反,面向对象编程OOP)范式提供了一个全新的抽象层次。通过定义类似属性和方法的 OOP 实体,你可以模块化你的代码。甚至可以通过继承或接口定义这些实体之间的关系。你还可以将功能相似的类分组在一起,例如辅助类;从而使你的项目突然感觉更加宽敞和可扩展。总之,OOP 语言的最大优势在于可发现性、模块化和可扩展性。

考虑到前述面向对象编程语言的特点,在本章中,我们将讨论 Scala 中的基本面向对象特性。简而言之,本章将涵盖以下主题:

  • Scala 中的变量

  • Scala 中的方法、类和对象

  • 包和包对象

  • 特征和特征线性化

  • Java 互操作性

然后,我们将讨论模式匹配,这是来自函数式编程概念的一个特性。此外,我们还将讨论 Scala 中的一些内置概念,如隐式和泛型。最后,我们将讨论一些广泛使用的构建工具,这些工具用于将我们的 Scala 应用程序构建为 JAR 文件。

Scala 中的变量

在深入讨论面向对象编程(OOP)特性之前,首先我们需要了解 Scala 中不同类型的变量和数据类型的细节。在 Scala 中声明变量,你需要使用 varval 关键字。Scala 中声明变量的形式语法如下:

val or var VariableName : DataType = Initial_Value

例如,让我们看看如何声明两个显式指定数据类型的变量:

var myVar : Int = 50
val myVal : String = "Hello World! I've started learning Scala."

你甚至可以只声明一个不指定 DataType 的变量。例如,让我们看看如何使用 valvar 来声明变量,如下所示:

var myVar = 50
val myVal = "Hello World! I've started learning Scala."

Scala 中有两种类型的变量:可变变量和不可变变量,可以如下定义:

  • 可变变量: 可以稍后更改其值的变量

  • 不可变变量: 一旦设置了值就不能更改其值的变量

一般来说,为了声明一个可变变量,会使用 var 关键字。另一方面,为了指定一个不可变变量,会使用 val 关键字。为了展示使用可变和不可变变量的示例,让我们考虑以下代码段:

package com.chapter3.OOP 
object VariablesDemo {
  def main(args: Array[String]) {
    var myVar : Int = 50 
    valmyVal : String = "Hello World! I've started learning Scala."  
    myVar = 90  
    myVal = "Hello world!"   
    println(myVar) 
    println(myVal) 
  } 
}

前面的代码在 myVar = 90 之前可以正常工作,因为 myVar 是一个可变变量。然而,如果你尝试更改不可变变量(即 myVal)的值,如前所示,IDE 会显示编译错误,提示“不能重新赋值给 val”,如下所示:

图 1: 不可变变量的重新赋值在 Scala 变量作用域中是不允许的

不用担心看到前面的代码中包含对象和方法!我们将在本章稍后讨论类、方法和对象,届时一切都会变得更加清晰。

在 Scala 中的变量,我们可以有三种不同的作用域,这取决于你声明它们的位置:

  • 字段: 这些是属于 Scala 代码中类的实例的变量。因此,字段可以从对象的每个方法内部访问。然而,取决于访问修饰符,字段也可以被其他类的实例访问。

如前所述,对象字段可以是可变的,也可以是不可变的(基于声明类型,使用 varval)。但它们不能同时是两者。

  • 方法参数: 这些是变量,当方法被调用时,可以用来传递值到方法内部。方法参数只能在方法内部访问。然而,传递的对象可能从外部被访问。

需要注意的是,方法的参数/参数总是不可变的,无论指定了什么关键字。

  • 局部变量: 这些变量是在方法内部声明的,只能在方法内部访问。然而,调用代码可以访问返回值。

引用与值的不可变性

根据前面的部分,val 用于声明不可变变量,那么我们能否更改这些变量的值?这是否与 Java 中的 final 关键字类似?为了帮助我们更好地理解这一点,我们将使用以下代码示例:

scala> var testVar = 10
testVar: Int = 10

scala> testVar = testVar + 10
testVar: Int = 20

scala> val testVal = 6
testVal: Int = 6

scala> testVal = testVal + 10
<console>:12: error: reassignment to val
 testVal = testVal + 10
 ^
scala>

如果你运行前面的代码,会发现编译时出现错误,提示你正在尝试重新赋值给 val 变量。通常,可变变量带来性能上的优势。原因是,这更接近计算机的行为方式,并且引入不可变值会强制计算机每次需要对特定实例进行更改(无论多么微小)时,都创建一个新的对象实例。

Scala 中的数据类型

如前所述,Scala 是一种 JVM 语言,因此与 Java 有很多相似之处。这些相似性之一就是数据类型;Scala 与 Java 共享相同的数据类型。简而言之,Scala 具有与 Java 相同的数据类型,内存占用和精度相同。如在第一章《Scala 简介》中提到,Scala 中几乎到处都有对象,所有数据类型都是对象,你可以在它们中调用方法,如下所示:

序号数据类型及描述
1Byte:8 位有符号值,范围从 -128 到 127
2Short:16 位有符号值,范围从 -32768 到 32767
3Int:32 位有符号值,范围从 -2147483648 到 2147483647
4Long:64 位有符号值,范围从 -9223372036854775808 到 9223372036854775807
5Float:32 位 IEEE 754 单精度浮点数
6Double:64 位 IEEE 754 双精度浮点数
7Char:16 位无符号 Unicode 字符,范围从 U+0000 到 U+FFFF
8String:一串字符
9Boolean:要么是字面值 true,要么是字面值 false
10Unit:对应于没有值
11Null:空引用
12Nothing:每个其他类型的子类型;不包括任何值
13Any:任何类型的超类型;任何对象的类型都是 Any
14AnyRef:任何引用类型的超类型

表格 1: Scala 数据类型、描述和范围

上面表格中列出的所有数据类型都是对象。然而,请注意,Scala 中没有像 Java 那样的原始数据类型。这意味着你可以在 IntLong 等类型上调用方法。

val myVal = 20
//use println method to print it to the console; you will also notice that if will be inferred as Int
println(myVal + 10)
val myVal = 40
println(myVal * "test")

现在,你可以开始玩这些变量了。让我们来看看如何初始化一个变量并进行类型注解。

变量初始化

在 Scala 中,初始化变量后再使用它是一种良好的实践。然而,需要注意的是,未初始化的变量不一定是 null(考虑像 IntLongDoubleChar 等类型),而已初始化的变量也不一定是非 null(例如,val s: String = null)。实际原因是:

  • 在 Scala 中,类型是从赋予的值中推断出来的。这意味着必须赋值,编译器才能推断出类型(如果代码是 val a,编译器无法推断出类型,因为没有赋值,编译器无法初始化它)。

  • 在 Scala 中,大多数时候你会使用 val。由于这些是不可变的,因此你不能先声明再初始化它们。

尽管 Scala 语言要求你在使用实例变量之前初始化它,但 Scala 并不会为你的变量提供默认值。相反,你必须手动设置它的值,可以使用通配符下划线 _,它类似于默认值,如下所示:

var name:String = _

与其使用像 val1val2 这样的名称,你可以定义自己的名称:

scala> val result = 6 * 5 + 8
result: Int = 38

你可以在后续的表达式中使用这些名称,如下所示:

scala> 0.5 * result
res0: Double = 19.0

类型注解

如果你使用 valvar 关键字声明一个变量,它的数据类型会根据你赋给这个变量的值自动推断。你也可以在声明时显式地指定变量的数据类型。

val myVal : Integer = 10

现在,让我们来看看在使用 Scala 中的变量和数据类型时,需要注意的其他方面。我们将看到如何使用类型说明和 lazy 变量。

类型说明

类型赋值用于告诉编译器你期望表达式的类型是哪些,从所有可能的有效类型中选择。因此,类型是有效的,前提是它符合现有的约束条件,比如变异性和类型声明,并且它要么是表达式所适用类型的某种类型,要么在作用域内有适用的转换。所以,从技术上讲,java.lang.String 扩展自 java.lang.Object,因此任何 String 也是一个 Object。例如:

scala> val s = "Ahmed Shadman" 
s: String = Ahmed Shadman

scala> val p = s:Object 
p: Object = Ahmed Shadman 

scala>

延迟值(Lazy val)

lazy val 的主要特征是绑定的表达式不会立即计算,而是在首次访问时计算。这就是 vallazy val 之间的主要区别。当第一次访问发生时,表达式会被计算,结果会绑定到标识符 lazy val 上。之后的访问不会再次计算,而是立即返回已存储的结果。我们来看一个有趣的例子:

scala> lazy val num = 1 / 0
num: Int = <lazy>

如果你查看 Scala REPL 中的前面的代码,你会注意到即使你把整数除以 0,代码也能运行得很好,不会抛出任何错误!让我们看一个更好的例子:

scala> val x = {println("x"); 20}
x
x: Int = 20

scala> x
res1: Int = 20
scala>

这段代码能正常工作,之后你可以在需要时访问变量 x 的值。这些只是使用 lazy val 概念的一些例子。有兴趣的读者可以访问这个页面以了解更多详细信息:blog.codecentric.de/en/2016/02/lazy-vals-scala-look-hood/.

Scala 中的方法、类和对象

在上一节中,我们学习了如何处理 Scala 变量、不同的数据类型及其可变性和不可变性,以及它们的使用范围。然而,在本节中,为了更好地理解面向对象编程(OOP)的概念,我们将涉及方法、对象和类。Scala 的这三个特性将帮助我们理解其面向对象的本质及其功能。

Scala 中的方法

在这一部分中,我们将讨论 Scala 中的方法。当你深入了解 Scala 时,你会发现有很多种方式可以定义方法。我们将通过以下几种方式来演示它们:

def min(x1:Int, x2:Int) : Int = {
  if (x1 < x2) x1 else x2
}

前面声明的方法接受两个变量并返回其中的最小值。在 Scala 中,所有方法都必须以 def 关键字开头,接着是该方法的名称。你可以选择不向方法传递任何参数,甚至可以选择不返回任何内容。你可能会想知道最小值是如何返回的,但我们稍后会讲到这个问题。此外,在 Scala 中,你可以在不使用大括号的情况下定义方法:

def min(x1:Int, x2:Int):Int= if (x1 < x2) x1 else x2

如果你的方法体很小,你可以像这样声明方法。否则,建议使用大括号以避免混淆。如前所述,如果需要,你可以选择不向方法传递任何参数:

def getPiValue(): Double = 3.14159

带有或不带括号的方法表明是否有副作用。此外,它与统一访问原则有着深刻的联系。因此,你也可以像下面这样避免使用大括号:

def getValueOfPi : Double = 3.14159

也有一些方法通过明确指定返回类型来返回值。例如:

def sayHello(person :String) = "Hello " + person + "!"

应该提到,前面的代码之所以能够正常工作,是因为 Scala 编译器能够推断返回类型,就像对待值和变量一样。

这将返回 Hello 和传入的姓名拼接在一起。例如:

scala> def sayHello(person :String) = "Hello " + person + "!"
sayHello: (person: String)String

scala> sayHello("Asif")
res2: String = Hello Asif!

scala>

Scala 中的返回值

在学习 Scala 方法如何返回值之前,让我们回顾一下 Scala 方法的结构:

def functionName ([list of parameters]) : [return type] = {
  function body
  value_to_return
}

对于前面的语法,返回类型可以是任何有效的 Scala 数据类型,参数列表将是以逗号分隔的变量列表,参数和返回类型是可选的。现在,让我们定义一个方法,添加两个正整数并返回结果,这个结果也是一个整数值:

scala> def addInt( x:Int, y:Int ) : Int = {
 |       var sum:Int = 0
 |       sum = x + y
 |       sum
 |    }
addInt: (x: Int, y: Int)Int

scala> addInt(20, 34)
res3: Int = 54

scala>

如果现在从 main() 方法调用前面的方法,并传入实际的值,比如 addInt(10, 30),方法将返回一个整数值的和,结果是 40。由于使用 return 关键字是可选的,Scala 编译器设计成当没有 return 关键字时,最后一个赋值会作为返回值。在这种情况下,更大的值将被返回:

scala> def max(x1 : Int , x2: Int)  = {
 |     if (x1>x2) x1 else x2
 | }
max: (x1: Int, x2: Int)Int

scala> max(12, 27)
res4: Int = 27

scala>

做得好!我们已经看到了如何使用变量以及如何在 Scala REPL 中声明方法。现在,接下来我们将看到如何将它们封装在 Scala 方法和类中。下一部分将讨论 Scala 对象。

Scala 中的类

类被视为蓝图,然后你实例化这个类以创建一个实际在内存中表示的对象。它们可以包含方法、值、变量、类型、对象、特征和类,这些统称为成员。让我们通过以下示例来演示:

class Animal {
  var animalName = null
  var animalAge = -1
  def setAnimalName (animalName:String)  {
    this.animalName = animalName
  }
  def setAnaimalAge (animalAge:Int) {
    this.animalAge = animalAge
  }
  def getAnimalName () : String = {
    animalName
  }
  def getAnimalAge () : Int = {
    animalAge
  }
}

我们有两个变量animalNameanimalAge,以及它们的 setter 和 getter。现在,如何使用它们来实现我们的目标呢?这就涉及到 Scala 对象的使用。接下来我们将讨论 Scala 对象,然后再回到我们的下一个讨论。

Scala 中的对象

Scala 中的 对象 的含义与传统面向对象编程中的对象略有不同,这一点需要说明。特别地,在面向对象编程中,对象是类的实例,而在 Scala 中,声明为对象的任何东西都不能被实例化!object 是 Scala 中的一个关键字。声明 Scala 对象的基本语法如下:

object <identifier> [extends <identifier>] [{ fields, methods, and classes }]

为了理解前面的语法,让我们回顾一下 hello world 程序:

object HelloWorld {
  def main(args : Array[String]){
    println("Hello world!")
  }
}

这个 hello world 示例与 Java 的类似。唯一的巨大区别是主方法不在类内部,而是放在对象内部。在 Scala 中,object 关键字可以表示两种不同的含义:

  • 正如在面向对象编程中,一个对象可以表示一个类的实例

  • 一个表示非常不同类型实例对象的关键字称为单例模式

单例和伴生对象

在这一小节中,我们将看到 Scala 中的单例对象与 Java 之间的对比分析。单例模式的核心思想是确保一个类只有一个实例可以存在。以下是 Java 中单例模式的一个示例:

public class DBConnection {
  private static DBConnection dbInstance;
  private DBConnection() {
  }
  public static DBConnection getInstance() {
    if (dbInstance == null) {
      dbInstance = new DBConnection();
    }
    return dbInstance;
  }
}

Scala 对象做了类似的事情,且由编译器很好地处理。由于只有一个实例,因此这里不能进行对象创建:

图 3: Scala 中的对象创建

伴生对象

当一个singleton object与类同名时,它被称为companion object。伴生对象必须在与类相同的源文件中定义。让我们通过以下示例来演示这一点:

class Animal {
  var animalName:String  = "notset"
  def setAnimalName(name: String) {
    animalName = name
  }
  def getAnimalName: String = {
    animalName
  }
  def isAnimalNameSet: Boolean = {
    if (getAnimalName == "notset") false else true
  }
}

以下是通过伴生对象调用方法的方式(最好使用相同的名称——即Animal):

object Animal{
  def main(args: Array[String]): Unit= {
    val obj: Animal = new Animal
    var flag:Boolean  = false        
    obj.setAnimalName("dog")
    flag = obj.isAnimalNameSet
    println(flag)  // prints true 

    obj.setAnimalName("notset")
    flag = obj.isAnimalNameSet
    println(flag)   // prints false     
  }
}

一个 Java 等效的实现会非常相似,如下所示:

public class Animal {
  public String animalName = "null";
  public void setAnimalName(String animalName) {
    this.animalName = animalName;
  }
  public String getAnimalName() {
    return animalName;
  }
  public boolean isAnimalNameSet() {
    if (getAnimalName() == "notset") {
      return false;
    } else {
      return true;
    }
  }

  public static void main(String[] args) {
    Animal obj = new Animal();
    boolean flag = false;         
    obj.setAnimalName("dog");
    flag = obj.isAnimalNameSet();
    System.out.println(flag);        

    obj.setAnimalName("notset");
    flag = obj.isAnimalNameSet();
    System.out.println(flag);
  }
}

干得好!到目前为止,我们已经了解了如何使用 Scala 的对象和类。然而,更重要的是如何使用方法来实现和解决数据分析问题。因此,接下来我们将简要地了解如何使用 Scala 方法。

object RunAnimalExample {
  val animalObj = new Animal
  println(animalObj.getAnimalName) //prints the initial name
  println(animalObj.getAnimalAge) //prints the initial age
  // Now try setting the values of animal name and age as follows:   
  animalObj.setAnimalName("dog") //setting animal name
  animalObj.setAnaimalAge(10) //seting animal age
  println(animalObj.getAnimalName) //prints the new name of the animal 
  println(animalObj.getAnimalAge) //Prints the new age of the animal
}

输出如下:

notset 
-1 
dog 
10

现在,让我们在下一节中简要概述一下 Scala 类的可访问性和可见性。

比较与对比:val 和 final

就像 Java 一样,final关键字在 Scala 中也存在,其作用与val关键字有些相似。为了区分 Scala 中的valfinal关键字,让我们声明一个简单的动物类,如下所示:

class Animal {
  val age = 2  
}

如第一章《Scala 简介》中所述,在列出 Scala 特性时,Scala 可以重载在 Java 中不存在的变量:

class Cat extends Animal{
  override val age = 3
  def printAge ={
    println(age)
  }
}

现在,在深入讨论之前,有必要快速讨论一下extends关键字。详细信息请参考下面的信息框。

使用 Scala 时,类是可扩展的。通过extends关键字的子类机制,可以通过继承给定的超类的所有成员,并定义额外的类成员,从而使类变得特化。让我们看一个示例,如下所示:

class Coordinate(xc: Int, yc: Int) {

val x: Int = xc

val y: Int = yc

def move(dx: Int, dy: Int): Coordinate = new Coordinate(x + dx, y + dy)

}

class ColorCoordinate(u: Int, v: Int, c: String) extends Coordinate(u, v) {

val color: String = c

def compareWith(pt: ColorCoordinate): Boolean = (pt.x == x) && (pt.y == y) && (pt.color == color)

override def move(dx: Int, dy: Int): ColorCoordinate = new ColorCoordinate(x + dy, y + dy, color)

}

然而,如果我们在 Animal 类中将年龄变量声明为 final,那么 Cat 类将无法重写它,会产生以下错误。对于这个 Animal 示例,你应该学会何时使用 final 关键字。让我们看一个例子:

scala> class Animal {
 |     final val age = 3
 | }
defined class Animal
scala> class Cat extends Animal {
 |     override val age = 5
 | }
<console>:13: error: overriding value age in class Animal of type Int(3);
 value age cannot override final member
 override val age = 5
 ^
scala>

干得好!为了实现最佳的封装 - 也称为信息隐藏 - 你应该始终用最少可行的可见性声明方法。在下一小节中,我们将学习类、伴生对象、包、子类和项目的访问和可见性如何工作。

访问和可见性

在这一小节中,我们将试图理解 Scala 变量在面向对象编程范式中的访问和可见性。让我们看看 Scala 中的访问修饰符。Scala 的一个类似的例子:

修饰符伴生对象子类项目
默认/无修饰符
受保护
私有

公共成员:与私有和受保护成员不同,对于公共成员不需要指定公共关键字。对于公共成员没有显式修饰符。这些成员可以从任何地方访问。例如:

class OuterClass { //Outer class
  class InnerClass {
    def printName() { println("My name is Asif Karim!") }

    class InnerMost { //Inner class
      printName() // OK
    }
  }
  (new InnerClass).printName() // OK because now printName() is public
}

私有成员:私有成员仅在包含成员定义的类或对象内可见。让我们看一个例子,如下所示:

package MyPackage {
  class SuperClass {
    private def printName() { println("Hello world, my name is Asif Karim!") }
  }   
  class SubClass extends SuperClass {
    printName() //ERROR
  }   
  class SubsubClass {
    (new SuperClass).printName() // Error: printName is not accessible
  }
}

受保护成员:受保护成员仅能从定义成员的类的子类中访问。让我们看一个例子,如下所示:

package MyPackage {
  class SuperClass {
    protected def printName() { println("Hello world, my name is Asif
                                         Karim!") }
  }   
  class SubClass extends SuperClass {
    printName()  //OK
  }   
  class SubsubClass {
    (new SuperClass).printName() // ERROR: printName is not accessible
  }
}

在 Scala 中,访问修饰符可以通过限定词进行增强。形如 private[X]protected[X] 的修饰符意味着访问是私有或受保护的,直到 X,其中 X 指定了封闭的包、类或单例对象。让我们看一个例子:

package Country {
  package Professional {
    class Executive {
      private[Professional] var jobTitle = "Big Data Engineer"
      private[Country] var friend = "Saroar Zahan" 
      protected[this] var secret = "Age"

      def getInfo(another : Executive) {
        println(another.jobTitle)
        println(another.friend)
        println(another.secret) //ERROR
        println(this.secret) // OK
      }
    }
  }
}

下面是关于前面代码段的简短说明:

  • 变量 jboTitle 将对 Professional 封闭包中的任何类可见

  • 变量 friend 将对 Country 封闭包中的任何类可见

  • 变量 secret 仅在实例方法(this)中对隐式对象可见

如果你看前面的例子,我们使用了关键字 package。然而,到目前为止我们还没有讨论这个。但别担心,本章后面将有专门的部分来讨论它。构造函数是任何面向对象编程语言的一个强大特性。Scala 也不例外。现在,让我们简要回顾一下构造函数。

构造函数

Scala 中构造函数的概念和用法与 C# 或 Java 中有些许不同。Scala 中有两种构造函数类型 - 主构造函数和辅助构造函数。主构造函数是类的主体,其参数列表紧跟在类名之后。

例如,以下代码段描述了如何在 Scala 中使用主构造函数:

class Animal (animalName:String, animalAge:Int) {
  def getAnimalName () : String = {
    animalName
  }
  def getAnimalAge () : Int = {
    animalAge
  }
}

现在,为了使用前面的构造函数,这个实现类似于之前的实现,唯一不同的是没有设置器和获取器。相反,我们可以像下面这样获取动物的名字和年龄:

object RunAnimalExample extends App{
  val animalObj = new animal("Cat",-1)
  println(animalObj.getAnimalName)
  println(animalObj.getAnimalAge)
}

参数是在类定义时提供的,用于表示构造函数。如果我们声明了构造函数,那么在不提供构造函数中指定的默认参数值的情况下,就无法创建类对象。此外,Scala 允许在不提供必要参数的情况下实例化对象:当所有构造函数参数都已定义默认值时,就会发生这种情况。

尽管在使用辅助构造函数时有一些限制,但我们可以自由地添加任意数量的附加辅助构造函数。一个辅助构造函数必须在其主体的第一行调用之前声明的另一个辅助构造函数,或者调用主构造函数。为了遵守这一规则,每个辅助构造函数将直接或间接地调用主构造函数。

例如,以下代码片段演示了在 Scala 中使用辅助构造函数的方式:

class Hello(primaryMessage: String, secondaryMessage: String) {
  def this(primaryMessage: String) = this(primaryMessage, "")
  // auxilary constructor
  def sayHello() = println(primaryMessage + secondaryMessage)
}
object Constructors {
  def main(args: Array[String]): Unit = {
    val hello = new Hello("Hello world!", " I'm in a trouble,
                          please help me out.")
    hello.sayHello()
  }
}

在之前的设置中,我们在主构造函数中包含了一个次要(即,第 2 个)消息。主构造函数将实例化一个新的Hello对象。方法sayHello()将打印拼接后的消息。

辅助构造函数:在 Scala 中,为 Scala 类定义一个或多个辅助构造函数,可以为类的使用者提供不同的创建对象实例的方式。将辅助构造函数定义为类中的方法,方法名为this。你可以定义多个辅助构造函数,但它们必须有不同的签名(参数列表)。此外,每个构造函数必须调用先前定义的一个构造函数。

现在让我们窥视一下 Scala 中另一个重要但相对较新的概念,称为特质。我们将在下一节中讨论这个概念。

Scala 中的特质

Scala 中的新特性之一是特质,它与 Java 中的接口概念非常相似,不同之处在于它还可以包含具体的方法。虽然 Java 8 已经支持这一特性。另一方面,特质是 Scala 中的一个新概念,但这个特性在面向对象编程中早已存在。因此,它们看起来像抽象类,只不过没有构造函数。

特质语法

你需要使用trait关键字来声明一个特质,后面应该跟上特质的名称和主体:

trait Animal {
  val age : Int
  val gender : String
  val origin : String
 }

扩展特质

为了扩展特质或类,你需要使用extend关键字。特质不能被实例化,因为它可能包含未实现的方法。因此,必须实现特质中的抽象成员:

trait Cat extends Animal{ }

值类不允许扩展特质。为了允许值类扩展特质,引入了通用特质,它扩展了Any。例如,假设我们有以下定义的特质:

trait EqualityChecking {
  def isEqual(x: Any): Boolean
  def isNotEqual(x: Any): Boolean = !isEqual(x)
}

现在,使用通用特性扩展前面提到的特性,我们可以按照以下代码段操作:

trait EqualityPrinter extends Any {
  def print(): Unit = println(this)
}

那么,Scala 中的抽象类和特性有什么区别呢?正如你所看到的,抽象类可以有构造函数参数、类型参数和多个参数。然而,Scala 中的特性只能有类型参数。

如果特性没有任何实现代码,那么它就是完全互操作的。此外,Scala 特性在 Scala 2.12 中与 Java 接口完全互操作。因为 Java 8 也允许在接口中实现方法。

特性也可能有其他用途,例如,抽象类可以扩展特性,或者如果需要,任何普通类(包括案例类)都可以扩展现有的特性。例如,抽象类也可以扩展特性:

abstract class Cat extends Animal { }

最后,一个普通的 Scala 类也可以扩展一个 Scala 特性。由于类是具体的(也就是说,可以创建实例),因此特性的抽象成员应该被实现。在下一节中,我们将讨论 Scala 代码的 Java 互操作性。现在,让我们深入探讨面向对象编程中的另一个重要概念——抽象类。我们将在下一节讨论这一内容。

抽象类

Scala 中的抽象类可以有构造函数参数和类型参数。Scala 中的抽象类与 Java 完全互操作。换句话说,可以从 Java 代码中调用它们,而不需要任何中间包装器。

那么,Scala 中的抽象类和特性有什么区别呢?正如你所看到的,抽象类可以有构造函数参数、类型参数和多个参数。然而,Scala 中的特性只能有类型参数。以下是一个简单的抽象类示例:

abstract class Animal(animalName:String = "notset") {
  //Method with definition/return type
  def getAnimalAge
  //Method with no definition with String return type
  def getAnimalGender : String
  //Explicit way of saying that no implementation is present
  def getAnimalOrigin () : String {} 
  //Method with its functionality implemented
  //Need not be implemented by subclasses, can be overridden if required
  def getAnimalName : String = {
    animalName
  }
}

为了让另一个类扩展这个类,我们需要实现之前未实现的方法getAnimalAgegetAnimalGendergetAnimalOrigin。对于getAnimalName,我们可以选择覆盖,也可以不覆盖,因为它的实现已经存在。

抽象类和override关键字

如果你想覆盖父类中的具体方法,那么override修饰符是必须的。然而,如果你实现的是抽象方法,严格来说,不一定需要添加override修饰符。Scala 使用override关键字来覆盖父类的方法。例如,假设你有以下抽象类和一个方法printContents()来在控制台打印你的消息:

abstract class MyWriter {
  var message: String = "null"
  def setMessage(message: String):Unit
  def printMessage():Unit
}

现在,添加前面抽象类的具体实现,将内容打印到控制台,如下所示:

class ConsolePrinter extends MyWriter {
  def setMessage(contents: String):Unit= {
    this.message = contents
  }

  def printMessage():Unit= {
    println(message)
  }
}

其次,如果你想创建一个特性来修改前面具体类的行为,如下所示:

trait lowerCase extends MyWriter {
  abstract override def setMessage(contents: String) = printMessage()
}

如果仔细查看前面的代码段,你会发现有两个修饰符(即抽象和覆盖)。现在,在前面的设置下,你可以通过以下方式使用前面的类:

val printer:ConsolePrinter = new ConsolePrinter()
printer.setMessage("Hello! world!")
printer.printMessage()

总结来说,我们可以在方法前添加 override 关键字,以使其按预期工作。

Scala 中的 case 类

case 类是一个可实例化的类,它包含几个自动生成的方法。它还包括一个自动生成的伴生对象,并且该对象有自己的自动生成方法。Scala 中 case 类的基本语法如下:

case class <identifier> ([var] <identifier>: <type>[, ... ])[extends <identifier>(<input parameters>)] [{ fields and methods }]

case 类可以进行模式匹配,并且已经实现了以下方法:hashCode(位置/作用域是类)、apply(位置/作用域是对象)、copy(位置/作用域是类)、equals(位置/作用域是类)、toString(位置/作用域是类)和unapply(位置/作用域是对象)。

和普通类一样,case 类会自动为构造函数参数定义 getter 方法。为了更好地理解前述功能或 case 类,让我们看看以下代码段:

package com.chapter3.OOP 
object CaseClass {
  def main(args: Array[String]) {
    case class Character(name: String, isHacker: Boolean) // defining a
                               class if a person is a computer hacker     
    //Nail is a hacker
    val nail = Character("Nail", true)     
    //Now let's return a copy of the instance with any requested changes
    val joyce = nail.copy(name = "Joyce")
    // Let's check if both Nail and Joyce are Hackers
    println(nail == joyce)    
    // Let's check if both Nail and Joyce equal
    println(nail.equals(joyce))        
    // Let's check if both Nail and Nail equal
    println(nail.equals(nail))    
    // Let's the hasing code for nail
    println(nail.hashCode())    
    // Let's the hasing code for nail
    println(nail)
    joyce match {
      case Character(x, true) => s"$x is a hacker"
      case Character(x, false) => s"$x is not a hacker"
    }
  }
}

上述代码生成的输出如下:

false 
false 
true 
-112671915 
Character(Nail,true) 
Joyce is a hacker

对于 REPL 和正则表达式匹配的输出,如果你执行上述代码(除去Objectmain方法),你应该能够看到更具交互性的输出,如下所示:

图 2: Scala REPL 中的 case 类

包和包对象

就像 Java 一样,包是一个特殊的容器或对象,它包含/定义了一组对象、类,甚至是包。每个 Scala 文件都有以下自动导入:

  • java.lang._

  • scala._

  • scala.Predef._

以下是基本导入的示例:

// import only one member of a package
import java.io.File
// Import all members in a specific package
import java.io._
// Import many members in a single import statement
import java.io.{File, IOException, FileNotFoundException}
// Import many members in a multiple import statement
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

你甚至可以在导入时重命名一个成员,以避免同名成员在不同包中的冲突。这种方法也叫做类的别名

import java.util.{List => UtilList}
import java.awt.{List => AwtList}
// In the code, you can use the alias that you have created
val list = new UtilList

正如在第一章《Scala 入门》中提到的,你也可以导入一个包的所有成员,但其中一些成员也被称为成员隐藏

import java.io.{File => _, _}

如果你在 REPL 中尝试此操作,它只会告诉编译器已定义类或对象的完整、规范名称:

package fo.ba
class Fo {
  override def toString = "I'm fo.ba.Fo"
}

你甚至可以使用大括号定义包的风格。你可以有一个单一的包和嵌套包,也就是说包内可以有包。例如,以下代码段定义了一个名为singlePackage的单一包,其中包含一个名为Test的类。Test 类则由一个名为toString()的方法组成。

package singlePack {
  class Test { override def toString = "I am SinglePack.Test" }
}

现在,你可以使包装变得嵌套。换句话说,你可以以嵌套方式拥有多个包。例如,下面的案例中,我们有两个包,分别是NestParentPackNestChildPack,每个包内都有自己的类。

package nestParentPack {
  class Test { override def toString = "I am NestParentPack.Test" }

  package nestChildPack {
    class TestChild { override def toString = "I am nestParentPack.nestChildPack.TestChild" }
  }
}

让我们创建一个新对象(命名为MainProgram),在其中调用我们刚刚定义的方法和类:

object MainProgram {
  def main(args: Array[String]): Unit = {
    println(new nestParentPack.Test())
    println(new nestParentPack.nestChildPack.TestChild())
  }
}

你可以在互联网上找到更多描述包和包对象复杂用例的示例。在下一节中,我们将讨论 Scala 代码的 Java 互操作性。

Java 互操作性

Java 是最流行的语言之一,许多程序员将 Java 编程作为他们进入编程世界的第一步。自 1995 年首次发布以来,Java 的流行度已经大幅提升。Java 之所以受欢迎,原因有很多。其中之一是其平台设计,任何 Java 代码都将编译为字节码,然后在 JVM 上运行。借助这一强大功能,Java 语言可以编写一次,随处运行。因此,Java 是一种跨平台语言。

此外,Java 在其社区中得到了广泛支持,并且有许多软件包可以帮助您使用这些软件包实现您的想法。接着是 Scala,它具有许多 Java 所缺乏的特性,如类型推断和可选的分号,不可变集合直接内置到 Scala 核心中,以及许多其他功能(详见第一章,Scala 简介)。Scala 也像 Java 一样运行在 JVM 上。

Scala 中的分号: 分号是完全可选的,当需要在单行上编写多行代码时,才需要它们。这可能是为什么编译器如果在行末放置分号则不会抱怨的原因:它被认为是一个代码片段,后面跟着一个空的代码片段,巧合地位于同一行。

正如你所看到的,Scala 和 Java 都运行在 JVM 上,因此在同一个程序中同时使用它们是有意义的,而不会受到编译器的投诉。让我们通过一个示例来演示这一点。考虑以下 Java 代码:

ArrayList<String> animals = new ArrayList<String>();
animals.add("cat");
animals.add("dog");
animals.add("rabbit");
for (String animal : animals) {
  System.out.println(animal);
}

为了在 Scala 中编写相同的代码,可以利用 Java 软件包。让我们借助使用诸如ArrayList等 Java 集合,将前面的示例翻译成 Scala:

import java.util.ArrayList
val animals = new ArrayList[String]
animals.add("cat")
animals.add("dog")
animals.add("rabbit")
for (animal <- animals) {
  println(animal)
}

在标准 Java 软件包上应用之前的混合,但是你想使用未包含在 Java 标准库中的库,甚至想使用自己的类。那么,你需要确保它们位于类路径中。

模式匹配

Scala 的广泛使用功能之一是模式匹配。每个模式匹配都有一组备选项,每个选项都以 case 关键字开头。每个备选项都有一个模式和表达式,如果模式匹配成功,则箭头符号=>将模式与表达式分隔开来。以下是一个示例,演示如何匹配整数:

object PatternMatchingDemo1 {
  def main(args: Array[String]) {
    println(matchInteger(3))
  }   
  def matchInteger(x: Int): String = x match {
    case 1 => "one"
    case 2 => "two"
    case _ => "greater than two"
  }
}

你可以通过将此文件保存为PatternMatchingDemo1.scala,然后使用以下命令来运行前述程序。只需使用以下命令:

>scalac Test.scala
>scala Test

您将会得到以下输出:

Greater than two

case 语句被用作将整数映射到字符串的函数。以下是另一个示例,用于匹配不同类型:

object PatternMatchingDemo2 {
  def main(args: Array[String]): Unit = {
    println(comparison("two"))
    println(comparison("test"))
    println(comparison(1))
  }
  def comparison(x: Any): Any = x match {
    case 1 => "one"
    case "five" => 5
    case _ => "nothing else"
  }
}

你可以通过与之前示例相同的方式运行此示例,并得到以下输出:

nothing else
nothing else
one

模式匹配是检查一个值是否符合某个模式的机制。成功的匹配还可以将一个值分解为其组成部分。它是 Java 中 switch 语句的更强大的版本,也可以用来代替一系列 if...else 语句。你可以通过查阅 Scala 官方文档了解更多关于模式匹配的内容(网址:www.scala-lang.org/files/archive/spec/2.11/08-pattern-matching.html)。

在接下来的章节中,我们将讨论 Scala 中的一个重要特性,它使得我们可以自动传递一个值,或者说,实现从一种类型到另一种类型的自动转换。

Scala 中的隐式

隐式是 Scala 引入的另一个令人兴奋且强大的特性,它可以指代两种不同的概念:

  • 一个可以自动传递的值

  • 从一种类型到另一种类型的自动转换

  • 它们可以用来扩展类的功能

实际的自动转换可以通过隐式 def 来完成,如下例所示(假设你在使用 Scala REPL):

scala> implicit def stringToInt(s: String) = s.toInt
stringToInt: (s: String)Int

现在,在我的作用域中有了前面的代码,我可以像这样做:

scala> def add(x:Int, y:Int) = x + y
add: (x: Int, y: Int)Int

scala> add(1, "2")
res5: Int = 3
scala>

即使传递给 add() 的参数之一是 String(而 add() 要求提供两个整数),只要隐式转换在作用域内,编译器就能自动将 String 转换为 Int。显然,这个特性可能会非常危险,因为它会使代码变得不易阅读;而且,一旦定义了隐式转换,编译器什么时候使用它、什么时候避免使用它就不容易判断了。

第一种类型的隐式是一个可以自动传递隐式参数的值。这些参数在调用方法时像任何普通参数一样传递,但 Scala 的编译器会尝试自动填充它们。如果 Scala 的编译器无法自动填充这些参数,它会报错。以下是演示第一种类型隐式的示例:

def add(implicit num: Int) = 2 + num

通过这个,你要求编译器在调用方法时如果没有提供 num 参数,就去查找隐式值。你可以像这样向编译器定义隐式值:

implicit val adder = 2

然后,我们可以像这样简单地调用函数:

add

在这里,没有传递任何参数,因此 Scala 的编译器会查找隐式值,即 2,然后返回 4 作为方法调用的输出。然而,很多其他选项也引发了类似的问题:

  • 方法可以同时包含显式和隐式参数吗?答案是可以的。我们来看一个 Scala REPL 中的示例:
 scala> def helloWold(implicit a: Int, b: String) = println(a, b)
 helloWold: (implicit a: Int, implicit b: String)Unit

 scala> val i = 2
 i: Int = 2

 scala> helloWorld(i, implicitly)
 (2,)

 scala>

  • 方法可以包含多个隐式参数吗?答案是可以的。我们来看一个 Scala REPL 中的示例:
 scala> def helloWold(implicit a: Int, b: String) = println(a, b)
 helloWold: (implicit a: Int, implicit b: String)Unit

 scala> helloWold(i, implicitly)
 (1,)

 scala>

  • 隐式参数可以显式地提供吗?答案是可以的。我们来看一个 Scala REPL 中的示例:
 scala> def helloWold(implicit a: Int, b: String) = println(a, b)
 helloWold: (implicit a: Int, implicit b: String)Unit

 scala> helloWold(20, "Hello world!")
 (20,Hello world!)
 scala>

如果在同一作用域中包含了多个隐式参数,隐式参数是如何解决的?是否有解决隐式参数的顺序?要了解这两个问题的答案,请参考此 URL:stackoverflow.com/questions/9530893/good-example-of-implicit-parameter-in-scala

在下一部分,我们将通过一些示例讨论 Scala 中的泛型。

Scala 中的泛型

泛型类是接受类型作为参数的类。它们对于集合类特别有用。泛型类可以用于日常数据结构的实现,如堆栈、队列、链表等。我们将看到一些示例。

定义一个泛型类

泛型类在方括号 [] 中接受一个类型作为参数。一种约定是使用字母 A 作为类型参数标识符,虽然可以使用任何参数名称。让我们看一个在 Scala REPL 中的最小示例,如下所示:

scala> class Stack[A] {
 |       private var elements: List[A] = Nil
 |       def push(x: A) { elements = x :: elements }
 |       def peek: A = elements.head
 |       def pop(): A = {
 |         val currentTop = peek
 |         elements = elements.tail
 |         currentTop
 |       }
 |     }
defined class Stack
scala>

前面实现的 Stack 类接受任何类型 A 作为参数。这意味着底层的列表 var elements: List[A] = Nil 只能存储类型为 A 的元素。程序 def push 只接受类型为 A 的对象(注意:elements = x :: elements 将元素重新赋值为一个通过将 x 添加到当前元素前面的新列表)。让我们来看一个如何使用前面类实现堆栈的示例:

object ScalaGenericsForStack {
  def main(args: Array[String]) {
    val stack = new Stack[Int]
    stack.push(1)
    stack.push(2)
    stack.push(3)
    stack.push(4)
    println(stack.pop) // prints 4
    println(stack.pop) // prints 3
    println(stack.pop) // prints 2
    println(stack.pop) // prints 1
  }
}

输出如下:

4
3
2
1

第二个用例也可以是实现一个链表。例如,如果 Scala 没有链表类,而你想自己编写,可以像这样编写基本功能:

class UsingGenericsForLinkedList[X] { // Create a user specific linked list to print heterogenous values
  private class NodeX {
    var next: Node[X] = _
    override def toString = elem.toString
  }

  private var head: Node[X] = _

  def add(elem: X) { //Add element in the linekd list
    val value = new Node(elem)
    value.next = head
    head = value
  }

  private def printNodes(value: Node[X]) { // prining value of the nodes
    if (value != null) {
      println(value)
      printNodes(value.next)
    }
  }
  def printAll() { printNodes(head) } //print all the node values at a time
}

现在,让我们看看如何使用前面的链表实现:

object UsingGenericsForLinkedList {
  def main(args: Array[String]) {
    // To create a list of integers with this class, first create an instance of it, with type Int:
    val ints = new UsingGenericsForLinkedList[Int]()
    // Then populate it with Int values:
    ints.add(1)
    ints.add(2)
    ints.add(3)
    ints.printAll()

    // Because the class uses a generic type, you can also create a LinkedList of String:
    val strings = new UsingGenericsForLinkedList[String]()
    strings.add("Salman Khan")
    strings.add("Xamir Khan")
    strings.add("Shah Rukh Khan")
    strings.printAll()

    // Or any other type such as Double to use:
    val doubles = new UsingGenericsForLinkedList[Double]()
    doubles.add(10.50)
    doubles.add(25.75)
    doubles.add(12.90)
    doubles.printAll()
  }
}

输出如下:

3
2
1
Shah Rukh Khan
Aamir Khan
Salman Khan
12.9
25.75
10.5

总结一下,在基本层面上,创建 Scala 中的泛型类就像在 Java 中创建泛型类一样,唯一的区别是方括号。好了!到目前为止,我们已经了解了一些开始使用面向对象编程语言 Scala 的基本特性。

尽管我们没有覆盖一些其他方面,但我们仍然认为你可以继续工作。在第一章,Scala 简介中,我们讨论了可用的 Scala 编辑器。在下一部分,我们将看到如何设置构建环境。更具体地说,我们将涵盖三种构建系统:Maven、SBT 和 Gradle。

SBT 和其他构建系统

对于任何企业软件项目,都需要使用构建工具。有许多构建工具可以选择,比如 Maven、Gradle、Ant 和 SBT。一个好的构建工具应该是让你专注于编码,而不是编译的复杂性。

使用 SBT 构建

在这里,我们将简要介绍 SBT。在继续之前,你需要通过其官方网站上适合你系统的安装方法来安装 SBT(URL: www.scala-sbt.org/release/docs/Setup.html)。

那么,让我们从 SBT 开始,演示如何在终端中使用 SBT。对于这个构建工具教程,我们假设你的源代码文件存放在一个目录中。你需要执行以下操作:

  1. 打开终端并使用 cd 命令切换到该目录,

  2. 创建一个名为 build.sbt 的构建文件。

  3. 然后,将以下内容填入该构建文件:

           name := "projectname-sbt"
           organization :="org.example"
           scalaVersion :="2.11.8"
           version := "0.0.1-SNAPSHOT"

让我们来看看这些行的含义:

  • name 定义了项目的名称。这个名称将在生成的 jar 文件中使用。

  • organization 是一个命名空间,用于防止具有相似名称的项目之间发生冲突。

  • scalaVersion 设置你希望构建的 Scala 版本。

  • Version 指定当前项目的构建版本,你可以使用 -SNAPSHOT 来标识尚未发布的版本。

创建完这个构建文件后,你需要在终端中运行 sbt 命令,然后会弹出一个以 > 开头的提示符。在这个提示符中,你可以输入 compile 来编译代码中的 Scala 或 Java 源文件。如果你的程序可以运行,也可以在 SBT 提示符中输入命令以运行程序。或者,你可以在 SBT 提示符中使用 package 命令来生成一个 .jar 文件,该文件会保存在一个名为 target 的子目录中。要了解更多关于 SBT 的信息和更复杂的示例,你可以参考 SBT 的官方网站。

Maven 与 Eclipse

使用 Eclipse 作为 Scala IDE,并以 Maven 作为构建工具是非常简单和直接的。在本节中,我们将通过截图演示如何在 Eclipse 和 Maven 中使用 Scala。为了在 Eclipse 中使用 Maven,你需要安装其插件,这些插件在不同版本的 Eclipse 中会有所不同。安装 Maven 插件后,你会发现它并不直接支持 Scala。为了让这个 Maven 插件支持 Scala 项目,我们需要安装一个名为 m2eclipse-scala 的连接器。

如果你在尝试向 Eclipse 添加新软件时粘贴这个 URL(alchim31.free.fr/m2e-scala/update-site),你会发现 Eclipse 能够识别这个 URL,并建议一些插件供你添加:

图 4: 在 Eclipse 中安装 Maven 插件以启用 Maven 构建

安装了 Maven 和 Scala 支持的连接器后,我们将创建一个新的 Scala Maven 项目。创建新的 Scala Maven 项目时,你需要导航到 New | Project | Other,然后选择 Maven Project。之后,选择具有 net.alchim31.maven 作为 Group Id 的选项:

图 5: 在 Eclipse 中创建一个 Scala Maven 项目

完成此选择后,你需要跟随向导输入必填项,如 Group Id 等。然后,点击 Finish,这样你就在工作空间中创建了第一个支持 Maven 的 Scala 项目。在项目结构中,你会发现一个名为 pom.xml 的文件,在那里你可以添加所有依赖项和其他内容。

有关如何向项目添加依赖项的更多信息,请参考此链接:docs.scala-lang.org/tutorials/scala-with-maven.html

作为本节的延续,我们将在接下来的章节中展示如何构建用 Scala 编写的 Spark 应用程序。

在 Eclipse 上使用 Gradle

Gradle Inc. 提供了适用于 Eclipse IDE 的 Gradle 工具和插件。该工具允许你在 Eclipse IDE 中创建和导入 Gradle 启用的项目。此外,它还允许你运行 Gradle 任务并监视任务的执行。

Eclipse 项目本身叫做 Buildship。该项目的源代码可以在 GitHub 上找到,地址为 github.com/eclipse/Buildship

在 Eclipse 上安装 Gradle 插件有两个选项。具体如下:

  • 通过 Eclipse Marketplace

  • 通过 Eclipse 更新管理器

首先,让我们看看如何使用 Marketplace 在 Eclipse 上安装 Buildship 插件以支持 Gradle 构建:Eclipse | 帮助 | Eclipse Marketplace:

图 6: 使用 Marketplace 在 Eclipse 上安装 Buildship 插件以支持 Gradle 构建

在 Eclipse 上安装 Gradle 插件的第二个选项是通过帮助 | 安装新软件... 菜单路径来安装 Gradle 工具,如下图所示:

图 7: 使用安装新软件方式在 Eclipse 上安装 Buildship 插件以支持 Gradle 构建

例如,以下 URL 可用于 Eclipse 4.6 (Neon) 版本:download.eclipse.org/releases/neon

一旦你通过之前描述的任意方法安装了 Gradle 插件,Eclipse Gradle 将帮助你设置基于 Scala 的 Gradle 项目:文件 | 新建 | 项目 | 选择向导 | Gradle | Gradle 项目。

****图 8: 在 Eclipse 上创建 Gradle 项目

现在,如果你点击 Next>,你将进入以下向导,用于为你的项目指定名称:

****图 9: 在 Eclipse 上创建 Gradle 项目并指定项目名称

最后,点击 Finish 按钮以创建项目。点击 Finish 按钮本质上会触发 Gradle init --type java-library 命令并导入该项目。然而,如果你希望在创建之前预览配置,点击 Next >,你将看到以下向导:

图 10: 创建前的配置预览

最后,您将看到在 Eclipse 上的以下项目结构。然而,我们将在后续章节中讨论如何使用 Maven、SBT 和 Gradle 构建 Spark 应用程序。原因是,在开始项目之前,更重要的是先学习 Scala 和 Spark。

图 11: 使用 Gradle 的 Eclipse 项目结构

在本节中,我们已经看到了三种构建系统,包括 SBT、Maven 和 Gradle。然而,在接下来的章节中,我将主要使用 Maven,因为它简单且更具代码兼容性。不过,在后续章节中,我们将使用 SBT 从您的 Spark 应用程序创建 JAR 文件。

概述

以合理的方式构建代码,使用类和特质,能够通过泛型增强代码的可重用性,并使用标准的广泛工具创建项目。通过了解 Scala 如何实现面向对象(OO)范式,来构建模块化的软件系统。在本章中,我们讨论了 Scala 中的基本面向对象特性,如类和对象、包和包对象、特质及特质线性化、Java 互操作性、模式匹配、隐式和泛型。最后,我们讨论了在 Eclipse 或任何其他 IDE 上构建 Spark 应用程序所需的 SBT 及其他构建系统。

在下一章中,我们将讨论什么是函数式编程,以及 Scala 如何支持它。我们将了解它的重要性以及使用函数式概念的优势。接下来,您将学习纯函数、高阶函数、Scala 集合基础(map、flatMap、filter)、for-comprehensions、单子处理,以及如何使用 Scala 的标准库将高阶函数扩展到集合之外。