MENU

【代码札记】Bagel

March 31, 2021 • 瞎折腾

今天在 CodeWars 上看到一个 JVM 的题,题不难,就是操作比较骚,故此写文记录下来。

题目

原题传送门

题目大意是给定一个类,其内部只有一个 getValue 方法,对于 Java,该方法修饰符含有 final,对于 Kotlin,该类没有指定 open 关键字。这个方法返回一个固定值 3,而通过测试的条件是让该方法返回的值变为 4。从答案入手的话,这样的说明可能会有些误导性,其中测试的语句如下:

Kotlin:

  • org.junit.Assert.assertEquals((bagel as Bagel).value == 4, java.lang.Boolean.TRUE)

Java:

  • Bagel bagel = BagelSolver.getBagel();
  • org.junit.Assert.assertEquals(
  • bagel.getValue() == 4,
  • java.lang.Boolean.TRUE
  • );

其中给出的 Bagel 类如下:

Kotlin:

  • class Bagel {
  • val value: Int get() = 3
  • }

Java:

  • public class Bagel {
  • public final int getValue() {
  • return 3;
  • }
  • }

我的思路

一开始我做的是 Kotlin 版的,通过语言的特性便知,如果没有明确的在类修饰符中写上 open 的话,编译器将会拒绝编译继承 Bagel 的类,即便是类似 object: Bagel() 这样也不行,因此即便是能用反射在运行时修改掉 open,也没办法在编译时就产生 Bagel 子类的字节码,故此法不行。退一步说,Kotlin 的反射机制并不能实现上述效果:

  1. Kotlin 的 KClassisOpen 字段是 val,即只读,无法按需修改成 true
  2. Kotlin 编译出的 val 变量没有对应的 setter

于是以下代码是不可行的:

  • class Bagel {
  • var value = 3
  • }
  • fun main() {
  • val obj = Bagel()
  • val p = obj::class.members.find { it.name == "value" }!! as KMutableProperty<Int>
  • val setter = p.setter as KFunction<Int>
  • setter.call(obj, 4)
  • println(obj.value)
  • }

运行上述代码将打印出 4,但前提是 valuevar 修饰的,因此对应的 KMutableProperty,而 val 产生的变量对应的是 KProperty,该类只有 getter 没有 setter,因此无法改变原题目中 value 的值。

如果 Kotlin 的反射行不通的话,就退回到 Java 的反射来试试看吧。

在 Java 中,虽然我们可以直接继承 Bagel 类,但由于 getValue 是由 final 修饰的方法,因此无法在子类中覆盖。即便我们可以在运行时中利用反射敲掉它的 final 修饰符,但由于不能够在编译时期做到,所以试图覆盖方法这条路走不通。对于普通的 Java 变量,我们可以通过反射来修改其中的值:

  • import java.lang.reflect.Field;
  • import java.lang.reflect.Modifier;
  • public class Main {
  • private static class Bagel {
  • private final int value = 3;
  • public int getValue() {
  • return value;
  • }
  • }
  • public static void main(String[] args) throws Exception {
  • var obj = new Bagel();
  • // get all declared field rather than public only
  • var prop = obj.getClass().getDeclaredField("value");
  • // remove `final` modifier of prop
  • Field modifiers = Field.class.getDeclaredField("modifiers");
  • modifiers.setAccessible(true);
  • modifiers.setInt(prop, prop.getModifiers() & ~Modifier.FINAL);
  • // now we can safely change the value of it
  • // by disable security check
  • prop.setAccessible(true);
  • prop.set(obj, 999);
  • System.out.println(prop.get(obj));
  • System.out.println(obj.getValue());
  • System.out.println(obj.value);
  • System.out.println(obj.getClass().getDeclaredMethod("getValue").invoke(obj));
  • }
  • }

运行结果如下:

  • 999
  • 3
  • 3
  • 3

看起来我们通过反射修改的值,只有通过反射获取才有效,而通过对象原本的 getter 访问,或者直接访问 value 时却又变回了 3,这是因为 value 在编译时期变成了一个常量,因此在编译时编译器将 3 这个东西写死给 value 和他的 getter,因此我们通过反射获取到 getter 并调用时,返回的还是 3。如果我们将 private final int value = 3; 中的 int 改成 Integer,这样编译器会认为 value 的值是一个对象,故此必须通过计算得到,这样我们的反射就能够成功修改了:

  • 999
  • 999
  • 999
  • 999

但遗憾的是 Badel 的代码是不能修改的,并且 Java 版的 Badel 只有一个 getValue 方法。

至此我的思路完全枯竭,我能想到的就是反射,而无论怎么反射似乎都没办法走通。于是乎只好放弃分数看看别人的答案,结果,就如同我开篇所说,题并不难,就是操作很骚。

Java 版答案

Java 作为一门老派的语言,答案固然中规中矩:

  • try {
  • Field b = Boolean.class.getDeclaredField("TRUE");
  • Field modifiers = Field.class.getDeclaredField("modifiers");
  • modifiers.setAccessible(true);
  • modifiers.setInt(b, b.getModifiers() & ~Modifier.FINAL);
  • b.setAccessible(true);
  • b.set(null, Boolean.FALSE);
  • } catch (Exception e) {}

还记得一开始提到的测试语句吗?

  • org.junit.Assert.assertEquals(
  • bagel.getValue() == 4,
  • java.lang.Boolean.TRUE
  • );

如果一般情况下要求结果为真的话,我们会这样写:

  • org.junit.Assert.assertTrue(
  • bagel.getValue() == 4
  • );
  • // or
  • org.junit.Assert.assertEquals(
  • bagel.getValue() == 4, true
  • );

写到这里应该就能看出一些门道了:之所以用 assertEquals,并且用它和 java.lang.Boolean.TRUE 做比较,就是为了让我们能够通过反射修改 java.lang.Boolean.TRUE 的值,反射的思路并没有错,只是需要修改 Boolean 类中名为 TRUE 的常量值就是了,由于 3 == 4 就是假,于是我们把 TRUE 的值改为假就能顺利通过测试了。

这波啊,这波就是骚操作啊。但是 Kotlin 版本还有更骚的操作。

Kotlin 版答案

如果说只是将上述代码翻译成 Kotlin,那就会显得这门语言不够活泼不够年轻。作为一门这几年新发展出来的语言,它与 Java 相比无不处处散发着年轻的气息,无论是其优点还是缺点,都富有年轻人的样子。所以 Kotlin 版的答案是这样的:

  • object org{object junit{object Assert{fun assertEquals(a: Any?, b: Any?){}}}}

没错,就这一行,还记得那条测试语句吗?

  • org.junit.Assert.assertEquals((bagel as Bagel).value == 4, java.lang.Boolean.TRUE)

正经人谁会在没有冲突的代码里写全包名啊?用一个 import 不好嘛?于是这也成了问题的一种解法:除了在 Kotlin 中使用 Java 的反射,还可以在同一个包内通过. 的二义性来通过测试,由于上面定义的 Assert 类与 Junit 中的 Assert 不是同一个包,因此根本不会造成重复声明的问题,于是上面的一大串包名就变成了对 Object 对象的一系列调用。通过覆盖 assertEquals 方法,让它什么都不干,就可以保证顺利通过测试了。

果然人的思维还是不要形成一种定式,有时候一些野路子、骚操作往往就是解决问题的关键,所以拒绝「机械式刷题」不是白说的。

- 全文完 -


知识共享许可协议
【代码札记】Bagel天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code