千家信息网

从Java走进Scala如何使用元组、数组和列表

发表于:2024-11-22 作者:千家信息网编辑
千家信息网最后更新 2024年11月22日,这篇文章主要介绍了从Java走进Scala如何使用元组、数组和列表,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。使用 Option(s
千家信息网最后更新 2024年11月22日从Java走进Scala如何使用元组、数组和列表

这篇文章主要介绍了从Java走进Scala如何使用元组、数组和列表,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

使用 Option(s)

在什么情况下,"无" 并不代表 "什么也没有"?当它为 0 的时候,与 null 有什么关系。

对于我们大多数人都非常熟悉的概念,要在软件中表示为 "无" 是一件十分困难的事。例如,看看 C++ 社区中围绕 NULL 和 0 进行的激烈讨论,或是 SQL 社区围绕 NULL 列值展开的争论,便可知晓一二。 NULL 或 null 对于大多数程序员来说都表示 "无",但是这在 Java 语言中引出了一些特殊问题。

考虑一个简单操作,该操作可以从一些位于内存或磁盘的数据库查找程序员的薪资:API 允许调用者传入一个包含程序员名字的 String,这会返回什么呢?从建模角度来看,它应该返回一个 Int,表示程序员的年薪;但是这里有一个问题,如果程序员不在数据库中(可能根本没有雇用她,或者已经被解雇,要不就是输错了名字……),那么应该返回什么。如果返回类型是 Int,则不能返回 null,这个 "标志" 通常表示没有在数据库中找到该用户(您可能认为应该抛出一个异常,但是大多数时候数据库丢失值并不能视为异常,因此不应该在这里抛出异常)。

在 Java 代码中,我们最终将方法标记为返回 java.lang.Integer,这迫使调用者知道方法可以返回 null。自然,我们可以依靠程序员来全面归档这个场景,还可以依赖程序员读取 精心准备的文档。这类似于:我们可以要求经理倾听我们反对他们要求的不可能完成的项目期限,然后经理再进一步把我们的反对传达给上司和用户。

Scala 提供了一种普通的函数方法,打破了这一僵局。在某些方面,Option 类型或 Option[T],并不重视描述。它是一个具有两个子类 Some[T] 和 None 的泛型类,用来表示 "无值" 的可能性,而不需要语言类型系统大费周折地支持这个概念。实际上,使用 Option[T] 类型可以使问题更加清晰(下一节将用到)。

在使用 Option[T] 时,关键的一点是认识到它实质上是一个大小为 "1" 的强类型集合,使用一个不同的值 None 表示 "nothing" 值的可能性。因此,在这里方法没有返回 null 表示没有找到数据,而是进行声明以返回 Option[T],其中 T 是返回的原始类型。那么,对于没有查找到数据的场景,只需返回 None,如下所示:

清单 1. 准备好踢足球了吗?

@Test def simpleOptionTest =  {    val footballTeamsAFCEast =      Map("New England" -> "Patriots",          "New York" -> "Jets",          "Buffalo" -> "Bills",          "Miami" -> "Dolphins",          "Los Angeles" -> null)        assertEquals(footballTeamsAFCEast.get("Miami"), Some("Dolphins"))    assertEquals(footballTeamsAFCEast.get("Miami").get(), "Dolphins")    assertEquals(footballTeamsAFCEast.get("Los Angeles"), Some(null))    assertEquals(footballTeamsAFCEast.get("Sacramento"), None)  }

注意,Scala Map 中 get 的返回值实际上并不对应于传递的键。相反,它是一个 Option[T] 实例,可以是与某个值有关的 Some(),也可以是 None,因此可以很清晰地表示没有在 map 中找到键。如果它可以表示 map 上存在某个键,但是有对应的 null 值,这一点特别重要了。比如清单 1 中 Los Angeles 键。

通常,当处理 Option[T] 时,程序员将使用模式匹配,这是一个非常函数化的概念,它允许有效地 "启用" 类型和/或值,更不用说在定义中将值绑定到变量、在 Some() 和 None 之间切换,以及提取 Some 的值(而不需要调用麻烦的 get() 方法)。清单 2 展示了 Scala 的模式匹配:

清单 2. 巧妙的模式匹配

@Test def optionWithPM =  {    val footballTeamsAFCEast =      Map("New England" -> "Patriots",          "New York" -> "Jets",          "Buffalo" -> "Bills",          "Miami" -> "Dolphins")              def show(value : Option[String]) =    {      value match      {        case Some(x) => x        case None => "No team found"     }    }        assertEquals(show(footballTeamsAFCEast.get("Miami")), "Dolphins")  }

C# 2.0 可变为 null 值的类型

其他语言已试图通过各种方法解决 "可 null 值化" 问题:C++ 一直都忽略了这个问题,直至最后确定 null 和 0 是不同的值。Java 语言仍然没有彻底解决这个问题,而是依赖于自动装箱(autobox)- 将原语类型自动转换为它们的包装器对象(在 1.1 以后引入)- 帮助 Java 程序员解决问题。一些模式爱好者建议每种类型都应该有一个对应的 "Null Object",即将自己的所有方法重写为不执行任何操作的类型(实际上是子类型)的实例 - 实践证明这需要大量工作。C# 1.0 发布后,C# 设计者决定采取一种完全不同的方法解决 null 值化问题。

C# 2.0 引入了可变为 null 值的类型 的概念,重要的是添加了语法支持,认为任何特定值类型(基本指原语类型)都可以通过将 null 封装到一个泛型/模板类 Nullable< T>,从而提供 null 支持。Nullable< T> 本身是在类型声明中通过 ? 修饰符号引入。因此,int? 表示一个整数也可能为 null。

表面上看,这似乎很合理,但是事情很快就变得复杂起来。int 和 int? 是否应该被视为可兼容类型,如果是的话,什么时候将 int 提升为 int?,反之呢?当将 int 添加到 int? 会发生什么,结果会是 null 吗?这类问题等等。随后类型系统进行了一些重要的调整,可变为 null 值的类型随后包含到了 2.0 中 - 而 C# 程序员几乎完全忽略了它们。

回顾一下 Option 类型的函数方法,它使 Option[T] 和 Int 之间的界限变得很清晰,看上去要比其他方法更加简单。在那些围绕可变为 null 值类型的反直觉(counterintuitive)提升规则之间进行比较时,尤其如此。(函数领域对该问题近二十年的思考是值得的)。要使用 Option[T] 必须付出一些努力,但是总的来说,它产生了更清晰的代码和期望。

元组和集合

在 C++ 中,我们将之称为结构体。在 Java 编程中,我们称之为数据传输对象或参数对象。在 Scala 中,我们称为元组。实质上,它们是一些将其他数据类型收集到单个实例的类,并且不使用封装或抽象 - 实际上,不 使用任何抽象常常更有用。

在 Scala 创建一个元组类型非常的简单,这只是主体的一部分:如果首先将元素公开给外部,那么在类型内部创建描述这些元素的名称就毫无价值。考虑清单 3:

清单 3. tuples.scala

// JUnit test suite  //  class TupleTest  {    import org.junit._, Assert._    import java.util.Date       @Test def simpleTuples() =    {      val tedsStartingDateWithScala = Date.parse("3/7/2006")       val tuple = ("Ted", "Scala", tedsStartingDateWithScala)            assertEquals(tuple._1, "Ted")      assertEquals(tuple._2, "Scala")      assertEquals(tuple._3, tedsStartingDateWithScala)    }  }

创建元组非常简单,将值放入一组圆括号内,就好象调用一个方法调用一样。提取这些值只需要调用 "_n" 方法,其中 n 表示相关的元组元素的位置参数:_1 表示第一位,_2 表示第二位,依此类推。传统的 Java java.util.Map 实质上是一个分两部分的元组集合。

元组可以轻松地实现使用单个实体移动多个值,这意味着元组可以提供在 Java 编程中非常重量级的操作:多个返回值。例如,某个方法可以计算 String 中字符的数量,并返回该 String 中出现次数最多的字符,但是如果程序员希望同时 返回最常出现的字符和 它出现的次数,那么程序设计就有点复杂了:或是创建一个包含字符及其出现次数的显式类,或将值作为字段保存到对象中并在需要时返回字段值。无论使用哪种方法,与使用 Scala 相比,都需要编写大量代码;通过简单地返回包含字符及其出现次数的元组,Scala 不仅可以轻松地使用 "_1"、"_2" 等访问元组的各个值,还可以轻松地返回多个返回值。

如下节所示,Scala 频繁地将 Option 和元组保存到集合(例如 Array[T] 或列表)中,从而通过一个比较简单的结构提供了极大的灵活性和威力。

数组带您走出阴霾

让我们重新审视一个老朋友 - 数组 - 在 Scala 中是 Array[T]。和 Java 代码中的数组一样,Scala 的 Array[T] 是一组有序的元素序列,使用表示数组位置的数值进行索引,并且该值不可以超过数组的总大小,如清单 4 所示:

清单 4. array.scala

object ArrayExample1  {    def main(args : Array[String]) : Unit =    {      for (i <- 0 to args.length-1)      {        System.out.println(args(i))      }    }  }

尽管等同于 Java 代码中的数组(毕竟后者是最终的编译结果),Scala 中的数组使用了截然不同的定义。对于新手,Scala 中的数组实际上就是泛型类,没有增加 "内置" 状态(至少,不会比 Scala 库附带的其他类多)。例如,在 Scala 中,数组一般定义为 Array[T] 的实例,这个类定义了一些额外的有趣方法,包括常见的 "length" 方法,它将返回数组的长度。因此,在 Scala 中,可以按照传统意义使用 Array,例如使用 Int 在 0 到 args.length - 1 间进行迭代,并获取数组的第 i 个元素(使用圆括号而不是方括号来指定返回哪个元素,这是另一种名称比较有趣的方法)。

扩展数组

事实证明 Array 拥有大量方法,这些方法继承自一个非常庞大的 parent 层次结构:Array 扩展 Array0,后者扩展 ArrayLike[A],ArrayLike[A] 扩展 Mutable[A],Mutable[A] 又扩展 RandomAccessSeq[A],RandomAccessSeq[A] 扩展了 Seq[A],等等。实际上,这种层次结构意味着 Array 可以执行很多操作,因此与 Java 编程相比,在 Scala 中可以更轻松地使用数组。

例如,如清单 4 所示,使用 foreach 方法遍历数组更加简单并且更贴近函数的方式,这些都继承自 Iterable 特性:

清单 5. ArrayExample2

object   {    def main(args : Array[String]) : Unit =    {      args.foreach( (arg) => System.out.println(arg) )    }  }

看上去您没有节省多少工作,但是,将一个函数(匿名或其他)传入到另一个类中以便获得在特定语义下(在本例中指遍历数组)执行的能力,是函数编程的常见主题。以这种方式使用更高阶函数并不局限于迭代;事实上,还得经常对数组内容执行一些过滤 操作去掉无用的内容,然后再处理结果。例如,在 Scala 中,可以轻松地使用 filter 方法进行过滤,然后获取结果列表并使用 map 和另一个函数(类型为 (T) => U,其中 T 和 U 都是泛型类型),或 foreach 来处理每个元素。我在清单 6 中采取了后一种方法(注意 filter 使用了一个 (T) : Boolean 方法,意味着使用数组持有的任意类型的参数,并返回一个 Boolean)。

清单 6. 查找所有 Scala 程序员

class ArrayTest  {    import org.junit._, Assert._        @Test def testFilter =    {      val programmers = Array(          new Person("Ted", "Neward", 37, 50000,            Array("C++", "Java", "Scala", "Groovy", "C#", "F#", "Ruby")),          new Person("Amanda", "Laucher", 27, 45000,            Array("C#", "F#", "Java", "Scala")),          new Person("Luke", "Hoban", 32, 45000,            Array("C#", "Visual Basic", "F#")),    new Person("Scott", "Davis", 40, 50000,      Array("Java", "Groovy"))        )       // 查找所有Scala程序员 ...      val scalaProgs =        programmers.filter((p) => p.skills.contains("Scala") )            // 应该只有2      assertEquals(2, scalaProgs.length)            // ... now perform an operation on each programmer in the resulting      // array of Scala programmers (give them a raise, of course!)      //      scalaProgs.foreach((p) => p.salary += 5000)            // Should each be increased by 5000 ...      assertEquals(programmers(0).salary, 50000 + 5000)      assertEquals(programmers(1).salary, 45000 + 5000)            // ... except for our programmers who don't know Scala      assertEquals(programmers(2).salary, 45000)   assertEquals(programmers(3).salary, 50000)    }  }

创建一个新的 Array 时将用到 map 函数,保持原始的数组内容不变,实际上大多数函数性程序员都喜欢这种方式:

清单 7. Filter 和 map

@Test def testFilterAndMap =  {    val programmers = Array(        new Person("Ted", "Neward", 37, 50000,          Array("C++", "Java", "Scala", "C#", "F#", "Ruby")),        new Person("Amanda", "Laucher", 27, 45000,          Array("C#", "F#", "Java", "Scala")),        new Person("Luke", "Hoban", 32, 45000,          Array("C#", "Visual Basic", "F#"))  new Person("Scott", "Davis", 40, 50000,    Array("Java", "Groovy"))      )     // Find all the Scala programmers ...    val scalaProgs =      programmers.filter((p) => p.skills.contains("Scala") )        // Should only be 2    assertEquals(2, scalaProgs.length)        // ... now perform an operation on each programmer in the resulting    // array of Scala programmers (give them a raise, of course!)    //    def raiseTheScalaProgrammer(p : Person) =    {      new Person(p.firstName, p.lastName, p.age,        p.salary + 5000, p.skills)    }    val raisedScalaProgs =       scalaProgs.map(raiseTheScalaProgrammer)        assertEquals(2, raisedScalaProgs.length)    assertEquals(50000 + 5000, raisedScalaProgs(0).salary)    assertEquals(45000 + 5000, raisedScalaProgs(1).salary)  }

注意,在清单 7 中,Person 的 salary 成员可以标记为 "val",表示不可修改,而不是像上文一样为了修改不同程序员的薪资而标记为 "var"。

Scala 的 Array 提供了很多方法,在这里无法一一列出并演示。总的来说,在使用数组时,应该充分地利用 Array 提供的方法,而不是使用传统的 for ... 模式遍历数组并查找或执行需要的操作。最简单的实现方法通常是编写一个函数(如果有必要的话可以使用嵌套,如清单 7 中的 testFilterAndMap 示例所示),这个函数可以执行所需的操作,然后根据期望的结果将该函数传递给 Array 中的 map、filter、foreach 或其他方法之一。

函数性列表

函数编程多年来的一个核心特性就是列表,它和数组在对象领域中享有相同级别的 "内置" 性。列表对于构建函数性软件非常关键,因此,您(作为一名刚起步的 Scala 程序员)必须能够理解列表及其工作原理。即使列表从未形成新的设计,但是 Scala 代码在其库中广泛使用了列表。因此学习列表是非常必要的。

在 Scala 中,列表类似于数组,因为它的核心定义是 Scala 库中的标准类 List[T]。并且,和 Array[T] 相同,List[T] 继承了很多基类和特性,首先使用 Seq[T] 作为直接上层基类。

基本上,列表是一些可以通过列表头或列表尾提取的元素的集合。列表来自于 Lisp,后者是一种主要围绕 "LISt 处理" 的语言,它通过 car 操作获得列表的头部,通过 cdr 操作获得列表尾部(名称渊源与历史有关;第一个可以解释它的人有奖励)。

从很多方面来讲,使用列表要比使用数组简单,原因有二,首先函数语言过去一直为列表处理提供了良好的支持(而 Scala 继承了这些支持),其次可以很好地构成和分解列表。例如,函数通常从列表中挑选内容。为此,它将选取列表的第一个元素 - 列表头部 - 来对该元素执行处理,然后再递归式地将列表的其余部分传递给自身。这样可以极大减少处理代码内部具有相同共享状态的可能性,并且,假如每个步骤只需处理一个元素,极有可能使代码分布到多个线程(如果处理是比较好的)。

构成和分解列表非常简单,如清单 8 所示:

清单 8. 使用列表

class ListTest  {    import org.junit._, Assert._        @Test def simpleList =    {      val myFirstList = List("Ted", "Amanda", "Luke")            assertEquals(myFirstList.isEmpty, false)      assertEquals(myFirstList.head, "Ted")      assertEquals(myFirstList.tail, List("Amanda", "Luke")      assertEquals(myFirstList.last, "Luke")    }  }

注意,构建列表与构建数组十分相似;都类似于构建一个普通对象,不同之处是这里不需要 "new"(这是 "case 类" 的功能,我们将在未来的文章中介绍到)。请进一步注意 tail 方法调用的结果 - 结果并不是列表的最后一个元素(通过 last 提供),而是除第一个元素以外的其余列表元素。

当然,列表的强大力量部分来自于递归处理列表元素的能力,这表示可以从列表提取头部,直到列表为空,然后累积结果:

清单 9. 递归处理

@Test def recurseList =  {    val myVIPList = List("Ted", "Amanda", "Luke", "Don", "Martin")        def count(VIPs : List[String]) : Int =    {      if (VIPs.isEmpty)        0     else       count(VIPs.tail) + 1   }        assertEquals(count(myVIPList), myVIPList.length)  }

注意,如果不考虑返回类型 count,Scala 编译器或解释器将会出现点麻烦 - 因为这是一个尾递归(tail-recursive)调用,旨在减少在大量递归操作中创建的栈帧的数量,因此需要指定它的返回类型。即使是这样,也可以轻松地使用 List 的 "length" 成员获取列表项的数量,但关键是如何解释列表处理强大的功能。清单 9 中的整个方法完全是线程安全的,因为列表处理中使用的整个中间状态保存在参数的堆栈上。因此,根据定义,它不能被多个线程访问。函数性方法的一个优点就是它实际上与程序功能截然不同,并且仍然创建共享的状态。

列表 API

列表具有另外一些有趣的特性,例如构建列表的替代方法,使用 :: 方法(是的,这是一种方法。只不过名称比较有趣)。因此,不必使用 "List" 构造函数语法构建列表,而是将它们 "拼接" 在一起(在调用 :: 方法时),如清单 10 所示:

清单 10. 是 :: == C++ 吗?

@Test def recurseConsedList =  {    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil        def count(VIPs : List[String]) : Int =    {      if (VIPs.isEmpty)        0     else       count(VIPs.tail) + 1   }        assertEquals(count(myVIPList), myVIPList.length)  }

在使用 :: 方法时要小心 - 它引入了一些很有趣的规则。它的语法在函数语言中非常常见,因此 Scala 的创建者选择支持这种语法,但是要正确、普遍地使用这种语法,必须使用一种比较古怪的规则:任何以冒号结束的 "名称古怪的方法" 都是右关联(right-associative)的,这表示整个表达式从它的最右边的 Nil 开始,它正好是一个 List。因此,可以将 :: 认定为一个全局的 :: 方法,与 String 的一个成员方法(本例中使用)相对;这又表示您可以对所有内容构建列表。在使用 :: 时,最右边的元素必须是一个列表,否则将得到一个错误消息。

什么是右关联?

要更好地理解 :: 方法,要记住 "冒号" 这类操作符仅仅是一些名称比较有趣的方法。对于普通的左管理语法,左侧的标记一般是我将要对其调用方法名(右侧的标记)的对象。因此,通常来说,表达式 1 + 2 在编译器看来等同于 1.+(2)。

但是对于列表而言,这些都不适合 - 系统中的每个类都需要对系统中的所有类型使用 :: 方法,而这严重违背了关注点分离原则。

Scala 的修复方法是:以冒号结束的任何具有奇怪名称的方法(例如 :: 或 :::,甚至是我自己创建的方法,比如 foo:)都是右关联的。因此,比方说,a :: b :: c :: Nil 转换为 Nil.::(c.::(b.::(a))),后者正是我需要的:List 在首位,这样每次调用 :: 都可以获取对象参数并返回一个 List,并继续执行下去。

最好为其他命名约定指定右关联属性,但是在撰写本文之际,Scala 已将这条规则硬编码到该语言中。就目前来说,冒号是惟一触发右关联行为的字符。

在 Scala 中,列表的一种最强大的用法是与模式匹配结合。由于列表不仅可以匹配类型和值,它还可以同时绑定变量。例如,我可以简化清单 10 的列表代码,方法是使用模式匹配区别一个至少具有一个元素的列表和一个空列表:

清单 11. 结合使用模式匹配和列表

@Test def recurseWithPM =  {    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil        def count(VIPs : List[String]) : Int =    {      VIPs match      {        case h :: t => count(t) + 1       case Nil => 0     }    }        assertEquals(count(myVIPList), myVIPList.length)  }

在第一个 case 表达式中,将提取列表头部并绑定到变量 h,而其余部分(尾部)则绑定到 t;在本例中,没有对 h 执行任何操作(实际上,更好的方法是指明这个头部永远不会被使用,方法是使用一个通配符 _ 代替 h,这表明它是永远不会使用到的变量的占位符)。但是 t 被递归地传递给 count,和前面的示例一样。还要注意,Scala 中的每一个表达式将隐式返回一个值;在本例中,模式匹配表达式的结果是递归调用 count + 1,当达到列表结尾时,结果为 0。

考虑到相同的代码量,使用模式匹配的价值体现在哪里?实际上,对于比较简单的代码,模式匹配的价值不很明显。但是对于稍微复杂的代码,例如扩展示例以匹配特定值,那么模式匹配非常有帮助。

清单 12. 模式匹配

@Test def recurseWithPMAndSayHi =  {    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil        var foundAmanda = false   def count(VIPs : List[String]) : Int =    {      VIPs match      {        case "Amanda" :: t =>          System.out.println("Hey, Amanda!"); foundAmanda = true; count(t) + 1       case h :: t =>          count(t) + 1       case Nil =>          0     }    }        assertEquals(count(myVIPList), myVIPList.length)    assertTrue(foundAmanda)  }

示例很快会变得非常复杂,特别是正则表达式或 XML 节点,开始大量使用模式匹配方法。模式匹配的使用同样不局限于列表;我们没有理由不把它扩展到前面的数组示例中。事实上,以下是前面的 recurseWithPMAndSayHi 测试的数组示例:

清单 13. 将模式匹配扩展到数组

@Test def recurseWithPMAndSayHi =  {    val myVIPList = Array("Ted", "Amanda", "Luke", "Don", "Martin")     var foundAmanda = false       myVIPList.foreach((s) =>      s match      {        case "Amanda" =>          System.out.println("Hey, Amanda!")          foundAmanda = true       case _ =>          ; // Do nothing      }    )     assertTrue(foundAmanda)  }

如果希望进行实践,那么尝试构建清单 13 的递归版本,但这不用在 recurseWithPMAndSayHi 范围内声明一个可修改的 var。提示:需要使用多个模式匹配代码块

感谢你能够认真阅读完这篇文章,希望小编分享的"从Java走进Scala如何使用元组、数组和列表"这篇文章对大家有帮助,同时也希望大家多多支持,关注行业资讯频道,更多相关知识等着你来学习!

0