Javaについて知らなかった10のこと

それで、あなたはJavaの始まりからJavaで働いていますか? 「オーク」と呼ばれた頃、彼らは隅々でOOPについて話していたとき、緩慢な鉱夫がJavaにはチャンスがないと考え、アプレットはクールなものと考えられていた時代を覚えていますか?

私はあなたが私があなたに話すつもりの少なくとも半分を知らなかったに違いない。 Javaの内部機能に関する驚くべき事実を発見しましょう。

1.チェック済み(チェック済み)例外は存在しません


はい、はい! JVMはそれらについて何も知らず、Javaだけが知っています。

今日、チェック例外が悪い考えであることに誰もが同意するでしょう。 Bruce EckelがプラハのGeeCONでの閉会のスピーチで述べたように 、Javaがチェック例外に関連付けられた後の単一言語ではなく、Java 8の新しいStreams APIでも拒否されました( ラムダがI / Oまたはデータベースを使用する場合に問題を引き起こす可能性があります)データ )。

JVMがそれらについて何も知らないことを確認したいですか? このコードを実行します:

public class Test { //  throws:    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; } } 

このプログラムはコンパイルするだけでなく、実際にSQLExceptionスローします。 Lombokの@SneakyThrowsも必要ありません。

これについての詳細は、 ここまたはStack Overflowをご覧ください

2.戻りタイプのみが異なる2つのメソッドを作成できます


そのようなコードはコンパイルされませんよね?

 class Test { Object x() { return "abc"; } String x() { return "123"; } } 

そうだね。 Java言語では、戻り型または宣言された例外が異なっていても、1つのクラスで2つの「同等にオーバーロードされた」メソッドを使用できません。

しかし、ちょっと待ってください。 Class.getMethod(String、Class ...)のドキュメントを読みましょう。 それは言います:
Java言語では同じシグネチャを持つ複数のメソッドを宣言することは禁止されていますが、Java仮想マシンでは戻り型が異なる場合でもこれを許可するため、クラスには複数の適切なメソッドが含まれることがあります。 このような仮想マシンの柔軟性を使用して、言語の一部の機能を実装できます。 たとえば、共変の戻り型は、ブリッジメソッドを使用して実装できます。ブリッジメソッドは、戻り型のみが実際のオーバーロードメソッドと異なります。
おお! はい、それは理にかなっています。 実際、次のように書くとこれが起こります。

 abstract class Parent<T> { abstract T x(); } class Child extends Parent<String> { @Override String x() { return "abc"; } } 

以下は、 Childクラス用に生成されるバイトコードです。

 // 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変換されることは明らかです。 合成ブリッジメソッドはコンパイラによって生成されます。これは、メソッドが呼び出される場所によっては、 ObjectParent.x()戻り値の型として期待されるためです。 ブリッジメソッドなしでジェネリック型を追加し、バイナリ互換性を提供することは困難です。 この可能性をサポートするためにJVMを変更することは、より小さな悪であることが判明しました(そして、共変の戻り値の型が副作用として現れました)。 賢く、はい?

戻り型のオーバーロードに興味がありますか? スタックオーバーフローに関するこの説明をお読みください。

3.これらはすべて2次元配列です!


 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[][] = {{}}; } 

狂気と言う? また、これにJava 8タイプの注釈を追加するとどうなりますか? オプションの数は時々増加します!

 @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.条件付き構文を理解していない


条件式に関するすべてを知っているように思えますか? 失望させていただきます。 ほとんどのプログラマは、次のコードフラグメントは同等であると考えています。

 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 

うん! 条件演算子は、 「necessary」および「necessary」が非常に太字の引用符で囲まれている場合、数値型をキャストします。 結局のところ、このプログラムがNullPointerExceptionをスローすることを期待していませんか?

 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.複合代入演算子も理解していない


信じられない? 次の2行のコードを検討してください。

 i += j; i = i + j; 

直感的には、それらは同等であるはずですよね? サプライズ! 彼らは違います。 JLSが言うように:

E1 op = E2という形式の複合代入演算子は、式E1 =(T)((E1)op(E2))と同等です。ここで、TはE1のタイプであり、E1は1回だけ計算されます。

これはとても美しいので、 スタックオーバーフローに関するピーターローリーの答えを引用したいと思います。

このようなキャストの例は、* =または/ =で表示できます。

 バイトb = 10;
 b * = 5.7;
 System.out.println(b);  // 57を印刷 

または

 バイトb = 100;
 b / = 2.5;
 System.out.println(b);  // 40を印刷 

または

  char ch = '0';
 ch * = 1.1;
 System.out.println(ch);  // '4'を出力します 

または

  char ch = 'A';
 ch * = 1.5;
 System.out.println(ch);  // '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整数キャッシュを書き換えることです。 これを自宅で繰り返そうとしないでください! さて、または休暇前の最後のコミットについて上の写真を覚えておいてください。

7.後藤


そして、ここが私のお気に入りです。 Javaにはgotoがあります! 試してください:

 int goto = 1; 

そして、あなたは得るでしょう:

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

これは、 goto未使用のキーワードであるためです。 念のため、それが重宝します。

しかし、これは最も興味深いものではありません。 最も印象的なのは、実際にbreakを使用してgotoを実装し、ラベルを使用してブロックすることです:

前方にジャンプ:

 label: { // ... - ... if (check) break label; // ... - ... } 

バイトコード内:

 2 iload_1 [check] 3 ifeq 6 //   6 .. 

戻る

 label: do { // ... - ... if (check) continue label; // ... - ... break label; } while(true); 

バイトコード内:

 2 iload_1 [check] 3 ifeq 9 6 goto 2 //   9 .. 


8. Javaには型エイリアスがあります


他の言語、たとえば( Ceylon )では、型のエイリアスを簡単に宣言できます。

 interface People => Set<Person>; 

Peopleタイプは、 Set<Person>代わりにどこでも使用できるように設計されています。

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

Javaでは、型エイリアスをグローバルに宣言することはできません。 しかし、クラスまたはメソッド内でこれを行うことは可能です。 長い名前のIntegerLongなどが気に入らない場合は、代わりに短い名前( IおよびLを使用しますL 簡単:

 class Test<I extends Integer> { <L extends Long> void x(I i, L l) { System.out.println( i.intValue() + ", " + l.longValue() ); } } 

このコードでは、 ITestクラス内のIntegerのエイリアスであり、 Lx()メソッド内のLongのエイリアスです。 このメソッドを安全に呼び出すことができます:

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

もちろん、そのような手法を真剣に考えることはできません。 私たちの場合、 Integer型とLong型はfinalと宣言されています。これは、ジェネリック型IL 効率的なエイリアスであることを意味します(ほとんどの場合、割り当てとの互換性は一方向にのみ機能します)。 finalとして宣言されていない型( Object )を使用した場合、これらは通常のジェネリックになります。

まあ、十分な愚かなトリック。 より深刻な何かの時が来ました!

9.型間のいくつかの関係は計算できません!


さて、今は本当に涼しいので、コーヒーを注いで集中してください。 次のタイプを検討してください。

 //  .     List interface Type<T> {} class C implements Type<Type<? super C>> {} class D<P> implements Type<Type<? super D<D<P>>>> {} 

タイプCD実際にはどういう意味ですか?

ある意味では、再帰的であり、 java.lang.Enumような宣言に(完全ではありませんが)ある程度似ていjava.lang.Enum 。 参照:

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

実際、 enum宣言は構文糖衣です:

 //  enum MyEnum {} //       class MyEnum extends Enum<MyEnum> { ... } 

これを覚えて、元のタイプに戻ります。 そのようなコードはコンパイルされますか?

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

質問は複雑であり、 Ross Tateには答えがあります。 通常、これを判断することは不可能です。

CはType <のサブタイプですか? スーパーC>?
ステップ0) C Type<? super C> Type<? super C>
ステップ1) Type<Type<? super C>> Type<Type<? super C>> Type<? super C> Type<? super C> (継承)
ステップ2) C Type<? super C> Type<? super C> (マスクをチェックしますか?スーパーC)
ステップ...(無限ループ)

Cを使用して単にサイクルを実行する場合、Dを使用するとさらに楽しくなります。
D <Byte>はType <のサブタイプですか? スーパーD <バイト>>?
ステップ0) D<Byte> Type<? super D<Byte>> Type<? super D<Byte>>
ステップ1) Type<Type<? super D<D<Byte>>>> Type<Type<? super D<D<Byte>>>> Type<? super D<Byte>> Type<? super D<Byte>>
ステップ2) D<Byte> Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
ステップ3) Type<Type<? super D<D<Byte>>>> Type<Type<? super D<D<Byte>>>> Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
ステップ4) D<D<Byte>> Type<? super D<D<Byte>>> Type<? super D<D<Byte>>>
ステップ...(無限の成長)

これをEclipseでコンパイルすると、スタックオーバーフローでクラッシュします。 ( 心配しないでください、私はすでにバグトラッカーで報告しました
同意する必要があります:
型間のいくつかの関係は計算できません!

一般的な型の問題に興味がある場合は、Ross Tateの記事「 Java型システムでテンプレートを使いこなす」(AlanLöngおよびSorin Lernerと共著)、またはトピックに関する私たちの考えを読んでください

10.型の交差


Javaには非常に特殊なものがあります-型交差です。 2つの型の共通部分であるジェネリック型を宣言できます。 例:

 class Test<T extends Serializable & Cloneable> { } 

Testクラスの特定のインスタンスでT対応する型は、 SerializableCloneable 両方を実装する必要があります。 たとえば、 Stringは機能しませんが、 Dateは問題ありません。

 //   Test<String> s = null; //  Test<Date> d = null; 

この機能はJava 8で開発されており、タイプを交差に変換できます。 これはどこで役立ちますか? ほとんどどこにもありませんが、ラムダ式をこの型にキャストする必要がある場合、他のオプションはありません。 メソッドにそのような非常識なタイプ制限があるとします:

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

Runnableだけに満足しています。これは、ネットワーク経由で転送して別の場所で実行する場合に備えてSerializableです。 原則として、ラムダシリアル化できます
ターゲット型とキャプチャされた引数がシリアル化可能な場合、ラムダ式をシリアル化できます。
ただし、これが尊重されても、ラムダはSerializableトークンインターフェイスを自動的に実装しません。 キャストが必要です。 しかし、 Serializableのみにつながる場合:

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

ラムダはRunnableできなくなります。

ああ...

残ります...

一度に2つのタイプにつながります。

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

おわりに


通常、これはSQLについてのみ言いますが、次のように記事を終了します。
Javaは神秘的で強力なメカニズムです。 彼の謎よりも急なのはおそらく彼の力でしょう。

Source: https://habr.com/ru/post/J253787/


All Articles