Про Правильные Инструменты

в 10:57, , рубрики: groovy, java, metaclass, reflection, метки: , , ,

Вместо посвящения

Всегда… Нет. Никогда не выходи в пургу пиши такой код ни для чего, кроме подобных забав.
image
Гийом — 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:

  1. У него есть кэш для небольших (наболее часто встречающихся) значений
  2. Во время auto-boxing-а этот кэш используется (вот в конструкторе, например, кэш не используется)

Также нам нужно знать как этот кэш зовут и где он живет, но благодаря исходникам rt.jar это не проблема. Что бы вы думали? Этот кэш — закрытое (private) поле в закрытом внутреннем классе. Ура-ура.

Делать будем вот что:

  1. Берем объект class класса Integer
  2. Вытаскиваем список его внутренних классов
  3. Среди них находим нужный (тут нам повезло — он всего один. Правда, поскольку мы копаемся там, где не следует, никаких гарантий что он останется всего один в будующих версиях нет, ага.)
  4. Берем поле кэша
  5. Оно закрытое, так что делаем его доступным (accessible), чтобы получить значение
  6. Это массив. Меняем значение нужной ячейки с «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 — динамический язык, решение совсем другое. Вот, что нужно знать:

  1. Groovy всегда работает только с объектами (никакого auto-boxing-а)
  2. Groovy реализирует операторы с помощью методов. Конкретно оператор "+" реализован методом (сюрприз:) «plus()»
  3. С помощью MetaClass-ов в Groovy можно запросто заменить любой метод замыканием (closure)

Вот чего мы сделаем:

  1. Берем MetaClass Integer-а
  2. Заменяем метод «plus()» реализацией, которая всегда возвращает 60

А всё! Вот код:

Integer.metaClass.plus = {int i -> 60 }

Тут, как говорится — без коментариев.

Вывода ровно два:

  1. Не занимайтесь подобной ерундой в настоящем коде.
  2. Используйте правильные инструменты для ваших целей.

Автор: jbaruch


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js