Вместо посвящения
Всегда… Нет. Никогда не выходи в пургу пиши такой код ни для чего, кроме подобных забав.
Гийом — lead разработки языка Groovy
Reflection во зло
Один мой друг — большой любитель головоломок. Всяких, и программистких в том числе. Вот его последняя забава:
Напишите нужный код в static initializer, чтобы assert перестал падать:
public class Test {
static {
//Write some code here
}
public static void main(String[] args) {
Integer a = 20;
Integer b = 20;
assert (a + b == 60);
}
}
Если вы решите попробовать, не забудьте включить assertions (флагом -ea).
Дальше будет решение и кое какие рассуждения на тему, так что если вы уже справились, или вам влом -смело под кат.
Начнем с решения. Тут ничего особо сложного, просто нужно знать reflection и два факта о классе Integer:
- У него есть кэш для небольших (наболее часто встречающихся) значений
- Во время auto-boxing-а этот кэш используется (вот в конструкторе, например, кэш не используется)
Также нам нужно знать как этот кэш зовут и где он живет, но благодаря исходникам rt.jar это не проблема. Что бы вы думали? Этот кэш — закрытое (private) поле в закрытом внутреннем классе. Ура-ура.
Делать будем вот что:
- Берем объект class класса Integer
- Вытаскиваем список его внутренних классов
- Среди них находим нужный (тут нам повезло — он всего один. Правда, поскольку мы копаемся там, где не следует, никаких гарантий что он останется всего один в будующих версиях нет, ага.)
- Берем поле кэша
- Оно закрытое, так что делаем его доступным (accessible), чтобы получить значение
- Это массив. Меняем значение нужной ячейки с «20» на «30»
Вот вам код:
static {
try {
Class<?>[] declaredClasses = Integer.class.getDeclaredClasses();
Field cacheField = declaredClasses[0].getDeclaredField("cache");
cacheField.setAccessible(true);
((Integer[]) cacheField.get(null))[20 + 128] = 30;
} catch (Exception e) { e.printStackTrace(); }
}
Не знаю как вам, а как по мне так — ужас-ужас. Уродливый код, который лезет куда не следует, нарушает правила видимости и энкапсуляцию (есть такое слово?), да и ломается с пол-пинка (например, стоит создать Integer-ы конструкторами, а не auto-boxing-ом).
Да и вообще, нам просто повезло, что у Integer-а есть этот кэш, который можно подкрутить. Иначе — никак нам этот финт с изменением плюса не провернуть.
Почему такую простую штуку так тяжело сделать? Потому что Java под такие вещи не заточена (она-же женского рода, правда?). Java — статический язык, и это прекрасно. Мы можем всегда расчитывать на то, что 20+20=40. Ну, почти всегда. Это хорошо.
Но что, если нам все таки нужно провернуть подобные трюки (не с переопределением поведения плюса, конечно, но похожие)? Для этого есть инструменты получше. Например — Groovy.
Будем ломать правильно!
Вот версия головоломки на Groovy:
//Write some code here
Integer a = 20
Integer b = 20
assert 60 == a + b
Практически то же самое в головоломке (без безобразия с main(), дурацких точек-с-запятой и необходимости в -ea), но благодаря тому, что Groovy — динамический язык, решение совсем другое. Вот, что нужно знать:
- Groovy всегда работает только с объектами (никакого auto-boxing-а)
- Groovy реализирует операторы с помощью методов. Конкретно оператор "+" реализован методом (сюрприз:) «plus()»
- С помощью MetaClass-ов в Groovy можно запросто заменить любой метод замыканием (closure)
Вот чего мы сделаем:
- Берем MetaClass Integer-а
- Заменяем метод «plus()» реализацией, которая всегда возвращает 60
А всё! Вот код:
Integer.metaClass.plus = {int i -> 60 }
Тут, как говорится — без коментариев.
Вывода ровно два:
- Не занимайтесь подобной ерундой в настоящем коде.
- Используйте правильные инструменты для ваших целей.
Автор: jbaruch