Metodi di Istanza in Java

Edoardo Midali
Edoardo Midali

I metodi di istanza rappresentano il cuore della programmazione orientata agli oggetti in Java, incarnando il comportamento specifico di ogni oggetto e permettendo l’interazione con lo stato interno delle istanze. A differenza dei metodi statici, i metodi di istanza operano nel contesto di oggetti specifici, accedendo e modificando le loro variabili di istanza.

Natura e Caratteristiche dei Metodi di Istanza

I metodi di istanza sono intrinsecamente legati agli oggetti della classe in cui sono definiti. Ogni invocazione di un metodo di istanza avviene nel contesto di un oggetto specifico, fornendo accesso completo allo stato interno di quell’istanza e permettendo comportamenti personalizzati basati sui dati dell’oggetto.

Accesso allo Stato dell’Oggetto

La caratteristica principale dei metodi di istanza è la loro capacità di accedere e modificare le variabili di istanza dell’oggetto su cui vengono invocati. Questo accesso diretto allo stato interno permette di implementare comportamenti complessi che dipendono dai dati specifici di ogni oggetto.

public class ContoCorrente {
    private double saldo;
    private String numeroConto;
    private String proprietario;
    private List<String> transazioni;

    public ContoCorrente(String numeroConto, String proprietario) {
        this.numeroConto = numeroConto;
        this.proprietario = proprietario;
        this.saldo = 0.0;
        this.transazioni = new ArrayList<>();
    }

    // Metodo di istanza che accede e modifica lo stato
    public boolean deposita(double importo) {
        if (importo <= 0) {
            return false;
        }

        this.saldo += importo;
        this.transazioni.add("Deposito: +" + importo + " | Saldo: " + this.saldo);
        return true;
    }

    // Metodo di istanza che utilizza stato per decisioni
    public boolean preleva(double importo) {
        if (importo <= 0 || importo > this.saldo) {
            return false;
        }

        this.saldo -= importo;
        this.transazioni.add("Prelievo: -" + importo + " | Saldo: " + this.saldo);
        return true;
    }

    // Metodo di istanza per interrogare lo stato
    public double getSaldoDisponibile() {
        return this.saldo;
    }
}

Il Riferimento ‘this’

Il riferimento this è fondamentale nei metodi di istanza, rappresentando l’oggetto corrente su cui il metodo è stato invocato. Sebbene spesso implicito, this diventa esplicito quando necessario per disambiguare tra parametri e variabili di istanza o per passare l’oggetto corrente ad altri metodi.

public class Persona {
    private String nome;
    private int eta;

    // 'this' esplicito per disambiguare
    public void setNome(String nome) {
        this.nome = nome;  // this.nome si riferisce alla variabile di istanza
                          // nome si riferisce al parametro
    }

    // 'this' per passare l'oggetto corrente
    public void confrontaCon(Persona altra) {
        int confronto = this.confrontaEta(altra);
        if (confronto > 0) {
            System.out.println(this.nome + " è più vecchio di " + altra.nome);
        }
    }

    // Method chaining utilizzando 'this'
    public Persona setEta(int eta) {
        this.eta = eta;
        return this;  // Ritorna l'oggetto corrente per concatenazione
    }

    public Persona setNome(String nome) {
        this.nome = nome;
        return this;
    }

    // Uso: persona.setNome("Mario").setEta(30);
}

Polimorfismo e Override

I metodi di istanza sono il meccanismo principale attraverso cui si realizza il polimorfismo in Java. La capacità di sovrascrivere metodi nelle sottoclassi permette di definire comportamenti specializzati mantenendo un’interfaccia comune.

Override e Specializzazione

Quando una sottoclasse sovrascrive un metodo della superclasse, può specializzare il comportamento mantenendo la stessa firma del metodo. Questo permette al codice client di utilizzare oggetti di classi diverse attraverso la stessa interfaccia.

public abstract class Veicolo {
    protected String marca;
    protected String modello;
    protected double velocitaAttuale;

    public abstract void accelera(double incremento);

    public void ferma() {
        this.velocitaAttuale = 0;
        System.out.println("Veicolo fermato");
    }

    public double getVelocita() {
        return velocitaAttuale;
    }
}

public class Automobile extends Veicolo {
    private boolean cambioAutomatico;

    @Override
    public void accelera(double incremento) {
        if (cambioAutomatico) {
            // Logica specifica per cambio automatico
            this.velocitaAttuale += incremento * 0.8; // Accelerazione graduale
        } else {
            this.velocitaAttuale += incremento;
        }
        System.out.println("Auto accelera a: " + this.velocitaAttuale + " km/h");
    }

    @Override
    public void ferma() {
        super.ferma(); // Chiama il metodo della superclasse
        System.out.println("Automobile parcheggiata con freno a mano");
    }
}

public class Motocicletta extends Veicolo {
    private boolean hasSidecar;

    @Override
    public void accelera(double incremento) {
        double fattore = hasSidecar ? 0.7 : 1.2; // Sidecar riduce accelerazione
        this.velocitaAttuale += incremento * fattore;
        System.out.println("Moto accelera a: " + this.velocitaAttuale + " km/h");
    }
}

Polimorfismo in Azione

Il polimorfismo permette di trattare oggetti di classi diverse attraverso un’interfaccia comune, con il metodo corretto che viene selezionato a runtime basandosi sul tipo effettivo dell’oggetto.

public class GestoreFlotta {
    private List<Veicolo> flotta;

    public GestoreFlotta() {
        this.flotta = new ArrayList<>();
    }

    public void aggiungiVeicolo(Veicolo veicolo) {
        this.flotta.add(veicolo);
    }

    // Polimorfismo: il metodo corretto viene chiamato per ogni tipo
    public void acceleraTuttiVeicoli(double incremento) {
        for (Veicolo veicolo : this.flotta) {
            veicolo.accelera(incremento); // Chiamata polimorfica
        }
    }

    public void fermaTuttiVeicoli() {
        for (Veicolo veicolo : this.flotta) {
            veicolo.ferma(); // Comportamento specifico per ogni tipo
        }
    }
}

Incapsulamento e Information Hiding

I metodi di istanza sono strumentali nell’implementazione dell’incapsulamento, uno dei principi fondamentali della programmazione orientata agli oggetti. Attraverso metodi getter, setter e altri metodi di accesso, le classi possono controllare come il loro stato interno viene accessibile e modificato.

Getter e Setter Intelligenti

I metodi getter e setter non sono semplici accessor, ma possono implementare logica di validazione, trasformazione e controllo dell’accesso.

public class TemperaturaController {
    private double temperaturaCelsius;
    private double temperaturaMinima = -273.15; // Zero assoluto
    private double temperaturaMassima = 1000.0;  // Limite pratico
    private List<Double> storiaTemperature;

    public TemperaturaController() {
        this.storiaTemperature = new ArrayList<>();
        this.temperaturaCelsius = 20.0; // Temperatura ambiente default
        this.storiaTemperature.add(this.temperaturaCelsius);
    }

    // Setter con validazione e side effects
    public void setTemperaturaCelsius(double temperatura) {
        if (temperatura < temperaturaMinima) {
            throw new IllegalArgumentException(
                "Temperatura non può essere inferiore a " + temperaturaMinima + "°C");
        }
        if (temperatura > temperaturaMassima) {
            throw new IllegalArgumentException(
                "Temperatura non può essere superiore a " + temperaturaMassima + "°C");
        }

        this.temperaturaCelsius = temperatura;
        this.storiaTemperature.add(temperatura);

        // Side effect: log del cambiamento
        System.out.println("Temperatura impostata a: " + temperatura + "°C");
    }

    // Getter semplice
    public double getTemperaturaCelsius() {
        return this.temperaturaCelsius;
    }

    // Getter con trasformazione
    public double getTemperaturaFahrenheit() {
        return (this.temperaturaCelsius * 9.0 / 5.0) + 32.0;
    }

    public double getTemperaturaKelvin() {
        return this.temperaturaCelsius + 273.15;
    }

    // Metodo per accesso controllato alla storia
    public List<Double> getStoriaTemperature() {
        return new ArrayList<>(this.storiaTemperature); // Copia difensiva
    }

    // Metodo calcolato basato sullo stato
    public double getTemperaturaMedia() {
        return this.storiaTemperature.stream()
               .mapToDouble(Double::doubleValue)
               .average()
               .orElse(0.0);
    }
}

Invarianti di Classe

I metodi di istanza possono implementare e mantenere invarianti di classe, assicurando che l’oggetto rimanga sempre in uno stato valido.

public class RettangoloValido {
    private double larghezza;
    private double altezza;

    public RettangoloValido(double larghezza, double altezza) {
        setDimensioni(larghezza, altezza);
    }

    // Metodo che mantiene l'invariante: dimensioni sempre positive
    public void setDimensioni(double larghezza, double altezza) {
        if (larghezza <= 0 || altezza <= 0) {
            throw new IllegalArgumentException("Le dimensioni devono essere positive");
        }
        this.larghezza = larghezza;
        this.altezza = altezza;
    }

    public void setLarghezza(double larghezza) {
        setDimensioni(larghezza, this.altezza);
    }

    public void setAltezza(double altezza) {
        setDimensioni(this.larghezza, altezza);
    }

    // Metodi che dipendono dall'invariante
    public double calcolaArea() {
        return this.larghezza * this.altezza; // Sicuro: sempre positive
    }

    public double calcolaPerimetro() {
        return 2 * (this.larghezza + this.altezza);
    }

    public boolean isQuadrato() {
        return Math.abs(this.larghezza - this.altezza) < 0.001;
    }
}

Design Patterns con Metodi di Istanza

I metodi di istanza sono centrali in molti design pattern che facilitano la creazione di codice flessibile e manutenibile.

Builder Pattern

Il Builder pattern utilizza metodi di istanza per costruire oggetti complessi in modo fluido e leggibile:

public class ConfigurazioneDatabase {
    private String host;
    private int porta;
    private String database;
    private String username;
    private String password;
    private boolean ssl;
    private int timeoutConnessione;

    private ConfigurazioneDatabase() {} // Costruttore privato

    public static class Builder {
        private ConfigurazioneDatabase config;

        public Builder() {
            this.config = new ConfigurazioneDatabase();
            // Valori di default
            this.config.porta = 5432;
            this.config.ssl = false;
            this.config.timeoutConnessione = 30000;
        }

        public Builder host(String host) {
            this.config.host = host;
            return this;
        }

        public Builder porta(int porta) {
            this.config.porta = porta;
            return this;
        }

        public Builder database(String database) {
            this.config.database = database;
            return this;
        }

        public Builder credenziali(String username, String password) {
            this.config.username = username;
            this.config.password = password;
            return this;
        }

        public Builder abilitaSSL(boolean ssl) {
            this.config.ssl = ssl;
            return this;
        }

        public ConfigurazioneDatabase build() {
            if (config.host == null || config.database == null) {
                throw new IllegalStateException("Host e database sono obbligatori");
            }
            return this.config;
        }
    }
}

Observer Pattern

I metodi di istanza facilitano l’implementazione del pattern Observer per la gestione degli eventi:

public class Termostato {
    private double temperatura;
    private List<TemperaturaObserver> observers;

    public Termostato() {
        this.observers = new ArrayList<>();
        this.temperatura = 20.0;
    }

    public void aggiungiObserver(TemperaturaObserver observer) {
        this.observers.add(observer);
    }

    public void rimuoviObserver(TemperaturaObserver observer) {
        this.observers.remove(observer);
    }

    public void setTemperatura(double nuovaTemperatura) {
        double vecchiaTemperatura = this.temperatura;
        this.temperatura = nuovaTemperatura;
        this.notificaCambioTemperatura(vecchiaTemperatura, nuovaTemperatura);
    }

    private void notificaCambioTemperatura(double vecchia, double nuova) {
        for (TemperaturaObserver observer : this.observers) {
            observer.onCambioTemperatura(vecchia, nuova);
        }
    }
}

Gestione dello Stato e Ciclo di Vita

I metodi di istanza gestiscono il ciclo di vita degli oggetti e le transizioni di stato, implementando logiche complesse che dipendono dallo stato corrente dell’oggetto.

State Pattern

public class Documento {
    private StatoDocumento stato;
    private String contenuto;
    private String autore;

    public Documento(String autore) {
        this.autore = autore;
        this.stato = new StatoBozza();
        this.contenuto = "";
    }

    public void modificaContenuto(String nuovoContenuto) {
        this.stato.modifica(this, nuovoContenuto);
    }

    public void inoltraPerRevisione() {
        this.stato.inoltraPerRevisione(this);
    }

    public void approva() {
        this.stato.approva(this);
    }

    public void rifiuta() {
        this.stato.rifiuta(this);
    }

    // Metodi per la gestione interna dello stato
    void setStato(StatoDocumento nuovoStato) {
        this.stato = nuovoStato;
        System.out.println("Documento transizionato a: " + stato.getClass().getSimpleName());
    }

    void setContenuto(String contenuto) {
        this.contenuto = contenuto;
    }

    public String getContenuto() {
        return this.contenuto;
    }

    public String getStatoCorrente() {
        return this.stato.getClass().getSimpleName();
    }
}

Performance e Ottimizzazioni

I metodi di istanza presentano caratteristiche di performance specifiche che influenzano il design delle applicazioni.

Lazy Loading

I metodi di istanza possono implementare lazy loading per ottimizzare l’uso della memoria:

public class ReportComplesso {
    private String nomeReport;
    private List<DatiReport> datiCache;
    private boolean datiCaricati = false;

    public ReportComplesso(String nomeReport) {
        this.nomeReport = nomeReport;
    }

    public List<DatiReport> getDati() {
        if (!this.datiCaricati) {
            this.caricaDati();
        }
        return new ArrayList<>(this.datiCache); // Copia difensiva
    }

    private void caricaDati() {
        System.out.println("Caricamento dati per: " + this.nomeReport);
        // Simulazione caricamento pesante
        this.datiCache = new ArrayList<>();
        // ... logica di caricamento
        this.datiCaricati = true;
    }

    public void invalidaCache() {
        this.datiCaricati = false;
        this.datiCache = null;
        System.out.println("Cache invalidata per: " + this.nomeReport);
    }
}

Pooling di Oggetti

I metodi di istanza possono gestire il pooling per riutilizzare oggetti costosi:

public class ConnessioneDatabase {
    private boolean inUso;
    private String connectionString;
    private long timestampUltimoUso;

    public ConnessioneDatabase(String connectionString) {
        this.connectionString = connectionString;
        this.inUso = false;
        this.timestampUltimoUso = System.currentTimeMillis();
    }

    public boolean prendiInPrestito() {
        if (this.inUso) {
            return false;
        }
        this.inUso = true;
        this.timestampUltimoUso = System.currentTimeMillis();
        return true;
    }

    public void rilascia() {
        this.inUso = false;
        this.timestampUltimoUso = System.currentTimeMillis();
    }

    public boolean isScaduta(long timeoutMs) {
        return System.currentTimeMillis() - this.timestampUltimoUso > timeoutMs;
    }

    public boolean isDisponibile() {
        return !this.inUso;
    }
}

Best Practices

Coesione Alta: I metodi di istanza dovrebbero operare logicamente sui dati dell’oggetto e implementare comportamenti coerenti con la responsabilità della classe.

Immutabilità Quando Possibile: Preferisci oggetti immutabili o metodi che restituiscono nuove istanze piuttosto che modificare lo stato esistente.

Validazione Consistente: Implementa validazione nei setter e mantieni invarianti di classe attraverso tutti i metodi che modificano lo stato.

Gestione delle Eccezioni: I metodi di istanza dovrebbero gestire appropriatamente situazioni eccezionali e mantenere l’oggetto in uno stato consistente.

Documentazione Comportamentale: Documenta non solo cosa fa un metodo, ma anche come modifica lo stato dell’oggetto e quali precondizioni/postcondizioni ha.

Thread Safety: Considera la thread safety quando i metodi di istanza modificano stato condiviso, implementando sincronizzazione appropriata.

Conclusione

I metodi di istanza sono il meccanismo fondamentale attraverso cui gli oggetti in Java esprimono il loro comportamento e interagiscono con il mondo esterno. Essi incarnano i principi dell’orientamento agli oggetti - incapsulamento, polimorfismo ed ereditarietà - e permettono la creazione di sistemi software flessibili e manutenibili.

La padronanza dei metodi di istanza richiede una comprensione profonda non solo della sintassi, ma anche dei principi di design che guidano la creazione di API efficaci e la gestione appropriata dello stato degli oggetti. Quando utilizzati correttamente, i metodi di istanza trasformano semplici strutture dati in entità software ricche e espressive che modellano efficacemente il dominio del problema.