Perché Java non consente la presenza di definizioni di funzioni al di fuori della classe?


22

A differenza del C ++, in Java, non possiamo avere solo dichiarazioni di funzioni nella classe e definizioni al di fuori della classe. Perché è così?

È da sottolineare che un singolo file in Java dovrebbe contenere solo una classe e nient'altro?



1
Per definizione intendi le proprietà o le firme dei metodi nei file di intestazione?
Deco

Una buona risposta satirica alla tua domanda: steve-yegge.blogspot.com/2006/03/…
Brandon

Risposte:


33

La differenza tra C ++ e Java sta in ciò che le lingue considerano la loro più piccola unità di collegamento.

Poiché C è stato progettato per coesistere con assembly, quell'unità è la subroutine chiamata da un indirizzo. (Questo vale per altri linguaggi che vengono compilati in file di oggetti nativi, come FORTRAN.) In altre parole, un file di oggetto contenente una funzione foo()avrà un simbolo chiamato _fooche verrà risolto in nient'altro che un indirizzo come0xdeadbeefdurante il collegamento. Questo è tutto quello che c'è. Se la funzione deve accettare argomenti, spetta al chiamante assicurarsi che tutto ciò che la funzione si aspetta sia in ordine prima di chiamare il suo indirizzo. Normalmente, questo viene fatto accatastando le cose sullo stack e il compilatore si occupa del lavoro grugnito e assicurandosi che i prototipi corrispondano. Non è possibile verificarlo tra i file oggetto; se si attiva il collegamento di chiamata, la chiamata non si interromperà come previsto e non si riceverà un avviso a riguardo. Nonostante il pericolo, ciò rende possibile che i file oggetto compilati da più lingue (incluso assembly) siano collegati insieme in un programma funzionante senza troppi problemi.

C ++, nonostante tutta la sua ulteriore fantasia, funziona allo stesso modo. Il compilatore calza gli spazi dei nomi, le classi e i metodi / membri / ecc. in questa convenzione, appiattendo il contenuto delle classi in nomi singoli che sono alterati in un modo che li rende unici. Ad esempio, un metodo come Foo::bar(int baz)potrebbe essere alterato _ZN4Foo4barEiquando messo in un file oggetto e un indirizzo come 0xBADCAFEin fase di esecuzione. Questo dipende interamente dal compilatore, quindi se provi a collegare due oggetti che hanno schemi di manipolazione diversi, sarai sfortunato. Per quanto sia brutto, significa che è possibile utilizzare un extern "C"blocco per disabilitare la manipolazione, rendendo possibile rendere il codice C ++ facilmente accessibile ad altre lingue. C ++ ha ereditato la nozione di funzioni fluttuanti da C, in gran parte perché il formato dell'oggetto nativo lo consente.

Java è una bestia diversa che vive in un mondo isolato con il suo formato di file oggetto, il .classfile. I file di classe contengono una grande quantità di informazioni sul loro contenuto che consente all'ambiente di fare cose con le classi in fase di esecuzione che il meccanismo di collegamento nativo non potrebbe nemmeno sognare. Tali informazioni devono iniziare da qualche parte e quel punto di partenza è ilclass. Le informazioni disponibili consentono al codice compilato di descriversi senza la necessità di file separati contenenti una descrizione nel codice sorgente come in C, C ++ o altre lingue. Ciò offre tutti i tipi di linguaggi dei benefici per la sicurezza che utilizzano la mancanza di collegamenti nativi, anche in fase di esecuzione, ed è ciò che consente di estrarre una classe arbitraria da un file utilizzando la riflessione e di utilizzarlo con un errore garantito se qualcosa non corrisponde .

Se non l'hai già capito, tutta questa sicurezza ha un compromesso: tutto ciò che colleghi a un programma Java deve essere Java. (Per "collegamento" intendo ogni volta che qualcosa in un file di classe fa riferimento a qualcosa in un altro.) Puoi collegare (nel senso nativo) al codice nativo usando JNI , ma c'è un contratto implicito che dice che se rompi il lato nativo , possiedi entrambi i pezzi.

Java era grande e non particolarmente veloce sull'hardware disponibile quando fu introdotto per la prima volta, proprio come Ada era stato nel decennio precedente. Solo Jim Gosling può dire con certezza quali fossero le sue motivazioni nel creare la più piccola unità di collegamento della classe Java, ma dovrei indovinare che l'ulteriore complessità che l'aggiunta di floater liberi avrebbe aggiunto al runtime avrebbe potuto essere un affare-killer.


collegamento interrotto, collegamento all'archivio web
noɥʇʎԀʎzɐɹƆ

14

Credo che la risposta sia, per Wikipedia, che Java è stato progettato per essere semplice e orientato agli oggetti. Le funzioni sono pensate per operare sulle classi in cui sono definite. Con quella linea di pensiero, avere funzioni al di fuori di una classe non ha senso. Salto alla conclusione che Java non lo consente perché non si adattava alla pura OOP.

Una rapida ricerca su Google per me non ha prodotto molto sulle motivazioni del design del linguaggio Java.


6
L'esistenza di tipi primitivi non esclude Java dall'essere "OOP puro"?
Radu Murzea,

5
@SoboLAN: Sì, e l'aggiunta di funzioni renderebbe ancora meno "OOP puro".
Giorgio,

8
@SoboLAN che dipende dalla tua definizione di "OOP puro". Ad un certo punto tutto è una combinazione di bit nella memoria del computer dopo tutto ...
jwenting

3
@jwenting: lo scopo dell'astrazione nei linguaggi di programmazione è nascondere il più possibile i bit sottostanti. Più puoi vedere quei bit nel tuo programma, più dovresti iniziare a pensare che il tuo linguaggio di programmazione offra astrazioni che perdono (a meno che non sia costruito apposta vicino al metallo di proposito). Quindi, sì, tutto si riduce a manipolare i bit, ma lingue diverse offrono diversi livelli di astrazione, altrimenti non si sarebbe in grado di distinguere assembly da un linguaggio di livello superiore.
Giorgio,

2
Dipende dalla definizione di "OOP puro": ogni valore di dati è un oggetto e ogni operazione di dati è l'invocazione del metodo risultato ottenuta attraverso il passaggio di messaggi. Per quanto ne so non c'è nient'altro.
Giorgio,

11

La vera domanda è quale sarebbe il merito di continuare a fare le cose nel modo C ++ e qual era lo scopo originale del file header? La risposta breve è che lo stile del file di intestazione ha permesso tempi di compilazione più rapidi su grandi progetti in cui molte classi potrebbero potenzialmente fare riferimento allo stesso tipo. Ciò non è necessario in JAVA e .NET a causa della natura dei compilatori.

Vedi questa risposta qui: i file di intestazione sono effettivamente buoni?


1
+1, anche se in realtà ho sentito le persone dire che a loro piace avere la separazione tra interfaccia pubblica e implementazione privata fornita dai file di intestazione. Quelle persone hanno torto, ovviamente. ;)
vaughandroid

6
@Baqueta In Java puoi farlo con interfacee class:) Non c'è bisogno di intestazioni!
Andres F.

1
Non capirò mai l'opportunità di dover guardare 3+ file per capire come funziona una semplice istanza di classe.
Erik Reppen, l'

@ErikReppen in genere, l'interfaccia (o il file di intestazione in C) è la cosa a cui i clienti ottengono in forma leggibile dall'utente per scrivere le proprie soluzioni, il resto viene fornito solo in forma binaria (ovviamente in Java, non è necessario fornire i sorgenti delle interfacce, i file di classe e javadoc lo faranno).
jwenting

@jwenting Non avevo pensato a quel caso. Sono più abituato a pensare al prossimo povero bastardo che deve mantenere questa stupida base di codice, perché troppo spesso sono io al lavoro successivo che mi spalanca gli occhi dopo aver trascorso ore a guardare una sorta di interfaccia bizzarra e una sotto / superclasse allegra architettura circolare.
Erik Reppen,

3

Un file Java rappresenta una classe. Se avessi una procedura fuori dalla classe, quale sarebbe l'ambito? Sarebbe globale? O appartiene alla classe rappresentata dal file Java?

Presumibilmente, lo metti in quel file Java invece di un altro file per un motivo - perché si abbina a quella classe più di qualsiasi altra classe. Se una procedura esterna a una classe è stata effettivamente associata a quella classe, allora perché non forzarla a entrare in quella classe a cui appartiene? Java gestisce questo come metodo statico all'interno della classe.

Se fosse consentita una procedura di classe esterna, presumibilmente non avrebbe alcun accesso speciale alla classe di cui è stato dichiarato il file, limitandolo a una funzione di utilità che non modifica alcun dato.

L'unico lato negativo possibile di questa limitazione Java è che se si hanno veramente procedure globali che non sono associate a nessuna classe, si finisce per creare una classe MyGlobals per conservarle e importare quella classe in tutti gli altri file che utilizzano tali procedure .

In effetti, il meccanismo di importazione Java ha bisogno di questa restrizione per funzionare. Con tutte le API disponibili, il compilatore java deve sapere esattamente cosa compilare e cosa compilare, quindi le dichiarazioni esplicite di importazione nella parte superiore del file. Senza dover raggruppare i tuoi globali in una classe artificiale, come diresti al compilatore Java di compilare i tuoi globali e non tutti i globali sul tuo percorso di classe? Che dire della collisione dello spazio dei nomi in cui hai un doStuff () e qualcun altro ha un doStuff ()? Non funzionerebbe. Costringendoti a specificare MyClass.doStuff () e YourClass.doStuff () risolve questi problemi. Obbligare le tue procedure a entrare in MyClass anziché all'esterno chiarisce solo questa restrizione e non impone ulteriori restrizioni al tuo codice.

Java ha sbagliato diverse cose: la serializzazione ha così tante piccole verruche che è quasi troppo difficile essere utile (pensa a SerialVersionUID). Può anche essere usato per rompere i singoli e altri modelli di design comuni. Il metodo clone () su Object dovrebbe essere diviso in deepClone () e shallowClone () ed essere sicuro per i tipi. Tutte le classi API potrebbero essere state rese immutabili per impostazione predefinita (come sono in Scala). Ma la restrizione che tutte le procedure devono appartenere a una classe è buona. Serve principalmente a semplificare e chiarire la lingua e il codice senza imporre restrizioni onerose.


3

Penso che la maggior parte delle persone che hanno risposto e i loro elettori abbiano frainteso la domanda. Riflette che non conoscono il C ++.

"Definizione" e "Dichiarazione" sono parole con un significato molto specifico in C ++.

L'OP non significa cambiare il modo in cui funziona Java. Questa è una domanda puramente sulla sintassi. Penso che sia una domanda valida.

In C ++ ci sono due modi per definire una funzione membro.

Il primo modo è il modo Java. Basta inserire tutto il codice tra parentesi graffe:

class Box {
public:
    // definition of member function
    void change(int newInt) { 
        this._m = newInt;
    }
private:
    int _m
}

Secondo modo:

class Box {
public:  
    // declaration of member function
    void change(int newInt); 
private:
    int _m
}

// definition of member function
// this can be in the same file as the declaration
void Box::change(int newInt) {
    this._m = newInt;
}

Entrambi i programmi sono uguali. La funzione changeè ancora una funzione membro: non esiste al di fuori della classe. Inoltre, la definizione della classe DEVE includere i nomi e i tipi di TUTTE le funzioni e variabili dei membri, proprio come in Java.

Jonathan Henson ha ragione sul fatto che questo è un artefatto del modo in cui funzionano le intestazioni in C ++: ti permette di mettere le dichiarazioni nel file di intestazione e le implementazioni in un file .cpp separato in modo che il tuo programma non violi l'ODR (One Definition Rule). Ma ha dei meriti al di fuori di quello: ti permette di vedere l'interfaccia di una grande classe a colpo d'occhio.

In Java puoi approssimare questo effetto con classi o interfacce astratte, ma quelle non possono avere lo stesso nome della classe di implementazione, il che lo rende piuttosto goffo.


e questo dimostra che entrambi non capisci le persone né Java. È possibile utilizzare lo stesso nome per l'interfaccia e l'implementazione, purché non si trovino nello stesso pacchetto.
jwenting

Considero il pacchetto (o spazio dei nomi o classe esterna) come parte del nome.
Erik van Velzen,

2

Penso che sia un artefatto del meccanismo di caricamento della classe. Ogni file di classe è un contenitore per un oggetto caricabile. Non esiste spazio "esterno" ai file di classe.


1
Non vedo perché organizzare il codice sorgente in unità separate impedirebbe di mantenere il formato del file di classe così com'è.
Mat

esiste una corrispondenza 1: 1 tra i file di classe e i file di origine, che è una delle migliori decisioni di progettazione nell'intero sistema.
tinto il

@ddyer Non hai mai visto Foo $ 2 $ 1.class allora? (vedi i nomi dei file di classe della classe interna Java )

Ciò renderebbe una mappatura "su"? in ogni caso, ogni classe viene generata dalla compilazione esattamente di un file sorgente, che è una buona decisione di progettazione.
tenero

0

C #, che è molto simile a Java, ha questo tipo di funzionalità attraverso l'uso di metodi parziali, tranne per il fatto che i metodi parziali sono esclusivamente privati.

Metodi parziali: http://msdn.microsoft.com/en-us/library/6b0scde8.aspx

Classi e metodi parziali: http://msdn.microsoft.com/en-us/library/wa80x488.aspx

Non vedo alcun motivo per cui Java non possa fare lo stesso, ma probabilmente dipende solo dalla necessità dell'utente di aggiungere questa funzionalità al linguaggio.

La maggior parte degli strumenti di generazione del codice per C # genera classi parziali in modo che lo sviluppatore possa aggiungere facilmente codice scritto manualmente alla classe in un file separato, se lo si desidera.


0

Il C ++ richiede che l'intero testo della classe sia compilato come parte di ogni unità di compilazione che ne utilizza i membri o produce istanze. L'unico modo per mantenere sani i tempi di compilazione è che il testo della classe stessa contenga - per quanto possibile - solo quelle cose che sono effettivamente necessarie ai suoi consumatori. Il fatto che i metodi C ++ siano spesso scritti al di fuori delle classi che li contengono è un vile hack davvero motivato dal fatto che richiedere al compilatore di elaborare il testo di ogni metodo di classe una volta per ogni unità di compilazione in cui viene utilizzata la classe porterebbe a tempi di costruzione folli.

In Java, i file di classe compilati contengono, tra le altre cose, informazioni che sono in gran parte equivalenti a un file .h C ++. I consumatori della classe possono estrarre tutte le informazioni di cui hanno bisogno da quel file, senza che il compilatore elabori il file .java. A differenza del C ++, in cui il file .h contiene informazioni rese disponibili sia alle implementazioni sia ai client delle classi in esso contenute, il flusso in Java è invertito: il file utilizzato dai client non è un file di origine utilizzato nella compilazione del codice di classe, ma viene invece prodotto dal compilatore utilizzando le informazioni del file di codice di classe. Poiché non è necessario dividere il codice di classe tra un file contenente le informazioni richieste dai client e un file contenente l'implementazione, Java non consente tale suddivisione.


-1

Penso che parte di esso sia che Java è molto un linguaggio protezionistico che si occupa dell'utilizzo in grandi team. Le lezioni non possono essere sovrascritte o ridefinite. Hai 4 livelli di modificatori di accesso che definiscono in modo molto specifico come i metodi possono e non possono essere utilizzati. Tutto è forte / tipizzato staticamente per proteggere gli sviluppatori dal tipo hijinx non corrispondente causato da altri o loro stessi. Avere classi e funzioni come unità più piccole rende molto più semplice reinventare il paradigma di come progettare un'app.

Confronta con JavaScript dove funzioni di prima classe nome / verbo a doppia proposizione piovono e passano in giro come caramelle da una pinata aperta, il costruttore di funzioni equivalenti alla classe può avere il suo prototipo alterato aggiungendo nuove proprietà o cambiando quelle vecchie per istanze che sono già stati creati in qualsiasi momento, assolutamente nulla ti impedisce di sostituire qualsiasi costrutto con la tua versione di esso, ci sono 5 tipi e si convertono automaticamente e si valutano automaticamente in diverse circostanze e puoi associare nuove proprietà a dannatamente vicino a qualsiasi cosa:

function nounAndVerb(){
}
nounAndVerb.newProperty = 'egads!';

In definitiva si tratta di nicchie e mercati. JavaScript è quasi appropriato e disastroso nelle mani di 100 sviluppatori (molti dei quali saranno probabilmente mediocri) come una volta Java era nelle mani di piccoli gruppi di sviluppatori che cercavano di scrivere l'interfaccia utente Web con esso. Quando lavori con 100 sviluppatori l'ultima cosa che vuoi è un ragazzo che reinventa il dannato paradigma. Quando si lavora con l'interfaccia utente o lo sviluppo rapido è una preoccupazione più critica, l'ultima cosa che si desidera è essere bloccati nel fare rapidamente cose relativamente semplici semplicemente perché sarebbe facile farle molto stupidamente se non si sta attenti.

Alla fine della giornata, però, sono entrambe le lingue di uso generale più diffuse, quindi c'è un po 'di argomento filosofico lì. Il mio più grande manzo personale con Java e C # è che non ho mai visto o lavorato con una base di codice legacy in cui la maggior parte degli sviluppatori sembrava aver compreso il valore di OOP di base. Quando entri in gioco dovendo avvolgere tutto in una classe, forse è più semplice supporre che stai facendo OOP piuttosto che far funzionare gli spaghetti travestiti da enormi catene di classi di 3-5 righe.

Detto questo, non c'è niente di più terribile di JavaScript scritto da qualcuno che sa solo abbastanza per essere pericoloso e non ha paura di mostrarlo. E penso che sia l'idea generale. Quel ragazzo dovrebbe presumibilmente seguire le stesse regole di Java. Direi che il bastardo troverà comunque un modo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.