Xpoint
   [напомнить пароль]

Передача параметров по ссылке в Java - вопрос по стилю

Метки: [без меток]
[арх]
2007-07-21 15:45:24 [обр] Даниэль Алиевский(35/125)[досье]

Один из стандартных упреков в адрес Java со стороны новичков - отсутствие параметров, передаваемых по ссылке (как "&arg" в C++ или "var arg" в Паскале). На самом деле упрек столь же несостоятелен, как и отсутствие goto - по крайней мере, в 99.99% случаев. Действительно, грамотно спроектированный объектно-ориентированный код попросту не нуждается в таких параметрах (что подтверждается огромным количеством довольно качественных библиотек от Sun, которые прекрасно обходятся без передачи по ссылке). Хороший метод либо модифицирует состояние объекта (может быть, своего аргумента), либо возвращает всю информацию в новом объекте.

Все это так, но меня не оставляет мысль, что существует те самые 0.01% случаев, когда передача параметра по ссылке все же оправдана. (Ведь и goto очень-очень редко все-таки применялся в высокопрофессиональных библиотеках на C++ и Паскале.) В данный момент я, как мне кажется, столкнулся как раз с таким случаем, и мне интересно, какой вариант решения посоветуют уважаемые коллеги с этого форума. Задаю вопрос отчасти потому, что с моим коллегой по работе наши мнения разошлись радикально :)

Итак. У меня есть метод

public static DoubleRange rangeOf(PArray array)

Здесь PArray - некий класс, описывающий массив чисел, а DoubleRange - пара чисел min и max (аналог апачевского DoubleRange). Метод одновременно вычисляет минимум и максимум в массиве, которые и возвращает в виде экземпляра DoubleRange.

Я хочу иметь перегруженную версию этого метода, которая, кроме диапазона min..max, вернет также индексы в массиве найденных минимума и максимума. Просто на всякий случай: подобная информация нужна редко. Это еще 2 целых числа, в моем случае long (мои массивы имеют 64-битовую индексацию). В Паскале можно было бы указать их в качестве дополнительных var-параметров. Что делать в Java?

Варианты следующие.

  1. Создаем новый класс DoubleRangeAndIndexes, содержащий min, max, indexOfMin, indexOfMax, и возвращаем его экземпляр. Крайне уродливо: такой класс, в отличие от DoubleRange, не описывает никакой осмысленной сущности. Получается "класс ради метода", хотя на самом деле все должно быть наоборот - методы ради обслуживания классов.
  1. Передаем оставшиеся 2 числа каким-нибудь кривым, но поддерживаемым в языке способом. Самый очевидный - массив:
public static DoubleRange rangeOf(PArray array, long[] minAndMaxIndexes)

Метод записывает индексы минимума и максимума в ячейки minAndMaxIndexes[0] и minAndMaxIndexes[1]. До чего же мерзко, однако. Особенно последующее использование: в коде появляются "магические номера" 0 и 1.

  1. Используем org.omg.CORBA.LongHolder:

public static DoubleRange rangeOf(PArray array, LongHolder indexOfMin, LongHolder indexOfMax)
Собственно, это единственный способ передать примитивное значение по ссылке в Java при помощи стандартного API. Более того, само происхождение пакета намекает на передачу параметров по ссылке - очевидно, эти Holder-ы появились в CORBA для взаимодействия с языками, поддерживающими такую возможность. В данный момент у меня так и сделано. Мой коллега, однако, грязно ругается: какое, мол, отношение имеет CORBA к моей алгоритмической библиотеке, тем более к подсчету минимумов и максимумов. А я говорю: зато этот пакет включен в стандартную поставку JRE.

  1. Пишем и используем свой эквивалент LongHolder, например, в виде вложенного класса, специально для этого метода. Не так плохо, как вариант #1, но все равно "класс ради метода". К тому же у пользователей возникает очевидный вопрос: зачем написан класс, стопроцентно эквивалентный уже имеющемуся в стандартных библиотеках?
  1. Пишем свой класс MinAndMaxPosition, позволяющий хранить (и модифицировать) 2 целых числа с названиями min и max. Передаем вместо пары LongHolder-ов. Но разве такой класс - осмысленная сущность?

Хотелось бы послушать, какой вариант предпочли бы уважаемые коллеги. Или, может быть, существует совершенно иное решение?

спустя 6 часов [обр] Давид Мзареулян(7/1003)[досье]
Создаем новый класс DoubleRangeAndIndexes, содержащий min, max, indexOfMin, indexOfMax, и возвращаем его экземпляр. Крайне уродливо: такой класс, в отличие от DoubleRange, не описывает никакой осмысленной сущности. Получается "класс ради метода", хотя на самом деле все должно быть наоборот - методы ради обслуживания классов.

Совершенно верно, методы ради классов. И именно поэтому (поскольку Вы не указали, методом ЧЕГО является Ваш “rangeOf”) что-то осмысленное тут сказать трудно.

Одно можно сказать точно: если возникает желание обойти ограничения (хорошо разработанного) языка, значит, что-то у Вас не так в консерватории, т.е. в дизайне приложения. Например, с моей наивной точки зрения совершенно непонятно, почему “rangeOf” не является методом PArray, хотя использует только его данные и ничего не меняет в окружающем мире.

спустя 14 часов [обр] Даниэль Алиевский(35/125)[досье]

На самом деле я задал этот же вопрос еще здесь: http://community.livejournal.com/ru_java/499927.html
Там я уже написал, метод чего - rangeOf :)

Он не является методом PArray просто потому, что PArray - весьма сложный интерфейс, и я не могу "запихнуть" в него все мыслимые методы обработки. Скажем, копирование элементов там есть, но уже сортировка, сумма элементов, поэлементная обработка произвольной математической функцией, построение гистограмм и прочее, прочее - привилегия других классов. Arrays - безэкземплярный класс, содержащий rangeOf - предоставляет наиболее простые и употребительные средства обработки моих массивов. Примерно как java.util.Collections. rangeOf - один из самых простых методов, причем довольно востребованный: во многих слуаях (скажем, при рисовании) бывает нужно определить диапазон изменения элементов. Проблема как раз в том, что версия rangeOf, возвращающая индексы, востребована несравненно меньше, но для полноты API должна присутствовать. И надо ее добавить так, чтобы она вписалась в общий пакет "органично": не порождая, скажем, своего собственного класса, обладающего своей немаленькой документацией.

В сущности, я не пытаюсь обойти ограничения языка: ведь современное мое решение, применяющее LongHolder, не выходит за рамки стандартного Java API. Три параметра у метода - тоже вполне "в рамках".

спустя 1 час 1 минуту [обр] Давид Мзареулян(7/1003)[досье]

Понял. Ну, если Вы строите свою программу на процедурной, а не объектной основе (а Arrays Ваш, как я понял, есть ни что иное как библиотека статических процедур), то Вам постоянно будут нужны всякие хаки типа передачи параметров по ссылке.

Как только у Вас возникает ситуация, что в статик-метод передаётся один объект, и работает метод только с ним — это верный признак того, что метод этот должен быть или а) методом этого объекта или б) методом наследника класса этого объекта или в) методом класса-обёртки вокруг этого объекта. Потому что если Вы пишете функцию специально для работы с PArray и только с ним, то выносом её в другой класс Вы только самого себя обманываете. Нет ничего уже, чем делать из класса библиотеку статик-методов, работающих с совершенно другими классами. Тогда на Паскале писать надо, а не на Яве.

Я бы предпочёл обёртку (в этом согласен с http://community.livejournal.c......7.html?thread=5241815#t5241815).

спустя 20 часов [обр] Даниэль Алиевский(35/125)[досье]

А Вы строите программы чисто объектно? Это ведь не всегда оправдано. Особенно в алгоритмах. И авторы Java тоже так считают. Посмотрите на классы java.util.Collections, java.util.Arrays, наконец, Math или Runtime. В моей библиотеке net.algart.arrays я выстроил довольно мощный API обработки обобщенных массивов с 64-битовой адресацией, разными моделями памяти и прочее, и прочее. Ситуация вполне сходна с java.util, и те же причины, которые побудили программистов Sun создать класс Collections, заставили меня сделать класс net.algart.arrays.Arrays. Сейчас там 42 public-static-метода, но вполне вероятно, что их количество в будущем увеличится.

Причина отхода от ООП вполне понятна. Мне понравилась формулировка из одной "умной книжки": ООП обеспечивает масштабируемость при создании генераторов (средств порождения объектов), а процедурное программирование - при создании анализаторов (средств анализа объектов).

Действительно, расширить набор классов (т.е. генераторов), не переписывая большой объем кода, в ООП-программе очень просто: просто добавляются новые реализации старых интерфейсов. И это, кто спорит, очень полезно в алгоритмике. Скажем, если я хочу применить ко всем элементам одного или нескольких массивов произвольную математическую операцию, я вызываю одну-единственную функцию:

public static <T extends PArray> asFunc(Func f, Class<? extends T> requiredArrayType, PArray ...x)

где Func - интерфейс, описывающий функцию с помощью максимально общего метода double f(double ...x). А когда мне потребовалась эффективность (скажем, мы имеем 2 битовых массива и функцию "минимум" - тут надо бы не вещественную f вызывать для вещественных 0 и 1, а использовать побитовое "и"), я просто написал пару сотен частных private-реализаций интерфейса PArray (каждая, благодаря наследованию, размером в десяток строк), учитывающих наиболее популярные частные случаи функции f и разрядности массивов x. Т.е. анализатор - один (метод asFunc), а генераторов - множество (различные варианты класса, реализующие разные алгоритмы).

Но, увы, масштабируемость набора анализаторов (т.е. методов) в рамках ООП оставляет желать лучшего. Основные сущности в ООП, как правило, описываются интерфейсами (как мой PArray или Func) либо абстрактными классами. Добавление метода в интерфейс означает необходимость пересмотра и, иногда, переписывания всех классов, его реализующих. Абстрактный класс формально позволяет добавить новый метод, гарантируя совместимость, но по сути ситуация та же: если мы хотим, чтобы он как-то взаимодействовал с прочей архитектурой класса, мы обязаны пересмотреть всех наследников - чтобы, например, переопределить этот новый метод там, где это разумно. Всякий открытый метод класса автоматически становится "известен" тысячам (сотням тысяч) клиентов этого класса; его требуется изучать и, время от времени, переопределять. Сравните с библиотекой анализаторов вроде Arrays: таких библиотек можно написать сотни, в разных пакетах и разных библиотеках, а знать нужно только о тех, которыми мы реально пользуемся. Соответственно, интерфейс типа PArray просто обязан быть минимальным: содержать только те методы, которые заведомо понадобятся и которые вполне очевидны. Все же прочие алгоритмы, работающие с PArray, должны быть где-то еще.

В самом деле, Давид, для обработки массива существуют даже не десятки, а десятки тысяч различных алгоритмов. От сортировки до Фурье-преобразования. Неужели Вы считаете, что все эти алгоритмы, даже если они работают "с PArray и только с ним", должны находиться либо в PArray, либо в его наследнике, либо в обертке? То же Фурье, вероятнее всего, будет оформлено в виде класса, может быть, обладающего какими-то настройками, но в конечном счете получающим на входе DoubleArray (наследник моего PArray) и запоминающим внутри объекта "Фурье" 2 DoubleArray для вещественной и мнимой части результата, которые можно будет вернуть get-методами. В самых простых случаях, когда никакие настройки или сложное хранение данных не требуется (суммирование, поиск минимума-максимума, построение гистограммы, сортировка, поиск и т.п.) наиболее естественное решение - static-метод с параметром PArray.

Кстати, LongHolder мне уже перестал нравится. Не потому, что плоха идея, а из-за устаревшей реализации. Сейчас должен был бы быть единственный общий Holder<T>, позволяющий сохранить любой генерализованный тип, в частности, Long или Integer.

Пока что я склоняюсь к варианту номер 5: класс MinMaxPositions. В конце концов, есть точный аналог: java.text.ParsePosition. По крайней мере, сложность API от этого не возрастает, как и в случае с LongHolder. Пока пользователю не нужны индексы, он может не обращать внимания на этот класс (в отличие от варианта #1). А если понадобятся, то, может быть, MinMaxPositions уже не покажется столь уж "лишней" сущностью.

спустя 3 часа 30 минут [обр] Давид Мзареулян(7/1003)[досье]
То же Фурье, вероятнее всего, будет оформлено в виде класса, может быть, обладающего какими-то настройками, но в конечном счете получающим на входе DoubleArray (наследник моего PArray) и запоминающим внутри объекта "Фурье" 2 DoubleArray для вещественной и мнимой части результата, которые можно будет вернуть get-методами.

Ну Вы же, фактически обёртку и описали. И в Java это стандартный паттерн — например, вся IO-библиотека на этом построена. Берётся класс, который читает/пишет байты, заворачивается в класс, который делает их буферизирует, заворачивается в класс, который из байт делает строки, заворачивается в класс, который из строк делает линии… потому Java-программы такие и большие:)

У Вас есть общий интерфейс для всех массивов. И Вы можете насочинять кучу обёрток, каждпая из которых будет принимать в конструктор объект с этим интерфейсом и делать с ним то, что умеет именно она. Хоть фурье, хоть максимумы искать.

спустя 5 часов [обр] Даниэль Алиевский(35/125)[досье]

В IO - действительно "обертки": новый класс, чем-то похожий на "оборачивамый", но с дополненной либо усеченной функциональностью. Все они полезны, имеют сопоставимую сложность и решают нетривиальные задачи. Но простейший класс, хранящий минимум и максимум, я бы никак не назвал "оберткой" массива. Получается пальба из пушки по воробьям: ради пары целых чисел заводится специальный класс.

Я сделал сейчас класс MinMaxInfo, передаваемый последним параметром вместо пары LongHolder-ов. (Возвращать класс в результате мне совсем не нравится: ведь тогда останется только один метод rangeOf, и даже при самом обычном, штатном применении клиенту придется разбираться с моим классом.) Полная реализация вместе с JavaDoc-комментариями заняла у меня 200 строк (!). Из которых комментарии - большая часть. А куда деваться - toString, hashCode, equals, 5 get-методов (кроме индексов, он хранит заодно и значения минимума и максимума). Спецификация поведения в многопоточной среде, описание использования... Неужели это правда хорошо? Ведь заведомо известно, что без rangeOf этот класс никогда не будет использоваться. Неужели "чистота ООП-стиля" важнее, чем краткость и очевидность 2 целых чисел, переданных по ссылке?

С тем же квадратным уравнением все было бы не так. Был бы класс-полином, различные пакеты, умеющие с ним работать - например, перемножать, разлагать на множители и пр. В качестве очевидного внутреннего свойства полином хранил бы свои комплексные (или вещественные) корни, которые мог бы вернуть. Работа с таким полиномом очень быстро оказалась бы удобнее, нагляднее и короче, чем применение Паскалевской процедуры с 3 параметрами-коэффициентами и 2 var-параметрами-корнями.

Powered by POEM™ Engine Copyright © 2002-2005