Как стать автором
Обновить

Комментарии 91

Что-то я, после перехода на С#, все больше разочаровываюсь в generic'ах Java. Хотели как лучше, а получилось как всегда?
Эм… а причём тут генерики? Статья совершенно о другом.
Согласен, неудачно выразился.
Autoboxing, generics — это попытки сделать что-то кардинально по-своему в Java.
И обе неудачные, ИМХО.
Autoboxing — вполне логичен и понятен. И разделение на примитивные типы (если уж без них никуда) и объекты тоже вполне логично (все объекты — ссылки).
Вот с generics да, всё немного неприятно, но не смертельно.
Integer a = 1000;
Integer b = 1000;
int c = 1000;

a==c;//true
b==c;//true;
a==b;//false
Где тут логика?
Я понимаю, почему оно так работает, но логика где?
Java всегда была очень легким и понятным языком, но опять неприятно. Да, не смертельно, но неприятно.
Я написал выше, что понимаю, почему такой результат.
У Java была идеально-строгая типизация:
byte a=100,b=200;
byte c = (byte) a*b;

Но это было до того, как появился волшебный autoboxing.
Это просто непривычно. В Си же вы помните какая переменная указатель, какая нет. И подобный код в Си будет иметь такую же логику.

Но в Java нет заветного аперсанда и мозг воспринимает переменную как примитив, а не как ссылку =)

НЛО прилетело и опубликовало эту надпись здесь
Какое счастье, что в Java нельзя переопределить operator == :)
НЛО прилетело и опубликовало эту надпись здесь
Проблема в том, что одному приятнее чистота языка, тогда как другому — отсутствие явных приведений типов.
Вот почему использование autoboxing/autounboxing в наший проектах считается за глюк и подлежит исправлению.
Думаю, тут необходимо соблюдать разумные границы и единство стиля, тогда проблем не будет. Либо везде Integer, либо везде int.
У Java никогда не было идеально-строгой типизации, она просто статическая. Строгая (сильная) типизация у Ada, например.

По настоящему сильная строгая статическая типизация подразумевает отсутствие приведения типов вообще, оператора явного приведения типов, отсутствие неявных приведений типа (например, от short к int), и в пределе отсутствие возвможности присваивать ссылки вида ParentType x = ChildType().
Со строками там такая же фигня, можно попытаться сравнить две строки оператором ==, а потом удивляться, почему результат всегда false.

Логика понятна, она есть, но C# всё же намного интуитивнее.
Про оператор "==" уж совсем загнули. Так и должно быть, он сравнивает ссылки, а не значения, как и во всех остальных случаях. К этому не надо «привыкать», это надо просто понять.
Логика понятна, она есть, но C# всё же намного интуитивнее.
Вы так говорите, как будто это плохо (ц) Интуитивность желаннее логичности ровно до того момента, когда начинаешь понимать, как всё работает.
Мне это по барабану, я просто описал свой experience. В работе использую и Java и на C#, и ещё много других языков, и мне удобнее когда строки сравниваются по-человечески, а не по-роботски.

С чьей-то узкой колокольни возможно всё иначе, who cares.
логика в том, что тут нужно использовать .equals()
Что лучше:
Integer.equals(int -> Integer)
или
int == (Integer -> int)
?
Ну, с этим можно поспорить. Автобоксинг как ещё можно было сделать то? Тут просто надо с умом использовать. О чём и сказано в статье, собственно. А генерики — да, больная тема, но сделать их другими было тоже нельзя, об этом писано немало статей. Совсем неудачной попыткой я бы их точно не назвал.
а нафига его вообще делать?
Кого его? Автобоксинг? Ну, блин, удобно же в некоторых случаях.
разделение int и Integer, и автобоксинги в том числе
А как не разделять? Надо, чтобы примитивов совсем не было? Или к чему Вы клоните, не понимаю?
чтобы небыло Integer и ему подобных
Вы хотите, чтобы не было врапперов, но были примитивы?? Весьма оригинальная позиция. Обратные пожелания часты, но такое…
И как конкретно в Java всё это должно в итоге работать, позвольте узнать? Ну, например, при использовании в коллекциях? Ну, или как ключа у Map. Как примитивный тип там приделать?
а в чём сложность?
Я даже затрудняюсь ответить на ваш вопрос на вопрос… Вы, верно, просто не очень понимаете как устроены коллекции и, в частности, тот же HashMap. Сделать, чтобы примитив мог быть ключём невозможно без горождения кучи костылей. С другой стороны сейчас там всё просто, логично и вписывается в общие единые схемы. Все эти «недоделанные» генерики, врапперы и т.д. как раз и сделаны (и сделаны именно так), чтобы не ломать общую схему и оставить простоту в реализации.
ну значит надо выкинуть нафиг это кривой HasMap и сделать по уму.
Проблема с врапперами единственно в том, что они съедают много служебного места, потому что это классы.
Сложность в том, что на уровне JVM никаких генериков нет, а ломать совместимость со старой машиной ради этого никто не хотел. В результате пошли на компромисс — сделали генерики, но только для ссылочных типов (все они компилируются в один и тот же код без размножения). Это хуже, чем в .NET, но там на обратную совместимость первой и второй версий положили болт.
Нужно просто принять тот факт, что в Java генерики не такие, как в C++/#/… (ну, или наоборот). Они устроены по-другому и работают по-другому. Меньше возможностей, но совместимость + простота. Свою задачу типизации времени проектирования они отлично выполняют. При этом полностью совместимы с бородатыми JVM. Чего ещё надо? Я нахожу их довольно удобными и нужными. Можно ведь их не использовать кому не нравится. Проблем нет никаких с этим вроде…
Принять можно все (зарабатываешь на яве — куда ж ты денешься), такие генерики лучше чем их отсутствие, простоты в автобоксинге нет никакой. Но от всего этого этого ява как рабочий инструмент отнюдь не выигрывает.
Чего еще надо? Нормальных лямбд, замыканий, лаконичности, вывода типов, генериков для типов-значений без побочных эффектов оберток, нормального switch, банального using. Да, можно работать без всего этого, но brainfuck тоже Тьюриг-полон.
Автобоксинг нужен тогда, когда мы хотим чтоб value-type (int например) прожил чуть дольше, чем функция, где он присваивается, но без копирования.

Если для int-а вернуть копию int'а или ссылку не очень большая разница, то для больших по размеру value-type-ов уже намного важнее.

А вот разделение… действительно это хоть и синтаксически верно (чтоб отличить value-type от reference-type), но вот практически необходимость явного задания весьма сомнительны.

Автобоксинг на то и «авто» чтоб программист не замарачивался над этим.

Как вариант более красивого решения — «заворачивать» value-type во враперы только тогда, когда синтаксически подразумевается reference-type.
ленивое копирование толстых типов? нэ?
ленивое копирование незабоксенных value-type? Это как Вы себе представляете? :)

Вот собираемся выйти из функции, в стеке лежит значение (толстое)… нам надо чет с ним сделать… тут не скопируешь — потеряешь.

Так что ленивое копирование как замену боксингу совсем никак не подходит — нельзя ленится ну никак тут :)
примитивный тип и иммутабл объект ничем принципиально не отличаются. и жонглировать боксами тут глупо
Ну тогда вы получите python/ruby (или еще что-то подобное), где циферка «1» — полноценный объект со всеми вытекающими.

Примитивные типы в общем то и сделали для производительности — во фрейме они занимают определенное место известное в compile-time, что позволяет быстро проводить с ними операции. По этой же причине (фиксированность во фрейме) для примитивных типов не используется «сборщик мусора»

Уберем их — получим потерю производительности, недопустимую для тех времен и платформ, где ява работала.

А боксинг — это типа костыль чтоб могли юзать примитивы там, где нужны объекты. Хотя зачем делать явными враперы… тут уже хз.

Думаю тут можно дать старичку-яве скидку что он как бы один из первых был в своем роде и дать ему спокойно умереть под Ораклом. Фейл с генериками его и так уже почти добил.
не получу. там такая же тупая реализация как и в яве с яваскриптом. что мешает примитивному значению наследовать свойства из прототипа напрямую, без посредников?
Да удобно. Но! неоднозначно. И это перечеркивает, для меня, все плюсы от удобства.

1 Ведь сделали же Integer unmutable.
2 И int == Integer сравнивают по значениям.
3 И Integer ==Integer, если значения в диапазоне -128 до 127

Почему не сравнивать значения при всех остальных Integer>127?
Ну вот тут уже я понятия не имею зачем кешируется и почему именно в диапазоне -128 до 127…
костыль обыкновенный. сделали кривую архитерктуру, а потом приделывают к ней кучу костылей, чтобы не тормозило.
>>Заменим в прототипе int value на Integer value и запустим профайлер заново. В начале картина такая же:
А точно не наоборот???
Точно. Там же value передаётся туда Integer, а потом внутри метода делается add, который int обратно боксирует в Integer. Смысл замены — в избежании боксинга/унбоксинга.
Уточню, что реиспользуются только значения, которые умещаются в байт. Например следующий код:

Integer a = 100;
Integer b = 100;
Integer c = (int)Byte.MAX_VALUE;
Integer d = (int)Byte.MAX_VALUE;
Integer e = (int)Byte.MIN_VALUE;
Integer f = (int)Byte.MIN_VALUE;
Integer g = (int)Byte.MAX_VALUE — 1 + 1;
Integer h = (int)Byte.MAX_VALUE — 1 + 1;
Integer i = (int)Byte.MAX_VALUE + 1;
Integer j = (int)Byte.MAX_VALUE + 1;
System.out.println(a == b);
System.out.println(c == d);
System.out.println(e == f);
System.out.println(g == h);
System.out.println(i == j);

выдаст:
true
true
true
true
false
Если посмотреть код метода Integer.valueOf(int i), то всё станет понятно:
    public static Integer valueOf(int i) {
        if(>= -128 && i <= IntegerCache.high)
            return IntegerCache.cache[+ 128];
        else
            return new Integer(i);
    }
 


Судя по всему при автобоксинге происходит то же самое.
Автобоксинг, собственно, и вызывает этот метод.
Меня после первого абзаца, когда 50==50, а 500<>500 уже передёрнуло.

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

«Неинтуитивно» для кого? Для того, кто только что на Java сел писать? Для людей, перешедших с C++? Или для кого-то лично? Стремясь к «интуитивности» очень сложно перейти черту, когда получается неразбериха, путаница и разные подводные камни. Чего в C++, например, дофига (сразу скажу, я на C++ писал очень много). Сравнивать строки через == не «интуитивно», это просто неправильно, если просто принять тот факт, что это Java и всё тут. Сравнивать строки (точно также, как и прочие объекты без исключений) надо через equals. Если это кажется неинтуитивно (потому что в каком-то другом языке это не так), то сначала к этому нужно привыкнуть, а потом со временем понять почему именно так и возрадоваться, что не по-другому.
Что логично и понятно… но совершенно неинтуитивно.
Язык программирования (сейчас речь о языках ниши Java/C++/C# итд) и должен быть логичным и понятным, ибо это формальная система. В этом и есть смысл ЯП — наличие строгой семантики и синтаксиса. Иначе бы все писали на «интуитивном» русском/английском/… языке и не парились.
Сорри, туплю: «очень сложно перейти черту» -> «очень легко перейти черту».
«Неинтуитивно» означает «для понимания и правильной формулировки необходимо точное знание неочевидных особенностей инструмента». Это недостаток, вполне преодолимый, но, простите, «возрадоваться» тут нечему.
Упор на строгость языка при наличие неявного боксинга несколько непоследователен, не так ли?
Конечно, плюсы в этом плане хуже многократно (там практически ВСЕ требует точных знаний особенностей реализации), но и ява не без греха.
Неинтуитивно, неинтуитивно…
А использование указателей на массивы в Си интуитивно?

Вспомним, что Java уже живет достаточно долго, и является, фактически, переходником с C++. И создавался он не так масштабно (иначе бы шаблоны сразу включили в язык, и не парились бы потом с Generic'ами), просто потом оказалось, что без этого жить становится сложно.

Да, хотелось бы много чего, а много чего не хотелось бы… Может быть, ты именно тот, кто, наконец, напишет идеальный, интуитивный и быстрый язык, в котором не будет таких неприятных «мелочей», и все будет работать именно так, как ты ожидаешь?
Если ты или кто-то другой осилит такую задачу и не просадит производительности и т.д., то программерское сообщество его не забудет )
А пока такого языка нет — будем использовать то, что лучше подходит для наших текущих задач. И будем изучать и запоминать эти тонкости, чтобы не наступать на грабли. И будем ругать создателей, говнокодеров и самих себя за то, что чего-то где-то не учли из-за незнания особенностей реализации…
В хороших книжках языки программирования описываются как инструменты для достижения цели, а тут ведь такая штука, что одни обжимные клещи обжимают как привык (С++), а другие отрезают, если не воткнуть иголку в отверстие возле «гарды» (Java.equals).
Вот насчет плюсов, в которых перегружается все и вся… я бы про «как привык» говорить не стал.
Смотря что использовать ) бойся чужого кода, особенно если он работает )
НЛО прилетело и опубликовало эту надпись здесь
По умолчанию флаги EliminateAutoBox и DoEscapeAnalysis выключены в стандартных релизах Oracle JDK/JRE.
Чтобы их включить, нужно запустить java с параметром -XX:+AggressiveOpts
Этого не знал, спасибо.
Идея понятна, но пример не совсем удачный.
Писать for (Integer i = 0; ...) — дурной тон.
В данном примере лучше не в put заменить int -> Integer, а в цикле Integer -> int.
Как я написал, это не поможет. Тогда всё равно внутри put будут создаваться каждый раз новые объекты. Если не хочется использовать Integer, надо внутри put доставать Integer-объекты из своего кэша.
Когда возникла необходимость писать под GAE, выбирал между java и python. И хотя java в корпоративной среде, где много-много денег, используется намного чаще, подавляюще чаще, python покорил в самое сердце — только объекты и ссылки на объекты, примитивы — это такие же полноценные объекты. Очень четкая и понятная логика:
a = 1000 # a - ссылка на объект 1000
b = 1000 # b - ссылка на объект 1000
a == b # True - один и тот же объект
a += 1 # a - ссылка на объект 1001
a == b # Flase - разные объекты
a -= 1 # а снова ссылается на объект 1000


Know your tool better.
>>> a = 1000
>>> b = 1000
>>> c = a
>>> a == b
True
>>> a == c
True
>>> a is c
True
>>> a is b
False

Оператор == сравнивает значения, а не ссылки, то есть это аналог .equals() в Java.
Я где-нибудь написал, что == сравнивает ссылки? :) Это вы додумали, видимо вас смутило выражение «один и тот же объект»
В комментариях в вашем коде написано «один и тот же объект». Так вот это неправда.
Хмм… интересно. А почему вы так думаете?
Вы правы. Я офакапился. Это не работает для простых чисел больше 256. Если число меньше, тогда a is b == True.
По сути так же, как и в джаве :-)

> Это не работает для простых чисел больше 256.
Математики не простили бы столь вольное употребление выражения «простые числа» =)
И не только математики, но и программисты. :)
Также есть смутное подозрение что unboxing еще 2 раза работает в цикле: при сравнении с 100000 и при инкременте. Ну и как сказали выше писать for (Integer i = 0; ...) это моветон.
Да, подозрение подтвердилось:
        for(Integer i = Integer.valueOf(0); i.intValue() < 0x186a0;) // Первый unboxing
        {
            Integer key;
            for(Iterator i$ = getRandomKeys().iterator(); i$.hasNext(); put(key, i.intValue()))
                key = (Integer)i$.next();

            Integer integer = i;
            Integer integer1 = i = Integer.valueOf(i.intValue() + 1); // Второй unboxing
            Integer _tmp = integer;
        }
Короче я вас обманул. Разница при изменении заключается в том что исчезают 1 unboxing и 1 boxing, причем boxing происходит в методе put()
Сравните:
1 вариант
     public static void put(Integer key, int value)
    {
        if(!subSets.containsKey(key))
            subSets.put(key, new HashSet());
        ((Set)subSets.get(key)).add(Integer.valueOf(value)); // BOXING
    }
.......
        for(Integer i = Integer.valueOf(0); i.intValue() < 0x186a0;)
        {
            Integer key;
            for(Iterator i$ = getRandomKeys().iterator(); i$.hasNext(); put(key, i.intValue())) // UNBOXING
                key = (Integer)i$.next();

            Integer integer = i;
            Integer integer1 = i = Integer.valueOf(i.intValue() + 1);
            Integer _tmp = integer;
        }

и 2 вариант:
    public static void put(Integer key, Integer value)
    {
        if(!subSets.containsKey(key))
            subSets.put(key, new HashSet());
        ((Set)subSets.get(key)).add(value);
    }
........
        for(Integer i = Integer.valueOf(0); i.intValue() < 0x186a0;)
        {
            Integer key;
            for(Iterator i$ = getRandomKeys().iterator(); i$.hasNext(); put(key, i))
                key = (Integer)i$.next();

            Integer integer = i;
            Integer integer1 = i = Integer.valueOf(i.intValue() + 1);
            Integer _tmp = integer;
        }
> for (Integer i = 0; ...) это моветон.
Это некрасиво, но в данном случае оптимально. Как я отметил, можно завести свой кэш Integer-объектов и пользоваться им только внутри put, а снаружи везде использовать int. Но это дополнительные ненужные накладные расходы. Тут весь код — низкоуровневая реализация некого алгоритма, поэтому ему позволительно быть не очень красивым.
Лишние unboxing'и не так страшны, это всего лишь по сути разыменование указателя. А вот лишний boxing — это существенно более неприятная операция.
И некрасиво, и неоптимально.
Лучше будет так:
    for (int i = 0; i < xxx; i++) {
        Integer I = i;
        …
    }
Это действительно будет самый оптимальный вариант, на 2 unboxing меньше. Код можно посмотреть ниже.
unboxing практически ничего не стоит. Особенно на фоне boxing.
> Видим, что правило писать int везде, где можно не писать Integer, работает не всегда: в каждом отдельном случае надо думать.
Оно как раз работает, если его все время применять. В частности, для счетчика цикла.
Перечитайте внимательно. Вообще в изначальном коде так и было — в обоих местах (и в for, и в прототипе put) был int, а память под Integer кушалась тоннами. Надо исправить оба места на Integer, чтобы эффект исчез. Возможно, мне не стоило видоизменять пример, чтобы оставить только одно узкое место…
А, все, понял. Не заметил сначала, что там каждое i много раз добавляется.

А что будет если в цикле оставить int, а внутри цикла создавать Integer? Как это повлияет на быстродействие i++?
Теоретически два unboxing'а можно сэкономить (смотрите комментарии от AlexanderYastrebov выше).
Это будет самый оптимальный вариант:
    public static void put(Integer key, Integer value)
    {
        if(!subSets.containsKey(key))
            subSets.put(key, new HashSet());
        ((Set)subSets.get(key)).add(value);
    }
...............
        for(int i = 0; i < 0x186a0; i++)
        {
            Integer key;
            for(Iterator i$ = getRandomKeys().iterator(); i$.hasNext(); put(key, Integer.valueOf(i)))
                key = (Integer)i$.next();

        }

Вернулись же к тому, с чего начали :-) Теперь boxing одного и того же числа делается кучу раз во внутреннем цикле ( put(key, Integer.valueOf(i)) ). Хоть код и «красивее», но память опять загаживается, и всё тормозит :-)
Блин, точняк. :)
спасибо за статью, напоминает ibm'овский цикл «5 things you didn't know about ...»
жду продолжения — полезно освежает
Че-то возникло желание отключить вообще автобоксинг.
аутбоксинг лучше избегать, то есть вообще не использовать. Создаешь объект — будь любезен вызови его конструктор) EIBTI («explicit is better than implicit»)! Задумываться а сколько же процентов памяти можно сэкономить если «не писать неправильно» (2 частицы «не» уже лажа) на мой взгляд то же самое что сравнивать зимнюю резину с летней в условиях гололедицы — результат заранее определен, за одним исключением — нормальный стиль программирования не зависит от погоды и календаря :)
НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории