在Scala中创建Java枚举

18

我的工作场所正在尝试将某些任务从Java转移到Scala,对于我们正在做的事情来说,这种方法效果很好。但是,一些现有的记录方法期望一个 java.lang.Enum。该记录方法在(Java)基类中定义,并且子类可以定义自己的枚举,该记录器将跟踪多个线程/机器中所有实例的枚举。

在Java中,它的工作方式如下:

public class JavaSubClass extends JavaBaseClass {
    enum Counters {
        BAD_THING,
        GOOD_THING
    }

    public void someDistributedTask() {
        // some work here
        if(terribleThing) {
            loggingMethod(Counters.BAD_THING)
        } else {
            loggingMethod(Counters.GOOD_THING)
            // more work here
        }
    }
}

然后,当任务完成时,我们可以看到

BAD_THING: 230
GOOD_THING: 10345

有没有办法在Scala中复制这个操作,可以通过创建Java Enum 或将 Enumeration 转换为 Enum 来实现吗?我尝试直接扩展 Enum,但是似乎被封闭了,因为控制台中出现了错误:

error: constructor Enum in class Enum cannot be accessed in object $iw
Access to protected constructor Enum not permitted because
enclosing object $iw is not a subclass of 
class Enum in package lang where target is defined

即使你转向Scala,你仍然可以编写Java枚举并从Scala代码中使用它们。在我看来,Scala并不是很容易创建枚举;一个密封特质和一组对象更简单,我认为也更受欢迎。 - vptheron
https://gist.github.com/viktorklang/1057513 - oluies
在对Scala中“枚举”所有选项进行了广泛的研究后,我在另一个StackOverflow线程上发布了一个更完整的概述。它包括了一个解决“sealed trait + case object”模式的解决方案,我已经解决了JVM类/对象初始化顺序问题:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium
4个回答

26

Java枚举

对于一个枚举类,使用Counter而不是Counters作为名称更加合适 - 每个枚举值都表示一个单数计数器。

当javac编译一个enum类时,它:

  1. 编译成一个普通的java类(例如Counter),包含所有构造函数、方法和其他成员(如果有)
  2. 每个enum值(GOOD_THINGBAD_THING)都成为(1)的公共静态字段,类等于(1)中的类(Counter):

    // Java Code:
    class Counter {
        public static Counter GOOD_THING;
        public static Counter BAD_THING;
    
        // constructors, methods, fields as defined in the enum ...
    
    }
    
  3. 类中的初始化逻辑会自动将每个enum值构造为单例对象

Scala选项

A. 从Scala引用Java枚举

导入Counter,像在Java中一样引用GOOD_THING和BAD_THING,并(如果需要)调用枚举类方法:

// Scala Code:
import JavaSubClass.Counter;

def someDistributedTask = {
    // some work here
    if (terribleThing) {
        loggingMethod(Counter.BAD_THING)
    } else {
        loggingMethod(Counter.GOOD_THING)
        // more work here
    }
}

// Other things you can do:
val GoodThing = Counter.valueOf("GOOD_THING")

Counter.values() foreach { // do something }

counter match {
  case Counter.GOOD_THING => "Hoorah"
  case Counter.BAD_THING => "Pfft"
  case _ => throw new RuntimeException("someone added a new value?")
}

优点:能够完成Java枚举所能做的一切,同时支持模式匹配。 缺点:因为基本特质不是 sealed ,所以执行模式匹配的任何代码都不能进行类型检查以确保覆盖了详尽的情况。

B. 使用Scala枚举

将Java enum 转换为等效的Scala Enumeration

// Scala Code:
object Counter extends Enumeration {
  type Counter = Value
  val GoodThing = Value("GoodThing") 
  val BadThing = Value("BadThing")
}

使用它:

// Scala Code:
import someScalaPackage.Counter;

def someDistributedTask = {
    // some work here
    if (terribleThing) {
        loggingMethod(Counter.BadThing)
    } else {
        loggingMethod(Counter.GoodThing)
        // more work here
    }
}

// Other things you can do:
val GoodThing = Counter.withName("GoodThing")
val label = Counter.BadThing.toString

Counter.values foreach { // do something }

myCounter match {
  case Counter.GOOD_THING => "Bully!"
  case Counter.BAD_THING => "Meh"
  case _ => throw new RuntimeException("someone added a new value?")
}
优点:Scala的Enumeration方法与Java的Enum一样丰富,还支持模式匹配。 缺点:无法像Java的enum一样做所有事情 - Java的enum被定义为带任意构造函数、方法和其他成员的类(即在枚举基类型上进行完整的面向对象建模)。因为基础特征没有被sealed封闭,所以进行模式匹配的任何代码都没有经过类型检查,不能确保涵盖了所有情况。
C. 使用Scala Case Classes:
可以将enum直接转换为Case Objects(即单例对象,而不是Case Class,后者不是单例):
sealed trait Counter
object Counter {
  case object GoodThing extends Counter;
  case object BadThing extends Counter; 
}

使用它:

// Scala Code:
import someScalaPackage.Counter;

def someDistributedTask = {
    // some work here
    if (terribleThing) {
        loggingMethod(Counter.BadThing)
    } else {
        loggingMethod(Counter.GoodThing)
        // more work here
    }
}

// Other things you can do:
// NO!!   val GoodThing = Counter.withName("GoodThing")
val label = Counter.BadThing.toString

// NO!!   Counter.values foreach { // do something }

myCounter match {
  case Counter.GOOD_THING => "Bully!"
  case Counter.BAD_THING => "Meh"
  case _ => throw new RuntimeException("someone added a new value?")
}
  • 相对于枚举的优势:每个值可以有不同的祖先或混合特性(只要每个值符合类型Counter)。可以为trait Counter和每个Value进行任意复杂的面向对象建模。然后,可以使用每个不同值的所有不同case对象参数进行任意复杂的模式匹配。通过拥有基本trait sealed,任何进行模式匹配的代码都会经过类型检查,以确保涵盖了穷尽所有情况。 (不适用于您的要求)。
  • 与枚举相比的缺点:不能'免费'获得枚举方法(例如values,withName,application)。可以通过将自定义实现添加到基类Counter来'修复'它(这有点矛盾,因为它需要手动编码...)。

在对Scala中所有“枚举”选项进行了广泛的研究后(实际上与您的非常相似),我在另一个StackOverflow线程上发布了该领域的更完整概述。其中包括解决了“sealed trait + case object”模式的JVM类/对象初始化顺序问题的解决方案:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium

19

如果您需要Java枚举,则需要使用Java编写。在Scala中,有一些替代Enum用例的方法,但在Scala中没有任何机制可以复制Java的Enum


在对Scala中“枚举”所有选项进行了广泛的研究后,我在另一个StackOverflow线程上发布了一个更完整的概述。它包括了一个解决“sealed trait + case object”模式的解决方案,我已经解决了JVM类/对象初始化顺序问题:https://dev59.com/KHI-5IYBdhLWcg3wW28L#25923651 - chaotic3quilibrium

6
虽然这可能不是个好主意(请参考其他帖子获取实际的好主意),但是在Scala中扩展java.lang.Enum是可行的。如果您将类和其伴生对象放在同一编译单元中(在REPL中,每个语句都在其自己的编译单元中执行,除非您使用:paste模式),则您的代码将会起作用。
如果您使用:paste模式,并粘贴以下代码,则Scala将愉快地编译它:
sealed class AnEnum protected(name: String, ordinal: Int) extends java.lang.Enum[AnEnum](name, ordinal)
object AnEnum {
  val ENUM1 = new AnEnum("ENUM1",0)
  case object ENUM2 extends AnEnum("ENUM2", 1) // both vals and objects are possible
}

然而,Java的互操作可能无法令人满意。 Java编译器会将静态valueOf方法添加到新的枚举类中,并确保名称和序数正确,而Scala不会这样做。
即使您自己采取这些步骤,Java也不会信任您的枚举,因为该类没有ENUM修饰符。这意味着Class::isEnum会说您的类不是一个枚举,例如,这将影响静态Enum::valueOf方法。Java的switch语句也无法使用它们(尽管如果枚举值是case对象,则Scala的模式匹配应该可以工作)。

4

此主题所述,Dotty将在Scala 3.0(2020年秋季)中拥有enum

Scala还重新设计了Enums
它们可以被参数化并且可以包含自定义成员。

// Scala 2 way:
object Day extends Enumeration {
  type Day = Value
  val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}

// replaced with:
enum Day {
  case Mon, Tue, Wed, Thu, Fri, Sat, Sun
}

来自 "Martin Odersky -- Scala 3之旅"(2019年6月):

枚举可以带参数。

enum Day(val mon: Int) {}

枚举类型:

  • 可以带有参数
  • 可以定义字段和方法
  • 可以与Java互操作
enum Planet(mass: Double, radius: Double) extends java.lang.Enum[Planet] { 
  private final val G = 6.67300E-11 
  def surfaceGravity = G * mass / (radius * radius) 

  case MERCURY extends Planet(3.303e+23, 2.4397e6) 
  case VENUS extends Planet(4.869e+24, 6.0518e6) 
  case EARTH extends Planet(5.976e+24, 6.37814e6) 
  case MARS extends Planet(6.421e+23, 3.3972e6)
  ... 
} 

枚举可以有类型参数,使它们成为代数数据类型(ADT)。
enum Option[+T] { 
  case Some(x: T) 
  case None 
}

枚举编译成封闭的层次结构,包括case类和对象。
sealed abstract class Option[+T] 

object  Option { 

  case class Some[+T](x: T) extends Option[T] 
  object Some { 
    def apply[T](x: T): Option[T] = Some(x) 
  } 

  val None = new Option[Nothing] { ... } }
}

枚举可以是广义代数数据类型(GADTs)。因此,情况可以使用不同的类型参数扩展基础类型。
enum Tree[T] { 
  case True extends Tree[Boolean] 
  case False extends Tree[Boolean] 
  case IsZero(n: Tree[Int]) extends Tree[Boolean] 
  case Zero extends Tree[Int] 
  case Succ(n: Tree[Int]) extends Tree[Int] 
  case If(cond: Tree[Boolean], thenp: Tree[T], elsep: Tree[T]) extends Tree[T] 
} 

原来在 Scala 3 中的普通枚举也只是对一个代数数据类型(ADT)的简化 - 参见 https://www.slideshare.net/pjschwarz/scala-3-by-example-algebraic-data-types-for-domain-driven-design-part-1#31 - Philip Schwarz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接