12  Desenvolupament de classes

Objectius

  • Definir una classe pròpia amb els seus atributs i mètodes.
  • Aplicar l’encapsulació amb els modificadors de visibilitat (private, public).
  • Escriure constructors (amb i sense paràmetres) i entendre el constructor per defecte.
  • Fer servir la paraula clau this.
  • Escriure getters i setters amb validació.
  • Redefinir els mètodes toString() i equals() heretats d’Object.
  • Comprendre la diferència entre membres d’instància i estàtics en una classe pròpia.

Fins ara hem vist com fer servir objectes de classes que ja existien (Scanner, Random, String). El següent pas és crear les teves pròpies classes. Deixarem de ser només usuari d’objectes per convertir-te en dissenyador d’objectes.

12.1 Què és una classe?

Una classe és el codi que descriu com seran els objectes d’un tipus: quines dades guarden (atributs) i què saben fer (mètodes).

Escriure una classe és omplir el motlle del qual, més tard, en trauràs tants objectes com vulguis amb new.

L’estructura mínima d’una classe en Java és:

public class Llibre {
    // 1) Atributs: l'estat de cada objecte
    String titol;
    String autor;
    int anyPublicacio;

    // 2) Mètodes: el comportament
    void mostrar() {
        System.out.println(titol + " (" + autor + ", " + anyPublicacio + ")");
    }
}

Amb aquesta classe ja podem crear i usar objectes des d’una altra classe amb main:

public class Programa {
    public static void main(String[] args) {
        Llibre ll = new Llibre();
        ll.titol = "La plaça del diamant";
        ll.autor = "Mercè Rodoreda";
        ll.anyPublicacio = 1962;
        ll.mostrar();   // La plaça del diamant (Mercè Rodoreda, 1962)
    }
}
NotaUn fitxer, una classe pública

En Java, cada classe public ha d’anar en un fitxer amb exactament el mateix nom i extensió .java. La classe Llibre ha de viure a Llibre.java.

S’han de respectar també les majúscules: els noms de classe s’escriuen en PascalCase (primera lletra de cada paraula en majúscula).

Mini-repte. Escriu l’esquelet d’una classe Rectangle amb dos atributs (base i alcada, de tipus double) i un mètode void mostrar() que no faci res encara.

12.2 Encapsulació i visibilitat

La classe anterior té un problema greu. Qualsevol part del programa pot escriure directament ll.anyPublicacio = -3000; i deixar l’objecte en un estat sense sentit. Les dades estan exposades i desprotegides.

L’encapsulació és el principi de la POO que diu: amaga les dades internes i controla com s’hi accedeix.

La idea és protegir l’estat de l’objecte perquè no es pugui corrompre des de fora.

ConsellAnalogia: el caixer automàtic

No pots ficar la mà a la caixa forta del banc per treure diners. Interactues a través d’una interfície controlada (la pantalla i el teclat del caixer), que comprova el teu PIN i que no treguis més del compte. L’encapsulació fa el mateix: amaga els “diners” (les dades) i només deixa operar-hi a través de mètodes que validen.

Per aconseguir-ho fem servir els modificadors de visibilitat davant de cada membre:

Modificador Qui hi pot accedir
private Només des de dins de la mateixa classe
public Des de qualsevol classe
protected La mateixa classe, les subclasses i el mateix paquet
(cap) Les classes del mateix paquet (visibilitat “de paquet”)


La recepta bàsica de l’encapsulació és:

Atributs private, mètodes d’accés public.

public class Llibre {
    private String titol;    // Ningú de fora hi arriba directament
    private String autor;
    private int anyPublicacio;
}

Ara ll.titol = "..." des de fora no compila. Bé: hem tancat la porta. Al següent apartat obrirem finestres controlades (getters i setters).

AlertaError típic: fer-ho tot public

És temptador posar public a tot per estalviar-se problemes de compilació. Fer-ho anul·la l’encapsulació i torna el programa fràgil: qualsevol pot deixar un objecte en un estat invàlid. Per defecte, atributs sempre private.

Mini-repte. Reescriu la teva classe Rectangle perquè base i alcada siguin private.

12.3 Getters i setters

Si els atributs són private, com els llegim i modifiquem des de fora? Amb mètodes públics especialitzats:

  • Un getter retorna el valor d’un atribut. Es diu getNom() o isNom() per a booleans.
  • Un setter modifica el valor d’un atribut. Es diu setNom(valor) i és el lloc ideal per validar abans d’acceptar el canvi.
public class Llibre {
    private String titol;
    private int anyPublicacio;

    public String getTitol() {
        return titol;
    }

    public void setTitol(String nouTitol) {
        this.titol = nouTitol;
    }

    public int getAnyPublicacio() {
        return anyPublicacio;
    }

    public void setAnyPublicacio(int any) {
        // Validació: no acceptem anys fora del rang establert
        if (any >= 1450 && any <= 2100) {
            this.anyPublicacio = any;
        } else {
            System.out.println("Any no vàlid, s'ignora: " + any);
        }
    }
}

El gran avantatge: el setAnyPublicacio filtra valors dolents. L’estat de l’objecte queda protegit encara que qui l’usa s’equivoqui.

ConsellNo tot atribut necessita setter

Si un atribut no ha de canviar mai després de crear l’objecte (per exemple, l’ISBN d’un llibre), no li posis setter. Deixa’l només amb getter (i assigna’l al constructor).

Menys portes obertes, menys errors.

Mini-repte. Afegeix a Rectangle un setter setBase(double b) que només accepti valors positius.

12.4 Constructors

Crear un objecte i després omplir-lo atribut per atribut amb els mètodes getter és pesat i propens a oblits o errors.

El constructor permet donar tots els valors inicials en el moment de crear l’objecte.

Un constructor es diu igual que la classe i no té tipus de retorn.

public class Llibre {
    private String titol;
    private String autor;
    private int anyPublicacio;

    // Constructor amb paràmetres
    public Llibre(String titol, String autor, int anyPublicacio) {
        this.titol = titol;
        this.autor = autor;
        this.anyPublicacio = anyPublicacio;
    }
}

Ara la creació és neta i garanteix que l’objecte neix complet:

Llibre ll = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);

12.4.1 El constructor per defecte

Si no escrius cap constructor, Java en crea un de buit sense paràmetres (el constructor per defecte), que és el que feies servir amb new Llibre().

AlertaError típic: perdre el constructor per defecte

En el moment que escrius qualsevol constructor propi, Java ja no defineix el constructor buit. Si després fas new Llibre() sense haver-lo definit, no compilarà:

Llibre ll = new Llibre();   // ERROR si només has definit
                            // el constructor amb 3 paràmetres

Solució: si necessites els dos, defineix-los tots dos.

12.4.2 Sobrecàrrega de constructors

Pots tenir diversos constructors a la mateixa classe sempre que tinguin paràmetres diferents (en nombre o tipus). Es diu sobrecàrrega.

public class Llibre {
    private String titol;
    private String autor;
    private int anyPublicacio;

    // Constructor complet
    public Llibre(String titol, String autor, int anyPublicacio) {
        this.titol = titol;
        this.autor = autor;
        this.anyPublicacio = anyPublicacio;
    }

    // Constructor curt: reaprofita el complet amb this(...)
    public Llibre(String titol, String autor) {
        this(titol, autor, 2025);   // any per defecte
    }
}

Fixa’t en this(...): crida un altre constructor de la mateixa classe per no repetir codi.

Mini-repte. Escriu un constructor per a Rectangle que rebi base i alcada, i un segon constructor sense paràmetres que creï un quadrat 1×1 reutilitzant el primer amb this(...).

12.5 La paraula clau this

Has vist this diverses vegades. this és una referència a l’objecte actual: l’objecte sobre el qual s’està executant el mètode o el constructor.

El seu ús més habitual és desambiguar quan un paràmetre es diu igual que un atribut:

public void setTitol(String titol) {
    this.titol = titol;   // this.titol = atribut ; titol = paràmetre
}

Sense this, titol = titol assignaria el paràmetre a si mateix i l’atribut es quedaria buit: un error silenciós molt típic.

ConsellAnalogia: “jo mateix”

Quan una persona diu “aquest cotxe és meu”, el “meu” es refereix a qui parla. this és el “meu” de l’objecte: quan un mètode diu this.titol, vol dir “el títol d’aquest objecte, el que està executant ara mateix el mètode”.

Usos de this:

  • this.atribut → distingir atribut de paràmetre.
  • this(...) → cridar un altre constructor de la mateixa classe.
  • this sol → passar l’objecte actual com a argument a un altre mètode.
AlertaError típic: oblidar this al setter
public void setPreu(double preu) {
    preu = preu;   // NO fa res útil: assigna el paràmetre a si mateix!
}

L’atribut preu de l’objecte no canvia. Ha de ser this.preu = preu;.

12.6 Mètodes: el comportament de la classe

A part dels getters i setters, una classe té mètodes que representen les accions pròpies del domini.

Aquests mètodes operen sobre l’estat de l’objecte (els seus atributs).

public class CompteBancari {
    private String titular;
    private double saldo;

    public CompteBancari(String titular, double saldoInicial) {
        this.titular = titular;
        this.saldo = saldoInicial;
    }

    public void ingressar(double quantitat) {
        if (quantitat > 0) {
            saldo += quantitat;
        }
    }

    public boolean retirar(double quantitat) {
        if (quantitat > 0 && quantitat <= saldo) {
            saldo -= quantitat;
            return true;                            // Operació feta
        }
        return false;                               // Fons insuficients
    }

    public double getSaldo() {
        return saldo;
    }
}

Fixa’t que dins d’un mètode pots accedir directament als atributs (saldo) sense escriure this, perquè no hi ha ambigüitat.

classDiagram
    class CompteBancari {
        -String titular
        -double saldo
        +CompteBancari(String, double)
        +ingressar(double) void
        +retirar(double) boolean
        +getSaldo() double
    }

NotaNotació UML: - i +

Al diagrama de dalt, el símbol - marca els membres privats i el + els públics. És la notació estàndard UML, que veurem més endavant.

Mini-repte. Afegeix a Rectangle dos mètodes: double area() i double perimetre().

12.7 Membres estàtics en una classe pròpia

Ja hem utilitzat membres static de classes com Math. Ara pots posar-ne a les teves. Un membre static pertany a la classe, no a cada objecte: se’n comparteix una sola còpia entre tots.

Un ús molt típic és comptar quants objectes s’han creat o definir constants:

public class Soci {
    public static final double QUOTA_ANUAL = 25.0;  // Constant compartida
    private static int totalSocis = 0;              // Comptador compartit

    private int numeroSoci;
    private String nom;

    public Soci(String nom) {
        this.nom = nom;
        totalSocis++;                       // Cada nou soci incrementa el total
        this.numeroSoci = totalSocis;       // i rep un número correlatiu
    }

    public static int getTotalSocis() {
        return totalSocis;
    }
}

Ús:

Soci a = new Soci("Aina");
Soci b = new Soci("Bru");
System.out.println(Soci.getTotalSocis());   // 2 (es crida amb el nom de la classe)
System.out.println(Soci.QUOTA_ANUAL);       // 25.0
AlertaError típic: usar this en un mètode estàtic

Un mètode static no s’executa sobre cap objecte concret, així que no pot usar this ni accedir a atributs d’instància. getTotalSocis() pot llegir totalSocis (que és static), però no podria llegir nom (que és d’instància).

Mini-repte. Afegeix a Rectangle una constant static final amb el nombre de costats (4).

12.8 Redefinició del mètode toString()

Tota classe en Java penja d’una classe base anomenada Object. S’hereten alguns mètodes que convé redefinir.

El primer és toString(), que retorna una representació de l’objecte en forma de text. Per defecte és inútil:

Llibre ll = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);
System.out.println(ll);   // Llibre@1b6d3586  ← poc útil!

Aquest Llibre@1b6d3586 és el nom de la classe i un codi intern. Redefinim toString() perquè digui alguna cosa útil:

@Override
public String toString() {
    return titol + " — " + autor + " (" + anyPublicacio + ")";
}

Ara:

System.out.println(ll);   // La plaça del diamant — Mercè Rodoreda (1962)


System.out.println crida toString() automàticament. L’anotació @Override indica que estem reescrivint un mètode heretat; la veurem més endavant amb més profunditat.

Mini-repte. Escriu el toString() de Rectangle que retorni, per exemple, "Rectangle 3.0×2.0".

12.9 Redefinir equals()

El segon mètode heretat clau és equals(), que decideix si dos objectes són “iguals”.

Aquí hi ha un parany important. L’operador == entre objectes compara referències (si són el mateix objecte a la memòria), no el contingut:

Llibre a = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);
Llibre b = new Llibre("La plaça del diamant", "Mercè Rodoreda", 1962);

System.out.println(a == b);        // Fals. Són dos objectes diferents
System.out.println(a.equals(b));   // Per defecte també false.

Per defecte, equals() fa el mateix que ==. Si volem que dos llibres amb el mateix contingut es considerin iguals, hem de redefinir equals():

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;                                       // Mateix objecte
    if (obj == null || getClass() != obj.getClass()) return false;
    Llibre altre = (Llibre) obj;                                        // Càsting segur
    return anyPublicacio == altre.anyPublicacio
            && titol.equals(altre.titol)
            && autor.equals(altre.autor);
}

Ara a.equals(b) retorna true perquè comparem els continguts.

AlertaError típic: comparar String amb ==

Aquest és, potser, l’error més comú entre principiants:

String s1 = teclat.nextLine();   // L'usuari escriu "hola"
if (s1 == "hola") { ... }        // gairebé sempre FALSE!

== compara si són el mateix objecte, no el mateix text. Per comparar el contingut de textos s’usa SEMPRE .equals():

if (s1.equals("hola")) { ... }   // Correcte
Notaequals() i hashCode() van de bracet

Per conveni, si redefineixes equals() també hauries de redefinir hashCode(). És imprescindible perquè els objectes funcionin bé dins de col·leccions com HashSet o HashMap. L’IDE els pot generar tots dos alhora automàticament.

Mini-repte. Explica per què "5" == "5" pot donar true però new String("5") == "5" dona false. (Pista: reserva de textos vs objecte nou.)

12.10 Posem-ho tot junt

Aquesta és la classe Llibre completa, ben encapsulada, amb tot el que hem vist:

public class Llibre {
    // --- Atributs (privats) ---
    private String titol;
    private String autor;
    private int anyPublicacio;
    private boolean prestat;

    // --- Constructor ---
    public Llibre(String titol, String autor, int anyPublicacio) {
        this.titol = titol;
        this.autor = autor;
        setAnyPublicacio(anyPublicacio);  // reaprofitem la validació
        this.prestat = false;             // un llibre nou no està prestat
    }

    // --- Getters i setters ---
    public String getTitol() { return titol; }
    public String getAutor() { return autor; }
    public int getAnyPublicacio() { return anyPublicacio; }
    public boolean isPrestat() { return prestat; }

    public void setAnyPublicacio(int any) {
        if (any >= 1450 && any <= 2100) {
            this.anyPublicacio = any;
        }
    }

    // --- Comportament del domini ---
    public void prestar() {
        if (!prestat) {
            prestat = true;
        } else {
            System.out.println("Ja estava prestat!");
        }
    }

    public void retornar() {
        prestat = false;
    }

    // --- Mètodes heretats redefinits ---
    @Override
    public String toString() {
        String estat = prestat ? "prestat" : "disponible";
        return titol + " — " + autor + " (" + anyPublicacio + ") [" + estat + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Llibre altre = (Llibre) obj;
        return anyPublicacio == altre.anyPublicacio
                && titol.equals(altre.titol)
                && autor.equals(altre.autor);
    }
}

12.11 Resum

  • Una classe defineix els atributs (estat) i els mètodes (comportament) dels seus objectes.
  • L’encapsulació protegeix l’estat: atributs private, accés a través de mètodes public.
  • Els getters llegeixen i els setters modifiquen atributs; els setters són el lloc per validar.
  • Els constructors inicialitzen l’objecte en crear-lo. En definir-ne un de propi es perd el constructor per defecte. Es poden sobrecarregar i encadenar amb this(...).
  • this és la referència a l’objecte actual; serveix per desambiguar atributs de paràmetres.
  • Els membres static pertanyen a la classe i es comparteixen entre tots els objectes; les constants es fan static final.
  • Convé redefinir toString() (representació en text) i equals() (igualtat per contingut). Recorda: per comparar textos s’usa .equals(), mai ==.

12.12 Per practicar

  1. Classe Rectangle completa. Recull totes les peces dels mini-reptes: atributs privats base i alcada, dos constructors, getters/setters amb validació (només positius), area(), perimetre(), la constant NOMBRE_COSTATS i toString().

  2. Classe Termometre. Atribut privat temperatura (double). Mètodes pujar(double), baixar(double) i un getter. El setter no ha de deixar baixar de −273.15.

  3. Comptador d’instàncies. Amplia Rectangle amb un comptador static que digui quants rectangles s’han creat. Prova-ho creant-ne tres i imprimint el total.

  4. Detecta l’error. Aquest setter no funciona. Digues per què i corregeix-lo:

public void setAlcada(double alcada) {
    alcada = alcada;
}
  1. equals() a la pràctica. Crea dos objectes Rectangle de 3×2 per separat i comprova què donen == i .equals() abans i després de redefinir equals().

  2. Disseny propi. Dissenya des de zero una classe CompteBancari amb número, titular i saldo, que no permeti descoberts. Escriu un main que en creï un, hi faci ingressos i retirades, i imprimeixi l’objecte amb toString().