Хотя бы один раз в жизни у каждого человека возникает мысль: каково это, в другой стране. Дома, конечно, хорошо: тут родная, и теперь бесплатная, Visual Studio, милый старичок Windows Forms со своим внуком WPF, в воспитании которого, определенно, недоглядели, таинственная мастерица на все руки WCF, братишки ADO и EF и многие другие. Большая семья - это хорошо, но душа тянется к приключениям, на чужбину, в страну Java.
В данном цикле статей я буду рассматривать Java, JVM, JRE и т.д. с позиции .NET разработчика, то есть акцентироваться на различиях. Целевой аудиторией я считаю .NET разработчиков, которым интересна Java, но нет желания читать кучу уже знакомой информации, ведь технологии все таки очень похожи.
Театр начинается с вешалки, а язык программирования с синтаксиса, его и рассмотрим. Я специально не буду заострять внимание на тех вещах, которые есть в C#, но нет в Java, дабы не разжигать холивар.
Кстати, как Вы думаете, что выведет следующий код?
Так же стоит отметить отсутствие беззнаковых примитивных типов и аналога decimal. Если по поводу первого можно махнуть рукой, почитав неплохую статью, то для высокоточных вычислений можно использовать java.math.BigDecimal.
В остальном ситуация с типами данных очень похожа. Тот же java.lang.Object содержит "классические" методы equals, hashCode, toString, несколько методов синхронизации, ничего необычного. Конечно, существует множество особенностей, но они не настолько очевидны.
Да, пакеты нельзя вложить один в другой, нельзя объявлять в одном файле больше одного пакета, но не больно то и хотелось. Ну и импортировать можно не весь пакет, а только конкретный класс. Кстати, в случае конфликта имен в двух пакетах именно импорт конкретного класса позволяет решить данную проблему:В данном цикле статей я буду рассматривать Java, JVM, JRE и т.д. с позиции .NET разработчика, то есть акцентироваться на различиях. Целевой аудиторией я считаю .NET разработчиков, которым интересна Java, но нет желания читать кучу уже знакомой информации, ведь технологии все таки очень похожи.
Театр начинается с вешалки, а язык программирования с синтаксиса, его и рассмотрим. Я специально не буду заострять внимание на тех вещах, которые есть в C#, но нет в Java, дабы не разжигать холивар.
Типы данных
Помните слоган "Everything is an object"? Так вот, в стране Java это "почти" так. Да, есть java.lang.Object, от которого наследуются все остальные объекты, но есть так называемые "примитивные типы", которые стоят в стороне от него. Познакомившись с данным списком вдумчивый шарпист задумается: а как же боксинг? А он есть, и куда более явный, чем в C#. Дело в том, что каждому примитивному типу соответствует ссылочный тип, с похожим названием, например, int <=> Integer. Таким образом, когда мы выполняем следующий код:
то должны понимать, что в данном случае прозрачно создается экземпляр типа Integer с его особенностями и оптимизациями. Это прекрасно видно в байт коде:int i = 42; Object o = i;
0: bipush 42 2: istore_1 3: iload_1 4: invokestatic #2// Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 7: astore_2То есть для прозрачного боксинга просто вызывается статический метод Integer.valueOf.
Кстати, как Вы думаете, что выведет следующий код?
int i1 = 200; int i2 = 200; Integer I1 = 200; Integer I2 = 200; out.println(i1 == i2); out.println(I1 == I2);Ответ: true false. Все дело в том, что в Java нельзя переопределять операторы, == всегда сравнивает экземпляры объектов. Стоит так же отметить, что если бы мы присваивали число меньше 128, то ответ был бы другим: класс Integer кеширует значения от 0 до 128, см. исходники.
Так же стоит отметить отсутствие беззнаковых примитивных типов и аналога decimal. Если по поводу первого можно махнуть рукой, почитав неплохую статью, то для высокоточных вычислений можно использовать java.math.BigDecimal.
В остальном ситуация с типами данных очень похожа. Тот же java.lang.Object содержит "классические" методы equals, hashCode, toString, несколько методов синхронизации, ничего необычного. Конечно, существует множество особенностей, но они не настолько очевидны.
Область видимости
В Java нет неймспесов, но есть пакеты, нет using, но есть import. На первый взгляд, разницы между ними практически нет:import a.b.*; package a.b;
import com.company.a.*; import com.company.b.*; import com.company.a.Test;Казалось бы, при чем тут область видимости и пакеты? Оказывается, связь прямая: все классы и члены классов выше private доступны внутри своего пакета. Таким образом, private - доступен только текущему классу, без модификатора - дополнительно доступен в своем пакете, protected - дополнительно доступен наследниками, даже из других пакетов, public - без ограничений.
Как видно из вышесказанного, пакеты и импорт это не просто "неймспейс другими словами", это полноценная отдельная абстракция со своим принципом работы.
Отдельно стоит отметить приятную конструкцию import static, позволяющую импортировать статические члены класс. В .NET стране это появилось только в Roslyn.
Подробнее про пакеты можно прочитать в соответствующей главе книги Java in a Nutshell и в документации.
Условные операторы
Эту тему вполне можно было бы не поднимать, так как что if и ?: аналогичны шарповым. Но switch имеет свои тонкости. Во первых, в Java нет goto, зато есть прямой переход между case:
switch (a){ case 0: out.print("Hello"); case 1: out.print("World"); break; default: break; }
В C# подобный код просто бы не скомпилировался.
Во вторых, при использовании строк в узлах case, их хеши вычисляются при компиляции и обрабатываются той же командой, что применяется для целых чисел и перечислений. Только при возникновении коллизий строки сравниваются "в лоб". Поэтому очень грустно смотреть на портянку equals-ов в il коде оператора switch, примененного для строк в C#.
Подробности можно почитать в документации: if-else, switch. Дополнительно, на хабре есть потрясающая статья: Тонкости оператора switch.
Во вторых, при использовании строк в узлах case, их хеши вычисляются при компиляции и обрабатываются той же командой, что применяется для целых чисел и перечислений. Только при возникновении коллизий строки сравниваются "в лоб". Поэтому очень грустно смотреть на портянку equals-ов в il коде оператора switch, примененного для строк в C#.
Подробности можно почитать в документации: if-else, switch. Дополнительно, на хабре есть потрясающая статья: Тонкости оператора switch.
Циклы
Собственно о циклах можно и не говорить, они абсолютно аналогичны, даже есть аналогичная foreach "проблема" с производительностью. Но есть одна интересная вещь, с ними связанная: именованные операторы break и continue. Я думаю, принцип их работы будет лучше понятен из кода:
root: while (true) { for (int i = 0; i < 10; i++) { System.out.print(i); if (i == 7) break root; } }
Очевидно, что на экран выведется строка "01234567".
То же самое и с оператором continue:
То же самое и с оператором continue:
final int length = 4; root: for (int i = 0; i < length; i++) { for (int j = 0; j < length; j++) { if (j + i >= length) continue root; System.out.print(j); } }
Данный код выводит на экран "0123012010".
По правде говоря, корректность применения именованных операторов break и continue у многих стоит под вопросом. Достаточно ввести в гугле "java break label best practice" и наслаждаться несколькими страницами душевных терзаний. В некоторых случаях данные операторы, как и goto, просто необходимы, а вот вопрос использования их в обычной практики остается на совести разработчика.
А Outer$Inner.class выглядит так:
То есть экземпляр внешнего класса передается экземпляру внутреннего. Более того, доступ к членам осуществляется по средствам сгенерированных метод аксессоров. Для каждого члена, будь то метод или поле, создается собственный метод.
И, наконец, локальные классы - это вложенные классы, объявленные в теле блока кода:
То есть анонимные классы это только синтаксический сахар вокруг локальных. Это доказывает рассмотрение .class файлов: мы легко найдем рядом с Outer.class файл с именем Outer$1.class:
public final class com.company.Day extends java.lang.Enum<com.company.Day> {
public static final com.company.Day SUNDAY;
public static final com.company.Day MONDAY;
public static final com.company.Day TUESDAY;
public static final com.company.Day WEDNESDAY;
public static final com.company.Day THURSDAY;
public static final com.company.Day FRIDAY;
public static final com.company.Day SATURDAY;
....
Именно это небольшое различие позволяет напичкать их любой произвольной логикой:
По правде говоря, корректность применения именованных операторов break и continue у многих стоит под вопросом. Достаточно ввести в гугле "java break label best practice" и наслаждаться несколькими страницами душевных терзаний. В некоторых случаях данные операторы, как и goto, просто необходимы, а вот вопрос использования их в обычной практики остается на совести разработчика.
Блоки инициализации
Блоки инициализации это, по сути дела, код фигурных скобках в теле класса. Этот код вызывается при создании экземпляра, до любого из конструкторов. Так же существует статическая версия, аналогичная по поведению статическим конструкторам в C#. Ну и, конечно, в одном классе может быть несколько блоков. Пожалуй, без примера это сложновато понять:
Кстати, по поводу инициализации полей - в Java можно это делать через экземлярные методы! Достаточно установить свойство final:
public class A { static { System.out.println("Static Initialization Block 1"); } { System.out.println("Initialization Block 1"); } public A() { System.out.println("Constructor"); } { System.out.println("Initialization Block 2"); } static { System.out.println("Static Initialization Block 2"); } }При создании экземпляра класса A в консоли мы увидим:
Как видно из вывода, сначала вызываются статические блоки инициализации, потом экземплярные, и только в конце конструктор. Заметьте, очередность вызова одинаковых блоков зависит от их взаимного расположения в коде. Это прекрасно видно в байт коде:Static Initialization Block 1
Static Initialization Block 2
Initialization Block 1
Initialization Block 2
Constructor
public class com.company.A { public com.company.A(); Code: 0: aload_0 1: invokespecial #1// Method java/lang/Object."<init>":()V 4: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3// String Initialization Block 1 9: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 15: ldc #5// String Initialization Block 2 17: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 20: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 23: ldc #6// String Constructor 25: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return static {}; Code: 0: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #7// String Static Initialization Block 1 5: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #8// String Static Initialization Block 2 13: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: return }Благодаря тому, что блоки вызываются всегда и до конструкторов, в них можно инициализировать поля значениями по умолчанию, вести логи и т.д.
Кстати, по поводу инициализации полей - в Java можно это делать через экземлярные методы! Достаточно установить свойство final:
private int count = GetDefaultCount(); private final int GetDefaultCount(){ return 0; }А вот для того, что бы понять, почему это работает, поговорим о полиморфизме.
Полиморфизм и наследование
У меня для Вас три новости и первая из них: в Java все методы вирутальны по умолчанию. "А как же производительность, как же inline?" - спросите Вы. Ничего страшного! JIT компилятор прекрасно справляется с задачей анализа таблицы виртуальных методов и инлайнит не стесняясь.
Вторая новость - можно повышать область видимости классов и членов классов при наследовании:
Ну и, наконец, третья новость: абстрактные классы необязательно должны определять интерфейс:
В данном случае прекрасно видно, что реализация классов никак не отличается, все как в C#.
Внутренние классы гораздо интереснее, так как они привязаны к конкретному экземпляру. Из этого следует несколько интересны особенностей:
В байт коде Outer.class можно увидеть следующий блок:
Вторая новость - можно повышать область видимости классов и членов классов при наследовании:
В других пакетах класс A будет недоступен, в отличие от класса B. Кстати, при импорте jar файла в Ваш проект ситуация будет той же самой.class A{ void method(){} } public class B extends A{ public void method(){ } }
Ну и, наконец, третья новость: абстрактные классы необязательно должны определять интерфейс:
interface MyInterface{ void MyMethod(); } abstract class MyClass implements MyInterface{ }Очевидно, что не абстрактные наследники MyClass все таки обязаны определить метод MyMethod.
Исключения
Исключения, они и в Java исключения, и способ их использования не сильно отличается от такового в C#:
А вот на последнем следует остановиться подробнее, так как подобного нет и, скорее всего, не будет в мире .NET. Дело в том, что в Java исключения, а именно наследники java.lang.Exception, невозможно выбросить просто так, не перехватив его, или не описав в сигнатуре метода информации о том, что метод может выбросить данное исключение.
Лучше давайте посмотрим как это реализовано в байт коде. Если с выбрасыванием исключений все очевидно (оператор athrow), то реализация блоков try+catch+finally очень интересна. Рассмотрим метод
- Можно перехватывать несколько исключений в одном блоке catch: catch(IOException|NullPointerException e)
- В try-with-resources(он же using) можно объявлять несколько объектов и блок catch:
try(InputStream stream1 = new FileInputStream("file1"); InputStream stream2 = new FileInputStream("file2")){ ... } catch(IOException e){ ... }
- Дерево наследования несколько сложнее чем в C#: https://docs.oracle.com/javase/tutorial/figures/essential/exceptions-throwable.gif. Где Error и его наследники сигнализируют о системных ошибках и нежелательны для обработки, Exception - прикладное проверяемое исключение. RuntimeException - прикладное не проверяемое исключение, "ошибка программиста". Подробнее ниже.
- Выбрасываемые исключения проверяются компилятором.
void test() throws IOException{
throw new IOException();
}
По правде говоря, данная тема настолько сильно раскрыта, что я просто оставлю ссылку на неплохую статью об этом: Исключения в Java, Часть II (checked/unchecked).Лучше давайте посмотрим как это реализовано в байт коде. Если с выбрасыванием исключений все очевидно (оператор athrow), то реализация блоков try+catch+finally очень интересна. Рассмотрим метод
void test(){ try { throw new Exception(); } catch (Exception e){ System.out.println("catch"); } finally { System.out.println("catch"); } }и его байт код
Как видно из байт кода, блоки try+catch+finally на самом деле представляют из себя таблицу переходов и некоторый сгенерированный код. Обратите внимание, что код из блока finally встречается два раза, первый раз после кода из блока catch, второй раз начиная с 28 метки. По сути дела, код из блока finally копируется после каждого блока, более того, в таблице исключений прекрасно видно, что перехват any действует в том числе и в блоке catch.void test(); Code: 0: new #9; //class java/lang/Exception 3: dup 4: invokespecial #10; //Method java/lang/Exception."<init>":()V 7: athrow 8: astore_1 9: getstatic #7; //Field java/lang/System.out:Ljava/io/PrintStream; 12: ldc #11; //String catch 14: invokevirtual #12; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 17: getstatic #7; //Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #13; //String finally 22: invokevirtual #12; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: goto 39 28: astore_2 29: getstatic #7; //Field java/lang/System.out:Ljava/io/PrintStream; 32: ldc #13; //String finally 34: invokevirtual #12; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 37: aload_2 38: athrow 39: return Exception table: from to target type 0 8 8 Class java/lang/Exception 0 17 28 any
Вложенные классы
В Java вложенные классы бывают трех типов: статические, внутренние и локальные. С первыми все просто: их логика аналогична вложенным классам в C#. Для того, что бы определить статический вложенный класс используйте модификатор static:
public class Outer { public static class Nested { } }
Стоит так же отметить, что только статические вложенные классы могут иметь статические члены.
Прежде чем следовать дальше заглянем немного внутрь. Как известно, при компиляции Java кода на каждый класс создается .class файл. В нашем случае мы увидим 2 файла: Outer.class и Outer$Nested.class. Посмотрим на их байт код:
Compiled from "Outer.java"
public class com.company.Outer {
public com.company.Outer();
Code:
0: aload_0
1: invokespecial #1// Method java/lang/Object."<init>":()V
4: return
}
...
Compiled from "Outer.java"
public class com.company.Outer$Nested {
public com.company.Outer$Nested();
Code:
0: aload_0
1: invokespecial #1// Method java/lang/Object."<init>":()V
4: return
}
Внутренние классы гораздо интереснее, так как они привязаны к конкретному экземпляру. Из этого следует несколько интересны особенностей:
- Из кода внутреннего класса можно получить доступ к приватным членам внешнего класса:
public class Outer { private int test = 1; public void Test() { Inner inner = new Inner(); } class Inner { public void test() { System.out.print(test); } } }
static int access$000(com.company.Outer); Code: 0: aload_0 1: getfield #1// Field test:I 4: ireturn
class com.company.Outer$Inner { final com.company.Outer this$0; com.company.Outer$Inner(com.company.Outer); Code: 0: aload_0 1: aload_1 2: putfield #1// Field this$0:Lcom/company/Outer; 5: aload_0 6: invokespecial #2// Method java/lang/Object."<init>":()V 9: return public void test(); Code: 0: getstatic #3// Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1// Field this$0:Lcom/company/Outer; 7: invokestatic #4// Method com/company/Outer.access$000:(Lcom/company/Outer;)I 10: invokevirtual #5// Method java/io/PrintStream.print:(I)V 13: return }
- Создать экземпляр внутреннего класса можно только с использованием экземпляра внешнего класса:
Outer outer = new Outer(); Outer.Inner inner = outer.new Inner();
- В случае перекрытия членов внешнего класса, получить доступ к ним можно напрямую, через его экземпляр:
public class Outer { private int test = 1; public Outer() { Inner inner = new Inner(); } public class Inner { int test = 2; public Inner() { System.out.print(test); System.out.print(Outer.this.test); } } }
public void test(){ class Inner { } Inner inner = new Inner(); }Локальный класс имеет доступ к статическим и экземплярным (если метод, в котором он объявлен, не статический) членам, а так же к локальным переменных. Данная реализация замыканий, добавленная в Java 8 обладает одной особенностью - локальные переменные должны быть неизменяемые, декларативно или фактически. То есть, можно указать модификатор final переменной, а можно просто не менять ее, компилятор сам определит, переприсваивалась ли она, и прервет компиляцию:
public class Outer { public void test() { int i = 42; class Inner { public void print() { System.out.println(i); } } new Inner().print(); System.out.println(i); } }
"Под капотом" гораздо интереснее. Метод Test в Outer.class:
Локальный же класс компилируется в файл Outer$1Inner.class:public void test(); Code: 0: bipush 42 2: istore_1 3: new #2// class com/company/Outer$1Inner 6: dup 7: aload_0 8: iload_1 9: invokespecial #3// Method com/company/Outer$1Inner."<init>":(Lcom/company/Outer;I)V 12: invokevirtual #4// Method com/company/Outer$1Inner.print:()V 15: getstatic #5// Field java/lang/System.out:Ljava/io/PrintStream; 18: iload_1 19: invokevirtual #6// Method java/io/PrintStream.println:(I)V 22: return
Ничего не напоминает? Да это точь в точь DisplayClass<>, генерируемый лямбдами в C#, правда замыкания работают по другому: вместо замены вызовов локальной переменной на вызовы поля класса, ее значение просто вытаскиваются из стека в блоке инициализации.class com.company.Outer$1Inner { final int val$i; final com.company.Outer this$0; com.company.Outer$1Inner(); Code: 0: aload_0 1: aload_1 2: putfield #1// Field this$0:Lcom/company/Outer; 5: aload_0 6: iload_2 7: putfield #2// Field val$i:I 10: aload_0 11: invokespecial #3// Method java/lang/Object."<init>":()V 14: return public void print(); Code: 0: getstatic #4// Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #2// Field val$i:I 7: invokevirtual #5// Method java/io/PrintStream.println:(I)V 10: return }
Анонимные классы
Услышав данное словосочетание, сразу вспоминается var, linq, etc. Советую забыть, так как в Java это совершенно другая вещь. Анонимные классы - это быстрый способ создания локальных классов. Требования:- класс должен наследоваться от другого класса или реализовывать интерфейс.
- нельзя указывать имя класса
- нельзя реализовывать конструкторы, но можно блоки инициализации
interface Console { void writeLine(String text); } public class Outer { public void test() { Console console = new Console() { @Override
public void writeLine(String text) { System.out.println(text); } }; console.writeLine("Hello World!"); } }Обратите внимание, мы пишем "new Console", но создавать экземпляры интерфейсов запрещено. В данном случае мы просто указываем, что анонимный класс должен реализовывать соответствующий интерфейс. Используя возможности локальных классов мы бы написали следующее:
public void test() { class TerminalConsole implements Console{ @Override
public void writeLine(String text) { System.out.println(text); } } Console console = new TerminalConsole(); console.WriteLine("Hello World!"); }Но нужно помнить, что анонимные классы всегда должны реализовывать интерфейс или наследоваться от какого либо класса. Требование справедливое, иначе мы бы не смогли обращаться к членам класса.
То есть анонимные классы это только синтаксический сахар вокруг локальных. Это доказывает рассмотрение .class файлов: мы легко найдем рядом с Outer.class файл с именем Outer$1.class:
class com.company.Outer$1 implements com.company.Console { final com.company.Outer this$0; com.company.Outer$1(com.company.Outer); Code: 0: aload_0 1: aload_1 2: putfield #1// Field this$0:Lcom/company/Outer; 5: aload_0 6: invokespecial #2// Method java/lang/Object."<init>":()V 9: return public void writeLine(java.lang.String); Code: 0: getstatic #3// Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_1 4: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return }Я думаю, что байт код говорит сам за себя.
Лямбда выражения
Официальное руководство пестрит примерами, как правильно применять лямбда выражения, но мы то это и так знаем, а вот как они устроены в Java изучать гораздо интереснее. Для начала нужно понять, в восьмую версию делегаты так и не завезли, так что лямбды реализуются по подобию анонимных классов. На первый взгляд между ними всего одно отличие: лямбды могут создаваться только на основе интерфейса, который должен содержать только один нестатический метод. Так что пример из предыдущего раздела можно написать вот так:
В остальном, все очень привычно, за исключением замыканий, но эта тема отдельного холивара. Гораздо интереснее что внутри.
Скомпилировав код и открыв папку с .class файлами Вы не найдете ожидаемые Outer#1.class или что то подобное, Вы вообще не найдете дополнительных классов. Все дело в том, что лямбды реализованы как приватные методы и вызываются через новую инструкцию invokedynamic.
И если вы думаете, что для этого генерируется приватный статический метод, то сильно ошибаетесь:
@FunctionalInterface
interface Console {
void writeLine(String text); } public class Outer { public void test() { Console console = text -> System.out.println(text); console.WriteLine("Hello World!"); } }Обратите внимание на аннотацию(то что в .NET называется атрибутом) FunctionalInterface. Применяя его к интерфейсу вы показываете, что его можно использовать в лямбда выражениях. Программа просто не скомпилируется, если в таком интерфейсе будет объявлено боле одного абстрактного метода.
В остальном, все очень привычно, за исключением замыканий, но эта тема отдельного холивара. Гораздо интереснее что внутри.
Скомпилировав код и открыв папку с .class файлами Вы не найдете ожидаемые Outer#1.class или что то подобное, Вы вообще не найдете дополнительных классов. Все дело в том, что лямбды реализованы как приватные методы и вызываются через новую инструкцию invokedynamic.
public class com.company.Outer { public com.company.Outer(); Code: 0: aload_0 1: invokespecial #1// Method java/lang/Object."<init>":()V 4: return public void Test(); Code: 0: invokedynamic #2, 0// InvokeDynamic #0:writeLine:()Lcom/company/Console; 5: astore_1 6: aload_1 7: ldc #3// String Hello World! 9: invokeinterface #4, 2// InterfaceMethod com/company/Console.writeLine:(Ljava/lang/String;)V 14: return private static void lambda$test$0(java.lang.String); Code: 0: getstatic #5// Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokevirtual #6// Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return }Как видно из байт кода, больше никаких инструкций new, никаких дополнительных классов, просто прямой, оптимизированный вызов метода. По поводу ситуации с invokedynamic можно посмотреть в докладе: Владимир Иванов — Invokedynamic: роскошь или необходимость? Замыкания же реализованы через параметры сгенерированного метода:
public void test() { int i = 42; Console console = text -> System.out.print(i); console.WriteLine(""); System.out.println(i); }
О внутреннем устройстве лямбда выражений можно почитать в статье Java 8 Lambdas - A Peek Under the Hood.Очень хотелось бы провести аналогию с реализацией в C#, но, так как я обещал не начинать холивары, перейду к следующей теме.public void test(); Code: 0: bipush 42 2: istore_1 3: iload_1 4: invokedynamic #2, 0 // InvokeDynamic #0:WriteLine:(I)Lcom/company/Console; 9: astore_2 10: aload_2 11: ldc #3 // String 13: invokeinterface #4, 2 // InterfaceMethod com/company/Console.WriteLine:(Ljava/lang/String;)V 18: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 21: iload_1 22: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 25: return private static void lambda$test$0(int, java.lang.String); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: iload_0 4: invokevirtual #7 // Method java/io/PrintStream.print:(I)V 7: return
Указатели на метод
Наверняка в прошлом примере Вы думали: "Ну почему нельзя вместо лямбды передать метод с той же сигнатурой?". На самом деле можно:
public void test() { Console console = System.out::print; console.writeLine("Hello World!"); }
public void test(); Code: 0: getstatic #2// Field java/lang/System.out:Ljava/io/PrintStream; 3: dup 4: invokevirtual #3// Method java/lang/Object.getClass:()Ljava/lang/Class; 7: pop 8: invokedynamic #4, 0// InvokeDynamic #0:writeLine:(Ljava/io/PrintStream;)Lcom/company/Console; 13: astore_1 14: aload_1 15: ldc #5// String Hello World! 17: invokeinterface #6, 2// InterfaceMethod com/company/Console.WriteLine:(Ljava/lang/String;)V 22: returnСогласитесь, очень похоже на то, что генерируется il кодом при передаче указателя на метод.
Интерфейсы
Казалось бы, такая важная тема, и почти в конце статьи. Но это не случайно, ведь различий между ними не так уж и много:
- Интерфейсы именуются в Pascal стиле без всяких префиксов
- В интерфейсах можно определять константы, просто объявив их в теле. Все очень просто, но с некоторой особенностью. Рассмотрим пример:
interface MyInterface { int VALUE = 42; } class MyClass implements MyInterface {} ... MyClass mc = new MyClass(); MyInterface mi = new MyClass(); System.out.print(mc.VALUE); System.out.print(mi.VALUE);
Вполне предсказуемо, что на экран будет выведено два раза число 42. А теперь поменяем реализацию:
class MyClass implements MyInterface { public final int VALUE = 24; }
На этот раз числа разные, 24 42. Причина кроется в самой природе констант, ведь их значения записываются напрямую в байт код:
0: new #2// class com/company/MyClass 3: dup 4: invokespecial #3// Method com/company/MyClass."<init>":()V 7: astore_1 8: new #2// class com/company/MyClass 11: dup 12: invokespecial #3// Method com/company/MyClass."<init>":()V 15: astore_2 16: getstatic #4// Field java/lang/System.out:Ljava/io/PrintStream; 19: aload_1 20: invokevirtual #5// Method java/lang/Object.getClass:()Ljava/lang/Class; 23: pop 24: bipush 24 26: invokevirtual #6// Method java/io/PrintStream.println:(I)V 29: getstatic #4// Field java/lang/System.out:Ljava/io/PrintStream; 32: aload_2 33: pop 34: bipush 42 36: invokevirtual #6// Method java/io/PrintStream.println:(I)V 39: return
Так что не забывайте, это Вам не абстрактные свойства.
- В интерфейсах можно определять методы по умолчанию. Эти методы должны содержать реализацию и их необязательно имплементировать. Основная цель данной функциональности, предоставить возможность дополнять интерфейсы в библиотеках с сохранением обратной совместимости:
interface MyInterface { default void newMethod(){ System.out.println("New method"); } }
Подробнее можно почитать в следующей статье: Java 8 default methods: what can and can not do? Кстати, возможно не очевидно, что при имплементации метода по умолчанию, вызвать его можно через [Interface name].super.[Method], например MyInterface.super.newMethod().
- В интерфейсах можно определять статические методы:
interface MyInterface { static void staticMethod(){ System.out.println("Static method"); } }
Точка. Тут действительно нечего добавить.
Перечисления
Как и интерфейсы, перечисления в Java и C# братья, но не близнецы. Они чем то похожи, даже реализация в байт коде аналогичная - каждое значение представляет из себя статическое поле. Только вот в Java они возвращают экземпляр сгенерированного класса (я специально опустил детали инициализации, так как они достаточно очевидны, но многословны):public final class com.company.Day extends java.lang.Enum<com.company.Day> {
public static final com.company.Day SUNDAY;
public static final com.company.Day MONDAY;
public static final com.company.Day TUESDAY;
public static final com.company.Day WEDNESDAY;
public static final com.company.Day THURSDAY;
public static final com.company.Day FRIDAY;
public static final com.company.Day SATURDAY;
....
Именно это небольшое различие позволяет напичкать их любой произвольной логикой:
public enum Day { SUNDAY (10), // передача параметра в конструктор MONDAY (-1000), TUESDAY (-100), WEDNESDAY (0), THURSDAY (100), FRIDAY (1000), SATURDAY (Integer.MAX_VALUE); private final int mood; Day(int mood){ this.mood = mood; } public int getMood(){ return mood; } public boolean isGoodDay(){ return mood > 0; } }Я уверен, что у Вас хватит фантазии, что бы придумать как это использовать.
Аннотации
Как Вы понимаете, в .NET мире их зовут атрибутами и отличий между ними, до Java 8, практически не было, достаточно очевидный синтаксис применения и декларации:
@MyAnnotation("Text") class MyClass{
...
@interface MyAnnotation{ String value(); int defaultValue() default 42; }
Не смотря на то, что перед нами как бы интерфейс, мы не можем создавать статические методы, а значения по умолчанию могут быть только константами. Подробнее про аннотации есть прекрасная доступная статься на хабре: Аннотации в JAVA: обзор синтаксиса и создание собственных.
На самом деле, согласно сгенерированному байт коду, аннотация и есть интерфейс, пронаследованный от java.lang.annotation.Annotation с дополнительным флагом ACC_ANNOTATION (несущественный код опущен):
interface com.company.MyAnnotation extends java.lang.annotation.Annotation flags: ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION Constant pool: ... #9 = Integer 42 ... { public abstract java.lang.String value(); flags: ACC_PUBLIC, ACC_ABSTRACT public abstract int defaultValue(); flags: ACC_PUBLIC, ACC_ABSTRACT AnnotationDefault: default_value: I#9}
Я не случайно отметил выше Java 8, ведь в новой версии аннотации можно применять не только в описании, но и при работе с экземплярами:
@NonNull String str;
В данном случае, при соответствующей реализации от IDE, Вы можете получать соответствующие предупреждения при компиляции. Ну и, конечно, эта функциональность удобна в аспектно ориентированном программировании. Подробнее можно почитать в статье Type Annotations in Java 8: Tools and Opportunities.
Обобщения
Обобщения в C# и Java так похожи на первый взгляд и так отличаются изнутри, что начать их изучение стоит с внутреннего устройства.
В Java generic классы, методы , интерфейсы и проч. создаются "затиранием" (Erase) строго типизированной информации и построении "мостов" (Brige) для поддержки полиморфизма. Рассмотрим некоторый класс:
Возможно, Вы заметите в данной реализации слабую сторону: затирание будет следовать в виртуальных методах не в обобщенных классах, наследующих обобщенные. Именно для этого применяется механизм "мостов" (Briges). Рассмотрим наследник нашего класса Generic<T>:
Да, предыдущая проблема прозрачно решается компилятором, но существуют другие, одна из которых имеет красивое название Hep Pollution. Возникает она, как правило, в методах с переменным количеством аргументов:
Следующей важной частью обобщений является так называемый Wild Card: возможность не указывать конкретный тип:
Но все таки, основное назначение Wild Card - ковариация и контрвариация аргументов типов, то что в C# достигается модификаторами in и out. Гораздо проще объяснить на примере. Для начала вернемся в мир .NET и рассмотрим следующие два интерфейса и реализацию.
interface ICovariantInterface<out T>
{
T Get();
}
interface IContrvariantInterface<in T>
{
void Set(T value);
}
class MyClass<T> : ICovariantInterface<T>, IContrvariantInterface<T>
{
private T _value;
public T Get()
{
return _value;
}
public void Set(T value)
{
_value = value;
}
}
А так же некоторый метод:
private static void Test()
{
ICovariantInterface<object> cov = new MyClass<string>();
object o = cov.Get();
IContrvariantInterface<string> contr = new MyClass<object>();
contr.Set("0");
}
Так вот, в Java мире мы можем с помощью конструкций <? extends ...> и <? super ...> реализовать ту же логику без применения специальных интерфейсов:
С точки же зрения использования обобщений отличий не много:
В Java generic классы, методы , интерфейсы и проч. создаются "затиранием" (Erase) строго типизированной информации и построении "мостов" (Brige) для поддержки полиморфизма. Рассмотрим некоторый класс:
В байт коде он будет представлен так:class Generic<T> { private T value; Generic(T value) { this.value = value; } void set(T value) { this.value = value; } T get() { return value; } }
Прекрасно видно, что в классе все упоминания об аргументе T были заменены на базовый тип java.lang.Object. Именно по этой причине в обобщениях невозможно использовать примитивные типы, зато можно не бояться создавать статические поля.class com.company.Generic extends java.lang.Object{ private java.lang.Object value; com.company.Generic(java.lang.Object); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: aload_1 6: putfield #2; //Field value:Ljava/lang/Object; 9: return void set(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2; //Field value:Ljava/lang/Object; 5: return java.lang.Object get(); Code: 0: aload_0 1: getfield #2; //Field value:Ljava/lang/Object; 4: areturn }
Возможно, Вы заметите в данной реализации слабую сторону: затирание будет следовать в виртуальных методах не в обобщенных классах, наследующих обобщенные. Именно для этого применяется механизм "мостов" (Briges). Рассмотрим наследник нашего класса Generic<T>:
Как Вы понимаете, затирать в методе set параметр было бы некорректно. Давайте взглянем на байт код:class NonGeneric extends Generic<Integer> { NonGeneric(Integer value) { super(value); } @Override void set(Integer value) { } }
При компиляции генерируется специальный метод, переопределяющий метод Set в базовом классе. Обратите внимание, параметр Object приводят к Integer и передают в качестве параметра в set(Integer).class com.company.NonGeneric extends com.company.Generic{ com.company.NonGeneric(java.lang.Integer); Code: 0: aload_0 1: aload_1 2: invokespecial #1;//Method com/company/Generic."<init>":(Ljava/lang/Object;)V 5: return void set(java.lang.Integer); Code: 0: return void set(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #2;//class java/lang/Integer 5: invokevirtual #3;//Method Set:(Ljava/lang/Integer;)V 8: return }
Да, предыдущая проблема прозрачно решается компилятором, но существуют другие, одна из которых имеет красивое название Hep Pollution. Возникает она, как правило, в методах с переменным количеством аргументов:
void heapPollution(Generic<Integer>... g) { Object[] o = g; Integer value = g[0].get(); o[0] = new Generic<String>(""); value = g[0].get(); }
В последней строчке будет выброшено java.lang.ClassCastException. Как вы понимаете, g представляет из себя массив обобщенных классов, а так как Generic<Integer> и Generic<String> в рантайме одинаковы, то подобная ошибка вполне возможна. Но не волнуйтесь, комилятор выводит предупреждение о возможности Heap Pollution.
Следующей важной частью обобщений является так называемый Wild Card: возможность не указывать конкретный тип:
void wildCard(Generic<?> g){
}
К сожалению, многие возможности при такой нотации невозможны, например, в данном случае, мы не сможем нормально вызвать метод Set. Зато доступен метод Get и вообще любые члены не использующие обобщенный аргумент.Но все таки, основное назначение Wild Card - ковариация и контрвариация аргументов типов, то что в C# достигается модификаторами in и out. Гораздо проще объяснить на примере. Для начала вернемся в мир .NET и рассмотрим следующие два интерфейса и реализацию.
interface ICovariantInterface<out T>
{
T Get();
}
interface IContrvariantInterface<in T>
{
void Set(T value);
}
class MyClass<T> : ICovariantInterface<T>, IContrvariantInterface<T>
{
private T _value;
public T Get()
{
return _value;
}
public void Set(T value)
{
_value = value;
}
}
А так же некоторый метод:
private static void Test()
{
ICovariantInterface<object> cov = new MyClass<string>();
object o = cov.Get();
IContrvariantInterface<string> contr = new MyClass<object>();
contr.Set("0");
}
Так вот, в Java мире мы можем с помощью конструкций <? extends ...> и <? super ...> реализовать ту же логику без применения специальных интерфейсов:
void test(){ Generic<? extends Object> cov = new Generic<String>("42"); Object o = cov.get(); Generic<? super String> contr = new Generic<Object>("42"); contr.set("0"); }Wild Card достаточно необычная тема, так что я бы советовал прочитать про нее в официальной документации: https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html.
С точки же зрения использования обобщений отличий не много:
- Упрощенная запись diamond: Generic<Integer> g = new Generic<>(1);
- Использование обобщенных типов как не обобщенных (Raw types), см https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html
- Конструкторы, как и любые методы могут иметь обобщенный аргумент:
<X> generic(T value) {...}
...
Generic<Integer> g = new <String>Generic<Integer>(1);
- Так как в рантайме не доступен тип обобщенного аргумента, то мы не можем создавать экземпляр данного класса:
static <T> T createInstance(){ return new T(); // ошибка компиляции}
Заключение
Данная статья является только началом путешествия в "мир Java". Дальше мы рассмотрим подробнее байт код, сравним его с Common Intermediate Language, изучим и сравним рантаймы и многое другое.