今天在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 处获得。
骚的闪了我的腰