04 - Programmazione a oggetti

Corso di Base di Java

Benvenuti al corso di base di Java! Questo corso è rivolto ai principianti assoluti e vi guiderà attraverso i concetti fondamentali della programmazione orientata agli oggetti (OOP) utilizzando Java.


I meccanismi della programmazione ad oggetti

Concetti filosofici ed operativi della OOP

La programmazione orientata agli oggetti (OOP) è un paradigma di programmazione che utilizza "oggetti" per rappresentare dati e metodi. Gli oggetti sono istanze di classi, che possono essere considerate come un modello o una descrizione di un tipo di oggetto.

Esempio di oggetto nel mondo reale: Un "Automobile" può essere un oggetto con proprietà come "colore", "modello" e "velocità", e comportamenti come "accelerare" e "frenare".


Le Classi

Le classi sono il fondamento dell'OOP. Una classe è una struttura che definisce lo stato e il comportamento degli oggetti.

// Definizione di una classe Automobile
public class Automobile {
    // Proprietà (variabili di istanza)
    String colore;
    String modello;
    int velocità;

    // Metodo (comportamento)
    void accelera() {
        velocità += 10;
    }
    
    void frena() {
        velocità -= 10;
    }
}

I Metodi

I metodi sono funzioni definite all'interno di una classe che descrivono i comportamenti degli oggetti. Possono manipolare le proprietà della classe e/o eseguire azioni.

// Esempio di metodi in una classe Automobile
public class Automobile {
    // Proprietà
    String colore;
    String modello;
    int velocità;

    // Metodi
    void accelera() {
        velocità += 10;
    }
    
    void frena() {
        velocità -= 10;
    }

    // Metodo principale per eseguire il programma
    public static void main(String[] args) {
        // Creazione di un'istanza di Automobile
        Automobile miaAuto = new Automobile();
        miaAuto.colore = "Rosso";
        miaAuto.modello = "Ferrari";
        miaAuto.velocità = 0;

        // Utilizzo dei metodi
        miaAuto.accelera();
        System.out.println("Velocità: " + miaAuto.velocità); // Output: Velocità: 10
    }
}

Le proprietà

Le proprietà, o variabili di istanza, rappresentano lo stato di un oggetto. Ogni oggetto avrà valori propri per queste variabili.

public class Automobile {
    // Proprietà
    String colore;
    String modello;
    int velocità;
}

Metodi Getter e Setter

I metodi getter e setter sono utilizzati per accedere e modificare le proprietà private di una classe. Questo approccio segue il principio dell'incapsulamento.

public class Automobile {
    // Proprietà private
    private String colore;
    private String modello;
    private int velocità;

    // Getter per la proprietà 'colore'
    public String getColore() {
        return colore;
    }

    // Setter per la proprietà 'colore'
    public void setColore(String colore) {
        this.colore = colore;
    }
}

Ereditarietà

L'ereditarietà è un meccanismo che permette a una classe di ereditare proprietà e metodi da un'altra classe. La classe che eredita è chiamata sottoclasse, mentre la classe da cui eredita è chiamata superclasse.

// Classe Superclasse
public class Veicolo {
    String tipo;
    int velocità;

    void accelera() {
        velocità += 10;
    }
}

// Classe Sottoclasse
public class Automobile extends Veicolo {
    String modello;

    public static void main(String[] args) {
        Automobile miaAuto = new Automobile();
        miaAuto.tipo = "Auto";
        miaAuto.modello = "Ferrari";
        miaAuto.velocità = 0;
        miaAuto.accelera();
        System.out.println("Velocità: " + miaAuto.velocità); // Output: Velocità: 10
    }
}

Incapsulamento

L'incapsulamento è il principio di nascondere i dettagli interni di un oggetto e fornire un'interfaccia pubblica per l'interazione. Questo si ottiene dichiarando le variabili di istanza come private e fornendo metodi pubblici (getter e setter) per accedervi.

public class Automobile {
    // Proprietà private
    private String colore;
    private String modello;
    private int velocità;

    // Getter e Setter
    public String getColore() {
        return colore;
    }

    public void setColore(String colore) {
        this.colore = colore;
    }

    public int getVelocità() {
        return velocità;
    }

    public void setVelocità(int velocità) {
        this.velocità = velocità;
    }
}

Modificatori di visibilità delle classi

I modificatori di visibilità controllano l'accesso alle classi, ai metodi e alle variabili in Java. I principali modificatori di visibilità sono:

Esempio:

La classe EsempioVisibilita viene definita nel package org.example

package org.example;  
public class EsempioVisibilita {  
    public int pubblico;  
    private int privato;  
    protected int protetto;  
    int predefinito; // default  
  
    public static void main(String[] args) {  
        EsempioVisibilita esempio = new EsempioVisibilita();  
        esempio.pubblico = 1;    // Accessibile  
        esempio.privato = 2;     // Accessibile  
        esempio.protetto = 3;    // Accessibile  
        esempio.predefinito = 4; // Accessibile  
    }  
}

All'interno della stessa classe, tutte le proprietà, anche quelle private, sono sempre accessibili

La classe StessoPackage viene definita anch'essa nel package org.example

package org.example
class StessoPackage{  
  
    public static void main(String[] args) {  
        EsempioVisibilita esempioEsterno = new EsempioVisibilita();  
        esempioEsterno.pubblico = 1; // Accessibile  
        esempioEsterno.privato = 2; // Non Accessibile  
        esempioEsterno.protetto = 3; // Accessibile  
        esempioEsterno.predefinito = 4; // Accessibile  
  
    }  
}

In questo caso le variabili private diventano inaccessibili, tutte le altre sono accessibili

La classe AltroPackage viene definita in un package differente

package org.example.sottopacchetto;  
import org.example.EsempioVisibilita;  
  
public class AltroPackage {  
    public static void main(String[] args) {  
        System.out.println("Hello world!");  
        EsempioVisibilita esempioFuoriDalPacchetto = new EsempioVisibilita();  
        esempioFuoriDalPacchetto.pubblico = 1; // Accessibile  
        esempioFuoriDalPacchetto.privato = 2; // Non Accessibile  
        esempioFuoriDalPacchetto.protetto = 3; // Non Accessibile  
        esempioFuoriDalPacchetto.predefinito = 4; // Non Accessibile  
    }  
}

In questa situazione bisogna innanzitutto importare la classe EsempioVisibilita per poterla usare, e inoltre tutte le variabili non pubbliche vengono rese inaccessibili


Polimorfismo

Il polimorfismo è la capacità di un oggetto di prendere molte forme. In Java, il polimorfismo si ottiene tramite l'overloading (sovraccarico) dei metodi e l'override (sovrascrittura) dei metodi.

// Overloading dei metodi
public class Formattatore {

    private String formattaNumero(int value) {
        return String.format("%d", value);
    }

    private String formattaNumero(double value) {
        return String.format("%.3f", value);
    }

    private String formattaNumero(String value) {
        return String.format("%.2f", Double.parseDouble(value));
    }

    public static void main(String[] args) {
        Formattatore f = new Formattatore();
        System.out.println(f.formattaNumero(500)); //output: 500
        System.out.println(f.formattaNumero(89.9934)); //output: 89.993
        System.out.println(f.formattaNumero("550")); //output: 550.00
    }
}

// Override dei metodi
public class Veicolo {
    void avvia() {
        System.out.println("Il veicolo si avvia");
    }
}

public class Automobile extends Veicolo {
    @Override
    void avvia() {
        System.out.println("L'automobile si avvia");
    }

    public static void main(String[] args) {
        Veicolo mioVeicolo = new Automobile();
        mioVeicolo.avvia(); // Output: L'automobile si avvia
    }
}

Costruttori

I costruttori sono metodi speciali utilizzati per inizializzare gli oggetti. Un costruttore ha lo stesso nome della classe e non ha un tipo di ritorno. I costruttori possono essere sovraccaricati.

public class Automobile {
    // Proprietà
    String colore;
    String modello;
    int velocità;

    // Costruttore senza parametri
    public Automobile() {
        this.colore = "Bianco";
        this.modello = "Standard";
        this.velocità = 0;
    }

    // Costruttore con parametri
    public Automobile(String colore, String modello, int velocità) {
        this.colore = colore;
        this.modello = modello;
        this.velocità = velocità;
    }

    public static void main(String[] args) {
        // Creazione di un oggetto utilizzando il costruttore senza parametri
        Automobile auto1 = new Automobile();
        System.out.println("Auto1 Modello: " + auto1.modello); // Output: Auto1 Modello: Standard

        // Creazione di un oggetto utilizzando il costruttore con parametri
        Automobile auto2 = new Automobile("Rosso", "Ferrari", 0);


        System.out.println("Auto2 Modello: " + auto2.modello); // Output: Auto2 Modello: Ferrari
    }
}

Classi Astratte e Interfacce

Classi Astratte

Cos'è una Classe Astratta?

Una classe astratta è una classe che non può essere istanziata direttamente. Può contenere sia metodi astratti (metodi dichiarati ma non implementati) sia metodi concreti (metodi con implementazione). Serve come base per altre classi che estenderanno questa classe astratta e forniranno implementazioni concrete per i metodi astratti.

Come si Usa?

Una classe astratta viene dichiarata usando la parola chiave abstract. Le classi che ereditano da una classe astratta devono implementare tutti i suoi metodi astratti. Tuttavia, le classi astratte possono avere campi e metodi concreti che possono essere utilizzati dalle sottoclassi.

// Dichiarazione di una classe astratta
public abstract class Animale {
    private String nome;

    public Animale(String nome) {
        this.nome = nome;
    }

    // Metodo astratto
    public abstract void verso();

    // Metodo concreto
    public void dormi() {
        System.out.println(nome + " sta dormendo.");
    }

    // Getter e Setter
    public String getNome() {
        return nome;
    }

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

Quando Vanno Usate?

Le classi astratte vanno usate quando si vuole creare una classe base che non dovrebbe essere istanziata direttamente, ma che condivide un comportamento comune tra le sottoclassi. Sono utili quando si vuole definire un comportamento comune e obbligare le sottoclassi a fornire una specifica implementazione di alcuni metodi.

Esempio di utilizzo della Classe Astratta

// Classe Cane che estende la classe astratta Animale
public class Cane extends Animale {
    public Cane(String nome) {
        super(nome);
    }

    @Override
    public void verso() {
        System.out.println(getNome() + " abbaia.");
    }
}

// Classe Gatto che estende la classe astratta Animale
public class Gatto extends Animale {
    public Gatto(String nome) {
        super(nome);
    }

    @Override
    public void verso() {
        System.out.println(getNome() + " miagola.");
    }
}

// Classe di Test
public class TestAnimale {
    public static void main(String[] args) {
        Animale cane = new Cane("Fido");
        Animale gatto = new Gatto("Whiskers");

        cane.verso();  // Output: Fido abbaia.
        gatto.verso(); // Output: Whiskers miagola.

        cane.dormi();  // Output: Fido sta dormendo.
        gatto.dormi(); // Output: Whiskers sta dormendo.
    }
}

Interfacce

Cos'è un'Interfaccia?

Un'interfaccia è un contratto che una classe può implementare. Contiene dichiarazioni di metodi ma non fornisce alcuna implementazione. Le interfacce possono essere utilizzate per definire un comportamento che può essere implementato da diverse classi non correlate. A partire da Java 8, le interfacce possono includere metodi predefiniti e metodi statici con implementazione.

Come si Usa?

Un'interfaccia viene dichiarata usando la parola chiave interface. Le classi implementano un'interfaccia utilizzando la parola chiave implements. Una classe può implementare più interfacce.

// Dichiarazione di un'interfaccia
public interface Volante {
    void vola();
}

// Implementazione di un'interfaccia
public class Uccello implements Volante {
    @Override
    public void vola() {
        System.out.println("L'uccello vola.");
    }
}

// Classe di Test
public class TestVolante {
    public static void main(String[] args) {
        Volante uccello = new Uccello();
        uccello.vola();  // Output: L'uccello vola.
    }
}

Quando Vanno Usate?

Le interfacce vanno usate quando si vuole definire un set di metodi che possono essere implementati da diverse classi, permettendo la flessibilità di diverse implementazioni. Sono utili per definire comportamenti comuni tra classi non correlate.

Esempio di Interfaccia

// Interfaccia Volante
public interface Volante {
    void vola();
}

// Interfaccia Nuotante
public interface Nuotante {
    void nuota();
}

// Classe Anatra che implementa Volante e Nuotante
public class Anatra implements Volante, Nuotante {
    @Override
    public void vola() {
        System.out.println("L'anatra vola.");
    }

    @Override
    public void nuota() {
        System.out.println("L'anatra nuota.");
    }
}

// Classe di Test
public class TestAnatra {
    public static void main(String[] args) {
        Anatra anatra = new Anatra();
        anatra.vola();  // Output: L'anatra vola.
        anatra.nuota(); // Output: L'anatra nuota.
    }
}

Differenze tra Classi Astratte e Interfacce

Caratteristica Classe Astratta Interfaccia
Istanziabile No No
Metodi Può avere sia metodi astratti che concreti Solo metodi astratti (fino a Java 7), può avere metodi di default e statici (da Java 8)
Ereditarietà Può estendere una sola classe Può estendere multiple interfacce
Parola Chiave abstract interface
Quando Usarla Quando si vuole una classe base con metodi comuni e alcune parti lasciate per l'implementazione Quando si vuole definire un contratto che varie classi possono implementare

Quando Usare Classi Astratte o Interfacce


Esempio Combinato: Classe Astratta e Interfacce

// Interfaccia Volante
public interface Volante {
    void vola();
}

// Classe Astratta Animale
public abstract class Animale {
    private String nome;

    public Animale(String nome) {
        this.nome = nome;
    }

    public abstract void verso();

    public void dormi() {
        System.out.println(nome + " sta dormendo.");
    }

    public String getNome() {
        return nome;
    }

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

// Classe Anatra che estende Animale e implementa Volante
public class Anatra extends Animale implements Volante {
    public Anatra(String nome) {
        super(nome);
    }

    @Override
    public void verso() {
        System.out.println(getNome() + " fa qua qua.");
    }

    @Override
    public void vola() {
        System.out.println(getNome() + " vola.");
    }
}

// Classe di Test
public class TestAnatra {
    public static void main(String[] args) {
        Anatra anatra = new Anatra("Daisy");

        anatra.verso();  // Output: Daisy fa qua qua.
        anatra.vola();   // Output: Daisy vola.
        anatra.dormi();  // Output: Daisy sta dormendo.
    }
}

Questi capitoli completano la presentazione sui concetti avanzati di classi astratte e interfacce, spiegandone l'uso e fornendo esempi pratici per una comprensione approfondita.