Java-Web-开发学习手册-五-

88 阅读59分钟

Java Web 开发学习手册(五)

原文:Learn Java for Web Development

协议:CC BY-NC-SA 4.0

八、使用 Java 和 Scala 玩游戏

哦,把它不好的部分扔掉。和另一半一起更纯洁地生活。

—威廉·莎士比亚

Java EE 继续在一种良性的连续体中发展。受 Spring 等框架的启发,Java EE 引入了注释和依赖注入等特性来处理事务和数据库连接等复杂任务。撰写本文时的最新版本 Java EE 7 进一步加强了 Java 持久性 API (JPA)和基于 REST 的 web 服务的 JAX-RS 的进步,这只是其中的几个例子。Java web 开发所涉及的大部分复杂性将继续通过商业或开放 web 框架的创新在许多方面得到解决。

然而,Play 2 web 框架偏离了常规。play 2不是以 Java EE 为中心,并且不受 Java EE 的约束。它是 Typesafe 堆栈的一部分,提供了 Java EE 堆栈的替代方案。Typesafe 重新定义了以前由 Java EE 定义的现代 Java 应用的层,在这个新的划分中,它构成了 web 层。

Play 是一个开源的 web 应用框架,用 Scala 和 Java 编写,为现代 Web 提供开箱即用的支持。Play 是为现代 web 和移动应用的需求而构建的,利用了 REST、JSON 和 Web Sockets 等技术。

Play 以 JVM 为目标,并通过提供诸如约定优于配置、热代码重载和浏览器中的错误等特性来提高开发人员的工作效率。Play 通过将 HTTP 请求映射到控制器和表示结果的视图模板的路由文件来实现 MVC 架构。Play 2 通过提供对 Scala 编程语言的本地支持,构建在函数范式之上,并提供了一个改编的特定于 Java 的 API,该 API 形成了一个高度反应性的 web 框架。

游戏 2 的特点

Play 2.0 于 2012 年与 Typesafe 堆栈一起发布。Play 2 使用 Scala 作为核心语言构建,而 Play 1 使用 Java 语言并通过插件提供对 Scala 的支持。Play 2.2 发布于 2013 年 9 月。表 8-1 描述了 Play 2 的主要特征。

表 8-1 。重头戏 2 的主要特点

|

特征

|

描述

| | --- | --- | | 异步输入输出 | 由于使用 JBoss Netty 1 作为 web 服务器,Play 2 可以异步服务长请求。 | | 内置 web 服务器 | Play 2 提供了开箱即用的 JBoss Netty web 服务器,但是 Play web 应用也可以打包成 WAR 文件分发到 Java EE 应用服务器。 | | 依赖性管理 | Play 2 提供 sbt 2 进行依赖管理。 | | 热重装 | 在基于 Play 2 的应用中,每次新请求到达时,都会检查开发模式下的代码是否有更新,并且任何更改的文件都会自动重新编译;如果有任何错误,错误会直接显示在浏览器中。 | | 内存数据库 | 像 Grails 一样,Play 2 支持开箱即用的 H2。 | | 原生 Scala 支持 | Play 2 本身使用 Scala,但完全可以与 Java 互操作。 | | 对象关系映射(Object Relation Mapping) | Play 2 提供了 Ebean 3 作为 JPA 访问数据库的 ORM 替代。 | | 无国籍的 | Play 2 是完全 RESTful 的,每个连接都没有 Java EE 会话。 | | 模板 | Play 2 使用 Scala 作为模板引擎。 | | 测试框架 | Play 2 为单元测试和功能测试提供了一个内置的测试框架,比如 JUnit 和 Selenium。 4 | | Web 套接字 | Play 2 实现了开箱即用的 Web 套接字,以实现客户机和服务器之间的双向连接。 |

游戏 2 中的 MVC

Play 2 应用遵循 MVC 架构模式。在 Play 2 应用中,MVC 层在 app 目录中定义,每个层都在一个单独的包中(见图 8-1 )。

9781430259831_Fig08-01.jpg

图 8-1 。行动 2 中的 MVC

图 8-1 中所示的 MVC 架构中的请求流如下:

  1. 路由器收到一个 HTTP 请求。
  2. 路由器找到控制器中定义的动作来处理该请求。
  3. 控制器监听 HTTP 请求,从请求中提取相关数据,并将更改应用于模型。
  4. 控制器呈现模板文件以生成视图。
  5. 然后,action 方法的结果被写成一个 HTTP 响应。

路由器

web 应用的主要入口点是 conf/routes 文件,它定义了应用所需的路由。每个路由都由一个 HTTP 方法和一个与动作方法调用相关联的 URI 模式组成。Conf/routes 是称为路由器的内置组件使用的配置文件,它将每个传入的 HTTP 请求转换为操作调用。

注意HTTP 方法可以是 HTTP 支持的任何有效方法(GET、POST、PUT、DELETE 和 HEAD)。

控制器

在基于 Java EE 的 web 应用中, 控制器是一个扩展 servlet 类型的 Java 类。因为 Play 不是以 Java EE 为中心的,所以 Play 2 中的控制器是 Java 中的一个类,或者是 Scala 中的一个对象,它扩展了控制器类型(Java 和 Scala 中都是如此)。play.api.mvc 包中提供了这种控制器类型。Play 2 中的控制器包含一个名为动作的公共静态方法。动作基本上是一种处理请求参数并产生要发送给客户机的结果的方法。控制器响应请求,处理它们,并调用模型上的更改。

默认情况下,控制器是在源根目录(app 文件夹)下的控制器包中定义的。

注意控制器是一种扩展 play.api.mvc 包中提供的控制器的类型。

模型

模型 是应用操作的信息的特定领域表示。这种表示最常用的对象是 JavaBean。然而,JavaBeans 会导致大量样板代码。Play 2 和 Grails 一样,通过字节码增强为您生成 getters 和 setters,减少了这种样板代码。例如,如果需要将模型对象保存到持久性存储中,那么它们可能包含诸如 JPA 注释之类的持久性工件。

注意即使 Play 2 对 ORM 使用 Ebean,您也可以继续在您的实体上使用 JPA 注释。

视角

在基于 Java EE 的 web 应用中,视图通常是使用 JSP 开发的。也就是说,基于 Java EE 的 web 应用中的视图由 JSP 元素和模板文本组成。由于 Play 不是以 Java EE 为中心的,因此 Play 中的视图由混合了 HTML 和 Scala 代码的模板组成。在 Play 1 中,模板基于 Groovy,但是从 Play 2 开始,模板基于 Scala。使用 Play 2,您可以开发基于 Java 和基于 Scala 的 web 应用,两者中的模板是相同的。

注意在 Play 1 中,模板基于 Groovy,但是从 Play 2 开始,模板基于 Scala。

玩耍入门

要运行 Play 框架,您需要 JDK 6 或更高版本。你可以从这里下载 Play 2:www.playframework.com/download。Play 2 有两种版本:标准版和类型安全激活器。

下载最新的单机版 Play 发行版,将归档文件解压到您选择的位置,并通过在环境变量对话框中添加/编辑 Path 变量来更新 Path 环境变量,使用 Play 安装的路径,如图图 8-2 所示。

9781430259831_Fig08-02.jpg

图 8-2 。添加/编辑路径变量

现在,在命令行工具中输入以下命令,检查是否已经正确设置了播放环境:

> play

如果 Play 安装正确,您应该会在控制台上看到输出,如图 8-3 中的所示。

9781430259831_Fig08-03.jpg

图 8-3 。验证 Play 2 是否安装正确

你也可以通过执行 help 命令获得一些帮助,如图图 8-4 所示。

> play help

9781430259831_Fig08-04.jpg

图 8-4 。游戏 2 中的帮助

现在您可以用 Play 创建您的第一个 Java web 应用了。让我们来玩吧!

Hello World Java 应用与播放

要创建一个新的应用,你只需使用 play 命令行工具,带参数 new,后跟新应用的名称,在这里是 helloworld,如图 8-5 所示。

9781430259831_Fig08-05.jpg

图 8-5 。创建 helloworld 应用

Play 2 会要求你指定你的应用是 Scala 还是 Java 应用,如图 8-6 所示。

9781430259831_Fig08-06.jpg

图 8-6 。指定应用是 Scala 还是 Java 应用

您必须指定 2,因为您想要创建一个 Java 应用。指定 2 为 Java 语言创建源文件和应用结构,如图 8-7 所示。

9781430259831_Fig08-07.jpg

图 8-7 。helloworld 项目的 创建

您可以使用 helloworld 目录中的 run 命令运行应用。为此,进入游戏控制台,如图 8-8 中的所示。

> cd helloworld
>play

9781430259831_Fig08-08.jpg

图 8-8 。进入游戏控制台

现在输入 run。这将启动运行您的应用的服务器。

$ run

控制台上的输出如下所示:

[helloworld] $ run
[info] Updating {file:/E:/ModernJava/play2-workspace/helloworld/}helloworld...
[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Resolving com.typesafe.play#play-java-jdbc_2.10;2.2.0 ...
  [info] Resolving com.typesafe.play#play-jdbc_2.10;2.2.0 ...
  [info] Resolving com.typesafe.play#play_2.10;2.2.0 ...
...............................................
  [info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
--- (Running the application from SBT, auto-reloading is enabled) ---
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
(Server started, use Ctrl+D to stop and go back to the console...)

正如您所看到的,控制台显示它已经启动了应用,并且一个 HTTP 服务器正在侦听端口 9000 上的 HTTP 请求。现在,您可以通过访问 URLlocalhost:9000/向该服务器发送请求。在请求服务器时,显示一个欢迎屏幕,如图图 8-9 所示。

9781430259831_Fig08-09.jpg

图 8-9 。Play 2 框架的默认欢迎页面

图 8-9 说明了默认的 Play 2 欢迎页面。对于初学者来说,应用的默认欢迎页面是一个很好的信息来源,建议您阅读一下。

run 命令在 helloworld 目录中创建应用的结构。其结构如图图 8-10 所示。

9781430259831_Fig08-10.jpg

图 8-10 。helloworld 应用的目录结构

图 8-10 中的各个文件夹描述如下:

  • app:这是所有服务器端源文件的根,比如 Java 和 Scala 源代码、模板和编译资产的源代码。在创建时,只创建两个子文件夹:控制器和视图,分别用于 MVC 架构模式的控制器和视图组件。您可以为 MVC 的模型组件添加目录 app/models。还有一个名为 app/assets 的可选目录,用于编译的资源,如 LESS 5 源和 CoffeeScript 6 源。

  • conf: The conf directory contains the application’s configuration files meant to configure the application itself, external services, and so on. There are two main configuration files:

    application.conf:应用的主配置文件,包含标准配置参数

    路线:路线定义文件

  • 项目:项目文件夹包含配置 Scala 构建工具 SBT 所需的所有文件。 7

  • public:这个目录包含图像、CSS 样式表和 JavaScript 文件的三个标准子目录。

注意存储在公共目录中的资源是静态资产,将由 web 服务器直接提供服务。

  • target: The target directory contains everything generated by the build system such as the following:

    classes:包含所有编译过的类(来自 Java 和 Scala 源代码)。

    classes_managed:这仅包含由框架管理的类(例如由路由器或模板系统生成的类)。

    resource_managed:这包含生成的资源,通常是编译的资产,如 LESS CSS 和 CoffeeScript 编译结果。

    src_managed:这包含生成的源代码,比如模板系统生成的 Scala 源代码。

  • test:最后一个文件夹将包含所有的测试文件以及框架提供的一些样本。

现在您已经看到了应用的目录结构,您将了解在测试 URL:localhost:9000时默认的欢迎页面是如何显示的。

conf/routes 文件中的每一行都是定义如何使用 HTTP 访问服务器组件的路由。如果您在 conf/routes 文件中看到生成的 routes 文件,您将看到第一条路由:

GET        /       controllers.Application.index()

这条路线由三部分组成:

  • GET:这是包含请求中使用的 HTTP 方法的路由(行)的第一部分。
  • /:这是路由中包含相对路径的第二部分。
  • 控制器。Application.index():这是路由中包含要调用的动作的第三部分。

路由中的这三个部分通知 Play 2,当 web 服务器收到对/ path 的 GET 请求时,它必须调用控制器。Application.index(),它调用驻留在控制器包中的应用类中的 index 方法。

现在你将看到控制器。Application.index 方法如下所示。为此,您需要打开 app/controllers/application . Java 源文件。这个文件在清单 8-1 中有说明。

清单 8-1 。应用控制器

 1.    package controllers;
 2.
 3.    import play.*;
 4.    import play.mvc.*;
 5.
 6.    import views.html.*;
 7.
 8.    public class Application extends Controller {
 9.
10.      public static Result index() {
11.        return ok(index.render("Your new application is ready."));
12.      }
13.
14.    }
  • 第 8 行:应用控制器类扩展 play.mvc.Controller。
  • 第 10 行:public static index()动作返回结果。所有操作方法都返回一个结果。结果表示要发送回浏览器的 HTTP 响应。
  • 第 11 行:这里,动作返回一个 200 OK 响应,带有 HTML 响应体。HTML 内容由模板提供。一个动作总是返回一个 HTTP 响应,这在 Play 2 中由结果类型表示。结果类型必须是有效的 HTTP 响应,因此它必须包含有效的 HTTP 状态代码。确定将其设置为 200。render()方法引用了 Play 2 中的模板文件。

模板在 app/views/index.scala.html 源文件中定义。这个文件在清单 8-2 中有说明。

清单 8-2 。index.scala.html

1.    @(message: String)
2.
3.    @main("Welcome to Play") {
4.
5.        @play20.welcome(message, style = "Java")
6.
7.    }
  • 第 1 行:Scala 语句以特殊的@字符开始。第一行定义了函数签名。这里它只接受一个字符串参数。模板就像一个函数,因此它需要参数,这些参数在模板文件的顶部声明。
  • 第 3 行:这一行用一个字符串参数调用一个名为 main 的函数。
  • 第 3 行到第 7 行:这些行组成了主功能块。
  • Line 5 : Line 5 使用了 Play 2 提供的一个叫 welcome 的功能。该函数呈现默认的欢迎 HTML 页面,它位于名为 main.scala.html 的文件中,该文件也位于 apps/views 文件夹中。
  • 第 5 行:欢迎函数有一个额外的参数 style,通知模板这是一个基于 Java 的应用,然后这个 style 参数被欢迎模板用来显示文档的链接。

为 Java 配置 Eclipse

Play 2 为您提供了使用 Eclipse IDE 的可能性。为此,您需要让 Play 2 生成 Eclipse 项目配置。你可以通过调用游戏控制台中的 Eclipse 来实现,如图 8-11 所示。

9781430259831_Fig08-11.jpg

图 8-11 。为 Eclipse 生成项目

现在你可以启动 Eclipse,如图图 8-11 所示,并将项目导入其中,如图图 8-12 所示。

9781430259831_Fig08-12.jpg

图 8-12 。选择工作区

导入项目时,选择文件image导入,在工作区选择常规image已有项目,点击下一步,如图图 8-13 所示。

9781430259831_Fig08-13.jpg

图 8-13 。导入项目

现在浏览你的文件系统,选择项目文件夹 helloworld,点击 OK,然后点击 Finish,如图 8-14 所示。

9781430259831_Fig08-14.jpg

图 8-14 。选择根目录

配置 Eclipse 项目所需的所有文件都会生成。您看到了如何创建一个项目并将其导入到您的开发环境中。现在您将修改应用。在 Application.java,改变 index 动作中响应的内容,如清单 8-3 所示。

清单 8-3 。修改指标动作

public static Result index() {
  return ok("Hello world");
}

索引动作现在将响应“Hello world”,如图 8-15 所示。

9781430259831_Fig08-15.jpg

图 8-15 。“你好,世界”

Play 2 在 play-2.2.0\samples\java\的 samples 文件夹中提供了一些示例应用。你可以运行 helloworld 应用,如图 8-16 所示。

9781430259831_Fig08-16.jpg

图 8-16 。Play 2 提供的 helloworld 示例应用

点击【提交查询】,根据选择显示用户名称,如图图 8-17 所示。

9781430259831_Fig08-17.jpg

图 8-17 。运行示例 helloworld 应用

您可以自己检查代码并改进应用。

Helloworld Scala 应用与 Play 2

如前所述,Play 2 允许您创建基于 Java 和基于 Scala 的 web 应用。生成基于 Scala 的应用的过程与生成 Java 应用是一样的。你可以创建一个 helloworld-scala 应用,如图 8-18 所示。

9781430259831_Fig08-18.jpg

图 8-18 。创建 helloworld-scala 应用

现在你可以从 Play 控制台使用 run 命令运行 helloworld-scala 项目,如图 8-19 所示。

9781430259831_Fig08-19.jpg

图 8-19 。为 helloworld-scala 应用播放控制台

现在你将看到 Play 2 为 helloworld-scala 生成的控制器(见清单 8-4 )。你可以在 hello world-Scala \ app \ controllers 中找到控制器。

清单 8-4 。Scala 中的应用控制器

 1.    package controllers
 2.
 3.    import play.api._
 4.    import play.api.mvc._
 5.
 6.    object Application extends Controller {
 7.
 8.      def index = Action {
 9.        Ok(views.html.index("Your new application is ready."))
10.      }
11.
12.    }
  • 第 6 行:正如你在第 6 行看到的,在 Java 中控制器是一个类,但是在 Scala 中控制器是一个对象。
  • 第 8 行:正如你在第 8 行看到的,在 Java 中 action 是一个静态方法,但是在 Scala 中 action 是一个函数(一个对象的方法)。
  • 第 9 行:如果与 Java 控制器进行比较,返回类型和关键字是缺失的。
  • 第 9 行 : Scala 使用了一种叫做 Action 的结构,这是一个代码执行器的块。

现在你已经看到了 Scala 中的控制器,它在语法上不同于 Java 控制器,是时候看看 helloworld-scala 中的模板了(见清单 8-5 ,你可以在 helloworld-scala\app\views 中找到它。

清单 8-5 。helloworld-scala 中的模板

1.    @(message: String)
2.
3.    @main("Welcome to Play") {
4.
5.        @play20.welcome(message)
6.
7.    }

正如您所注意到的,helloworld 和 helloworld-scala 中的模板是相同的,除了第 5 行。Scala 版本没有初始化样式参数,因为它的默认值是 Scala。

为 Scala 配置 Eclipse】

您可以将 Scala IDE 用于基于 Scala 的应用。Scala IDE 是一个 Eclipse 插件,你可以通过选择 Help image Install New Software 来安装这个插件。在“工作方式”字段中,输入插件的路径(scala-ide.org/download/current.html),如图 8-20 中所示。你还可以在 scala-ide.org/documentati… Scala 配置 Eclipse 的详细说明。](scala-ide.org/documentati…)

9781430259831_Fig08-20.jpg

图 8-20 。为 Scala IDE 安装 Eclipse 插件

要导入项目,只需重复前面在 helloworld Java 应用中为 Eclipse 生成项目配置时执行的相同步骤。

现在您可以修改应用控制器中的 index 动作来显示“Hello world”,如清单 8-6 所示。

清单 8-6 。修改索引动作以显示“Hello world”

def index = Action {
    Ok("Hello world")
  }

一个基本的 CRUD Play 2 Java 应用

在本节中,您将学习编写一个简单的 CRUD 应用,它允许您创建、查看、编辑和删除书籍。对于这些操作,您需要动作和 URL 来调用这些动作。这个应用的代码可以在 Apress 网站的可下载文档中找到。

定义路线

第一步是在 conf/routes 文件中为这些操作定义路线,如清单 8-7 所示。

清单 8-7 。编辑会议/路线文件

1.    # Home page
2.    GET     /                    controllers.Application.index()
3.
4.    # Books
5.    GET     /books               controllers.Application.books()
6.    POST    /books               controllers.Application.newBook()
7.    POST    /books/:id/delete    controllers.Application.deleteBook(id: Long)
  • 第 5 行:在第 5 行中,您创建了一个列出所有书籍的路径。
  • 第 6 行:在第 6 行中,您创建了一个处理图书创建的路由。
  • 第 7 行:在第 7 行中,您创建了一个处理删除的路由。处理图书删除的路由在 URL 路径中定义了一个变量参数 ID。然后,该值被传递给 deleteBook 操作。

现在如果你刷新你的浏览器,你会看到 Play 2 不能编译你的 routes 文件,如图 8-21 所示。

9781430259831_Fig08-21.jpg

图 8-21 。路线文件编译错误

Play 无法编译您的路线文件,因为它引用了不存在的动作。下一步是将这些操作添加到 Application.java 文件中。

创建控制器和动作

在本节中,您将创建动作,如清单 8-8 中的所示。

清单 8-8 。图书应用中的应用控制器

 1.    public class Application extends Controller {
 2.
 3.      public static Result index() {
 4.        return ok(index.render("Your new application is ready."));
 5.      }
 6.
 7.      public static Result books() {
 8.        return TODO;
 9.      }
10.
11.      public static Result newBook() {
12.        return TODO;
13.      }
14.
15.      public static Result deleteBook(Long id) {
16.        return TODO;
17.      }
18.
19.    }
  • 第 7、11 和 15 行:这些行显示了在清单 8-7 中的 routes 文件中指定的动作。
  • 第 8、12 和 16 行:使用内置结果 TODO,返回“未实现”响应 503。这个结果告诉 Play 2,稍后将提供动作实现。当您通过localhost:9000/books访问该应用时,您会看到在图 8-22 中显示的结果。

9781430259831_Fig08-22.jpg

图 8-22 。Play 2 中的内置 TODO 结果

创建模型

下一步是定义可以存储在关系数据库中的模型书。为此,在 app/models/Book.java 文件中创建一个类,如清单 8-9 所示。

清单 8-9 。Book.java

 1.    package models;
 2.    import java.util.*;
 3.    public class Book {
 4.      public Long id;
 5.      public String label;
 6.      public static List<Book> all() {
 7.        return new ArrayList<Book>();
 8.      }
 9.      public static void create(Book book) {
10.      }
11.      public static void delete(Long id) {
12.      }
13.    }
  • 第 6 行到第 12 行:您创建静态方法来管理 Book 上的 CRUD 操作。稍后,您将实现这些操作来将书籍存储在关系数据库中。

表单和视图模板

表单对象封装了一个 HTML 表单定义,包括验证约束。要为 Book 类创建一个表单,需要将以下内容添加到应用控制器中:

static  Form<Book>  bookForm = Form.form(Book.class);

前面的代码用于定义包装现有类的 play.data.Form。书单类型为表格。

您可以使用 JSR-303 注释向图书类型添加约束。清单 8-10 说明了如何使标签字段成为必填字段。

清单 8-10 。添加验证约束

package models;

import java.util.*;

import play.data.validation.Constraints.*;

public class Book {

  public Long id;

  @Required
  public String label;

  ...

现在您需要修改视图模板来显示创建图书和列出所有图书的屏幕。

模板被编译成标准的 Scala 函数。如果你创建了一个 views/Application/index . scala . html 模板文件,Scala 会生成一个 views.html.Application.index 类,这个类有一个 render()方法。 清单 8-11 显示了一个简单的模板。

清单 8-11 。简单模板

@(books: List[Book])
 <ul>
@for(book <- books) {
  <li>@book.getTitle()</li>
}
</ul>

然后,您可以从任何 Java 代码中调用它,就像您通常调用类的方法一样。

Content html = views.html.Application.index.render(books);

清单 8-12 展示了 index.scala.html 模板的代码,你可以在 app/views 文件夹中找到。

清单 8-12 。index.scala.html

 1.    @(books: List[Book], bookForm: Form[Book])
 2.
 3.    @import helper._
 4.
 5.    @main("books") {
 6.
 7.    <h1>@books.size() book(s)</h1>
8.
 9.    <ul>
10.            @for(book <- books) {
11.    <li>
12.                    @book.label
13.
14.                    @form(routes.Application.deleteBook(book.id)) {
15.    <input type="submit" value="Delete">
16.                    }
17.    </li>
18.            }
19.    </ul>
20.
21.    <h2>Add a new book</h2>
22.
23.        @form(routes.Application.newBook()) {
24.
25.            @inputText(bookForm("label"))
26.
27.    <input type="submit" value="Create">
28.
29.        }
30.
31.    }

在清单 8-12 中,模板有两个参数。@content 是一个参数,表示要写入文档主体的有效 HTML。内容的类型是 HTML,它是 Scala 结构,被模板调用时可以写成 Html。输入的助手。_ 提供了表单创建助手——也就是说,表单函数创建填充了动作和方法属性的 HTML <表单>,输入文本函数创建作为表单字段给出的 HTML 输入。

注意play . data 包包含几个助手来处理 HTTP 表单数据提交和验证。

现在你可以实现 books()动作,,如清单 8-13 所示。

清单 8-13 。实施图书行动

public static Result books() {
    return ok(
    views.html.index.render(Book.all(), bookForm)
  );
}

books()操作呈现一个用 HTML 填充的 200 OK 结果,该结果由用图书列表和 bookForm 表单调用的 index.scala.html 模板呈现。

你现在可以尝试在你的浏览器中访问localhost:9000/books(参见图 8-23 )。

9781430259831_Fig08-23.jpg

图 8-23 。展示书籍创作形式

如果您提交图书创建表单,您仍然会看到 TODO 页面。您需要实现 newBook()操作才能创建图书。清单 8-14 展示了 newBook()动作的实现。

清单 8-14 。newBook()动作的实现

 1.    public static Result newBook() {
 2.        Form<Book> filledForm = bookForm.bindFromRequest();
 3.      if(filledForm.hasErrors()) {
 4.        return badRequest(
 5.          views.html.index.render(Book.all(), filledForm)
 6.        );
 7.      } else {
 8.        Book.create(filledForm.get());
 9.        return redirect(routes.Application.books());
10.      }
11.    }
  • Line 2 :我们使用 bindFromRequest 来创建一个新的表单,其中填充了请求数据。
  • 第 3 行到第 7 行:如果表单中有任何错误,我们重新显示它(这里我们使用 400“错误请求”而不是 200“好”)。
  • 第 7 行到第 10 行:如果没有错误,我们创建图书,然后重定向到图书列表。

访问数据库

Play 2 支持一个现成的对象关系映射(ORM)Ebean 来填补领域模型和关系数据库之间的空白,如图 8-24 所示。为 Java 提供 ORM 的其他流行选项是 Hibernate 和 Java Persistence API,后者是由 Oracle 标准化的。

9781430259831_Fig08-24.jpg

图 8-24 。使用 Ebean 查询数据库

像任何其他 ORM 一样,Ebean 旨在通过实现基于模型属性的查找器,在处理关系数据库时方便模型的使用。您将使用 Play 2 附带的轻量级 DBMS H2。Play 的配置包含使用 H2 和 Ebean 的默认设置,但它们被注释掉了。因此,打开应用目录中的文件 conf/application.conf,找到并取消注释以下行,以便在应用中启用数据库:

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"

您将使用 Ebean 来查询数据库。因此,您还必须在 application.conf 文件中启用它:

ebean.default="models.*"

这将创建一个连接到默认数据源的 Ebean 服务器,管理 models 包中的所有实体。

现在是时候将 Book 类转换成有效的 Ebean 实体了。你可以通过让 Book 类扩展 play.db.ebean.Model 超类来访问 play 的内置 ebean 助手,如清单 8-15 所示。

清单 8-15 。将 Book 类转换为有效的 Ebean 实体

 1.    package models;
 2.
 3.    import java.util.*;
 4.    import play.db.ebean.*;
 5.    import play.data.validation.Constraints.*;
 6.
 7.    import javax.persistence.*;
 8.
 9.    @Entity
10.    public class Book extends Model {
11.      @Id
12.      public Long id;
13.      @Required
14.      public String label;
15.
16.    public static Finder<Long,Book> find = new Finder(
17.        Long.class, Book.class
18.      );
19.
20.
21.    public static List<Book> all() {
22.      return find.all();
23.    }
24.
25.    public static void create(Book book) {
26.      book.save();
27.    }
28.    public static void update(Long id, Book book) {
29.        book.update(id);
30.    }
31.
32.    public static void delete(Long id) {
33.      find.ref(id).delete();
34.    }
35.
36.
37.    }
  • 第 13 行:第 13 行增加了持久性注释。
  • 第 16 行到第 18 行:这几行创建了一个名为 find 的查找助手来启动查询。
  • 第 21 行到第 34 行:这些行执行 CRUD 操作。例如,当您调用 create()操作时, Ebean 将 save()方法调用转换成一个或多个 SQL INSERT 语句,这些语句使用 SQL 在数据库表中存储一条新记录。

现在当你测试网址 localhost:9000/ 时,你会看到如图图 8-25 所示的页面。

9781430259831_Fig08-25.jpg

图 8-25 。在默认数据库上应用脚本

当您定义连接到 H2 数据库的数据库连接时,它不会自动创建模式,换句话说,就是表和列定义。为了创建模式,Play 2 生成一个 SQL 脚本并要求运行它:“现在应用这个脚本。”一旦你点击“立即应用这个脚本”按钮运行这个脚本,你就可以测试 URLlocalhost:9000/books和新书,如图图 8-26 所示。

9781430259831_Fig08-26.jpg

图 8-26 。添加图书

删除书籍

既然您可以创建图书,那么您需要能够删除它们。为此你需要实现 deleteBook()动作,如清单 8-16 所示。

清单 8-16 。执行删除动作

1.    public static Result deleteBook(Long id) {
2.      Book.delete(id);
3.      return redirect(routes.Application.books());
4.    }

本章和本书到此结束。一章不足以涵盖 Play 2 框架(或任何框架)的所有特性。与此同时,框架和 web 架构正朝着实时处理的方向快速发展,集成了更多的并发实时数据,因此 web 框架需要支持完整的异步 HTTP 编程模型,并需要通过 Web 套接字使用事件模型。Play 2 提供了异步 HTTP API,而不是标准的 Servlet API,从而脱离了标准的 JEE 约定。Play 2.0 采用基于角色的模型通过 Akka 处理高度并发的系统。Akka 是 Java 和 Scala 中基于角色模型的最佳实现。Play 2.0 提供了原生的 Akka 支持,使得编写高度分布式的系统成为可能。

这本书的目的是向您展示 Java 语言在 90 年代末引入 Web 开发的浪潮仅仅是今天正在被超越的类型安全时代的开始,Java 的流行应该归功于 Java 虚拟机。这是一部相当不错的机器。

摘要

在本章中,您对 Play 2 框架进行了高度概括。您为 Java 和 Scala 开发了一个 helloworld web 应用,并且学习了所有 Play 2 web 应用共有的基础知识:Java 和 Scala 控制器、动作,甚至一些视图。此外,您还研究了 Java 和 Scala 控制器之间的差异。您看到了 Play 2 提供的最佳特性,例如动态编译和浏览器上显示的错误。然后您开发了一个简单的基于 Java 的 CRUD web 应用。

1netty.io/】??

2www.scala-sbt.org/】??

3www.avaje.org/】??

4www.seleniumhq.org/】??

5lesscss.org/】??

6jashkenas.github.io/coffee-scri…??

7www.scala-sbt.org/】??

九、附录 A:Java 简介

处理复杂性的基本方法之一是抽象,它几乎应用于每一个学科。例如,抽象艺术是不表现或模仿外部现实或自然物体的艺术。再比如,有些词是抽象的,只存在于头脑中,像真理正义。在 Java 中,抽象允许开发人员通过使用类层次结构将复杂的系统分成可管理的部分,这些类层次结构突出了对象的基本属性和行为,将它与其他对象区分开来。

类和对象

一个是面向对象(OO)程序的基本构件。每个类通常代表一个现实世界的对象。类是一个模板,用于表示和创建对象的类别,从而通过定义表示抽象的对象的属性和行为来对抽象进行建模。对象的属性由字段定义,字段是可以存储表示该属性的值的变量。对象的行为是由方法定义的。一个类的字段和方法被称为它的成员。Java 中的类定义由成员声明(变量声明和方法声明)组成,并以 class 关键字开始,如清单 A-1 中的所示。

上市 A-1 。Java 类

class ClassA {
// members declarations
}

一个对象是一个类的实例。对象是以类为蓝本构建的,是类所代表的抽象的具体实例。必须先创建对象,然后才能在程序中使用它。从一个类创建对象的过程被称为实例化。当一个类被实例化时,返回一个引用值,该值表示所创建的对象。参考值表示特定的对象。对象引用(或者简单地说, reference )是一个变量,它可以存储一个引用值并提供一个对象的句柄。清单 A-2 中的代码创建了一个 ClassA 的对象。这个对象的引用值存储在变量 var1 中。

列表 A-2 。创建对象

ClassA  var1 = new ClassA();

创建对象的过程包括声明一个引用变量来存储对象的引用值,然后使用 new 关键字创建对象,接着通过调用构造函数来初始化对象。构造函数的概念将在后面的附录中解释。清单 A-3 将清单 A-2 分开,将声明和创建显示为单独的步骤。

列表 A-3 。在单独的步骤中进行声明和创建

1.    ClassA  var1 ;
2.    var1 = new ClassA();
  • 第 1 行声明了变量 var1。引用变量 var1 现在可以用来操作引用值存储在引用变量中的对象。
  • 第 2 行使用 new 关键字创建对象,并通过调用构造函数 ClassA()进行初始化。

变量

在 Java 中,变量存储原始数据类型的值和对象的引用值。

列出 A-4 说明了可以存储原始值的变量声明。

上市 A-4 。变量声明

int a, b, c;  // a, b and c are integer variables.
boolean flag; // flag is a boolean variable.
int i = 10,   // i is an int variable with initial value 10

存储对象参考值的变量称为参考变量。引用变量指定引用的类型,可以是类、数组或接口。清单 A-5 说明了引用变量的声明。

列表 A-5 。引用变量声明

ClassA  var1 ; // Variable var1 can reference objects of class ClassA.

清单 A-5 中的声明没有创建 ClassA 的任何对象;它只是创建一个变量来存储 ClassA 对象的引用。

实例成员

创建的每个对象(如清单 A-2 中的所示)都有自己在类中定义的字段的副本。对象的字段被称为实例变量。对象中实例变量的值构成了对象的状态。对象的方法定义了它的行为。这些方法被称为实例方法。属于对象的实例变量和实例方法被称为实例成员 (见清单 A-6 )以区别于只属于类的静态成员。

列表 A-6 。实例成员

1.    class ClassA{
2.    // instance Members
3.    int i ; // instance variable
4.    void methodA(){// instance method
5.    // do something
6.    }
7.    }
  • 在第 3 行,I 是 int 类型的实例变量,int 是 I 的原始数据类型。
  • 在第 4 行,methodA(){}是一个实例方法。

静态成员

用关键字 Static 声明的静态成员是只属于类而不属于类的任何特定对象的成员。一个类可以有静态变量和静态方法。静态变量在运行时类被加载时被初始化。类似地,一个类可以有静态方法,这些方法属于该类,而不属于该类的任何特定对象,如清单中的 A-7 所示。

上市 A-7 。静态成员

1.    class ClassA{
2.    static int i ;
3.    static void methodA(){
4.    // do something
5.    }
6.    }

与实例成员不同,类中的静态成员可以使用类名来访问,如下所示:

ClassA.i          // accessing static variable in Line 2 of Listing A-7

ClassA.methodA(); // accessing static method in Line 3 of Listing A-7

尽管类中的静态成员可以通过对象引用来访问,但这样做被认为是不好的做法。

方法重载

每个方法都有一个名字和一个形参表。方法的名称及其形参表与形参表中形参的类型和顺序一起构成了方法的签名。只要方法签名不同,一个以上的方法可以有相同的方法名。这种方法名称相同,签名不同的方法称为重载方法,这种现象称为方法重载 。因此,重载方法是具有相同名称但参数列表不同的方法。清单 A-8 显示了方法 methodA 的五个实现。

列表 A-8 。重载方法 a()

1.    void methodA{(int a, double b) }
2.    int methodA(int a) { return a; }
3.    int methodA() { return 1; }
4.    long methodA(double a, int b) { return b; }
5.    long methodA(int c, double d) { return a; } //  Not ok.
  • 方法的前四个实现被正确重载,每次都有不同的参数列表,因此也有不同的签名。
  • 第 5 行的声明与第 1 行的声明具有相同的签名方法 A(int,double)。仅仅改变返回类型不足以重载一个方法;声明中的参数列表必须不同。

注意只有在同一个类中声明的方法和被该类继承的方法才能被重载。

数组

一个数组是一个数据结构,由固定数量的基本上相同数据类型的数据元素组成。数组中的任何元素都可以使用索引来访问。第一个元素总是在索引 0 处,最后一个元素在索引 n-1 处,其中 n 是数组中长度字段的值。在 Java 中,数组是这样的对象,数组中的所有元素都可以是特定的原始数据类型或特定的引用类型。清单 A-9 声明了引用数组对象的引用。

列表 A-9 。数组声明

int [] intArray;
ClassA[]  classAArray ;

清单 A-9 中的两个声明将 intArray 和 classAArray 声明为引用变量,可以引用 int 值数组和 ClassA 对象数组。使用 new 运算符,可以为特定类型的固定数量的元素构造数组。给定前面的数组声明,数组可以按如下方式构造:

intArray = new int[10];      // array for 10 integers
classAArray = new ClassA[5]; // array of 5 objects of ClassA

构造函数

当使用 new 运算符创建对象时,调用构造函数来设置对象的初始状态。构造函数声明由可访问性修饰符组成,后跟带有以下声明的参数列表:

  • 构造函数头中不允许除可访问性修饰符之外的修饰符。
  • 构造函数不能返回值,因此,不要在构造函数头中指定返回类型,甚至是 void。
  • 构造函数名必须与类名相同。

当在一个类中没有指定构造函数时,则由编译器为该类生成隐式默认构造函数,即没有任何参数的隐式构造函数,该编译器包括对超类的构造函数的调用。编译器将这个调用插入到超类的构造函数中,以确保对象的继承状态被初始化。列出 A-10 说明了对隐式默认构造函数的调用。

上市 A-10 。隐式默认构造函数

class ClassA {
int i;
}
class ClassB {

ClassA var1 = new ClassA(); // (1) Call to implicit default constructor.
}

在清单中,当在 ClassB 中创建 ClassA 对象时,调用下面的隐式默认构造函数:

ClassA() { super(); }

在清单 A-11 的中,ClassA 类在第 4 行提供了一个显式的默认构造函数。

列表 A-11 。显式默认构造函数

1.    class ClassA {
2.    int i ;
3.    // Explicit Default Constructor:
4.    ClassA() {
5.    i = 1;
6.    }
7.
8.    }
9.    class ClassB {
10.    // ...
11.    ClassA var1 = new ClassA(); //  Call of explicit default constructor.
12.    }

显式默认构造函数确保任何使用对象创建表达式 new ClassA()创建的对象(如 ClassB 所示)都将其字段 I 初始化为 1。如果一个类定义了任何显式构造函数,那么编译器不会通过调用超类的构造函数来生成隐式默认构造函数,因此对象的状态不会被设置。在这种情况下,需要提供默认构造函数的实现。在清单 A-12 的中,ClassA 类在第 4 行只提供了一个非默认的构造函数。当使用 new 操作符创建 ClassA 类的对象时,在第 8 行调用它。任何调用默认构造函数的尝试都将被标记为编译时错误,如第 11 行所示。

列表 A-12 。非默认构造函数

1.    class ClassA {
2.    int i;
3.    // Only non-default Constructor:
4.    ClassA(int i) {
5.    this.i = i;
6.    }
7.    }
8.    class ClassB {
9.    // ...
10.    ClassA var1 = new ClassA(2);
11.    //ClassA var2 = new ClassA(); // Compile-time error.
12.    }

像方法一样,构造函数可以重载,因为所有构造函数的名称都被限制为与类名相同,所以只有当这些构造函数的参数列表不同时,它们的签名才能不同。在清单 A-13 的中,ClassA 在第 4 行提供了默认构造函数的显式实现,在第 8 行提供了非默认构造函数。当在第 14 行创建 ClassA 类的对象时,调用非默认构造函数,在第 15 行调用默认构造函数。

列表 A-13 。默认和非默认构造函数

1.    class ClassA {
2.    int i;
3.    // Explicit Default Constructor:
4.    ClassA() {
5.    i = 3;
6.    }
7.    // Non-default Constructor:
8.    ClassA(int i) {
9.    this.i = i;
10.    }
11.    }
12.    class ClassB {
13.    // ...
14.    ClassA var1 = new ClassA(4);
15.    ClassA var2 = new ClassA();
16.    }

封装

封装是通过防止数据和代码被外部代码(即来自类外的代码)随机访问和操纵来实现数据隐藏的技术。在实现方面,封装是通过将类中的字段私有并通过公共方法提供对这些字段的访问来实现的。如果一个字段被声明为私有,那么该类之外的任何人都不能访问它。清单 A-14 展示了一个封装的 Book 类。

列表 A-14 。包装

public class Book {
       private String title ;
       public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
 }

继承

继承是面向对象编程的基本原则之一。在 Java 中,默认情况下,所有类都扩展 java.lang.Object 类。一个类可以使用 extends 关键字扩展另一个类。Java 通过实现继承支持单一继承,其中一个类通过类扩展从另一个类继承状态和行为。Java 也以两种方式支持多重继承。

  • 通过实现,类可以从一个或多个接口继承行为。
  • 一个接口可以通过扩展从一个或多个接口继承行为。

图 A-1 展示了一个 UML 1 类图,描述了一个类 ClassA 和一个子类 ClassB 之间的父子关系。ClassB 是 ClassA 的子类。请注意,带箭头的线条用于描述泛化,换句话说,就是父子关系。

9781430259831_AppA-01.jpg

图 A-1 。父子关系。ClassB 扩展了 ClassA

清单 A-15 展示了如何用代码实现图 A-1 。它在第 26 行使用关键字 extends 说明了实现继承。

列表 A-15 。实现继承

1.    package apress.appendix_A
2.
3.    public class ClassA {
4.
5.        // Instance methods
6.        public void method1() {
7.            System.out.println(" classA - method1");
8.
9.        }
10.
11.        private void method2() {
12.            System.out.println(" classA - method2");
13.
14.        }
15.
16.        // Static methods
17.        public static void method3() {
18.            System.out.println(" classA - method3");
19.
20.        }
21.
22.    }
23.
24.    package apress.appendix_A;
25.
26.    public class ClassB extends ClassA {
27.
28.
29.    }

清单 A-16 是一个驱动类,用于测试清单 A-15 中的继承。

列表 A-16 。测试继承

package apress.appendix_A;

public class Test {

    public static void main(String[] args) {
        ClassB var1 = new ClassB();

        var1.method1();
        // var1.method2(); // private method not Inherited

        ClassB.method3();// static method

    }

}

清单 A-16 说明了即使 ClassB 中没有定义方法,ClassA 的方法在 ClassB 中也是可用的,并且可以在实际对象类型为 ClassB 的引用变量上调用。第 9 行显示私有方法没有被继承。

以下是输出:

classA - method1
classA - method3

构造函数链接

当子类通过调用它的一个构造函数被实例化时,该构造函数首先调用超类的无参数构造函数。在超类中,构造函数也调用其超类的构造函数。这个过程不断重复,直到到达 java.lang.Object 类的构造函数。换句话说,当你创建一个子类的对象时,它的所有超类也被实例化。清单 A-17 说明了这个构造函数链接

列表 A-17 。构造函数链接

package apress.appendix_A;

public class ClassA {

    public ClassA() {

        System.out.println("Class A no-arg constructor");

    }

    public ClassA(String title) {
        System.out.println("Class A constructor");

    }

}

package apress.appendix_A;

public class ClassB extends ClassA {

    public ClassB(String title){
        System.out.println("Class B constructor ");

    }

}

package apress.appendix_A;

public class Test {

    /**
     * @param args
     */
    public static void main(String[] args) {
        ClassB var1 = new ClassB("classB");

    }

}

以下是输出:

Class A no-arg constructor
Class B constructor

输出证明了子类的构造函数调用了基类的无参数构造函数。Java 编译器将 ClassB 的构造函数更改为:

public ClassB(String title) {
 super();
 System.out.println("Class B constructor ");
}

关键字 super 表示当前对象的直接超类的一个实例。因为 super 是从子类的实例中调用的,所以 super 表示 ClassA 的实例。可以使用 super 关键字从子类的构造函数中显式调用父类的构造函数,但 super 必须是构造函数中的第一条语句。如果希望调用超类中的另一个构造函数,使用 super 关键字会很方便。

public ClassB(String title) {
 super(title);
 System.out.println("Class B constructor ");
}

多态性

多态性是面向对象编程最重要的原则之一,也是面向对象编程的核心。多态性指的是 Java 中的一个对象可以有多种形式。为了理解多态在 Java 中是如何工作的,让我们看一个扩展另一个类的例子(见图 A-2 )。

9781430259831_AppA-02.jpg

图 A-2 。扩展一个类

图 A-2 显示了一个超类-子类的关系。此关系允许您将对象分配给类型不同于对象类型的引用变量,如下所示:

ClassA var1 = new ClassB();

这将 ClassB 类型的对象分配给引用类型为 ClassA 的引用变量 var1。这种赋值在编译时和运行时有不同的含义。在编译时,由于 var1 的类型是 ClassA,编译器将不允许调用 var1 上不在 ClassA 中的方法,即使该方法在 ClassB 中。清单 A-18 显示了图 A-2 的代码实现。

列表 A-18 。遗产

package apress.appendix_A;
public class ClassA {

    public void methodA() {
        System.out.println("methodA() in ClassA");
    }

}

package apress.appendix_A;

public class ClassB extends ClassA {

    public void methodB() {
        System.out.println("methodB() in ClassB");
    }

}

清单 A-19 说明了该测试。

清单 A-19 。清单 A-18 的驱动程序

package apress.appendix_A;

public class Test {
    public static void main(String[] args) {

        ClassA var1 = new ClassB();
        // var1.methodB(); uncommenting this code will result in compile time
        // error
        var1.methodA();

    }
}

在清单 A-19 的第 2 行,即使 methodB()是 ClassB,也不可能对 var1 调用 methodB(),因为 var1 的引用类型是 ClassA,而 ClassA 没有 methodB()。

现在让我们考虑 ClassB 覆盖 ClassA 的 methodA()的情况(见图 A-3 )。

9781430259831_AppA-03.jpg

图 A-3 。重写方法

现在,ClassB 的代码看起来像清单 A-20 中的所示。

上市 A-20 。重写方法 a()

package apress.appendix_A;

public class ClassB extends ClassA {

    public void methodA() {
        System.out.println("methodA() in ClassB");
    }

}

现在 ClassA 和 ClassB 都有相同的方法,methodA()。

以下作业:

ClassA var1 = new ClassB();

确认可以对 var1 调用 methodA(),因为 ClassA 有 methodA()。因此,下面的测试将会编译:

package apress.appendix_A;

public class Test {
    public static void main(String[] args) {

        ClassA var1 = new ClassB();
        var1.methodA();

    }
}

也就是说,在编译时,编译器将对照 var1 的引用类型 ClassA 检查 var1 上的调用 methodA(),并且编译器将允许它,因为 methodA()存在于 ClassA 中。

但是如果我们进行测试会发生什么呢?也就是会调用哪个 methodA(),ClassA 中的 methodA()还是 ClassB 中的 methodA()。运行测试时,它会给出以下输出:

methodA() in ClassB

编译器检查了类 a 中的方法 a(),但执行了类 b 中的方法 a()。这是因为在运行时,JVM 根据实际的对象类型验证而不是编译 methodA()。代码中的实际对象类型(class a var 1 = new class b();)是 ClassB,而 ClassA 是引用类型。因此,JVM 检查调用方法 a()是否在 ClassB()中,并调用它。这种现象被称为多态性。如果 methodA()不在 ClassB 中会发生什么?为了理解这一点,我们实现了人物 A-4 的代码。

9781430259831_AppA-04.jpg

图 A-4 。b 类层次结构

图 A-4 显示了 InterfaceA,只是为了说明引用类型可以是一个接口或者一个类(或者抽象类),编译器会以类似的方式对照其中任何一个的引用类型检查方法的存在性。清单 A-21 实现了图 A-4 所示的层级。

列表 A-21 。B 类层次结构

package apress.appendix_A;
public interface InterfaceA {

    public void methodA();

}

package apress.appendix_A;
public class ClassA implements InterfaceA{

    @Override
    public void methodA() {
        System.out.println("methodA() in ClassA");
    }
}

package apress.appendix_A;
public class ClassB extends ClassA {
    public void methodB() {
        System.out.println("methodB() in ClassB");
    }
}

在清单 A-21 中,调用 methodA()对照引用类型 InterfaceA 进行验证,在检查到 methodA()存在于 InterfaceA 中后,编译器批准调用;也就是说,没有编译时错误。运行清单 A-22 中所示的测试。

列表 A-22 。测试应用

package apress.appendix_A;
public class Test {
    public static void main(String[] args) {
        InterfaceA var1 = new ClassB();
        var1.methodA();
    }
}

您将获得以下输出:

methodA() in ClassA

清单 A-22 的第 4 行中的实际对象类型是 ClassB,所以在运行时 JVM 检查 methodA()是否存在于 ClassB 中。在 ClassB 中找不到 methodA()时(因为它不存在于 ClassB 中),JVM 检查 methodA()在 ClassB 的层次结构中是否存在,因为 JVM 认为在 ClassB 的层次结构中一定存在 methodA ();否则,编译器不会批准该调用。因为 methodA()存在于 ClassA 中,所以 JVM 在运行时执行 ClassA 中的 methodA()。

摘要

本附录向您介绍了 Java 和面向对象编程的基础。您了解了类是面向对象程序的基本构件,以及如何从类中实例化对象。接下来,向您介绍了面向对象编程的三大支柱:封装、继承和多态。

1www.uml.org/】??

十、附录 B:Groovy 简介

Groovy 是一种用于 Java 虚拟机的敏捷而动态的语言,它建立在 Java 的优势之上,但受 Python、Ruby 和 Smalltalk 等语言的启发,它还增加了一些强大的特性。它与所有现有的 Java 类和库无缝集成,并编译成 Java 字节码,因此您可以在任何可以使用 Java 的地方使用它。Groovy 提供了静态类型检查和静态编译代码以提高健壮性和性能的能力,并支持特定领域语言和其他紧凑语法,因此您的代码变得易于阅读和维护。

Groovy 让 Java 开发人员几乎没有学习曲线就可以使用现代编程特性;在开发 web、GUI、数据库或控制台应用时,通过减少脚手架代码来提高开发人员的工作效率。并通过支持单元测试和开箱即用来简化测试。

入门指南

让我们从一个传统的“你好,世界”节目开始。但是首先你需要安装 Groovy。Groovy 以. zip 文件或特定平台安装程序的形式捆绑在 Windows、Ubuntu 和 Debian(以及 openSUSE,直到最近的版本)上。本节解释如何安装压缩版本,因为它涵盖了大多数平台。

要安装 Groovy,请遵循以下步骤:

  1. 下载最新稳定的 Groovy 二进制版本。来自 groovy.codehaus.org/Download 的 zip 文件。
  2. 将 groovy-binary-X.X.X.zip 解压缩到您选择的位置。
  3. 将 GROOVY_HOME 环境变量设置为解压缩。压缩文件。
  4. 将%GROOVY_HOME%\bin 目录添加到您的系统路径中。

注意 Groovy 需要 Java,所以你需要有一个可用的版本(虽然 Groovy 1.6 支持 JDK 1.4 或更高版本,但对于 Groovy 1.7 以后,至少需要 JDK 1.5)。

要验证您的安装,请打开控制台并键入以下内容:

>groovy -v

您应该会看到类似这样的内容:

Groovy Version: 2.0.0 JVM: 1.6.0_31 Vendor: Sun Microsystems Inc. OS: Windows 7

现在你可以编写你的第一个“Hello World”程序了(见清单 B-1 )。

列表 B-1 。Java 中的“Hello World”

1.   public class HelloWorld {
2.      public static void main( String[] args )
3.   System.out.println("Hello World!");
4.      }
5.   }
  • 第 1 行到第 2 行:方法和字段的默认可见性是 public,所以可以去掉 public 修饰符。
  • 第 2 行 : Groovy 支持动态类型,所以可以在 main()上丢弃类型信息和返回类型 void。
  • 第 3 行:每个 Groovy 对象都有自己的 disposure println,可以看作是 System.out.println 的快捷方式。
  • 第 3 行:行尾的分号是可选的,所以你也可以删除它。

根据这些规则,您可以将清单 B-1 的转换为清单 B-2 的。

列表 B-2 。应用 Groovy 规则转换“Hello World”

1.   class HelloWorld {
2.      static main( args ){
3.   println "Hello World!"
4.      }
5.   }

如您所见,清单 B-2 要紧凑得多。您可以以脚本的形式编写和执行 Groovy 代码,这些脚本也被编译成字节码。因此,你可以为“Hello World”程序编写清单 B-3 中的 Groovy 代码。

注意任何 Java 类/对象也是 Groovy 类/对象。

列表 B-3 。“Hello World”的精彩脚本

println "Hello World!"

您可以通过命令行、GroovyShell 或 GroovyConsole 运行 Groovy 脚本和类。

格罗维谢尔

GroovyShell 是一个交互式命令行应用(Shell ),允许您创建、运行、保存和加载 Groovy 脚本和类。要启动 GroovyShell,请运行 groovysh。图 B-1 展示了如何使用 GroovyShell 来执行一个简单的脚本。

9781430259831_AppB-01.jpg

图 B-1 。使用 GroovyShell

如你所见,这个脚本打印了 vishal。然后你看到===> null。按照惯例,Groovy 总是返回方法的结果。在这种情况下,没有结果,因此返回 null。GroovyShell 包含一个内置的帮助工具,您可以使用它来了解关于 Shell 的更多信息。要访问它,请在提示符下键入 help。图 B-2 显示了帮助列表。

9781430259831_AppB-02.jpg

图 B-2 。使用 GroovyShell 帮助

GroovyConsole

图 B-3 中的【GroovyConsole 是 GroovyShell 的图形化版本。它是使用 SwingBuilder 编写的,Swing builder 是一个 Groovy 模块,它使构建 Swing 用户界面变得更加容易。

9781430259831_AppB-03.jpg

图 B-3 。groovycconsole(groovycconsole)

图 B-4 显示了输出分离的 GroovyConsole。您可以通过多种方式启动 GroovyConsole,这取决于您的环境和您安装 Groovy 的方式。最简单的方法是执行 GroovyConsole,它位于 Groovy bin 目录中。

9781430259831_AppB-04.jpg

图 B-4 。使用 GroovyConsole 并分离输出

控制台提供了创建、保存、加载和执行类和脚本的能力。控制台的一些不错的特性是撤销/重做和检查变量的能力。如果你必须在使用 GroovyShell 和 GroovyConsole 之间做出选择,我推荐 GroovyConsole。您还可以在脚本中定义类并立即使用它们,如清单 B-4 中的所示。

清单 B-4 。在脚本中定义类

class HelloWorld {
   def hello( name ){
      "Hello ${name}!"
   }
}
def hw = new HelloWorld()
println hw.hello("Vishal")
  • 方法 hello 的返回类型不是特定的类型,所以使用保留关键字 def。
  • 字符串“Hello”不是一个简单的 java.lang.String,事实上,它是 Groovy 的一个特性:GString。这些类型的字符串允许字符串插值,这将在下一节中解释。

Groovy 支持两种类型的字符串:普通 Java 字符串和 GStrings。如果 Groovy 中的字符串被单引号或双引号或三引号包围,并且没有未转义的美元符号($),则该字符串是 java.lang.String 的实例。

GStrings

g string 是 groovy.lang.GString 的一个实例,允许文本中包含占位符。gstring 不是 String 的子类,因为 String 类是最终的,不能扩展。GString 就像一个普通的字符串,但是它允许使用${..}.只有当嵌入变量是表达式的占位符时,才需要花括号。Groovy 支持在 Perl 和 Ruby 等许多其他语言中发现的一个概念,称为字符串插值 ,即在字符串中替换表达式或变量的能力。如果您有使用 Unix shell 脚本、Ruby 或 Perl 的经验,这应该很熟悉。Java 不支持字符串插值。您必须手动连接这些值。清单 B-5 是您需要用 Java 编写的代码类型的一个例子,而清单 B-6 展示了使用 GString 的相同代码。

清单 B-5 。用 Java 构建字符串

String name = "Vishal" ;
String helloName = "Hello " + name ;
System.out.println(helloName) ;

清单 B-6 。Groovy/GString 中的字符串插值

1.   str1= "Vishal"
2.   str2 = "Hello "
3.   println "$str2$str1"

在第 3 行中,没有使用花括号,因为只有当嵌入的变量是表达式的占位符时才需要花括号。当 Groovy 看到一个用双引号或斜杠定义的字符串和一个嵌入的表达式时,Groovy 会构造一个 org . code Haus . Groovy . runtime . gstringimpl,而不是 java.lang.String。注意,可以在${}符号中包含任何有效的 Groovy 表达式;这包括方法调用或变量名。

Groovy 支持单行字符串和跨多行的字符串。在接下来的小节中,您将学习 Groovy 中支持的各种字符串。

单行字符串

单行字符串可以是单引号或双引号。单引号中的字符串是字面意思。用单引号定义的字符串不解释嵌入的表达式,如清单 B-7 所示。

清单 B-7 。单引号字符串不解释嵌入的表达式

name = "vishal"
s1 = 'hello $name'
println s1
Here is the output:
hello $name

您可以在单引号中嵌套双引号,如清单 B-8 中的所示。

清单 B-8 。单引号中嵌套的双引号

s1 = 'hello "vishal"'
println s1

使用双引号定义的字符串将解释字符串中嵌入的表达式,如清单 B-9 中的所示。

清单 B-9 。双引号字符串解释嵌入的表达式

def name = "vishal"
s1 = "hello $name"
println s1
Here is the output:
hello vishal

您可以在双引号中嵌套单引号,如清单 B-10 所示。

清单 B-10 。双引号中嵌套的单引号

s1 = "hello 'vishal'"
println s1
Here is the output:
hello 'vishal'

多行字符串

Groovy 支持跨多行的字符串。多行字符串通过使用三个双引号或三个单引号来定义。多行字符串支持对于创建模板或嵌入式文档(如 XML 模板、SQL 语句、HTML 等)非常有用。例如,您可以使用多行字符串和字符串插值来构建电子邮件消息的正文,如清单 B-11 所示。多行字符串的字符串插值与常规字符串的工作方式相同:用双引号创建的多行字符串计算表达式,而单引号字符串则不计算表达式。

清单 B-11 。使用多行字符串

def name = "Vishal"
def multiLineString = """
Hello, ${name}
This is a multiline string with double quotes
"""
println multiLineString
Hello, Vishal
This is a multiline string with double quotes

斜线弦

如前所述,斜线可以用来定义字符串。斜线符号有一个好处:不需要额外的反斜杠来转义特殊字符。唯一的例外是转义反斜杠:/。当创建需要反斜杠或路径的正则表达式时,斜杠符号会很有帮助。清单 B-12 展示了使用正则引号和斜杠定义正则表达式来匹配文件系统路径的区别。

清单 B-12 。使用斜线字符串

def quotedString = 'Hello Vishal'
def slashyString = /Hello Vishal/
println slashyString
Hello Vishal

清单 B-12 定义了两个变量,并将它们分配给一个目录路径。第一个变量定义 quotedString 使用单引号符号来定义字符串。使用单引号符号要求使用额外的反斜杠对嵌入的反斜杠进行转义。

多行斜线字符串

斜线字符串也可以跨越多行。当使用 regex freespacing 注释风格时,这对于多行 regex 特别有用(见清单 B-13)。

清单 B-13 。使用多行斜线字符串

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $
7.    path = c:\/groovy
8.    /
9.    println multilineSlashy
Hello vishal
path= c:/groovy
dollar = $
path = c:/groovy

让我们更详细地看看清单 B-13 。

  • 第 1 行定义了一个变量 name,并将值“vishal”赋给它。
  • 第 2 行定义了一个变量 path,并将值“c:/groovy”赋给它。
  • 第 3 行定义了一个变量 multilineSlashy,并为它分配了一个多行字符串,包括斜杠之间的第 8 行。
  • 第 4 行有一个表达式$name,其计算结果为 vishal,如输出所示。
  • 第 5 行有一个表达式$path,其计算结果为 c:/groovy,如输出所示。
  • 第 6 行有一个$符号,但它不是一个表达式,所以它显示在输出中。
  • 第 7 行有一个斜杠,需要转义。

美元斜线字符串

在多行斜线字符串中,斜线仍然需要转义。此外,在多行斜线字符串中,不是表达式的未转义美元符号会导致 MissingPropertyException,如清单 B-14 中的所示。

清单 B-14 。多行斜线字符串中缺少 PropertyException

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $test
7.    path = c:\/groovy
8.     /
9.    println multilineSlashy
Caught: groovy.lang.MissingPropertyException: No such property: test for class:
hello
groovy.lang.MissingPropertyException: No such property: test for class: hello
at hello.run(hello.groovy:3)

在清单 B-14 中,没有 test 这样的属性;第 6 行中的$test 被解释为一个表达式,这会导致 MissingPropertyException。

现在,让我们看看清单 B-15 中的代码,特别是第 6 行。

清单 B-15 。多行斜线字符串中的非转义美元符号

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def multilineSlashy = /
4.    Hello $name
5.    path= $path
6.    dollar = $ test
7.    path = c:\/groovy
8.     /
9.    println multilineSlashy

这一次,Groovy 没有将第 6 行中的test解释为表达式,因为 test 解释为表达式,因为和 test 之间有一个空格,它的输出如下所示:

Hello vishal
path= c:/groovy
dollar = $ test
path = c:/groovy

有了美元斜杠字符串,您不再需要用前面的反斜杠对斜杠进行转义(多行斜杠字符串需要对斜杠进行转义),如果需要,您可以使用$$对进行转义,或者使用进行转义,或者使用/对斜杠进行转义,如清单 B-16 中的所示。

清单 B-16 。使用美元斜线字符串

1.    def name = "vishal"
2.    def path= "c:/groovy"
3.    def dollarSlashy = $/
4.    Hello $name
5.    path = $path
6.    dollar = $$test
7.    path = c:/groovy
8.    /$
9.    println dollarSlashy
Hello vishal
path= c:/groovy
dollar = $test
path = c:/groovy

让我们更详细地看看清单 B-16 。

  • 第 3 行定义了一个直到第 8 行的 dollarSlashy 字符串。
  • 第 6 行有一个test,它在清单B14中的多行斜线字符串的情况下导致了MissingPropertyException,现在使用一个test,它在清单 B-14 中的多行斜线字符串的情况下导致了 MissingPropertyException,现在使用一个对其进行转义。

集体数据类型

Groovy 支持许多不同的集合,包括数组、列表、映射、范围和集合。让我们看看如何创建和使用每种集合类型。

数组

Groovy 数组是一个对象序列,就像 Java 数组一样(见列出 B-17 )。

上市 B-17 。创建和使用数组

1.    def stringArray = new String[3]
2.    stringArray[0] = "A"
3.    stringArray[1] = "B"
4.    stringArray[2] = "C"
5.    println stringArray
6.    println stringArray[0]
7.    stringArray.each { println it}
8.     println stringArray[-1..-3]
  • 第 1 行创建了一个大小为 3 的字符串数组。
  • 第 2 到 4 行使用一个索引来访问数组。
  • 第 7 行演示了如何使用 each()方法遍历数组。each()方法用于遍历每个元素并对其应用闭包。
  • 第 8 行显示了一些有趣的东西——它使用一个范围来访问数组,稍后将讨论这个范围。
[A, B, C]
A
A
B
C
[C, B, A]

列表

Groovy 列表是对象的有序集合,就像在 Java 中一样。它是 java.util.List 接口的实现。清单 B-18 说明了如何创建一个清单和常见用法。

清单 B-18 。创建和使用列表

1.    def emptyList = []
2.    def list = ["A"]
3.    list.add "B"
4.    list.add "C"
5.    list.each { println it }
6.    println list[0]
7.    list.set(0, "Vishal")
8.    println list.get(0)
9.     list.remove 2
10.    list.each { println it }
  • 在第 1 行,通过给属性赋值[]来创建一个空列表。
  • 第 2 行到第 4 行创建一个列表,其中已经有一个条目,并向列表中添加条目。
  • 第 5 行遍历列表,调用闭包来打印内容。each()方法提供了遍历列表中所有元素的能力,在每个元素上调用闭包。
  • 第 6 行演示了如何使用索引来访问列表。列表是从零开始的。
  • 第 7 行显示了如何使用一个索引为位置 0 赋值“Vishal”。
  • 第 8 行使用 get()方法访问列表。
A
B
C
A
Vishal
Vishal
B

地图

Groovy 映射是一个无序的键-值对集合,其中的键是惟一的,就像在 Java 中一样。它是 java.util.Map 的一个实现。

清单 B-19 。创建和使用地图

1.    def map = ['a':'Value A', 'b':'Value B ']
2.    println map["a"]
3.    println map."a"
4.    println map.a
5.    println map.getAt("b")
6.    println map.get("b")
7.    println map.get("c", "unknown")
8.    println map
9.    map.d = "Value D"
10.    println map.d
11.    map.put('e', 'Value E')
12.    println map.e
13.    map.putAt 'f', 'Value F'
14.    println map.f
15.    map.each { println "Key: ${it.key}, Value: ${it.value}" }
16.    map.values().each { println it }
  • 第 1 行演示了如何定义一个包含多个条目的映射。使用方括号符号时,冒号将键和值分开。
  • 第 2 行到第 10 行展示了几种不同的访问地图的技术。
  • 第 11 行到第 13 行展示了将项目放入地图的几种不同技术。
Value A

Value A

Value A

Value B

Value B

unknown

[a:Value A, b:Value B , c:unknown]

Value D

Value E

Value F

Key: a, Value: Value A

Key: b, Value: Value B

Key: c, Value: unknown

Key: d, Value: Value D

Key: e, Value: Value E

Key: f, Value: Value F

Value A

Value B

unknown

Value D

Value E

Value F

范围

范围是连续值的列表。从逻辑上来说,你可以认为它是从 1 到 10 或者从 a 到 z。事实上,范围的声明就是:1..10 或者 a。z '。范围是实现 java.lang.Comparable 的任何对象的列表。这些对象具有 next()和 previous()方法,以便于在范围内导航。清单 B-20 展示了你可以用范围做的一些事情。

清单 B-20 。创建和使用范围

1.    def numRange = 0..9
2.    numRange.each {print it}
3.    println ""
4.    println numRange.contains(5)
5.    def reverseRange = 9..0
6.    reverseRange.each {print it}
7.    def exclusiveRange = 1..<10
8.    println ""
9.    exclusiveRange.each {print it}
10.    def alphabet = 'a'..'z'
11.    println ""
12.    println alphabet.size()
13.    println alphabet[25]
  • 第 1、5、7 和 10 行说明了如何定义范围。
  • 第 1 行定义了数字的包含范围。
  • 第 10 行定义了小写字母的包含范围。
  • 第 7 行定义了一个排他的数字列表。该范围产生 1 到 9 的数字范围,不包括 10。
  • 第 5 行以相反的顺序创建一个范围,从 9 到 0。通常,范围用于迭代。在清单 B-20 的中,each()用于在范围内迭代。清单 B-21 展示了使用范围进行迭代的三种方式:一种在 Java 中,两种在 Groovy 中。
0123456789

true

9876543210

123456789

26

z

清单 B-21 。使用范围进行迭代

for (i in 0..9) {
println i
}
(0..9).each { i->
println i
}

each()方法用于遍历每个元素并对其应用闭包。

设置

Groovy 集合是一个无序的对象集合,没有副本,就像 Java 一样。它是 java.util.Set 的实现。默认情况下,除非您另外指定,否则 Groovy 集合是 java.util.HashSet。如果您需要一个除 HashSet 之外的集合,您可以通过实例化它来创建任何类型的集合,如 def TreeSet = new TreeSet()。清单 B-22 展示了如何创建集合和常用用法。

清单 B-22 。创建和使用集合

def  set = ["A", "B" ] as Set
set.add "C"
println set
set.each { println it }
set.remove "B"
set.each { println it }
[A, B, C]

A

B

C

A

C

创建空集类似于创建空列表。不同的是增加了 Set 子句。列表和集合之间的一个重要区别是,列表提供基于索引的访问,而集合不提供。

方法

清单 B-23 展示了用 Java 方式在 Groovy 中定义一个方法,而清单 B-24 展示了同样的事情,但是使用了 Groovy 语法。

清单 B-23 。用 Java 方式定义方法

public String hello(String name) {
return "Hello, " + name;
}

清单 B-24 。使用 Groovy 习惯用法定义方法

def hello(name) {
"Hello, ${name}"
}
  • 返回类型和返回语句不包含在方法体中。Groovy 总是返回最后一个表达式的结果——在本例中,是 GString“Hello,。。."。
  • 未定义访问修饰符 public。除非您另外指定,否则 Groovy 默认所有的类、属性和方法都是公共访问。

关闭

函数式编程为您提供了在底层原则 : 【参考透明性】高阶函数,以及不可变值的基础上考虑并发性的正确基础。理解这些关键元素对于理解闭包(以及 Groovy 中最近引入的其他功能特性)至关重要。函数式编程是建立在纯函数的前提下的。在数学中,函数是纯粹的,因为它们没有副作用。考虑经典函数 sin(x): y = sin(x)。无论调用 sin(x)多少次,sin(x)都不会在内部修改全局或上下文状态。这样的函数是纯粹的,没有副作用,并且不受上下文影响。这种对周围环境的遗忘被称为参照透明性*。如果没有修改全局状态,函数的并发调用是稳定的。在函数式编程中,函数是一等公民,这意味着函数可以赋给变量,函数可以传递给其他函数,函数可以作为其他函数的值返回。而这种以函数为自变量或者返回一个函数的函数,叫做高阶函数*。**

**引用透明性、高阶函数和不可变值一起使函数式编程成为编写并发软件的更好方式。虽然函数式语言都是关于消除副作用的,但是一种从不考虑副作用的语言是没有用的。事实上,引入副作用对任何语言都是至关重要的。所有函数式语言都必须提供以受控方式引入副作用的机制,因为即使函数式语言是关于纯编程的,不认可副作用的语言也是无用的,因为输入和输出本质上是副作用的衍生物。

理解闭包

以受控方式引入副作用的技术之一是关闭。Groovy 中的闭包定义遵循以下语法:

{ [closure parameters ->] closure body}

其中[closure parameters->]是可选的逗号分隔的参数列表,闭包体可以是一个表达式以及零个或多个 Groovy 语句。参数看起来类似于方法的参数列表,这些参数可以是有类型的,也可以是无类型的。Groovy 闭包是花括号{}内的可重用代码块,它可以赋给属性或变量,或者作为参数传递给方法。闭包只在被调用时执行,而不是在被定义时执行。清单 B-25 说明了这一点。

清单 B-25 。叫停

1.    def closureVar = {println 'Hello world'}
2.    println "closure is not called yet"
3.    println " "
4.    closureVar.call()
  • 第 1 行:这一行包含不带参数的闭包,由一个 println 语句组成。因为没有参数,所以省略了参数列表和->分隔符。闭包由标识符 closureVar 引用。
  • 第 4 行:这一行通过 call()方法使用显式机制来调用闭包。您也可以使用隐式的无名调用方法:closureVar()。如输出所示,闭包在第 4 行调用时打印“Hello world ”,而不是在第 1 行定义时。

以下是输出:

closure is not called yet
Hello world

清单 B-26 展示了与清单 B-25 中相同的闭包,但带有参数。

清单 B-26 。带参数的闭包

1.    def closureVar = {param -> println "Hello ${param}"}
2.    closureVar.call('world')
3.    closureVar ('implicit world')
  • 第 2 行:这是一个带有实际参数‘world’的显式调用。
  • 第 3 行:这是一个隐式调用,实际参数为‘隐式世界’。
Hello world
Hello implicit world

如清单 B-27 所示,闭包的形参可以被赋予默认值。

清单 B-27 。具有默认值的参数

1.    def sayHello= {str1, str2= " default world" -> println "${str1} ${str2}" }
2.    sayHello("Hello", "world")
3.    sayHello("Hello")
  • 第 1 行:say hello 闭包有两个参数,其中一个参数 str2 有一个默认值。
  • 第 3 行:闭包只提供一个实际参数,使用第二个参数的默认值。

以下是输出:

Hello world
Hello default world

闭包总是有返回值。该值可以通过闭包体内的一个或多个显式 return 语句来指定,如清单 B-28 中的所示,或者如果 return 没有显式指定,则作为最后执行的语句的值,如清单 B-29 中的所示。

清单 B-28 。使用 Return 关键字

def sum = {list -> return list.sum()}
assert sum([2,2]) == 4

清单 B-29 。Return 关键字可选

def sum = {list -> list.sum()}
assert sum([2,2]) == 4

要理解闭包,你必须理解自由变量的概念。当函数体引用一个或多个自由变量时,就形成了闭包。自由变量不是函数的局部变量,不作为参数传递给函数,而是在定义函数的封闭作用域中定义的。因此,闭包指的是参数列表中没有列出的变量(自由变量)。它们被“绑定”到定义它们的范围内的变量。清单 B-30 说明了这一点。

清单 B-30 。自由变量

def myConst = 5
def incByConst = { num -> num + myConst }
assert  incByConst(10) == 15

运行时“关闭”自由变量(清单 B-30 中的中的 myConst ),以便在执行函数时它是可用的。也就是说,编译器创建一个闭包来封装自由变量的外部上下文并绑定它们。

隐式变量

在 Groovy 闭包内,定义了一个具有特殊含义的隐式变量(it)。如果只有一个参数被传递给闭包,那么参数列表和->符号可以省略,闭包将可以访问它,它代表那个参数,如清单 B-31 所示。

清单 B-31 。使用它

def closure = {println "Hello ${it}"}
closure.call('world')
Hello world

一个闭包总是至少有一个参数,如果没有定义显式参数,那么这个参数可以通过隐式参数 it 在闭包体内使用。开发人员永远不必声明 it 变量——就像对象中的 this 参数一样,它是隐式可用的。如果闭包被调用时没有参数,那么它将是 null。

明确宣布关闭

Groovy 中定义的所有闭包本质上都是从类型闭包派生出来的。因为 groovy.lang 是自动导入的,所以您可以在代码中将 Closure 作为一种类型来引用。这是一个闭包的显式声明。显式声明闭包的优点是不会无意中将非闭包赋给这样的变量。清单 B-32 展示了如何显式声明一个闭包。

清单 B-32 。闭包的显式声明

Closure closure = { println it }

将该方法作为闭包重用

Groovy 提供了方法闭包运算符(。&) 重用方法作为闭包。方法闭包操作符允许像闭包一样访问和传递方法。清单 B-33 说明了这一点。

清单 B-33 。将该方法作为闭包重用

1.    def list = ["A","B","C","D"]
2.    String printElement(String element) {
3.    println element
4.    }
5.    list.each(this.&printElement)

清单 B-33 创建一个名字列表,并遍历列表打印出名字。在第 5 行,方法闭包运算符(。&)使方法 printElement 作为闭包被访问。以下是输出:

A

B

C

D

将闭包作为参数传递

闭包是一个对象。您可以像传递任何其他对象一样传递闭包。一个常见的例子是使用闭包迭代一个集合(参见清单 B-34 )。

清单 B-34 。将闭包作为参数传递

def list = ["A", "B", "C"]
def x = { println it }
list.each(x)
A

B

C

专业操作员

Groovy 包括几个在其他编程语言中可以找到的标准操作符,以及特定于 Groovy 的操作符,这些操作符使 Groovy 变得如此强大。在接下来的小节中,您将学习 Groovy 中的特殊操作符,比如 spread、Elvis、安全导航、字段、方法闭包和菱形操作符。

传播算子

展开运算符(*。) 是一种在对象集合上调用方法或闭包的速记技术。清单 B-35 展示了 spread 操作符在列表中迭代的用法。

清单 B-35 。使用扩展运算符

1.    def map = [1:"A", 2:"B", 3:"C", 4:"D"]
2.    def keys = [1, 2, 3, 4]
3.    def values = ["A", "B", "C", "D"]
4.    assert map*.key == keys
5.    assert map*.value == values

第 4 行和第 5 行使用 spread 操作符来访问映射的键和值。

Elvis 操作员

猫王操作员(?:) 是 Java 三元运算符的简写版本。比如 b= a?:1 可以解释如下:

if(a != 0)
b = a
else
b = 1

清单 B-36 展示了在 Groovy 中使用 Java 三元和 Elvis 操作符。

清单 B-36 。使用 Elvis 操作员

def firstName = author.firstName == null ? "unknown" : author.firstName // Java ternary
def firstName2 = author.firstName ?: "unknown" // Groovy Elvis

在这两种情况下,如果 author.firstName 为 null,那么 firstName 将设置为 unknown。Elvis 运算符示例的 author.firstName 片段被称为表达式。如果表达式的计算结果为 false 或 null,则返回冒号后的值。

安全导航/解引用操作员

安全导航/解引用运算符(?。) 用于避免空指针异常。考虑一下的情况,您有一个 Author 对象,您想打印这个名字。如果当您访问 firstName 属性时,Author 对象为空,您将得到一个 NullPointerException(参见清单 B-37 )。

清单 B-37 。使用安全导航/取消引用运算符

class Author {
String firstName
String lastName
def printFullName = {
println "${firstName} ${lastName}"
}
}
Author author
println author.firstName

清单 B-37 中的代码抛出一个 NullPointerException。在 Java 中,您可以这样添加一个空检查:

if (author != null) {
println "Author FirstName = ${author.firstName}"
}

清单 B-38 展示了如何在 Groovy 中使用安全导航/解引用操作符来添加 null 检查。

清单 B-38 。使用安全导航/取消引用运算符

class Author {
String firstName
String lastName
def printFullName = {
println "${firstName} ${lastName}"
}
}
Author author
println "Author FirstName = ${author?.firstName}"

现场操作员

Groovy 提供了一种绕过 getter 直接访问底层字段的方法。然而,不建议绕过 getter 访问底层字段,因为这违反了封装。清单 B-39 显示了使用字段操作符(.@)。

清单 B-39 。使用字段运算符

class Author {
String name
}
def author = new Author(name: "Vishal")
println author.name
printlnauthor.@name
Vishal

Vishal

在这个例子中,第一个 println 使用 getter 访问 name,第二个 println 绕过 getter 直接访问 name。

方法闭包运算符

方法闭包运算符(。&) 允许像闭包一样访问和传递方法(参见清单 B-40 )。

列出 B-40 。使用方法闭包运算符

def list = ["A","B","C"]
list.each { println it }
String printName(String name) {
println name
}
list.each(this.&printName)
A

B

C

A

B

C

本示例创建一个姓名列表,并遍历该列表以打印出姓名。创建一个 printName()方法来打印 Name 参数。最后,也是这个例子的要点,列表被迭代,执行 printName()方法作为闭包。使用方法闭包操作符,您能够将 Java 方法公开为闭包。

菱形算子

Groovy 中引入了菱形运算符(<> ) ,以避免参数化类型的重复。参数化类型可以省略,用尖括号代替,尖括号看起来像一个菱形。清单 B-41 展示了一种定义列表的常见冗长方式。

列出 B-41 。一个简单的 Groovy 脚本:Hello.groovy

List<List<String>> list1 = new ArrayList<List<String>>()

清单 B-42 展示了如何使用菱形运算符。

清单 B-42 。使用菱形运算符

List<List<String>> list1 = new ArrayList<>()

摘要

本附录介绍了 Groovy 的基础知识。学习任何语言或技术,一个附录是不够的,但是本附录中对 Groovy 的介绍对于使用 Grails 编写 web 应用来说已经足够了。在本附录中,您首先学习了如何安装 Groovy 然后展示了如何用 Groovy 编写一个“Hello World”程序。然后,您学习了如何运行 Groovy 脚本和类,并查看了 Groovy 中支持的各种字符串。然后这一章简要介绍了 Groovy 的集合数据类型,您学习了什么是闭包以及如何在 Groovy 中使用它。最后,您学习了如何使用 Groovy 中可用的特殊操作符。**