轻松学模块化编程-一-

96 阅读46分钟

轻松学模块化编程(一)

原文:Modern Programming Made Easy

协议:CC BY-NC-SA 4.0

一、介绍

根据我的经验,学习如何编程(在典型的计算机科学课上)可能非常困难。课程趋向于枯燥、抽象和脱离“真实世界”的编码。由于技术进步的速度如此之快,计算机科学课程倾向于教授很快就过时和脱离现实的材料。我相信教授编程可以简单得多,我希望这本书能实现这个目标。

Note

整本书会有很多半开玩笑的幽默,但第一部分是严肃的。别担心,会好起来的。

解决问题

在你学习编程之前,这个任务可能看起来相当令人生畏,就像你爬山之前看着一座山一样。然而,随着时间的推移,你会意识到编程其实就是解决问题。

在您学习编码的过程中,就像生活中的许多事情一样,您会遇到许多障碍。你可能以前听过这句话,但它确实是真的:成功的道路是尝试,尝试,再尝试。最坚持不懈的人往往是最成功的人。

编程充满了反复试验。尽管随着时间的推移,事情会变得更容易,但你永远不会永远正确。所以,就像生活中的大多数事情一样,你必须耐心、勤奋和好奇才能成功。

关于这本书

这本书由几章组成,从最基本的概念开始。如果你已经理解了一个概念,你可以放心地进入下一章。虽然这本书主要讲述 Java,但它也涉及其他语言,如 Groovy、Scala 和 JavaScript,因此您将对所有编程语言的通用概念有更深入的理解。

img/435475_2_En_1_Figa_HTML.jpg 提示像这样的文本提供了你可能会发现有用的附加信息。

这种风格的文本通常会向好奇的读者提供额外的信息。

img/435475_2_En_1_Figc_HTML.jpg 警告诸如此类的文字告诫警惕的读者。许多人走上了计算机编程的道路。

img/435475_2_En_1_Figd_HTML.jpg 演习这是演习。我们在实践中学习得最好,所以尝试这些是很重要的。

二、要安装的软件

在你开始编程之前,你必须安装一些基本的工具。

Java/Groovy

对于 Java 和 Groovy,您必须安装以下软件:

  • JDK (Java 开发工具包),比如 OpenJDK 11。你可以按照 adoptopenjdk.net 的说明安装 OpenJDK。 1

  • IDE(集成开发环境),比如 NetBeans 11。

  • Groovy :类似 Java 的动态语言,运行在 JVM (Java 虚拟机)上。

img/435475_2_En_2_Figa_HTML.jpg安装 Java 和 NetBeans 11 或更高版本。下载并安装 Java JDK 和 NetBeans。 2 打开 NetBeans,选择文件➤新项目… ➤ Java with Gradle,Java Application。当被询问时,提供组“test”,版本“0.1”,以及包,如“com.gradleproject1”。单击“完成”,然后单击“确定”

安装 Groovy:进入 Groovy 网站并安装 Groovy。 3

尝试一下

安装 Groovy 后,应该用它来尝试编码。打开命令提示符(或终端),键入groovyConsole,然后按 Enter 键开始。

img/435475_2_En_2_Figb_HTML.jpggroovyConsole中,键入以下内容,然后按 Ctrl+r 运行代码。

1 打印“你好”

因为大多数 Java 代码都是有效的 Groovy 代码,所以您应该打开 Groovy 控制台,用它来尝试本书中的所有示例。

您也可以通过以下方式轻松尝试 JavaScript:

  • 只需打开网络浏览器,进入jsfiddle.net

其他人

一旦安装了上述组件,您最终应该安装以下组件:

  • Scala 4 :基于 JVM 构建的面向对象语言

  • Git 5 :版本控制程序

  • Maven 6 :模块化构建工具

如果你有心情,就安装这些吧。我等着。

要试用 Scala,安装后在命令提示符或终端中键入scala

GitHub 上的代码

这本书的很多代码可以在github.com/modernprog上找到。 7 你可以随时去那里跟着书走。

Footnotes 1

https://adoptopenjdk.net/installation.html

  2

https://netbeans.apache.org/download/index.html

  3

https://groovy.apache.org/download.html

  4

www.scala-lang.org/

  5

https://git-scm.com/

  6

https://maven.apache.org/

  7

https://github.com/modernprog

 

三、基础知识

在这一章中,我们将介绍 Java 和类似语言的基本语法。

编码术语

源文件是指人类可读的代码。二进制文件是指计算机可读的代码(编译后的代码)。在 Java 中,这种二进制代码被称为字节码,由 Java 虚拟机(JVM) 读取。

在 Java 中,源文件以.java结尾,二进制文件以.class结尾(也叫类文件)。你使用编译器编译源文件,它给你二进制文件或字节码。

在 Java 中,编译器被称为javac;在 Groovy 中是groovyc;而且是 Scala 中的scalac(看到这里的一个趋势?).所有这三种语言都可以编译成字节码,并在 JVM 上运行。字节码是一种通用格式,不管它是从哪种编程语言生成的。

但是,有些语言,比如 JavaScript,是不需要编译的。这些被称为解释语言。JavaScript 可以在你的浏览器(如 Firefox 或 Google Chrome)中运行,也可以在使用 Node.js 的服务器上运行,这是一个基于 Chrome 的 V8 JavaScript 引擎构建的 JavaScript 运行时。

原语和引用

Java 中的原语类型指的是存储数字的不同方式,具有实际意义。Java 中存在以下原语:

  • char:单个字符,如 A(字母 A )。

  • byte:从-128 到 127 的一个数(8 位 1 )。通常是一种存储或传输原始数据的方式。

  • short:16 位有符号整数。最多也就 32000 左右。

  • int:32 位有符号整数。它的最大值大约是 2 的 31 次方。

  • long:64 位有符号整数。最大为 2 的 63 次方。

  • float:32 位浮点数。这种格式以二为基数存储分数,不直接转换为十为基数的数字(数字通常是这样写的)。它可以用于模拟之类的事情。

  • double:类似于float,但为 64 位。

  • boolean:只有两个可能的值:truefalse(很像 1 位)。

img/435475_2_En_3_Figa_HTML.jpg详见 Java 教程—数据类型 2

Groovy, Scala, and JavaScript

Groovy 类型与 Java 类型非常相似。在 Scala 中,一切都是对象,所以原语是不存在的。但是,它们被替换为相应的值类型 ( IntLong等。).JavaScript 只有一种类型的数字,Number,类似于 Java 的float

一个变量是一个在内存中被名字引用的值。在 Java 中,你可以通过写类型和任何有效的名字来声明一个变量。例如,要创建一个名为price的整数,初始值为 100,请编写以下代码:

1  int price = 100;

Java 中其他类型的变量都是一个引用。它指向内存中的某个对象。这将在后面讨论。

在 Java 中,每个原语类型也有对应的类类型:Byte代表byteInteger代表intLong代表long,以此类推。使用类类型允许变量为null(意味着没有值)。但是,在处理大量值时,使用基元类型可以获得更好的性能。Java 可以自动包装和解包相应类中的原语(这叫做装箱拆箱)。

字符串/声明

字符串是一个字符列表(文本)。它是 Java(和大多数语言)中非常有用的内置类。要定义一个字符串,只需用引号将一些文本括起来。例如:

1   String hello = "Hello World!";

这里变量hello被赋予字符串"Hello World!"

在 Java 中,必须将变量的类型放在声明中。所以这里第一个字是String

在 Groovy 和 JavaScript 中,字符串也可以用单引号('hello')括起来。此外,每种语言中声明变量的方式也不同。Groovy 允许使用关键字def,而 JavaScript 和 Scala 使用var。Java 10 还引入了使用var来定义局部变量。例如:

1   def hello = "Hello Groovy!" //groovy
2   var hello = "Hello Scala/JS!" //Scala or JS

声明

Java 中几乎每条语句都必须以分号(;)结尾。在许多其他语言中,比如 Scala、Groovy 和 JavaScript,分号是可选的,但是在 Java 中,分号是必需的。就像每句话尾的句号帮助你理解书写的单词一样,分号帮助编译器理解代码。

按照惯例,我们通常将每个语句放在自己的行上,但这不是必需的,只要用分号分隔每个语句即可。

分配

赋值是一个非常重要的概念,但是对于初学者来说很难理解。然而,一旦你理解了它,你就会忘记它有多难学。

先来一个比喻。假设你想藏一些有价值的东西,比如一枚金币。你把它放在一个安全的地方,然后把地址写在一张纸上。这篇论文就像是对黄金的引用。你可以把它传来传去,甚至复制它,但是金子仍然在同一个地方,不会被复制。另一方面,任何有黄金参考的人都可以拿到它。这就是参考变量的工作方式。

让我们看一个例子:

1   String gold = "Au";
2   String a = gold;
3   String b = a;
4   b = "Br";

运行前面的代码后,golda是指字符串"Au",而b是指"Br"

类别和对象

一个是面向对象语言中代码的基本构建块。一个类通常定义状态和行为。下面的类被命名为SmallClass:

1   package com.example.mpme;
2   public class  SmallClass  {
3   }

在 Java 中,类名总是以大写字母开头。使用 CamelCase 构造名称是常见的做法。这意味着我们不使用空格(或其他任何东西)来分隔单词,而是大写每个单词的第一个字母。

第一行是类的包。包就像文件系统中的一个目录。事实上,在 Java 中,包必须与 Java 源文件的路径相匹配。因此,前面的类将位于源文件系统中的路径com/example/mpme/中。包有助于组织代码,并允许多个类具有相同的名称,只要它们在不同的包中。

一个对象是内存中一个类的实例。因为一个类中可以有多个值,所以一个类的实例将存储这些值。

img/435475_2_En_3_Figb_HTML.jpg创建类

  • 打开您的 IDE (NetBeans)。

  • 请注意文件系统中典型 Java 项目的常见组织结构:

    • src/main/java : Java 类

    • src/main/resources:非 Java 资源

    • src/test/java : Java 测试类

    • src/test/resources:非 Java 测试资源

  • 右键单击您的 Java 项目,然后选择“新建➤ Java 类”。在“类名”下填入“小班”。将“com.example.mpme”作为包名。

字段、属性和方法

接下来,您可能希望向您的类添加一些属性和方法。字段是与特定值或对象相关联的值。一个属性本质上是一个具有“getter”或“setter”或两者兼有的字段(一个 getter 获取属性值,一个 setter 设置属性值)。一个方法是一个类上的代码块,稍后可以被调用(在被调用之前它不会做任何事情)。

1   package  com.example.mpme;
2   public  class  SmallClass  {
3       String name; //field
4       String getName() {return  name;} //getter
5       void print() {System.out.println(name);} //method
6   }

在前面的代码中,name是一个属性,getName是一个称为 getter 的特殊方法,print是一个不返回任何内容的方法(这就是void的意思)。在这里,name被定义为字符串。System.out内置在 JDK 中,并链接到我们稍后讨论的“标准输出”,而println打印文本并在输出后追加一个换行符。

方法可以有参数(传入方法的值),修改类的字段,并且可以使用return语句有返回值(方法返回的值)。例如,将前面的方法print修改为:

1   public String print(String value) {
2     name = "you gave me " + value;
3     System.out.println(name);
4     return name;
5   }

该方法更改name字段,打印出新值,然后返回该值。通过定义类,然后执行以下命令,在 groovyConsole 中尝试这个新方法:

1  new SmallClass().print("you gave me dragons")

Groovy 类

Groovy 与 Java 极其相似,但总是默认为 public(我们将在后面的章节中讨论 public 的含义)。

1   package com.example.mpme;
2   class SmallClass {
3       String name //property
4       def print() { println(name) } //method
5   }

Groovy 还自动为属性提供“getter”和“setter”方法,所以编写getName方法是多余的。

JavaScript 原型

JavaScript 虽然有对象,但是没有class关键字(ECMAScript 2015 之前)。相反,它使用了一个叫做prototype的概念。例如,创建一个类可能如下所示:

1   function SmallClass() {}
2   SmallClass.prototype.name = "name"
3   SmallClass.prototype.print = function() { console.log(this.name) }

这里name是一个属性,print是一个方法。

Scala 类

Scala 的语法非常简洁,将类的属性放在括号中。此外,类型位于名称和冒号之后。例如:

1   class SmallClass(var name:String) {
2       def  print =  println(name)
3   }

创建新对象

在所有四种语言中,创建一个新对象都使用new关键字。例如:

1   sc = new  SmallClass();

评论

作为一个人,有时在源代码中为其他人——甚至为你自己——留下注释是很有用的。我们称这些笔记为评论。您可以这样写注释:

1   String gold = "Au"; // this is a comment
2   String a = gold; // a is now "Au"
3   String b = a; // b is now  "Au"
4   b = "Br";
5   /* b is now "Br".
6      this is still a comment */

最后两行演示了多行注释。所以,总而言之:

  • 两个正斜杠表示单行注释的开始。

  • 斜杠-星号标记多行注释的开始。

  • 星号斜杠标记多行注释的结束。

本书涵盖的所有语言的注释都是相同的。

摘要

在本章中,您学习了编程的基本概念:

  • 将源文件编译成二进制文件

  • 对象如何成为类的实例

  • 基本类型、引用和字符串

  • 字段、方法和属性

  • 变量赋值

  • 源代码注释如何工作

Footnotes 1

一位是最小的信息量。它对应于 1 或 0。

  2

https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

 

四、数学

(或者数学,如果你喜欢的话。)

加法、减法等。

你的朋友鲍勃刚刚被僵尸咬了一口,但却活着逃脱了。不幸的是,现在又多了一个僵尸需要担心。

1   zombies = zombies + 1;

有一种更短的方式来写同样的东西(这里我们时间很紧;僵尸来了)。

1   zombies += 1;

实际上,还有一种更短的方式来写它,它被称为增量操作符

1   zombies++;

幸运的是,还有一个递减运算符(在我们杀死僵尸时使用)。

1   zombie--;

加法和减法很简单,但是它们的表亲乘法和除法呢?幸运的是,这些符号在几乎所有编程语言中都是相同的:*/

1   int legs = zombies * 2;
2   int halfZombies = zombies / 2;

默认情况下,用 Java 编写的数字属于int类型。但是如果我们想处理不是整数的分数呢?

1   float oneThirdZombies = zombies / 3.0f;

不,3.0f不是错别字。f3变成了float。你可以使用小写或大写字母(D表示双倍;F意为浮动;而L表示长)。

这就是数学开始变得棘手的地方。为了接合浮点除法(记住从第章 3 ,float是一个不精确的数字),我们需要 3 是一个float。如果我们改为写zombies / 3,这将导致的整数除法,余数将丢失。比如32 / 3就是 10。

Modulo

你真的不需要理解模,但是如果你想,继续读下去。想象一下,你和三个哥们要攻击一群丧尸。你必须知道你们每个人要杀多少人,这样你们每个人才能杀死同等数量的僵尸。为此你要做整数除法。

1   int numberToKill = zombies / 4;

但你想知道会剩下多少。为此,你需要 ( %):

1   int leftOverZombies = zombies % 4;

这给了你将僵尸除以四的余数。

更复杂的数学

如果你想做除了加、减、乘、除和取模之外的任何事情,你必须使用java.lang.Math类。Math类是 Java 开发工具包(JDK) 的一部分,它总是作为核心 Java 的一部分可用。我们将会遇到许多这样的课程。

假设你想计算一个数的 2 次方。例如,如果你想估计指数增长的僵尸数量,如下:

1   double nextYearEstimate = Math.pow(numberOfZombies, 2.0d);

这种类型的方法被称为静态方法,因为它不需要对象实例。(别担心,稍后你会学到更多。)下面总结一下java.lang.Math中最常用的方法。

  • abs:返回一个值的绝对值

  • min:两个数的最小值

  • max:两个数的最大值

  • pow:返回第一个参数的第二次幂

  • sqrt:返回双精度值的正确舍入的正平方根

  • cos:返回一个角度的三角余弦值

  • sin:返回一个角度的三角正弦值

  • tan:返回一个角度的三角正切值

img/435475_2_En_4_Figa_HTML.jpg有关Math中所有方法的列表,请参见 Java 文档。 1

例如,如果你不熟悉正弦和余弦,当你想画一个圆时,它们就非常有用。如果你现在在电脑上,想了解更多关于正弦和余弦的知识,请看看本页末尾脚注中引用的这个动画 2 ,一直看下去,直到你理解了正弦波。

随机数

创建随机数最简单的方法是使用Math.random()方法。

random()方法返回一个大于或等于零且小于一的 double 值。

例如,要模拟掷骰子(以确定谁来处理下一波僵尸),请使用以下内容:

1   int roll = (int) (Math.random() * 6);

这将产生一个从 0 到 5 的随机数。然后我们可以加 1 得到数字 1 到 6。我们这里需要有(int)来将从random()返回的 double 转换成一个int——这叫做 casting

JavaScript 也有一个Math.random()方法。例如,要获得一个介于min(包含)和max(不包含)之间的随机整数,您可以执行以下操作(Math.floor返回小于或等于给定数字的最大整数):

1   Math.floor(Math.random() * (max - min)) + min;

然而,如果你想在 Java 中创建大量的随机数,最好使用java.util.Random类。它有几种不同的方法来创建随机数,包括

  • nextInt(int n):从 0 到n的随机数(不包括n

  • nextInt():均匀分布在所有可能的int值上的随机数

  • nextLong():同nextInt(),但long

  • nextFloat():同nextInt(),但float

  • nextDouble():同nextInt(),但double

  • nextBoolean():真或假

  • nextBytes(byte[] bytes):用随机字节填充给定的字节数组

您必须首先创建一个新的Random对象,然后您可以使用它来创建随机数,如下所示:

1   Random randy = new Random();
2   int roll6 = randy.nextInt(6) + 1; // 1 to 6
3   int roll12 = randy.nextInt(12) + 1; // 1 to 12

现在你可以创建随机数,并用它们做数学运算。万岁!

img/435475_2_En_4_Figc_HTML.jpg种子如果你用一个种子创建一个Random(例如new Random(1234)),当给定相同的种子时,它将总是生成相同的随机数序列。

摘要

在本章中,您学习了如何编写数学程序,例如

  • 如何加、减、乘、除和取模

  • 在 Java 中使用Math

  • 创建随机数

Footnotes 1

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Math.html

  2

https://upload.wikimedia.org/wikipedia/commons/0/08/Sine_curve_drawing_animation.gif

 

五、数组、列表、集合和映射

到目前为止,我只讨论了单个值,但是在编程中,您经常需要处理大量的值集合。为此,我们在语言中内置了许多数据结构。对于 Java、Groovy、Scala 甚至 JavaScript 来说,这些都是类似的。

数组

一个数组是一个固定大小的数据值集合。

在 Java 中,通过向类型追加[]来声明数组类型。例如,int的数组被定义为int[]

1   int[] vampireAges = new  int[10]; // ten vampires

设置和访问数组中的值使用相同的方括号语法,如下所示:

1   vampireAges[0] = 1565; // set age of first vampire
2    int age = vampireAges[0]  // get age of first vampire

如你所见,数组的第一个索引是零。编程时事情往往从零开始;试着记住这个。

这里有一个有用的比喻:第一个引发疾病爆发(例如僵尸爆发)的人被称为零号病人,而不是一号病人。患者一是第被感染的人。

这也意味着数组的最后一个索引总是比数组的大小小 1。列表也是如此。

1   vampireAges[9] = 442; // last vampire

您可以像访问任何其他变量一样重新分配和访问数组值。

1   int year = 2020; // current year
2   int firstVampBornYear = year - vampireAges[0];

也可以声明对象数组。在这种情况下,数组的每个元素都是对内存中一个对象的引用。例如,下面将声明一个由Vampire对象组成的数组:

1   Vampire[] vampires = new Vampire[10]; // Vampire array with length 10

你也可以直接填充你的数组,比如你正在创建一个字符串数组。

1   String[] names = {"Dracula", "Edward"};

JavaScript 中的Array对象更像一个 Java List。Java 数组是一种比较低级的结构,仅仅是为了提高性能。在 Groovy 3 中,支持 Java 风格的数组声明。在以前的版本中,你必须使用List风格,我们将在下面介绍。

在 Scala 中,您可以像下面这样定义一个Array:

1   var names = new ArrayString // size of 2 without values
2   var names = Array("Dracula", "Edward") // size of 2 with values

列表

当然,我们并不总是知道我们需要在一个数组中存储多少个元素。出于这个原因(以及许多其他原因),程序员发明了List,一个可调整大小的有序元素集合。

在 Java 中,你用下面的方法创建List<E>:

1   List<Vampire> vampires = new ArrayList<>();

第一个尖括号(<>)之间的类定义了列表的通用类型——可以放入列表的内容(在本例中是Vampire)。第二组尖括号可以是空的,因为 Java 可以从表达式的左边推断出泛型类型。你现在可以整天将吸血鬼添加到这个列表中,它会根据需要在后台扩展。

你这样补充到List:

1   vampires.add(new  Vampire("Count Dracula", 1897));

List还包含了大量其他有用的方法,包括

  • size():获取List的大小

  • get(int index):获取该索引处的值

  • remove(int index):删除该索引处的值

  • remove(Object o):删除给定的对象

  • isEmpty():仅当List为空时返回true

  • clear():删除List中的所有值

    img/435475_2_En_5_Figb_HTML.jpg在 Java 中,List是一个接口(我们将在第八章深入讨论接口),有许多不同的实现,但这里有两个:

  • java.util.ArrayList

  • java.util.LinkedList

    您应该关心的唯一区别是,一般来说,LinkedList在任意索引处插入值时增长更快,而ArrayListget()方法在任意索引处更快。

你将在下一章学习如何循环遍历列表、数组和集合(以及“循环”是什么意思)。现在,只需要知道列表是编程中的一个基本概念。

Groovy 列表

Groovy 有一个更简单的创建列表的语法,内置在语言中。

1   def list = []
2   list.add(new Vampire("Count Dracula", 1897))
3   // or
4   list << new Vampire("Count Dracula", 1897)

Scala 列表

在 Scala 中,你创建一个列表并以稍微不同的方式添加到列表中:

1   var list = List[Vampire]();
2   list :+ new  Vampire("Count Dracula", 1897)

此外,这实际上创建了一个新列表,而不是修改现有列表(出于性能原因,它在后台重用了现有列表)。这是因为 Scala 中的默认List不可变,这意味着它不能被修改(默认实现是不可变的,但是您可以使用来自scala.collection.mutable包的可变实现)。虽然这看起来很奇怪,但是结合功能编程,它使得并行编程(多处理器编程)变得更加容易,我们将在第十章中看到。

JavaScript 数组

如前所述,JavaScript 使用Array 1 而不是List。此外,由于 JavaScript 不是严格的类型化的,所以一个Array总是可以保存任何类型的对象。

数组可以像 Groovy 中的列表一样创建。然而,可用的方法有些不同。比如用push代替add

1   def array = []
2   array.push(new Vampire("Count Dracula", 1897))

也可以声明Array的初始值。例如,以下两行是等效的:

1   def years = [1666, 1680, 1722]
2   def years = new Array(1666, 1680, 1722)

更令人困惑的是,JavaScript 中的数组可以像 Java 数组一样被访问。例如:

1   def firstYear = years[0]
2   def size = years.length

设置

Set<E>很像List<E>,但是每个值或对象在Set中只能有一个实例,而在以前的集合中,可以有重复。

Set有很多和List一样的方法。然而,它遗漏了使用索引的方法,因为Set不一定是任何特定的顺序。

1   Set<String> dragons = new HashSet<>();
2   dragons.add("Lambton");
3   dragons.add("Deerhurst");
4   dragons.size(); // 2
5   dragons.remove("Lambton");
6   dragons.size(); // 1

Note

为了保持插入顺序,您可以使用一个LinkedHashSet<E>,它使用一个双向链表来存储元素的顺序,此外还使用一个散列表来保持惟一性。

Java 里有个叫SortedSet<E>的东西,是用TreeSet<E>实现的。例如,假设您需要一个按姓名排序的列表,如下所示:

1   SortedSet<String> dragons = new TreeSet<>();
2   dragons.add("Lambton");
3   dragons.add("Smaug");
4   dragons.add("Deerhurst");
5   dragons.add("Norbert");
6   System.out.println(dragons);
7   // [Deerhurst, Lambton, Norbert, Smaug]

会神奇地按正确的顺序排列。

img/435475_2_En_5_Figc_HTML.jpg好吧,这不是真的魔法。要排序的对象必须实现Comparable接口,但是你还没有学过接口(接口在第八章中有涉及)。

JavaScript 还没有内置的Set类。Groovy 使用与 Java 相同的 Set 类。Scala 有自己的Set实现。例如,您可以在 Scala 中定义一个普通的SetSortedSet,如下所示:

1  var nums = Set(1, 2, 3)
2  var sortedNums = SortedSet(1, 3, 2)

地图

Map<K,V>是与值相关联的键的集合。K指定键的通用类型,V指定值的通用类型。举个例子可能更容易理解:

1   Map<String,String> map = new  HashMap<>();
2   map.put("Smaug", "deadly");
3   map.put("Norbert", "cute");
4   map.size(); // 2
5   map.get("Smaug"); // deadly

Map也有以下方法:

  • containsKey(Object key):如果该映射包含指定键的映射,则返回true

  • containsValue(Object value):如果该映射将一个或多个键映射到指定值,则返回true

  • keySet():返回包含在这个映射中的键的Set视图

  • putAll(Map m):将指定映射中的所有映射复制到该映射

  • remove(Object key):从该映射中删除键的映射(如果存在的话)

绝妙的地图

就像List一样,Groovy 创建和编辑Map的语法更简单。

1   def map = ["Smaug": "deadly"]
2   map.Norbert = "cute"
3   println(map) // [Smaug:deadly, Norbert:cute]

Scala 地图

Scala 的Map语法也更短一些。

1   var map = Map("Smaug" -> "deadly")
2   var  map2 =  map + ("Norbert" -> "cute")
3   println(map2) // Map(Smaug -> deadly, Norbert -> cute)

ListSet一样,Scala 的默认Map也是不可变的。

JavaScript 地图

JavaScript 还没有内置的Map类,但是可以通过使用内置的Object 2 语法来近似。例如:

1   def map = {"Smaug": "deadly", "Norbert": "cute"}

然后,您可以使用下面的任意一个来访问 map 值:map.Smaugmap["Smaug"]

摘要

本章向您介绍了以下概念:

  • 数组:固定大小的数据集合

  • 列表:对象或值的可扩展集合

  • 集合:唯一对象或值的可扩展集合

  • 地图:类似字典的收藏

Footnotes 1

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array

  2

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object

 

六、条件语句和循环

为了超越计算器的标签,编程语言必须有条件语句和循环。

条件语句是一个根据具体情况可能执行也可能不执行的语句。

一个循环是一个被重复多次的语句。

如果,那么,别的

最基本的条件语句是if语句。只有给定的条件为真时,它才执行一些代码。这在本书涉及的所有语言中都是一样的。例如:

1   if (vampire) { // vampire is a boolean
2           useWoodenStake();
3   }

花括号 ( {})定义了一个代码块(在 Java、Scala、Groovy 和 JavaScript 中)。为了定义如果你的条件是false会发生什么,你可以使用else关键字。

1   if (vampire) {
2           useWoodenStake();
3   } else {
4           useAxe();
5   }

实际上,这可以缩短,因为在这种情况下,每个条件只有一个语句。

1   if (vampire) useWoodenStake();
2   else useAxe();

一般来说,最好在 Java 中使用花括号风格,以避免以后当另一个程序员添加更多代码时出现任何意外。如果您有多个条件需要测试,您可以使用else if样式,如下所示:

1   if  (vampire) useWoodenStake();
2   else if (zombie) useBat();
3   else useAxe();

Switch 语句

有时候你有太多的条件,以至于你的else if语句跨越了好几页。在这种情况下,您可以考虑使用switch关键字。它允许你测试同一个变量的几个不同的值。例如:

1   switch (monsterType) {
2   case "Vampire": useWoodenStake(); break;
3   case "Zombie": useBat(); break;
4   case "Orc": shoutInsult();
5   default: useAxe();
6   }

case关键字表示要匹配的值。

break关键字总是导致程序退出当前代码块。这在switch语句中是必要的;否则,case后的每一条语句都将被执行。例如,在前面的代码中,当monsterType"Orc"时,shoutInsultuseAxe都被执行,因为shoutInsult()之后没有break

default关键字表示在没有其他匹配的情况下要执行的代码。这很像 i f / else区块的最后一个else区块。

img/435475_2_En_6_Figa_HTML.jpg还有更多关于switch的陈述,但这涉及到我们稍后将涉及的概念,所以我们将回到这个主题。

img/435475_2_En_6_Fig1_HTML.jpg

图 6-1

形式逻辑—XKCD 1033(承蒙 xkcd. com/ 1033/ )

布尔逻辑

计算机使用一种特殊的数学,称为布尔逻辑(也称为布尔代数)。你真正需要知道的只是以下三个布尔运算符和六个比较器。操作员首先:

  • &&AND:仅当左右值为truetrue

  • ||OR: true如果左值或右值为true

  • !NOT:对一个布尔型求反(true变成falsefalse变成了true

现在比较器:

  • ==Equal:如果两个值相等,则为真。

  • !=Not Equal:左右值不相等。

  • <Less than:左侧小于右侧。

  • >Greater than:左侧大于右侧。

  • <= —小于或等于。

  • >= —大于或等于。

条件(如if)对布尔值(true / false)进行操作——与您在第三章中了解到的布尔类型相同。正确使用时,所有前面的运算符都会产生一个布尔值。

例如:

1   if (age > 120 && skin == Pale && !wrinkled) {
2           probablyVampire();
3   }

两种最简单的循环方式是while循环和do / while循环。

循环条件true时,while循环简单重复。在每次循环开始时测试while条件。

1   boolean repeat = true;
2   while (repeat) {
3           doSomething();
4           repeat = false;
5   }

前面的代码将调用一次doSomething()方法。前面代码中的循环条件是repeat。这是一个简单的例子。通常,循环条件会更复杂。

do循环类似于while循环,除了它总是至少经历一次。每次运行循环后,测试while条件。例如:

1   boolean repeat = false;
2   do  {
3           doSomething();
4   } while(repeat);

在循环中增加一个数字通常很有帮助,例如:

1   int i = 0;
2   while (i < 10) {
3           doSomething(i);
4           i++;
5   }

循环十次的前一个循环可以使用for循环进行压缩,如下所示:

1   for  (int  i = 0; i < 10; i++) {
2           doSomething(i);
3   }

for循环有一个初始子句、一个循环条件和一个增量子句。初始子句最先出现(前一个循环中的int i = 0),在循环运行前只被调用一次。接下来是循环条件(我是< 10),很像while条件。增量子句出现在最后(i++),在每次循环执行后被调用。这种类型的循环对于遍历带有索引的数组非常有用。例如:

1   String[] strArray = {"a", "b", "c"};
2   for (int i = 0; i < strArray.length; i++)
3           System.out.print(strArray[i]);

这会打印出“abc”上述循环相当于以下循环:

1   int i = 0;
2   while  (i < strArray.length) {
3       String str = strArray[i];
4           System.out.print(str);
5           i++;
6   }

在 Java 中,可以用更简洁的方式为数组或集合(列表或集合)编写 for 循环。例如:

1   String[] strArray = {"a", "b", "c"};
2   for  (String str : strArray)
3             System.out.print(str);

这被称为for each循环。注意,它使用了冒号而不是分号。

摘要

在本章中,您学习了以下内容:

  • 使用if语句

  • 如何使用布尔逻辑

  • switch报表

  • 使用fordowhilefor each循环

七、方法

一个方法是在一个类中组合成一个块的一系列语句,并给定一个名称。在冷战时期,这些被称为子例程,许多其他语言称它们为函数。然而,方法和函数之间的主要区别在于方法必须与类相关联,而函数则不需要。

打电话给我

方法的存在是为了被调用。你可以把一个方法想象成一条被发送的消息或者一个被给出的命令。为了调用一个方法(也称为调用一个方法),你通常写下对象的名字,一个点,然后是方法名。例如:

1   Dragon dragon = new  Dragon();
2   dragon.fly(); // dragon is the object, and fly is the method

fly方法将在Dragon类中定义。

1   public void fly() {
2           // flying code
3   }

img/435475_2_En_7_Figa_HTML.jpg Void 在 Java 中,void表示尽管方法可能做很多事情,但不返回任何结果。

方法也可以有参数。参数是一个值(或参考值),它是方法调用的一部分。方法的名称、返回类型和参数一起被称为方法签名。例如,以下方法有两个参数:

1   public void fly(int x, int y) {
2           // fly to that x, y coordinate.
3   }

非 Java

其他语言对方法(或函数)的定义不同。例如,在 Groovy 中,可以使用def关键字定义一个方法(除了 Java 的普通语法之外),如下所示:

1   def fly() { println("flying") }

Scala 也使用def关键字来定义方法,但是你还需要一个等号(=)。

1   def fly() = { println("flying") }

JavaScript 使用function关键字来定义函数:

1   function fly() { alert("flying") }

将它分解

方法也可以用来组织你的代码。一个经验法则是永远不要有超过一个屏幕的方法。对电脑来说没什么区别,但对人类(包括你)来说就完全不一样了。

给你的方法起个好名字也很重要。例如,发射箭头的方法应该称为“fireArrow”,而不是“fire”、“arrow”或“arrowBigNow”。

这似乎是一个简单的概念,但是你可能会惊讶于有多少人没有理解它。当你匆忙的时候,它也可能被忽略。如果你没有很好地命名一个东西,它会让你(和其他与你一起工作的程序员)的生活在未来变得更加艰难。

返回发件人

通常,您会希望一个方法返回一个结果。在 Java 中,您可以使用return关键字来实现这一点。例如:

1   public Dragon makeDragonNamed(String name) {
2       return new Dragon(name);
3   }

一旦到达return语句,该方法就完成了。调用该方法的任何代码都将继续执行。如果有返回类型(像前面的Dragon),该方法可以返回该类型的值,并且可以被调用代码使用(前面的方法返回一个新的Dragon对象)。

在一些语言中,比如 Groovy 和 Scala,return关键字是可选的。无论在方法的最后一行输入什么值,都将被返回。例如,在 Groovy 中,以下代码是可接受的:

1   def makeDragonNamed(name) {
2           new Dragon(name)
3   }

静态

在 Java 中,静态方法是不链接到对象实例的方法。它不能引用定义它的类的非静态字段。但是,它必须是类的一部分。

比如我们之前学过的java.util.Math类中的random()方法就是一个静态方法。

要声明一个静态方法,只需添加单词static,如下面的代码所示:

1   public static String getWinnerBetween(Dragon d, Vampire v) {
2           return "The Dragon wins";
3   }

例如,如果前面的方法定义在一个名为Fight的类中,它可以从另一个名为Fight.getWinnerBetween(dragon, vampire)的类中调用,其中dragon是一个Dragon的实例,vampire是一个Vampire的实例。

因为 Java 是一种面向对象的编程(OOP)语言(以及 Scala 和 Groovy),所以静态方法应该少用,因为它们不链接到任何对象实例。然而,它们在许多情况下是有用的。例如,它们可以用于“工厂”方法(创建对象的方法)。之前定义的方法makeDragonNamed()是工厂方法的一个很好的例子。静态方法对于许多不同类中使用的代码也很有用;java.util.Arrays.asList()就是一个例子——它接受任意数量的参数并返回一个包含这些值的新的List

Varargs

*Varargs、*或“可变参数”,允许您用省略号(...)声明方法的最后一个参数,它将被解释为接受给定类型的任意数量的参数(包括零个参数),并在您的方法中将它们转换为数组。例如,请参见下面的代码:

1   void printSpaced(Object... objects) {
2           for (Object o : objects) System.out.print(o + " ");
3   }

将所有这些放在一起,您可以得到以下代码(输出在注释中):

1   printSpaced("A", "B", "C"); // A B C
2   printSpaced(1, 2, 3); // 1 2 3

主要方法

现在您已经了解了静态方法,您终于可以运行 Java 程序了(抱歉花了这么长时间)。下面是如何用 Java 创建一个可执行文件 main method (类名可以不同,但是 main method 必须有这个签名,以便 Java 执行它):

1   import static java.lang.System.out;
2   /** Main class. */
3   public class Main {
4       public static void main(String ... args) {
5           out.println("Hello World!");
6       }
7   }

然后,要对其进行编译,请打开命令提示符或终端,并键入以下内容:

1   javac Main.java
2   java Main

在 groovyConsole 中,只需按 Ctrl+R。

或者在 NetBeans 中,执行以下操作:

  • 右键单击Main类。

  • 选择运行文件。

练习

img/435475_2_En_7_Figb_HTML.jpg尝试方法。创建了Main类之后,尝试向它添加一些方法。尝试从其他方法调用方法,看看会发生什么。

img/435475_2_En_7_Figc_HTML.jpgJava 中的列表、集合、映射,所有这些数据结构都在java.util包下。所以,从导入整个包开始:

1   import    java.util.*;

然后回到第五章并尝试那里的一些代码。

摘要

本章解释了方法的概念以及应该如何使用它们。

我们还将您到目前为止所学的所有内容整合在一起,制作了一个小型 Java 应用程序。

八、继承

继承是在对象间共享功能的好方法。当一个类有一个父类时,我们说它继承了其父类的字段和方法。

在 Java 中,使用extends关键字来定义类的父类。例如:

1   public class Griffon extends FlyingCreature {
2   }

另一种共享功能的方式叫做组合。这意味着一个对象持有对另一个对象的引用,并使用它来做事情。例如,参见下面的GriffonWing类:

1   class Griffon {
2       Wing leftWing = new Wing()
3       Wing rightWing = new Wing()
4       def fly() {
5           leftWing.flap()
6           rightWing.flap()
7       }
8   }
9   class Wing {
10      def flap() { println 'flap'}
11  }
12  new Griffon().fly()

在 groovyConsole 中运行前面的代码会打印出“flap flap”。这样,你就可以拥有一个同样使用Wing类的Bird类。

使具体化

究竟什么是对象?一个对象是一个类的实例(在 Java、Groovy 和 Scala 中)。它可以将状态(字段,也称为实例变量)存储在内存中。

在 Java 中,类有构造器,它可以有多个参数来初始化对象。例如,请参见以下内容:

1   class  FlyingCreature  {
2           String name;
3           // constructor
4           public  FlyingCreature(String name) {
5               this.name = name;
6           }
7   }

FlyingCreature的构造器有一个参数name,它存储在name字段中。必须使用new关键字调用构造器来创建对象,例如:

1   String name = "Bob";
2   FlyingCreature fc = new  FlyingCreature(name);

一旦一个对象被创建,它就可以被传递(这被称为通过引用的传递)。虽然String是一个特殊的类,但它是一个类,所以您可以传递它的一个实例,如前面的代码所示。

Java Script 语言

在 JavaScript 中,构造器是用来定义一个原型的函数(JavaScript 中的原型有点像 Java 中的类定义)。在构造器内部,使用关键字this引用原型。例如,您可以在 JavaScript 中定义一个Creature,如下所示:

1   function Creature(n) {
2       this.name = n;
3   }
4   var  bob = new  Creature('Bob');
This constructor adds a name variable to the Creature prototype

. The object defined earlier (bob) has the name value of ‘Bob’.

Note

JavaScript 中的所有函数和对象都有一个原型。

育儿 101

一个父类定义了多个类共有的共享功能(方法)和状态(字段)。您可以使用像publicprotected这样的访问修饰符来指定字段和方法的可见性(我们将在后面更深入地讨论这些)。

例如,让我们创建一个定义了一个fly()方法并有名称的FlyingCreature类。

 1   class FlyingCreature {
 2           String name;
 3           public FlyingCreature(String name) {
 4                   this.name = name;
 5           }
 6           public void fly() {
 7                   System.out.println(name + " is flying");
 8           }
 9   }
10   class Griffon extends FlyingCreature {
11           public  Griffon(String n) { super(n); }
12   }
13   class Dragon extends FlyingCreature {
14           public  Dragon(String n) { super(n); }
15   }
16   public  class  Parenting  {
17           public static void main(String ... args) {
18                   Dragon d = new  Dragon("Smaug");
19                   Griffon g = new   Griffon("Gilda");
20                   d.fly(); // Smaug is flying
21                   g.fly(); // Gilda is flying
22           }
23   }

在前面的代码中有两个类,GriffonDragon,它们扩展了FlyingCreatureFlyingCreature有时被称为基类GriffonDragon统称为子类

GriffonDragon的每个构造器中,关键字super指的是父类的(FlyingCreature)构造器。

请记住,您可以使用父类的类型来引用任何子类。例如,你可以让任何飞行生物飞行,如下所示:

1   FlyingCreature creature = new Dragon("Smaug");
2   creature.fly(); // Smaug is flying
3   FlyingCreature gilda = new Griffon("Gilda");
4   gilda.fly(); //Gilda is flying

这个概念被称为扩展、继承或多态。你扩展了父类(本例中为 F lyingCreature)。

Java Script 语言

在 JavaScript 中,我们可以使用原型来扩展功能。

例如,假设我们有一个名为Undead的原型。

1   function Undead() {
2       this.dead = false;

3   }

现在让我们创建另外两个构造器,ZombieVampire。JavaScript 还有一个名为Object的内置对象,它有一个基于给定原型创建新对象的create方法。例如:

 1   function Zombie() {
 2       Undead.call(this); // calls the Undead constructor
 3       this.diseased = true;
 4       this.talk = function() { alert("BRAINS!") }
 5   }
 6   Zombie.prototype = Object.create(Undead.prototype);
 7
 8   function Vampire() {
 9       Undead.call(this); // calls the Undead constructor
10       this.pale = true;
11       this.talk = function() { alert("BLOOD!") }
12   }
13   Vampire.prototype = Object.create(Undead.prototype);

注意我们如何将ZombieVampire的原型设置为Undead原型的实例。这样僵尸和吸血鬼可以继承Undead的属性,同时拥有不同的talk功能,如下:

1   var zombie = new Zombie();
2   var vamp = new Vampire();
3   zombie.talk();   //BRAINS
4   zombie.diseased;  // true
5   vamp.talk();     //BLOOD
6   vamp.pale; //true
7   vamp.dead; //false

包装

在 Java(以及相关语言,Groovy 和 Scala)中,是类的名称空间。名称空间只是一个名称库的简称(如果名称在不同的库中,它们可以被重用)。每种现代编程语言都有某种类型的名称空间特性。这是必要的,因为在典型的项目中有许多类。

正如你在第三章中所学的,Java 文件的第一行定义了类的包,例如:

1   package com.github.modernprog;

Java 文件也需要驻留在对应于包的目录中,所以在本例中是com/github/modernprog。此外,有一个共识是包名通常对应于一个 URL(在本例中是github.com/modernprog)。然而,这不是必须的。

公共部分

你可能想知道为什么单词 public 在迄今为止的例子中到处出现。原因与封装有关。封装是一个很大的词,意思是“一个类应该尽可能少地暴露以完成工作”(有些东西应该是私有的)。这有助于降低代码的复杂性,因此更容易理解和思考。

Java 中有三个不同的关键字来表示不同级别的“曝光”

  • private:只有这个类可以看到。

  • 只有这个类及其后代才能看到它。

  • public:大家都能看到。

还有“缺省”保护(没有关键字),它限制使用同一个包中的任何类(包保护)。

这就是为什么类倾向于被声明为public,因为,否则,它们的用途将会非常有限。但是,当在另一个类中声明一个类时,该类可以是私有的,如下所示:

1   public class Griffon extends FlyingCreature {
2           private class GriffonWing {}
3   }

Java Script 语言

JavaScript 没有包的概念,但是,相反,你必须依赖于scope。变量只在创建它们的函数内部可见,除了全局变量。JavaScript 中有提供类似包的框架,但是它们超出了本书的范围。一个是 RequireJS 1 ,它允许你定义模块和模块之间的依赖关系。

接口

一个接口声明了将由实现该接口的类实现的方法签名。这使得 Java 代码可以处理几个不同的类,而不必知道接口“下面”是什么特定的类。接口就像一个契约,它规定了一个实现类必须实现什么。

例如,您可以拥有一个包含一个方法的接口,如下所示:

1   public interface  Beast  {
2           int getNumberOfLegs();
3   }

然后你可以有几个不同的类来实现这个接口。默认情况下,接口方法是公共的。例如:

1   public class Griffon extends FlyingCreature implements  Beast {
2            public int getNumberOfLegs() { return 2; }
3   }
4   public class Unicorn implements Beast {
5            public int getNumberOfLegs() { return 4; }
6   }

在 Java 8 中,增加了添加静态方法的能力和“默认方法”特性,这允许您在一个接口中实现一个方法。例如,您可以使用default关键字并提供一个实现(它仍然可以被覆盖——以不同的方式实现——通过实现类):

1   public interface  Beast  {
2           default int getNumberOfLegs() { return 2; }
3   }

Note

JavaScript 没有与接口等价的概念;然而,由于 JavaScript 不是强类型的,所以接口没有用。你可以调用任何你想要的方法。

抽象类

抽象类是可以有抽象方法但不能有实例的类。它类似于一个具有功能的界面。但是,一个类只能扩展一个超类,而它可以实现多个接口。

例如,要将前面的Beast接口编写为抽象类,您可以执行以下操作:

1   public abstract class Beast {
2           public abstract int getNumberOfLegs();
3   }

然后,您可以添加非抽象方法和/或字段。例如:

1   public abstract class Beast {
2           protected String name;
3           public String getName() { return name; }
4           public abstract int getNumberOfLegs();

枚举数

在 Java 中,enum关键字创建一个类型安全的常量值有序列表。例如:

1   public enum BloodType {
2           A, B, AB, O, VAMPIRE, UNICORN;
3   }

一个enum变量只能指向枚举中的一个值。例如:

1   BloodType type = BloodType.A;

枚举被自动赋予一组方法,例如

  • values():给出枚举中所有可能值的数组(静态)

  • valueOf(String):将给定的字符串转换成具有给定名称的枚举值(静态)

  • name():枚举上给出其名称的实例方法

另外,枚举在switch语句中有特殊处理。例如,在 Java 中,可以使用缩写语法(假设type是一个BloodType)。

1   switch (type) {
2           case VAMPIRE: return vampire();
3           case UNICORN: return unicorn();
4           default: return human();
5   }

释文

Java 注释允许您向 Java 代码中添加元信息,编译器、各种 API 甚至您自己的代码都可以在运行时使用这些元信息。它们可以放在方法、类、字段、参数和其他一些地方的定义之前。

您将看到的最常见的注释是@Override注释,它向编译器声明您正在从超类或接口重写一个方法。例如:

1   @Override
2   public String toString() {
3           return "my own string";
4   }

这很有用,因为如果您键入错误的方法名或参数类型,就会导致编译时错误。不要求重写一个方法,但是使用它是一个好习惯。

其他有用的注释是那些在javax.annotation中的注释,比如@Nonnull@Nonnegative,它们可以被添加到参数中来声明你的意图,并被 IDE 用来帮助捕捉代码中的错误。

像 Hibernate、Spring Boot 和其他框架使用的其他注释也非常有用。像@Autowired@Inject这样的注释被 Spring 和 Google Guice2T5 这样的直接注入框架用来减少“连线”代码。

汽车人

虽然 Java 是一种面向对象的语言,但这有时会与它的原始类型(int, longfloatdouble等)发生冲突。).出于这个原因,Java 在语言中加入了自动装箱和取消装箱。

汽车人

Java 编译器会在必要的时候自动在相应的对象中封装一个原语类型,比如intIntegerbooleanBooleandoubleDoublefloatFloat。例如,当向函数传递参数或给变量赋值时,如下所示:Integer number = 1

取消订阅

取消装箱与自动装箱相反。在可能的情况下,Java 编译器会将一个对象展开成相应的原语类型。例如,以下代码是可接受的:double d = new Double(1.1) + new Double(2.2)

摘要

读完这一章后,你应该理解 OOP,多态,以及以下的定义:

  • 扩展和组合

  • 公共与私有、受保护与包保护

  • 类、抽象类、接口和枚举

  • 释文

  • 汽车爆炸和脱氧核糖核酸病毒

Footnotes 1

https://requirejs.org

  2

https://github.com/google/guice

 

九、设计模式

在面向对象编程(OOP)中,设计模式是状态和行为的有用组织,使您的代码更具可读性、可测试性和可扩展性。现在你已经理解了类、继承、对象和编程的基础,让我们回顾一些常见的设计模式——排列应用程序代码的常见方式。

观察者

observer 模式允许您将信息从一个类传播到许多其他类,而不需要它们直接相互了解(低耦合)。

它经常和事件一起使用。例如,Java Swing 中的KeyListenerMouseListener和许多其他“监听器”接口(这是用于构建桌面应用程序的 JDK 的内置部分)实现了 observer 模式并使用了事件。

这种模式的另一个例子是 Java 中提供的Observable类和Observer接口。下面是一个简单的例子,简单地永远重复相同的事件:

 1   import java.util.Observable;
 2
 3   public class EventSource extends Observable implements Runnable {
 4       @Override
 5       public void run() {
 6           while  (true) {
 7               notifyObservers("event");
 8           }
 9       }
10   }

尽管在本例中事件是一个字符串,但它可以是任何类型。

下面的类实现了Observer接口并打印出任何类型为String的事件:

 1   import java.util.Observable;
 2   import java.util.Observer;
 3
 4   public class StringObserver implements Observer {
 5       public void update(Observable obj, Object event) {
 6           if (event instanceof String) {
 7               System.out.println("\nReceived Response: " + event );
 8           }
 9       }
10   }

要运行这个示例,请在您的main方法中编写以下代码:

1   final EventSource eventSource = new EventSource();
2   // create an observer
3   final StringObserver stringObserver = new StringObserver();
4   // subscribe the observer to the event source
5   eventSource.addObserver(stringObserver);
6   // starts the event thread
7   Thread thread = new  Thread(eventSource);
8   thread.start();

Although you are only adding one observer on line 5, you could add any number of observers without changing the code of EventSource. This is what is meant by low coupling.

手动音量调节

模型-视图-控制器(MVC)可能是最流行的软件设计模式(图 9-1 )。顾名思义,它由三大部分组成:

img/435475_2_En_9_Fig1_HTML.jpg

图 9-1

模型视图控制器

  • 模型:被显示和操作的数据或信息

  • 视图:什么实际上定义了模型如何显示给用户

  • 控制器:定义动作如何操纵模型

这种设计允许控制器、模型和视图彼此知之甚少。这减少了耦合——软件的不同组件依赖其他组件的程度。当你有低耦合时,你的软件更容易理解和扩展。

我们将在关于 web 应用程序和 Grails 的章节(第十七章)中看到一个很好的 MVC 例子。

数字式用户线路

特定领域语言(DSL)是为特定领域定制的编程语言。例如,您可以将 HTML 视为显示网页的 DSL。

有些语言给你很大的自由,你可以在语言内部创建 DSL。例如,Groovy 和 Scala 允许您覆盖数学符号(+-等)。).这些语言的其他自由(可选的括号和分号)允许类似 DSL 的接口。我们称这些类似 DSL 的接口为流畅接口

还可以用 Java 和其他语言创建流畅的界面。下面几节讨论用 Groovy 构建 DSL。

关闭

在 Groovy 中,您可以将一段代码(一个闭包)作为参数,然后使用一个局部变量作为委托来调用它——这使得该对象的所有方法都可以在闭包中直接引用。例如,假设您有以下发送 SMS 文本的代码:

 1   class SMS {
 2           def from(String fromNumber) {
 3                   // set the from
 4           }
 5           def to(String toNumber) {
 6                   // set the to
 7           }
 8           def body(String body) {
 9                   // set the body of text
10           }
11           def send() {
12                   // send the text.
13           }
14   }

在 Java 中,您必须按照以下方式使用它(注意重复的部分):

1   SMS m = new  SMS();
2   m.from("555-432-1234");
3   m.to("555-678-4321");
4   m.body("Hey there!");
5   m.send();

在 Groovy 中,您可以将下面的static方法添加到类似 DSL 的SMS类中(它接受一个闭包,将委托设置为SMS类的一个实例,调用块,然后在SMS实例上调用 send):

1   def static send(Closure block) {
2           SMS m = new SMS()
3           block.delegate = m
4           block()
5           m.send()
6   }

这会将SMS对象设置为该块的委托,以便将方法转发给它。这样,您现在可以执行以下操作:

1   SMS.send {
2           from '555-432-1234'
3           to '555-678-4321'
4           body 'Hey there!'
5   }

覆盖运算符

在 Scala 或 Groovy 中,您可以创建一个 DSL 来计算特定单位的速度,比如米每秒。

1   val time =  20 seconds
2   val dist =  155 meters
3   val speed =  dist / time
4   println(speed.value) //  7.75

通过重写操作符,您可以约束 DSL 的用户以减少错误。例如,在这里不小心键入time/dist会导致这个 DSL 出现编译错误。

下面是如何在 Scala 中定义这个 DSL:

 1   class Second(val value: Float) {}
 2   class MeterPerSecond(val  value:  Float) {}
 3   class Meter(val value: Float) {
 4     def /(sec: Second) = {
 5       new MeterPerSecond(value / sec.value)
 6     }
 7   }
 8   class EnhancedFloat(value: Float) {
 9     def seconds =  {
10       new   Second(value)
11     }
12     def  meters =  {
13       new  Meter(value)
14     }
15   }
16   implicit  def  enhanceFloat(f:  Float) =  new  EnhancedFloat(f)

img/435475_2_En_9_Figa_HTML.jpg Scala 有implicit关键字,允许编译器为你做隐式转换。

注意 divide /操作符是如何定义的,就像任何其他使用def关键字的方法一样。

img/435475_2_En_9_Figb_HTML.jpg在 Groovy 中,你通过定义带有特殊名称 1 的方法重载操作符,比如plusminusmultiplydiv等。

演员

actor 设计模式是开发并发软件的有用模式。在这种模式中,每个参与者都在自己的线程中执行,并操作自己的数据。数据不能被其他任何人操纵。消息在参与者之间传递,使他们改变数据(图 9-2 )。

img/435475_2_En_9_Fig2_HTML.jpg

图 9-2

演员

Note

当数据一次只能被一个线程改变时,我们称之为线程安全。如果多个线程同时修改相同的数据,这是非常糟糕的(它可能会导致异常)。

您可以使用这种模式的许多实现,包括:

  • akka【2】

  • 喷气织机【3】

  • functional Java4

  • gpars【5】

责任链

责任链模式允许你分割代码来处理不同的情况,而不需要每个部分都知道所有其他的部分。

例如,在设计根据用户访问的 URL 采取不同操作的 web 应用程序时,这可能是一个有用的模式。在这种情况下,您可以拥有一个带有方法的WebHandler接口,该方法可能处理也可能不处理该 URL 并返回一个String:

1   public interface WebHandler {
2       String handle(String url);
3       void setNext(WebHandler next);
4   }

然后,您可以实现该接口,如果您不处理该 URL,则调用链中的下一个处理程序:

1   public class ZombieHandler implements WebHandler {
2       WebHandler next;
3       public String handle(String url) {
4           if (url.endsWith("/zombie")) return "Zombie!";
5           else return next.handle(url);
6       }
7       public void setNext(WebHandler next) {this.next = next;}
8   }

只有当 URL 以/zombie结尾时,这个类才会返回值。否则,它将委托给链中的下一个处理程序。

外表

Facade 模式允许您将更大系统的复杂性隐藏在更简单的设计之下。例如,您可以让一个类包含一些调用许多其他类的方法的方法。

让我们以前面的例子为例,创建一个 facade 来处理传入的 web URL,而不需要引用任何特定的WebHandler实现。创建一个名为WebFacade的类:

1  public class WebFacade {
2    public String handle(String url) {
3        WebHandler firstHandler = new ZombieHandler();
4        WebHandler secondHandler = new DragonHandler();
5        WebHandler finalHandler = new DefaultHandler();
6        firstHandler.setNext(secondHandler);
7        secondHandler.setNext(finalHandler);
8        return firstHandler.handle(url);
9    }
10 }

WebFacade创建我们所有的处理程序类,将它们连接在一起(调用setNext),最后通过调用第一个WebHandlerhandle方法返回值。

WebFacade的用户不需要知道 URL 是如何处理的。这就是 Facade 模式的用处。

摘要

在本章中,你学习了一些常见的设计模式和设计应用程序的方法。这不是设计模式的完整列表。有关面向对象设计模式的更多信息,请查阅 oodesign。com6 在本章中,你学到了

  • 什么是 DSL 以及如何编写 DSL

  • 观察者、MVC、责任链和外观模式

  • 处理并发的参与者模式

Footnotes 1

http://groovy-lang.org/operators.html#Operator-Overloading

  2

https://akka.io

  3

https://github.com/jetlang

  4

http://functionaljava.org/

  5

http://gpars.org/

  6

www.oodesign.com/

 

十、函数式编程

函数式编程 (FP)是一种以函数为中心,最小化状态变化(使用不可变数据结构)的编程风格。它更接近于用数学来表达解决方案,而不是通过一步一步的指令。

在 FP 中,函数应该是“无副作用的”(函数之外的任何东西都不会改变),并且引用透明(当给定相同的参数时,函数每次都返回相同的值)。例如,这将允许值被缓存(保存在内存中)。

FP 是更常见的命令式编程的替代,它更接近于告诉计算机要遵循的步骤。

虽然函数式编程可以在 Java-8 之前的 Java 中实现,但 Java 8 启用了语言级 FP 支持,包括λ表达式和函数接口

Java 8、JavaScript、Groovy 和 Scala 都支持函数式编程,尽管它们不是 FP 语言。

Note

诸如 Common Lisp、Scheme、Clojure、Racket、Erlang、OCaml、Haskell 和 F#等著名的函数式编程语言已经被各种各样的组织用于工业和商业应用中。Clojure 2 是一种运行在 JVM 上的类似 Lisp 的语言。

函数和闭包

函数作为一级特性是函数式编程的基础。一级特性意味着一个函数可以用在一个值可以用的任何地方。

例如,在 JavaScript 中,您可以将一个函数赋给一个变量,并像下面这样调用它:

1   var func = function(x) { return x + 1; }
2   var three = func(2); //3

虽然 Groovy 没有一流的函数,但是它有一些非常相似的东西:闭包。闭包就是一个用大括号括起来的代码块,参数定义在->(箭头)的左边。例如:

1   def closr = {x -> x + 1}
2   println( closr(2) ); //3

如果一个闭包有一个参数,那么在 Groovy 中它可以作为it被引用。例如,以下内容与前面的closr含义相同:

1   def closr = {it + 1}

Java 8 引入了 lambda 表达式,它类似于实现函数接口的闭包(具有单一抽象方法的接口)。lambda 表达式的主要语法如下:

parameters -> body

Java 编译器使用表达式的上下文来确定正在使用哪个函数接口(以及参数的类型)。例如:

1   Function<Integer,Integer> func = x -> x + 1;
2   int three = func.apply(2); //3

这里的函数接口是Function<T,R>,它有apply方法— T为参数类型,R为返回类型。返回值和参数类型都是 I nteger s,因此Integer,Integer是泛型类型参数。

在 Java 8 中,函数接口被定义为只有一个抽象方法的接口。这甚至适用于用以前版本的 Java 创建的接口。

在 Scala 中,一切都是表达式,函数是一级特性。下面是 Scala 中的一个函数示例:

1   var  f =  (x:  Int) =>  x + 1;
2   println(f(2)); //3

虽然 Java 和 Scala 都是静态类型的,但是 Scala 实际上是使用右边来推断被声明的函数的类型,而 Java 在大多数情况下是相反的。Java 11 引入了局部变量类型 var ,这使得语法非常接近 Scala 的 var

img/435475_2_En_10_Figb_HTML.jpg在 Java、Groovy 和 Scala 中,如果函数/闭包中有一个表达式,那么可以省略return关键字。但是,在 Groovy 和 Scala 中,如果返回值是最后一个表达式,也可以省略return关键字。

地图、过滤器等。

一旦您掌握了函数,您很快就会意识到您需要一种方法来对数据集合(或序列或流)执行操作。因为这些都是常见的操作,*顺序操作,*如mapfilterreduce等。,都是被发明的。

对于本节中的例子,我们将使用 JavaScript,因为它更容易阅读,并且函数名在各种编程语言中相当标准。创建以下Person原型和Array:

1 function Person(name, age) { this.name = name; this.age = age; }
2 var persons = [new Person("Bob", 18),
3     new Person("Billy", 21), new Person("sam", 12)]

The map函数将输入元素翻译或改变成其他东西(图 10-1 )。以下代码收集每个人的姓名:

img/435475_2_En_10_Fig1_HTML.jpg

图 10-1

地图

1   var names = persons.map(function(person) { return person.name })

filter给出了元素的子集(从某个谓词函数返回true,该函数返回给定一个参数的布尔值【图 10-2 】)。例如,以下代码只收集年龄大于或等于 18 岁的人:

img/435475_2_En_10_Fig2_HTML.jpg

图 10-2

过滤器

1   var adults = persons.filter(function(person) { return person.age >= 18 })

reduce对元素进行缩减(图 10-3 )。例如,以下代码收集所有人的总年龄:

img/435475_2_En_10_Fig3_HTML.jpg

图 10-3

减少

1   var totalAge = persons.reduce(function(total, p) { return total+p.age },0)

limit只给出前 N 个元素(图 10-4 )。在 JavaScript 中,您可以使用Array.slice(start, end)函数来实现这一点。例如,下面的代码获取前两个人:

img/435475_2_En_10_Fig4_HTML.jpg

图 10-4

限制

1   var firstTwo = persons.slice(0, 2)

concat组合两个不同的元素集合(图 10-5 )。这可以在 JavaScript 中完成,如下例所示:

img/435475_2_En_10_Fig5_HTML.jpg

图 10-5

联结合并多个字符串

1 var morePersons = [new Person("Mary", 55), new Person("Sue", 22)]
2 var all = persons.concat(morePersons);

不变

不变性和 FP 就像花生酱和果冻一样。虽然没有必要,但它们融合得很好。

在纯函数式语言中,其思想是每个函数对自身之外没有影响——没有副作用。这意味着每次你调用一个函数,在给定相同输入的情况下,它返回相同的值。

为了适应这种行为,有不可变的数据结构。不可变的数据结构不能被直接改变,但是每次操作都会返回一个新的数据结构。

例如,正如您之前了解到的,Scala 的默认Map是不可变的。

1   val map = Map("Smaug" -> "deadly")
2   val map2 = map + ("Norbert" -> "cute")
3   println(map2) // Map(Smaug -> deadly, Norbert -> cute)

所以,在前文中,map将保持不变。

每种语言都有一个定义不可变变量(值)的关键字:

  • Scala 使用val关键字来表示不可变的值,与用于可变变量的var相反。

  • Java 有用于声明变量不可变的关键字final(这仅阻止值被修改,如果它是对另一个对象的引用,该对象的变量仍然可以被修改)。

  • 除了final关键字,Groovy 还包括@Immutable注释 3 ,用于声明整个类不可变。

  • JavaScript 使用了const关键字。4

例如(在 Groovy 中):

1   public class Centaur {
2       final String name
3       public Centaur(name) {this.name=name}
4   }
5   Centaur c = new Centaur("Bane");
6   println(c.name) // Bane
7
8   c.name = "Firenze" //error

这适用于简单的引用和原语,比如数字和字符串,但是对于列表和映射,就比较复杂了。对于这些情况,开源不可变库已经为不包含它的语言开发出来,如下所示:

  • 番石榴 5 用于 Java 和 Groovy

  • 不可变-JS6 为 JavaScript

爪哇

在 Java 8 中,引入了Stream<T>接口。流就像一个改进的迭代器,支持链接方法来执行复杂的操作。

要使用流,您必须首先通过以下方式之一创建一个流:

  • Collection's stream() 方法或 parallelStream() 方法:这些创建了一个由集合支持的流。使用parallelStream()方法可以使流操作并行运行。

  • Arrays.stream() 方法:用于将数组转换为流。

  • Stream.generate(Supplier<T> s):返回一个无限序列流,其中每个元素都是由给定的供应商生成的。

  • Stream.iterate(T seed, UnaryOperator<T> f):返回一个函数对一个初始元素 seed 迭代应用产生的无限顺序有序流,产生一个由 seed,f(seed),f(f(seed))等组成的流。

一旦有了一个流,就可以使用filtermapreduce操作简洁地对数据执行计算。例如,以下代码从龙的列表中查找最长的名称:

1   String longestName = dragons.stream()
2       .filter(d -> d.name != null)
3       .map(d -> d.name)
4       .reduce((n1, n2) -> n1.length() > n2.length() ? n1 : n2)
5       .get();

绝妙的

在 Groovy 中,findAll和其他方法对每个对象都可用,但对列表和集合尤其有用。Groovy 中使用了以下方法名:

  • findAll:与filter非常相似,它查找所有匹配闭包的元素。

  • collect:很像map,这是一个构建集合的迭代器。

  • inject:与reduce非常相似,它遍历这些值并返回一个值。

  • each:使用给定的闭包遍历值。

  • eachWithIndex:使用两个参数进行迭代:一个值和一个索引(值的索引,从零开始向上)。

  • find:查找匹配闭包的第一个元素。

  • findIndexOf:查找匹配闭包的第一个元素并返回其索引。

  • any : True如果有任何元素返回true结束。

  • every : True如果所有元素返回true则结束。

例如,下面假设dragons是具有name属性的Dragon对象的列表:

1   String longestName = dragons
2      .findAll { it.name != null }
3      .collect { it.name }
4      .inject("") { n1, n2 -> n1.length() > n2.length() ? n1 : n2 }

img/435475_2_En_10_Figc_HTML.jpg记住 Groovy 中的it可以用来引用闭包的单个参数。

斯卡拉

Scala 的内置集合中有很多这样的方法,包括:

  • map:将值从一个值转换为另一个值

  • flatMap:将值转换为值的集合,然后将结果连接在一起(类似于 Groovy 中的flatten()方法)

  • filter:根据某个布尔表达式限制返回值

  • find:返回与给定谓词匹配的第一个值

  • forAll : True仅当所有元素都匹配给定的谓词时

  • exists : True如果至少有一个元素匹配给定的谓词

  • foldLeft:使用给定的闭包将值减少到一个值,从最后一个元素开始向左

  • foldRight:与foldLeft相同,但从第一个值开始向上(类似 Java 中的 reduce)

例如,您可以使用map对值列表执行操作,如下所示:

1   val list = List(1, 2, 3)
2   list.map(_  * 2) // List(2, 4, 6)

img/435475_2_En_10_Figd_HTML.jpg与 Groovy 中的it非常相似,在 Scala 中,您可以使用下划线来引用单个参数。

假设dragons是 dragon 对象的列表,您可以在 Scala 中执行以下操作来确定最长的名称:

1   var longestName = dragons.filter(_ != null).map(_.name).foldRight("")(
2       (n1:String,n2:String) => if (n1.length() > n2.length()) n1 else n2)

摘要

在本章中,您应该已经了解了

  • 功能为一级功能

  • 映射、过滤、减少

  • 不变性及其与 FP 的关系

  • Java、Groovy、Scala 和 JavaScript 中支持 FPs 的各种特性

Footnotes 1

http://functionaljava.org/

  2

https://clojure.org/

  3

http://bit.ly/32vyU8n

  4

https://mzl.la/33u0JyY

  5

https://github.com/google/guava

  6

https://github.com/facebook/immutable-js