/**
* 谨献给我最爱的YoYo
* 原文出处:https://madusudanan.com/blog/scala-tutorials-part-1-getting-started/
* @author dogstar.huang <chanzonghuang@gmail.com> 2017-02-23
*/
本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。
Scala入门
这是关于Scala系列教程的第一章。点击这里可查看整个系列。
如果你之前是Java开发人员,这里将会有很多让你头疼的东西,而最容易的学习方式就是忘却已知的一切,从头学起(译者注:即保持空性)。
让我们开始吧。
目录
以下是本章将会覆盖的内容。
- Scala简介和环境搭建
- Scala REPL,一段简短的介绍
- Hello World !!
- 变量
- 引用和值不可变性
- 幕后的不可变性
val
和final
的对比- Scala里的数据类型
- 类型接口
- 变量初始化
- 类型注解
- 类型归属
- 惰性求值
Scala简介和环境搭建
如果你完全没有接触过Scala,我建议你先阅读一下这篇博客。
在网上,有很多谈论Scala安装的教程,所以我就不再赘述。
我推荐的安装如下:
- 操作系统:首选Mac/Linux
- 选择社区版本的IDE,并安装Scala插件
如果你已完成了安装,那么就开始吧。
Scala REPL(交互模式),一段简短的介绍
以下是关于Scala REPL一段简短的介绍。
Scala REPL介绍
在学习Scala的过程中,REPL就是你的瑞士军刀。
Hello World !!
如果你是来自Java背景的,那么Scala在表达方面会与Java有所不同。先来看一些代码。
object Test {
def main(args : Array[String]){
println("Hello World")
}
}
我推荐把这些代码放到你的IDE里去,然后右击运行它。如果一切顺利的话,你应该会在控制台看到Hello World。
恭喜你!!你刚刚编写了第一个Hello World程序。
现在来对这段代码进行分解。
首先要注意的事,就是整段代码是在一个Object代码块里。Java程序员可能觉得这样会很困惑。暂且把这个问题放一边,下一章会以更详细讲解这点。不像Java,类名不需要与文件名匹配,虽然这并不是什么大不了的事情,但这里我们有这样的自由。^_^
下一件事是def main()
奇怪的语法。以def
为开头的def
是声明方法的关键字。在接下来的教程里我们会更详尽地讲解方法。
既然有了方法,那么自然就会有参数。在这里,它就是Array[String]
。这与Java的main方法相似,对于main方法有一个string类型的数组作为参数。可用于启动配置或者其他任何东西,当然使用说明完全是可选的。
接下来调用了一个叫println()
的方法,它会在控制台上打印语句。如果使用的是IDE(你应该是),你可以通过在这个方法上按住“ctrl + 单击”来追踪整个调用。Python的同学应该会觉得这种语法很熟悉,并且事实上对于一名程序员来说,这无非就是把一段字符串打印到控制台上而已,别无其他。
但让我们更进一步。对于Java的同学,通常在控制台上打印应该像这样System.out.println
。那么,为什么这里会不一样呢?
答案就是,其实并没有什么不一样,println()
是一个叫做Predef.scala类里的一个方法,随后这个方法会调用Console.scala的println方法,近而调用PrintStream.class或者PrintStream.java上的out.println
,如果你有源代码绑定的话,否则来自Intellij的编译器就会显示反编译后的代码。来自Scala和Java的打印都是以相同的方法调用而结束。正如前面说过的,Scala是构建在JVM之上的,所以可以像我们刚刚看到的那样,Scala能和Java代码无缝地互操作,如果这种实现不同的话,那么应该有一个特定的理由,否则它就是在重复造轮子。
事实上,在我们学习Scala的旅途上,还有很多很多采用了Java类库的示例。
一个简单的Hello World开启了很多有待学习的主题,尤其是这三部分:
- 方法(将会有后面覆盖到)
- 对象和类(将会有后面覆盖到)
- 类型接口 -- 为什么Scala是一种静态类型动态语言的原因,下面会解释。
Scala里的变量
在深入变量之前,我应该先解释一下数据类型,但有一些基本的区别我想先说说,以便我们可以更深入地理解他们。
var
和val
是在Scala用来声明变量的两个关键字。
var
用来声明可以修改的变量,而val
用来声明不可变的变量。但这些变量是什么类型呢?对于原始数据类型,可变性的概念来自哪里?我建议你阅读一下StackOverflow上的这篇文章。原始自己(Primitives by themselves)是不可变的,例如他们的类型一旦声明了就不能改变,但他们的值是可变的,例如他们可以被修改。
关于为什么不可变的概念来自变量而不是对象,一开始会令人困惑,这点会在下面的数据类型部分中进行解释,在Scala里没有原语类型,一切皆是对象。
引用和值不可变性
如果val
是不可变的,那么它就不能被改变吗?这是不是和Java里的final关键字类似,或者它是否和String的不可变性有所联系?
让我们来看一些示例代码,如下所示,来帮助我们更好地理解。
class Test {
def main (args: Array[String]) {
var myVar = 10
// 完美运行
myVar = myVar + 10
val myNum = 6
// 会导致编译时错误
// Reassignment to val
myNum = myNum + 10
}
}
如果你运行上面的代码,你会在编译里发现一个诸如“reassignment to val”的错误。如果你正在使用IDE,这些在你敲代码时就会显示出来,因为IDE提供了预编译。
首先,它绝然不像是String的不可变性,String的不可变性对程序员是不可见的,而这里是在编译级别控制的。下一个问题,它和Java里的final类似吗?
从鸟瞰图来看,这看起来是类似的,因为一旦某个值赋给它后,它就不能被改变了,但在JVM内部final
与不可变性没有任何关系,并且使用它是为了类不能被扩展,以及以防它的方法被重载。
考虑以下用来演示他们是不同的Java代码。
final ArrayList<Integer> arrList = new ArrayList<Integer>();
/*
这样不会导致错误,因为我们改变的是对象本身。如果它被改变了的话,会触发像它不能被修改这样的错误
*/
arrList.add(20);
/*
这样会导致错误,因为我们修改了引用,而非对象本身
*/
arrList = null;
如果你尝试对原始类型用同样的final
,那么它的值就不能被改变。这是不是意味着原始类型就是不可变的?这把我们带回了上面stackoverflow的讨论,但是之所会出现错误的原因是因为Java对原始类型使用了按值传递,既然他们根本不是对象,所以按引用传递毫无意义。所以如果一个变量改变了,它的引用(也可以说成是在内存里的位置)随之改变,这是因为按值传递的机制,而不是因为原始类型是不可变的。
理解的要点就是在Scala里,引用和不可变性的区分并不是特别重要,因为Scala根本就没有原始类型,只有对象。
所以不管何时我们谈论Scala的不可变性,我们就是在谈论引用不可变性。
不可变的变量确实有性能上的优势,而且也更进一步地学习编写没有副作用代码这一概念。
幕后的不可变性
让我们来通过查看反编译生成的Scala类文件的字节码,进一步加深对不可变性的理解。
我们声明了一个叫做Parent
的类,以及在它里面的一个变量。
class Parent {
val x = 10
}
看着生成的字节码,我们可以看到它翻译成了一种Java原始类型,就运行时而言没有什么特别的。 javap -c filename.class
生成了以下反编译的代码。
public class Parent {
public int x();
Code:
0: aload_0
1: getfield #13 // 字段 x:I
4: ireturn
public Parent();
Code:
0: aload_0
1: invokespecial #19 // 方法 java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #13 // 字段 x:I
10: return
}
很明显,val
只是一个编译时的限制,与生成的字节码没有半毛钱关系。
通过阅读反编译的字节码这一途径,我们可以理解地更深入,但在大多数情况里,这并不是必须的。
val
和final
的对比
另一方面,Scala也有final
关键字,其工作非常类似。区别可视化val和final更好的方式是通过一个示例。
让我们来看一个简单的Parent类。
class Parent {
val age = 10
}
不像Java,Scala也能重载变量。
class Child extends Parent{
override val age = 30
def printVal ={
println(age)
}
}
现在如果我们在Parent类里把age
变量声明为final,那么其子类就不能重载它,否则就会抛出以下的异常。
这是现实世界中会使用到final
的示例。
注意,通过在子类里重载某个val,在这里我们并没有破坏不可变性。子类创建了一个它自己的实例,就像Java字符串池里的一个字符串一样。
Scala里的数据类型
Scala有和Java一样的数据类型,以及一样的内存和缜密。
全部的数据类型都是对象,并且在他们里面可以调用方法,就像你是在一个对象上面那样。
val t = 69
// 打印“E”,E对应的ASCII码是 69
println(t.toChar)
val s = "Hello World"
// 就像是在字符串里的字符,打印 l
// 可追踪到相同的String类charAt方法
println(s.charAt(2))
到现在为止,另一个问题是否已水落大出?在Scala代码里类型在哪?
不像Java那样用数据类型声明变量然后给变量一个名字,Scala有一些叫做类型接口(参考下面的主题)的东西,但先别跳到那去,这是为什么我把它从变量部分单独出来的一个原因。
类型接口
如果你对这个短语不熟悉的话,它不过就是在编译时的类型推断而已。且慢,这不正是动态类型的意思吗?其实不是的,请注意我说的是类型推断,这明显与动态类型语言所做的大为不同,而另一件事是它是在编译时而不是在运行时完成的。
很多语言内置了这一点,但实现方式各有各有不同。可能一开始会困惑,但通过示例代码这将会慢慢变得清晰明朗。
让我们进入到Scala交互模式,做一些实验。
从上面的图中,明显没有什么魔法,这些变量在编译时会自动被推断成他们认为最合适的类型。
为了进一步理解,这里有更多的代码。
val x = 20
// 打印到控制台
// 合法调用,将x推断为一个整型
println(x+10)
// 以下是一些愚蠢的做法,会抛出一个编译时错误
val z = 40
println(z * "justastring")
继续往前,和这些变量玩一玩,你会受到编译时类型安全的保护,所以不要犹豫,尽情尝试吧。
如果对类是怎么对变量进行扩展的感到好奇,你可以更深入地挖掘并且会找到一个叫AnyVal
的类。这是Scala统一类型系统完全不同的主题的一部分,也就是类层次结构。
译者注:Scala的类层次结构如下:
类型接口会在这系列教程的第2章,进一步讲解。
变量初始化
在Scala里,你不能简单地创建一个变量,而未对它进行初始化。
这是Scala语言开发者所做的一个设计选择。明显的原因是为了避免变量未初始化和空指针异常。
唯一一个地方不需要把值赋给变量的地方是在抽象类里。当学习Scala的类时会看到更多相关的内容。
类型注解
Scala可以让我们明确地指定类型。 val y : Integer = 20
这些类型注解的类型在公开的API/方法里很重要。
温馨提示:方法会在第3章解释。
def getInfoFromBackend() = {
val dataList = List(1,"Literature",2,"Science")
dataList
}
没有明确地注释类型信息的话,开发人员觉得这样会引起理解上的困惑。记住,并不是在JVM上的所有语言都有像Java这样的类型接口,所以对类型的小小改变就会破坏用户(假设)的代码。
更好的版本如下:
def getInfoFromBackend() = List {
val dataList = List(1,"Literature",2,"Science")
dataList
}
译者注:多了List类型注解。
这种理念不仅适用于方法参数,也适用于使用val
关键字声明的变量。上面的示例只是演示了一种更容易理解的方式这一想法。
类型归属(Type ascriptions)
类型归属是更为复杂的东西。它指的是告诉编译器,你将要执行的某个操作所期望输出的是什么类型这一过程。
一个典型的使用场景是Java里的类型转换。
int x = 20;
// 有效的转换
System.out.println((byte) x);
// 运行时错误
Object s = new Object();
System.out.println((byte) s);
上面代码会导致一个运行时的异常/错误,如下所示。
20
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Byte
at JavaExample.main(JavaExample.java:14)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
在Scala里,当我们使用类型归属,此代码甚至不能编译。
类型注释和类型归属之间没有语法区别,并且经常会导致这两者之间的混淆。
使用在运行时的类型转换,我们可以取得同样的Java路线。
object Demo extends App{
val x = new Object().asInstanceOf[Byte]
}
在上面场景中,你可以观察到它并没有抛出一个编译时的错误,但会导致运行时同样的异常堆栈,例如:java.lang.ClassCastException
。
在执行类型转换时,类型归属令人难以置信地好用。你可以在编译时而不是在运行时检查类型安全,以确保不会导致有缺陷的代码。
惰性求值(lazy val)
正如名字所示,Scala的惰性求值和val类似,但它的值仅当此变量被用到时才会被求值。
import scala.io.Source._
object ReadFileExample extends App{
println(System.getProperty("user.dir"))
lazy val lines = fromFile(System.getProperty("user.dir") + "/file1.txt").getLines
println(lines)
}
我建议你亲自尝试一下这段代码。首先通过注释掉println(line)
,你会看到即使那个文件不存在也不会导致错误。
但这是一个可以工作的示例,你可以在项目的顶级放置一个真实的文件,这里是项目根目录下的一个指定路径,随后这段程序将会把文件内容打印出来。
这是在编译时控制的,因为在编译时可以知道变量访问。
在诸如浏览器里的文件上传窗口这样的场景里相当有用。用户有可能上传文件,也可以不上传,所以最好是延迟/懒初始化直到事件发生。
这里接近本章的尾声了,我们从重要的部分开始了。如果还没明白的话,我建议你重新阅读这篇文章,并且参考在网上的相关文档。
把其他语言简单的代码片段翻译成Scala也会有所帮助。
一旦我发布了我在这里提及到的更多的主题,我会尽快更新相关链接到这篇文章。如果想保持更新你也可以进行订阅。
Scala并不容易,但它也不难,如果我们脚踏实地,一步一个脚印慢慢地学的话。我的目标不是要教会你所有东西,而是简单地指明一个方向,之前很少被覆盖过的一个方向。
好好学习,敬请关注更多博客。^_^
------------------------
- 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。
- 本文翻译作者为:dogstar,发表于艾翻译(itran.cc);欢迎转载,但请注明出处,谢谢!