如果你不能投球,那就没什么乐趣了。但扩展Throwable也使它实现了可序列化,这就是真正有趣的开始。使用序列化,我们可以创建一个球,该球应该被捕获的次数与序列化数据声明的次数相同。
这场比赛似乎破坏了乐趣。你不能抛出它;但更重要的是你不能序列化它。如果您试图直接将球序列化,它也将尝试序列化它所附加到的游戏,从而导致NotSerializableException。
请注意,从技术上讲,从球到其比赛的参考只是一个类似于 this$0的字段。我说的“附加”是指指定给那个字段。因此,这个问题相当于序列化一个具有不可序列化的正常字段的对象。
问题在于我们根本不需要序列化游戏和球。我们只需要将球反序列化,并将其附加到游戏中。该Game实例不需要直接来自序列化流。我们可以在它的位置放置替代品,并覆盖readResolve,在它附着到球之前用一个game替换它。
在实践中作弊
有多种方法可以创建包含附加到替代游戏的球的原始数据(字节数组)。我们创建了一个类似于ball(ba)的类。我们给它和ball一样的serialversionuid和我们期望的caught值。它附加到我们的替代实现readResolve(Player)上。我们将其序列化,并将ba类的名称替换为ball类。将byte[]转换为一个String并返回,允许我们使用string.replace。
选择ba这个名称是为了让play.player$ba的长度与game.game$ball的长度相同。否则,直接用一个替换另一个会损坏流。
<b>package</b> play; <b>import</b> game.Game; <b>import</b> game.Game.Ball; <b>import</b> java.io.*; <b>public</b> <b>class</b> Player implements Serializable { <b>public</b> <b>static</b> <b>void</b> main(String[] args) throws Exception { ByteArrayOutputStream bos = <b>new</b> ByteArrayOutputStream(); <b>new</b> ObjectOutputStream(bos).writeObject(<b>new</b> Player().<b>new</b> Ba()); byte[] bytes = <b>new</b> String(bos.toByteArray(), <font>"ISO-8859-1"</font><font>) .replace(</font><font>"play.Player$Ba"</font><font>, </font><font>"game.Game$Ball"</font><font>) .getBytes(</font><font>"ISO-8859-1"</font><font>); Ball ball = (Ball) <b>new</b> ObjectInputStream(<b>new</b> ByteArrayInputStream(bytes)) .readObject(); ball.caught(); } <b>class</b> Ba implements Serializable { <b>static</b> <b>final</b> <b>long</b> serialVersionUID = -7172046060844866133L; <b>private</b> <b>long</b> caught = -1; } Object readResolve() { <b>return</b> <b>new</b> Game(); } } </font>
这就是发生的情况:
结论
创建具有对不可序列化对象的引用的可序列化对象是一个坏主意,因为您无法对它们进行序列化。但是,你仍然可以反序列化它们。
Java序列化充满了令人讨厌的意外可能性。关于这一点你可以做一系列的谜题。但是,如果你真的陷入此种境地,那么你需要做的就是查看过去几年的JDK安全漏洞。