JavaRush /Java Blog /Random-IT /10 cose che non sapevi su Java
minuteman
Livello 32

10 cose che non sapevi su Java

Pubblicato nel gruppo Random-IT
Allora, hai iniziato di recente a lavorare con Java? Ricordi i tempi in cui si chiamava "Oak", quando l'orientamento agli oggetti era ancora un tema caldo, quando gli utenti del C++ pensavano che Java non avesse alcuna possibilità e quando nessuno aveva nemmeno sentito parlare di applet? Presumo che tu non sappia nemmeno la metà delle seguenti cose. Iniziamo la settimana con alcune belle sorprese sul funzionamento interno di Java. 10 cose che non sapevi su Java - 11. Non esiste un'eccezione verificata. Giusto! La JVM non ha idea di una cosa del genere, solo il linguaggio Java lo fa. Oggi tutti concordano nel ritenere che le eccezioni verificate siano state un errore. Come ha detto Bruce Eckel nel suo intervento finale al GeeCON di Praga, nessun altro linguaggio poiché Java utilizza le eccezioni controllate, anche Java 8 non le copre più nella nuova API Streams (il che può essere un po' fastidioso quando le tue lambda utilizzano IO o JDBC ). Vuoi la prova che la JVM non sa una cosa del genere? Prova il seguente codice: Non solo verrà compilato, ma lancerà anche un'eccezione SQLException, non è nemmeno necessario utilizzare @SneakyThrows di Lombok per questo. 2. È possibile avere metodi sovraccaricati che differiscono solo nei tipi restituitipublic 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; } } Questo non verrà compilato, giusto? class Test { Object x() { return "abc"; } String x() { return "123"; } } Giusto. Il linguaggio Java non consente l'override simultaneo di due metodi all'interno della stessa classe, indipendentemente dalle differenze nei lanci o nei tipi restituiti. Ma aspetta un attimo. Controlla di nuovo la documentazione per Class.getMethod(String, Class…). Dice: Nota che è possibile che ci sia più di un metodo corrispondente in una classe, perché mentre il linguaggio Java proibisce più metodi con la stessa firma ma tipi di ritorno diversi, la Java Virtual Machine no. Questa flessibilità in una macchina virtuale può essere utilizzata per implementare varie funzionalità linguistiche. Ad esempio, i rendimenti covarianti possono essere ottenuti con i metodi bridge; Il metodo bridge e il metodo sottoposto a override avrebbero la stessa firma ma tipi di restituzione diversi. Wow, ha senso. In realtà c'è molto da fare quando scrivi quanto segue: Guarda il bytecode generato: Quindi t è effettivamente un oggetto nel bytecode. Questo è ben compreso. Il metodo bridge sintetico viene effettivamente generato dal compilatore perché è possibile prevedere il tipo restituito di Parent.x() in alcune parti delle chiamate. L'aggiunta di farmaci generici senza tali metodi bridge non sarà più possibile nella rappresentazione binaria. Quindi, modificare la JVM per consentire tale funzionalità ha prodotto meno problemi (il che consente anche l'override del metodo covariante come effetto collaterale...) Intelligente, giusto? 3. Tutti i seguenti sono array bidimensionali. Questo è effettivamente vero. Anche se il tuo analizzatore mentale non riesce a capire immediatamente il tipo di ritorno dei metodi sopra descritti, sono tutti uguali! Come il prossimo pezzo di codice. Pensi che sia pazzesco? Anche il numero di opportunità per scrivere fa esplodere semplicemente l'immaginazione! Digitare l'annotazione. Un dispositivo il cui mistero è secondo solo alla sua potenza. O in altre parole: quando prendo il mio ultimo impegno poco prima delle mie 4 settimane di vacanza. Ti do il permesso di usarlo nel modo che preferisci. 4. Non riceverai il condizionale Quindi, pensavi di sapere già tutto sui condizionali quando hai iniziato a usarli? Permettimi di deluderti: ti sbagliavi. La maggior parte di voi penserà che i due esempi seguenti siano equivalenti: equivalente a questo? NO. Usiamo un test veloce. Il programma restituirà quanto segue: Sì! L'operatore condizionale eseguirà i cast del tipo, se necessario. Perché altrimenti ti aspetteresti che il programma lanci una NullPointerException? 5. Inoltre non otterrai un operatore di assegnazione composto. L'intraprendenza è sufficiente? Diamo un'occhiata ai seguenti due frammenti di codice: 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 cose che non sapevi su 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; Intuitivamente dovrebbero essere uguali, giusto? Ma sai una cosa: sono diversi. La specifica JLS dice: Un'espressione composta di tipo E1 op = E2 è equivalente a E1 = (T) ((E1) op (E2)), dove T è il tipo di E1, tranne per il fatto che E1 viene valutato solo una volta. Un buon esempio è usare *= or /= : byte b = 10; b *= 5.7; System.out.println(b); // prints 57 or: or byte b = 100; b /= 2.5; System.out.println(b); // prints 40 : char ch = '0'; ch *= 1.1; System.out.println(ch); // prints '4' or: char ch = 'A'; ch *= 1.5; System.out.println(ch); // prints 'a' Quindi, è ancora uno strumento utile? 6. Interi casuali Ora passiamo a un compito più difficile. Non leggere la soluzione. Vedi se riesci a trovare la risposta da solo. Quando eseguo il seguente programma: for (int i = 0; i < 10; i++) { System.out.println((Integer) i); } A volte ottengo il seguente output: 92 221 45 48 236 183 39 193 33 84 Ma come è possibile? Ok, la risposta sta nell'override della cache dei numeri interi di JDK tramite riflessione e quindi nell'utilizzo dell'auto-boxing e dell'auto-unboxing. Non farlo senza il permesso di un adulto! O in altre parole: 10 cose che non sapevi su Java - 3 7. GOTO Uno dei miei preferiti. Java ha GOTO! Scrivi questo: int goto = 1; e ottieni questo: Questo perché goto è una parola riservata inutilizzata, per ogni evenienza... Ma non è questa la parte emozionante. La cosa interessante è che puoi includere goto abbinato a break, continue e blocchi contrassegnati: Salto in avanti Nel byte code: Salto all'indietro Nel byte code: 8. Java ha alias di tipo In altri linguaggi (come Ceylon), possiamo definire il tipo alias molto semplici: la classe People qui è costruita in modo tale da poter essere scambiata con una 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 ; : In Java non possiamo semplicemente definire un alias al livello più alto. Ma possiamo farlo per le esigenze di una classe o di un metodo. Supponiamo che non siamo soddisfatti di nomi come Integer, Long, ecc. e vogliamo nomi più brevi: I e L. Facile: Nell'esempio sopra, Integer viene convertito in I per la visibilità della classe Test, mentre Long viene convertito in L per le esigenze del metodo x(). Ora possiamo chiamare questo metodo in questo modo: Naturalmente, questa tecnica non dovrebbe essere presa sul serio. In questo caso, Integer e Long sono tipi finali, il che significa che I e L sono conversioni efficienti (quasi la conversione avviene solo in una direzione). Se decidessimo di utilizzare tipi non finali (ad esempio Object), potremmo cavarcela con i generici regolari. Abbiamo giocato un po' e questo è bastato. Passiamo a qualcosa di veramente interessante. 9. Alcune relazioni di tipo sono indecidibili! Ok, ora la cosa diventerà davvero interessante, quindi prendi una tazza di caffè concentrato e diamo un'occhiata ai due tipi seguenti: 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 >>> {} Allora cosa significano C e D? In un certo senso sono ricorsivi, simili alla ricorsione in java.lang.Enum. Considera: date le specifiche di cui sopra, l'effettiva implementazione di enum è solo zucchero sintattico: con questo in mente, torniamo ai nostri due tipi. Il seguente codice verrà compilato? Una domanda difficile... e in realtà non è stata risolta? C è un sottotipo di Type ? Prova a compilarlo nel tuo Eclipse o Idea e ti diranno cosa pensano. Buttatelo nello scarico... Alcune relazioni di tipo in Java sono indecidibili! 10. Intersezione dei tipi Java ha una caratteristica molto interessante chiamata intersezione dei tipi. Puoi dichiarare un tipo (generico) che è in realtà l'intersezione di due tipi. Ad esempio: il parametro di tipo personalizzato T associato alle istanze della classe Test deve includere entrambe le interfacce Serializable e Cloneable. Ad esempio, String non può essere limitato, ma Date sì: questa funzionalità ha molteplici usi in Java8, dove è possibile eseguire il cast dei tipi. In che modo questo aiuta? Quasi nulla, ma se vuoi trasformare la tua espressione lambda nel tipo che ti serve, non c'è altro modo. Diciamo che hai una limitazione così folle nel tuo metodo: vuoi Runnable che allo stesso tempo è serializzabile solo se vuoi eseguirlo in un altro posto e inviare il risultato in rete. Lambda e serializzazione aggiungono un po' di ironia. Puoi serializzare l'espressione lambda se il tipo di destinazione e gli argomenti sono serializzabili. Ma anche se ciò fosse vero, non abilitano automaticamente l'interfaccia Serializable. Devi portarli tu stesso a questo tipo. Ma quando esegui il cast solo su Serializable: allora lambda non sarà più Runnable, quindi lanciali su entrambi i tipi: E in conclusione: 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 è tanto potente quanto misterioso.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION