Primitive

基本类型

Byte、Short、Int、Lont、Char称为整形类型,与 Double、Float 一起构成整个数字类型。这些都定义在scala包中。

而 String 是一个由 Char 构成的序列。

scalajava.lang会被自动引入 Scala 源文件。

字面值

所有这些基本类型都可以用字面值表示,通过字面值可以在代码中显式的定义常量。

整形字面值

整形类型,即 Byte、Short、Int、Lont、Char,有两种表示形式:十进制和十六进制,十六进制以0x0X开头。

无论以何种形式初始化整形字面值,Scala 都会以十进制打印该字面值。

# 以 16 进制初始化整形字面值
scala> val hex = 0x5 			# hex: Int = 5
scala> val hex2 = 0x00FF 		# hex2: Int = 255
scala> val magic = 0xcafebabe 	# magic: Int = -889275714

# 以 10 进制初始化整形字面值
scala> val dec1 = 31 			# dec1: Int = 31
scala> val dec2 = 255 			# dec2: Int = 255
scala> val dec3 = 20 			# dec3: Int = 20

如果一个整形字面值以字母Ll开头,则为类型Long

scala> val prog = 0XCAFEBABEL 	# prog: Long = 3405691582
scala> val tower = 35L 			# tower: Long = 35
scala> val of = 31l 			# of: Long = 31

如果一个整形字面值被赋值给一个类型为ShortByte的变量,则这个字面值为被当做ShortByte类型,并且该字面值必须在这些类型的有效取值范围内。

浮点数字面值

浮点数字面值由 10 进制数字创建,带有可选的小数点,和一个Ee及对应的指数。

scala> val big = 1.2345 		# big: Double = 1.2345
scala> val bigger = 1.2345e1 	# bigger: Double = 12.345
scala> val biggerStill = 123E45 # biggerStill: Double = 1.23E47

同时,可以用结尾字符Dd表示双精度浮点,fF表示单精度浮点。

字符字面值

字符字面量由使用单引号包围的任意 Unicode 字符构成。

scala> val a = 'A' 				# a: Char = A

同时可以使用以\u开头的 Unicode 码表示:

scala> val d = '\u0041' 		# d: Char = A

同时可以在任意位置使用 Unicode 字符:

scala> val B\u0041\u0044 = 1 	# BAD: Int = 1

字符串字面值

字符串字面值是一个由双引号包围的字符字面值序列。

同时,可以定义多行字符串:

println("""Welcome to Ultamix 3000. 
			Type "HELP" for help.""")
println("""|Welcome to Ultamix 3000. 
		   |Type "HELP" for help.""".stripMargin)

符号字面值(Symbol)

符号字面值写作'indentindent部分可以任意字符数字标示符。这样的一个标示符被被映射为scala.Symbol的一个实例,编译器会将它编译为一个工厂方法调用:Symbol("indent")

字符字面量并不能做太多操作,只能获取它的name属性:

scala> val s = 'aSymbol 		# s: Symbol = 'aSymbol
scala> val nm = s.name 			# nm: String = aSymbol

同时需要注意的是字符字面量是interned,编写一个字符字面量两次,表达式会引用同一个Symbol对象。

字符串插值

可以使用s直接在字符串字面量中引用变量进行插值:

val name = "reader" 
println(s"Hello, $name!")
  
s"The answer is ${6 * 7}." 		// res0: String = The answer is 42.

使用raw创建的字符串不会对字面值进行转义:

println(raw"No\\\\escape!") 	// prints: No\\\\escape!

使用f创建格式化字符串:

scala> f"${math.Pi}%.5f" 		// res1: String = 3.14159

操作符都是方法

基本类型的操作符实际是普通的方法调用:

// Scala invokes 1.+(2)
scala> val sum = 1 + 2 			// sum: Int = 3

Int同时包含一些重载方法来接收不同类型的参数:

// Scala invokes 1.+(2L)
scala> val longSum = 1 + 2L 	// longSum: Long = 3

所有的方法都可以作为操作符。中缀操作符(infix)接收两个运算数,一个在左边一个在右边。前缀操作符(prefix)接收一个操作数,位于操作符的右边。而后缀操作符(postfix)则是操作数位于操作符的左边。

7 + 2		// infix
-7			// prefix
7 toLong	// postfix

在前缀操作符中,会将表达式转换成对应的方法调用:

-2.0
(2.0).unary_-

可以作为前缀操作符的标示符只有+、-、!、~。因此只有使用这四种标示符类定义方法,比如unary_!,才能以!param这样的语法调用。

对象相等

==可以用于所有的对象相等性比较。该方法定义在Any包中,实际的意义为:

final def == (that: Any): Boolean = if (null eq this) {null eq that} else {this equals that}

x == that
if (x eq null) that eq null else x.equals(that)

Java 中的==可以用于比较基本类型和引用类型。基本类型时会进行值比较,这与 Scala 一致。

但是在比较引用类型时,Java 进行引用相等性比较,即两个变量是否指向 JVM 堆中的同一个对象,Scala 会使用equals进行引用类型的比较,该方法由用户定义。

而 Scala 中的引用相等性比较则使用eq方法。而 Java 中的equal仅作为引用比较。

创建比较方法

在定义equals方法时,有四种影响判等行为的陷阱

  1. equals方法签名错误
  2. 改变equals放但是没有改变hashCode方法
  3. 依据可变字段定义equals方法
  4. 没有为equals定义正确的等价关系

1、方法签名错误

现在有一个简单的类:

class Point(val x: Int, val y: Int) { ... }

现在是第一种equals方法的实现:

def equals(other: Point): Boolean = 
  this.x == other.x && this.y == other.y

进行测试:

val p1, p2 = new Point(1, 2)
val q = new Point(2, 3)
p1 equals p2		// Boolean = true
p1 equals q			// Boolean = false

看起来一切正常,但是把他们放入集合时:

val coll = mutable.HashSet(p1)
coll contains p2	// Boolean = false

虽然p1p2相等,但是contains方法却判断失败。

同时,当我们把p2赋值给一个Any类型的对象时:

val p2a: Any = p2
p1 equals p2a		// Boolean = false

比较结果任然错误。

下面是Any中的equals定义:

def equals(other: Any): Boolean

在一开始我们定义的equals方法中,参数类型设置为Point而不是Any,同时没有对Any中的方法进行重写,即使用override关键字标识。因此,这只是一个方法重载。当前,Scala 与 Java 中的重载已经通过参数的静态类型解决,但并非运行时类型。因此,当参数的静态类型为Point时会调用接收Poiont类型参数的方法,一旦参数的静态类型为Any,则会调用Any类型的方法。

因此在调用Setcontaions方法时,它会调用object类型的泛型equals方法而不是Point类型的方法。同时也是p1 equals p2a失败的原因。

下面是正确的equals定义:

override def equals(other: Any) = other match { 
	case that: Point => this.x == that.x && this.y == that.y 
	case _ => false 
}

同时以相同的签名重写==方法,因为他被定义为final

2、未重新定义 hashCode 方法

现在重复测试coll contains p2是仍然会出现错误,但并不是 100%。因为Set会以元素的 hash 值来进行比较,但是Point并未定义新的hashCode方法,仍然是原始的定义:只是对已分配对象的地址的转换。

在调用equals结果为true后会分别调用两个对象的hashCode方法并对结果进行比较。

同时,hashCode只能依赖于字段的值。下面是一个正确的定义:

class Point(val x: Int, val y: Int) { 
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => this.x == that.x && this.y == that.y 
		case _ => false 
	}
}

##方法是用于计算主要类型、引用类型、null的快捷方式。

3、依据可变字段定义 equals 方法

比如下面的定义,字段被定义为var而不再是val

class Point(var x: Int, var y: Int) {		// var
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => this.x == that.x && this.y == that.y 
		case _ => false 
	}
}

这是在通过Setcontains方法进行判断:

val p = new Point(1, 2)
val coll = collection.mutable.HashSet(p)
coll contains p				// true
// 修改 p 的字段值
p.x += 1
coll contains p				// false
coll.iterator contains p	// true

如果改变了p的字段值,将会判断失败,但是通过iterator方法发现p仍然是Set的元素。

这是因为,修改字段值后的p,其 hash 值也跟着改变,contaions方法通过 hash 值比较的结果必然失败。

4、错误的等价关系

scala.Anyequals方法约定中,指定equals方法必须为non-null对象实现正确的等价关系。

  • 反射性:non-null值 x,表达式x.equals(x)必须返回true
  • 对称性:任何non-null值 x 和 y,当且仅当x.equals(y)返回true时,y.equals(x)才会返回true
  • 传递性:任何non-null值 x、y、z,如果x.equals(y)y.equals(z)都返回true,则x.equals(z)也会返回true
  • 一致性:任何non-null值 x 和 y,多次调用x.equals(y)都会一致的返回truefalse
  • 对任何non-null值 x,x.equals(null)应该返回false

上面的Point类已经能够很好的工作,但是如果它有一个新的子类,并且新增了一个字段:

object Color extends Enumeration {
	val Red, Orange, Yellow, Green, Blue, Indigo, Violet = Value 
}

class ColoredPoint(x: Int, y: Int, val color: Color.Value)
	extends Point(x, y) {
	override def equals(other: Any) = other match { 
		case that: ColoredPoint => this.color == that.color && super.equals(that) 
		case _ => false 
	}
}

通常会以上面的方式实现。这个子类继承父类,并重写了equals方法,该方法类似父类的形式,比较新字段并利用父类的equals方法比较原有的字段。

注意当前这个例子中,并不需要重写hashCode方法,因为子类中的equals实现比父类中的实现更为严谨(它与更小范围内的对象相等),因此hashCode的契约依然有效。???

这个子类中的实现看起来没有问题,但是当他与父类混合时:

scala> val p = new Point(1, 2) 		
# p: Point = Point@5428bd62
scala> val cp = new ColoredPoint(1, 2, Color.Red) 
# cp: ColoredPoint = ColoredPoint@5428bd62

scala> p equals cp 		# res9: Boolean = true
scala> cp equals p 		# res10: Boolean = false

p equals cp会调用pequals方法,这个方法只会对对象的坐标进行比较,并返回了true

cp equals p会调用cpequals方法,因为p并不是一个ColoredPoint对象,因此返回false

因此,equals中定义的相等性并不是对称的。

canEqual

在继承类型的比较中,需要引入一个canEqual方法。这个想法是,一旦一个类重新定义了equals(或同时也冲定义了hashCode),它也必须明确指出,这类对象永远不能等于那些实现了不同判等方法的父类对象。

def canEqual(other: Any): Boolean

这个方法中,如果other对象是一个(重)定义了canEqual方法的类的实例,返回true,否则返回false。在equals方法中调用这个方法来确保将要比较的两个对象能够进行双向比较。

class Point(val x: Int, val y: Int) { 
	override def hashCode = (x, y).## 
	override def equals(other: Any) = other match { 
		case that: Point => 
			(that canEqual this) && (this.x == that.x) && (this.y == that.y) 
		case _ => false
	} 
	def canEqual(other: Any) = other.isInstanceOf[Point]	// 运行时类型相同
}

然后是子类的定义:

class ColoredPoint(x: Int, y: Int, val color: Color.Value) extends Point(x, y) {
	override def hashCode = (super.hashCode, color).## 		// 重写 hashCode
	override def equals(other: Any) = other match { 
		case that: ColoredPoint => 							// 重写 equals
		(that canEqual this) && super.equals(that) && this.color == that.color 
		case _ => false
	} 
	override def canEqual(other: Any) = other.isInstanceOf[ColoredPoint]
}

对象相等性的实现依赖于场景。当前场景中,两个不同的Point对象拥有相同的坐标即视作相等。但是两个对象拥有相同坐标,但是一个没有颜色,一个为红色,则视作不相等。

拓展:Java 中的字符串相等性比较