Java Language
Metodi di classe oggetto e costruttore
Ricerca…
introduzione
Questa pagina di documentazione è per mostrare dettagli con esempio su classi Java costruttori e circa i metodi della classe Object che vengono automaticamente ereditate dalla superclasse Object
di qualsiasi classe appena creata.
Sintassi
- classe nativa finale pubblica <?> getClass ()
- public final native void notify ()
- public final nativo vuoto notifyAll ()
- attesa finale nativa finale del pubblico (timeout lungo) genera InterruptedException
- public final void wait () genera InterruptedException
- attesa finale vuota pubblica (long timeout, int nanos) genera InterruptedException
- public native int hashCode ()
- public boolean equals (Object obj)
- public String toString ()
- protetto oggetto nativo clone () genera CloneNotSupportedException
- protected void finalize () lancia Throwable
metodo toString ()
Il metodo toString()
viene utilizzato per creare una rappresentazione String
di un oggetto utilizzando il contenuto dell'oggetto. Questo metodo dovrebbe essere sovrascritto durante la scrittura della classe. toString()
viene chiamato implicitamente quando un oggetto è concatenato a una stringa come in "hello " + anObject
.
Considera quanto segue:
public class User {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return firstName + " " + lastName;
}
public static void main(String[] args) {
User user = new User("John", "Doe");
System.out.println(user.toString()); // Prints "John Doe"
}
}
Qui toString()
dalla classe Object
viene sovrascritto nella classe User
per fornire dati significativi sull'oggetto durante la stampa.
Quando si utilizza println()
, viene chiamato implicitamente il metodo toString()
dell'oggetto. Pertanto, queste dichiarazioni fanno la stessa cosa:
System.out.println(user); // toString() is implicitly called on `user`
System.out.println(user.toString());
Se toString()
non è sovrascritto nella suddetta classe User
, System.out.println(user)
può restituire User@659e0bfd
o una stringa simile con quasi nessuna informazione utile eccetto il nome della classe. Questo perché la chiamata utilizzerà l'implementazione toString()
della classe Java Object
base che non conosce nulla sulla struttura della classe User
o sulle regole aziendali. Se si desidera modificare questa funzionalità nella classe, è sufficiente sovrascrivere il metodo.
equals () metodo
TL; DR
==
verifica l'uguaglianza di riferimento (indipendentemente dal fatto che siano lo stesso oggetto )
.equals()
verifica l'uguaglianza dei valori (indipendentemente dal fatto che siano logicamente "uguali" )
equals()
è un metodo utilizzato per confrontare due oggetti per l'uguaglianza. L'implementazione predefinita del metodo equals()
nella classe Object
restituisce true
se e solo se entrambi i riferimenti puntano alla stessa istanza. Si comporta quindi come il confronto di ==
.
public class Foo {
int field1, field2;
String field3;
public Foo(int i, int j, String k) {
field1 = i;
field2 = j;
field3 = k;
}
public static void main(String[] args) {
Foo foo1 = new Foo(0, 0, "bar");
Foo foo2 = new Foo(0, 0, "bar");
System.out.println(foo1.equals(foo2)); // prints false
}
}
Anche se foo1
e foo2
sono creati con gli stessi campi, stanno puntando a due oggetti diversi in memoria. L'implementazione di default equals()
quindi è considerata false
.
Per confrontare il contenuto di un oggetto per l'uguaglianza, equals()
deve essere sovrascritto.
public class Foo {
int field1, field2;
String field3;
public Foo(int i, int j, String k) {
field1 = i;
field2 = j;
field3 = k;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Foo f = (Foo) obj;
return field1 == f.field1 &&
field2 == f.field2 &&
(field3 == null ? f.field3 == null : field3.equals(f.field3));
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + this.field1;
hash = 31 * hash + this.field2;
hash = 31 * hash + (field3 == null ? 0 : field3.hashCode());
return hash;
}
public static void main(String[] args) {
Foo foo1 = new Foo(0, 0, "bar");
Foo foo2 = new Foo(0, 0, "bar");
System.out.println(foo1.equals(foo2)); // prints true
}
}
Qui il metodo equals()
over equals()
sovrascritto decide che gli oggetti sono uguali se i loro campi sono uguali.
Si noti che anche il metodo hashCode()
è stato sovrascritto. Il contratto per tale metodo afferma che quando due oggetti sono uguali, i loro valori hash devono essere uguali. Ecco perché bisogna quasi sempre sostituire hashCode()
ed equals()
insieme.
Presta particolare attenzione al tipo di argomento del metodo equals
. È Object obj
, non Foo obj
. Se metti quest'ultimo nel tuo metodo, questo non è un override del metodo equals
.
Quando scrivi la tua classe, dovrai scrivere una logica simile quando esegui l'override di equals()
e hashCode()
. La maggior parte degli IDE può generare automaticamente questo per te.
Un esempio di un'implementazione equals()
può essere trovato nella classe String
, che fa parte dell'API Java principale. Piuttosto che confrontare i puntatori, la classe String
confronta il contenuto della String
.
Java 1.7 ha introdotto la classe java.util.Objects
che fornisce un metodo di convenienza, equals
, che confronta due riferimenti potenzialmente null
, quindi può essere utilizzato per semplificare le implementazioni del metodo equals
.
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Foo f = (Foo) obj;
return field1 == f.field1 && field2 == f.field2 && Objects.equals(field3, f.field3);
}
Confronto di classe
Poiché il metodo equals può essere eseguito su qualsiasi oggetto, una delle prime cose che il metodo spesso esegue (dopo aver verificato il null
) consiste nel verificare se la classe dell'oggetto confrontato corrisponde alla classe corrente.
@Override
public boolean equals(Object obj) {
//...check for null
if (getClass() != obj.getClass()) {
return false;
}
//...compare fields
}
Questo è tipicamente fatto come sopra confrontando gli oggetti di classe. Tuttavia, ciò può fallire in alcuni casi speciali che potrebbero non essere ovvi. Ad esempio, alcuni framework generano proxy dinamici di classi e questi proxy dinamici sono in realtà una classe diversa. Ecco un esempio usando JPA.
Foo detachedInstance = ...
Foo mergedInstance = entityManager.merge(detachedInstance);
if (mergedInstance.equals(detachedInstance)) {
//Can never get here if equality is tested with getClass()
//as mergedInstance is a proxy (subclass) of Foo
}
Un meccanismo per ovviare a questa limitazione è confrontare le classi usando instanceof
@Override
public final boolean equals(Object obj) {
if (!(obj instanceof Foo)) {
return false;
}
//...compare fields
}
Tuttavia, ci sono alcune insidie che devono essere evitate quando si usa instanceof
. Dato che Foo potrebbe potenzialmente avere altre sottoclassi e quelle sottoclassi potrebbero sovrascrivere equals()
potresti entrare in un caso in cui un Foo
è uguale a FooSubclass
ma FooSubclass
non è uguale a Foo
.
Foo foo = new Foo(7);
FooSubclass fooSubclass = new FooSubclass(7, false);
foo.equals(fooSubclass) //true
fooSubclass.equals(foo) //false
Ciò viola le proprietà di simmetria e transitività e quindi è un'implementazione non valida del metodo equals()
. Di conseguenza, quando si usa instanceof
, una buona pratica consiste nel rendere final
metodo equals()
(come nell'esempio precedente). Ciò garantirà che nessuna sottoclasse sovrascrive equals()
e viola le ipotesi chiave.
metodo hashCode ()
Quando una classe Java sovrascrive il metodo equals
, dovrebbe sovrascrivere anche il metodo hashCode
. Come definito nel contratto del metodo :
- Ogni volta che viene invocato sullo stesso oggetto più di una volta durante l'esecuzione di un'applicazione Java, il metodo
hashCode
deve restituire costantemente lo stesso numero intero, a condizione che non vengano modificate le informazioni utilizzate nei confronti degli uguali sull'oggetto. Questo numero intero non deve rimanere coerente da un'esecuzione di un'applicazione a un'altra esecuzione della stessa applicazione.- Se due oggetti sono uguali secondo il metodo
equals(Object)
, quindi chiamare il metodohashCode
su ciascuno dei due oggetti deve produrre lo stesso risultato intero.- Non è necessario che se due oggetti non sono uguali secondo il metodo
equals(Object)
, quindi chiamare il metodohashCode
su ciascuno dei due oggetti deve produrre risultati interi distinti. Tuttavia, il programmatore dovrebbe essere consapevole del fatto che la produzione di risultati interi distinti per oggetti non uguali può migliorare le prestazioni delle tabelle hash.
I codici hash vengono utilizzati nelle implementazioni hash come HashMap
, HashTable
e HashSet
. Il risultato della funzione hashCode
determina il bucket in cui verrà inserito un oggetto. Queste implementazioni di hash sono più efficienti se l'implementazione hashCode
fornita è buona. Una proprietà importante di una buona implementazione di hashCode
è che la distribuzione dei valori hashCode
è uniforme. In altre parole, esiste una piccola probabilità che numerose istanze vengano archiviate nello stesso bucket.
Un algoritmo per calcolare un valore di codice hash potrebbe essere simile al seguente:
public class Foo {
private int field1, field2;
private String field3;
public Foo(int field1, int field2, String field3) {
this.field1 = field1;
this.field2 = field2;
this.field3 = field3;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Foo f = (Foo) obj;
return field1 == f.field1 &&
field2 == f.field2 &&
(field3 == null ? f.field3 == null : field3.equals(f.field3);
}
@Override
public int hashCode() {
int hash = 1;
hash = 31 * hash + field1;
hash = 31 * hash + field2;
hash = 31 * hash + (field3 == null ? 0 : field3.hashCode());
return hash;
}
}
Utilizzo di Arrays.hashCode () come scorciatoia
In Java 1.2 e versioni successive, invece di sviluppare un algoritmo per calcolare un codice hash, è possibile generare uno utilizzando java.util.Arrays#hashCode
fornendo una matrice Object o primitive contenente i valori del campo:
@Override
public int hashCode() {
return Arrays.hashCode(new Object[] {field1, field2, field3});
}
Java 1.7 ha introdotto la classe java.util.Objects
che fornisce un metodo di convenienza, hash(Object... objects)
, che calcola un codice hash basato sui valori degli oggetti forniti. Questo metodo funziona proprio come java.util.Arrays#hashCode
.
@Override
public int hashCode() {
return Objects.hash(field1, field2, field3);
}
Nota: questo approccio è inefficiente e produce oggetti spazzatura ogni volta che viene chiamato il metodo hashCode()
personalizzato:
- Viene creato un
Object[]
temporaneoObject[]
. (Nella versioneObjects.hash()
, la matrice viene creata dal meccanismo "varargs".) - Se uno dei campi è di tipo primitivo, deve essere inserito in una scatola e ciò può creare più oggetti temporanei.
- La matrice deve essere popolata.
- L'array deve essere ripetuto dal metodo
Arrays.hashCode
oObjects.hash
. - Le chiamate a
Object.hashCode()
cheArrays.hashCode
oObjects.hash
devono rendere (probabilmente) non possono essere inline.
Memorizzazione nella cache interna dei codici hash
Poiché il calcolo del codice hash di un oggetto può essere costoso, può essere interessante memorizzare nella cache il valore del codice hash all'interno dell'oggetto la prima volta che viene calcolato. Per esempio
public final class ImmutableArray {
private int[] array;
private volatile int hash = 0;
public ImmutableArray(int[] initial) {
array = initial.clone();
}
// Other methods
@Override
public boolean equals(Object obj) {
// ...
}
@Override
public int hashCode() {
int h = hash;
if (h == 0) {
h = Arrays.hashCode(array);
hash = h;
}
return h;
}
}
Questo approccio elimina il costo di (ripetutamente) il calcolo del codice hash contro il sovraccarico di un campo aggiuntivo per memorizzare il codice hash. Se ciò si ripaga, l'ottimizzazione delle prestazioni dipenderà dalla frequenza con cui un determinato oggetto viene sottoposto a hashing (cercato) e da altri fattori.
Noterete anche che se il vero codice hash di un ImmutableArray
sembra essere pari a zero (una possibilità su 2 32), la cache è inefficace.
Infine, questo approccio è molto più difficile da implementare correttamente se l'oggetto che stiamo tritando è mutabile. Tuttavia, vi sono maggiori preoccupazioni se i codici hash cambiano; vedi il contratto di cui sopra.
metodi wait () e notify ()
wait()
e notify()
funzionano in tandem - quando un thread chiama wait()
su un oggetto, quel thread bloccherà fino a quando un altro thread chiama notify()
o notifyAll()
su quello stesso oggetto.
(Vedi anche: wait () / notify () )
package com.example.examples.object;
import java.util.concurrent.atomic.AtomicBoolean;
public class WaitAndNotify {
public static void main(String[] args) throws InterruptedException {
final Object obj = new Object();
AtomicBoolean aHasFinishedWaiting = new AtomicBoolean(false);
Thread threadA = new Thread("Thread A") {
public void run() {
System.out.println("A1: Could print before or after B1");
System.out.println("A2: Thread A is about to start waiting...");
try {
synchronized (obj) { // wait() must be in a synchronized block
// execution of thread A stops until obj.notify() is called
obj.wait();
}
System.out.println("A3: Thread A has finished waiting. "
+ "Guaranteed to happen after B3");
} catch (InterruptedException e) {
System.out.println("Thread A was interrupted while waiting");
} finally {
aHasFinishedWaiting.set(true);
}
}
};
Thread threadB = new Thread("Thread B") {
public void run() {
System.out.println("B1: Could print before or after A1");
System.out.println("B2: Thread B is about to wait for 10 seconds");
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000); // sleep for 1 second
} catch (InterruptedException e) {
System.err.println("Thread B was interrupted from waiting");
}
}
System.out.println("B3: Will ALWAYS print before A3 since "
+ "A3 can only happen after obj.notify() is called.");
while (!aHasFinishedWaiting.get()) {
synchronized (obj) {
// notify ONE thread which has called obj.wait()
obj.notify();
}
}
}
};
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("Finished!");
}
}
Alcuni esempi di output:
A1: Could print before or after B1
B1: Could print before or after A1
A2: Thread A is about to start waiting...
B2: Thread B is about to wait for 10 seconds
B3: Will ALWAYS print before A3 since A3 can only happen after obj.notify() is called.
A3: Thread A has finished waiting. Guaranteed to happen after B3
Finished!
B1: Could print before or after A1
B2: Thread B is about to wait for 10 seconds
A1: Could print before or after B1
A2: Thread A is about to start waiting...
B3: Will ALWAYS print before A3 since A3 can only happen after obj.notify() is called.
A3: Thread A has finished waiting. Guaranteed to happen after B3
Finished!
A1: Could print before or after B1
A2: Thread A is about to start waiting...
B1: Could print before or after A1
B2: Thread B is about to wait for 10 seconds
B3: Will ALWAYS print before A3 since A3 can only happen after obj.notify() is called.
A3: Thread A has finished waiting. Guaranteed to happen after B3
Finished!
metodo getClass ()
Il metodo getClass()
può essere utilizzato per trovare il tipo di classe runtime di un oggetto. Vedi l'esempio qui sotto:
public class User {
private long userID;
private String name;
public User(long userID, String name) {
this.userID = userID;
this.name = name;
}
}
public class SpecificUser extends User {
private String specificUserID;
public SpecificUser(String specificUserID, long userID, String name) {
super(userID, name);
this.specificUserID = specificUserID;
}
}
public static void main(String[] args){
User user = new User(879745, "John");
SpecificUser specificUser = new SpecificUser("1AAAA", 877777, "Jim");
User anotherSpecificUser = new SpecificUser("1BBBB", 812345, "Jenny");
System.out.println(user.getClass()); //Prints "class User"
System.out.println(specificUser.getClass()); //Prints "class SpecificUser"
System.out.println(anotherSpecificUser.getClass()); //Prints "class SpecificUser"
}
Il metodo getClass()
restituirà il tipo di classe più specifico, motivo per cui quando viene chiamato getClass()
su anotherSpecificUser
, il valore restituito è class SpecificUser
perché è inferiore nell'albero di eredità di User
.
È interessante notare che, mentre il metodo getClass
è dichiarato come:
public final native Class<?> getClass();
Il tipo statico effettivo restituito da una chiamata a getClass
è di Class<? extends T>
dove T
è il tipo statico dell'oggetto su cui viene chiamato getClass
.
cioè il seguente compila:
Class<? extends String> cls = "".getClass();
metodo clone ()
Il metodo clone()
è usato per creare e restituire una copia di un oggetto. Questo metodo dovrebbe essere evitato in quanto è problematico e un costruttore di copia o qualche altro approccio per la copia dovrebbe essere usato a favore di clone()
.
Per il metodo da utilizzare tutte le classi che chiamano il metodo devono implementare l'interfaccia Cloneable
.
L'interfaccia Cloneable
stessa è solo un'interfaccia di tag usata per cambiare il comportamento del metodo native
clone()
che controlla se la classe degli oggetti chiamanti implementa Cloneable
. Se il chiamante non implementa questa interfaccia verrà lanciata una CloneNotSupportedException
.
La classe Object
non implementa questa interfaccia in modo che venga generata una CloneNotSupportedException
se l'oggetto chiamante è di classe Object
.
Perché un clone sia corretto dovrebbe essere indipendente dall'oggetto da cui viene clonato, quindi potrebbe essere necessario modificare l'oggetto prima che venga restituito. Ciò significa essenzialmente creare una "copia profonda" copiando anche uno qualsiasi degli oggetti mutabili che costituiscono la struttura interna dell'oggetto che viene clonato. Se questo non è implementato correttamente, l'oggetto clonato non sarà indipendente e avrà gli stessi riferimenti agli oggetti mutabili come l'oggetto da cui è stato clonato. Ciò comporterebbe un comportamento inconsistente in quanto eventuali modifiche a quelle in una influenzerebbero l'altra.
class Foo implements Cloneable {
int w;
String x;
float[] y;
Date z;
public Foo clone() {
try {
Foo result = new Foo();
// copy primitives by value
result.w = this.w;
// immutable objects like String can be copied by reference
result.x = this.x;
// The fields y and z refer to a mutable objects; clone them recursively.
if (this.y != null) {
result.y = this.y.clone();
}
if (this.z != null) {
result.z = this.z.clone();
}
// Done, return the new object
return result;
} catch (CloneNotSupportedException e) {
// in case any of the cloned mutable fields do not implement Cloneable
throw new AssertionError(e);
}
}
}
finalize () metodo
Questo è un metodo protetto e non statico della classe Object
. Questo metodo viene utilizzato per eseguire alcune operazioni finali o pulire le operazioni su un oggetto prima che venga rimosso dalla memoria.
Secondo il documento, questo metodo viene chiamato dal garbage collector su un oggetto quando la garbage collection determina che non ci sono più riferimenti all'oggetto.
Ma non ci sono garanzie che il metodo finalize()
venga chiamato se l'oggetto è ancora raggiungibile o non viene eseguito nessun Garbage Collector quando l'oggetto diventa idoneo. Ecco perché è meglio non fare affidamento su questo metodo.
Nelle librerie principali Java sono stati trovati alcuni esempi di utilizzo, ad esempio in FileInputStream.java
:
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
/* if fd is shared, the references in FileDescriptor
* will ensure that finalizer is only called when
* safe to do so. All references using the fd have
* become unreachable. We can call close()
*/
close();
}
}
In questo caso è l'ultima possibilità di chiudere la risorsa se quella risorsa non è stata chiusa prima.
Generalmente è considerata una cattiva pratica usare il metodo finalize()
in applicazioni di qualsiasi tipo e dovrebbe essere evitato.
I finalizzatori non sono pensati per liberare risorse (ad es. Chiudere i file). Il garbage collector viene chiamato quando (se!) Il sistema si scarica sullo spazio heap. Non si può fare affidamento su di esso per essere chiamato quando il sistema sta per esaurirsi sugli handle di file o, per qualsiasi altro motivo.
Il caso d'uso previsto per i finalizzatori è per un oggetto che sta per essere reclamato per notificare qualche altro oggetto sul suo imminente destino. A questo scopo esiste ora un meccanismo migliore --- la classe java.lang.ref.WeakReference<T>
. Se pensi di aver bisogno di scrivere un metodo finalize()
, allora dovresti esaminare se puoi risolvere lo stesso problema usando WeakReference
. Se ciò non risolve il tuo problema, potrebbe essere necessario ripensare il tuo design a un livello più profondo.
Per ulteriori letture, ecco un articolo sul metodo finalize()
libro "Effective Java" di Joshua Bloch.
Costruttore di oggetti
Tutti i costruttori in Java devono effettuare una chiamata al costruttore Object
. Questo è fatto con la chiamata super()
. Questa deve essere la prima riga in un costruttore. La ragione di ciò è che l'oggetto può effettivamente essere creato nell'heap prima che venga eseguita qualsiasi inizializzazione aggiuntiva.
Se non si specifica la chiamata a super()
in un costruttore, il compilatore lo inserirà automaticamente.
Quindi tutti e tre questi esempi sono funzionalmente identici
con chiamata esplicita al costruttore super()
public class MyClass {
public MyClass() {
super();
}
}
con chiamata implicita al costruttore super()
public class MyClass {
public MyClass() {
// empty
}
}
con costruttore implicito
public class MyClass {
}
Che dire di Costruttore-Concatenamento?
È possibile chiamare altri costruttori come prima istruzione di un costruttore. Poiché sia la chiamata esplicita a un super costruttore che la chiamata a un altro costruttore devono essere entrambe le prime istruzioni, si escludono a vicenda.
public class MyClass {
public MyClass(int size) {
doSomethingWith(size);
}
public MyClass(Collection<?> initialValues) {
this(initialValues.size());
addInitialValues(initialValues);
}
}
Chiamando la nuova MyClass(Arrays.asList("a", "b", "c"))
chiamerà il secondo costruttore con l'argomento List, che a sua volta delegherà al primo costruttore (che delegherà implicitamente a super()
) e quindi chiamare addInitialValues(int size)
con la seconda dimensione dell'elenco. Questo viene utilizzato per ridurre la duplicazione del codice in cui più costruttori devono eseguire lo stesso lavoro.
Come posso chiamare un costruttore specifico?
Dato l'esempio sopra, si può chiamare new MyClass("argument")
o new MyClass("argument", 0)
. In altre parole, proprio come l' overloading dei metodi , basta chiamare il costruttore con i parametri necessari per il costruttore scelto.
Cosa succederà nel costruttore della classe Object?
Niente di più di quanto accadrebbe in una sottoclasse che ha un costruttore vuoto predefinito (meno la chiamata a super()
).
Il costruttore vuoto predefinito può essere definito in modo esplicito, ma in caso contrario il compilatore lo inserirà finché non saranno già definiti altri costruttori.
Come viene creato un oggetto dal costruttore in Object?
La vera creazione di oggetti è verso la JVM. Ogni costruttore in Java appare come un metodo speciale denominato <init>
che è responsabile per l'inizializzazione dell'istanza. Questo metodo <init>
viene fornito dal compilatore e poiché <init>
non è un identificatore valido in Java, non può essere utilizzato direttamente nella lingua.
In che modo JVM richiama questo metodo
<init>
?
La JVM invocherà il metodo <init>
usando l'istruzione invokespecial
e può essere invocato solo su istanze di classe non inizializzate.
Per ulteriori informazioni, consulta le specifiche JVM e le specifiche del linguaggio Java:
- Metodi speciali (JVM) - JVMS - 2.9
- Costruttori - JLS - 8.8