是否可以在没有外部类的情况下序列化匿名类?
我在网上进行了一项小型研究并审查了该网站上的相关主题,但答案是矛盾的:有些人说这是不可能的,有些人说这是可能的,但很危险。
目标是将匿名类的对象作为RMI方法的参数传递。 由于RMI要求,此类必须是可序列化的。 这没问题,很容易使类Serializable。
但我们知道内部类的实例包含对外部类的引用(而匿名类是内部类)。 因此,当我们序列化内部类的实例时,外部类的实例被序列化以及字段。 这里是问题出现的地方:外部类不可序列化,更重要的是 – 我不想序列化它。 我想要做的只是发送匿名类的实例。
简单示例 – 这是一个RMI服务,其方法接受Runnable:
public interface RPCService { Object call(SerializableRunnable runnable); }
以下是我想要调用该方法的方法
void call() { myRpcService.call(new SerializableRunnable() { @Override public Object run { System.out.println("It worked!"); } } }
正如您所看到的,我想要做的是向另一方发送“操作” – 系统A描述应该在系统B上运行的代码。这就像用Java发送脚本一样。
如果可能的话,我可以很容易地看到一些危险的后果:例如,如果我们从Runnable访问字段或捕获外部类的最终变量 – 我们将遇到麻烦,因为调用者实例不存在。 另一方面,如果我在Runnable中使用安全代码(编译器可以检查它),那么我没有看到禁止此操作的原因。
因此,如果有人知道,如何在匿名类中正确覆盖writeObject()
和readObject()
方法或如何引用外部类transient
或解释为什么在java中不可能,它将非常有用。
UPD另一个需要考虑的重要事项:外部类不存在于将执行该方法的环境中(系统B),这就是为什么应该完全排除有关它的信息以避免NoClassDefFoundError
。
您可以尝试将Caller.call()
作为static
方法。
但是,仍然需要在反序列化序列化实例的上下文中提供匿名类。 这是不可避免的。
(很难想象匿名类可用但封闭类不可用的情况。)
所以,如果有人可以展示,我如何在我的匿名类中正确覆盖writeObject和readObject方法……
如果你将Caller.call()
静态,那么就像你想要的那样,你会这样做。 (我相信你可以找到适合自己的例子。)
实际上,(模块化匿名类可用性问题)它的工作原理。 这里, static main
方法替换了static Classer.call()
方法。 程序编译并运行,表明在静态方法中声明的匿名类可以被序列化和反序列化。
import java.io.*; public class Bar { private interface Foo extends Runnable, Serializable {} public static void main (String[] args) throws InterruptedException, IOException, ClassNotFoundException { Runnable foo = new Foo() { @Override public void run() { System.out.println("Lala"); } }; Thread t = new Thread(foo); t.start(); t.join(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(foo); oos.close(); Foo foofoo = (Foo) new ObjectInputStream( new ByteArrayInputStream(baos.toByteArray())).readObject(); t = new Thread(foofoo); t.start(); t.join(); } }
另一个需要记住的重要事项是:在执行方法的环境中不存在
Caller
类,因此我希望在序列化期间排除有关它的所有信息以避免NoClassDefFoundError
。
没有办法避免这种情况。 远程JVM中的反序列化抱怨的原因是类描述符包含对外部类的引用。 反序列化方需要解析该引用,即使您设法破坏引用,即使您从未在反序列化对象中显式或隐式使用合成变量。
问题是远程JVM的类加载器在加载内部类的类文件时需要知道外部类的类型。 它需要进行validation。 反思需要它。 垃圾收集器需要它。
没有解决方法。
(我不确定这是否也适用于static
内部类……但我怀疑它确实如此。)
尝试在没有外部类的情况下序列化匿名Runnable实例不仅指序列化问题,还指在另一个环境中执行任意代码的可能性。 很高兴看到JLS参考,描述这个问题。
没有JLS参考。 JLS中未指定序列化和类加载器。 (类初始化是……但这是一个不同的问题。)
可以通过RMI在远程系统上运行任意代码。 但是,您需要实现RMI动态类加载才能实现此目的。 这是一个参考:
请注意,将远程类的动态类加载添加到RMI会引入重大的安全问题。 而且你必须考虑像classloader泄漏这样的问题。
如果你疯狂地做这个技巧,你可以使用reflection来查找包含对外部类的引用的字段并将其设置为null
。
上面描述的示例无法在Java中工作,因为匿名内部类在类Caller中声明,并且您明确声明类Caller在RPC服务器上不可用(如果我理解正确的话)。 请注意,对于Java RPC,仅通过网络发送数据,这些类必须已在客户端和服务器上可用。 它尊重你的例子是没有意义的,因为它看起来像你想发送代码而不是数据。 通常,您可以在服务器和客户端可用的JAR中使用可序列化类,并且每个可序列化类应具有唯一的serialVersionUID。
答案是不。 你不能这样做,因为Inner类需要外部类来序列化。 当你尝试在内部类中调用外部类的实例方法时,也会遇到麻烦。 你为什么不再举办另一个顶级课程呢?
你不能完全按照自己的意愿去做,也就是序列化一个匿名的内部类,而不是让它的封闭实例可序列化并将其序列化。 这同样适用于本地课程。 这些不可避免地具有引用其封闭实例的隐藏字段,因此序列化实例也将尝试序列化其封闭实例。
您可以尝试几种不同的方法。
如果您使用的是Java 8,则可以使用lambda表达式而不是匿名内部类。 可序列化的lambda表达式(必然)不会引用其封闭的实例。 您只需要确保lambda表达式不显式或隐式引用this
,例如使用封闭类的字段或实例方法。 这个代码看起来像这样:
public class Caller { void call() { getRpcService().call(() -> { System.out.println("It worked!"); return null; }); }
( return null
是因为RPCService.Runnable.run()
被声明为返回Object
。)
另请注意,此lambda捕获的任何值(例如,局部变量或封闭类的静态字段)也必须是可序列化的。
如果您不使用Java 8,那么您的下一个最佳选择是使用静态嵌套类。
public class Caller { static class StaticNested implements RPCService.Runnable { @Override public Object run() { System.out.println("StaticNested worked!"); return null; } } void call() { getRpcService().call(new StaticNested()); } }
这里的主要区别在于它缺乏从call()
方法捕获Caller
或局部变量的实例字段的能力。 如有必要,可以将它们作为构造函数参数传递。 当然,通过这种方式传递的一切都必须是可序列化的。
如果您真的想使用匿名类,那么对此进行修改就是在静态上下文中实例化它。 ( 参见JLS 15.9.2 。)在这种情况下,匿名类将不具有封闭实例。 代码如下所示:
public class Caller { static RPCService.Runnable staticAnonymous = new RPCService.Runnable() { @Override public Object run() { System.out.println("staticAnonymous worked!"); return null; } }; void call() { getRpcService().call(staticAnonymous); } }
但是,与静态嵌套类相比,这几乎不会给你带来任何好处。 您仍然必须为其存储的字段命名,但您仍然无法捕获任何内容,甚至无法将值传递给构造函数。 但它确实满足了您的初始问题的字母,即如何在不序列化封闭实例的情况下序列化匿名类的实例。