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