Участник:Phersu/Java vs. CSharp
Адаптеры (анонимные классы) vs. делегаты
Адаптеры в Java более flexible, так как могут хранить своё состояние между вызовами (что бывает очень полезно). В случае с делегатами в C# состояние предполагается хранить во внешнем scope, что замусоривает код. И делегаты, и адаптеры поддерживают замыкания и отличаются только синтаксисом.
Синтаксис у делегатов покороче, однако адаптеры позволяют объединять несколько методов в один логический listener, благодаря чему в итоге можно сделать API в Java компактнее:
C#:
- public delegate void ClickDelegate(object sender, ClickEventArgs e);
- public delegate void MoveDelegate(object sender, MoveEventArgs e);
- public delegate void KeyDelegate(object sender, KeyEventArgs e);
- void obj_onClick(object sender, ClickEventArgs e) { }
- void obj_onMove(object sender, MoveEventArgs e) { }
- void obj_onKey(object sender, KeyEventArgs e) { }
- obj.Click += obj_onClick;
- obj.Move += obj_onMove;
- obj.Key += obj_onKey;
Java:
- public abstract class Listener {
- public void onClick(ClickEvent e) { }
- public void onMove(MoveEvent e) { }
- public void onKey(KeyEvent e) { }
- }
- obj.addListener(new Listener() {
- public void onClick(ClickEvent e) { }
- public void onMove(MoveEvent e) { }
- public void onKey(KeyEvent e) { }
- });
- public abstract class Listener {
Также делегаты суть second-class citizens, чужды ООП, усложняют язык и VM, вводят дополнительные ненужные типы (MulticastDelegate наследует Delegate, при том что реально используется только класс MulticastDelegate, который нельзя наследовать (хотя он не sealed!), а класс Delegate вообще никак не используется и является технической ошибкой проектирования в .NET 1.0)
См. http://java.sun.com/docs/white/delegates.html
Ещё немного о замыканиях
Если анонимный класс ссылается на свободную переменную, то в Java эта переменная в строгом порядке должна быть final, т.к. при замыкании анонимный класс копирует значение в свои внутренние структуры и никак не гарантирует, что реальное значение в реальном контексте не будет до вызова изменено.
Напр.:
- final int i = 10; // НЕ КОМПИЛИРУЕТ, ЕСЛИ НЕ FINAL
- Object obj = new Object() {
- public int hashCode() {
- return i;
- }
- public int hashCode() {
- };
- i++; // ОШИБКА КОМПИЛЯЦИИ
- System.out.println(obj.hashCode());
Что же C#? А C# пофиг. Он без проблем скомпилирует данный кусок (без модификатора final) и на консоль тихо выведет 10 вместо предполагаемого 11.
Generics
Параметризированные типы в C# поддерживаются на уровне VM, в Java же имеем type erasure. С одной стороны, generics в CLR более производительны: нет cast'ов и boxing/unboxing, как в Java. С другой стороны, на уровне языка C# и системы CLR вводятся очередные хаки:
- примитивные типы наследуют ValueType, который наследует Object, но в то же время примитивные типы по семантике и поведению совсем не являются Object'ами и, наоборот, противопоставляются им (ну что общего у int и Object?)
- вызов виртуальных методов на value types нуждается в магическом опкоде constrained
- каждый параметризированный класс (напр. List<int>) имеет в памяти специальный инстанцированный объект System.Type, каждый со своими сгенерированными JIT-данными (хотя сейчас уже даже mono умеет sharing), каждый со своими внутренними структурами
- противоречит ООП-иерархии, дублирует на уровне системы типов саму первоначальную идею, которая заключается в том, что все классы наследуют Object именно с целью иметь обобщённые контейнеры с апкастом в Object
Среди плюсов, конечно — лучшая производительность, возможность определить тип T для конкретно взятого контейнера. В Java же подход другой: generics не стоят в центре системы типов, а являются просто удобным инструментом для проверки типов в compile-time.
Минусы Java
- примитивные объекты должны оборачиваться в wrappers типа Integer, Float и т.д. на куче. Зато это позволяет иметь систему типов довольно семантически интуитивной и без лишних хаков. А вот в C# даже boxed primitive types умудрились сделать через хак: они являются очередным «магическим» типом, реализуемом где-то в недрах VM, в то время как в Java это просто объекты с одним единственным полем int, float и т.д.
- при добавлении объекта в контейнер происходит апкаст, который в 100% случаях no op, при извлечении же объекта нужен даункаст, но я не думаю, что это серьёзный bottleneck (бенчмарки?) Думаю, что JIT имеет возможность соптимизировать здесь.
- нет возможности определить параметр типа в рантайме, напр. в Java нельзя написать new T[10] — нужно писать new Object[10].
- overhead из-за boxing'а довольно большой, напр. заголовок объекта на куче равен как минимум 8 байтам + выравнивание на 8 байт + ссылка на объект из внутр. массива — итого один int в контейнере занимает 20 байт vs. 4 байта в C#.
Таким образом, между производительностью и семантической чистотой Java опять выбирает семантическую чистоту.
JIT
В CLR абсолютно все методы при первом вызове компилируются в машинный код пользовательского процессора. Из-за такого дизайна — из-за того что огромное количество методов должно быть на лету скомпилировано без видимых пауз — кодогенератор C#/CLR обречён выдавать некачественный малопроизводительный код, так как в JIT-системах качество/производительность кода обратно пропорциональны количеству компилируемого кода.
В JVM же код по умолчанию интерпретируется, а компилируются только самые вызываемые методы, что позволяет сэкономить время на качественную компиляцию нескольких актуальных методов вместо траты времени на некачественную компиляцию ВСЕХ методов. Более того, JVM умеет динамически адаптироваться и деоптимизировать/переоптимизировать уже скомпилированные методы, если ситуация с прошлого раза кардинально изменилась. Дизайн C#/CLR не позволяет ничего из этого делать.
Для обхода такой трагедии .NET вроде как предоставляет утилиту NGEN для прегенерации машинного кода, однако сам же MSDN и говорит, что получаемый код ещё медленнее, чем в JIT'е (то есть тот всё-таки делает кое-какие ограниченные динамические оптимизации).
Ещё о value types
В C# есть user-defined value types, живущие в стеке. Они полезны в двух случаях: при interop и при небольших утилитных иммутабельных объектах типа Vector или Matrix. В Java user-defined value types нет, т.к. interop через JNI предполагает контролированный ручной маршалинг, а что касается иммутабельных объектов, то эта проблема уже неактуальна в связи с введением escape analysis в Java 7. Единственно, конечно — если объект возвращается из функции, то он будет создан в куче в Java vs. на стеке в C#, но зато хотя бы промежуточные объекты в Java выделяется в стеке.
Таким образом, в очередной раз, Java выбирает более чистый дизайн и переносит решение проблем производительности с дизайна системы на реализацию системы. Создаётся объект на куче или в стеке — это должна решать VM, а не программист, т.к. это вопрос реализации, а не дизайна. Не очень хорошо, что в 21 веке язык C#, позиционируемый как современный, всё ещё перекладывает на программиста задачи, которые обычно встречаются в языках типа C.
Собственные грабли
P.S. Смешно ли — команда C# впоследствии стала бороться сама с собой, введя класс Nullable<T> where T: struct для структур, которые могут быть нулём (при том что по спецификации структуры не могут быть нулём). В Java всё это время можно было использовать те самые java.lang.Integer, java.lang.Float и т.д., от которых C#-исты отказались.
Enum
В C#/CLR перечисляемые типы, enum'ы, могут накладываться только на integer-типы:
- enum : int
- {
- }
Это сильно негибко, и такой выбор был обусловлен чисто interop-соображениями: в C/C++ enum'ы тоже являются простыми целочисленными значениями и, таким образом, предполагалось, что проще чем 1-к-1 маппинг в managed-to-native transitions не найти.
Но в Java выждали время и в очередной раз решили задачу куда мудрее. Да, первоначальный стандарт Java вообще не имел enum'ов и зависел от нисколь не типизированных целочисленных констант. Но парни исправились и ввели такую штуку: каждое enum-значение это просто навсего синглтон, автоматически генерируемый компилятором. Так как каждое enum-значение является обычным объектом, его можно связывать с любым значением, через поля объекта. Если нам нужен interop, то мы можем связать enum с численным значением:
- public enum MeshUsage {
- STATIC(GL_STATIC_DRAW),
- DYNAMIC(GL_DYNAMIC_DRAW),
- STREAM(GL_STREAM_DRAW);
- public enum MeshUsage {
- public final int id;
- MeshUsage(int id) {
- this.id = id;
- }
- MeshUsage(int id) {
- }
и использовать таким образом:
- MeshUsage usage = getUsage();
- NativeFunctions.doSomething(usage.id);
Но енто ещё не всё: каждый enum располагает методом name(), а также можно итерировать по списку:
- for(ImageFormat imf: ImageFormat.values()) {
- if(extension.equals(imf.name())) {
- format = imf;
- break;
- }
- if(extension.equals(imf.name())) {
- }
- for(ImageFormat imf: ImageFormat.values()) {
Лепота.
TODO
final