印度朋友手把手教你学Scala(7):无处不在的对象


/**
 * 谨献给我最爱的YoYo 
 *
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-7-objects-everywhere/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-06
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

无处不在的对象

在这篇文章中,我们会讨论为什么Scala中对象的概念更为普遍,以及它的优点是什么。大部分示例是数据类型,因为它们是Scala语言的基础。

这是关于Scala系列教程的第七章。点击这里可查看整个系列。

目录

  • 简介
  • 作为对象的数据类型
  • 类型的操作
  • 创建自定义类型
  • Java数据类型装箱/拆箱的比较
  • Bigint例子
  • 类型转换(Typecasting)
  • 值比较
  • 在其他语言的实现和一些注意事项

简介

Scala是一种多范式语言,主要是结合了面向对象和函数式。

为了支持这种语言,有必要制定一个围绕什么东西而构建的想法。正如我们前面所看到的,在Scala中没有原始类型,一切都是对象。不要和我们在第4章看到的单例对象混淆,这里说的对象是指类的实例。在本文的其余部分,一个对象意味着通常Java所说的一个类的实例。

这两个概念,前面我们已经见过,即面向对象和函数式是相互正交的。

我不会给出一个详尽的列表,什么是所有的对象和什么不是,但我会解释的背后的想法和优势,以及为什么它是重要的。

作为对象的数据类型

在scala中没有本地数据类型,所有数据类型都有自己的类。

现在你将如何设计一些基本的数据类型?事实证明,只要看起来适当,所有的数据类型映射到Java中的本地数据类型。我们将看一个例子,即Int类型,因为它更容易解释,同样的想法可扩展到几乎所有的类型。

您可以尝试反编译包含Int的类,并看到它会编译成public static int,即Java的基本类型int。

除了String之外,Scala中的所有数据类型都存放在scala包中。

以下列出在JVM上直接转换为本地类型的数据类型。

Scala类型运行时映射
[Int.scala](http://www.scala-lang.org/api/current/scala/Int.html)int
[Short.scala](http://www.scala-lang.org/api/current/scala/Short.html)short
[Double.scala](http://www.scala-lang.org/api/current/scala/Double.html)double
[Long.scala](http://www.scala-lang.org/api/current/scala/Long.html)long
[Byte.scala](http://www.scala-lang.org/api/current/scala/Byte.html)byte
[Float.scala](http://www.scala-lang.org/api/current/scala/Float.html)float
[Boolean.scala](http://www.scala-lang.org/api/current/scala/Boolean.html)boolean
[Char.scala](http://www.scala-lang.org/api/current/scala/Char.html)char

Scala变量没有创建对象的额外开销,它们都映射到JVM中的本地类型。

类型的操作

我们已经理解所有基本类型都是Scala中的对象。下一个重要的事情是,怎么做操作它们呢?由于添加两个对象是不可能的,Scala使用了我们前面在类中看到的称为合成方法的东西。

我们在Java原始类型(如+-*等)中执行的所有操作都将作为方法实现。 让我们以Int.scala为例,看看加法是如何实现的。

 /** Returns the sum of this value and `x`. */
  def +(x: Byte): Int
  /** Returns the sum of this value and `x`. */
  def +(x: Short): Int
  /** Returns the sum of this value and `x`. */
  def +(x: Char): Int
  /** Returns the sum of this value and `x`. */
  def +(x: Int): Int
  /** Returns the sum of this value and `x`. */
  def +(x: Long): Long
  /** Returns the sum of this value and `x`. */
  def +(x: Float): Float
  /** Returns the sum of this value and `x`. */
  def +(x:Double): Double

从上面的代码片段和Int.scala,可以推断出很多东西。

  • 上面的代码示例定义了Int类型和其他类型全部可能的加法操作。对于BooleanDouble等其他类型的操作具有相同的逻辑。
  • 上面的定义是在一个抽象类中,它是final的,因此不能被扩展。
  • 正如我们在第二章看到的,所有带有值类型的变量扩展了AnyVal,它也扩展了Any

上述定义基本运算符的所有方法都是抽象的。那么如何实现呢?

如果你尝试相同的普通类,肯定会产生一个错误。

abstract class CustomVariable {

  def +(x: Byte): Int

}

那么它是如何工作,并被翻译为本地类型呢?所有这些都编译器的魔法。像往常一样,让我们通过反编译几个类来理解。

记住,Scala类型类有特殊的意义,因为它们遵循层次结构,这就是为什么Int.scala是抽象的,但仍然可以使用它的原因。

考虑以带自定义实现+函数的类。

class CustomVariable {

  def +(y:Int) : Int = {
    this+y
  }

}

编译如下。

public class CustomVariable {  
  public int $plus(int);
    Code:
       0: aload_0
       1: iload_1
       2: invokevirtual #12                 // Method $plus:(I)I
       5: ireturn

  public CustomVariable();
    Code:
       0: aload_0
       1: invokespecial #20                 // Method java/lang/Object."<init>":()V
       4: return
}

+会编译成$plus,因为前者不是一个合法的Java标识符。

再来看另外一个例子。

class CustomVariable {

  def add_1(x: Int, y: Int) = x + y

  def add_2(x: Int, y: Int) = (x).+(y)

}

反编译如下。

public class CustomVariable {  
  public int add_1(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn

  public int add_2(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn

  public CustomVariable();
    Code:
       0: aload_0
       1: invokespecial #20                 // Method java/lang/Object."<init>":()V
       4: return
}

+.+被编译为相同的iadd操作。这种不用点运算符调用类成员的方法称为中缀符号,我们稍后在专门的教程中会看到。

因为+魔术在编译时完成,所以+本身表示为一个合成函数。

创建自定义类型

我们看看Int.scala是怎么写的。但是如何没有在编译器的帮助下,你怎么创建一些基本的东西。

如果你参加Coursera的“Scala中的函数式编程原理 - Martin Odersky”课程,那么你会熟悉这一点。尽管如此,下面是一个以简洁的方式解释的视频。
https://www.youtube.com/watch?v=Uu9BaV6sKPQ

如果上面任何列出的YouTube视频被删的话,你可以在Coursera注册一下课程,即使你不需要证书,你可以审核课程/看视频。此视频列在课程中的讲座4.1中。

可能有一些部分的视频,你不完全理解,先忽略他们,我后面会讲到。

这应该给你一个直觉,为什么在Scala做了这样的一个选择。

Java数据类型装箱/拆箱的比较

与Scala不同,Java有原始类型和盒式类型(boxed types)。当与Scala比较时,这是丑陋的,因为Scala里一切都是对象。

我们已经看到了Scala类型如何转换为本地JVM原始类型,但是在Java中它们如何工作的呢?

让我们以下面的类为例。

public class Test {

    public static void main(String[] args) {

        Integer i = 10;

        Integer k = 30;

        System.out.println(i+k);

    }
}

它会编译成,

public class Test {  
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: bipush        30
       8: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      11: astore_2
      12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: aload_1
      16: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
      19: aload_2
      20: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
      23: iadd
      24: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
      27: return
}
  • Integer.valueOf把原始类型10,30包装成盒装类型。
  • i+k相加会转换成i.intValue + k.intValue
  • intValue方法返回赋给盒装类型的原始类型。

所以上面的操作创建了四个Integer.java类的实例。这是必须要做的,因为Integer.java在运行时本质上是对象,不像Scala的int,它在编译时转换为原始类型。

这种区分很重要,尤其在理解Scala集合时至关重要。

我们将讨论关于这个整体的两个优点,一切都是对象的表示。

Bigint例子

除了Scala不为本地类型创建对象的优势外,它还为其他类型(如Bigint)提供了方便的语法。

在Java中,要相加两个BigInteger类型,我们需要使用特殊的方法调用。

import java.math.BigInteger;

public class Test {

    public static void main(String[] args) {

        BigInteger a = new BigInteger("2000");
        BigInteger b = new BigInteger("3000");

        // 会导致一个错误
        System.out.println(a+b);

        // 正确的版本
        System.out.println(a.add(b));


    }
}

现在这对大整数是没问题的,因为我们知道在Java中它不是一种原始类型,但对于开发者来说则是一个额外的负担。

在Scala中,由于一切都是对象,操作符本身也是方法,所以相加两个大整数相当简单。

object Runnable extends App{


  val x = BigInt("92839283928392839239829382938")

  val y = BigInt("19020930293293209302932309032")

  println(x+y)

}

我们必须将它们表示为字符串,因为即便对于long类型,它们还是太大了。

这提供了一种便利的语法,我们可以使用我们用于已知数据类型的相同运算符。

可以看到,对于开发人员这是一个方便的功能,但它的好处不止这些。随着我们更多地进入Scala的世界,我们还将遇到更多高级的数据类型,如代数(Algebraic)类型,它们在机器学习/数学相关的计算中是很有用的。更有趣的是,因为一切都表示为对象,我们现在可以抽象某些事情,使得程序员更容易编写自己的类型。

类型转换(Typecasting)

既然现在所有的类型都是对象/类,并且它们都遵循顶层类型是Any类型的树层次结构,它们可以有一些对所有类型都有用的通用方法。

我们将在下面探讨某中一些。

object Runnable extends App{


  val IntegerType = 20
  val DoubleType = 20.0
  val LongType : Long = 20
  val FloatType : Float = 20.4f

  println(IntegerType.toDouble)
  println(DoubleType.toInt)
  println(LongType.toShort)
  println(FloatType.toDouble)


}

每种类型都具有转换成一个或多个其他类型的类型转换。由于这些方法是通用的,并且如果有任何错误,它们会是在编译时而不是运行时,所以不必担心当前正在考虑的类型。

在Java中,这些类型的转换是相当麻烦的。

public class Test {

    public static void main(String[] args) {

        int a = 20;
        double d = 30.0;
        long b = 30l;
        float c = 20.0f;

        // 原始类型转换
        System.out.println((double) a);
        System.out.println((int) d);
        System.out.println((short) b);
        System.out.println((double) c);

        // 盒装类型转换
        System.out.println(Integer.valueOf(a).doubleValue());
        System.out.println(Double.valueOf(d).intValue());
        System.out.println(Long.valueOf(b).shortValue());
        System.out.println(Float.valueOf(c).doubleValue());


    }
}

这是为什么一切皆是对象会是有利的另一个例子。甚至可以包括一个方法,如toRoman,将给定的值转换为罗马数字。程序员可以选择放置在哪个类,一个例子是Int.scala。

还有好几个,但重点是给你一个感性的认识,而不是一个详尽的列表。

值比较

在Java中,你将被教导说不要使用==运算符来比较对象类型,因为它们会比较引用。

Scala对此有不同的看法。正如我们看到的,可以用==来比较样本类。这是因为一切都是Scala中的值

这样说,既然一切都是对象,我们可以针对==比较定义自己的方法。

就像上面的+方法一样,``==```是Scala库中的一个合成函数。

  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Byte): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Short): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Char): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Int): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Long): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Float): Boolean
  /** Returns `true` if this value is equal to x, `false` otherwise. */
  def ==(x: Double): Boolean

上面的代码编译为像比较的本地Java代码,即对于字符串,它背后是逐个字符进行比较的equals方法。

在其他语言的实现和一些注意事项

原来,Scala不是第一个实现“无处不在的对象”这一概念的语言。

Ruby中,没有原始类型,它们的操作几乎和Scala类似,除了Scala是以非常纯粹的面向对象外这意味着一切皆是对象。

在后面的教程中,我们将探讨这个概念如何能让我们可以在基本类型编写自定义的方法,如toRoman和如代数数据类型这样的自定义类型。

第8部分,即下一章,我将解释另一个很酷的Scala特性,即物质(比样本类更酷?),以及它如何真正把面向对象编程带到一个新的层次。

敬请关注&新快乐!^_^

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州