返回介绍

数学基础

统计学习

深度学习

工具

Scala

六、Array

发布于 2023-07-17 23:38:22 字数 6560 浏览 0 评论 0 收藏 0

  1. ArrayScala 提供的数组,它是可变对象,即:可以修改Array 的元素内容。

    数组在 scala 中是一种特殊的集合。

    • 一方面,scala 的数组跟 java 的数组一一对应。即,scala 中的 Array[Int] 是用 javaint[] 来表示, scala 中的 Array[Double] 是用 javadouble[] 来表示。

    • 另一方面,scala 的数组比 java 数组提供更多的功能。

      • 首先,scala 数组支持泛型。即,可以有 Array[T]T 为类型参数或者抽象类型。
      • 其次,scala 数组跟 scala 的序列兼容(可以在要求 Seq[T] 的任何地方传入 Array[T] )。
      • 最后,scala 数组还支持所有的序列操作。
  2. scala 通过使用隐式转换来支持java 数组额外的那些功能。Array 并不是 Seq 类型,因为 Array 并不是 Seq 的子类型。

    • 每当数组被用于 Seq 时,它都被隐式转换为 Seq 的子类,这个子类叫做 scala.collection.mutable.WrappedArray

      
      
      xxxxxxxxxx
      val a1 = Array(1,2,3) val seq: Seq[Int] = a1 // seq 为 WrappedArray[Int] 类型 val a2 = seq.toArray // a2 为 Array[Int] 类型

      从这些交互可以看到:数组和序列是兼容的,因为有一个从ArrayWrappedArray 的隐式转换。

      如果反过来,希望从 WrappedArray 转换为 Array,则可以通过Traversable 中定义的 toArray 方法。此时可以得到与原始数组相同的结果(a2 的内容和 a1 完全相同)。

    • 另外,每当对数组执行序列的操作时,Array 都被转换为ArrayOps 对象。ArrayArrayOps 对象的转换仅仅是将所有的序列方法 “添加” 到数组,但是并没有将数组转换成 Seq 的子类。ArrayOps 对象支持所有的序列方法。

      通常 ArrayOps 的生命周期很短:通常在调用完序列方法之后就不会再使用了,因此它的存储空间可以被立即收回。现代的 VM 会完全避免创建这个对象。

    这两种隐式转换的区别可以通过以下示例看到:

    
    
    xxxxxxxxxx
    val a1 = Array(1,2,3) val seq: Seq[Int] = a1 // seq 为 WrappedArray[Int] 类型 val seq2 = seq.reverse // seq2 为 WrappedArray[Int] 类型 val ops: collection.mutable.ArrayOps[Int] = a1 //ops 为 ArrayOps[Int] 类型 val a3 = ops.reverse // a3 为 Array[Int] 类型

    WrappedArray 调用 reverse 返回 WrappedArray 类型,这合乎逻辑。因为对 Seq 调用 reverse 都会返回 Seq

    ArrayOps 调用 reverse 返回的是 Array 而不是 Seq ,也不是 ArrayOps

  3. 通常你从来都不会自己去创建 ArrayOps 类型的对象,只需要对数据调用 Seq 方法即可:

    
    
    xxxxxxxxxx
    val a1 = Array(1,2,3) val a3 = a1 reverse // 等价于先人工创建 ArrayOps[Int] 对象,在对其调用 reverse

    scala 会自动插入 ArrayOps 对象,因此上述代码和下面的代码等价:

    
    
    xxxxxxxxxx
    val ops: collection.mutable.ArrayOps[Int] = a1 //ops 为 ArrayOps[Int] 类型 val a3 = ops.reverse // a3 为 Array[Int] 类型
  4. 编译器是如何知道选择 WrappedArray 隐式转换,还是选择 ArrayOps 隐式转换呢?毕竟这两种隐式转换都可以将数组转换为支持 reverse 的类型。

    答案是:这两个隐式转换存在优先级。ArrayOps 的优先级高于 WrappedArray 转化。前者定义在 Predef 对象中,后者定义在 scala.LowPriorityImplicits 类中,这个类是 Predef 的超类。子类和子对象中的隐式转换比基类的隐式转换优先级更高。因此,如果两个隐式转换同时可用,编译器会选择 Predef 中的那个。

  5. Java 中,你没有办法定义数组的泛型,即没办法写 T[],其中 T 为类型参数。scala 中的数组支持泛型,因此可以写出像 Array[T] 的泛型数组。

    scalaArray[T] 存储的对象统一映射到 AnyRef ,从而支持 byte, short,char,int,long,float,double,booleanjava 基础类型,以及java 对象类型。在运行时,当类型为 Array[T] 的数组的元素被访问或更新时,有一系列的类型检查来决定实际的数组类型。然后才是对 java 数组的正确操作。这些类型检查会减慢数组操作。可以预期,对泛型数组的访问跟基本类型数组或对象数组的访问相比会慢三到四倍。

    这意味着:如果你希望最大限度的改善性能,应该考虑具体类型的确定数组,而不是泛型数组。

  6. 仅能够表示泛型的数组类型是不够的,我们还需要某种方式来创建泛型数组。为说明问题,我们考虑以下代码:

    
    
    xxxxxxxxxx
    def evenElems[T](xs: Vector[T]): Array[T] = { val arr = new Array[T]((xs.length + 1) / 2) for(i <- 0 untile xs.length by 2){ arr(i/2) = xs(i) } arr }

    这段代码返回输入 xs 的偶数位置元素组成的Array。考虑到类型参数 T 对应的实际类型信息在运行时被擦除,因此 new Array[T]((xs.length + 1) / 2) 甚至无法编译通过。

    这时编译器需要提供额外的信息来获取类型参数T 的运行时线索。这个线索的表现形式是类型为 scala.reflect.ClassTag 的类标签class tag

    类标签描述的是给定类型 “被擦除的类型”,这也是构造该类型的数组所需要的全部信息。在很多情况下,编译器多可以自行生成类标签。对于具体类型Int 或者 String 就是如此。但是对于某些泛型类型,如 List[T] 也是如此,因为有足够多的已知信息来预测被擦除的类型。

    对于完全泛化的场景,通常的做法是用上下文界定传入类型标签,如:

    
    
    xxxxxxxxxx
    import scala.reflect.ClassTag def evenElems[T: ClassTag](xs: Vector[T]): Array[T] = { .... }

    此时,当Array[T] 被创建时,编译器会查找类型参数 T 的类标签。也就是说,它会查找一个类型为 ClassTag[T] 的隐式值。如果找到这样的值,类标签就被用于构造正确类型的数组。否则,编译器报错。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文