JavaRush /Java блог /Random UA /10 речей яких ви не знали в Java
minuteman
32 рівень

10 речей яких ви не знали в Java

Стаття з групи Random UA
Отже, ви з недавніх пір почали працювати з Java? А пам'ятаєте часи коли вона називалася "Oak", коли об'єктно-орієнтованість була ще гарячою темою, коли люди С++ думали що Java не має жодних шансів, і коли про аплети ніхто навіть не чув? Я можу припустити, що ви не знаєте навіть половини таких речей. Давайте почнемо тиждень із кількох крутих сюрпризів внутрішньої роботи Java. 10 речей яких ви не знали в Java - 11. Немає такого поняття як – checked exception. Все вірно! JVM поняття не має про таку річ, лише мову Java знає. Сьогодні всі погоджуються, що checked exceptions були помилкою. Як говорив Брюс Еккель на своєму заключному виступі на GeeCON у Празі, жодна інша мова після Java не використовує checked exception, навіть Java 8 більше не охоплює їх у новому Streams API (що правда може завдавати невеликих незручностей, коли ваші лямбди будуть використовувати IO або JDBC). Ви хочете доказів того, що JVM не знає такої речі? Спробуйте наступний код: Це не тільки скомпілюється, це також кине SQLException, вам навіть не потрібно використовувати Lombok @SneakyThrows для цього. 2. Ви можете мати перевантажені методи, що відрізняються тільки типами, що повертаються.public class Test { // No throws clause here public static void main(String[] args) { doThrow(new SQLException()); } static void doThrow(Exception e) { Test. doThrow0(e); } @SuppressWarnings("unchecked") static void doThrow0(Exception e) throws E { throw (E) e; } } Це не відкомпілюється, правда? class Test { Object x() { return "abc"; } String x() { return "123"; } } Правильно. Мова Java не дозволяє одночасно двом методам бути еквівалентно перевизначеними в межах одного класу, не звертаючи уваги на їх розрізняючи у throws чи return типах. Але зачекайте на хвабонку. Перевірте ще раз документацію щодо Class.getMethod(String, Class…). Там написано: Відзначте, що, можливо, є більше одного відповідного методу в класі, тому що поки мова Java забороняє безліч методів з однаковою сигнатурою, але різними типами, що повертаються, віртуальна машина Java цього не робить. Ця гнучкість у віртуальній машині можна використовувати реалізації різних функцій мови. Наприклад, підступні повернення можуть здійснюватися з методами bridge; bridge метод і перевизначений метод, мали б однакову сигнатуру та різні типи, що повертаються. Нічого собі, та це має сенс. Насправді це досить багато що відбувається, коли ви пишете наступне: Подивіться на згенерований байт код: Отже, t насправді об'єкт у байт коді. Це добре розуміється. Синтетичний bridge метод насправді генерується компілятором тому що тип значення Parent.x(), що повертається, можна очікувати на певних ділянках викликів. Додавання generics без таких методів bridge перестане бути можливим у двійковому поданні. Отже, зміни в JVM щоб дозволити таку функцію зробило менше болю (який також дозволяє коваріантне перевизначення методу як побічний ефект…) По розумному правда? 3. Все наступне – двомірні масиви. Це насправді так. Навіть якщо ваш розумовий аналізатор, не може відразу зрозуміти тип, що повертається з описаних вище способів, всі вони однакові! Як і наступний шматок коду. Ви думаєте, що це божевілля? Кількість можливостей написати теж просто підриває уяву! Тип annotation. Пристрій загадковість якого поступається лише його мощі. Або іншими словами: Коли я роблю останній коміт якраз перед моєю 4-тижневою відпусткою. Я дозволяю вам користуватися будь-яким способом, що вам сподобався. 4. Ви не отримаєте умовного виразу Отже, ви думали, що вже знаєте все про умовні вирази, коли почали їх використовувати? Дозвольте засмутити вас – ви помилялися. Більшість із вас подумає, що наступні два приклади еквівалентні: еквівалентно цьому? Ні. Давайте використовуємо швидкий тест Програма виведе наступне: Так! Умовний оператор здійснюватиме наведення типів, якщо знадобиться. Оскільки в іншому випадку ви очікували, що програма кине NullPointerException? 5. Ви також не отримаєте складовий оператор призначення. Викрутливості достатньо? Давайте розглянемо наступні два фрагменти коду: abstract class Parent { abstract T x(); } class Child extends Parent { @Override String x() { return "abc"; } } // Method descriptor #15 ()Ljava/lang/String; // Stack: 1, Locals: 1 java.lang.String x(); 0 ldc [16] 2 areturn Line numbers: [pc: 0, line: 7] Local variable table: [pc: 0, pc: 3] local: this index: 0 type: Child // Method descriptor #18 ()Ljava/lang/Object; // Stack: 1, Locals: 1 bridge synthetic java.lang.Object x(); 0 aload_0 [this] 1 invokevirtual Child.x() : java.lang.String [19] 4 areturn Line numbers: [pc: 0, line: 1] class Test { int[][] a() { return new int[0][]; } int[] b() [] { return new int[0][]; } int c() [][] { return new int[0][]; } } class Test { int[][] a = {{}}; int[] b[] = {{}}; int c[][] = {{}}; } @Target(ElementType.TYPE_USE) @interface Crazy {} class Test { @Crazy int[][] a1 = {{}}; int @Crazy [][] a2 = {{}}; int[] @Crazy [] a3 = {{}}; @Crazy int[] b1[] = {{}}; int @Crazy [] b2[] = {{}}; int[] b3 @Crazy [] = {{}}; @Crazy int c1[][] = {{}}; int c2 @Crazy [][] = {{}}; int c3[] @Crazy [] = {{}}; } 10 речей яких ви не знали в Java - 2 Object o1 = true ? new Integer(1) : new Double(2.0); Object o2; if (true) o2 = new Integer(1); else o2 = new Double(2.0); System.out.println(o1); System.out.println(o2); 1.0 1 Integer i = new Integer(1); if (i.equals(1)) i = null; Double d = new Double(2.0); Object o = true ? i : d; // NullPointerException! System.out.println(o); i += j; i = i + j; Інтуїтивно, вони мають бути рівними правда? Але знаєте що вони різні. Специфікація JLS каже: Складове вираз типу Е1 ор = Е2 еквівалентно Е1 = (Т) ((Е1) ор (Е2)), де Т це тип Е1, за винятком що Е1 обчислюється лише один раз. Хороший приклад це використовувати *= або /= : byte b = 10; b *= 5.7; System.out.println(b); // prints 57 або: byte b = 100; b /= 2.5; System.out.println(b); // prints 40 або: char ch = '0'; ch *= 1.1; System.out.println(ch); // prints '4' або: char ch = 'A'; ch *= 1.5; System.out.println(ch); // prints 'a' Отже, це досі корисний інструмент? 6. Випадкові цілочисельні числа Тепер важче завдання. Чи не читайте рішення. Подивіться, чи зможете ви знайти відповідь самостійно. Коли я запущу наступну програму: for (int i = 0; i < 10; i++) { System.out.println((Integer) i); } іноді я отримую наступний висновок: 92 221 45 48 236 183 39 193 33 84 Але як таке взагалі можливе? Ок, відповідь у криється в перевизначенні JDK кеша Integer через рефлексію, а потім у використанні auto-boxing і auto-unboxing. Не робіть цього без дозволу дорослих! Або іншими словами: 10 речей яких ви не знали в Java - 3 7. GOTO Одне з моїх найулюбленіших. Java має GOTO! Напишіть це: int goto = 1; і ви отримаєте це: Це тому, що goto це невикористовуване зарезервоване слово, просто про всяк випадок… Але це не найбільш захоплююча частина. Найцікавіше те, що ви можете включити goto в парі з break, continue і помічених блоків: Стрибки вперед У байт коді: Стрибки назад У байт коді: 8. У Java є псевдоніми типів В інших мовах (наприклад Ceylon), ми можемо визначати псевдоніми типів дуже легко: Клас People тут побудований таким чином, що може взаємозамінюватися безліччю Set Test.java:44: error: expected int goto = 1; ^ label: { // do stuff if (check) break label; // do more stuff } 2 iload_1 [check] 3 ifeq 6 // Jumping forward 6 .. label: do { // do stuff if (check) continue label; // do more stuff break label; } while(true); 2 iload_1 [check] 3 ifeq 9 6 goto 2 // Jumping backward 9 .. interface People => Set ; : У Java ми не можемо просто так визначити псевдонім на верхньому рівні Але ми можемо зробити так для потреб класу чи способу. Давайте припустимо, що нас не влаштовують такі імена як Integer, Long і т.д. і ми хочемо імена по коротше: I і L. Так легко: У прикладі вище, Integer перетворений на I для видимості класу Test в той час як Long перетворений на L для потреб методу х(). Тепер ми можемо викликати цей метод так: Звичайно цю техніку не слід сприймати всерйоз. В даному випадку Integer і Long final типи, що означає, що I і L - ефективні перетворення (майже, перетворення йде тільки в одну сторону). Якби ми вирішабо використовувати non-final типи (наприклад, Object), тоді ми могли б обійтися звичайними дженеріками. Погралися трохи та вистачить. Давай перейдемо до чогось справжнього цікавого. 9. Деякі відносини типів нерозв'язні! Добре, зараз буде дійсно цікаво, тому візьміть чашку концентрованої кави і давайте розглянемо наступні два типи: People? p1 = null; Set ? p2 = p1; People? p3 = p2; class Test { void x(I i, L l) { System.out.println( i.intValue() + ", " + l.longValue() ); } } new Test().x(1, 2L);// A helper type. You could also just use List interface Type {} class C implements Type > {} class D

implements Type >>> {} То що С і D взагалі означають? В певному сенсі вони рекурсивні, схоже на рекурсію в java.lang.Enum. Подумайте: З урахуванням зазначених вище специфікацій, фактична реалізація enum – це лише синтаксичний цукор: Думаючи про це давайте повернемося до наших двох типів. Чи компілюється наступний код? Складне питання… і воно фактично не вирішується? Чи є З підтипом Type ? Спробуйте скомпілювати це у вашому Eclipse або Idea і вони розкажуть все, що про вас думають. Спустіть це в стічні труби ... Деякі відносини типів в Java нерозв'язні! 10. Перетин типів У Java є дуже цікава особливість під назвою перетин типів (type intersection). Ви можете оголосити (generic) тип, який є насправді перетином двох типів. Наприклад: Параметр типу Т, який ви зв'язуєте зі зразками класу Test повинен включати в себе як Serializable так і Cloneable інтерфейси. Наприклад, String неможливо обмежити, але Date – будь ласка: Ця особливість знаходить багаторазове використання в Java8, де ви можете наводити типи. Як це допомагає? Практично ніяк, але якщо ви хочете привести своє лямбда-вираз у потрібний вам тип, іншого шляху немає. Допустимо у вас є таке божевільне обмеження у вашому методі: Ви хочете Runnable який в той же час Serializable тільки в тому випадку, якщо хочете виконати це в іншому місці і відправити результат по мережі. Лямбда та серіалізація привносять трохи іронії. Ви можете серіалізувати ваше лямбда-вираз якщо його цільовий тип і аргументи серіалізуються. Але навіть якщо це правильно, вони автоматично не включають Serializable інтерфейс. Ви повинні самостійно наводити їх до цього типу. Але коли ви приводите тільки до Serializable: тоді лямбда більше не буде Runnable, тому приводьте їх до обох типів: І на закінчення: public abstract class Enum > { ... } // This enum MyEnum {} // Is really just sugar for this class MyEnum extends Enum { ... } class Test { Type c = new C(); Type> d = new D (); } Step 0) C Step 1) Type > >? Step 0) D > Step 1) Type >>> > Step 2) D >> Step 3) List >> > Step 4) D > >> Step . . . (expand forever) class Test { } // Doesn't compile Test s = null; // Compiles Test d = null; void execute(T t) {} execute((Serializable) (() -> {}));execute((Runnable & Serializable) (() -> {}));

Java настільки ж потужна, наскільки і загадкова.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ