轻松学模块化编程-全-

轻松学模块化编程(全)

原文: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 你可以随时去那里跟着书走。

三、基础知识

在这一章中,我们将介绍 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 */

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

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

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

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

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

摘要

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

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

  • 对象如何成为类的实例

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

  • 字段、方法和属性

  • 变量赋值

  • 源代码注释如何工作

四、数学

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

加法、减法等。

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

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

  • 创建随机数

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

到目前为止,我只讨论了单个值,但是在编程中,您经常需要处理大量的值集合。为此,我们在语言中内置了许多数据结构。对于 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"]

摘要

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

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

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

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

  • 地图:类似字典的收藏

六、条件语句和循环

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

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

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

如果,那么,别的

最基本的条件语句是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,多态,以及以下的定义:

  • 扩展和组合

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

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

  • 释文

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

九、设计模式

在面向对象编程(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、责任链和外观模式

  • 处理并发的参与者模式

十、函数式编程

函数式编程 (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 的各种特性

十一、重构

重构 1 是指以对功能没有影响的方式改变代码。这只是为了让代码更容易理解,或者为将来增加一些功能做准备。例如,有时您重构代码以使其更容易测试。许多 ide 都提供了执行常见重构的菜单和快捷键。

我们将讨论两类重构,面向对象的和函数式的,对应于两种不同的编程风格。

面向对象的重构

以下操作是 OOP 中常见的重构:

  • 更改方法或类名(重命名)

  • 将方法从一个类移动到另一个类(委托)

  • 将字段从一个类移动到另一个类

  • 在方法中添加或删除参数

  • 使用类中的一组方法和字段创建新类

  • 将局部变量更改为类字段

  • 用常量(static final)替换一组文字(字符串或数字)

  • 用枚举替换一些常量或文字

  • 将一个类从匿名类移动到顶级类

  • 重命名字段

功能重构

以下动作是 FP 中常见的重构:

  • 重命名函数

  • 将一个函数包装在另一个函数中并调用它

  • 在函数被调用的任何地方内联它

  • 将公共代码提取到一个函数中(与前面的相反)

  • 重命名函数参数

  • 添加或移除参数

您可能会注意到两个列表之间的一些相似之处。重构的原则是通用的。

重构示例

与许多 ide 一样,NetBeans 支持重构。您可以通过选择一些代码,右键单击,并选择 Refactor 菜单来尝试一下。这里有一些重构代码的例子。

重命名方法

之前:

1   public static void main(String...args) {
2       animateDead();
3   }
4   public  static void  animateDead() {}

之后:

1   public static void main(String...args) {
2       doCoolThing();
3   }
4   public  static void  doCoolThing() {}

将方法从一个类移动到另一个类(委托)

之前:

1   public static void main(String...args) {
2       animateDead();
3   }
4   public  static void  animateDead() {}

之后:

1   public class Animator() {
2       public void animateDead() {}
3   }
4   public static void main(String...args) {
5       new Animator().animateDead();
6   }

用常量(Static Final)替换一组文字(字符串或数字)

之前:

1   public static void main(String...args) {
2       animateDead(123);
3       System.out.println(123);
4   }
5   public static void animateDead(int n) {}

之后:

1   public static final int NUM = 123;
2   public static void main(String...args) {
3       animateDead(NUM);
4       System.out.println(NUM);
5   }
6   public static void animateDead(int n) {}

重命名函数

之前:

1   function castaNastySpell() { /* cast a spell here */ }

之后:

1   function castSpell() { /* cast a  spell here  */ }

将一个函数包装在另一个函数中并调用它

之前:

1   castSpell('my cool spell');

之后:

1   (function(spell) { castSpell(spell) })('my cool spell');

在函数被调用的任何地方内联它

当一个函数太简单或者只在一个地方使用时,这可能在重构过程中完成。

之前:

1   function castSpell(spell) { alert('You cast ' + spell); }
2   castSpell('crucio');
3   castSpell('expelliarmus');

之后:

1   alert('You cast ' + 'crucio');
2   alert('You cast ' + 'expelliarmus');

将公共代码提取到一个函数中(与前面的相反)

之前:

1   alert('You cast crucio');
2   alert('You cast expelliarmus');

之后:

1   function castSpell(spell) { alert('You cast ' + spell); }
2   castSpell('crucio');
3   castSpell('expelliarmus');

十二、实用工具

每种编程语言都内置了许多非常有用的类、函数或对象(有时称为实用程序)。java.util包包含许多日常编程有用的类。同样,JavaScript 和其他语言带有许多用于执行常见任务的内置对象。我将讲述其中的一些。

日期和时间

永远不要将日期值存储为文本。太容易搞砸了(图 12-1 )。

img/435475_2_En_12_Fig1_HTML.jpg

图 12-1

我可以用绳子来存储数据吗?

Java 日期-时间

Java 8 在java.time包中引入了一个新的改进的日期时间应用程序接口(API ),它比以前的 API 更安全、更易读、更全面。

例如,创建日期如下所示:

1   LocalDate date = LocalDate.of(2014, Month.MARCH, 2);

还有一个LocalDateTim e 类来表示日期和时间,LocalTime只表示时间,ZonedDateTime表示带时区的时间。

在 Java 8 之前,只有两个内置类来帮助处理日期:DateCalendar。尽可能避免这些。

  • Date实际上表示日期和时间。

  • Calendar用于操作日期。

在 Java 7 和更低版本中,您必须执行以下操作来为日期添加五天:

1   Calendar cal = Calendar.getInstance();
2   cal.setTime(date);
3   cal.add(5, Calendar.DAY);

而在更高版本的 Java 中,您可以执行以下操作:

1   LocalDate.now().plusDays(5)

绝妙的约会

Groovy 有许多内置特性,使得日期更容易处理。例如,数字可用于加减天数,如下所示:

1   def date = new Date() + 5; //adds 5 days

Groovy 也有时间类别 1 用于操作日期和时间。这可以让你加减任意长度的时间。例如:

1   import groovy.time.TimeCategory
2   now = new Date()
3   println now
4   use(TimeCategory) {
5       nextWeekPlusTenHours = now + 1.week + 10.hours - 30.seconds
6   }
7   println nextWeekPlusTenHours

一个Category是一个类,可以用来给其他现有的类添加功能。在这种情况下,TimeCategoryInteger类添加了一堆方法。

Categories

这是 Groovy 中可用的众多元编程技术之一。要创建一个类别,您需要创建一组静态方法,这些方法对特定类型的一个参数进行操作(例如,Integer)。当使用类别时,该类型似乎具有那些方法。调用方法的对象用作参数。请看一下 TimeCategory 的文档,其中有一个实际例子。

JavaScript 日期

JavaScript 也有内置的Date 2 对象。

您可以用几种方法创建一个Date对象的实例(这些方法都创建相同的日期):

1   Date.parse('June 13, 2014')
2   new Date('2014-06-13')
3   new Date(2014, 5, 13)

请注意,如果您遵循国际标准(yyyy-MM-dd),将采用 UTC 时区;否则,它会假设您需要一个本地时间。

和通常的 JavaScript 一样,浏览器都有稍微不同的规则,所以你必须小心。

img/435475_2_En_12_Figa_HTML.jpg永远不要用getYear!在 Java 和 JavaScript 中,Date对象的getYear方法并不像你想的那样,应该避免。由于历史原因,getYear实际上并不返回年份(如 2014)。你应该在 JavaScript 中使用getFullYear(),在 Java 中使用LocalDateLocalDateTime

Java 日期格式

DateFormat虽然在java.text里面,但是和java.util.Date是齐头并进的。

SimpleDateFormat对于将日期格式化成您想要的任何格式都很有用。例如:

1   SimpleDateFormat sdf = new  SimpleDateFormat("MM/dd/yyyy");
2   Date date = new  Date();
3   System.out.println(sdf.format(date));

这将按照美国标准格式化日期:月/日/年。

Java 8 引入了java.time.format.DateTimeFormatter来使用新的日期和时间类进行格式化或解析。每个java.time类,比如LocalDate,都有一个格式方法和一个静态解析方法,这两个方法都采用了DateTimeFormatter.的一个实例

例如:

1  LocalDate date = LocalDate.now();
2  DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
3  String text = date.format(formatter);
4  LocalDate parsedDate = LocalDate.parse(text, formatter);

关于它的更多信息,请参见 SimpleDateFormat3的文档。有关更多信息,请参见 datetime formatter4文档。

货币

在 Java 中,如果您的代码必须处理多个国家的货币,那么java.util. Currency非常有用。它提供了以下方法:

  • getInstance(Locale): Static method to get an instance of Currency based on Locale

  • getInstance(String): Static method to get an instance of Currency based on a currency code

  • getSymbol():当前区域设置的货币符号

  • getSymbol(Locale):给定地区的货币符号

  • static getAvailableCurrencies():返回可用货币的集合

例如:

1   String pound = Currency.getInstance(Locale.UK).getSymbol(); // GBP
2   String dollar = Currency.getInstance("USD").getSymbol(); // $

时区

在 Java 8 和更高版本中,时区由java.time.ZoneId类表示。有两种类型的ZoneId,固定偏移量和地理区域。这是为了补偿夏令时等可能非常复杂的做法。

您可以通过多种方式获得一个ZoneId的实例,包括以下两种:

1   ZoneId mountainTime = ZoneId.of("America/Denver");
2   ZoneId myZone = ZoneId.systemDefault();

要打印出所有可用的 id,使用getAvailableZoneIds(),如下所示:

1   System.out.println(ZoneId.getAvailableZoneIds());

写一个这样做的程序并运行它。例如,在 groovyConsole 中,编写以下代码并执行:

import java.time.*

println(ZoneId.getAvailableZoneIds())

扫描仪

Scanner可用于解析文件或用户输入。它使用给定的模式将输入分成标记,默认模式是空白(“空白”是指空格、制表符或任何在文本中不可见的东西)。

例如,使用以下代码从用户处读取两个数字:

1   System.out.println("Please type two numbers");
2   Scanner sc = new Scanner(System.in);
3   int num1 = sc.nextInt();
4   int num2 = sc.nextInt();

写一个这样做的程序,然后试一试。因为这需要输入,所以不能用 groovyConsole 来完成。使用 NetBeans 构建一个 Java 应用程序,或者使用命令行上的groovy运行一个 Groovy 脚本。

十三、构建器

构建过程通常是编译项目的源文件,运行测试,并生产一个或多个成品。

在一些公司中,整个团队的唯一工作就是维护和更新构建过程。在任何规模的项目中,拥有一个好的自动化构建工具是有帮助的。

还有许多其他的构建工具,但我们只打算介绍三种:

  • ant

  • maven【2】

  • 等级3T3〕

蚂蚁

Ant 是第一个真正流行的 Java 项目构建器。它是基于 XML 的,要求您用 XML 创建可以由 Ant 执行的任务。Ant 构建文件通常被命名为build.xml,并且有一个<project>根元素。

一个任务是分工。任务依赖于其他任务。例如,jar任务通常依赖于compile任务。Maven 虽然扔掉了任务概念,但是在 Gradle 中又被使用了。

Ant 的批评者抱怨说,它使用 XML(一种声明性的、非常冗长的格式),做简单的事情需要大量的工作。然而,多年来它一直是一个非常受欢迎的工具,并且仍然在许多地方使用。

专家

Maven 是一个基于 XML 的声明式项目管理器。Maven 用于构建 Java 项目,但还能做更多的事情。Maven 也是一组标准,允许 Java/JVM 开发人员轻松地定义依赖项并将其集成到大型项目中。Maven 在某种程度上取代了 Ant,但也可以与它和其他构建工具集成。

Maven 主要是对 Java 项目倾向于依赖的大量开源库的一种反应。它有一个内置的依赖性管理标准(管理开源库的相互依赖性)。

虽然 Maven 是一个 Apache 开源项目,但可以说 Maven 的核心是由 Maven 背后的公司 Sonatype 运营的开源库库 Maven Central 。还有许多其他的库也遵循 Maven 标准,比如 JFrog 的 jCenter、、 4 ,所以您并不局限于 Maven Central。

Note

Ivy 5 是一个类似的构建工具,但与 Ant 的关系更密切。

许多构建工具,如 Ivy 和 Gradle,都是基于 Maven 的概念构建的。

使用 Maven

定义 Maven 项目的主要文件是 POM (项目对象模型)。POM 文件是用 XML 编写的,包含所有的依赖项、插件、属性和特定于当前项目的配置数据。POM 文件通常由以下内容组成:

  • 基本属性(artifactIdgroupIdnameversionpackaging)

  • 属国

  • 外挂程式

每个主流的基于 Java 的 IDE(Eclipse、NetBeans 和 IntelliJ IDEA)都有一个 Maven 插件,它们非常有用。您可以使用 Maven 插件来创建项目、添加依赖项和编辑 POM 文件。

开始一个新项目

使用archetype:generate命令创建一个新的配置文件(pom.xml)和项目文件夹有一个简单的方法。

   mvn archetype:generate

这将列出您可以创建的所有不同类型的项目。选择一个代表你想要的项目类型的数字(现在有数千个选项),然后回答一些问题,比如你的项目的名称。完成该过程后,运行以下命令来构建项目:

1   mvn package

如果您想使用任何额外的第三方库,您必须编辑 POM 以包含每个依赖项。幸运的是,大多数 ide 都很容易向 POM 添加依赖项。

img/435475_2_En_13_Figa_HTML.jpg * Maven:完整参考* 6 如果你想了解更多,可以在线获得。

生存期

Maven 使用声明式风格(不像 Ant 使用更强制性的方法)。这意味着您不是列出要采取的步骤,而是描述在构建的某些阶段应该发生什么。Maven 中的阶段是内置的,如下所示(并按此顺序执行):

  1. validate:验证项目是否正确以及所有必要的信息是否可用

  2. compile:编译项目的源代码

  3. test:使用合适的单元测试框架测试编译后的源代码

  4. package:获取编译后的代码,并将其打包成可分发的格式,比如 JAR

  5. integration-test:如果需要的话,将包处理并部署到一个可以运行集成测试的环境中

  6. verify:运行任何检查以验证包是有效的并且符合质量标准

  7. install:将包安装到本地存储库中,作为本地其他项目的依赖项使用

  8. deploy:在集成或发布环境中,将最终的包复制到远程存储库中,以便与其他开发人员和项目共享

    有更多的阶段, 7 但是你不需要知道所有的阶段,直到你在做更复杂的构建。

执行代码

然而,有时你只是需要更多的控制你的构建。在 Maven 中,可以执行 Groovy 代码、Ant 构建文件和 Scala 代码,甚至可以用 Groovy 编写自己的插件。

例如,您可以通过以下方式将 Groovy 代码放入 POM 文件中:

 1   <plugin>
 2    <groupId>org.codehaus.groovy.maven</groupId>
 3    <artifactId>gmaven-plugin</artifactId>
 4    <executions>
 5     <execution>
 6       <id>groovy-magic</id>
 7       <phase>prepare-package</phase>
 8       <goals>
 9         <goal>execute</goal>
10       </goals>
11         <configuration>
12           <source>
13             def depFile =
14             new File(project.build.outputDirectory, 'deps.txt')
15            project.dependencies.each {
16               depFile.write(
17                   "${it.groupId}:${it.artifactId}:${it.version}")
18             }
19             ant.copy(todir: project.build.outputDirectory ) {
20               fileset(dir: project.build.sourceDirectory)
21             }
22           </source>
23         </configuration>
24       </execution>
25     </executions>
26   </plugin>

前面的代码将把项目的每个依赖项写到文件deps.txt中。然后它会将所有的源文件复制到project.build.outputDirectory(通常是target/classes)。

img/435475_2_En_13_Figc_HTML.jpg参见 The Maven Cookbook 中的 2 、 3 和 4 章节。 8

格拉德尔

Gradle 是一个自动化的构建工具,带有 Groovy 原生 DSL(领域特定语言),用于定义项目构建。它还有一个 Kotlin 原生 DSL,是 Android 项目的官方构建工具。

在撰写本文时,Gradle 网站是这样描述 Gradle 的:

从移动应用到微服务,从小型创业公司到大型企业,Gradle 帮助团队更快地构建、自动化和交付更好的软件。

——格雷尔。org9

Gradle 入门

要轻松入门,可以使用 Gradle 内置的 init 插件。安装 Gradle 后,运行以下命令:

gradle init

出现提示时,您可以选择带有 Junit 4 测试和 Groovy 构建脚本的 Java 应用程序(键入 2,enter,3,enter,1,enter,1,enter)。这将创建一个build.gradle文件、一个settings.gradle文件、gradlewgradlew.bat(允许你从任何系统运行 Gradle,甚至不需要安装它)以及一些基本代码和一个测试。

项目和任务

每个 Gradle 构建由一个或多个项目组成,每个项目由一个或多个任务组成。

Gradle 构建的核心build.gradle文件,称为构建脚本。任务可以通过写task,然后写一个任务名,最后写一个闭包来定义。例如:

1   task upper doLast {
2           String someString = 'test'
3           println "Original: $someString"
4           println "Uppercase: " + someString.toUpperCase()
5   }

就像在 Ant 中一样,一个任务可以依赖于其他任务,这意味着它们必须在该任务之前运行。在您的任务定义中,您可以使用任意数量的任务名称将dependsOn指定为List。例如:

1  task buildApp(dependsOn: [clean, jar]) {
2      // define task here
3  }

任务可以包含任何 Groovy 代码(或者如果使用 Kotlin DSL,可以包含 Kotlin 代码),但是您可以利用一些现有的 Gradle 插件来快速生成可靠且快速的构建。

插件

Gradle core 几乎没有内置。它有强大的插件,这使得它非常灵活。插件可以执行以下一项或多项操作:

  • 向项目添加任务(例如,编译、测试)。

  • 使用有用的默认值预先配置添加的任务。

  • 将依赖项配置添加到项目中。

  • 通过扩展向现有类型添加新的属性和方法。

我们将专注于构建基于 Java 的项目,所以我们将使用java插件;然而,Gradle 并不局限于 Java 项目。

例如,在您的build.gradle文件的开头,您应该会看到类似以下代码的内容:

1   plugins {
2      id 'java'
3      id 'application'
4   }

这将启用java插件和application插件。

Gradle 使用标准的项目组织惯例。例如,它期望在src/main/java下找到您的生产 Java 源代码,在src/test/java下找到您的测试 Java 源代码。这也是 Maven 所期待的风格。

属国

每个 Java 项目都倾向于依赖许多开源项目来构建。Gradle 建立在它之前的概念之上,因此您可以使用一个简单的 DSL 轻松定义您的依赖关系,如下例所示:

 1   plugins { id 'java' }
 2
 3   sourceCompatibility = 1.11
 4
 5   repositories {
 6      mavenLocal()
 7      mavenCentral()
 8   }
 9
10   dependencies {
11      implementation 'com.google.guava:guava:23.0'
12      implementation 'io.reactivex.rxjava2:rxjava:2.2.10'
13      testImplementation group: 'junit', name: 'junit', version: '4.12+'
14      testImplementation "org.mockito:mockito-core:1.9.5"
15   }

这个构建脚本使用sourceCompatibility来定义Java 11的 Java 源代码版本(在编译过程中使用)。接下来,它告诉 Maven 首先使用本地存储库(mavenLocal),然后使用 Maven Central。

dependencies块中,这个构建脚本为implementation范围和testImplementation范围定义了两个依赖项。testImplementation范围内的罐子仅用于测试,不会包含在任何最终产品中。

JUnit 行显示了定义依赖关系的更详细的样式。这里的“+”表示该版本或更高版本。

从头到尾做

您可以使用doFirstdoLast来指定应该在任务之前和之后运行的代码。例如,让我们从前面的任务开始,添加一些额外的代码:

1  task buildApp(dependsOn: [clean, jar]) {
2      doFirst { println "starting to build" }
3      doLast { println "done building" }
4  }

这将在任务运行之前打印出“开始构建”,在任务完成之后打印出“完成构建”。在命令提示符下键入gradle buildApp来运行该任务。您应该会看到类似如下的输出:

> Task :buildApp
starting to build
done building

BUILD SUCCESSFUL in 983ms
4 actionable tasks: 4 executed

您的 jar 文件现在将位于项目的build/libs/目录中。

img/435475_2_En_13_Figd_HTML.jpg Gradle 有一个庞大的在线用户指南,可在 gradle 上获得。org10

十四、测试

测试是软件创建过程中非常重要的一部分。没有自动化测试,错误很容易潜入软件。

事实上,有些人甚至认为应该在编写代码之前先编写测试。这被称为 TDD (测试驱动开发)。

有多种测试框架和工具可以帮助你测试你的代码。这本书将涵盖其中一些工具,JUnit 和 Spock。

测试类型

以下是您可以编写的不同类型的测试:

  • 单元测试:对单个 API 调用或一些孤立的代码或组件进行的测试

  • 集成测试:将两个或多个组件集成在一起的大型系统的测试

  • 验收测试:符合业务需求的高级测试

  • 兼容性:确保所有的东西都能协同工作

  • 功能性:确保工作正常

  • 黑盒:在不知道/不考虑代码中发生了什么的情况下进行的测试

  • 白盒:考虑到代码内部而编写的测试

  • 灰盒:黑白盒混合测试

  • 回归:在发现一个 bug 后创建一个测试,以确保 bug 不再出现

  • 烟雾:测试中使用的大量数据样本

  • 负载/压力/性能:系统如何处理负载(例如,一个网站的大量流量)

基于许多因素,您编写的测试的类型和数量会有所不同。一段代码越简单,需要的测试就越少。例如,getter 或 setter 本身不需要测试。

单元测试

JUnit 1 是一个编写可重复测试的简单框架。

典型的 JUnit 测试由用@Test注释标注的多个方法组成。

在每个 JUnit 测试类的顶部,您应该包括以下导入:

1   import static org.junit.jupiter.api.Assertions.*;
2   import org.junit.jupiter.api.Test;
3   import org.junit.jupiter.api.BeforeEach;
4   import org.junit.jupiter.api.AfterEach;

使用@BeforeEach注释每次测试前运行的初始化方法,使用@AfterEach注释每次测试后运行的分解方法。这些方法应该确保每个测试都是独立的。

每个测试方法应该测试一件事,方法名应该反映测试的目的。例如:

1   @Test
2   public void mapSizeIsCorrect() {
3

   Map<String,String> map = new HashMap<>();
4        map.put("Smaug", "deadly");
5        map.put("Norbert", "cute");
6        assertEquals(2, map.size());
7   }

assertEquals方法的第一个参数是预期值,第二个参数是要测试的实际值。当两个值不相等时,这将抛出一个异常,测试将失败。失败的测试意味着代码没有满足我们的期望。在这一点上,软件应该被认为是不正确的,并且在测试成功(没有失败)之前不要再进一步。

哈姆克雷斯特

您可以使用 Hamcrest 核心匹配器创建更具可读性的测试。在 JUnit 中,您必须单独导入 Hamcrest 2 匹配器。您可以按如下方式导入它们:

1  import static org.hamcrest.CoreMatchers.equalTo;
2  import static org.hamcrest.CoreMatchers.is;
3  import static org.hamcrest.MatcherAssert.assertThat;

下面是一个使用 Hamcrest 匹配器的示例:

1   @Test
2   public void sizeIs10() {
3            assertThat(map.size(), is(2));
4   }

这个测试将断言 map 的大小是 2。还有许多其他匹配器,你甚至可以建立自己的。

假设

通常,在测试之外有一些超出你控制的变量,但是你的测试假设这些变量是真实的。当一个假设失败时,它不一定意味着你的测试失败。为此,JUnit 添加了Assumptions,您可以像这样导入它:

1   import static org.junit.jupiter.api.Assumptions.*;

您可以在测试断言之前验证假设。例如,如果将以下内容放在测试的开头,则该测试的其余部分只能在使用“/”作为文件分隔符的系统上运行(即,不是 Windows):

1   assumeTrue(File.separatorChar, is('/'));

当假设失败时,测试要么被标记为通过,要么被忽略,这取决于 JUnit 的版本。 3

斯波克

Spock 是 Java、Groovy 或任何其他 JVM 语言的测试框架。它充分利用了 Groovy 的优势,并且内置了对象模仿。斯波克网站 4 这样描述斯波克:

  • 让它脱颖而出的是其优美且极具表现力的规范语言。由于它的 JUnit runner,Spock 与大多数 ide、构建工具和持续集成服务器兼容。斯波克的灵感来自于 JUnit、RSpec、jMock、Mockito、Groovy、Scala、Vulcans 和其他迷人的生命形式。

史巴克基础

Spock 中的测试类被称为规范。Spock 中规范的基本结构是一个扩展spock.lang.Specification的类,有多个测试方法(可能有描述性的字符串名)。类名应该以Spec结尾,例如,一个关于Vampire类的规范可以被命名为VampireSpec

Spock 处理测试代码,并允许您使用基于 Groovy 的语法来指定测试。Spock 测试应该放在src/test/groovy目录下。

每个测试都由带有标签的代码块组成,比如whenthenwhere。学习斯波克最好的方法是用例子。

首先,将 Spock 作为一个依赖项添加到您的项目中。例如,在 Gradle 构建文件中,放置以下内容:

1   dependencies {
2    testImplementation "org.spockframework:spock-core:1.3-groovy-2.5"
3   }

简单的测试

让我们从重新创建一个简单的测试开始:

1   def "toString yields the String representation"() {
2           def array = ['a', 'b', 'c'] as String[]
3           when:
4           def arrayWrapper = new ArrayWrapper<String>(array)
5           then:
6           arrayWrapper.toString() == '[a, b, c]'
7   }

如图所示,断言只是 Groovy 条件表达式。在then:之后的每一行都将被测试 Groovy 的真实性。如果==表达式返回false,测试将失败,Spock 将给出详细的打印输出来解释失败的原因。

在没有任何when子句的情况下,可以用expect子句代替then;例如:

1   def "empty list size is zero"() {
2           expect: [].size() == 0
3   }

嘲弄的

模仿是指你使用一个工具来扩展一个接口或类,这个接口或类可以在测试中模仿那个类或接口的行为来帮助测试你的代码。在 JUnit 测试中,你需要使用一个库,像 Mockito5来模拟其他不属于测试的类。

在 Spock 中模仿接口或类是非常容易的。 6 简单使用Mock方法,如下例所示(其中Subscriber为接口):

1   class  APublisher  extends  Specification {
2     def  publisher = new  Publisher()
3     def subscriber = Mock(Subscriber)

现在subscriber是被嘲讽的对象。您可以简单地使用重载的>>操作符来实现方法,如下所示。每当调用receive时,下面的例子抛出一个Exception:

1   def "can cope with misbehaving subscribers"() {
2       subscriber.receive(_) >> { throw   new   Exception() }
3
4       when:
5       publisher.send("event")
6       publisher.send("event")
7
8       then:
9       2 * subscriber.receive("event")
10   }

预期的行为可以通过使用一个数字或范围乘以(*)方法调用来描述,如下所示(它期望receive方法应该被调用两次)。

下划线(_)被视为通配符(很像 Scala)。

数据列表或表格

Spock 允许您使用数据列表或表格来更简单地在一个测试中测试多个测试用例。

例如:

1   def "subscribers receive published events at least once"(){
2       when: publisher.send(event)
3       then: (1.._) * subscriber.receive(event)
4       where: event << ["started", "paused", "stopped"]
5   }

重载的<<操作符用于为event变量提供一个列表。虽然这里是一个列表,但是任何Iterable的东西都可以使用。这具有为列表中的每个值运行测试的效果。

img/435475_2_En_14_Figa_HTML.jpg Ranges

这里的范围1.._表示“一次或多次”。你也可以用_..3来表示“三次或更少”的次数。

也可以使用表格格式的数据。例如,下面的测试有一个包含两列(名称和长度)的表:

1   def "length of NASA mission names"() {
2         expect:
3         name.size() == length
4
5         where:
6         name       | length
7         "Mercury"  | 7
8         "Gemini"   | 6
9         "Apollo"   | 6
10   }

在这种情况下,两列(namelength)用于替换expect块中的相应变量。可以使用任意数量的列。

您还可以添加@Unroll注释,以便每个表行产生一个单独的测试输出。然后,您可以使用#来引用列。例如,将先前测试的签名更改为以下内容:

1   @Unroll
2   def "length of NASA mission name, #name"() {

预期异常

使用then块中的thrown方法来期待抛出的Exception

1   def "peek on empty stack throws"() {
2       when: stack.peek()
3       then: thrown(EmptyStackException)
4   }

您也可以通过简单地将抛出的Exception分配给thrown()来捕获它。例如:

1   def "peek on empty stack throws"() {
2       when: stack.peek()
3       then:
4       Exception e = thrown()
5       e.toString().contains("EmptyStackException")
6   }

在编写了一些测试之后,用 Gradle 或 Maven 运行测试。对于 Gradle run ./gradlew test (results go under build/reports/tests/test/)。对于 Maven 运行mvn test (results are under build/surefire-reports/).

其他测试框架

不幸的是,我们没有时间讨论许多其他的测试框架。有些用于实现 web 应用程序的自动化浏览器测试,如 Geb 7 和 Selenium。88

其他的像黄瓜一样启用 BDD(行为驱动开发)。黄瓜使测试能够用接近简单的英语来编写。例如,测试本书前面的一些代码的场景:

Scenario: Longest Dragon name
  Given a list of Dragons
  When the longest Dragon name is found
  Then the name is "Norbert"

摘要

在这一章中,你已经学习了测试是软件开发的一个非常重要的部分,以及你应该编写的测试类型。您已经学习了如何使用 JUnit 和 Spock 来运行测试,并且学习了其他用于运行集成测试或 BDD 的测试框架。

十五、输入/输出

很多时候,在开发应用程序时,您需要读取或写入文件或网络连接。这些东西通常简称为输入输出或 I/O。

在这一章中,我们将介绍 Java 和 JVM 中一些可用于 I/O 的实用程序。

文件

在 Java 中,java.io.File类用来表示文件和目录。例如:

1   File file = new  File("path/file.txt");
2   File dir = new File("path/"); //directory

Java 7 增加了几个新的类和接口,用于在java.nio包下操作文件和文件系统。这种新的应用程序接口(API)允许开发人员访问许多以前无法从 Java API 获得的低级操作系统操作,例如WatchService和创建链接的能力(在 Linux/Unix 操作系统中)。

路径用于更一致地表示文件或目录路径。

1    Path path = Paths.get("/path/file");

这是以下内容的简写:

1   Path path = FileSystems.getDefault().getPath("/path/file");

下面的列表定义了 API 的一些最重要的类和接口:

  • 这个类专门由静态方法组成,这些方法操作文件、目录或其他类型的文件。

  • 这个类专门由静态方法组成,这些方法通过转换路径字符串或 URI 来返回路径。

  • WatchService:查看各种文件系统事件的界面,如创建、删除、修改等。

读取文件

要用 Java 读取文本文件,可以使用BufferedReader。例如:

1   public void readWithTry() {
2     Charset utf = StandardCharsets.UTF_8;
3     try (BufferedReader reader = Files.newBufferedReader      (path, utf)) {
4       for (String line = br.readLine(); line!=null; line =         br.readLine())
5          System.out.println(line);
6      } catch (IOException e) {
7        e.printStackTrace();
8     }
9   }

Java 的自动资源管理特性使得处理资源(比如文件)变得更加容易。在 Java 7 之前,程序员需要显式关闭所有打开的流,导致一些非常冗长的代码。通过使用前面的try语句,BufferedReader将自动关闭。

然而,在 Groovy 中,这可以减少到一行(省去异常处理),如下所示:

1   println path.toFile().text

Groovy 中的File类中添加了一个getText()方法,该方法只读取整个文件。一个getBytes()方法也可用于从文件中读取字节。

写文件

写文件类似于读文件。对于写入文本文件,应该使用PrintWriter。它包括以下方法(以及其他方法):

  • print(Object):直接调用toString()打印给定对象

  • println(Object):打印给定的对象,然后换行

  • println():打印换行符序列

  • printf(String format, Object...args):使用给定输入打印格式化字符串

例如,您可以使用PrintWriter轻松写入文件,如下所示:

1   public void printWithTry() {
2           try (FileOutputStream fos = new             FileOutputStream("books.txt");
3                 PrintWriter pw = new PrintWriter(fos)) {
4                    pw.println("Modern Java");
5            } catch (IOException e) {
6                     // log the exception
7            }
8   }

还有其他方式输出到文件,比如DataOutputStream,例如:

1   public void writeWithTry() {
2           try (FileOutputStream fos = new             FileOutputStream("books.txt");
3                 DataOutputStream dos = new                   DataOutputStream(fos)) {
4                    dos.writeUTF("Modern Java");
5            } catch (IOException e) {
6                     // log the exception
7            }
8   }

允许应用程序将原始 Java 数据类型写入输出流。然后,您可以使用DataInputStream将数据读回。如果你只是处理文本,你可以用PrintWriterBufferedReader来代替。

在 Groovy 中,可以更容易地写入文件,如下所示:

1   new File("books.txt").text = "Modern Java"
2   new File("binary.txt").bytes = "Modern Java".bytes

Groovy 在File类中添加了一个setText方法和一个setBytes,这使得这个语法可以工作。

下载文件

尽管在实践中您可能从来没有这样做过,但是用代码下载一个网页/文件是相当简单的。

下面的 Java 代码在给定的 URL 上打开一个 HTTP 连接(本例中是 https://www.google.com ),将数据读入一个字节数组,并打印出结果文本。

 1   URL url = new URL("https://www.google.com");
 2   InputStream input = (InputStream) url.getContent();
 3   ByteArrayOutputStream out = new ByteArrayOutputStream();
 4   int n = 0;
 5   byte[] arr = new  byte[1024];
 6
 7   while  (-1 != (n = input.read(arr)))
 8       out.write(arr, 0, n);
 9
10   System.out.println(new String(out.toByteArray()));

然而,在 Groovy 中,这也可以减少到一行(忽略异常)。

1   println "https://www.google.com".toURL().text

在 Groovy 中的String类中添加了一个toURL()方法,在URL类中添加了一个getText()方法。

摘要

阅读完本章后,您应该了解如何

  • 探索 Java 中的文件系统

  • 从文件中读取

  • 写入文件

  • 下载互联网

十六、版本控制

一旦人们开始他们的编程生涯,他们就会受到版本控制系统的沉重打击(VCS)。

版本控制软件用于跟踪、管理和保护对文件的更改。这是现代软件开发项目中非常重要的一部分。

这本书将涵盖两个流行的(但还有更多):

  • SVN(颠覆)

  • 去(去)

每个 VCS 都有以下基本动作:

  • Add

  • Commit

  • Revert

  • Remove

  • Branch

  • Merge

ide 有处理版本控制系统的插件,通常有对流行系统的内置支持,比如 SVN 和 Git。

破坏

SVN 1 这是一个巨大的飞跃。除了其他好处之外,它还允许将层次结构中的任何目录从系统中签出并使用。SVN 需要一个服务器来存储代码的历史、标签和分支。然后,程序员使用 SVN 客户端提交代码更改。

要开始在命令行上使用 SVN,您将签出一个项目,然后提交文件,例如:

1   svn checkout http://example.com/svn/trunk
2   svn add file
3   svn commit

饭桶

Git 2 是一个分布式版本控制系统。这意味着源代码的每个副本都包含代码的整个历史。然而,与大多数其他系统不同,它以一种非常紧凑、高效和安全的方式存储历史——每次提交都与一个哈希(一个紧凑但唯一的值,通过单向算法从较大的值中生成)相关联。它最初是由 Linux 的创造者(Linus Torvalds)创建的,非常受欢迎。

要开始在新项目中使用 Git,只需运行以下命令:

1   git init

创建一个名为README的文件,然后提交它,如下所示:

1   git add README
2   git commit -m "this is my comment"

img/435475_2_En_16_Figa_HTML.jpg安装 Git。转到 github.com 3 克隆一个库,例如git clone https://github.com/adamldavis/learning-groovy.git 。现在创建您自己的 GitHub 帐户,创建一个新的存储库,克隆它,并按照前面的说明向其中添加一个新文件。最后,使用git push将更改推送到 GitHub。

当您在本地 git 存储库中设置了一个远程主机时(就像您从 GitHub 克隆了一个存储库时),您可以向它推送更改,也可以从中提取更改。例如,git push命令将您的提交推送到远程主机,而git pull命令从主机获取更改(其他开发人员可能已经放在那里)。

其他有用的命令:

  • git log:显示所有提交,最近的先显示

  • git status:显示您的 git 库的当前状态

  • git show:给定一个提交散列,显示该提交的所有更改

  • git checkout:给定一个分支名称,加载该分支

  • git merge:将两个或多个开发历史结合在一起

  • git branch:可用于列出、创建或删除分支

  • git tag:可用于列出、创建或删除标签

  • 给你有用的文档。像git help <command>一样使用时给出帮助的具体命令

水银的

Mercurial 4 早于 Git,但与它非常相似。它被用于 Google Code 和 Bitbucket 上的很多项目。T55

img/435475_2_En_16_Figb_HTML.jpg安装 Mercurial。转到 Bitbucket 并使用 Mercurial 克隆一个存储库,例如,hg clone https://bitbucket.org/adamldavis/dollar

十七、互联网

(由 xkcd 提供:Interblag)

img/435475_2_En_17_Figa_HTML.jpg

现在,几乎所有的软件项目都是基于互联网的,要么是网络应用程序(生成 HTML 并通过 Firefox 等浏览器显示的程序),要么是网络服务(通过浏览器中的 JavaScript 或移动应用程序连接的程序,如 Android 和 iOS 设备上的程序)。

本章致力于学习与 web 应用程序和 web 服务相关的概念和一些代码。

网络 101

网络是一只复杂的野兽。你需要知道的是:

  • 服务器:提供网页和其他内容的计算机

  • 客户端:个人使用的接收网页的电脑

  • 请求:客户端发送给服务器的数据

  • 响应:请求后发送回客户端的数据

  • HTML:用来定义网页的语言

  • CSS :“级联样式表”;定义网页的样式

  • JavaScript:一种在网页中使用并在客户端执行的编程语言,尽管它也可以在服务器端使用

我的第一个网络应用

你应该为你的第一个 web 应用做一些非常基本的东西。这样,您将更好地理解许多 web 框架的“幕后”发生了什么。一个 web 框架是一组用于构建 web 应用程序的相关工具和库。

创建一个名为App.java的文件,并将以下代码复制到其中:

 1   import java.io.IOException;
 2   import java.io.OutputStream;
 3   import java.net.InetSocketAddress;
 4   import com.sun.net.httpserver.*;
 5
 6   public class App {
 7
 8       static class MyHandler implements HttpHandler {
 9           public void handle(HttpExchange t) throws IOException {
10               String response = "<html> Hello Inter-webs! </html>";
11               t.sendResponseHeaders(200, response.length());
12               OutputStream os = t.getResponseBody();
13               os.write(response.getBytes());
14               os.close();
15           }
16       }
17
18       public static void main(String[] args) throws Exception {
19           HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
20           server.createContext("/", new MyHandler());
21           server.setExecutor(null); // creates a default executor
22           server.start();
23           System.out.println("Server running at http://localhost:8000");
24       }
25
26   }

所有这些只是创建一个HttpServer来监听端口 8000 上的连接,并用一条消息来响应。

运行这段代码(javac App.java && java App)后,打开你的网络浏览器,指向http://localhost:8000/(它应该显示“Hello Inter-webs!”).按 Ctrl+C 停止应用程序。

img/435475_2_En_17_Figb_HTML.jpg localhost是指你所在的电脑,:8000是指端口 8000。

恭喜你!您刚刚制作了一个 web 应用程序!现在还没有放到网上,而且极其简单,但是这是一个很好的开始。

Port?

URL(统一资源定位符):用于在任何网络或机器上定位资源的唯一名称。有时以“http”开头;有时它包括一个端口。

HTTP 超文本传输协议(HTTP Hypertext Transfer Protocol):用于网络通信的典型协议。

HTTPS(安全 HTTP) :类似于 HTTP,但使用非对称密钥对所有数据进行编码,因此除了预定的接收者之外,任何设备都无法读取数据。

端口:计算机间通信时必须指定的一个数字(HTTP 默认端口为 80)。

圣杯

Grails 是一个 Groovy 的 web 框架,它遵循了 Ruby on Rails 的例子(因此有了 Grails )。这是一个自以为是的 web 框架,带有命令行工具,可以非常快速地完成工作。Grails 使用约定优于配置来减少配置开销。这可以大大减少开始新项目或添加额外功能所需的工作量。

Grails 牢牢地活在 Java 生态系统中,并建立在 Spring Boot 和 Hibernate 等技术之上。Grails 还包括一个对象关系映射(ORM)框架,它将对象映射到数据库表,称为 GORM,,并有一个大的插件集合。

快速概述

这个概述基于 Grails 4.0.0,但是对于所有版本的 Grails,3.0 和更高版本,基础知识应该是相同的。安装 Grails 后, 1 您可以通过在命令行上运行以下命令来创建一个应用程序:

1   $ mkdir g4
2   $ cd g4
3   $ grails create-app --inplace

然后,您可以运行诸如create-domain-classgenerate-all这样的命令来创建您的应用程序。运行grails help来查看可用命令的完整列表。我们将在本章后面更全面地介绍这些命令。

Grails 应用程序有一个非常特殊的项目结构。下面是该结构的大多数的简单分解:

  • grails-app:特定于 Grails 的文件夹。

    • conf:配置文件,如 application.yml、logback.groovy。

    • controllers:具有索引/创建/编辑/删除或任何其他方法的控制器。

    • domain:领域模型;表示持久数据的类。

    • 消息包,用于支持多种语言(英语、西班牙语等)。).

    • init:包含您的Application.groovyBootstrap.groovy文件,它们在应用程序启动时初始化应用程序。

    • services:后端服务,您的后端或“业务”逻辑就在其中。

    • 你可以很容易地定义你自己的标签,在你的 GSP 文件中使用。

    • views:MVC 的观点;通常,这些是 GSP 文件(基于 HTML 并嵌入 Groovy 代码)。

  • assets

    • stylesheets : CSS。

    • images:您的网络应用程序使用的图像。

    • javascripts:你的 JavaScript 文件。

  • 不适合其他地方的通用代码。

    • main/groovy : Groovy 代码。

    • 常规测试。

  • gradle:包含 Gradle 包装罐。

要创建新的域(模型)类,使用create-domain-class命令。运行以下命令(在项目的根目录下):

1   $ grails create-domain-class example.Comment
2   $ grails create-domain-class example.User
3   $ grails create-domain-class example.Post

为您的域类包含一个包是一个好主意(比如example.Post))。该命令创建域类和相关的 Spock 规范。将UserComment更改如下:

 1   class User { String name }
 2   class Comment { String text }

Grails 中的域类也定义了它到数据库的映射。例如,编辑表示博客文章的域类,如下所示(假设已经创建了UserComment):

 1   class Post {
 2       String text
 3       int rating
 4       Date created = new Date()
 5       User createdBy
 6
 7       static hasMany = [comments: Comment]
 8
 9       static constraints = {
10           text(size:10..500)
11       }
12   }

静态的hasMany字段是表示数据库中一对多关系的映射——这意味着一个Post可以有多个Comments。Grails 在后台使用 Hibernate 为所有的域类和关系创建表。默认情况下,每个表都有一个自动分配的主键字段id

要让 Grails 在定义域类之后自动创建控制器和视图(以及测试),请运行以下命令:

1   $ grails generate-all example.User
2   $ grails generate-all example.Comment
3   $ grails generate-all example.Post

如果现有文件存在,Grails 会询问您是否要覆盖它们。因此,使用该命令时要小心。

当您想要测试您的应用程序时,只需运行以下命令:

1   $ grails run-app

它最终应该输出以下内容:

Grails 应用程序运行于环境:开发中的http://localhost:8080

接下来,打开浏览器并转到该 URL。基于默认生成的视图,您应该会看到以下内容,其中包含控制器列表、应用程序状态、工件和已安装插件列表:

img/435475_2_En_17_Figd_HTML.jpg

如果您跟随链接到各自的控制器,您可以创建用户,然后创建帖子,然后在这些帖子上创建评论。

外挂程式

Grails 4.0 系统现在包括 190 多个插件。要列出所有插件,只需执行以下命令:

1   $ grails list-plugins

当你选择了一个你想要使用的插件,执行下面的命令来查看更多关于这个插件的信息(包括插件名称):

1   $ grails plugin-info [NAME]

这将告诉您如何将插件添加到项目中。编辑您的build.gradle文件,并在那里添加依赖项。

img/435475_2_En_17_Fige_HTML.jpg这只是对 Grails 的一个简要概述。已经有很多关于 Grails 以及如何使用它的书籍。关于使用 Grails 的更多信息,请访问 grails.org22

以下云提供商支持 Grails:

  • cloudfoundry【3】

  • 亚马逊4

  • 希罗库【5】

然而,这并不在本书的讨论范围之内,但是我们将很快讨论 Heroku。

Heroku 是首批云平台之一,自 2007 年 6 月开始开发。开始时,它只支持 Ruby,但后来它增加了对 Java、Scala、Groovy、Node.js、Clojure 和 Python 的支持。Heroku 支持多层账户,包括一个免费账户。

Heroku 依靠git将变化推送到你的服务器。例如,要使用 CLI 在 Heroku 中创建应用程序,请执行以下操作:

1   $ heroku create
2   $ git push heroku master

您的应用程序将启动并运行,Heroku 将识别您可以找到它的 URL。

去 Heroku 上启动一个 Grails 应用程序吧!

其余的

REST 代表具象状态转移。 6 它是在一篇博士论文中设计的,并作为新的 web 服务标准而广受欢迎。许多开发人员称赞它是比 SOAP 好得多的标准(我不打算描述 SOAP)。

在 REST 的最基本层面,每个 CRUD (创建、读取、更新、删除)操作都被映射到一个 HTTP 方法。例如:

  • 创建 : POST

  • 读作 : GET

  • 更新 : PUT

  • 删除 : DELETE

假设传输机制是 HTTP,但是消息内容可以是任何类型,通常是 XML 或 JSON。

JSR 社区已经设计了用于构建 RESTful Java web 服务的 JAX-RS API,而 Groovy 和 Scala 都有一些对 XML 和 JSON 的内置支持以及各种构建 web 服务的方式。Spring Boot 7 和春天 MVC 对休息也有很大的支持。

使用 Maven 原型

您可以使用 Maven 创建一个简单的 Java REST (JAX-RS)应用程序,如下所示:

1   mvn archetype:generate

等东西下载完再选“tomcat-maven-archetype”(键入tomcat-maven按回车键,再键入“1”);进入;回车)。你需要输入一个groupIdartifactId

创建应用程序后,您可以通过键入以下命令来启动它:

1   mvn tomcat:run

使用 Grails JSON 视图

Grails 有一个将视图呈现为 JSON 的插件。首先运行 plugin-info,看看如何将它包含在您的构建中:

1   $ grails plugin-info views-json

将它添加到 Grails 项目的构建依赖项之后,您可以使用 Groovy DSL 来定义如何在 JSON 中呈现您的响应。更多信息参见文档 8

总的来说,JSON views 允许您在带有.gson扩展名的grails-app/views目录下定义视图,这些视图可以使用 DSL 来生成 JSON,而不是使用生成 HTML 的.gsp文件。例如,这在编写生成 JSON 的 web 服务时非常有用。

使用以下内容创建一个名为grails-app/views/hello.gson的文件:

json.message {
    hello "world"
}

这将产生作为 JSON 的{"message":{ "hello":"world"}}

摘要

恭喜你!你现在明白互联网了。是的,它一系列的管子。特德·史蒂文斯(见下文)是对的!

他们想通过互联网传递大量信息。再说一次,互联网不是你随便扔东西的地方。它不是一辆大卡车。这是一系列的管子。如果你不明白,这些管道可以被填满,如果它们被填满,当你把你的信息放进去,它会排队,它会被任何人延迟,把大量的材料放进那个管道,大量的材料。

——老西奥多·泰德·富尔顿·史蒂文斯,美国阿拉斯加州参议员,1968 年 12 月 24 日至 2009 年 1 月 3 日

十八、Swing 图形

Swing 是用于构建跨平台 GUI(图形用户界面)的 Java API。

如果你想写一个图形程序(例如,一个电脑游戏),你必须使用 Swing,或者 JavaFX,或者类似的东西。

在 Java 中有许多其他的图形库,但是 Swing 是内置的。

你好窗户

图形最基本的概念是把东西放到屏幕上。

在 Swing 中最简单的方法是使用JWindow,例如:

 1   import javax.swing.*; import java.awt.Graphics;
 2
 3   public class HelloWindow extends JWindow {
 4
 5           public HelloWindow() {
 6                    setSize(500, 500); //width, height
 7                    setAlwaysOnTop(true);
 8                    setVisible(true);
 9           }
10
11           @Override
12           public void paint(Graphics g) {
13                    g.setFont(g.getFont().deriveFont(20f));
14                    g.drawString("Hello Window", 10, 20); //x,y
15           }
16
17           public static void main(String[] args) {
18                    new HelloWindow();
19           }
20
21   }

运行这段代码将在屏幕的左上方创建一个窗口,上面印有“Hello Window”字样。它看起来应该如图 18-1 所示。

img/435475_2_En_18_Fig1_HTML.jpg

图 18-1

你好窗口

在构造器中,会发生以下情况:

  • 窗口的宽度和高度都设置为 500 像素。

  • setAlwaysOnTop方法将窗口设置为总是显示(在所有其他窗口之上)。

  • 最后,调用setVisible(true)使窗口可见。

每次在屏幕上绘制窗口时,都会调用paint方法。这种方法只完成以下工作:

  • 将字体大小设置为 20

  • 在坐标 x=10,y=20 处绘制字符串“Hello World”(坐标始终以像素为单位)

你可能会注意到“窗口”没有你习惯的任何边缘、标题、菜单或最小化/最大化图标(要关闭它,你需要按下 Ctrl+C )。要得到这些东西,你用一个JFrame。这里有一个非常简单的例子:

 1   import javax.swing.*;
 2
 3   public class HelloFrame extends JFrame {
 4
 5           public HelloFrame() {
 6                   super("Hello");
 7                   setSize(500, 500); //width, height
 8                   setAlwaysOnTop(true);
 9                   setVisible(true);
10                   setDefaultCloseOperation(EXIT_ON_CLOSE);
11           }
12
13           public static void main(String[] args) {
14                   new HelloFrame();
15           }
16
17   }

运行这段代码创建一个 500×500 的“带框架的窗口”,名称为“Hello”(图 18-2 ),关闭窗口将退出应用程序。

img/435475_2_En_18_Fig2_HTML.jpg

图 18-2

有 JFrame 的窗户

按我的按钮

按钮是用户与程序交互的方式之一。当按钮被按下时,你可以使用一个ActionListener,例如:

JOptionPaneshowMessageDialog方法类似于 JavaScript 的alert方法,显示一个弹出窗口。

1   button.addActionListener(e -> JOptionPane.showMessageDialog(this, "Hello!"));

这使用了一个 Java lambda 表达式,因为 ActionListener 有一个抽象方法,因此是一个函数接口,正如我们前面所学的。

Groovy 语法略有不同(它只需要一个{ and })。

1   button.addActionListener({e -> JOptionPane.showMessageDialog(this, "Hello!")})

Swing 有很多以单词Listener结尾的接口,比如

  • KeyListener

  • MouseListener

  • WindowListener

监听器模式与观察者设计模式非常相似。

假浏览器

让我们做一个网络浏览器吧!

让我们从添加必要的导入开始:

1  import java.awt.*;
2  import java.awt.event.*;
3  import java.io.*;
4  import java.net.*;
5  import javax.swing.*;

然后,让我们继续为该类创建字段和构造器,如下所示:

 1   public class Browser extends JFrame {
 2
 3           JTextField urlField = new JTextField();
 4           JEditorPane viewer = new JEditorPane();
 5           JScrollPane pane = new JScrollPane();
 6
 7           public Browser() {
 8                   super("Browser");
 9                   setSize(800,600);
10                   setAlwaysOnTop(true);
11                   setDefaultCloseOperation(EXIT_ON_CLOSE);
12                   init();
13           }

JTextField将用于输入网址。JEditorPane用于显示 HTML,JScrollPane允许页面滚动。

接下来,我们定义了init()方法来把所有东西放在一起。

 1   private void init() {
 2           viewer.setContentType("text/html");
 3           pane.setViewportView(viewer);
 4           JPanel panel = new JPanel();
 5           panel.setLayout(new BorderLayout(2,2));
 6           panel.add(pane, BorderLayout.CENTER);
 7           panel.add(urlField, BorderLayout.NORTH);
 8           setContentPane(panel);
 9           urlField.addKeyListener(new KeyAdapter() {
10                   @Override
11                   public void keyReleased(KeyEvent e) {
12                           handleKeyPress(e);
13                   }
14           });
15   }

viewer被设置为JScrollPane的视口视图,因此可以滚动。

JPanel是用BorderLayout创建的。这允许我们将urlField安排在滚动窗格的顶部,就像在真正的浏览器中一样。KeyListener用于在urlField内按下一个键时调用handleKeyPress

接下来,我们填写handleKeyPress方法。

 1   private void handleKeyPress(KeyEvent e) {
 2           if (e.getKeyCode() == KeyEvent.VK_ENTER) {
 3                   try {
 4                           viewer.setPage(new URL(urlField.getText()));
 5                   } catch (MalformedURLException ex) {
 6                           ex.printStackTrace();
 7                   } catch (IOException ex) {
 8                           ex.printStackTrace();
 9                   }
10           }
11   }

每当按下 Enter 键时,该方法简单地将页面JEditorPane设置为来自urlField的 URL。

最后,我们定义主方法。

1   public static void main(String[] args) {
2    new  Browser().setVisible(true);
3   }

从第十七章开始运行你的应用。打开你的假浏览器,指向http://localhost:8000/处的 app。它应该看起来像图 18-3 。

img/435475_2_En_18_Fig3_HTML.jpg

图 18-3

运行假浏览器

狮身鹫首的怪兽

Griffon 1 是一个受 Grails 启发的桌面应用平台。它是用 Java 编写的,所以可以从纯 Java 中使用,但是使用 Groovy 增加了额外的功能。

首先安装懒骨头 2 和 Gradle。您可以使用以下命令安装它们:

$ curl -s http://get.sdkman.io | bash
$ sdk install lazybones
$ sdk install gradle

接下来编辑 lazybones 配置文件以添加griffon-lazybones-templates存储库。编辑$USER_HOME/.lazybones/config.groovy并输入以下内容:

bintrayRepositories = [
    "griffon/griffon-lazybones-templates",
    "pledbrook/lazybones-templates"
]

要开始新的项目类型,请使用以下命令:

$  lazybones create griffon-swing-groovy griffon-example

这将使用 Groovy 和 Swing 创建一个名为griffon-example的项目。对每个提示填写适当的响应(它将要求您提供包、版本、类名和其他值)。使用lazybones list命令查看其他类型的项目是可能的。

Griffon 使用 MVC 设计模式和 Groovy DSL 使构建 Swing 应用程序变得更加容易。

高级图形

虽然远远超出了本书的范围,但有几个库可以用于 2D 或 3D 图形。以下是其中的一些:

爪哇 2D

  • Java FX3

  • jfree chart【4】

  • 小 2d【5】

  • jmagick【6】

Java 3D

  • jog【7】

  • jmmonkey yengene【8】

二维 JavaScript

  • D3 . js9

  • 高图【10】

JavaScript 3D

  • 三. js 11

图形词汇表

  • 组件:Java 图形 API 中定义的任何图形元素。

  • 双缓冲:图形中使用的一种技术,其中元素在被发送到计算机屏幕之前被绘制在内存中。这避免了闪烁。

  • 框架:在 Swing 中,框架(JFrame)用来表示我们通常所说的 GUI 中的“窗口”。

  • GUI :图形用户界面。

  • 布局:Swing 在面板或其他组件中排列组件时使用的策略。

  • 菜单:有两种菜单:windows 内置菜单(JMenu)和弹出菜单(JPopupMenu)。

  • 菜单项:在 Swing 中,JMenuItem表示菜单中的一行,可以有与之相关联的动作。

  • 面板:在 Swing 中,JPanel用来包含其他组件。

  • 像素:可绘制的屏幕最小单位。典型的屏幕有数百万像素排列在一个网格中。

  • 窗口:屏幕的矩形部分。在 Swing 中,Window 对象没有边框,所以它可以用于一个闪屏图像。

摘要

您刚刚学到了以下内容:

  • 用 Java 和 Groovy 创建跨平台 GUI

  • 如何让网页浏览器比 IE 差

  • 一些可用的图形库

十九、创造神奇的用户体验

首先,要开始学习在设计应用程序时考虑用户体验,您应该知道以下缩写:

  • UX :用户体验。使用应用程序的总体体验

  • UI :用户界面。用户使用的网页或图形界面

  • :简单点,笨蛋。总体设计理念

  • RWD:响应式网页设计(一种允许网页在多种不同设备上呈现的网页设计方法)

应用程序层次

您应该根据以下特征从高到低排列 UX 的优先级:

  1. 功能:软件做它应该做的事情。

  2. 有用性:软件好用吗?

  3. 效率:用户能否高效工作?

  4. 神奇:体验神奇吗?

如果你的软件没有功能,你就不能专注于可用性。如果你的软件不可用,你就不能专注于提高效率。

在你掌握了所有的基础知识(功能性、可用性和效率)之后,你才可以尝试让你的 UI 变得神奇。

考虑你的观众

为你的软件考虑受众总是很重要的。你应该尽可能多地了解他们。

你们中熟悉哈利波特(或魔法)的人会认出这些词:巫师/、女巫和麻瓜。在波特的世界里,哑炮是知道魔法但不会使用魔法的人,麻瓜是不知道魔法的正常人。

我们可以将这个类比应用于软件。当设计你的软件时,你需要记住每一类可能使用它的人:

  • 初始用户:麻瓜

  • 熟练用户:哑炮

  • 高度熟练用户:巫师/女巫

例如,如果你只为巫师和女巫设计,麻瓜会感到失落。如果你只为麻瓜设计,巫师们会觉得软件不完整而且太简单。

选择是一种幻觉

一个人的选择越多,就越需要思考。作为一名设计师,你应该做到以下几点:

  • 限制选择。

  • 为每一个可能的选择做好准备。

  • 为你的观众量身定制选择。

  • 验证用户输入,确保它符合预期。

你经常需要决定是给你的用户一个选择,还是替他们做出选择。

最简单的方法(对你来说)总是让用户决定,但是更好的方法通常是少给用户一个选择。这将使你的软件更简单,因此更容易使用。

方向

本能地工作——本能是你的朋友。动作是吸引用户注意力的一种微妙而有效的方式。然而,太多的运动会分散注意力,所以应该保持在最低限度。

另一种本能视觉是人脸。人脸首先被注意到。这就是为什么你总是在文本的左侧看到面孔(在从左向右阅读的语言中)。眼睛首先被吸引到脸部,然后是伴随的文字。

拟真

Skeuomorph 是现实生活中的东西,在软件中被模仿。

模拟现实生活中的特征,如边缘、斜面和按钮,对于交流可负担性(用户可以用某物做什么)是有用的。然而,如果你在模拟一个完整的物体(比如一本书),你必须 100%正确。这就是为什么 skeuomorphism 通常是一个坏主意。如果做得不完美,模仿真实世界的东西会被认为是假的。

你可以采取相反的方法,试图消除所有的隐喻。UI 可以是非常平坦和无边框的。然而,你可能把这个概念看得太远了。例如,什么是可点击的,应该仍然是显而易见的。

背景很重要

没有上下文的三颗星可能意味着任何事情。然而,给定上下文(3/5 颗星),其含义变得显而易见。

上下文对于导航也很重要。用户在软件中的位置以及如何导航到其他地方必须是显而易见的。否则,你的用户会感到失落,这不是一种舒服的感觉。

一个相关的概念是避免“模式”与软件交互的方式越多,它看起来就越复杂。

最重要的是,保持简单——对用户来说简单。例如,一般来说,在软件中应该总是有一种方法来做一些事情。此外,作为一般规则,你的用户界面应该遵循现有软件/网站设置的惯例(例如,总是给链接加下划线)。

随着软件的增长,你必须不断地选择新的 UI 特性。除了其他考虑之外,您还应该考虑如何使它们变得更简单。

你不是用户

除非你只是为自己开发软件,否则绝大多数的可能性是你的用户和你非常不同。因此,你不仅要试着像你的用户一样思考,还要真正了解他或她。这意味着理想情况下,你坐下来看你的用户操作软件。

同样,在生产系统中,你应该监控你的用户在做什么。他们在使用这项新功能吗?他们在做什么意想不到的事吗?指标有助于分析用户的行为。

摘要

从本章中,您应该已经了解了以下内容:

  • 你的 UI 应该是功能性的、可用的、高效的,按照这个顺序。

  • 在设计的所有阶段,考虑谁是你的用户。

  • 限制选择并处理所有情况。

  • 本能是你的朋友,但不要模仿现实。

  • 为用户保持简单,并倾听他们的意见。

关于可用性的更多信息,我强烈推荐史蒂夫·克鲁格的不要让我思考(新骑手,2014)。

二十、数据库

数据库是大多数软件项目极其重要的组成部分。简而言之,数据库是一个以标准格式存储数据的软件系统,根据数据库的不同,它可以实现以下一项或多项功能:快速存储和检索、执行复杂查询的能力、数据输入的验证以及对数据的计算。

可以追溯到几十年前的经典数据库被称为关系数据库。除了原始数据,它还存储数据库中表之间的关系。数据库通常由几个具有定义的约束的高度结构化的数据表组成。例如,表中的每一列都有一个类型,它是否可以为空,它是否必须是唯一的,以及其他约束。

有一种高度标准化的语言用于在数据库上执行操作和计算,称为 SQL (结构化查询语言)。SQL 已经存在很长时间了,可以很容易地保证它有自己的书,所以这本书将只涵盖基础知识。

自从所谓的大数据产品和网络应用(例如一个特定的“脸”主题社交网络)出现以来,第二类数据库已经出现: NoSQL非关系型数据库。通常,这些更像是键值或文档存储,而不是关系数据库。它们包括 Redis、MongoDB、Cassandra、DynamoDB 等数据库。

Note

SQL/NoSQL 分类过于简化,但它提供了比实际复杂现实更简单的叙述。换句话说,“这里有龙!”

SQL(关系)数据库

经典关系数据库的一部分是 ACID(原子性、一致性、隔离性、持久性)的概念。总结一下 ACID,它意味着数据库总是处于一致的状态(带有强制约束),即使系统在更新过程中崩溃。例如,如果表中的一列被标记为“not null”,它将永远不会为 null。它还支持事务,它们是工作的原子单位——要么全部发生,要么什么都不发生。乍一看,这似乎是一件简单的事情,但实际上这是一个非常复杂的问题。

在关系数据库中,数据的主要存储被称为表。一个表有,它们是表模式(定义)的一部分,定义具体存储哪种数据。一个表有,它是存储在表中的数据,每一行都有为每一列定义的值。

一些好的开源数据库包括 PostgreSQL、MySQL 和 H2。对于本节,您可以安装 PostgreSQL。在 Debian 或 Ubuntu Linux 上键入“sudo apt install postgresql”。对于其他系统,请参见网站 2 获取安装说明。DBeaver 是一个很好的连接和操作数据库的图形化工具。 3

结构化查询语言

关系数据库的基本语言是 SQL。它包括定义表和对这些表执行复杂查询的能力。

例如,创建一个表如下所示:

1   CREATE TABLE dragon(
2       dragon_id INTEGER,
3       dragon_name VARCHAR(100),
4       birth_date DATE NOT NULL,
5       PRIMARY KEY (dragon_id)
6      );

一个表总是需要有一个主键—它充当表中每一行的标识符,所以它在表中的每一行都必须是唯一的。这种情况下,主键是dragon_id

接下来,您可以使用insert语句向表中添加数据。例如,在 PostgreSQL 中,您可以通过以下方式插入两行:

insert into dragon values (1, 'Smaug', current_date);
insert into dragon values (2, 'Drago', current_date);

数据库类型涵盖了基础知识,如INTEGER,但其他不熟悉的类型包括:

  • VARCHAR(长度)类似于String对象。它有一个给定的最大长度。

  • TIMESTAMP用于存储日期和时间。

  • NUMERIC(precision, scale)DECIMAL(precision, scale)用于存储货币值等数字(例如,数字 123.45 的精度为 5,小数位数为 2)。

  • BLOB通常用于存储二进制数据。

select语句允许您指定要从一个或多个表中提取哪些列。您还可以在select语句中使用MINMAXCOUNT等聚合函数来执行更复杂的查询。例如,要查找最老的龙的生日,可以执行以下查询:

1   SELECT MIN(birth_date) FROM dragon;

可以使用一个where子句将查询限制在表中的某些行。要选择名称以 S 开头的所有龙(按字母顺序),运行以下命令:

1   SELECT dragon_id, dragon_name FROM dragon
2       WHERE dragon_name LIKE 'S%'
3       ORDER BY dragon_name;

order by子句用于对查询返回的结果进行排序。like关键字用于根据匹配表达式匹配varchar列,其中%匹配任何值。

外键

外键只是一个表中引用另一个表的主键的一列。

例如,假设你有一张wizard桌子,每个巫师都有多条他们作为宠物饲养的龙。

1   CREATE TABLE wizard(
2       wizard_id INTEGER,
3       wizard_name VARCHAR(100),
4       PRIMARY KEY (wizard_id)
5      );

如果wizard表的主键是wizard_id,那么dragon表可能会有如下带有所有者列和外键约束的新定义:

1   CREATE TABLE dragon(
2       dragon_id INTEGER,
3       dragon_name VARCHAR(100),
4       birth_date DATE NOT NULL,
5       PRIMARY KEY (dragon_id)
6       owner INTEGER,
7       FOREIGN KEY owner REFERENCES wizard (wizard_id)
8      );

虽然 SQL 关键字以大写形式显示,但这并不是 PostgreSQL 所要求的,只是为了便于说明。

连接

数据库系统通常作为一个单独的进程运行,您的代码以某种方式连接到它。

有许多不同的方法可以做到这一点。在 Java 中,连接数据库的最基本标准被称为 JDBC。

它允许您在数据库上运行 SQL 语句。您将需要一个特定的驱动程序——一个为您的数据库实现 JDBC 标准— 的库。

在实际应用程序中,您还应该使用 JDBC 连接池,比如 HikariCP。 4 连接池允许应用程序多次重用连接,这提高了应用程序的吞吐量和性能,因为连接启动需要时间。

还有对象关系映射 (ORM)框架,比如 Hibernate。 5 这些框架让你把 Java 对象映射到数据表。它们是在 JDBC 的基础上建造的。例如,Hibernate 有自己的查询语言,叫做 HQL,由 Hibernate 翻译成 SQL。我们前面讨论过的 GORM 默认使用 Hibernate。

或者,有代码生成框架允许您使用 DSL 进行查询。jOOQ 就是这样一个 Java 框架。 6 它允许你用本地语言编写类型安全的查询。例如:

1   create.selectFrom(DRAGON)
2     .where(DRAGON.NAME.like("S%"))
3     .orderBy(DRAGON.NAME)

NoSQL(非关系)数据库

大型网络项目(如维基百科)在使用关系数据库扩展到数百万用户时存在问题。他们必须将数据库划分到多台机器上(称为分片),这破坏了外键引用。关于这一点有一个定理,CAP 定理,它说你可以在一个数据库中拥有一致性、可用性和分区中的两个,但不是所有三个。因此,随着时间的推移,大数据项目转移到了 NoSQL 或非关系数据库,这做出了不同的权衡,因此它们可以更容易地扩大规模。很多时候,权衡是最终一致性而不是完全一致性。换句话说,在另一个用户输入新值后,一个用户可能会在短时间内读取旧值。

网飞、Reddit、Twitter、GitHub、Pinterest、易贝、eHarmony、craigslist 和许多其他网站都使用 NoSQL 数据库。

Note

我将在这里介绍一些 NoSQL 数据库,但还有许多其他的数据库。

使用心得

Redis 7 是键值存储。在 Redis 中,一切都以字符串形式存储,包括二进制数据。它是用 C 写的,有一长串命令。88

有多个客户端可以使用许多不同语言的 Redis,包括 Java、Node.js、Scala、Ruby、Python 和 Go。

MongoDB

MongoDB 9 是文档数据库。它存储 JSON 风格(JavaScript)的文档,并具有丰富的查询语法。它是用 C++写的,但是 JavaScript 可以用在查询和聚合函数中。

MongoDB 支持文档中任何字段的索引。它使用分片进行水平扩展,并使用复制提供高可用性和更高的吞吐量。最近,它增加了对 ACID 事务的支持。

MongoDB 也可以用作文件系统。

卡桑德拉

卡珊德拉 10 最初是在脸书开发的,并于 2008 年 7 月作为开源项目发布。它是用 Java 编写的,现在是一个成熟的顶级 Apache 项目。

Cassandra 是可伸缩的、分散的、容错的,并且具有可调的一致性。它还使用复制来提高容错能力和性能。

Cassandra 有一个类似 SQL 的替代方案,叫做 CQL (Cassandra 查询语言)。语言驱动程序可用于 Java (JDBC)、Python (DBAPI2)和 Node。JS(海莱纳斯)。

VoltDB

VoltDB 11 提供了 SQL/NoSQL 划分的反例。它是分布式的,内存中的,速度极快,但它也是一个关系数据库,并支持 SQL。

摘要

  • 有两种主要类型的数据库:SQL 和 NoSQL 或关系和非关系。

  • 关系(SQL)数据库是高度结构化的、一致的和持久的,但是难以扩展。

  • 大数据项目倾向于使用非关系数据库,这就像键值存储,可以更容易地扩展。

posted @ 2024-08-06 16:41  绝不原创的飞龙  阅读(24)  评论(0编辑  收藏  举报