Статьи

10 вещей, которые вы не знали о Java

Итак, вы работали с Java с самого начала? Помните времена, когда он назывался «Дуб», когда ОО все еще оставалась горячей темой, когда люди из C ++ думали, что у Java нет шансов, когда апплеты все еще были чем-то особенным?

Могу поспорить, что вы не знали, по крайней мере, половину из следующих вещей. Давайте начнем на этой неделе с некоторых замечательных сюрпризов о внутренней работе Java.

1. Нет такой вещи как проверенное исключение

Это правильно! JVM не знает ничего подобного, знает только язык Java.

Сегодня все согласны с тем, что проверенные исключения были ошибкой. Как сказал Брюс Экель в своем заключительном выступлении в GeeCON в Праге , ни один другой язык после Java не использовал использование проверенных исключений, и даже Java 8 больше не использует их в новом API-интерфейсе Streams ( что на самом деле может быть немного болезненно, когда ваши лямбды используют IO или JDBC ).

Хотите доказательства того, что JVM не знает ничего подобного? Попробуйте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class Test {
  
    // No throws clause here
    public static void main(String[] args) {
        doThrow(new SQLException());
    }
  
    static void doThrow(Exception e) {
        Test.<RuntimeException> doThrow0(e);
    }
  
    @SuppressWarnings("unchecked")
    static <E extends Exception>
    void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

Мало того, что этот компилятор, он также на самом деле выдает @SneakyThrows , вам даже не нужны @SneakyThrows от @SneakyThrows .

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

Это не компилируется, верно?

1
2
3
4
class Test {
    Object x() { return "abc"; }
    String x() { return "123"; }
}

Правильно. Язык Java не позволяет двум методам быть «эквивалентными переопределению» в одном и том же классе, независимо от их потенциально различающихся throws или return типов.

Но подожди секунду. Проверьте Javadoc Class.getMethod(String, Class...) . Это гласит:

Обратите внимание, что в классе может быть несколько подходящих методов, потому что, хотя язык Java запрещает классу объявлять несколько методов с одной и той же сигнатурой, но разными типами возврата, виртуальная машина Java этого не делает. Эта повышенная гибкость в виртуальной машине может использоваться для реализации различных языковых функций. Например, ковариантные возвраты могут быть реализованы с помощью методов моста; метод моста и переопределяемый метод будут иметь одну и ту же сигнатуру, но разные типы возврата.

Вау, да, это имеет смысл. Фактически, именно это и происходит, когда вы пишете следующее:

1
2
3
4
5
6
7
8
abstract class Parent<T> {
    abstract T x();
}
 
class Child extends Parent<String> {
    @Override
    String x() { return "abc"; }
}

Проверьте сгенерированный байт-код в Child :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// Method descriptor #15 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  java.lang.String x();
    0  ldc <String "abc"> [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]

Итак, T на самом деле просто Object в байтовом коде. Это хорошо понято.

Метод синтетического моста на самом деле генерируется компилятором, поскольку можно ожидать, что возвращаемый тип подписи Parent.x() будет Object на определенных сайтах вызовов. Добавление дженериков без таких методов моста не было бы возможным в двоичном совместимом способе. Таким образом, изменение JVM для учета этой функции было меньшей болью (что также позволяет переопределять коварианты как побочный эффект …) Умно, а?

Вы знакомы с языковой спецификой и внутренностями? Тогда найдите еще несколько очень интересных деталей здесь .

3. Все это двумерные массивы!

1
2
3
4
5
class Test {
    int[][] a()  { return new int[0][]; }
    int[] b() [] { return new int[0][]; }
    int c() [][] { return new int[0][]; }
}

Да, это правда. Даже если ваш ментальный синтаксический анализатор может не сразу понять тип возврата вышеупомянутых методов, они все одинаковы! Аналогично следующему коду:

1
2
3
4
5
class Test {
    int[][] a = {{}};
    int[] b[] = {{}};
    int c[][] = {{}};
}

Ты думаешь, это безумие? Представьте себе, как вы используете аннотации типа JSR-308 / Java 8 выше. Количество синтаксических возможностей взрывается!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@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 [] = {{}};
}

Введите аннотации. Устройство, тайна которого превосходит только его сила

Или другими словами:

Когда я делаю это последний коммит перед моими 4-недельными каникулами

hexhyZ8

Я предоставил вам фактическое упражнение по поиску прецедента для любого из вышеперечисленного.

4. Вы не получаете условное выражение

Итак, вы думали, что знаете все, что касается использования условного выражения? Позвольте мне сказать вам, вы не сделали. Большинство из вас подумают, что два приведенных ниже фрагмента эквивалентны:

1
Object o1 = true ? new Integer(1) : new Double(2.0);

… так же, как это?

1
2
3
4
5
6
Object o2;
 
if (true)
    o2 = new Integer(1);
else
    o2 = new Double(2.0);

Нет. Давайте проведем быстрый тест

1
2
System.out.println(o1);
System.out.println(o2);

Эта программа напечатает:

1
2
1.0
1

Ага! Условный оператор реализует продвижение числового типа, если оно «необходимо» , с очень и очень сильным набором кавычек на этом «нужном» . Потому что, вы ожидаете, что эта программа выдаст NullPointerException ?

1
2
3
4
5
6
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);

5. Вы также не получаете составной оператор присваивания

Причудливый достаточно? Давайте рассмотрим следующие две части кода:

1
2
i += j;
i = i + j;

Интуитивно, они должны быть эквивалентны, верно? Но угадайте что. Это не так! JLS определяет:

Составное выражение присваивания в форме E1 op = E2 эквивалентно E1 = (T) ((E1) op (E2)), где T — это тип E1, за исключением того, что E1 оценивается только один раз.

Это так прекрасно, я хотел бы привести ответ Питера Лори на этот вопрос переполнения стека :

Хорошим примером этого кастинга является использование * = или / =

1
2
3
byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57

или же

1
2
3
byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40

или же

1
2
3
char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'

или же

1
2
3
char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'

Теперь, как это невероятно полезно? Я собираюсь разыграть / умножить символы прямо в моем приложении. Потому что, ты знаешь …

6. Случайные целые числа

Теперь это больше загадка. Пока не читайте решение. Посмотрите, сможете ли вы найти это сами. Когда я запускаю следующую программу:

1
2
3
for (int i = 0; i < 10; i++) {
  System.out.println((Integer) i);
}

… Тогда «иногда» я получаю следующий вывод:

01
02
03
04
05
06
07
08
09
10
92
221
45
48
236
183
39
193
33
84

Как это вообще возможно??

,

,

,

,

,

, спойлер… решение впереди…

,

,

,

,

,

Хорошо, решение уже здесь и связано с переопределением кеша Integer JDK с помощью отражения, а затем с использованием автобокса и авторазблокирования. Не делай этого дома! Или, другими словами, давайте подумаем об этом так, еще раз

Когда я делаю это последний коммит перед моими 4-недельными каникулами

hexhyZ8 (1)

7. GOTO

Это один из моих любимых. У Java есть GOTO! Введи это…

1
int goto = 1;

Это приведет к:

1
2
3
Test.java:44: error: <identifier> expected
    int goto = 1;
       ^

Это потому, что gotoнеиспользуемое ключевое слово , на всякий случай …

Но это не самая захватывающая часть. Самое интересное, что вы можете реализовать goto с блоками break , continue и маркированными:

Прыгать вперед

1
2
3
4
5
label: {
  // do stuff
  if (check) break label;
  // do more stuff
}

В байт-коде:

1
2
3
2  iload_1 [check]
3  ifeq 6          // Jumping forward
6  ..

Прыгать назад

1
2
3
4
5
6
label: do {
  // do stuff
  if (check) continue label;
  // do more stuff
  break label;
} while(true);

В байт-коде:

1
2
3
4
2  iload_1 [check]
 3  ifeq 9
 6  goto 2          // Jumping backward
 9  ..

8. У Java есть псевдонимы типов

В других языках ( например, Цейлон ) мы можем очень легко определить псевдонимы типов:

1
interface People => Set<Person>;

Тип People созданный таким образом, может затем использоваться взаимозаменяемо с Set<Person> :

1
2
3
People?      p1 = null;
Set<Person>? p2 = p1;
People?      p3 = p2;

В Java мы не можем определять псевдонимы типов на верхнем уровне. Но мы можем сделать это для области действия класса или метода. Давайте рассмотрим, что мы недовольны именами Integer , Long т. Д., Нам нужны более короткие имена: I и L Легко:

1
2
3
4
5
6
7
8
class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " +
            l.longValue()
        );
    }
}

В приведенной выше программе Integer «псевдоним» для I для области видимости класса Test , в то время как Long «псевдоним» для L для области видимости метода x() . Затем мы можем вызвать вышеуказанный метод следующим образом:

1
new Test().x(1, 2L);

Эту технику, конечно, нельзя воспринимать всерьез. В этом случае Integer и Long оба являются окончательными типами, что означает, что типы I и L являются фактически псевдонимами (почти. Совместимость присваивания идет только в одну сторону). Если бы мы использовали не финальные типы (например, Object ), то мы бы действительно использовали обычные дженерики.

Хватит этих глупых уловок. Теперь о чем-то действительно замечательном!

9. Некоторые типы отношений неразрешимы!

Ладно, теперь все будет очень весело, так что возьмите чашку кофе и сконцентрируйтесь. Рассмотрим следующие два типа:

1
2
3
4
5
// A helper type. You could also just use List
interface Type<T> {}
 
class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

Теперь, что вообще означают типы C и D ?

Они несколько рекурсивные, похожим (но слегка отличающимся) способом, которым java.lang.Enum является рекурсивным. Рассматривать:

1
public abstract class Enum<E extends Enum<E>> { ... }

С приведенной выше спецификацией фактическая реализация enum является просто синтаксическим сахаром:

1
2
3
4
5
// This
enum MyEnum {}
 
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }

Имея это в виду, давайте вернемся к нашим двум типам. Компилируется ли следующее?

1
2
3
4
class Test {
    Type<? super C> c = new C();
    Type<? super D<Byte>> d = new D<Byte>();
}

Трудный вопрос, и у Росса Тэйта есть ответ на него. Вопрос на самом деле неразрешимый:

Является ли C подтипом типа <? супер C>?

1
2
3
4
Step 0) C <?: Type<? super C>
Step 1) Type<Type<? super C>> <?: Type (inheritance)
Step 2) C  (checking wildcard ? super C)
Step . . . (cycle forever)

А потом:

Является ли D подтипом типа <? супер D <Байт >>?

1
2
3
4
5
6
Step 0) D<Byte> <?: Type<? super C<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
Step 2) D<Byte> <?: Type<? super D<D<Byte>>>
Step 3) List<List<? super C<C>>> <?: List<? super C<C>>
Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
Step . . . (expand forever)

Попробуйте скомпилировать все вышеперечисленное в своем Eclipse, оно рухнет! ( не волнуйтесь. Я подал ошибку )

Пусть этот тонет в …

Некоторые типы отношений в Java неразрешимы !

Если вам интересны более подробные сведения об этой своеобразной причуде Java, прочитайте статью Росса Тэйта «Укрощение подстановочных знаков в системе типов Java» (в соавторстве с Аланом Ленгом и Сорином Лернером), а также наши собственные размышления о сопоставлении полиморфизма подтипа с универсальным полиморфизмом.

10. Тип пересечения

У Java есть очень специфическая особенность, называемая пересечениями типов. Вы можете объявить (универсальный) тип, который фактически является пересечением двух типов. Например:

1
2
class Test<T extends Serializable & Cloneable> {
}

Параметр универсального типа T который вы связываете с экземплярами класса Test должен реализовывать как Serializable и Cloneable . Например, String не является возможной границей, но Date :

1
2
3
4
5
// Doesn't compile
Test<String> s = null;
 
// Compiles
Test<Date> d = null;

Эта функция была повторно использована в Java 8, где теперь вы можете приводить типы к специальным пересечениям типов. Чем это полезно? Почти нет, но если вы хотите привести лямбда-выражение в такой тип, другого пути нет. Предположим, у вас есть сумасшедшее ограничение типа для вашего метода:

1
<T extends Runnable & Serializable> void execute(T t) {}

Вам нужен Runnable который также Serializable на тот случай, если вы хотите выполнить его где-то еще и отправить по сети. Лямбды и сериализация немного странны.

Лямбды можно сериализовать :

Вы можете сериализовать лямбда-выражение, если его целевой тип и захваченные аргументы являются сериализуемыми

Но даже если это так, они не реализуют интерфейс маркера Serializable автоматически. Чтобы привести их к этому типу, вы должны сыграть. Но когда вы используете только Serializable

1
execute((Serializable) (() -> {}));

… тогда лямбда больше не будет работать.

Egh …

Так…

Приведите его к обоим типам:

1
execute((Runnable & Serializable) (() -> {}));

Вывод

Я обычно говорю это только о SQL, но пришло время завершить статью со следующим:

Java — это устройство, тайна которого превосходит только его мощь.

Ссылка: 10 вещей, которые вы не знали о Java от нашего партнера JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .