вторник, 24 ноября 2009 г.

Старое заблуждение о выносе локальной переменной за пределы цикла

Очень старое заблуждение, которое активно пропагандируется некоторыми некомпетентными преподавателями вузов. Заблуждение иллюстрируется следующим кодом:
    public void test1()
    {
        for (int i = 0; i < 20; i++)
        {
            int j = i;
            j++;
        }
    }

    public void test2()
    {
        int j;
        for (int i = 0; i < 20; i++)
        {
            j = i;
            j++;
        }
    }
Считается, что нужно самому выносить локальную переменную из цикла, иначе память под неё будет выделяться на каждой итерации. На самом деле это совсем не так. Компилятор без труда оптимизирует данный кусочек кода. Память будет выделена всего лишь один раз. Причем, это касается не только Java, но и других языков, таких как C, C++ и т.д. Это типичная оптимизация.
Для сомневающихся привожу байт-код обоих методов:
 // Stack: 2, Locals: 3
  public void test1();
     0  iconst_0
     1  istore_1 [i]
     2  goto 13
     5  iload_1 [i]
     6  istore_2 [j]
     7  iinc 2 1 [j]
    10  iinc 1 1 [i]
    13  iload_1 [i]
    14  bipush 20
    16  if_icmplt 5
    19  return
      Line numbers:
        [pc: 0, line: 14]
        [pc: 5, line: 16]
        [pc: 7, line: 17]
        [pc: 10, line: 14]
        [pc: 19, line: 19]
      Local variable table:
        [pc: 0, pc: 20] local: this index: 0 type: my.test.ByteCodeTest
        [pc: 2, pc: 19] local: i index: 1 type: int
        [pc: 7, pc: 10] local: j index: 2 type: int
      Stack map table: number of frames 2
        [pc: 5, append: {int}]
        [pc: 13, same]
  
  // Method descriptor #6 ()V
  // Stack: 2, Locals: 3
  public void test2();
     0  iconst_0
     1  istore_2 [i]
     2  goto 13
     5  iload_2 [i]
     6  istore_1 [j]
     7  iinc 1 1 [j]
    10  iinc 2 1 [i]
    13  iload_2 [i]
    14  bipush 20
    16  if_icmplt 5
    19  return
      Line numbers:
        [pc: 0, line: 24]
        [pc: 5, line: 26]
        [pc: 7, line: 27]
        [pc: 10, line: 24]
        [pc: 19, line: 29]
      Local variable table:
        [pc: 0, pc: 20] local: this index: 0 type: my.test.ByteCodeTest
        [pc: 7, pc: 13] local: j index: 1 type: int
        [pc: 2, pc: 19] local: i index: 2 type: int
      Stack map table: number of frames 2
        [pc: 5, full, stack: {}, locals: {my.test.ByteCodeTest, _, int}]
        [pc: 13, same]
Как мы видим, память выделяется под 2 переменные.

понедельник, 23 ноября 2009 г.

Подсчет временной разницы между двумя датами

Возникла такая задача: подсчитать разницу, прошедшую между двумя датами и уметь выводить её в годах, месяцах, днях и т.д., а также в такой форме "1 день 2 месяца 3 года".

При решении данной задачи нужно учитывать, что нельзя просто взять и вычесть количество миллисекунд одной даты из другой. Таким образом мы не можем в точности посчитать ничего, т.к. существует переход на летнее/зимнее время. Например, мы вычтем кол-во мс из даты, созданной на 30 марта 2009 года(установим также 0 часов, минут, секунд и миллисекунд) и 29 марта 2009 года(также обнулим лишние поля). Если перевести получившееся количество миллисекунд в часы, то мы получим 23 часа, а должно быть 24. Почему? Потому что именно в это время у нас переводят на зимнее время. Соответственно, необходимо учитывать данный факт.

То есть для вычисления мс, прошедших между двумя датами нужно использовать следующий метод:
public static long getDiffInMillis(Calendar c1, Calendar c2)
    {
        // учитываем перевод времени
        long time1 = c1.getTimeInMillis() + c1.getTimeZone().getOffset(c1.getTimeInMillis());
        long time2 = c2.getTimeInMillis() + c2.getTimeZone().getOffset(c2.getTimeInMillis());
        return Math.abs(time1 - time2);
    }

Таким образом мы легко получим кол-во прошедших часов, минут и секунд. Но если мы хотим получить кол-во прошедших дней, не учитывая часы, минуты и прочее, то нам необходим следующий метод:
public static int getDiffInDays(Calendar c1, Calendar c2)
    {
        Calendar gc1 = (Calendar)c1.clone();
        Calendar gc2 = (Calendar)c2.clone();

        // обнуляем часы, минуты и секунды
        gc1.set(Calendar.HOUR, 0);
        gc2.set(Calendar.HOUR, 0);
        gc1.set(Calendar.SECOND, 0);
        gc2.set(Calendar.SECOND, 0);
        gc1.set(Calendar.MILLISECOND, 0);
        gc2.set(Calendar.MILLISECOND, 0);

        return (int)(getDiffInMillis(c1, c2) / MILLISECONDS_IN_A_DAY);
    }

Но используя данную методику нельзя сказать сколько прошло месяцев и лет. Для этого мы будем использовать следующий метод:
/**
     * 

Высчитывает разницу между двумя временами, в зависимости от значения field. * field - это константное значение из java.util.Calendar, обозначающее * конкретную временную метрику(год, месяц и т.д.).

* *

Устанавливает поля, заданные в массиве clearFields равными 0.

* * @param field * константное значение из java.util.Calendar(YEAR, MONTH...) * @param c1 * первая дата * @param c2 * вторая дата * @param clearFields * поля даты, которые не учитываются при сравнении. Задаются массивом констант из из java.util.Calendar. * Например, можно не учитывать миллисекунды, секунды и часы. Данные поля будут обнулены. * @return * разницу между c1 и c2 в метрике, заданной field */ public static int getTimeDifference(int field, Calendar c1, Calendar c2, int... clearFields) { Calendar gc1, gc2; if (c2.after(c1)) { gc1 = (Calendar)c1.clone(); gc2 = (Calendar)c2.clone(); } else { gc1 = (Calendar)c2.clone(); gc2 = (Calendar)c1.clone(); } if (clearFields != null) { // очищаем поля, которые мы не будем учитывать при сравнении дат for (int clearField : clearFields) { gc1.clear(clearField); gc2.clear(clearField); } } int count = 0; for (gc1.add(field, 1); gc1.compareTo(gc2) <= 0; gc1.add(field, 1)) { count++; } return count; }
Как мы видим, данный метод просто пытается прибавить 1 к заданному временному промежутку. В clearFields мы задаем поля, которые необходимо обнулить. Таким образом мы можем найти разницу не только в годах и месяцах, но и в чем угодно, но это будет неэффективно. Следующей задачей является вывод нужного слова для временной единицы. Например, мы говорим прошло "5 лет", "2 месяца", "1 год", "11 секунд" и т.д. Оказывается, что все названия временных промежутков подчиняются единому закону. Достаточно всего лишь трех слов :). Этот закон выражен данным кодом:
/**
     * Возвращает слово, добавляющееся к временной единице, когда указывается сколько прошло
     * временных единиц.
     * В русском языке для временных единиц достаточно трех слов.
     * Значение time берется по модулю.
     * 
     * @param time
     *            количество временной единицы
     * @param timeUnitName1
     *            Название временной единицы, которое используется с 1. Например, 1 "минута".
     * @param timeUnitName2
     *            Название временной единицы, которое используется с 2. Например, 2 "минуты".
     * @param timeUnitName5
     *            Название временной единицы, которое используется с 5. Например, 5 "минут".
     * @return
     *         слово для единицы времени
     */
    public static String getTimeUnitPeriodName(long time, String timeUnitName1, String timeUnitName2, String timeUnitName5)
    {
        String result;
        time = Math.abs(time); // для отрицательных чисел
        int small = (int)(time % 10);
        int middle = (int)(time % 100);

        // если заканчивается на 11, то всегда оформляется словом timeUnitName5
        if (small == 1 && middle != 11)
        {
            result = timeUnitName1;
        }
        // если оканчиваются на 2, 3, 4, за исключением 12, 13 и 14
        else if (small >= 2 && small <= 4 && (middle < 12 || middle > 14))
        {
            result = timeUnitName2;
        }
        else
        {
            result = timeUnitName5;
        }

        return result;
    }
Оказалось, что так устроен наш язык, что особыми временными единицами являются 11, 12, 13 и 14(точнее оканчивающимся на эти числа). Остальное подчиняется общим правилам. То ли дело в английском языке. Теперь мы можем считать промежуток между двумя датами в любой временной единице и находить для данной единицы нужное слово. Осталась ещё одна нерешенная задача: как выводить временной промежуток в формате "X дней Y месяцев Z лет", но делать это так, чтобы у нас не было ситуации "12 дней 13 месяцев 1 год", а было например так "12 дней 1 месяц 2 года". Т.е. у нас не может быть больше 12 месяцев или больше 31 дня. Данную задачу мы решим следующим образом. Будем находить разницу в годах и затем вычитать данную разницу из конца промежутка. Дальше поступим также для месяца. Оставшиеся дни же просто выведем. Вот кусок кода(from и to - экземпляры Calendar):
// если конечная дата больше, то меняем их местами
        if (from.after(to))
        {
            to = from;
        }

        int diffInYears = DateUtils.getDiffInYears(from, to);
        to.add(Calendar.YEAR, -diffInYears); // вычитаем учтенные года

        int diffInMonths = DateUtils.getDiffInMonths(from, to);
        to.add(Calendar.MONTH, -diffInMonths); // вычитаем учтенные месяцы

        int diffInDays = DateUtils.getDiffInDays(from, to);
Не забудьте, что вы вычитаете из даты, поэтому нужно клонировать переданный экземпляр Calendar. Ну а далее форматируем полученные числа по своему желанию.

p.s. также не забывайте указывать первый день недели на понедельник(в России) - calendar.setFirstDayOfWeek(Calendar.MONDAY)

пятница, 20 ноября 2009 г.

Успех

Есть люди, которые работают быстро и качественно. Результаты таких людей просто потрясают. Фактически пара людей может написать достаточно большую и сложную систему. Пара! Причем сделать это, например, за полгода или меньше. Пример перед глазами. Это круто. Конечно же речь идет не о написании бизнес-лапши. Ибо бизнес лапша на моих весах находится где-то в соотношении 100:1. 100 классов бизнес-лапши равны одному утилитарному(системному) классу.
Вообще рекомендую ввести метрики для подсчета своей эффективности. А также для интереса подсчитать коллег. Соответственно следовать своей метрике и улучшать её.
Мне очень кажется, что увольняться с работы стоит тогда, когда ты в принципе достиг на ней хороших результатов. Дальше уже или скучно или некуда. То есть пока не уволился, достигай результатов. Ставь цель и достигай её. Иначе будешь отвечать на тупые вопросы на знание Java вместо того, чтобы рассказывать о своих успехах на предыдущей работе. Например, одной из метрик может быть кол-во комментариев до и после тебя. Или ты сделал так, что делать некоторую рутинную задачу стало в 2 раза быстрее. Либо ты увеличил скорость некоторого участка в 3 раза. Разница должна выражаться в разах. Возможно ты так отрефакторил класс, что он стал в 2 раза короче, а его использование стало в 4 раза проще. Может быть ты наладил систему тестирования и обеспечил этим в 3 раза меньшее кол-во багов.