Scala学习笔记-入门(1)-Scala简介

Scala简介

Scala是一门以JVM为运行环境,并将面向对象和函数式编程的最佳特性结合在一起的一种静态类型编程语言。

  • Scala是一门多范式的编程语言,支持面向对象和函数式编程

    多范式指多种编程方式,如面向过程、面向对象、泛型、函数式编程等

  • Scala的源代码会被编译成字节码,然后运行在JVM上,并且可以调用现有的Java类库,实现两种语言的无缝对接

在学习Scala的过程中,应当多加注意Scala与Java的区别。

在编译运行过程中,Java使用javacjava命令。由于同样是运行在JVM之上,Scala也有对应的过程。Scala的源代码文件后缀为.scala,通过scalac命令可以将源代码编译成字节码文件.class,之后使用scala命令进行运行。但是具体细节有所不同,这点在后续的HelloWorld原理对比中会进行说明。

环境搭建

本地环境搭建

  1. 确保JDK 1.8安装成功
  2. 下载对应的Scala安装文件,这里选择的是scala2.12版本
  3. 之后解压到某个路径
  4. 将对应路径配置为SCALA_HOME,并将对应的bin目录配置到path环境变量中

完成上面步骤之后,可以进入命令行输入scala,之后就可以看到Scala的版本并进入交互式命令行模式,在里面可以尝试简单的指令。通过:quit命令退出。

IDEA开发环境配置

  1. 正常创建一个Maven项目
  2. 默认情况下IDEA不支持Scala的开发,需要安装Scala插件
  3. 之后需要关联Scala的依赖包,具体路径:File->Project Structure->Platfrom Settings->Global Libraries->+,在其中关联对应路径,应选择SCALA_HOME
  4. 为项目添加框架依赖,右键项目目录,选择Add Framework Support,之后添加其中的Scala即可

如果在Global Libraries中已经关联了对应的依赖,可以右键选择添加到对应的Module上。

目前我们的项目中已经可以书写Scala代码了。在Maven项目中,我们的Java源代码写在java目录下,为了规范,我们也可以创建对应的源文件目录。在main目录下新建一个文件夹,命名为scala,然后将这个文件夹标记为Sources Root。原本的java目录可以删除,但是建议保留。因为Scala可以和Java代码无缝连接,后续项目中的Java代码可以写在对应的文件夹中,便于管理。这样就可以进入HelloWorld的编写了。

关联源码

  1. 首先在官网下载源码包scala-sources-2.12.15.tar.gz
  2. 然后将其拷贝到对应SCALA_HOME/lib目录下,并解压为scala-source-2.12.15文件夹
  3. 之后Attach Sources选择对应文件夹即可关联源码

HelloWorld以及原理对比

HelloWorld

下面是经典HelloWrold程序,Scala源代码如下:

1
2
3
4
5
object HelloWorld {
def main(args: Array[String]): Unit = {
println("Hello World!")
}
}

简要说明:

  • 类名和文件名保持一致
  • args为参数,后面Array为类型,[]中的String为泛型
  • :后面为返回值 Unit表示返回值为空
  • = {} 存放函数体
  • ;句尾分号可有可无

可以使用scalacscala进行运行,也可以在IDEA中直接运行。

与Java混合

前面我们说到,Scala代码中可以调用Java类库,所以我们也可以在Scala代码中直接插入Java代码,如下:

1
2
3
4
5
6
object HelloWorld {
def main(args: Array[String]): Unit = {
println("Hello Scala!")
System.out.println("Hello Java!")
}
}

可以正确输出结果。

原理分析

观察scalac命令得到的结果,发现得到了两个字节码文件,分别是HelloWorld.classHelloWorld$.class

使用jad反编译命令(需要额外下载.exe,为了方便使用可以直接放入$JAVA_HOME$/bin目录下)

通过反编译软件可以将这两个字节码文件反编译成Java的两个类,结果如下:

HelloWorld.class

1
2
3
4
5
6
7
8
public final class HelloWorld
{

public static void main(String args[])
{
HelloWorld$.MODULE$.main(args);
}
}

HelloWorld$.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import scala.Predef$;

public final class HelloWorld$
{

public void main(String args[])
{
Predef$.MODULE$.println("Hello World!");
}

private HelloWorld$()
{
MODULE$ = this;
}

public static HelloWorld$ MODULE$;

static
{
new HelloWorld$();
}
}

观察反编译后的代码,可以发现,我们利用scala运行的HelloWorld类实际上是一个入口类。在其中,我们调用了HelloWorld$这个类中的一个对象,来执行对应的方法。而这个对象采用了单例模式构造,整个系统中只有这一个对象。

scala源文件中的HelloWorld对象编译后成为一个类,对象本身编译后是生成的另一个类HelloWorld$的单例对象HelloWorld$.MODULE$,称之为单例对象。在HelloWrold$中有一个main的实例方法,HelloWrold类的静态方法通过单例对象来调用HelloWorld$中的main实例方法,完成方法的调用。

这种逻辑实际上是Scala为了做到完全的面向对象而采用的。在Java中存在static关键字,被static关键字修饰的属性通过类名直接调用。但是这并不符合面向对象中一切皆对象的逻辑,如果是一切皆对象,应该所有的情况都是通过对象来调用,但是这里还是存在通过类来调用。因此Scala中改进了这种方式,任何调用都是通过对象来完成的。但是这样的话就需要以某种方式来实现原来通过static实现的功能,于是引入了伴生对象。伴生对象通过单例模式实现,在整个系统中只有一个,即Scala中每个类只有一个伴生对象。static完成的静态功能通过单例对象的实例方法和属性来完成,做到了更加纯粹的面向对象。

我们可以通过一个等价的Java和Scala实现来更加深入的了解这一机制:

实现如下Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Student {
private String name;
private Integer age;
private static String school = "xxxSchool";

public Student(String name, Integer age) {
this.name = name;
this.age = age;
}

public void printInfo() {
System.out.println(this.name + " " + this.age + " " + Student.school);
}

public static void main(String[] args) {
Student syh = new Student("syh", 21);
syh.printInfo();
}
}

其中static实现的静态属性需要使用类名来调用。同样的功能通过Scala代码来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student(name:String, age:Int) {
def printInfo() : Unit = {
println(name + " " + age + " " + Student.school)
}
}
// 如果这样指定,Student部分会标红,表示无法找到
// 前面提到Scala中一切都需要使用对象来调用,这里的Student表示的是类Student的伴生对象,因此我们需要引入伴生对象
// 伴生对象的引入,需要名称一致,并且在同一个文件中
// 并且由于main函数也需要由对象来调用,因此main函数要写在object中
// 类中也可以写main函数,但是无法调用
object Student {
val school: String = "xxxSchool"

def main(args: Array[String]): Unit = {
val syh = new Student("syh", 21)
syh.printInfo()
}
}

将生成的两个字节码文件Student.classStudent$.class进行反编译得到代码,如下:

Student.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import scala.Predef$;

public class Student
{

public static void main(String args[])
{
Student$.MODULE$.main(args);
}

public static String school()
{
return Student$.MODULE$.school();
}

public void printInfo()
{
Predef$.MODULE$.println((new StringBuilder(2)).append(name).append(" ").append(age).append(" ").append(Student$.MODULE$.school()).toString());
}

public Student(String name, int age)
{
this.name = name;
this.age = age;
super();
}

private final String name;
private final int age;
}

Student$.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class Student$
{

public String school()
{
return school;
}

public void main(String args[])
{
Student syh = new Student("syh", 21);
syh.printInfo();
}

private Student$()
{
MODULE$ = this;
}

public static Student$ MODULE$;
private final String school = "xxxSchool";

static
{
new Student$();
}
}

可以看到,这里的Student类依然是一个入口类,其中有name和age的定义,用于Student$中单例对象的生成。利用单例对象的属性school来达到static的功能。

与Java HelloWorld对比

由于Java和Scala的源代码都是编译成字节码文件然后在JVM上运行,那么它们的命令之间是否可以相互调用呢?这里首先构建如下的测试代码:

HelloJava.java

1
2
3
4
5
6
public class HelloJava {
public static void main(String[] args) {
System.out.println("Hello Java!");
}
}

HelloScala.scala

1
2
3
4
5
object HelloScala {
def main(args: Array[String]): Unit = {
println("Hello Scala!")
}
}

之后分别通过javacscalac来生成对应字节码文件,可以分别得到HelloJava.classHelloScala.classHelloScala$.class

  • 通过scala调用javac生成的字节码文件

可以直接调用:

1
2
(base) PS C:\Users\Lenovo\Desktop\ScalaTest> scala HelloJava
Hello Java!
  • 通过java调用scalac生成的字节码文件

不可以直接调用:

1
2
3
4
5
6
7
8
9
10
(base) PS C:\Users\Lenovo\Desktop\ScalaTest> java HelloScala
Exception in thread "main" java.lang.NoClassDefFoundError: scala/Predef$
at HelloScala$.main(HelloScala.scala:3)
at HelloScala.main(HelloScala.scala)
Caused by: java.lang.ClassNotFoundException: scala.Predef$
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 2 more

发现报错Class无法找到,原因是没有引入Scala的库,添加classpath既可以只能执行:

(下面的命令如果使用PowerShell无法执行?)

1
2
C:\Users\Lenovo\Desktop\ScalaTest>java -cp %SCALA_HOME%/lib/scala-library.jar; HelloScala
Hello Scala!

Scala学习笔记-入门(1)-Scala简介
http://example.com/2022/04/29/Scala学习笔记-入门-1-Scala简介/
作者
EverNorif
发布于
2022年4月29日
许可协议