具有默认空值的 Scala 按名称参数抛出 NullPointerException [英] Scala by-name parameter with default null throws NullPointerException

查看:45
本文介绍了具有默认空值的 Scala 按名称参数抛出 NullPointerException的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下代码段抛出 NullPointerException.这是 Scala 的预期和正常行为吗?

The following snippet throws NullPointerException. Is it expected and normal behavior of Scala?

object ATest extends App {
    def getX[T <: X](constr: ⇒ T = null.asInstanceOf[T]): Unit = {
        constr
    }
    getX()
}
class X

从片段生成(反编译)Java 代码:

Generated (decompied) Java code from snippet:

public final class ATest {
public static void main(String[] arrstring) {
    ATest$.MODULE$.main(arrstring);
}
public static void delayedInit(Function0<BoxedUnit> function0) {
    ATest$.MODULE$.delayedInit(function0);
}
public static String[] args() {
    return ATest$.MODULE$.args();
}
public static void scala$App$_setter_$executionStart_$eq(long l) {
    ATest$.MODULE$.scala$App$_setter_$executionStart_$eq(l);
}
public static long executionStart() {
    return ATest$.MODULE$.executionStart();
}
public static void delayedEndpoint$test$ATest$1() {
    ATest$.MODULE$.delayedEndpoint$test$ATest$1();
}
public static <T extends X> T getX$default$1() {
    return ATest$.MODULE$.getX$default$1();
}
public static <T extends X> void getX(Function0<T> function0) {
    ATest$.MODULE$.getX(function0);
}
}


public final class ATest$ implements App {
public static final ATest$ MODULE$;
private final long executionStart;
private String[] scala$App$$_args;
private final ListBuffer<Function0<BoxedUnit>> scala$App$$initCode;

public static {
    new test.ATest$();
}
public long executionStart() {
    return this.executionStart;
}
public String[] scala$App$$_args() {
    return this.scala$App$$_args;
}
public void scala$App$$_args_$eq(String[] x$1) {
    this.scala$App$$_args = x$1;
}
public ListBuffer<Function0<BoxedUnit>> scala$App$$initCode() {
    return this.scala$App$$initCode;
}
public void scala$App$_setter_$executionStart_$eq(long x$1) {
    this.executionStart = x$1;
}
public void scala$App$_setter_$scala$App$$initCode_$eq(ListBuffer x$1) {
    this.scala$App$$initCode = x$1;
}
public String[] args() {
    return App.class.args((App)this);
}
public void delayedInit(Function0<BoxedUnit> body) {
    App.class.delayedInit((App)this, body);
}
public void main(String[] args) {
    App.class.main((App)this, (String[])args);
}
public <T extends X> void getX(Function0<T> constr) {
    constr.apply();
}
public <T extends X> T getX$default$1() {
    return null;
}
public final void delayedEndpoint$test$ATest$1() {
    this.getX((Function0<T>)new scala.Serializable(){
        public static final long serialVersionUID = 0;

        public final Nothing. apply() {
            return (Nothing.)ATest$.MODULE$.getX$default$1();
        }
    });
}
private ATest$() {
    MODULE$ = this;
    App.class.$init$((App)this);
    this.delayedInit((Function0<BoxedUnit>)new ATest.delayedInit$body(this));
}
}

public final class ATest$.anonfun extends AbstractFunction0<Nothing.>implements Serializable {
public final Nothing. apply() {
        return (Nothing.)ATest$.MODULE$.getX$default$1();
        }
}

<小时>

最后是动作部分:


And finally action part:

public <T extends X> void getX(Function0<T> constr) {
    constr.apply();
}
public <T extends X> T getX$default$1() {
    return null;
}
public final void delayedEndpoint$test$ATest$1() {
    this.getX((Function0<T>)new scala.Serializable(){
        public final Nothing. apply() {
            return (Nothing.)ATest$.MODULE$.getX$default$1();
        }
    });
}

即:调用 getX 传递新的匿名 Function0,它 apply() 只调用为 null 的 getX$default$1().所以我看不到任何可以抛出 NPE 的点.

That is: call to getX passes new anon Function0 which apply() just calls getX$default$1() that is null. So i can not see any point where NPE can be thrown.

发现未解决的问题:https://issues.scala-lang.组织/浏览/SI-8097

Expression null.asInstanceOf[T] 为类型 T 生成默认值.如果 Scala 将结果类型 T 推断为 Nothing 我们来运行时表达式

Expression null.asInstanceOf[T] generates default value for type T. In case when Scala infers resulting type T as Nothing we come to runtime expression

null.asInstanceOf[Nothing]

这显然抛出 Exeption 作为 Nothing is Exception 的默认值.

that obviously throws Exeption as a default for Nothing is Exception.

但是为什么这个片段只在最后一行抛出 NPE?

But than why this snippet throws NPE only at last line?

object ATest extends App {
    def getX[T](x: T = null.asInstanceOf[T]): T = x
    getX[Nothing]() // Ok
    val x = getX() // Ok 
    val y = null
    println("x= "+x) // prints 'x= null'
    println(s"y= $y") // prints 'y= null'
    println(s"x= $x") // throws NPE !?
    println("x==null ? "+(x==null)) // prints 'x= null'
}

为什么这个代码段会抛出 NPE(它与之前的隐式参数不同)?

And why this snippet throws NPE (it is only different from previous in implicit param)?

object ATest extends App {
    def getX[T](x: T = null.asInstanceOf[T])(implicit s: String = null): T = x
    getX() // throws NPE !?
}

所以情况还是很模糊.问题是开放的.

So the situation is still vague. And question is open.

推荐答案

所以我必须稍微修改一下我的答案.

So I have to revise my answer a bit.

然而,从字节码中可以清楚地看到触发 NPE 的原因,而不是反向编译的 Java 代码.字节码比 Java 代码有更多的特性,重要的是,你可以有两种方法,只是返回类型不同,做不同的事情.

What triggers the NPE is however clear from the byte code, but not the reverse compiled Java code. Byte code has more features than Java code, the important one being, that you can have two methods that differ only in the return type and do different things.

所以让我们先看看堆栈跟踪:

So lets first look at the stack trace:

at ATest$$anonfun$1.apply(Test.scala:7)
at ATest$.getX(Test.scala:5)
at ATest$.delayedEndpoint$ATest$1(Test.scala:7)
at ATest$delayedInit$body.apply(Test.scala:3)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:76)
at scala.App$$anonfun$main$1.apply(App.scala:76)
at scala.collection.immutable.List.foreach(List.scala:383)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:35)
at scala.App$class.main(App.scala:76)
at ATest$.main(Test.scala:3)
at ATest.main(Test.scala)

所以出错的方法是 ATest$$anonfun$1.apply

So the method where it all goes wrong is ATest$$anonfun$1.apply

让我们看看:

public final scala.runtime.Nothing$ apply();
Code:
   0: getstatic     #19                 // Field ATest$.MODULE$:LATest$;
   3: invokevirtual #23                 // Method ATest$.getX$default$1:()LX;
   6: checkcast     #25                 // class scala/runtime/Nothing$
   9: areturn

public final java.lang.Object apply();
Code:
   0: aload_0
   1: invokevirtual #30                 // Method apply:()Lscala/runtime/Nothing$;
   4: athrow

我们注意到的第一件事是有两个方法叫做apply,所以会调用哪一个(athrow 是一个提示……)好吧,让我们看看调用它的方法:

The first thing we notice is there are two Methods called apply, so which one is called (the athrow is a hint...) Well, lets look at the method calling it:

public <T extends X> void getX(scala.Function0<T>);
Code:
   0: aload_1
   1: invokeinterface #62,  1  // InterfaceMethod scala/Function0.apply:()Ljava/lang/Object;
   6: pop
   7: return

因此,我们正在调用返回对象并具有 athrow 指令的那个.那么为什么这会给我们一个 NullPointer 异常?

So, we are calling the one that returns an Object and has the athrow instruction. So why does that give us a NullPointer exception?

好吧,该方法执行以下操作:将 this 放在堆栈上,然后调用另一个 apply 方法(返回 Nothing$),该方法实际上返回 null,因为它返回我们的默认参数.现在我们在堆栈上有一个空值并执行 athrow.如果 athrow 在堆栈上发现空值,则会抛出 NPE.

Well, the method does the following: it places this on the stack, then it invokes the other apply method (returning a Nothing$), this method actually returns null as it returns our default argument. And now we have a null on the stack and execute athrow. And athrow throws a NPE instead if it finds a null on the stack.

这就是这里发生的事情.

So this is what happens here.

好吧,让我们看看在类型检查之后 scalac 是怎么做的:

Well, lets look at what scalac makes of it after typechecking:

object ATest extends AnyRef with App {
    def <init>(): ATest.type = {
      ATest.super.<init>();
      ()
    };
    def getX[T <: X](constr: => T = null.asInstanceOf[T]): Unit = {
      constr;
      ()
    };
    <synthetic> def getX$default$1[T <: X]: T = null.asInstanceOf[T];
    ATest.this.getX[Nothing](ATest.this.getX$default$1[Nothing])
  }

在没有 asInstanceOf 的情况下它会做什么:

And what it does in the case without the asInstanceOf:

object ATest extends AnyRef with App {
    def <init>(): ATest.type = {
      ATest.super.<init>();
      ()
    };
    def getX[T <: X](constr: => T = null): Unit = {
      constr;
      ()
    };
    <synthetic> def getX$default$1[T <: X]: Null = null;
    ATest.this.getX[Null](ATest.this.getX$default$1[Nothing])
  }

好吧,不知何故,默认参数为 Null 的信息在第一种情况下丢失了.

Well, somehow the information, that the default parameter is Null is lost in the first case.

在第二种情况下,我们得到了关键方法的字节码:

In the second case we get this byte code for the critical method:

public final java.lang.Object apply();
Code:
   0: aload_0
   1: invokevirtual #27                 // Method apply:()Lscala/runtime/Null$;
   4: pop
   5: aconst_null
   6: areturn

所以在这里,编译器知道参数为 null,并使用类 Null$ 生成将 null 装箱的代码.

So here, the compiler knows, that the argument is null, and generates code to box null using the class Null$.

嗯,当然不是空指针异常.但是为什么编译器首先生成那个 athrow 呢?可能是因为 asInstanceOf[T] 变成了 asInstanceOf[Nothing],如果在 null 上调用它应该抛出异常.

Well, not a null pointer exception, for sure. But why does the compiler generate that athrow in the first place? Probably because of the asInstanceOf[T] which becomes an asInstanceOf[Nothing] which should throw an exception if invoked on a null.

让我们快速尝试一下,如果我们在 repl 中这样做会发生什么:

Let's quickly try, what happens if we do that in repl:

"".asInstanceOf[Nothing]
java.lang.ClassCastException: java.lang.String cannot be cast to scala.runtime.Nothing$

到目前为止一切顺利,还有这个:

So far so good, and this:

null.asInstanceOf[Nothing]
java.lang.NullPointerException

好吧,也许我应该从这个开始……看起来,asInstanceOf 的代码生成有一些错误并抛出错误的异常.

Well, maybe I should have started with this... it seems, that code generation for asInstanceOf has some bug and throws the wrong exception.

为什么下限 :> Null 解决了这个问题,也很清楚:推断的类型不再是 Nothing but Null 并且 instanceOf 很好.

Why a lower bound :> Null fixes the problem, is also clear: the inferred type is no longer Nothing but Null and the instanceOf is fine.

所以更有趣的问题是为什么类型检查器在您现在删除的复杂示例上失败.

So the more interesting problem is why the type checker fails on your complex example that you have removed now.

class X
object ATest extends App {
  def getX[T<:X](clas: Class[T], constr: ⇒ T = null): T ={
    val x = constr
    if (x == null) clas.newInstance() else x
  }
  val clas: Class[_ <: X] = classOf[X]
  getX(clas) // Ooops: type mismatch..
}

好吧,类型检查器怎么说:

Well, what does the type checker say:

def getX[T <: X](clas: Class[T], constr: => T = null): T = {
  val x: T = constr;
    if (x.==(null))
      clas.newInstance()
    else
      x
  };
  <synthetic> def getX$default$2[T <: X]: Null = null;
  private[this] val clas: Class[_ <: X] = classOf[X];
  <stable> <accessor> def clas: Class[_ <: X] = ATest.this.clas;
  ATest.this.getX[T](<clas: error>, ATest.this.getX$default$2)
}

不知何故,他无法推断出 T 的类型,但他应该推断出 Null,因为只有引用类型的类.有趣的是,编译器并不知道这一点.好像直接使用Java中Class的定义,那里的类型参数没有下界(因为Java没有类型Null),所以下界是Nothing.这也告诉我们如何修复它:

Somehow he cant infer a type for T, but he should infer Null, because there are only classes for reference types. Interestingly, the compiler does not know that. It seems to directly use the definition from Class from Java and the type parameter there has no lower bound (because Java has no type Null), so the lower bound is Nothing. This also tells us, how to fix it:

val clas: Class[_ >: Null <: X] = classOf[X]
getX(clas)

这终于奏效了.因此,您可以准确地做您最初想做的事情.您只需要告诉编译器,您对不能为 null 的类型的类不感兴趣.

This finally works. So you can do exactly, what you wanted to do in the first place. You just have to tell the compiler, that you are not interested in classes for types that cannot be nulled.

我想我仍然更喜欢带有 Option 的版本:

I think I still prefer the version with Option though:

def getX[T <: X](clas: Class[T], constr: ⇒ Option[T] = None): T = {
  val x = constr
   x match {
    case None => clas.newInstance()
    case Some(x) => x
  }
}
val clas: Class[_ <: X] = classOf[X]
getX(clas)

现在也很清楚了,为什么会这样:None 是一个 Option[Nothing],所以这段代码可以很好地处理 Class[Nothing].

Its now also clear, why this works: None is a Option[Nothing], so this code can handle a Class[Nothing] just fine.

这篇关于具有默认空值的 Scala 按名称参数抛出 NullPointerException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆