JavaRush /Java Blog /Random-IT /Polimorfismo e suoi amici
Viacheslav
Livello 3

Polimorfismo e suoi amici

Pubblicato nel gruppo Random-IT
Il polimorfismo è uno dei principi base della programmazione orientata agli oggetti. Ti consente di sfruttare la potenza della digitazione forte di Java e di scrivere codice utilizzabile e gestibile. Di lui si è detto molto, ma spero che tutti possano trarre qualcosa di nuovo da questa recensione.
Il polimorfismo e i suoi amici - 1

introduzione

Penso che tutti sappiamo che il linguaggio di programmazione Java appartiene a Oracle. Il nostro percorso inizia quindi dal sito: www.oracle.com . C'è un "Menu" nella pagina principale. In esso, nella sezione "Documentazione" è presente una sottosezione "Java". Tutto ciò che riguarda le funzioni di base del linguaggio appartiene alla "documentazione Java SE", quindi selezioniamo questa sezione. Si aprirà la sezione della documentazione per l'ultima versione, ma per ora la sezione "Cerchi una versione diversa?" Scegliamo l'opzione: JDK8. Nella pagina vedremo molte opzioni diverse. Ma a noi interessa Imparare la lingua: " Percorsi didattici Tutorial Java ". In questa pagina troveremo un'altra sezione: " Imparare il linguaggio Java ". Questo è il più sacro dei santi, un tutorial sulle nozioni di base di Java da Oracle. Java è un linguaggio di programmazione orientato agli oggetti (OOP), quindi l'apprendimento del linguaggio anche sul sito Oracle inizia con una discussione dei concetti di base dei “ Concetti di programmazione orientata agli oggetti ”. Dal nome stesso è chiaro che Java si concentra sul lavoro con gli oggetti. Dalla sottosezione " Che cos'è un oggetto? ", è chiaro che gli oggetti in Java sono costituiti da stato e comportamento. Immaginiamo di avere un conto bancario. La quantità di denaro sul conto è uno stato e i metodi per lavorare con questo stato sono un comportamento. Gli oggetti devono essere descritti in qualche modo (indicare quale stato e comportamento possono avere) e questa descrizione è la classe . Quando creiamo un oggetto di qualche classe, specifichiamo questa classe e questa è chiamata " tipo di oggetto ". Quindi si dice che Java è un linguaggio fortemente tipizzato, come affermato nelle specifiche del linguaggio Java nella sezione " Capitolo 4. Tipi, valori e variabili ". Il linguaggio Java segue i concetti OOP e supporta l'ereditarietà utilizzando la parola chiave extends. Perché l'espansione? Perché con l'ereditarietà, una classe figlia eredita il comportamento e lo stato della classe genitore e può completarli, ad es. estendere la funzionalità della classe base. Un'interfaccia può anche essere specificata nella descrizione della classe utilizzando la parola chiave implements. Quando una classe implementa un'interfaccia, significa che la classe è conforme a un contratto: una dichiarazione del programmatore al resto dell'ambiente che la classe ha un determinato comportamento. Ad esempio, il lettore ha vari pulsanti. Questi pulsanti costituiscono un'interfaccia per controllare il comportamento del lettore e il comportamento modificherà lo stato interno del lettore (ad esempio, il volume). In questo caso, lo stato e il comportamento come descrizione daranno una classe. Se una classe implementa un'interfaccia, allora un oggetto creato da questa classe può essere descritto da un tipo non solo dalla classe, ma anche dall'interfaccia. Diamo un'occhiata ad un esempio:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
Il tipo è una descrizione molto importante. Indica come lavoreremo con l'oggetto, ad es. quale comportamento ci aspettiamo dall'oggetto. I comportamenti sono metodi. Pertanto, comprendiamo i metodi. Sul sito Web Oracle, i metodi hanno una propria sezione nel Tutorial Oracle: " Definizione dei metodi ". La prima cosa da togliere dall'articolo: una firma del metodo è il nome del metodo e i tipi di parametri :
Il polimorfismo e i suoi amici - 2
Ad esempio, quando si dichiara un metodo public void (Oggetto o), la firma sarà il nome del metodo e il tipo del parametro Oggetto. Il tipo di reso NON è incluso nella firma. È importante! Successivamente, compiliamo il nostro codice sorgente. Come sappiamo, per questo il codice deve essere salvato in un file con il nome della classe e l'estensione java. Il codice Java viene compilato utilizzando il compilatore " javac " in un formato intermedio che può essere eseguito dalla Java Virtual Machine (JVM). Questo formato intermedio si chiama bytecode ed è contenuto nei file con estensione .class. Eseguiamo il comando per compilare: javac MusicPlayer.java dopo che il codice Java è stato compilato, possiamo eseguirlo. Utilizzando l'utility " java " per l'avvio, verrà avviato il processo della macchina virtuale Java per eseguire il bytecode passato nel file di classe. Eseguiamo il comando per avviare l'applicazione: java MusicPlayer. Vedremo sullo schermo il testo specificato nel parametro di input del metodo println. È interessante notare che, avendo il bytecode in un file con estensione .class, possiamo visualizzarlo utilizzando l'utilità " javap ". Eseguiamo il comando <ocde>javap -c MusicPlayer:
Polimorfismo e suoi amici - 3
Dal bytecode possiamo vedere che la chiamata di un metodo tramite un oggetto di cui è stato specificato il tipo della classe viene eseguita utilizzando invokevirtual, e il compilatore ha calcolato quale firma del metodo deve essere utilizzata. Perché invokevirtual? Perché c'è una chiamata (invoke è tradotto come chiamata) di un metodo virtuale. Cos'è un metodo virtuale? Questo è un metodo il cui corpo può essere sovrascritto durante l'esecuzione del programma. Immagina semplicemente di avere un elenco di corrispondenze tra una determinata chiave (firma del metodo) e il corpo (codice) del metodo. E questa corrispondenza tra la chiave e il corpo del metodo può cambiare durante l'esecuzione del programma. Pertanto il metodo è virtuale. Per impostazione predefinita, in Java, i metodi che NON sono statici, NON finali e NON privati ​​sono virtuali. Grazie a ciò Java supporta il principio di programmazione orientata agli oggetti del polimorfismo. Come avrai già capito, questo è ciò di cui tratta la nostra recensione oggi.

Polimorfismo

Sul sito Oracle nel loro Tutorial ufficiale c'è una sezione separata: " Polimorfismo ". Usiamo Java Online Compiler per vedere come funziona il polimorfismo in Java. Ad esempio, abbiamo una classe astratta Number che rappresenta un numero in Java. Cosa consente? Ha alcune tecniche di base che tutti gli eredi avranno. Chiunque erediti da Numero dice letteralmente: "Io sono un numero, puoi lavorare con me come numero". Ad esempio, per qualsiasi successore è possibile utilizzare il metodo intValue() per ottenere il valore intero. Se guardi l'API Java di Number, puoi vedere che il metodo è astratto, ovvero ogni successore di Number deve implementare questo metodo da solo. Ma cosa ci dà questo? Diamo un'occhiata ad un esempio:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
Come si vede dall'esempio, grazie al polimorfismo possiamo scrivere un metodo che accetterà argomenti di qualsiasi tipo come input, che sarà un discendente di Number (non possiamo ottenere Number, perché è una classe astratta). Come nel caso dell'esempio del giocatore, in questo caso stiamo dicendo che vogliamo lavorare con qualcosa, come Number. Sappiamo che chiunque sia un Numero deve essere in grado di fornire il suo valore intero. E questo ci basta. Non vogliamo entrare nei dettagli dell'implementazione di un oggetto specifico e vogliamo lavorare con questo oggetto attraverso metodi comuni a tutti i discendenti di Number. L'elenco dei metodi che saranno a nostra disposizione sarà determinato in base al tipo in fase di compilazione (come abbiamo visto in precedenza in bytecode). In questo caso, il nostro tipo sarà Number. Come puoi vedere dall'esempio, stiamo passando numeri diversi di tipo diverso, ovvero il metodo summ riceverà Integer, Long e Double come input. Ma ciò che hanno tutti in comune è che sono discendenti del Numero astratto e quindi sovrascrivono il loro comportamento nel metodo intValue, perché ogni tipo specifico sa come eseguire il cast di quel tipo su Integer. Tale polimorfismo viene attuato attraverso il cosiddetto overriding, in inglese Overriding.
Il polimorfismo e i suoi amici - 4
Polimorfismo prevalente o dinamico. Quindi, iniziamo salvando il file HelloWorld.java con il seguente contenuto:
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
Facciamo javac HelloWorld.javae javap -c HelloWorld:
Polimorfismo e suoi amici - 5
Come puoi vedere, nel bytecode delle righe con chiamata al metodo è indicato lo stesso riferimento al metodo per la chiamata invokevirtual (#6). Facciamolo java HelloWorld. Come possiamo vedere, le variabili parent e child sono dichiarate con tipo Parent, ma l'implementazione stessa viene chiamata a seconda di quale oggetto è stato assegnato alla variabile (cioè che tipo di oggetto). Durante l'esecuzione del programma (dicono anche in runtime), la JVM, a seconda dell'oggetto, quando chiamava metodi utilizzando la stessa firma, eseguiva metodi diversi. Cioè, utilizzando la chiave della firma corrispondente, abbiamo prima ricevuto il corpo del metodo e poi ne abbiamo ricevuto un altro. A seconda dell'oggetto presente nella variabile. Questa determinazione al momento dell'esecuzione del programma di quale metodo verrà chiamato è anche chiamata associazione tardiva o associazione dinamica. Cioè, la corrispondenza tra la firma e il corpo del metodo viene eseguita dinamicamente, a seconda dell'oggetto su cui viene chiamato il metodo. Naturalmente non è possibile sovrascrivere i membri statici di una classe (Membro della classe), così come i membri della classe con tipo di accesso privato o finale. Anche le annotazioni @Override vengono in aiuto degli sviluppatori. Aiuta il compilatore a capire che a questo punto sovrascriveremo il comportamento di un metodo antenato. Se abbiamo commesso un errore nella firma del metodo, il compilatore ce lo avviserà immediatamente. Per esempio:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
Non viene compilato con errore: errore: il metodo non sovrascrive né implementa un metodo da un supertipo
Il polimorfismo e i suoi amici - 6
La ridefinizione è anche associata al concetto di “ covarianza ”. Diamo un'occhiata ad un esempio:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
Nonostante l'apparente astrusità, il significato si riduce al fatto che in caso di override possiamo restituire non solo il tipo specificato nell'antenato, ma anche un tipo più specifico. Ad esempio, l'antenato ha restituito Numero e noi possiamo restituire Integer, il discendente di Numero. Lo stesso vale per le eccezioni dichiarate nei lanci del metodo. Gli eredi possono sovrascrivere il metodo e perfezionare l'eccezione generata. Ma non possono espandersi. Cioè, se il genitore lancia una IOException, allora possiamo lanciare la più precisa EOFException, ma non possiamo lanciare un'eccezione. Allo stesso modo, non è possibile restringere l’ambito né imporre ulteriori restrizioni. Ad esempio, non è possibile aggiungere static.
Il polimorfismo e i suoi amici - 7

Nascondersi

Esiste anche il concetto di “ occultamento ”. Esempio:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
Questa è una cosa abbastanza ovvia se ci pensi. I membri statici di una classe appartengono alla classe, ad es. al tipo di variabile. Pertanto, è logico che se child è di tipo Parent, il metodo verrà chiamato su Parent e non su child. Se osserviamo il bytecode, come abbiamo fatto in precedenza, vedremo che il metodo statico viene chiamato utilizzando invokestatic. Ciò spiega alla JVM che deve esaminare il tipo e non la tabella dei metodi, come hanno fatto invokevirtual o invokeinterface.
Il polimorfismo e i suoi amici - 8

Metodi di sovraccarico

Cos'altro vediamo nel Java Oracle Tutorial? Nella sezione precedentemente studiata " Definizione dei metodi " c'è qualcosa sull'overloading. Cos'è? In russo questo è "sovraccarico del metodo", e tali metodi sono chiamati "sovraccarico". Quindi, sovraccarico del metodo. A prima vista, tutto è semplice. Apriamo un compilatore Java online, ad esempio tutorialspoint online java compiler .
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
Quindi, qui tutto sembra semplice. Come affermato nel tutorial di Oracle, i metodi sovraccaricati (in questo caso il metodo say) differiscono nel numero e nel tipo di argomenti passati al metodo. Non puoi dichiarare lo stesso nome e lo stesso numero di tipi identici di argomenti, perché il compilatore non sarà in grado di distinguerli l'uno dall'altro. Vale subito la pena notare una cosa molto importante:
Il polimorfismo e i suoi amici - 9
Cioè, durante l'overload, il compilatore ne verifica la correttezza. È importante. Ma come fa effettivamente il compilatore a determinare che è necessario chiamare un determinato metodo? Utilizza la regola "il metodo più specifico" descritta nelle specifiche del linguaggio Java: " 15.12.2.5. Scelta del metodo più specifico ". Per dimostrare come funziona, prendiamo un esempio da Oracle Certified Professional Java Programmer:
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
Prendi un esempio da qui: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... Come puoi vedere, stiamo passando null per il metodo. Il compilatore tenta di determinare il tipo più specifico. L'oggetto non è adatto perché tutto è ereditato da lui. Andare avanti. Esistono 2 classi di eccezioni. Diamo un'occhiata a java.io.IOException e vediamo che c'è una FileNotFoundException in "Direct Known Subclasses". Cioè, risulta che FileNotFoundException è il tipo più specifico. Pertanto, il risultato sarà l'output della stringa "FileNotFoundException". Ma se sostituiamo IOException con EOFException, risulta che abbiamo due metodi allo stesso livello della gerarchia nell'albero dei tipi, ovvero per entrambi IOException è il genitore. Il compilatore non sarà in grado di scegliere quale metodo chiamare e genererà un errore di compilazione: reference to method is ambiguous. Un altro esempio:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
Verrà restituito 1. Non ci sono domande qui. Il tipo int... è un vararg https://docs.oracle.com/javase/8/docs/technotes/guides/lingual/varargs.html e in realtà non è altro che "zucchero sintattico" ed è in realtà un int. .. array può essere letto come int[] array. Se ora aggiungiamo un metodo:
public static void method(long a, long b) {
	System.out.println("2");
}
Quindi verrà visualizzato non 1, ma 2, perché stiamo passando 2 numeri e 2 argomenti sono una corrispondenza migliore di un array. Se aggiungiamo un metodo:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
Quindi vedremo ancora 2. Perché in questo caso le primitive hanno una corrispondenza più esatta rispetto al boxing in Integer. Tuttavia, se eseguiamo, method(new Integer(1), new Integer(2));stamperà 3. I costruttori in Java sono simili ai metodi e poiché possono essere utilizzati anche per ottenere una firma, ad essi si applicano le stesse regole di "risoluzione dell'overloading" dei metodi sovraccarichi. Le specifiche del linguaggio Java ce lo dicono in " 8.8.8. Constructor Overloading ". Sovraccarico del metodo = associazione anticipata (nota anche come associazione statica) Spesso si sente parlare di associazione anticipata e tardiva, nota anche come associazione statica o associazione dinamica. La differenza tra loro è molto semplice. L'inizio è la compilazione, il ritardo è il momento in cui il programma viene eseguito. Pertanto, l'associazione anticipata (associazione statica) è la determinazione di quale metodo verrà chiamato e su chi al momento della compilazione. Bene, l'associazione tardiva (associazione dinamica) è la determinazione di quale metodo chiamare direttamente al momento dell'esecuzione del programma. Come abbiamo visto in precedenza (quando abbiamo cambiato IOException in EOFException), se sovraccarichiamo i metodi in modo tale che il compilatore non possa capire dove effettuare quale chiamata, otterremo un errore in fase di compilazione: il riferimento al metodo è ambiguo. La parola ambiguo tradotta dall'inglese significa ambiguo o incerto, impreciso. Si scopre che il sovraccarico si lega presto, perché il controllo viene eseguito in fase di compilazione. Per confermare le nostre conclusioni, apriamo la Specifica del linguaggio Java al capitolo " 8.4.9. Overloading ":
Polimorfismo e suoi amici - 10
Risulta che durante la compilazione, le informazioni sui tipi e sul numero di argomenti (disponibili al momento della compilazione) verranno utilizzate per determinare la firma del metodo. Se il metodo è uno dei metodi dell'oggetto (ovvero, il metodo di istanza), la chiamata effettiva al metodo verrà determinata in fase di esecuzione utilizzando la ricerca dinamica del metodo (ovvero, associazione dinamica). Per renderlo più chiaro, prendiamo un esempio simile a quello discusso in precedenza:
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
Salviamo questo codice nel file HelloWorld.java e compiliamolo utilizzando javac HelloWorld.java Ora vediamo cosa ha scritto il nostro compilatore nel bytecode eseguendo il comando: javap -verbose HelloWorld.
Il polimorfismo e i suoi amici - 11
Come affermato, il compilatore ha determinato che in futuro verrà chiamato un metodo virtuale. Cioè, il corpo del metodo verrà definito in fase di esecuzione. Ma al momento della compilazione, tra tutti e tre i metodi, il compilatore ha scelto quello più adatto, quindi ha indicato il numero:"invokevirtual #13"
Polimorfismo e suoi amici - 12
Che tipo di metodo è questo? Questo è un collegamento al metodo. In parole povere, questo è un indizio grazie al quale, in fase di esecuzione, la Java Virtual Machine può effettivamente determinare quale metodo cercare per eseguire. Maggiori dettagli possono essere trovati nel super articolo: " In che modo JVM gestisce l'overloading e l'override del metodo internamente ".

Riassumendo

Quindi, abbiamo scoperto che Java, come linguaggio orientato agli oggetti, supporta il polimorfismo. Il polimorfismo può essere statico (legame statico) o dinamico (legame dinamico). Con il polimorfismo statico, noto anche come associazione anticipata, il compilatore determina quale metodo deve essere chiamato e dove. Ciò consente l'uso di un meccanismo come il sovraccarico. Con il polimorfismo dinamico, noto anche come associazione tardiva, basato sulla firma precedentemente calcolata di un metodo, un metodo verrà calcolato in fase di esecuzione in base all'oggetto utilizzato (ovvero, quale metodo dell'oggetto viene chiamato). Il funzionamento di questi meccanismi può essere visto utilizzando il bytecode. L'overload esamina le firme del metodo e, quando si risolve l'overload, viene scelta l'opzione più specifica (più accurata). L'override esamina il tipo per determinare quali metodi sono disponibili e i metodi stessi vengono chiamati in base all'oggetto. Oltre ai materiali sull'argomento: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION