3

I know from this question that Scala generates for a trait such as

trait A {
  def a = { ... }
}

a structure that would look similar to the following Java code

public interface A {
  public void a();
}
public class A$class {
  public static void a(A self) { ... }
}

However, in Scala it is possible for a trait to extend a class:

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}

How is this translated in a Java equivalent, where interfaces cannot inherit from classes? Is an additional interface generated for B? Does this Scala feature have any impact on Java interoperability?

Danilo Pianini
  • 966
  • 8
  • 19

1 Answers1

6

Well, you can just compile

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { ??? }
}

and check the bytecode:

> javap -c -l -p B
Compiled from "B.scala"
public class B {
  public scala.runtime.Nothing$ b();
    Code:
       0: getstatic     #16                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: invokevirtual #19                 // Method scala/Predef$.$qmark$qmark$qmark:()Lscala/runtime/Nothing$;
       6: areturn
    LineNumberTable:
      line 2: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   LB;

  public B();
    Code:
       0: aload_0
       1: invokespecial #25                 // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 4: 0
      line 1: 4
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LB;
}
> javap -c -l -p A
Compiled from "B.scala"
public interface A {
  public static scala.runtime.Nothing$ a$(A);
    Code:
       0: aload_0
       1: invokespecial #15                 // InterfaceMethod a:()Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0 $this   LA;

  public scala.runtime.Nothing$ a();
    Code:
       0: getstatic     #22                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
       3: invokevirtual #25                 // Method scala/Predef$.$qmark$qmark$qmark:()Lscala/runtime/Nothing$;
       6: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       7     0  this   LA;

  public static void $init$(A);
    Code:
       0: return
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0 $this   LA;
}

As you see scalac make A just an interface with default methods (they are available since Java 8). You might wonder, what would happen if you wanted to call some B method in A since we don't see that A extends B in bytecode:

trait A extends B {
  def a = { b; ??? }
}

now A changed to:

> javap -c -l -p A
Compiled from "B.scala"
public interface A {
  public static scala.runtime.Nothing$ a$(A);
    Code:
       0: aload_0
       1: invokespecial #15                 // InterfaceMethod a:()Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0 $this   LA;

  public scala.runtime.Nothing$ a();
    Code:
       0: aload_0
       1: checkcast     #18                 // class B
       4: invokevirtual #21                 // Method B.b:()Lscala/runtime/Nothing$;
       7: athrow
    LineNumberTable:
      line 5: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       8     0  this   LA;

  public static void $init$(A);
    Code:
       0: return
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0 $this   LA;
}

As we can see the code uses checkcasts to cast this to B and then calls 'B's method - this means that scalac will have to make sure that then you instantiate A it will be something that also extends B! So let's check what will happen when we do this:

class B {
  def b = { ??? }
}
trait A extends B {
  def a = { b; ??? }
}
class C extends A

and the C is

> javap -c -l -p C
Compiled from "B.scala"
public class C extends B implements A {
  public scala.runtime.Nothing$ a();
    Code:
       0: aload_0
       1: invokestatic  #16                 // InterfaceMethod A.a$:(LA;)Lscala/runtime/Nothing$;
       4: areturn
    LineNumberTable:
      line 7: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LC;

  public C();
    Code:
       0: aload_0
       1: invokespecial #22                 // Method B."<init>":()V
       4: aload_0
       5: invokestatic  #26                 // InterfaceMethod A.$init$:(LA;)V
       8: return
    LineNumberTable:
      line 7: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       9     0  this   LC;
}

As you can see this is very context-dependent how things end up with bytecode - the compiler will juggle things around to make it fit things in the end, but only if it will know how you use it.

Therefore if you want interoperability with Java, stick to simple traits and classes as your common interface, and let Scala instantiate more complex implementations. Doing this yourself from Java might bite you in many unexpected ways.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • 1
    Very nice, I was going through the same process but I got stuck when I was trying to understand how `(new A {}).isInstanceOf[B]` could be `true` despite that information not being in the bytecode. Is that a Scala thing only? `classOf[A] isAssignableFrom classOf[B]` returns `false`. – stefanobaghino Aug 27 '20 at 10:46
  • 3
    Scala compiler is aware of many things that aren't available to JVM, which allows it to make the final result work, while "intermediate" steps seem like "wrong". Java compiler doesn't have access to these metadata as it only understand its own metadata (and JVM data in general) so you can have situations like this: A is NOT a subclass of B, but `scalac` knows that when it will instantiate `A`, then it always will make that instance subtype of both A and B. The JVM bytecode cannot prove that `A extends B`, but scalac knows that instanceOf[A] implies instanceOf[B] because it will make it so. – Mateusz Kubuszok Aug 27 '20 at 10:51
  • 2
    As a result in runtime you never run into instance of A that is not an instance of B (unless you instantiate A manually from Java!), even though bytecode on its own offer no such guarantees. – Mateusz Kubuszok Aug 27 '20 at 10:52