今天在 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 的反射机制并不能实现上述效果:
- Kotlin 的
KClass
的isOpen
字段是val
,即只读,无法按需修改成true
- 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
,但前提是 value
是 var
修饰的,因此对应的 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 处获得。
骚的闪了我的腰