Skip to content

Enum (extends java) => null in java, when scala looks at it before java #12637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
benjhm opened this issue May 28, 2021 · 6 comments
Open

Enum (extends java) => null in java, when scala looks at it before java #12637

benjhm opened this issue May 28, 2021 · 6 comments

Comments

@benjhm
Copy link

benjhm commented May 28, 2021

Compiler version

Scala 3.0.0, OpenJDK 15.0.2, sbt 1.5.2,

Minimized code

TestenumS.scala

object TestenumS :
    def go() = println("Scala: Testme Hello= " + Testme.Hello)

enum Testme extends java.lang.Enum[Testme] :
    case Hello

TestenumJ.java

public class TestenumJ {
    public static void main(String[] args) {
    TestenumS.go();    // line 3 calls Scala to look at Testme
    System.out.println("Java: Testme Hello= " + Testme.Hello); //=> null, unless line 3 commented, then => Hello
    }
}

Output

[info] running TestenumJ 
Scala: Testme Hello= Hello
Java: Testme Hello= null

Expectation

Java: Testme Hello= Hello (not null !!)

It works as expected (Hello) only when line 3 is commented, i.e. when java sees the enum before scala.
As it compiles ok, so the null propagates to cause runtime errors elsewhere, it can takes a long time to find the cause
(especially when it looks ok called from scala)

@benjhm
Copy link
Author

benjhm commented May 28, 2021

Here's another variant of the same problem:
enum TimeOfDay works in java, although Greeting doesn't ( => null )
Similar to simpler version above, as TimeOfDay initialises Greeting (in scala code, although viewed in java)

TestenumS.scala

enum Greeting extends java.lang.Enum[Greeting] :
    case Hello
    case Goodbye

import Greeting.*
enum TimeOfDay (val mygreeting : Greeting) extends java.lang.Enum[TimeOfDay] :    
    case Morning extends TimeOfDay (Hello)
    case Afternoon extends TimeOfDay (Goodbye)

TestenumJ.java

public class TestenumJ {
    public static void main(String[] args) {
    System.out.println("Java: TimeOfDay Morning = " + TimeOfDay.Morning); 
    System.out.println("Java: TimeOfDay Morning mygreeting = " + TimeOfDay.Morning.mygreeting()); 
    System.out.println("Java: Greeting Hello = " + Greeting.Hello); 
    }
}

Output:

[info] running TestenumJ 
Java: TimeOfDay Morning = Morning
Java: TimeOfDay Morning mygreeting = Hello
Java: Greeting Hello= null 

@ecartner
Copy link

Any word on this? Any chance we can get this mentioned in the Enum docs?

@prolativ
Copy link
Contributor

The problem also occurs if Testme.Hello isn't referenced in scala code but instead in java code one references first Testme$.Hello and then Testme.Hello

@andiogenes
Copy link

andiogenes commented May 6, 2025

It seems that initializing classes in a specific order causes null to be stored in an enum field.


Here is the bytecode of Testme and Testme$ class initializers produced by Scala 3.0.0:

Testme.<clinit>

  private static {};
    descriptor: ()V
    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #43                 // Field Testme$.Hello:LTestme;
         3: putstatic     #44                 // Field Hello:LTestme;
         6: return

Testme$.<clinit>

  public static {};
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #2                  // class Testme$
         3: dup
         4: invokespecial #27                 // Method "<init>":()V
         7: putstatic     #29                 // Field MODULE$:LTestme$;
        10: getstatic     #29                 // Field MODULE$:LTestme$;
        13: iconst_0
        14: ldc           #30                 // String Hello
        16: invokespecial #34                 // Method $new:(ILjava/lang/String;)LTestme;
        19: putstatic     #36                 // Field Hello:LTestme;
        22: iconst_1
        23: anewarray     #38                 // class Testme
        26: dup
        27: iconst_0
        28: getstatic     #29                 // Field MODULE$:LTestme$;
        31: pop
        32: getstatic     #36                 // Field Hello:LTestme;
        35: aastore
        36: checkcast     #39                 // class "[LTestme;"
        39: putstatic     #41                 // Field $values:[LTestme;
        42: return

If Testme$ is initialized first (e.g., while being accessed in TestenumS$.go), Testme will be subsequently initialized during the execution of Testme$.$new:

  private Testme $new(int, java.lang.String);
    descriptor: (ILjava/lang/String;)LTestme;
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=4, locals=3, args_size=3
         0: new           #11                 // class Testme$$anon$1
         3: dup
         4: iload_1
         5: aload_2
         6: invokespecial #85                 // Method Testme$$anon$1."<init>":(ILjava/lang/String;)V
         9: areturn

The execution of 0: new #11 // class Testme$$anon$1 here will lead to the initialization of Testme$$anon$1 (according to JVMS §5.5, "A class or interface C may be initialized only as a result of ... [execution of the instruction] new ... that references C"), which, by itself, inherits Testme:

// Shortened output of `javap Testme\$\$anon\$1.class`
final class Testme$$anon$1 extends Testme implements scala.runtime.EnumValue,scala.deriving.Mirror$Singleton {
  ...
}

Therefore, Testme will be initialized during the initialization of Testme$$anon$1 (read JVMS §5.5, starting from "The procedure for initializing C is then as follows: ...", "7. Next, if C is a class ...").

During the execution of Testme.<clinit>, the static field Testme$.Hello—containing null at the moment—will be read, and this exact value will be assigned to the field Testme.Hello.


On the other hand, if Testme is initialized first, execution of getstatic will trigger the initialization of Testme$, including the proper initialization of Testme$.Hello field, and expected output will be observed.

@andiogenes
Copy link

It also looks like the same problem occurred in #16391 .

@nau
Copy link

nau commented Jun 9, 2025

Confirm, this still exists in Scala 3.3.6.
OpenJDK 64-Bit Server VM Zulu21.36+17-CA (build 21.0.4+7-LTS, mixed mode, sharing).
Any workaround for this, guys?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants