вторник, 12 марта 2013 г.

Eclipse - горячие клавиши в русском регистре

Среда разработки Eclipse, являющаяся классической для программирования под Android на языке Java, имеет маленький но неприятный недостаток - сочетания горячих клавиш перестают работать при переключении на русский язык. В частности, меня достало то, что при нажатии сочетания "Ctrl+/" в русском регистре вместо закомментировывания текущей или всех выделенных строк курсор ускакивал на первое предупреждение в коде, выполняя команду "Ctrl+." Что же, зато в Eclipse можно назначить свои горячие клавиши! Поэтому для решения проблемы надо просто задублировать команду комментария на сочетание "Ctrl+."!

Открываем: "Окно → Параметры... → Общие → Клавиши". В поле "фильтр" набираем "Ctrl+/" и получаем список с этой командой для поддерживаемых языков программирования. Щёлкаем по строчке со значением "Изменение исходного кода Java" в столбце "Когда" и нажимаем кнопку "Добавить". Теперь просто меняем сочетание в поле "Привязка" на  "Ctrl+." и применяем изменения. Всё! Теперь Eclipse будет комментировать строки и в английском и в русском регистрах одинаковыми кнопками!

понедельник, 11 марта 2013 г.

Табуляция или пробелы?


Среди программистов не утихают религиозные войны на тему форматирования кода. Один из острейших вопросов на эту тему — чем делать отступы и выравнивание в исходном коде программ — табуляцией, пробелами или одно — одним, а другое — другим? У каждого варианта, определённо, есть и свои плюсы, и свои минусы.

По моему наблюдению, среди матёрых программистов в большинстве случаев всё же побеждают сторонники пробелов, и решающим аргументом здесь служит визуальная одинаковость выровненных строк вне зависимости от настроек программы у каждого конкретного программиста. А о количестве пробелов в отступе они предпочитают договариваться внутри команды. Что им мешает так же договориться о величине таба — непонятно :)

Вероятно, актуальность использования пробелов возникает в том случае, если программисты не договорились о величине «таба», но готовы работать с кодом друг друга, имеющим  разные отступы. В этом случае код и комментарии никогда не будут ползти, и для сохранения такого стабильного внешнего вида не нужно будет всякий раз переключать настройки редактора. Проблема в этом случае возникнет лишь при слиянии блоков кода, выполненных разными программистами, в одном файле.


Различия в поведении пробелов и табов

Рассмотрим следующий показательный фрагмент кода:

if (mStatus != Status.PENDING) {
    switch (mStatus) {
        case RUNNING:
            throw new IllegalStateException("Cannot execute task: "
                                          + "the task is already running.");
        case FINISHED:
            throw new IllegalStateException("Cannot execute task: "
                                          + "the task has already been executed "
                                          + "(a task can be executed only once)");
    }
}

Мы видим, что блоки текстовых сообщений легко читаются, потому что начало каждой строчки выровнено по левому краю. Когда все отступы (и кода, и выравнивания) выполнены пробелами, то блоки текста будут выровнены всегда, на любом компьютере и в любом редакторе исходного кода (ведь при написании исходного кода традиционно применяется моноширинный шрифт с пробелами одинаковой с символами ширины). Но если размер отступов будет непривычен другому программисту, он не сможет просто так его изменить.

Если же мы будем выравнивать не пробелами, а табуляцией, то мы сами получим тот же самый вид, а вот у другого программиста, у которого редактор настроен на другую кратность табуляции, например, не 4, как у нас, а 8, строчки сильно сместятся вправо, а при кратности табуляции 2 — влево. При этом у нас неминуемо поползут вторые строчки текстового сообщения. То же самое будет происходить и с выравниванием комментариев в конце строк кода. Они уже не будут красиво выровнены по левому краю на протяжении некоторого блока, имеющего разные уровни вложенности.

Промежуточным вариантом называют создание отступов — табуляцией, а выравнивания — комбинированным методом. Начало выравнивания осуществляется табуляцией, а по достижении уровня предыдущей строки — пробелами. Минусом такого способа является относительная трудоёмкость такого метода и потенциальная путаница — ведь без включения визуализации управляющих символов, сразу непонятно, где табуляция, а где пробелы. Непонятно также, как они поведут себя при копировании в другие места кода.

Более того, вышеописанный способ не спасёт от нарушения выравнивания комментариев в конце строк. Да и не все программисты, которые впоследствии будут редактировать ваш код, поймут или примут вашу задумку.

Так как же правильно оформлять код — пробелами или табуляцией? А может и тем и другим? Давайте попробуем разобраться с этим.


История табуляции — интересные факты

Небольшой экскурс в историю. Табуляция (горизонтальная табуляция) изначально была введена ещё в механических печатных машинках для удобства построения таблиц и применялась также для создания абзацного отступа. Она и не имела жёсткого размера. Перед работой пользователь сам устанавливал нужные ему позиции табуляции с помощью специального механизма. Так же, как это сейчас можно сделать в MS Word. В результате, при последовательных нажатиях на клавишу табуляции (обозначалась как ←) каретка под действием пружины автоматически перемещалась влево по всем установленным ранее позициям, перемещая тем самым область ввода вправо.

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

Почему же выбор размера фиксированной табуляции пал именно на 8 знакомест? Как правило, печатные машинки имели длину строки 80 знакомест. Телетайпы, применявшиеся в первых ЭВМ для вывода информации, тоже, поскольку являлись наследниками пишущих машинок. Даже на перфокартах информация стала храниться строками по 80 символов. Таким образом, печатные машинки задали некий стандарт на длину строки.

Поскольку в первую очередь табуляция использовалась для создания колонок таблиц, то наиболее удобный размер должен был быть не меньше разницы в длине наиболее употребительных слов, если их писать в столбик (чтобы для перехода к следующему столбцу не надо было делать разное число табуляций). В то же время, размер этот должен был позволять делать максимально возможное количество столбцов. Третье условие — размер должен укладываться в длину строки 80 символов кратное число раз.

Согласно последнему условию, у разработчиков был выбор из следующих пяти реальных вариантов: 4, 5, 8, 10 и 16. Первые два варианта были не очень удобными, поскольку разница в длинах слов английского языка часто превышала эти значения. Отступы в 16 символов выглядели чрезмерно большими, снижали удобство пользования этим инструментом и сильно ограничивали число столбцов в таблицах. Оставался выбор между 8 и 10 символами.

Значение в 8 символов стало первым значением, которое удовлетворяло условию разницы длин слов. Кроме того, только оно и удовлетворяло второму условию — построения максимального возможного числа столбцов. Вариант с табуляцией в 10 символов выглядел неоправданно расточительно и давал всего 8 столбцов, что могло несколько сузить сферу применения табуляции. Кроме того, число 10 для максимального количества столбцов психологически выглядело более внушительно. Все эти причины, видимо, и склонили разработчиков телетайпа к табуляции именно в 8 символов.

Несколько слов про абзацный отступ, поскольку он тоже проходит где-то рядом с нашей темой. Согласно ОСТ 29.115-88, при печати на пишущих машинках абзацный отступ должен быть равным трём ударам, но допускается использовать отступ и в пять ударов. Кроме того, предписывается размещать на одной странице 29 (+-1) строк (что примерно соответствует полуторному интервалу на печатной машинке). Требование отступа в 3 удара при полуторном интервале довольно странное, и ниже я объясню, почему.

В типографической вёрстке абзацный отступ равен полуторному размеру кегля шрифта, то есть, грубо говоря, вертикальному размеру строки (т.е. расстоянию от низа одной строки до низа другой), умноженному на 1,5. Поскольку в типографической вёрстке полутроный интервал почти не используется, а строчки следуют сразу одна за другой, то правильный абзац в этом случае приблизительно равен трём средним символам кириллицы.

Вероятно, составители ОСТ 29.115-88, не вдаваясь в подробности типографики, приняли это значение за константу и для моноширинных фиксированных шрифтов печатных машинок, и вместе с тем установили стандартом полуторный межстрочный интервал, поскольку при моноширинном шрифте и низком качестве печати одинарный интервал смотрелся реально очень плохо.

Но увеличенный интервал, исходя из правил типографики, требует и увеличенного абзацного отступа. На момент составления стандарта такой увеличенный отступ в 5 ударов уже интуитивно широко использовался. Видимо поэтому в стандарте и был узаконен применявшийся де-факто увеличенный отступ в 5 ударов.

Кстати, печатные машинки оказали влияние не только на количество столбцов текстового режима монитора, но и на его вертикальный размер в 30 строк. Ведь при печати с полуторным интервалом именно столько строк и вмещалось на один лист!


Стратегический взгляд на решение проблемы

Конечно, наиболее логично делать отступы уровней вложенности кода табуляцией, потому что она изначально для этого и предназначена — начинать печать текста с разных позиций. Кроме того, в эпоху компьютеров символ табуляции очень хорошо подошёл и на роль логического делителя. Количество последовательных символов табуляции чётко указывало уровень логического вложения.

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

Выходом могла бы стать принудительная привязка размера табуляции к языку программирования. Специалистами по эргономике могли бы быть чётко определены наиболее удобные размеры табуляции для синтаксиса каждого из языков, и этот размер мог бы быть зафиксирован в компиляторе, выдавая настойчивые предупреждения при его нарушении.

Поскольку мы не можем повлиять на стратегию решения проблемы, мы вынуждены как-то выкручиваться тактически в рамках имеющейся реальности. Чтобы выработать для себя правильный вариант, давайте рассмотрим проблемы пробелов и табуляции чуть подробнее.


Проблемы пробелов

Когда программист открывает чужой исходный код с отступами и выравниванием, осуществлёнными пробелами, но отступ этот оказывается неудобен ему, он, по сути, ничего не сможет сделать для приведения его к удобному виду без проведения каких-то сложных манипуляций.

Другой проблемой пробелов являются ошибки, связанные со случайным удалением одного из пробелов отступа, оказывающиеся незамеченными. При переходе на новую строку редактор автоматически создаст тот же ошибочный отступ, что и у предыдущей строки, и эта ошибка может распространиться на несколько строк кода. Исправление этой ошибки потребует определённого времени, потраченного на унылые, чисто механические операции.

Кроме того, для удаления лишнего отступа вместо привычного нажатия на Backspace приходится нажимать сочетание клавиш Shift+Tab. Но это уже, конечно, дело привычки.


Проблемы табуляции

У табуляции только одна проблема — при изменении её размера нарушается выравнивание комментариев и других выровненных элементов, хотя их, как правило, не так много, чтобы это затрудняло чтение исходника, да и подправить их уже не так сложно.

К тому же, никто не мешает просто включить в редакторе тот размер табуляции, который был установлен у автора, сведя внешний вид к тому, который был бы, если бы исходный код был просто отформатирован пробелами. Специально для таких случаев некоторые программисты пишут в верхнем комментарии к коду рекомендуемый размер табуляции — на мой взгляд, очень правильное решение.


Выводы

Сначала скажу пару слов о размере отступов, как таковых. Применительно для большинства распространённых языков программирования идеальным размером отступов является ровно 4 символа. Почему ровно? Потому что уже сложилось так, что большинство исходников отформатированы именно таким образом, и тонкая подгонка под субъективный «идеал» в 3 или 5 символов теряет смысл.

Встречающиеся иногда отступы в 8 символов делают код излишне размазанным по горизонтали и совсем не оставляют места для комментариев справа. Кроме того, из-за превышения размера отступов над длиной операторов, в ступеньках лесенки вложенности обнажаются пробелы между операторами и операндами, которые создают визуальные провалы, что никак не идёт на пользу читабельности кода.

Также не очень хорошим решением является использование отступов в два символа, что тоже встречается. В этом случае, хотя и экономится место по ширине, но становится довольно затруднительно ориентироваться в уровнях вложенности, что неизбежно вызывает ускоренное утомление.

Если программисты будут придерживаться золотой середины в 4 символа, то спор между сторонниками пробелов и табуляции станет неактуальным. А пока можно дать следующие рекомендации:

  1. Используйте отступы стандартного де-факто размера. Для Java, Pascal, C, C++  и т.п. стандартом де-факто является отступ в 4 символа, как бы нам ни хотелось использовать другой размер.
  2. Если соблюдён пункт 1, то абсолютно неважно, чем вы будете делать отступы. Если вы сделаете их пробелами, то будет хорошо — ваш код везде будет выглядеть читаемо, и никогда ничего не поползёт. Другие программисты при чтении вашего кода будут привыкать к правильному форматированию. Если же вы сделаете отступы табуляцией, то будет ещё лучше — вы дадите другим программистам выбор — включить в редакторе правильный размер табуляции и читать правильно отформатированный код, или  читать его с привычными им отступами, но поползшими комментариями и отдельными строками, в которых применялось выравнивание.
  3. Выбор в пункте 2 можно осуществлять в зависимости от того, как ваш код будет использоваться в дальнейшем. Если его блоки будут вставляться в чужой код, тогда точно имеет смысл выбрать табуляцию, чтобы вставляющему проще было подогнать форматирование. Если исходник предназначен для публикации в Интернете, где табуляция либо съедается, либо зафиксирована на 8 символов, то тогда можно выбрать пробелы, чтобы исключить дополнительный шаг подготовки исходника к публикации. Если же в вашей организации уже установлены определённые правила форматирования, то выбора у вас уже нет :)

Если несмотря ни на что вы твёрдо намерены использовать своё количество символов в отступах, то в этом случае использование пробелов и табуляции обязательно будет зависеть от того, как впоследствии будет использоваться ваш код другими программистами.

Если для обычного сопровождения в плане исправления ошибок — наверное, лучше использовать пробелы. С вашим кодом будут работать недолго, и если он хорошо отформатирован, то особой нужды в изменении размера отступов ни у кого не будет.

Если вашим кодом будут активно пользоваться другие программисты, вставляя его в свои программы или просто развивая ваш проект, то лучше использовать табуляцию. В этом случае другому программисту будет проще адаптировать вид вашего кода к принятому в той или иной группе стандарту.


Если у вас одно, а нужно другое

Есть такой замечательный редактор — Notepad++. В нём замена табуляции на пробелы и наоборот выполняется одним щелчком мыши, сделанным разделе в меню «Правка →  Операции с Пробелами». Я использую этот редактор для перегона табулированных кусков кода, предназначенных для публикации в блоге, поскольку последний автоматом меняет один знак табуляции на один пробел, что неприемлемо.


Заключение

Многие могут задать вопрос, а что же выбрал для себя автор этой статьи? А выбрал он табуляцию размером 4 знакоместа. Я не нашёл достаточно веских причин для использования пробелов ради того, чтобы мой код открывался у кого-то, кто не соизволил настроить отступ в своём редакторе кода на де-факто стандарт в 4 символа.

Что касается Notepad из состава Windows, который не имеет настройки размера табуляции, и прочих подобных редакторов, делающих табуляцию принудительно в 8 символов, то я не могу себе представить причину, по которой программист открывал бы мой код именно в них, тогда как уже давно для всех ОС существуют удобные специализированные редакторы для исходного кода.

Работать в блокноте не круто. Круто работать в шестнадцатеричном редакторе :)

суббота, 9 марта 2013 г.

AsyncTask. Максимальное количество одновременных потоков.

Вступление

В настоящее время операционная система Android переживает бурное развитие, что в то же время негативно сказывается на постоянстве результата работы одних и тех же классов и методов в разных версиях этой системы. Одним из примеров такого непостоянства может служить класс асинхронных задач AsyncTask.

Асинхронные задачи - это потоки, выполняемые параллельно с основным потоком, в котором работает пользовательский интерфейс приложения, и никак не синхронизированные с ним. Это значит, что поток пользовательского интерфейса, запустив такую асинхронную задачу, не знает, когда в него вернётся результат её работы. Плюсом такой архитектуры является постоянная доступность пользовательского интерфейса для взаимодействия. Он не подвисает, ни замирает, ожидая окончания работы какой-либо длительной задачи, а продолжает работать.

Минусом такого подхода является сложность в возврате результата работы асинхронной задачи в пользовательский интерфейс. Поскольку интерфейс не знает, когда асинхронный поток закончит работу, поток должен сам производить вывод результата своей работы в интерфейс, как только этот результат у него будет готов. Кроме того, он должен иметь возможность периодически сообщать интерфейсу о ходе своей работы (например, двигать прогрессбар).

Всё это осуществляется соответствующими классами (Thread, ThreadPoolExecutor, Runnable) и приёмами программирования, но выглядит относительно сложно, поскольку во избежание конфликтов потоков просто так нельзя из них манипулировать интерфейсом. Приходится прибегать к дополнительным конструкциям. Вот в этом случае и помогает AsyncTask.

Класс AsyncTask является обёрткой над классами потоков и облегчает работу с ними. Класс содержит несколько переопределяемых методов, из которых обязательным для переопределения является метод doInBackground(), содержимое которого, собственно, и будет выполняться в отдельном потоке. Также достаточно часто используемыми являются методы onPreExecute(), onProgressUpdate() и onPostExecute(), вызываемые перед запуском потока, по команде из потока, и после окончания работы потока соответственно, из которых, собственно, и происходит манипулирование интерфейсом (мы ведь помним, что непосредственно из потока манипулировать интерфейсом нельзя).


Особенности работы AsyncTask в разных версиях Android.

До версии Android 1.5 (т.е. до API 3) AsyncTask вообще назывался UserTask. Вплоть до версии 1.6 (т.е. до API 4) его поведение было следующим. При создании и запуске нескольких объектов класса AsyncTask на выполнение, они все выполняли переопределённое содержимое своего метода onPreExecute(), выполняемого перед запуском потока, а их методы doInBackground() вставали в очередь. В каждый момент времени выполнялся только один метод doInBackground(), а остальные ждали завершения работы предыдущего.

Таким образом, фактически существовал один длинный поток, представляющий собой несколько мелких потоков, выстроившихся в очередь. Размер такой очереди был ограничен только наличием свободной памяти в телефоне.
Такой вариант последовательной работы был во многих случаях удобен, поскольку всегда было известно, что результаты работы появятся в той же последовательности, в которой потоки были запущены. Но это было не очень правильно с точки зрения многопоточности как таковой.

Начиная с версии 1.6 (API 4) логика работы AsyncTask была изменена. Теперь первые 5 потоков стали запускаться параллельно, и только начиная с 6-го, они начинали становиться в очередь. Если очередь, ограниченная 10 потоками, заканчивалась (5 запущенных потоков +10 в очереди), 16-й и каждый последующий потоки начинали выполняться параллельно остальным запущенным, а те 10 продолжали ждать своей очереди, но уже могли рассчитывать на места любых из уже работающих параллельно потоков (пяти и более). Максимальное ограничение количества одновременно работающих потоков — 128. Таким образом, можно запустить 138 потоков (128 + 10 в очереди). При превышении этого количества вызывается исключение RejectedExecutionException.

Помимо вышеперечисленных, в AsyncTask появился ещё один внутренний параметр, влияющий на его поведение. Это интервал, в течение которого резервируется место для выполнения новых параллельных потоков после завершения работы дополнительных параллельных потоков. То есть, места 6-го и последующих параллельно выполнявшихся потоков (если таковые были) не сокращаются снова до пяти, а предоставляются потокам из очереди или вновь появившимся потокам, если они успели появиться в течение этого интервала. До версии Android 2.3 (до API 9) этот интервал был равен 10 секундам, а начиная с версии 2.3 (API 9) — 1 секунда.

Практика программирования показала, что выгода от использования параллельности затмевалась валом ошибок, допускаемых программистами при реализации своих многопоточных алгоритмов, и, начиная с версии Android 3.0 (API 11), разработчики вернули прежнее поведение AsyncTask, хотя и добавили помимо стандартного метода execute() новый метод executeOnExecutor(), позволяющий всё же реализовывать прежнее параллельное поведение.

Возможно, важную роль в отказе от параллельного выполнения потоков в Android сыграли какие-то не совсем удобные для использования многопоточности особенности системы, а может быть, дело в низкой квалификации большого процента программистов, ринувшихся на новую платформу. Так или иначе, но при выполнении метода execute() запущенные потоки объектов AsyncTask снова вставали в очередь, которая теперь ограничивалась 128 потоками.

Всё это даёт разное поведение программ, активно использующих множественные вызовы потоков. Если один из потоков застревает, другие просто не выполняются. И это, однозначно, плохо. Конечно, в новых версиях можно использовать метод executeOnExecutor() с параметром AsyncTask.THREAD_POOL_EXECUTOR для параллельного выполнения или с параметром AsyncTask.SERIAL_EXECUTOR для последовательного выполнения потоков аналогично методу execute(). Но как быть, если требуется совместимость программы со старыми версиями, где этого метода ещё не было?


Свой класс AsyncTask

Можно написать свой класс AsyncTask, а точнее, взять его из новых версий Android и, если вы оставляете совместимость с Android 2.2 (API 8) и младше, убрать оттуда возможность последовательного выполнения потоков, поскольку он использует метод ArrayDeque, появившийся только в Android 2.3 (API 9), или же, использовать эту возможность в версиях Android с 2.3 до 3.0, где ранее её не было:

package com.example.testasynctask;

/**
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import java.util.ArrayDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import android.os.Handler;
import android.os.Message;
import android.os.Process;

/**
 * ### I delete this comments as it make the answer too long to submit ###
 */
public abstract class AsyncTask<Params, Progress, Result> {
    private static final String LOG_TAG = "AsyncTask";

    private static final int CORE_POOL_SIZE = 10;
    private static final int MAXIMUM_POOL_SIZE = 128;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(10);

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
            TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

    /**
     * An {@link Executor} that executes tasks one at a time in serial
     * order. This serialization is global to a particular process.
     */
    //    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static final int MESSAGE_POST_RESULT = 0x1;
    private static final int MESSAGE_POST_PROGRESS = 0x2;

    private static final InternalHandler sHandler = new InternalHandler();

    //    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR;
    private final WorkerRunnable<Params, Result> mWorker;
    private final FutureTask<Result> mFuture;

    private volatile Status mStatus = Status.PENDING;

    private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

    //    private static class SerialExecutor implements Executor {
    //        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    //        Runnable mActive;
    //
    //        public synchronized void execute(final Runnable r) {
    //            mTasks.offer(new Runnable() {
    //                public void run() {
    //                    try {
    //                        r.run();
    //                    } finally {
    //                        scheduleNext();
    //                    }
    //                }
    //            });
    //            if (mActive == null) {
    //                scheduleNext();
    //            }
    //        }
    //
    //        protected synchronized void scheduleNext() {
    //            if ((mActive = mTasks.poll()) != null) {
    //                THREAD_POOL_EXECUTOR.execute(mActive);
    //            }
    //        }
    //    }

    /**
     * Indicates the current status of the task. Each status will be set only once
     * during the lifetime of a task.
     */
    public enum Status {
        /**
         * Indicates that the task has not been executed yet.
         */
        PENDING,
        /**
         * Indicates that the task is running.
         */
        RUNNING,
        /**
         * Indicates that {@link AsyncTask#onPostExecute} has finished.
         */
        FINISHED,
    }

    /** @hide Used to force static handler to be created. */
    public static void init() {
        sHandler.getLooper();
    }

    /** @hide */
    public static void setDefaultExecutor(Executor exec) {
        sDefaultExecutor = exec;
    }

    /**
     * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
     */
    public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                return postResult(doInBackground(mParams));
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    final Result result = get();

                    postResultIfNotInvoked(result);
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                } catch (Throwable t) {
                    throw new RuntimeException("An error occured while executing "
                            + "doInBackground()", t);
                }
            }
        };
    }

    private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }

    private Result postResult(Result result) {
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

    /**
     * Returns the current status of this task.
     * 
     * @return The current status.
     */
    public final Status getStatus() {
        return mStatus;
    }

    /**
     * Override this method to perform a computation on a background thread. The
     * specified parameters are the parameters passed to {@link #execute} by the caller of this task.
     * 
     * This method can call {@link #publishProgress} to publish updates
     * on the UI thread.
     * 
     * @param params The parameters of the task.
     * 
     * @return A result, defined by the subclass of this task.
     * 
     * @see #onPreExecute()
     * @see #onPostExecute
     * @see #publishProgress
     */
    protected abstract Result doInBackground(Params... params);

    /**
     * Runs on the UI thread before {@link #doInBackground}.
     * 
     * @see #onPostExecute
     * @see #doInBackground
     */
    protected void onPreExecute() {
    }

    /**
     * <p>
     * Runs on the UI thread after {@link #doInBackground}. The specified result is the value returned by {@link #doInBackground}.
     * </p>
     * 
     * <p>
     * This method won't be invoked if the task was cancelled.
     * </p>
     * 
     * @param result The result of the operation computed by {@link #doInBackground}.
     * 
     * @see #onPreExecute
     * @see #doInBackground
     * @see #onCancelled(Object)
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    protected void onPostExecute(Result result) {
    }

    /**
     * Runs on the UI thread after {@link #publishProgress} is invoked.
     * The specified values are the values passed to {@link #publishProgress}.
     * 
     * @param values The values indicating progress.
     * 
     * @see #publishProgress
     * @see #doInBackground
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    protected void onProgressUpdate(Progress... values) {
    }

    /**
     * <p>
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished.
     * </p>
     * 
     * <p>
     * The default implementation simply invokes {@link #onCancelled()} and ignores the result. If you write your own implementation, do not call <code>super.onCancelled(result)</code>.
     * </p>
     * 
     * @param result The result, if any, computed in {@link #doInBackground(Object[])}, can be null
     * 
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    @SuppressWarnings({ "UnusedParameters" })
    protected void onCancelled(Result result) {
        onCancelled();
    }

    /**
     * <p>
     * Applications should preferably override {@link #onCancelled(Object)}. This method is invoked by the default implementation of {@link #onCancelled(Object)}.
     * </p>
     * 
     * <p>
     * Runs on the UI thread after {@link #cancel(boolean)} is invoked and {@link #doInBackground(Object[])} has finished.
     * </p>
     * 
     * @see #onCancelled(Object)
     * @see #cancel(boolean)
     * @see #isCancelled()
     */
    protected void onCancelled() {
    }

    /**
     * Returns <tt>true</tt> if this task was cancelled before it completed
     * normally. If you are calling {@link #cancel(boolean)} on the task,
     * the value returned by this method should be checked periodically from {@link #doInBackground(Object[])} to end the task as soon as possible.
     * 
     * @return <tt>true</tt> if task was cancelled before it completed
     * 
     * @see #cancel(boolean)
     */
    public final boolean isCancelled() {
        return mFuture.isCancelled();
    }

    /**
     * <p>
     * Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason. If successful, and this task has not started when <tt>cancel</tt> is
     * called, this task should never run. If the task has already started, then the <tt>mayInterruptIfRunning</tt> parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.
     * </p>
     * 
     * <p>
     * Calling this method will result in {@link #onCancelled(Object)} being invoked on the UI thread after {@link #doInBackground(Object[])} returns. Calling this method guarantees that {@link #onPostExecute(Object)} is never invoked. After invoking
     * this method, you should check the value returned by {@link #isCancelled()} periodically from {@link #doInBackground(Object[])} to finish the task as early as possible.
     * </p>
     * 
     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     *        task should be interrupted; otherwise, in-progress tasks are allowed
     *        to complete.
     * 
     * @return <tt>false</tt> if the task could not be cancelled,
     *         typically because it has already completed normally; <tt>true</tt> otherwise
     * 
     * @see #isCancelled()
     * @see #onCancelled(Object)
     */
    public final boolean cancel(boolean mayInterruptIfRunning) {
        return mFuture.cancel(mayInterruptIfRunning);
    }

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     * 
     * @return The computed result.
     * 
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     */
    public final Result get() throws InterruptedException, ExecutionException {
        return mFuture.get();
    }

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result.
     * 
     * @param timeout Time to wait before cancelling the operation.
     * @param unit The time unit for the timeout.
     * 
     * @return The computed result.
     * 
     * @throws CancellationException If the computation was cancelled.
     * @throws ExecutionException If the computation threw an exception.
     * @throws InterruptedException If the current thread was interrupted
     *         while waiting.
     * @throws TimeoutException If the wait timed out.
     */
    public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
            ExecutionException, TimeoutException {
        return mFuture.get(timeout, unit);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * 
     * <p>
     * Note: this function schedules the task on a queue for a single background thread or pool of threads depending on the platform version. When first introduced, AsyncTasks were executed serially on a single background thread. Starting with
     * {@link android.os.Build.VERSION_CODES#DONUT}, this was changed to a pool of threads allowing multiple tasks to operate in parallel. After {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this back to a single thread to
     * avoid common application errors caused by parallel execution. If you truly want parallel execution, you can use the {@link #executeOnExecutor} version of this method with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for
     * warnings on its use.
     * 
     * <p>
     * This method must be invoked on the UI thread.
     * 
     * @param params The parameters of the task.
     * 
     * @return This instance of AsyncTask.
     * 
     * @throws IllegalStateException If {@link #getStatus()} returns either {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
     */
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    /**
     * Executes the task with the specified parameters. The task returns
     * itself (this) so that the caller can keep a reference to it.
     * 
     * <p>
     * This method is typically used with {@link #THREAD_POOL_EXECUTOR} to allow multiple tasks to run in parallel on a pool of threads managed by AsyncTask, however you can also use your own {@link Executor} for custom behavior.
     * 
     * <p>
     * <em>Warning:</em> Allowing multiple tasks to run in parallel from a thread pool is generally <em>not</em> what one wants, because the order of their operation is not defined. For example, if these tasks are used to modify any state in common
     * (such as writing a file due to a button click), there are no guarantees on the order of the modifications. Without careful work it is possible in rare cases for the newer version of the data to be over-written by an older one, leading to
     * obscure data loss and stability issues. Such changes are best executed in serial; to guarantee such work is serialized regardless of platform version you can use this function with {@link #SERIAL_EXECUTOR}.
     * 
     * <p>
     * This method must be invoked on the UI thread.
     * 
     * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a
     *        convenient process-wide thread pool for tasks that are loosely coupled.
     * @param params The parameters of the task.
     * 
     * @return This instance of AsyncTask.
     * 
     * @throws IllegalStateException If {@link #getStatus()} returns either {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}.
     */
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

    /**
     * Convenience version of {@link #execute(Object...)} for use with
     * a simple Runnable object.
     */
    public static void execute(Runnable runnable) {
        sDefaultExecutor.execute(runnable);
    }

    /**
     * This method can be invoked from {@link #doInBackground} to
     * publish updates on the UI thread while the background computation is
     * still running. Each call to this method will trigger the execution of {@link #onProgressUpdate} on the UI thread.
     * 
     * {@link #onProgressUpdate} will note be called if the task has been
     * canceled.
     * 
     * @param values The progress values to update the UI with.
     * 
     * @see #onProgressUpdate
     * @see #doInBackground
     */
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

    private static class InternalHandler extends Handler {
        @SuppressWarnings({ "unchecked", "RawUseOfParameterizedType" })
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult result = (AsyncTaskResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

    private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }

    @SuppressWarnings({ "RawUseOfParameterizedType" })
    private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
}
Сам код небольшой, основной объём занимают комментарии. После добавления класса в проект просто удалите или закомментируйте в вашем коде импорт: import android.os.AsyncTask; и новый класс будет использоваться вместо штатного.

Кроме всего прочего, теперь мы можем сами менять поведение AsyncTask путём манипулирования его константами прямо в коде класса AsyncTask:
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAXIMUM_POOL_SIZE = 128;
    private static final int KEEP_ALIVE = 1;
Можем увеличить количество изначально одновременно выполняющихся потоков с 5 до, например, 10. Можем увеличить максимально возможное количество параллельно выполняющихся потоков (хотя это вряд ли нам может пригодиться), а также время резервирования места под новые потоки. Кроме того, мы можем изменить размер очереди с 10 на любое другое число в строке:
private static final BlockingQueue<Runnable> sPoolWorkQueue =
   new LinkedBlockingQueue<Runnable>(10);

Простейшее приложение для тестирования работы потоков

Для наглядного изучения поведения потоков можно написать следующее приложение. Создадим приложение Android с активити, содержащей одну кнопку, которой мы будем запускать новые потоки:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/button1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="Добавить новый поток" />

</LinearLayout>
В коде обработаем нажатие на кнопку. По нажатию будем создавать и запускать новый поток, который будет создавать новое TextView, устанавливать его в Layout и в течение заданного времени выдавать в него каждую секунду увеличивающееся на единицу число, после чего TextView будет уничтожаться:
package com.example.testasynctask;

import android.app.Activity;
//import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.TextView;

public class MainActivity extends Activity implements OnClickListener { // Наследуем класс Activity и добавим интерфейс OnClickListener  
    LinearLayout linearLayout;                                          // Переменная для хранения ссылки на LinearLayout

    // Создадим активити
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);                     // Вызовем родительский класс создания активити
        setContentView(R.layout.activity_main);                 // Создадим интерфейс согласно разметке в xml-файле
        linearLayout = (LinearLayout) findViewById(R.id.LinearLayout1); // Найдём в интерфейсе LinearLayout  
    }                                                                   // для добавления в него в дальнейшем TextView

    // Переопределим метод интерфейса OnClickListener для обработки нажатия кнопки
    @Override
    public void onClick(View view) {
        (new AsyncTask<Integer, Integer, Void>() {              // Создадим новый объект AsyncTask по классу, описанному прямо здесь 
            TextView textView;                                  // Переменная для хранения ссылки на созданный TextView

            // Выполняется в потоке интерфейса сразу после запуска AsyncTask перед выполнением потока
            @Override
            protected void onPreExecute() {
                super.onPreExecute();                           // Вызовем родительский метод
                textView = new TextView(MainActivity.this);     // Создадим TextView
                // Создадим объект для хранения параметров разметки TextView и зададим их
                LayoutParams lpView = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                textView.setLayoutParams(lpView);               // Присвоим параметры TextView
                linearLayout.addView(textView);                 // Добавим созданное TextView в LinearLayout
                textView.setText("...");                        // Выведем многоточие в TextView
            }

            // Выполняется в отдельном потоке
            @Override
            protected Void doInBackground(Integer... arg) {     
                for (int i = arg[0]; i >= 0; i--) {             // Цикл с количеством итераций, заданным при запуске потока
                    publishProgress(i);                         // Запустить метод onProgressUpdate, выводящий число i в TextView
                    try {
                        Thread.sleep(1000);                     // Задержка в 1000 миллисекунд
                    } catch (InterruptedException e) {
                    }
                }
                return null;
            }

            // Выполняется в потоке интерфейса по команде publishProgress(), дающейся в отдельном потоке
            @Override
            protected void onProgressUpdate(Integer... values) {
                super.onProgressUpdate(values);                 // Вызовем родительский метод
                textView.setText(values[0].toString());         // Вывести число i в TextView
            }

            // Выполняется в потоке интерфейса после окончания работы отдельного потока
            @Override
            protected void onPostExecute(Void arg) {
                super.onPostExecute(arg);                       // Вызовем родительский метод
                linearLayout.removeView(textView);              // Удалим TextView из интерфейса
            }

        }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 20);   // Выполнить объект асинхронного потока в параллельном режиме,
    }                                                               // передав ему число 20 в качестве параметра количества циклов
}
Добавим в приложение нашу реализацию класса AsyncTask, запустим его, и будем нажимать на кнопку. Мы увидим, как после каждого нажатия под кнопкой будет появляться новая строчка с бегущим числом. Числа в новых строчках будут бежать, пока количество строчек не достигнет константы количества одновременно запущенных потоков CORE_POOL_SIZE, по умолчанию равняющейся 5. Затем начнут появляться строчки с многоточиями — это потоки, стоящие в очереди. Их будет 10 штук. Затем снова начнут появляться строчки с бегущими цифрами, потому что очередь переполнена. Всё так, как и было описано мной ранее.

среда, 6 марта 2013 г.

Хеш строки

/**
 * Получить хеш строки entity по алгоритму HmacSHA256 с шумом salt.
 * @param entity Строка для хеширования
 * @param salt строка шума (salt)
 * @return Строка шестнадцатеричного хеша
 * @throws Exception
 */
public static String getHmac(String entity, String salt) throws Exception {
 Mac mac = Mac.getInstance("HmacSHA256");
 mac.init(new SecretKeySpec(salt.getBytes(), "HmacSHA1"));
 byte[] bs = mac.doFinal(entity.getBytes());
 return bin2hex(bs);
}

// Перевод байтов в шестнадцатиричную строку
static String bin2hex(byte[] data) {
 return String.format("%0" + (data.length * 2) + 'x', new BigInteger(1, data));
}

Сравнение строк в Java

Очень много ошибок возникает от того, что мы забываем, что строки – это объекты, а не примитивы. Поэтому они и ведут себя как объекты. И даже тип данных String пишется с большой буквы, как пишутся и другие объектные типы в отличие от примитивных типов (boolean, char, byte, short, int, long, float, double).

Замечу, что примитивные типы данных имеют своих дублёров в виде классов-обёрток, которые представляют эти типы, как объекты, снабжая их необходимыми методами. Поскольку такие типы являются объектами, они и пишутся с большой буквы (Boolean, Character, Byte, Short, Integer, Long, Float, Double).

Итак, как же правильно сравнивать строки? Изначально хочется написать такое:
if (sting1 == string2) {} 
Но поскольку строки — это объекты, а не примитивы, то вышеприведённой строкой мы сравниваем содержимое ссылок на строки, то есть адреса, по которым они располагаются в памяти. А так как два объекта не могут располагаться по одному и тому же адресу, то такое условие всегда будет возвращать false.

Чтобы сравнить строки, надо использовать метод equals() одной из сравниваемых строк:
if (sting1.equals(string2)) {} 
Следует обязательно использовать метод той строки, которая инициализирована реальным адресом, а не нулевым указателем, поскольку при вызове метода несуществующего объекта произойдёт исключение NullPointerExeption. Проверка на существование строки осуществляется так:
if (sting1 == null) {} 
То есть, мы проверяем, имеется ли в данной ссылке какой-нибудь реальный адрес, или она инициализирована значением null. Такую проверку надо обязательно производить при получении данных из полей интерфейса, файлов настроек, баз данных и пр., поскольку там не всегда могут оказаться нужные вам данные.

Кроме проверки наличия самого строкового объекта можно осуществлять также проверку наличия содержимого в этом объекте. Такая проверка может осуществляться несколькими способами:
if (string1.length() == 0) {} 
if (string1.isEmpty()) {} 
if (string1.equals("")){} 
Первый вариант самый предпочтительный — просто проверяется длина строки в объекте строки. Можно добавить к проверке удаление начальных и конечных пробелов: (string1.trim().length() == 0).

Второй вариант возможен только начиная с версии API 9 (Android 2.3), но, по сути, это тот же первый вариант, записанный более наглядно.

Третий вариант самый плохой. Такая конструкция работает медленнее остальных, потому что в ней создаётся дополнительный объект пустой строки "", с содержимым которого и производится сравнение.


Полезный пример

Часто требуется одновременная проверка строки на существование и на пустоту. Для этого можно использовать следующий метод:
public static boolean isZero(final String string) 
{ 
    return (string == null) || (string.trim().length() == 0); 
} 

Неизменяемость объекта строки

Замечу, что создание нового строкового объекта происходит всякий раз, когда мы присваиваем строку переменной:
String string1 = "Строка"; 
Объект строки является неизменяемым, и при попытке его изменить просто создаётся новый объект с новым значением:
String string1 = "Строка"; 
string1 = string1 + "Дополнительная строка"; 
или
String string1 = "Строка"; 
string1 = "Другая строка"; 
После выполнения этих действий в обоих случаях string1 будет указывать на совсем другую область памяти (на другой объект), чем он указывал до этого в первых строчках примеров.

понедельник, 4 марта 2013 г.

Отслеживание изменений в EditText

Как выяснилось, среди слушателей (Listeners) визуального компонента EditText нет такого крайне востребованного слушателя, как слушатель изменения данных внутри компонента. У него имеются лишь следующие 11 слушателей:

OnClickListener - срабатывает при щелчке по полю ввода.
OnCreateContextMenuListener - срабатывает при построении контекстного меню.
OnDragListener - срабатывает при событиях перетаскивания отпускания.
OnEditorActionListener - срабатывает при нажатии Enter и при других команд редактирования, но не срабатывает на ввод обычных символов.
OnFocusChangeListener - срабатывает при потере или получении фокуса компонентом
OnGenericMotionListener - срабатывает при перемещении мыши над компонентом
OnHoverListener - срабатывает при возникновении события "hover", т.е. когда при наведении мыши изменяется внешний вид компонента.
OnKeyListener - срабатывает при нажатии кнопок аппаратной клавиатуры. При нажатии кнопок программной клавиатуры, как правило, не срабатывает.
OnLongClickListener - срабатывает при длительном удержании нажатия на компоненте
OnSystemUiVisibilityChangeListener - срабатывает при изменении видимости статусной строки в пользовательском интерфейсе системы.
OnTouchListener - срабатывает при касании компонента. При обработке этого события у меня почему-то зависала обработка события OnLongClickListener.

Как видите, среди них нет ничего, что могло бы отследить элементарные изменения текста внутри компонента. Но помимо методов установки слушателей "setOn...", у компонента  EditText есть интересный метод добавления слушателя с названием addTextChangedListener! Почему вместо установки слушателя, как сделано в 11 случаях, используется его добавление (в единственном случае) - мне непонятно.Тем не менее, используем эту возможность.

В качестве слушателя метод предлагает нам установить интерфейсный класс: TextWatcher. Как мы знаем, интерфейсный класс - это класс с пустыми методами, которые заполняются самим программистом. Методов в классе три: beforeTextChanged, onTextChanged и afterTextChanged. При возникновении события изменения текста они автоматически вызываются в определённом порядке. Их названия говорят сами за себя:
 
beforeTextChanged(CharSequence s, int start, int count, int after) - метод вызывается до изменений, чтобы уведомить нас, что в строке s, начиная с позиции start вот-вот будут заменены count символов, новыми after символами. Изменение текста s в этом методе является ошибкой.

onTextChanged(CharSequence s, int start, int before, int count) - метод вызывается, чтобы уведомить нас, что в строке s, начиная с позиции start, только что заменены after символов, новыми count символами. Изменение текста s в этом методе является ошибкой.

afterTextChanged(Editable s) - метод вызывается, чтобы уведомить нас, что где-то в строке s, текст был изменен. В этом методе можно вносить изменения в текст s, но будьте осторожны, чтобы не зациклиться, потому что любые изменения в s рекурсивно вызовут этот же метод.

Итак, пример подключения слушателя изменения текста к компоненту EditText:
editText1.addTextChangedListener(new TextWatcher(){
    @Override
    public void afterTextChanged(Editable s) {
        // Прописываем то, что надо выполнить после изменения текста
    }
    
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }
});

воскресенье, 3 марта 2013 г.

Передача объектов в Java – по ссылке или по значению?


В Интернете до сих пор ломают копья в жарких спорах о том, как передаются объекты в качестве параметров в методы в языке Java. Кто-то говорит, что объекты передаются по ссылке, кто-то, что по значению, а кто-то — что передаётся ссылка по значению. Эта статья призвана пролить свет на причину этих споров, и поставить, наконец, в них точку.

Ссылки в Java

Официальная документация называет их ссылками. На самом деле это новая сущность, больше похожая на указатели с автоматическим разыменовыванием, как у ссылок, чтобы создать видимость работы с самим объектом, а не с указателем на него. Классические ссылки, в отличие от указателей, нельзя переопределять, приравнивать нулю и пр. А в языке Java — пожалуйста. Получился эдакий псевдоним объекта на базе указателя. Но раз разработчики определили название этой новой сущности, как ссылка, тогда давайте этим термином в контексте языка Java и пользоваться в дальнейшем.

Пару слов об указателях и их отсутствии в Java. На самом деле, указатели в Java имеются, но они работают на уровне виртуальной машины, и в синтаксисе языка не реализованы. Именно оттуда, с нижнего уровня, в случае обращения по нулевой ссылке генерируется исключение NullPointerException, которое переводится именно как «исключение нулевого указателя».

Передача параметров в метод

Давайте разберёмся, как вообще может передаваться что-либо в метод? Очевидно, что только двумя путями — либо путём передачи фактического значения передаваемой сущности (достигается созданием копии этой сущности внутри метода), либо путём передачи числа, интерпретируемого, как адрес памяти, по которому эта сущность фактически располагается (внутри метода, естественно, тоже создаётся копия этого числа). Первый вариант называется передачей параметра по значению, второй — по адресу (по ссылке, указателю или иной адресной сущности — это уже детали). Всё. В параметре либо сам объект, либо адрес памяти.

Поскольку поведение ссылок в Java не совсем совпадает с поведением классических ссылок, привычных, например, «сишникам», коих, наверное, большинство среди программистов, родилось определение, выносящее новичкам мозг: «передача ссылки по значению» вместо всем понятного «передача параметра по ссылке». В чём же разница? Что этим хотят подчеркнуть искушённые программописатели?

В общих чертах разница между этими понятиями в том, что любая Java-ссылка, в том числе и переданная в метод, ведёт себя не как классическая ссылка в других языках, которую единожды создав, невозможно переопределить, а отчасти как указатель, т.е. как переменная, просто хранящая значение адреса, которое всегда можно поменять. Кроме того, мы знаем, что при передаче параметров в метод создаются копии этих параметров. Так что получается, что если внутри метода мы попытаемся переопределить переданную ссылку, то внешняя ссылка не изменится, а поменяется только значение её копии внутри метода. Всё так же, как и с параметрами, переданными по значению. Вот этот нюанс и хотят подчеркнуть, когда говорят о передаче ссылки по значению.

Но коль уж мы называем Java-ссылки ссылками, то не стоит тут же в рамках одного языка отражать в терминологии особенности их поведения, если других вариантов в этих рамках всё равно нет. Вряд ли стоит очевидную передачу параметров по ссылке называть загадочной передачей ссылки по значению, пытаясь как-то указать на её особенности. Ведь это не передача такая (по значению), это ссылки такие (которые можно переопределить). А копия адреса внутри метода возникает чисто технически в любом языке (а где ещё метод будет хранить переданный ему параметр?), так что в любом языке ссылки передаются по значению. Просто их переопределить нельзя.

Примеры

Проиллюстрирую сказанное на примерах. Ниже представлен классический пример неудачной попытки переопределить ссылку внутри метода:
public class HelloWorld {

    public static void main(String[] argvc) {
        String test = new String("Hello World!");
        changeIt(test);
        System.out.println("После изменения: " + test);
    }

    static void changeIt(String value) {
        value = new String("Hello!");
    }
}
В первой строке основного метода main мы создали новую строку "Hello World!". Во второй строке мы вызываем метод changeIt, передавая ему эту строку для изменения. Поскольку строки в Java – это объекты, а объекты в Java всегда передаются по ссылке, то строка эта передалась в метод changeIt тоже по ссылке — во внутренней переменной value находится ссылка (в терминологии Java) на строку "Hello World!".

Внутри метода changeIt первой и единственной строкой мы создаём новую строку "Hello!", и думаем, что записываем её адрес во внешнюю ссылку, переданную внутрь метода, ранее содержащую строку "Hello World!". Либо мы можем подумать, что таким образом мы записываем вновь созданный объект строки по адресу, хранящемуся в этой ссылке.

И то и другое, конечно, неверно. На момент присваивания объект уже создан и уже находится где-то в памяти, и перегонять его по адресу, сохранённому в ссылке, большого смысла нет. А что касается первого варианта, то изнутри метода мы не имеем доступа к переменной, хранящей внешнюю ссылку, чтобы её изменить, поскольку работаем с копией этой переменной.

Таким образом, вышеприведённый такой код записывает адрес новой строки не во внешнюю ссылку, а в копию этой ссылки, автоматически созданную внутри метода во время передачи параметров. Именно поэтому третья строка основного метода распечатает старое значение "Hello World!"

Тот же пример, но ещё менее очевидный:
public class HelloWorld {

    public static void main(String[] argvc) {
        String test = "Hello World!";
        changeIt(test);
        System.out.println("После изменения : " + test);
    }

    static void changeIt(String value) {
        value = "Hello!";
    }
}
Здесь мы проделаем всё то же самое, но отсутствие явной команды создания нового объекта строки может ввести в заблуждение, что новый объект не создаётся, а редактируется старый. Это было бы так, и код бы работал так, как задумывалось, если бы объекты строк были бы изменяемы (мутабельны [mutable], как любят говорить программисты). Но в Java строки – это константы (неизменяемые объекты [Immutable]). Любым образом пытаясь изменить содержание строки мы фактически создаём новый объект строки по новому адресу памяти. Поэтому, пытаясь записать новое слово "Hello!" в старый объект, мы фактически создаём новый объект строки, адрес которой просто присваиваем копии переданной ссылки, который просто теряется при выходе из этого метода.

Итак, как же правильно называть способ передачи объекта в Java? Называйте его в терминах Java — просто передачей по ссылке. По крайней мере, это не будет вносить путаницы в головы новичков или программистов, у которых Java – первый язык.