未验证 提交 cb4f1731 编写于 作者: 梦境迷离's avatar 梦境迷离 提交者: GitHub

support @equalsAndHashCode (#70)

* equalsAndHashCode
* support custom canEqual
上级 67ee0bc3
......@@ -43,6 +43,7 @@
- `@log`
- `@apply`
- `@constructor`
- `@equalsAndHashCode`
> 涉及到交互操作的注解在IDEA插件中都得到了支持。在插件市场中搜索`Scala-Macro-Tools`可下载。
......
......@@ -27,6 +27,7 @@ Learn Scala macro and abstract syntax tree.
- `@log`
- `@apply`
- `@constructor`
- `@equalsAndHashCode`
> Annotations involving interaction are supported in the idea plug-in (named `Scala-Macro-Tools` in Marketplace).
......
......@@ -213,4 +213,44 @@ def <init>(int: Int, j: Int, k: Option[String], t: Option[Long], b: Int) = {
<init>(int, j, k, t);
this.b = b
}
```
\ No newline at end of file
```
## @equalsAndHashCode
`@equalsAndHashCode`注解用于为普通类生成`equals``hashCode`方法,同时均考虑超类的影响。
- 说明
- `verbose` 指定是否开启详细编译日志。可选,默认`false`
- `excludeFields` 指定是否需要排除不需要用于`equals``hashCode`方法的字段。可选,默认空(class内部所有非私有的`var、val`字段都将被应用于生成这两个方法)。
- `equals``hashCode`方法均会被超类影响,`canEqual`使用`isInstanceOf`,有些人在实现时,使用的是`this.getClass == that.getClass`
- 采用简单hashCode算法,父类的hashCode是直接被累加的。该算法也是`case class`所使用的。
- 如果注解所在类已经定义了相同签名的`canEqual`方法,则不会生成该方法。
- 示例
```scala
@equalsAndHashCode(verbose = true)
class Person(var name: String, var age: Int)
```
宏生成的中间代码:
```scala
class Person extends scala.AnyRef {
<paramaccessor> var name: String = _;
<paramaccessor> var age: Int = _;
def <init>(name: String, age: Int) = {
super.<init>();
()
};
def canEqual(that: Any) = that.isInstanceOf[Person];
override def equals(that: Any): Boolean = that match {
case (t @ (_: Person)) => t.canEqual(this).$amp$amp(Seq(this.name.equals(t.name), this.age.equals(t.age)).forall(((f) => f))).$amp$amp(true)
case _ => false
};
override def hashCode(): Int = {
val state = Seq(name, age);
state.map(((x$2) => x$2.hashCode())).foldLeft(0)(((a, b) => 31.$times(a).$plus(b)))
}
}
```
\ No newline at end of file
......@@ -71,7 +71,7 @@ val ret = TestClass1.builder().i(1).j(0).x("x").build()
assert(ret.toString == "TestClass1(1,0,x,Some())")
```
Compiler macro code:
Macro expansion code:
```scala
object TestClass1 extends scala.AnyRef {
......@@ -136,7 +136,7 @@ def getStr(k: Int): String = {
}
```
Compiler macro code:
Macro expansion code:
```scala
// Note that it will not judge whether synchronized already exists, so if synchronized already exists, it will be used twice.
......@@ -194,6 +194,7 @@ The `@constructor` used to generate secondary constructor method for classes, on
- The internal fields are placed in the first bracket block if constructor is currying.
- The type of the internal field must be specified, otherwise the macro extension cannot get the type.
At present, only primitive types and string can be omitted. For example, `var i = 1; var j: int = 1; var k: Object = new Object()` is OK, but `var k = new object()` is not.
- Example
```scala
......@@ -209,11 +210,53 @@ class A2(int: Int, val j: Int, var k: Option[String] = None, t: Option[Long] = S
println(new A2(1, 2, None, None, 100))
```
Compiler macro code(Only constructor def):
Macro expansion code (Only constructor def):
```scala
def <init>(int: Int, j: Int, k: Option[String], t: Option[Long], b: Int) = {
<init>(int, j, k, t);
this.b = b
}
```
## @equalsAndHashCode
The `@equalsAndHashCode` annotation is used to generate `equals` and `hashCode` methods for ordinary classes, and them takes into account the influence of super classes.
- Note
- `verbose` Whether to enable detailed log.
- `excludeFields` specifies whether to exclude fields that are not required for the `equals` and `hashCode` methods. Optional,
default is `Nil` (all non private `var` and `val` fields in the class will be used to generate the two methods).
- Both `equals` and `hashCode` methods are affected by super classes, and `canEqual` uses `isInstanceOf` in `equals` method.
Some equals implementations use `that.getClass == this.getClass`
- It uses simple hashcode algorithm, and the hashcode of the parent class are accumulated directly. The algorithm is also used by `case class`.
- If the class of the annotation has already defined the `canEqual` method with the same signature, `canEqual` will not be generated.
- Example
```scala
@equalsAndHashCode(verbose = true)
class Person(var name: String, var age: Int)
```
Macro expansion code:
```scala
class Person extends scala.AnyRef {
<paramaccessor> var name: String = _;
<paramaccessor> var age: Int = _;
def <init>(name: String, age: Int) = {
super.<init>();
()
};
def canEqual(that: Any) = that.isInstanceOf[Person];
override def equals(that: Any): Boolean = that match {
case (t @ (_: Person)) => t.canEqual(this).$amp$amp(Seq(this.name.equals(t.name), this.age.equals(t.age)).forall(((f) => f))).$amp$amp(true)
case _ => false
};
override def hashCode(): Int = {
val state = Seq(name, age);
state.map(((x$2) => x$2.hashCode())).foldLeft(0)(((a, b) => 31.$times(a).$plus(b)))
}
}
```
\ No newline at end of file
package io.github.dreamylost
import io.github.dreamylost.macros.equalsAndHashCodeMacro
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
/**
* annotation to generate equals and hashcode method for classes.
*
* @author 梦境迷离
* @param verbose Whether to enable detailed log.
* @param excludeFields Whether to exclude the specified internal fields.
* @since 2021/7/18
* @version 1.0
*/
@compileTimeOnly("enable macro to expand macro annotations")
final class equalsAndHashCode(
verbose: Boolean = false,
excludeFields: Seq[String] = Nil
) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro equalsAndHashCodeMacro.impl
}
......@@ -115,11 +115,9 @@ trait MacroCommon {
if (companionOpt.isEmpty) {
resTree
} else {
val q"$mods object $obj extends ..$bases { ..$body }" = companionOpt.get
val companion = q"$mods object $obj extends ..$bases { ..$body }"
q"""
$resTree
$companion
${companionOpt.get}
"""
}
}
......@@ -133,7 +131,7 @@ trait MacroCommon {
* @return Return the result of modifyAction
*/
def handleWithImplType(c: whitebox.Context)(annottees: c.Expr[Any]*)
(modifyAction: (c.universe.ClassDef, Option[c.universe.ModuleDef]) => Any): c.Expr[Nothing] = {
(modifyAction: (c.universe.ClassDef, Option[c.universe.ModuleDef]) => Any): c.Expr[Nothing] = {
import c.universe._
annottees.map(_.tree) match {
case (classDecl: ClassDef) :: Nil => modifyAction(classDecl, None).asInstanceOf[c.Expr[Nothing]]
......@@ -168,11 +166,42 @@ trait MacroCommon {
* @param field
* @return
*/
def fieldTermName(c: whitebox.Context)(field: c.universe.Tree): c.universe.TermName = {
def getFieldTermName(c: whitebox.Context)(field: c.universe.Tree): c.universe.TermName = {
import c.universe._
field match {
case q"$mods val $tname: $tpt = $expr" => tname.asInstanceOf[TermName]
case q"$mods var $tname: $tpt = $expr" => tname.asInstanceOf[TermName]
case q"$mods val $pat = $expr" => pat.asInstanceOf[TermName] //for equalsAndHashcode, need contains all fields.
case q"$mods var $pat = $expr" => pat.asInstanceOf[TermName]
}
}
/**
* Expand the method params and get the param Name.
*
* @param c
* @param field
* @return
*/
def getMethodParamName(c: whitebox.Context)(field: c.universe.Tree): c.universe.Name = {
import c.universe._
field match {
case q"$mods val $tname: $tpt = $expr" => tpt.asInstanceOf[Ident].name.decodedName
}
}
/**
* Check whether the mods of the fields has a `private[this]`, because it cannot be used in equals method.
*
* @param c
* @param field
* @return
*/
def classParamsIsPrivate(c: whitebox.Context)(field: c.universe.Tree): Boolean = {
import c.universe._
field match {
case q"$mods val $tname: $tpt = $expr" => if (mods.asInstanceOf[Modifiers].hasFlag(Flag.PRIVATE)) false else true
case q"$mods var $tname: $tpt = $expr" => true
}
}
......@@ -183,7 +212,7 @@ trait MacroCommon {
* @param annotteeClassParams
* @return
*/
def fieldAssignExpr(c: whitebox.Context)(annotteeClassParams: Seq[c.Tree]): Seq[c.Tree] = {
def getFieldAssignExprs(c: whitebox.Context)(annotteeClassParams: Seq[c.Tree]): Seq[c.Tree] = {
import c.universe._
annotteeClassParams.map {
case q"$mods var $tname: $tpt = $expr" => q"$tname: $tpt" //Ignore expr
......@@ -202,7 +231,7 @@ trait MacroCommon {
*/
def modifiedCompanion(c: whitebox.Context)(
compDeclOpt: Option[c.universe.ModuleDef],
codeBlock: c.Tree, className: c.TypeName): c.universe.Tree = {
codeBlock: c.Tree, className: c.TypeName): c.universe.Tree = {
import c.universe._
compDeclOpt map { compDecl =>
val q"$mods object $obj extends ..$bases { ..$body }" = compDecl
......@@ -229,11 +258,25 @@ trait MacroCommon {
* @param c
* @param annotteeClassDefinitions
*/
def getClassMemberValDef(c: whitebox.Context)(annotteeClassDefinitions: Seq[c.Tree]): Seq[c.Tree] = {
def getClassMemberValDefs(c: whitebox.Context)(annotteeClassDefinitions: Seq[c.Tree]): Seq[c.Tree] = {
import c.universe._
annotteeClassDefinitions.filter(p => p match {
case _: ValDef => true
case _ => false
case _ => false
})
}
/**
* Extract the methods belonging to the class, contains Secondary Constructor.
*
* @param c
* @param annotteeClassDefinitions
*/
def getClassMemberDefDefs(c: whitebox.Context)(annotteeClassDefinitions: Seq[c.Tree]): Seq[c.Tree] = {
import c.universe._
annotteeClassDefinitions.filter(p => p match {
case _: DefDef => true
case _ => false
})
}
......@@ -248,7 +291,7 @@ trait MacroCommon {
*/
def getConstructorWithCurrying(c: whitebox.Context)(typeName: c.TypeName, fieldss: List[List[c.Tree]], isCase: Boolean): c.Tree = {
import c.universe._
val allFieldsTermName = fieldss.map(f => f.map(ff => fieldTermName(c)(ff)))
val allFieldsTermName = fieldss.map(f => f.map(ff => getFieldTermName(c)(ff)))
// not currying
val constructor = if (fieldss.isEmpty || fieldss.size == 1) {
q"${if (isCase) q"${typeName.toTermName}(..${allFieldsTermName.flatten})" else q"new $typeName(..${allFieldsTermName.flatten})"}"
......@@ -272,7 +315,7 @@ trait MacroCommon {
*/
def getApplyMethodWithCurrying(c: whitebox.Context)(typeName: c.TypeName, fieldss: List[List[c.Tree]], classTypeParams: List[c.Tree]): c.Tree = {
import c.universe._
val allFieldsTermName = fieldss.map(f => fieldAssignExpr(c)(f))
val allFieldsTermName = fieldss.map(f => getFieldAssignExprs(c)(f))
val returnTypeParams = extractClassTypeParamsTypeName(c)(classTypeParams)
// not currying
val applyMethod = if (fieldss.isEmpty || fieldss.size == 1) {
......
......@@ -36,7 +36,7 @@ object constructorMacro extends MacroCommon {
}
// Extract the internal fields of members belonging to the class, but not in primary constructor.
val classFieldDefinitions = getClassMemberValDef(c)(annotteeClassDefinitions)
val classFieldDefinitions = getClassMemberValDefs(c)(annotteeClassDefinitions)
val excludeFields = args._2
/**
......@@ -44,7 +44,7 @@ object constructorMacro extends MacroCommon {
*/
def getClassMemberVarDefOnlyAssignExpr(): Seq[c.Tree] = {
import c.universe._
getClassMemberValDef(c)(annotteeClassDefinitions).filter(_ match {
getClassMemberValDefs(c)(annotteeClassDefinitions).filter(_ match {
case q"$mods var $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true
case _ => false
}).map {
......@@ -72,7 +72,7 @@ object constructorMacro extends MacroCommon {
// Extract the field of the primary constructor.
val ctorFieldNamess = annotteeClassParams.asInstanceOf[List[List[Tree]]]
val allFieldsTermName = ctorFieldNamess.map(f => f.map(ff => fieldTermName(c)(ff)))
val allFieldsTermName = ctorFieldNamess.map(f => f.map(ff => getFieldTermName(c)(ff)))
/**
* We generate this method with currying, and we have to deal with the first layer of currying alone.
......@@ -80,7 +80,7 @@ object constructorMacro extends MacroCommon {
def getThisMethodWithCurrying(): c.Tree = {
// not currying
// Extract the field of the primary constructor.
val classParamsAssignExpr = fieldAssignExpr(c)(ctorFieldNamess.flatten)
val classParamsAssignExpr = getFieldAssignExprs(c)(ctorFieldNamess.flatten)
val applyMethod = if (ctorFieldNamess.isEmpty || ctorFieldNamess.size == 1) {
q"""
def this(..${classParamsAssignExpr ++ classFieldDefinitionsOnlyAssignExpr}) = {
......@@ -90,7 +90,7 @@ object constructorMacro extends MacroCommon {
"""
} else {
// NOTE: currying constructor overload must be placed in the first bracket block.
val allClassParamsAssignExpr = ctorFieldNamess.map(cc => fieldAssignExpr(c)(cc))
val allClassParamsAssignExpr = ctorFieldNamess.map(cc => getFieldAssignExprs(c)(cc))
q"""
def this(..${allClassParamsAssignExpr.head ++ classFieldDefinitionsOnlyAssignExpr})(...${allClassParamsAssignExpr.tail}) = {
this(..${allFieldsTermName.head})(...${allFieldsTermName.tail})
......
package io.github.dreamylost.macros
import scala.reflect.macros.whitebox
/**
*
* @author 梦境迷离
* @since 2021/7/18
* @version 1.0
*/
object equalsAndHashCodeMacro extends MacroCommon {
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val args: (Boolean, Seq[String]) = extractArgumentsTuple2(c) {
case q"new equalsAndHashCode(verbose=$verbose)" => (evalTree(c)(verbose.asInstanceOf[Tree]), Nil)
case q"new equalsAndHashCode(excludeFields=$excludeFields)" => (false, evalTree(c)(excludeFields.asInstanceOf[Tree]))
case q"new equalsAndHashCode(verbose=$verbose, excludeFields=$excludeFields)" => (evalTree(c)(verbose.asInstanceOf[Tree]), evalTree(c)(excludeFields.asInstanceOf[Tree]))
case q"new equalsAndHashCode()" => (false, Nil)
case _ => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN)
}
val annotateeClass: ClassDef = checkAndGetClassDef(c)(annottees: _*)
val isCase: Boolean = isCaseClass(c)(annotateeClass)
if (isCase) {
c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $annotateeClass")
}
val excludeFields = args._2
def modifiedDeclaration(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = {
val (className, annotteeClassParams, annotteeClassDefinitions, superClasses) = classDecl match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
c.info(c.enclosingPosition, s"modifiedDeclaration className: $tpname, paramss: $paramss", force = args._1)
(tpname, paramss, stats.asInstanceOf[Seq[Tree]], parents)
case _ => c.abort(c.enclosingPosition, s"${ErrorMessage.ONLY_CLASS} classDef: $classDecl")
}
val ctorFieldNames = annotteeClassParams.asInstanceOf[List[List[Tree]]].flatten.filter(cf => classParamsIsPrivate(c)(cf))
val allFieldsTermName = ctorFieldNames.map(f => getFieldTermName(c)(f))
c.info(c.enclosingPosition, s"modifiedDeclaration compDeclOpt: $compDeclOpt, ctorFieldNames: $ctorFieldNames, " +
s"annotteeClassParams: $superClasses", force = args._1)
/**
* Extract the internal fields of members belonging to the class.
*/
def getClassMemberAllTermName: Seq[c.TermName] = {
getClassMemberValDefs(c)(annotteeClassDefinitions).filter(_ match {
case q"$mods var $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true
case q"$mods val $tname: $tpt = $expr" if !excludeFields.contains(tname.asInstanceOf[TermName].decodedName.toString) => true
case q"$mods val $pat = $expr" if !excludeFields.contains(pat.asInstanceOf[TermName].decodedName.toString) => true
case q"$mods var $pat = $expr" if !excludeFields.contains(pat.asInstanceOf[TermName].decodedName.toString) => true
case _ => false
}).map(f => getFieldTermName(c)(f))
}
val existsCanEqual = getClassMemberDefDefs(c)(annotteeClassDefinitions) exists {
case q"$mods def $tname[..$tparams](...$paramss): $tpt = $expr" if tname.toString() == "canEqual" && paramss.nonEmpty =>
val params = paramss.asInstanceOf[List[List[Tree]]].flatten.map(pp => getMethodParamName(c)(pp))
params.exists(p => p.decodedName.toString == "Any")
case _ => false
}
// + super.hashCode
val SDKClasses = Set("java.lang.Object", "scala.AnyRef")
val canEqualsExistsInSuper = if (superClasses.nonEmpty && !superClasses.forall(sc => SDKClasses.contains(sc.toString()))) { // TODO better way
true
} else false
// equals template
def ==(termNames: Seq[TermName]): c.universe.Tree = {
val getEqualsExpr = (termName: TermName) => {
q"this.$termName.equals(t.$termName)"
}
val equalsExprs = termNames.map(getEqualsExpr)
val modifiers = if (canEqualsExistsInSuper) Modifiers(Flag.OVERRIDE, typeNames.EMPTY, List()) else Modifiers(NoFlags, typeNames.EMPTY, List())
val canEqual = if (existsCanEqual) q"" else q"$modifiers def canEqual(that: Any) = that.isInstanceOf[$className]"
q"""
$canEqual
override def equals(that: Any): Boolean =
that match {
case t: $className => t.canEqual(this) && Seq(..$equalsExprs).forall(f => f) && ${if (canEqualsExistsInSuper) q"super.equals(that)" else q"true"}
case _ => false
}
"""
}
// hashcode template
def ##(termNames: Seq[TermName]): c.universe.Tree = {
// the algorithm see https://alvinalexander.com/scala/how-to-define-equals-hashcode-methods-in-scala-object-equality/
// We use default 1.
if (!canEqualsExistsInSuper) {
q"""
override def hashCode(): Int = {
val state = Seq(..$termNames)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
"""
} else {
q"""
override def hashCode(): Int = {
val state = Seq(..$termNames)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + super.hashCode
}
"""
}
}
val allTernNames = allFieldsTermName ++ getClassMemberAllTermName
val hashcode = ##(allTernNames)
val equals = ==(allTernNames)
val equalsAndHashcode =
q"""
..$equals
$hashcode
"""
// return with object if it exists
val resTree = annotateeClass match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" =>
val originalStatus = q"{ ..$stats }"
val append =
q"""
..$originalStatus
..$equalsAndHashcode
"""
q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..${append} }"
}
c.Expr[Any](treeResultWithCompanionObject(c)(resTree, annottees: _*))
}
val resTree = handleWithImplType(c)(annottees: _*)(modifiedDeclaration)
printTree(c)(force = args._1, resTree.tree)
resTree
}
}
......@@ -55,9 +55,9 @@ object logMacro extends MacroCommon {
treeResultWithCompanionObject(c)(resTree, annottees: _*) //we should return with companion object. Even if we didn't change it.
case q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _ =>
q"$mods object $tpname extends { ..$earlydefns } with ..$parents { $self => ..${List(logTree) ::: stats.toList} }"
// Note: If a class is annotated and it has a companion, then both are passed into the macro.
// (But not vice versa - if an object is annotated and it has a companion class, only the object itself is expanded).
// see https://docs.scala-lang.org/overviews/macros/annotations.html
// Note: If a class is annotated and it has a companion, then both are passed into the macro.
// (But not vice versa - if an object is annotated and it has a companion class, only the object itself is expanded).
// see https://docs.scala-lang.org/overviews/macros/annotations.html
}
printTree(c)(force = args._1, resTree)
......
package io.github.dreamylost
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class EmployeeTests extends AnyFlatSpec with Matchers {
class Employee(name: String, age: Int, var role: String)
extends Person(name, age) {
override def canEqual(a: Any): Boolean = a.isInstanceOf[Employee]
override def equals(that: Any): Boolean =
that match {
case that: Employee =>
that.canEqual(this) &&
this.role == that.role &&
super.equals(that)
case _ => false
}
override def hashCode: Int = {
val state = Seq(role)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) + super.hashCode
}
}
class Person(var name: String, var age: Int) {
// Step 1 - proper signature for `canEqual`
// Step 2 - compare `a` to the current class
def canEqual(a: Any): Boolean = a.isInstanceOf[Person]
// Step 3 - proper signature for `equals`
// Steps 4 thru 7 - implement a `match` expression
override def equals(that: Any): Boolean =
that match {
case that: Person =>
that.canEqual(this) &&
this.name == that.name &&
this.age == that.age
case _ => false
}
// Step 8 - implement a corresponding hashCode c=method
override def hashCode: Int = {
val state = Seq(name, age)
state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
}
}
"equals1" should "ok" in {
// these first two instances should be equal
val nimoy = new Person("Leonard Nimoy", 82)
val nimoy2 = new Person("Leonard Nimoy", 82)
val shatner = new Person("William Shatner", 82)
val stewart = new Person("Patrick Stewart", 47)
// all tests pass
assert(nimoy != null)
// these should be equal
assert(nimoy == nimoy)
assert(nimoy == nimoy2)
assert(nimoy2 == nimoy)
// these should not be equal
assert(nimoy != shatner)
assert(shatner != nimoy)
assert(nimoy != "Leonard Nimoy")
assert(nimoy != stewart)
}
"equals2" should "ok" in {
// these first two instance should be equal
val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor")
val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor")
val pNimoy = new Person("Leonard Nimoy", 82)
val eShatner = new Employee("William Shatner", 82, "Actor")
// equality tests
assert(eNimoy1 == eNimoy1)
assert(eNimoy1 == eNimoy2)
assert(eNimoy2 == eNimoy1)
// non-equality tests
assert(eNimoy1 != pNimoy)
assert(pNimoy != eNimoy1)
assert(eNimoy1 != eShatner)
assert(eShatner != eNimoy1)
}
}
package io.github.dreamylost
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
/**
*
* @author 梦境迷离
* @since 2021/7/18
* @version 1.0
*/
class EqualsAndHashCodeTest extends AnyFlatSpec with Matchers {
@equalsAndHashCode(verbose = true)
@toString
class Employee(name: String, age: Int, var role: String) extends Person(name, age)
@toString
@equalsAndHashCode(verbose = true)
class Person(var name: String, var age: Int)
"equals1" should "ok" in {
// these first two instances should be equal
val nimoy = new Person("Leonard Nimoy", 82)
val nimoy2 = new Person("Leonard Nimoy", 82)
val shatner = new Person("William Shatner", 82)
val stewart = new Person("Patrick Stewart", 47)
println(nimoy)
println(nimoy.hashCode())
// all tests pass
assert(nimoy != null)
// these should be equal
assert(nimoy == nimoy)
assert(nimoy == nimoy2)
assert(nimoy2 == nimoy)
// these should not be equal
assert(nimoy != shatner)
assert(shatner != nimoy)
assert(nimoy != "Leonard Nimoy")
assert(nimoy != stewart)
}
"equals2" should "ok" in {
// these first two instance should be equal
val eNimoy1 = new Employee("Leonard Nimoy", 82, "Actor")
val eNimoy2 = new Employee("Leonard Nimoy", 82, "Actor")
val pNimoy = new Person("Leonard Nimoy", 82)
val eShatner = new Employee("William Shatner", 82, "Actor")
// equality tests
assert(eNimoy1 == eNimoy1)
assert(eNimoy1 == eNimoy2)
assert(eNimoy2 == eNimoy1)
// non-equality tests
assert(eNimoy1 != pNimoy)
assert(pNimoy != eNimoy1)
assert(eNimoy1 != eShatner)
assert(eShatner != eNimoy1)
println(eNimoy1)
println(eNimoy1.hashCode())
}
"equals3" should "ok even if exists a canEqual" in {
"""
| @equalsAndHashCode
| class Employee(name: String, age: Int, var role: String) extends Person(name, age) {
| override def canEqual(that: Any) = that.getClass == classOf[Employee];
| }
|""".stripMargin should compile
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册