Declaració de classes

L’element fonamental de tot programa orientat a objectes és l’objecte. Aquests objectes es generen a partir d’un fitxer de codi font, una classe, on es defineixen les seves propietats i el seu comportament (atributs i mètodes). El llenguatge Java proporciona un conjunt de classes ja creades que es poden usar directament, però gairebé sempre és necessari generar classes noves, d’acord a les necessitats de cada programa concret. Per tant, la clau per poder generar el codi font d’un programa orientat a objectes està en el fet de saber com generar codi per declarar classes correctament.

Pas a codi de classes

La codificació d’una classe segueix la sintaxi següent, en què s’aprecien dues parts ben diferenciades.

DeclaracióDeLaClasse {
  CosDeLaClasse
}

Cadascuna de les dues parts (declaració i cos) pot ser més o menys complexa i, com acostuma a succeir en l’aprenentatge de qualsevol llenguatge, començarem per les formes més simples per avançar posteriorment cap a formes més complexes.

En principi, totes les classes que hem dissenyat han tingut, com a declaració, la sintaxi següent:

public class <NomClasse>

El modificador davant el nom d’una classe possibilita que la classe sigui accessible des d’altres classes.

Aquesta declaració es pot veure ampliada amb altres modificadors (a més del public) a l’esquerra de la paraula class i amb uns modificadors a la dreta de NomClasse. Per crear les primeres classes, però, no els necessitem.

El cos de la classe és una seqüència de tres tipus de components:

  • Els relatius a les dades que contindran els objectes de la classe (els atributs).
  • Els relatius a blocs de codi sense nom, coneguts com a iniciadors.
  • Els relatius als mètodes que la classe proporciona per gestionar les dades que emmagatzema.

En principi aquests tres tipus de components es poden incloure dins la definició de la classe en qualsevol ordre, però hi ha el conveni de començar amb les dades, continuar amb els iniciadors i finalitzar amb els mètodes.

Així, doncs:

public class <NomClasse> {
  <seqüènciaDeclaracionsDeDades>;
  <seqüènciaIniciadors>;
  <seqüènciaDefinicionsDeMètodes>
}

Un fitxer de codi Java pot incorporar diverses classes, però normalment el millor és que només es declari una a cada fitxer. El nom del fitxer ha de ser exactament igual al de la classe (incloses majúscules/minúscules)

Declaració de les dades

La seqüència de declaracions de dades consisteix en declaracions de variables de tipus primitius i/o de referències a objectes d’altres classes, seguint la sintaxi següent:

[<modificadors>] <nomTipus> <nomDada> [=<valorInicial>];

En aquesta sintaxi veiem que la declaració de la dada pot estar precedida d’uns modificadors. Normalment, sempre s’usarà el modificador private, excepte en casos especials, com la declaració de constants. En aquest cas, s’usa public static final.

Veiem també que la declaració d’una dada pot estar acompanyada d’una inicialització explícita (=<valorInicial>).

Java inicialitza implícitament les dades dels objectes durant el procés de creació, però en canvi no inicialitza les variables declarades en mètodes.

En el moment en què crea cada dada, Java efectua una inicialització implícita de totes les dades amb valor zero pels tipus enters, reals i caràcter, amb valor false per al tipus lògic i amb valor null per a les variables de referència. Posteriorment s’executen les inicialitzacions explícites que hagi pogut indicar el programador en la declaració de la dada.

Iniciadors

Els iniciadors són blocs de codi (sentències entre claus) que s’executen cada vegada que es crea un objecte de la classe. Es defineixen seguint la sintaxi següent:

{
  <conjunt_de_sentències>;
}

Quin sentit té l’existència d’iniciadors si ja disposem dels constructors per indicar el codi a executar en la creació d’objectes? La resposta és que de vegades podem tenir blocs de codi a executar en el procés de creació d’un objecte de la classe, sigui quin sigui el constructor (n’hi poden haver diversos) emprat en la creació, i la utilització d’iniciadors ens permet no haver de repetir el mateix codi dins els diversos constructors.

A més, els iniciadors també són indicats per ser utilitzats en el disseny de classes anònimes, les quals, en no tenir nom, no poden tenir mètodes constructors.

En cas d’existir diversos iniciadors s’executen en l’ordre en què es trobin dins la classe.

Definició de les operacions

La seqüència de definicions d’operacions consisteix en la definició (prototipus i contingut) dels diversos mètodes amb la sintaxi de Java. La manera més simple de definir un mètode en Java segueix la sintaxi següent:

[<modificadors>] <tipusRetorn> <nomMètode> (<llistaArguments>) {
  <declaracióVariablesLocals>
  <cosDelMètode>
}

En aquesta sintaxi veiem que la declaració del mètode pot anar precedida d’uns modificadors, tot i que el més habitual (però no sempre) serà public. Per crear els primers mètodes, però, no els necessitem. Per indicar que un mètode no retorna cap resultat, s’utilitza el tipus void.

Respecte a la llista d’arguments, cal comentar que el pas de paràmetres en Java sempre és usant el mecanisme anomenat per valor, o sigui, es garanteix que tot paràmetre utilitzat en una crida a un mètode manté el valor inicial en finalitzar l’execució del mètode, però, si el paràmetre és una variable que fa referència a un objecte, l’objecte sí pot ser modificat (no substituït) dins el mètode. Al acabar la crida, aquesta modificació es manté.

Ja estem en condicions de dissenyar la primera classe i fer un petit programa que comprovi el funcionament dels diferents mètodes desenvolupats.

Primera versió d'una classe per gestionar persones

Suposem que es vol dissenyar una classe per gestionar persones, per a les quals interessa gestionar-ne el dni, el nom i l’edat.

Mètodes accessors

Totes les classes acostumen a proporcionar uns mètodes de lectura (get) i escriptura (set) sobre els atributs de la classe, de manera que poden ser manipulats: són les anomenades operacions accessores.

Prenem les primeres decisions de disseny i decidim que dni i nom han de ser objectes String i que edat ha de ser un short. Respecte als mètodes, en un principi se’ns acut desenvolupar els mètodes corresponents a les operacions accessores i, potser, un mètode visualitzar() per mostrar tot el contingut d’una persona.

Una possible solució és:

//Fitxer Persona.java
public class Persona {
   String dni;
   String nom;
   short edat;
   // Retorna: 0 si s'ha pogut canviar el dni
   //          1 si el nou dni no és correcte - No s'efectua el canvi
   int setDni(String nouDni) {
      // Aquí hi podria haver una rutina de verificació del dni
      // i actuar en conseqüència. Com que no la incorporem, retornem sempre 0
      dni = nouDni;
      return 0;
   }

   void setNom(String nouNom) {
      nom = nouNom;
   }
   
   // Retorna: 0 si s'ha pogut canviar l'edat
   //          1 : Error per passar una edat negativa
   //          2 : Error per passar una edat "enorme"
   int setEdat(int novaEdat) {
      if (novaEdat<0) return 1;
      if (novaEdat>Short.MAX_VALUE) return 2;
      edat = (short)novaEdat;
      return 0;
   }
   
   String getDni() { return dni; }
   String getNom() { return nom; }
   short getEdat() { return edat; }
   
   void visualitzar() {
      System.out.println("Dni...........:" + dni);
      System.out.println("Nom...........:" + nom);
      System.out.println("Edat..........:" + edat);
   }

   public static void main(String args[]) {
      Persona p1 = new Persona();
      Persona p2 = new Persona();
      p1.setDni("00000000");
      p1.setNom("Pepe Gotera");
      p1.setEdat(33);
      System.out.println("Visualització de persona p1:");
      p1.visualitzar();
      System.out.println("El dni de p1 és " + p1.getDni());
      System.out.println("El nom de p1 és " + p1.getNom());
      System.out.println("L'edat de p1 és " + p1.getEdat());
      System.out.println("Visualització de persona p2:");
      p2.visualitzar();
   }
}

L’execució d’aquest programa dóna el resultat següent:

Visualització de persona p1:
Dni...........:00000000
Nom...........:Pepe Gotera
Edat..........:33
El dni de p1 és 00000000
El nom de p1 és Pepe Gotera
L'edat de p1 és 33
Visualització de persona p2:
Dni...........:null
Nom...........:null
Edat..........:0

Classes embolcall

El llenguatge Java proporciona per a cadascun dels vuit tipus primitius una classe corresponent, amb el mateix nom que el tipus però iniciades amb majúscula, anomenades classes embolcall (wrapper, en anglès), que proporcionen dades i mètodes per a la gestió dels tipus de dades corresponents.

L’execució del programa sembla adequada. Veiem que les dades de la persona “p2”, no inicialitzades explícitament, han estat inicialitzades - tal i com hem dit més amunt - implícitament amb valor zero les numèriques i valor null les referències. Aprofitem aquest exemple per presentar una problemàtica que ens podem trobar en el disseny de moltes classes, relativa al fet que la classe conté dades de tipus byte o short i, en canvi, els arguments dels mètodes que recullen valors per emplenar aquestes dades es defineixen de tipus int. Per què ho fem? Què hem de tenir en compte?:

  • La declaració dels arguments dels mètodes de tipus int està fonamentada en el tipus de dada associat als literals que s’acostumaran a utilitzar. Així, com que és molt possible cridar el mètode setEdat() passant un literal enter (com en l’exemple), és lògic declarar l’argument d’aquest mètode de tipus int, ja que els literals enters són d’aquest tipus (o long si s’afegeix la lletra “L” al final del literal). Si haguéssim declarat l’argument de tipus short (adequat al tipus de la dada a què s’assignarà en l’interior del mètode), la crida al mètode s’hauria de fer passant un valor de tipus short o explicitant conversions com setEdat( (short) 33) i això no és desitjable.
  • El fet de declarar els arguments dels mètodes amb els tipus de dada més usuals tenint en compte els literals amb els quals podem cridar el mètode ens porta al fet que a l’interior del mètode haguem de fer comprovacions relatives a si el valor que arriba és adequat en termes de grandària (rang). En l’exemple, el mètode setEdat() rep per paràmetre un valor int i en el seu interior, abans d’emplenar la dada edat declarada de tipus short, ens interessa comprovar si el valor és assumible per a una dada de tipus short. Per fer aquests tipus de comprovacions, el llenguatge Java ens proporciona mecanismes per saber quins són els rangs de valors permesos pels diferents tipus de dades. En l’exemple, utilitzem el valor MAX_VALUE de la classe Short per comprovar si el valor enter de l’argument “novaEdat” del mètode setEdat() és massa gran per la dada edat.

Modificadors dins d'una classe

A l’hora de definir atributs o mètodes dins una classe, és possible indicar un modificador d’accés. Vegem amb més detall quins són a la sintaxi del Java.

[<modificadorAccés>] [<altresModificadors>] <tipusDada> <nomDada>;

[<modificadorAccés>] [<altresModificadors>] <tipusRetorn> <nomMètode> (<llistaArgs>)
{...}

Paquets

Les classes es poden organitzar en paquets i aquesta possibilitat s’acostuma a utilitzar quan tenim un conjunt de classes relacionades entre elles. Totes les classes no incloses explícitament en cap paquet i que estan situades en un mateix directori es consideren d’un mateix paquet.

El modificador d’accés pot prendre quatre valors:

  • Public, que dóna accés a tothom.
  • Private, que prohibeix l’accés a tothom menys pels mètodes de la pròpia classe.
  • Protected, que es comporta com a public per a les classes derivades de la classe i com a private per a la resta de classes.
  • Sense modificador, que es comporta com a public per a les classes del mateix paquet i com a private per a la resta de classes.

Donada la classe Persona, si desenvolupem un programa que instanciï objectes de la classe, tenim accés directe a les dades dni, nom i edat? Considerem el programa següent en què es creen objectes de la classe Persona.

//Fitxer CridaPersona.java
public class CridaPersona {
   public static void main(String args[]) {
      Persona p = new Persona();
      p.dni = "--$%#@--";
      p.nom = "";
      p.edat = -23;
      System.out.println("Visualització de la persona p:");
      p.visualitzar();
   }
}

En aquest cas estem en un programa extern a la classe Persona i es veu com accedim directament a les dades dni, nom i edat de la persona creada, i podem fer autèntiques animalades. El compilador no es queixa (cal haver compilat també l’arxiu Persona.java en el mateix directori) i l’execució dóna el resultat:

Visualització de la persona p:
Dni...........:--$%#@--
Nom...........:
Edat..........:-23

Acabem de veure, doncs, que la versió actual de la classe Persona permet el lliure accés als valors dels seus atributs, ja que en la definició d’aquestes dades no s’ha posat al davant el modificador adequat per evitar-ho. Les classes CridaPersona i Persona, en estar situades en el mateix directori, s’han considerat del mateix paquet i, per tant, en no haver-hi cap modificador d’accés en la definició de les dades dni, nom i edat, la classe CridaPersona hi ha tingut accés total. A més, en no haver-hi cap modificador d’accés en la definició dels mètodes, aquests no poden ser cridats per classes de paquets diferents del paquet al qual pertany la classe Persona.

Normalment, al crear classes el més correcte és que els atributs no tinguin accés directe. Els motius són:

  • Protegir les dades de modificacions impròpies.
  • Facilitar el manteniment de la classe, ja que si per algun motiu es creu que cal efectuar alguna reestructuració de dades o de funcionament intern, es podran efectuar els canvis pertinents sense afectar les aplicacions desenvolupades (sempre que no es modifiquin els prototipus dels mètodes existents).

Sembla lògic, doncs, fer evolucionar la versió actual de la classe Persona cap a una classe que tingui les dades declarades com a privades i els mètodes com a públics. Fixem-nos que el mètode main per comprovar el funcionament d’una classe sempre ha estat declarat public.

Versió de la classe Persona amb modificadors d'accés adequats

A continuació presentem una versió evolucionada de la classe Persona que inclou els modificadors d’accés adequats: dades a private i mètodes a public.

//Fitxer Persona.java

public class Persona {
   private String dni;
   private String nom;
   private short edat;
   
   // Retorna: 0 si s'ha pogut canviar el dni
   //          1 si el nou dni no és correcte - No s'efectua el canvi
   public int setDni(String nouDni) {
      // Aquí hi podria haver una rutina de verificació del dni
      // i actuar en conseqüència. Com que no la incorporem, retornem sempre 0
      dni = nouDni;
      return 0;
   }

   public void setNom(String nouNom) {
      nom = nouNom;
   }
   
   // Retorna: 0 si s'ha pogut canviar l'edat
   //          1 : Error per passar una edat negativa
   //          2 : Error per passar una edat "enorme"
   public int setEdat(int novaEdat) {
      if (novaEdat<0) return 1;
      if (novaEdat>Short.MAX_VALUE) return 2;
      edat = (short)novaEdat;
      return 0;
   }
   
   public String getDni() { return dni; }
   
   public String getNom() { return nom; }
   
   public short getEdat() { return edat; }
   
   public void visualitzar() {
      System.out.println("Dni...........:" + dni);
      System.out.println("Nom...........:" + nom);
      System.out.println("Edat..........:" + edat);
   }
     
  public static void main(String args[]) {
     Persona p1 = new Persona();
     Persona p2 = new Persona();
     p1.setDni("00000000");
     p1.setNom("Pepe Gotera");
     p1.setEdat(33);
     System.out.println("Visualització de persona p1:");
     p1.visualitzar();
     System.out.println("El dni de p1 és " + p1.getDni());
     System.out.println("El nom de p1 és " + p1.getNom());
     System.out.println("L'edat de p1 és " + p1.getEdat());
     System.out.println("Visualització de persona p2:");
     p2.visualitzar();
  }
}

Amb aquesta versió de la classe Persona compilada, vegem què succeeix quan intentem compilar la classe CridaPersona que crea una persona i intenta accedir directament a les dades:

CridaPersona.java:11: dni has private access in Persona
   p.dni = "--$%#@--";
   ^
CridaPersona.java:12: nom has private access in Persona
   p.nom = "";
   ^
CridaPersona.java:13: edat has private access in Persona
   p.edat = -23;
   ^
3 errors

Mètodes privats

Pot tenir sentit un mètode private? La resposta és afirmativa, ja que en el disseny d’una classe pot interessar desenvolupar un mètode intern per ser cridat en el disseny d’altres mètodes de la classe i no es vol donar a conèixer a la comunitat de programadors que utilitzaran la classe.

Fixem-nos que el compilador ja detecta que no hi ha accés a les dades. Hem aconseguit el nostre objectiu: protegir l’accés directe a les dades. Ara potser no sigui massa evident encara, però els avantatges d’assolir aquest objectiu s’aniran fent més evidents a mesura que es vagi avançant en l’aprenentatge de la creació de programes orientats a objectes.

Sobrecàrrega de mètodes

De vegades, en els programes, cal dissenyar diverses versions de mètodes que tenen un mateix significat i/o objectiu però que s’apliquen en diferents tipus i/o nombre de dades. Així, si necessitàvem disposar d’una funció que sabés sumar dos enters i d’una funció que sabés sumar dos reals, podriem fer simplement dos mètodes diferents anomenats sumaEnters i sumaReals. Els dos tenen el mateix objectiu i significat, tot i que la gestió interna pot ser força diferent, i des d’un punt de vista lògic, com que les dues permeten calcular una suma,

Java permet declarar mètodes repetits amb el mateix nom. Això no és possible a tots els llenguatges de programació. Per exemple:

public int suma (int n1, int n2) { ... }
public double suma (double r1, double r2) { ... }

El terme anglès per a la sobrecàrrega, molt emprat en informàtica, és overloading.

La sobrecàrrega de mètodes és la funcionalitat que permet tenir mètodes diferents amb un mateix nom.

Normalment la sobrecàrrega d’un nom de mètode s’utilitza en aquells que tenen un mateix objectiu, però és lícit utilitzar-la en mètodes que no tinguin res a veure. Això no acostuma a succeir si el dissenyador assigna a els mètodes noms que tinguin a veure amb el seu objectiu.

Hi ha dues regles per poder aplicar la sobrecàrrega de mètodes:

  • La llista d’arguments ha de ser suficientment diferent per permetre una determinació inequívoca del mètode que es crida.
  • Els tipus de dades que retornen poden ser diferents o iguals i no n’hi ha prou de tenir els tipus de retorn diferents per distingir el mètode que es crida.

El compilador només pot distingir el mètode que es crida a partir del nombre i tipus dels paràmetres indicats en la crida.

Exemples de mètodes sobrecarregats els podem trobar en moltes classes proporcionades pel llenguatge Java. Així, per exemple, la coneguda classe String té molts mètodes sobrecarregats, com ara format(), getBytes(), indexOf(), etc.

Inicialització d'objectes

La construcció d’un objecte s’efectua amb la utilització de l’operador new acompanyada d’un constructor de la classe. Si bé per classes ja existents dins les llibreries de Java aquests constructors ja existeixen, pel cas de classes noves generades dins un programa orientat a objectes, caldrà declarar-hi aquests constructors entre els seus mètodes disponibles.

Procés d'inicialització d'un objecte al Java

Els passos que segueix la màquina virtual davant l’execució de l’operador new són:

  1. Reserva memòria per desar el nou objecte i totes les seves dades són inicialitzades amb valor zero pels tipus enters, reals i caràcter, amb valor false pel tipus booleà, i amb valor null per les variables on es desen objectes.
  2. S’executen les inicialitzacions explícites. Les dades membres d’una classe es poden inicialitzar explícitament tot assignant expressions en la declaració dels membres.
  3. S’executen els iniciadors (blocs de codi sense nom) que hi ha dins la classe seguint l’ordre d’aparició dins d’aquesta.
  4. S’executa el constructor indicat en la construcció de l’objecte amb l’operador new.

Exemple d'inicialització explícita de dades membres en una classe

//Fitxer InicialitzacioExplicita.java
import java.util.Date;

public class InicialitzacioExplicita {
   private int x = 20;
   private int y;
   private Date d = new Date (100,0,1);
   private String s;
   
   public static void main(String args[]) {
      InicialitzacioExplicita obj = new InicialitzacioExplicita();
      System.out.println("x = " + obj.x);
      System.out.println("y = " + obj.y);
      System.out.println("d = " + obj.d);
      System.out.println("s = " + obj.s);
   }
}

En aquesta classe veiem que conté quatre dades membre (“x”, “y”, “d” i “s”) de les quals n’hi ha dues que són inicialitzades explícitament en el moment de la declaració corresponent. Posteriorment, en crear un objecte “obj” de la classe, podem comprovar que les diferents dades d’aquest objecte tenen el valor esperat (“x” i “d” són inicialitzades amb els valors indicats en la declaració i “y” i “s” són inicialitzades amb els valors zero i null que assigna Java).

L’execució d’aquest programa és:

x = 20
y = 0
d = Sat Jan 01 00:00:00 CET 2000
s = null

Declaració de constructors

El mecanisme d’inicialització explícita és una manera senzilla d’inicialitzar els camps d’un objecte. No obstant això, de vegades es necessita executar un mètode constructor en concret per implementar la inicialització, ja que pot ser necessari fer el següent:

  • Recollir valors (pas de paràmetres en el moment de construcció) de manera que es puguin tenir en compte en la construcció de l’objecte.
  • Gestionar errors que puguin aparèixer en la fase d’inicialització.
  • Aplicar processos, més o menys complicats, en els quals poden intervenir tot tipus de sentències (condicionals i repetitives).

Tot això és possible gràcies a l’existència dels mètodes constructors, un dels quals sempre es crida en crear un objecte amb l’operador new. Per tant, a les classes creades per vosaltres pot ser necessari declarar constructors. En el disseny d’una classe es poden dissenyar mètodes constructors, però si no se’n dissenya cap, el llenguatge proveeix automàticament d’un constructor sense paràmetres.

Els mètodes constructors d’una classe han de seguir les normes següents:

  • El nom del mètode és idèntic al nom de la classe.
  • No se’ls pot definir cap tipus de retorn (ni tant solsvoid, no es posa absolutament res. Es deixa en blanc).
  • Poden estar sobrecarregats, és a dir, podem definir diversos constructors amb el mateix nom i diferents arguments. En cridar l’operador new, la llista de paràmetres determina quin constructor s’utilitza.
  • Si es defineix algun constructor (amb paràmetres o no), el llenguatge Java deixa de proporcionar el constructor sense paràmetres automàtic i, per tant, per poder crear objectes cridant un constructor sense paràmetres, caldrà definir-lo explícitament.

Exemple de constructors adequats per a la classe Persona

A continuació presentem un parell de constructors adequats per a la classe Persona:

public Persona () {}

public Persona (String sDni, String sNom, int nEdat) {
  dni = sDni;
  nom = sNom;
  if (nEdat>=0 && nEdat<=Short.MAX_VALUE)            
   edat = (short)nEdat;
}

Gràcies als dos constructors podem crear objectes com mostra el mètode següent main():

public static void main(String args[]) {
  Persona p1 = new Persona("00000000","Pepe Gotera",33);
  Persona p2 = new Persona();
  System.out.println("Visualització de persona p1:");
  p1.visualitzar();
  System.out.println("Visualització de persona p2:");
  p2.visualitzar();
}

El constructor que permet passar per paràmetres el dni, el nom i l’edat de l’objecte Persona a construir s’ha utilitzat per crear l’objecte a què fa referència la variable “p1”.

El constructor sense paràmetres permet la creació de l’objecte Persona a què fa referència la variable “p2”. Si no haguéssim definit el constructor sense paràmetres, la creació d’aquest objecte no hauria estat possible.

L’execució del mètode main() presentat facilita la sortida:

Visualització de persona p1:
Dni...........:00000000
Nom...........:Pepe Gotera
Edat..........:33
Visualització de persona p2:
Dni...........:null
Nom...........:null
Edat..........:0

La paraula reservada "this"

En Java existeix una paraula reservada especialment útil per tractar la manipulació d’atributs i la seva inicialització. Es tracta de this, que té dues finalitats principals:

  • Dins els mètodes no constructors, per fer referència a l’objecte actual sobre el qual s’està executant el mètode. Així, quan dins un mètode d’una classe es vol accedir a una dada de l’objecte actual, podem utilitzar la paraula reservada this, escrivint this.nomDada, i si es vol cridar un altre mètode sobre l’objecte actual, podem escriure this.nomMètode(…). En aquests casos, la utilització de la paraula this és redundant, ja que dins un mètode, per referir-nos a una dada de l’objecte actual, podem escriure directament nomDada, i per cridar un altre mètode sobre l’objecte actual podem escriure directament nomMètode(…). De vegades, però, la paraula reservada this no és redundant, com en el cas en què es vol cridar un mètode en una classe i cal passar l’objecte actual com a argument: nomMètode(this).
  • Dins els mètodes constructors, com a nom de mètode per cridar un altre constructor de la pròpia classe. De vegades pot passar que un mètode constructor hagi d’executar el mateix codi que un altre mètode constructor ja dissenyat. En aquesta situació seria interessant poder cridar el constructor existent, amb els paràmetres adequats, sense haver de copiar el codi del constructor ja dissenyat, i això ens ho facilita la paraula reservada this utilitzada com a nom de mètode: this(<llistaParàmetres>). La paraula reservada this com a mètode per cridar un constructor en el disseny d’un altre constructor només es pot utilitzar en la primera sentència del nou constructor. En finalitzar la crida d’un altre constructor mitjançant this, es continua amb l’execució de les instruccions que hi hagi després de la crida this(…).

Exemple d'utilització de la paraula reservada "this" en mètodes de la classe Persona

En primer lloc veiem que ens pot interessar tenir un constructor per crear una persona a partir d’una persona ja existent, és a dir, el constructor Persona (Persona p).

Però, d’altra banda, ja tenim un constructor (anomenem-lo xxx) que ens sap construir una persona a partir d’un dni, un nom i una edat passats per paràmetre. Per tant, per construir una persona a partir d’una persona p donada, ens interessa cridar el constructor xxx passant-li com a paràmetres el dni, el nom i l’edat de la persona p. Això ens ho facilita la paraula reservada this com a crida d’un constructor existent:

public Persona (Persona p) {
  this (p.dni, p.nom, p.edat);
}

En segon lloc, suposem que volem tenir un mètode, anomenat clonar, que aplicat sobre un objecte Persona en creï un clon, és a dir, una altra persona idèntica, i retorni la referència a la nova persona. Per aconseguir-ho hem de dissenyar el mètode que en seu interior cridi un dels constructors de la classe. Si optem per utilitzar el constructor Persona (Persona p) necessitem la paraula reservada this per fer referència a l’objecte actual:

public Persona clonar () {
  return new Persona (this);
}

El mètode main() següent permet comprovar el funcionament de tots dos mètodes:

public static void main(String args[]) {
  Persona p1 = new Persona("00000000","Pepe Gotera",33);
  Persona p2 = new Persona(p1);
  Persona p3 = p1.clonar();
  System.out.println("Visualització de persona p2:");
  p2.visualitzar();
  System.out.println("Visualització de persona p3:");
  p3.visualitzar();
}

Veiem que els dos mètodes proporcionen el mateix resultat (creació d’una nova persona com a còpia d’una persona existent) i, per tant, el mètode clonar és irrellevant si ja tenim el constructor, però ens ha servit per veure una aplicació de la paraula reservada this per fer referència a l’objecte actual sobre el qual s’executa un mètode.

L’execució del mètode main() presentat facilita la sortida:

Visualització de persona p2:
Dni...........:00000000
Nom...........:Pepe Gotera
Edat..........:33
Visualització de persona p3:
Dni...........:00000000
Nom...........:Pepe Gotera
Edat..........:33

Elements estàtics d'una classe

Les dades membre estàtic, com que són comunes per a tots els objectes de la classe, també s’anomenen variables classe.

Alguns elements d’una classe es poden declarar com “estàtics”. Per fer-ho, el llenguatge Java proporciona la paraula reservada static, amb tres finalitats:

1) Com a modificador en la declaració de dades membres d’una classe, per aconseguir que la dada afectada sigui comuna a tots els objectes de la classe. Per aconseguir aquest efecte, la dada corresponent es declara amb el modificador static, seguint la sintaxi següent:

static [<altresModificadors>] <tipusDada> <nomDada> [=<valorInicial>];

Les dades static es creen en efectuar la càrrega de la classe, quan encara no hi ha cap instància (objecte) de la classe. Atès que una dada static és comuna per a tots els objectes de la classe, s’hi accedeix de manera diferent de la utilitzada per les dades no static:

  • Per accedir-hi des de fora de la classe (possible segons el modificador d’accés que l’acompanyi), no es necessita cap objecte de la classe i s’utilitza la sintaxi NomClasse.nomDada. Recordeu que perquè això funcioni, igualment, la dada s’ha de declarar com pública.
  • Per accedir-hi des de la pròpia classe, no cal indicar cap nom d’objecte (nomObjecte.nomDada), sinó directament el seu nom.

En qualsevol cas, el llenguatge Java permet accedir a una dada static mitjançant el nom d’un objecte de la classe, però no és una nomenclatura coherent.

2) Com a modificador en la declaració de mètodes d’una classe, per aconseguir que el mètode afectat es pugui executar sense necessitat de ser cridat sobre cap objecte concret de la classe.

Si feu una ullada a la documentació del llenguatge Java, en la majoria de les classes us adonareu de l’existència de mètodes que tenen una sintaxi similar a la següent:

... static <valorRetorn> <nomMètode> (<llistaArguments>)

Com a exemple, dins la classe String, podeu veure el mètode:

public static String valueOf(char[]data)

L’explicació que l’acompanya ens diu que aquest mètode, a partir d’una taula de caràcters, ens proporciona un nou objecte String que conté la seqüència de valors de la taula de caràcters. Per tant, és clar que l’execució d’aquest mètode no necessita cap objecte String i, per tant, és lògic que sigui declarat static. Davant aquest raonament, pot aparèixer la pregunta de per què, si no necessita de cap objecte String, és declarat com un mètode de la classe String? La resposta rau en el fet que en el llenguatge Java tot mètode s’ha d’implementar en alguna classe i, ja que aquest mètode permet aconseguir un objecte String, sembla lògic que resideixi dins la classe String.

Un altre cas potser més habitual i evident és el mètode main que s’usa en les classes principals. Per poder invocar un mètode cal fer-ho sobre un objecte. Però com és possible cridar main, si en iniciar l’execució del programa encara no existeix cap objecte? Els objectes es creen precisament dins el main! Aquest problema seria un peix que es mossega la cua. La resposta està a fer-lo static, de manera que és possible fer-ne la crida sense la necessitat que hi hagi cap objecte existent prèviament.

Dels mètodes static cal saber:

  • Es criden utilitzant la sintaxi NomClasse.nomMètode(). El llenguatge Java permet cridar-los pel nom d’un objecte de la classe, però no és lògic.
  • En el seu codi no es pot utilitzar la paraula reservada this, ja que l’execució no s’efectua sobre cap objecte en concret de la classe.
  • En el seu codi només es pot accedir als seus propis arguments i a les dades static de la classe.
  • No es poden sobreescriure (sobrecarregar-los en classes derivades) per fer-los no static en les classes derivades.

3) Com a modificador d’iniciadors (blocs de codi sense nom), per aconseguir un iniciador que s’executi únicament quan es carrega la classe. La càrrega d’una classe es produeix en la primera crida d’un mètode de la classe, que pot ser el constructor involucrat en la creació d’un objecte o un mètode estàtic de la classe. La declaració d’una variable per fer referència a objectes de la classe no provoca la càrrega de la classe.

La sintaxi a emprar és:

static {...}

Exemple d'utilització de la paraula reservada "static" en les diverses possibilitats

La classe següent ens mostra una situació en què la declaració d’una dada static és necessària, ja que es vol portar un comptador del nombre d’objectes creats de manera que a cada nou objecte es pugui assignar un número de sèrie a partir del nombre d’objectes creats fins al moment.

Així mateix sembla oportú proporcionar un mètode, anomenat nombreObjectesCreats() per donar informació, com el seu nom indica, referent al nombre d’objectes creats de la classe en un moment donat.

Per acabar, s’ha inclòs un parell d’iniciadors per comprovar el funcionament dels iniciadors static i no static.

//Fitxer ExempleUsosStatic.java

public class ExempleUsosStatic {
   private static int comptador = 0;
   private int numeroSerie;

   static { System.out.println ("Iniciador \"static\" que s'executa en carregar la classe"); }
   
   { System.out.println ("Iniciador que s'executa en la creació de cada objecte"); }
   
   public ExempleUsosStatic () {
      comptador++;
      numeroSerie = comptador;
      System.out.println ("S'acaba de crear l'objecte número " + numeroSerie);
   }

   public static int nombreObjectesCreats () {
      return comptador;
   }
   
   public static void main(String args[]) {
      ExempleUsosStatic d1 = new ExempleUsosStatic();
      ExempleUsosStatic d2;
      d2 = new ExempleUsosStatic();
      System.out.println("Número de sèrie de d1 = " + d1.numeroSerie);
      System.out.println("Número de sèrie de d2 = " + d2.numeroSerie);
      System.out.println("Objectes creats: " + nombreObjectesCreats());
   }
}

L’execució del programa dóna el resultat:

Iniciador "static" que s'executa en carregar la classe
Iniciador que s'executa en la creació de cada objecte
S'acaba de crear l'objecte número 1
Iniciador que s'executa en la creació de cada objecte
S'acaba de crear l'objecte número 2
Número de sèrie de d1 = 1
Número de sèrie de d2 = 2
Objectes creats: 2

Exemple per comprovar quan es produeix la càrrega d'una classe

El programa següent demostra en quin moment es carrega una classe i, per tant, s’executen els iniciadors static que pugui tenir definits. Per executar aquest programa cal tenir en el mateix directori el fitxer compilat de la classe ExempleUsosStatic.

//Fitxer: CarregaClasse.java

public class CarregaClasse {
   public static void main (String args[]) {
      System.out.println ("Punt 1. Abans de declarar la variable obj");
      ExempleUsosStatic obj;
      System.out.println ("Punt 2. Després de declarar la variable obj");
      System.out.println ("        i abans d'invocar el mètode static");
      System.out.println ("Anem a invocar el mètode static: " +  
                           ExempleUsosStatic.nombreObjectesCreats());
   }
}

L’execució d’aquest programa mostra com l’execució de l’iniciador static de la classe ExempleUsosStatic es produeix just abans de la sentència que inclou la crida del mètode static, malgrat que abans s’hagi declarat una variable per fer referència a objectes de la classe ExempleUsosStatic.

Punt1.Abans de declarar la variable obj
Punt2.Després de declarar la variable obj
   i abans d'invocar el mètode static
Iniciador "static" que s'executa en carregar la classe
Anem a invocar el mètode static: 0

Llibreries de classes

Normalment, a l’hora de generar diferents classes, serà desitjable organitzar-les de manera que se’n pugui facilitar la gestió i saber quines estan relacionades entre si, per exemple, formant part d’un mateix programa. El llenguatge Java proporciona un mecanisme, anomenat package, per poder agrupar classes.

Abans d’entrar a veure en profunditat el funcionament dels packages del Java, és important tenir clar com es representa una classe Java dins el sistema de fitxers quan no intervenen els packages (o sigui, tal com hem treballant amb classes fins ara), tant a nivell de codi font com un cop compilada. D’aquesta manera, és més senzill entendre el seu impacte dins l’estructura d’un programa fet en Java. Això es deu al fet que, en usar un IDE, tot aquest procés de gestió dels fitxers de codi font i compilats és transparent al desenvolupador, però és igualment important saber quins fitxers estan jugant algun rol en la fase de desenvolupament d’una aplicació en Java.

Cada classe dins un programa es representa normalment dins un fitxer amb extensió .java i amb un nom idèntic (incloent majúscules i minúscules) al de la pròpia classe tal com s’ha definit al codi font (public class NomClasse {…}). Quan una classe es compila, es genera un fitxer amb extensió .class amb el mateix nom de la classe. Aquest fitxer es genera al mateix directori que el fitxer .java si s’usa el compilador amb intèrpret de comandes, però els IDE habitualment els ordenen en carpetes diferents dins els seus projectes. Per exemple, el Netbeans ubica els fitxers .java dins la carpeta src, mentre que els fitxers .class els ubica a la carpeta build\classes.

Packages

La pertinença d’una classe a un paquet s’indica amb la sentència package a l’inici del fitxer font en què resideix la classe i afecta a totes les classes definides en el fitxer. La sentència package ha de ser la primera sentència del fitxer font. Abans hi pot haver línies en blanc i/o comentaris, però res més.

Cal seguir la sintaxi següent:

package <nomPaquet>;

Els noms dels paquets (per conveni, amb minúscules) poden ser paraules separades per punts, fet que provoca que els corresponents .class s’emmagatzemin en una estructura jeràrquica de directoris que coincideix, en noms, amb les paraules que constitueixen el nom del paquet.

La inexistència de la sentència package implica que les classes del fitxer font es consideren en el paquet per defecte (sense nom) i els corresponents .class s’emmagatzemen en el mateix directori que el fitxer font.

Un paquet està constituït pel conjunt de classes dissenyades en fitxers font que incorporen la sentència package amb un nom de paquet idèntic. El paquet per defecte està constituït per totes les classes dissenyades en fitxers font que no incorporen la sentència package.

En el cas del Netbeans, les classe estaran a la carpeta “build/classes/xxx/yyy/zzz”.

Totes les classes d’un paquet anomenat “xxx.yyy.zzz” resideixen dins la subcarpeta “zzz” de l’estructura de directoris “xxx/yyy/zzz”, però podem tenir físicament aquesta estructura en diferents ubicacions. És a dir, donades les classes C1 i C2 del mateix paquet “xxx.yyy.zzz”, es podria donar el cas que el fitxer .class corresponent a C1 residís en path “xxx/yyy/zzz/C1” i que el fitxer .class corresponent a C2 residís en path “xxx/yyy/zzz/C2”.

Recordem que el codi incorporat en una classe (iniciadors i mètodes) té accés a tots els membres sense modificador d’accés de totes les classes del mateix paquet (a més de l’accés als membres amb modificador d’accés public).

En el disseny d’una classe es té accés a totes les classes del mateix paquet, però per accedir a classes de diferents paquets cal emprar un dels dos mecanismes següents:

  • Utilitzar el nom de la classe precedit del nom del paquet cada vegada que s’hagi d’utilitzar el nom de la classe, amb la sintaxi següent:
nomPaquet.NomClasse
  • Explicitar les classes d’altres paquets a les quals es farà referència amb una sentència import abans de la declaració de la nova classe, seguint la sintaxi següent:
import <nomPaquet>.<NomClasse>;

És factible carregar totes les classes d’un paquet amb una única sentència utilitzant un asterisc:

import <nomPaquet>.*;

Les sentències import en un fitxer font han de precedir a totes les declaracions de classes incorporades en el fitxer.

Així, doncs, si tenim una classe C en un paquet xxx.yyy.zzz i l’hem d’utilitzar en una altra classe, tenim dues opcions:

  • Escriure xxx.yyy.zzz.C cada vegada que haguem de referir-nos a la classe C.
  • Utilitzar la sentència import xxx.yyy.zzz.C abans de cap declaració de classe i utilitzar directament el nom C per referir-nos a la classe.

Exemple de definició de paquets de classes i accés corresponent

Considerem les classes dissenyades en el fitxer següent:

//Fitxer ClasseC1.java
package xxx.yyy.zzz;

public class ClasseC1 {
  int mc1=10;
}

class ClasseC1Bis {
  int mc1=20;
}

Veiem que aquest fitxer defineix les classes ClasseC1 i ClasseC1Bis dins un paquet anomenat “xxx.yyy.zzz”. Fixem-nos que una d’elles té el modificador public perquè s’hi pugui accedir des de fora del paquet, i recordem que en un fitxer .java només hi pot haver una classe public.

Considerem un nou fitxer .java que crea més classes en el mateix paquet “xxx.yyy.zzz”: ClasseC2.java

//Fitxer ClasseC2.java
package xxx.yyy.zzz;

public class ClasseC2 {
  int mc2=10;
}

class ClasseC2Bis {
  int mc2=20;
}

Vegem, en primer lloc, que qualsevol classe d’un paquet té accés a totes les classes del mateix paquet i als membres de les que no hagin estat declarades private. Som-hi:

//Fitxer: AccesIntern.java
package xxx.yyy.zzz;

class AccesIntern {
  public static void main (String args[]) {
   ClasseC1 c1 = new ClasseC1();
   ClasseC1Bis c1b = new ClasseC1Bis();
   ClasseC2 c2 = new ClasseC2();
   ClasseC2Bis c2b = new ClasseC2Bis();
   System.out.println ("c1.mc1 = " + c1.mc1);
   System.out.println ("c1b.mc1 = " + c1b.mc1);
   System.out.println ("c2.mc2 = " + c2.mc2);
   System.out.println ("c2b.mc2 = " + c2b.mc2);
  }
}

Procedim a compilar els fitxers ClasseC1.java i ClasseC2.java. Veiem que la compilació no dóna cap error i podem comprovar l’estructura de directoris que hem generat dins de la carpeta del projecte del vostre IDE, amb aquestes compilacions:

\xxx
\xxx\yyy
\xxx\yyy\zzz
\xxx\yyy\zzz\ClasseC1.class
\xxx\yyy\zzz\ClasseC1Bis.class
\xxx\yyy\zzz\ClasseC2.class
\xxx\yyy\zzz\ClasseC2Bis.class

Si procedim a executar el programa de la classe AccesIntern obtenim:

c1.mc1 = 10
c1b.mc1 = 20
c2.mc2 = 10
c2b.mc2 = 20

Veiem que la classe AccesIntern té accés a totes les classes del mateix paquet i a les seves dades membres, ja que no s’havien definit com a private.

Comprovem ara què cal fer per accedir a les classes del paquet “xxx.yyy.zzz” des d’una classe d’un altre paquet. Comprovarem que no podem accedir a les classes no públiques del paquet “xxx.yyy.zzz” ni als membres no públics de les classes públiques. Per fer aquestes comprovacions, considerem la classe AccesExtern següent:

//Fitxer AccesExtern.java
import xxx.yyy.zzz.*;

class AccesExtern {
   public static void main (String args[]) {
      ClasseC1 c1 = new ClasseC1();
      //ClasseC1Bis c1b = new ClasseC1Bis();    // No és classe pública
      ClasseC2 c2 = new ClasseC2();
      //ClasseC2Bis c2b = new ClasseC2Bis();    // No és classe pública
      //System.out.println ("c1.mc1 = " + c1.mc1); // No són membres públics
      //System.out.println ("c2.mc2 = " + c2.mc2); // No són membres públics
   }
}

Veiem que les instruccions comentades donarien error pels motius següents:

  • Les classes ClasseC1Bis i ClasseC2Bis no són públiques i, per tant, no s’hi té accés des de fora del paquet “xxx.yyy.zzz”.
  • El membre “mc1” de la classe ClasseC1 i el membre “mc2” de la classe ClasseC2 no són públics i, per tant, no s’hi té accés des de fora del paquet “xxx.yyy.zzz”.

En el desenvolupament d’aplicacions en Java cal tenir especial cura a utilitzar noms que siguin únics i així poder-ne assegurar la reutilització en una gran organització i, encara més, en qualsevol lloc del món. Això pot ser una tasca difícil en una gran organització i absolutament impossible dins la comunitat d’Internet. Per això es proposa que tota organització utilitzi el nom del seu domini, invertit, com a prefix per a totes les classes. És a dir, els paquets de classes desenvolupats per la Generalitat de Catalunya, que té el domini “gencat.cat”, podrien començar per “cat.gencat”.

Arxius jar

Una aplicació Java normalment es compon dels compilats de molts fitxers .java, la majoria dels quals formaran part de diferents paquets i, per tant, a l’hora de distribuir l’aplicació caldria mantenir l’estructura de directoris corresponent als paquets, cosa que pot convertir-se en una feina feixuga.

L’entorn JDK de Java ens proporciona l’eina “jar”, executada sobre línia de comandes, per empaquetar totes les estructures de directoris i els fitxers .class en un únic arxiu d’extensió.jar, que no és més que un arxiu que conté a l’interior altres fitxers, similar als .zip del compressor WinZip o als .rar del compressor WinRAR.

Per crear un fitxer .jar cal seguir les indicacions que la mateixa eina ens dóna si l’executem sense passar-li cap informació referent al que cal empaquetar:

G:\>jar
Uso: jar {ctxui}[vfm0Me] [archivo-jar] [archivo-manifiesto] [punto-entrada] [-C dir] archivos...

Opciones:
    -c crear archivo de almacenamiento
    -t crear la tabla de contenido del archivo de almacenamiento
    -x extraer el archivo mencionado (o todos) del archivo de almacenamiento
    -u actualizar archivo de almacenamiento existente
    -v generar salida detallada de los datos de salida estándar
    -f especificar nombre del archivo de almacenamiento
    -m incluir información de un archivo de manifiesto especificado
    -e especificar punto de entrada de la aplicación para aplicación autónoma
      que se incluye dentro de un archivo jar ejecutable
    -0 sólo almacenar; no utilizar compresión ZIP
    -M no crear un archivo de manifiesto para las entradas
    -i generar información de índice para los archivos jar especificados
    -C cambiar al directorio especificado e incluir el archivo siguiente
Si algún archivo coincide también con un directorio, ambos se procesarán.
El nombre del archivo de manifiesto, el nombre del archivo de almacenamiento y el nombre del pun
to de entrada se especifican en el mismo orden que las marcas 'm', 'f' y 'e'.

Ejemplo 1: para archivar dos archivos de clases en un archivo de almacenamiento llamado classes.
jar:
        jar cvf classes.jar Foo.class Bar.class
Ejemplo 2:utilice un archivo de manifiesto ya creado, 'mymanifest', y archive todos los
        archivos del directorio foo/ en 'classes.jar':
        jar cvfm classes.jar mymanifest -C foo/ .

Així, doncs, per obtenir un arxiu .jar cal executar quelcom similar a:

jar cf nomArxiu.jar fitxer1.class fitxer2.class... directori1 directori2...

Un fitxer .jar es pot descomprimir i generar tota l’estructura de directoris en la ubicació en què es vulgui tot executant quelcom similar a:

jar xf nomArxiu.jar

També hi ha possibilitats d’extreure únicament el(s) fitxer(s) desitjat(s).

El gran avantatge dels fitxers .jar és que la màquina virtual permet l’execució dels fitxers que conté sense necessitat de desempaquetar, amb la sintaxi següent:

java -cp nomArxiu.jar fitxerQueContéMètodeMain

Però, tot i així, cal saber quin és el fitxerQueContéMètodeMain. Per evitar haver de recordar el nom de la classe amb el main es pot indicar en un fitxer especial, anomenat fitxer de manifest, i incloure aquest fitxer dins l’arxiu .jar. Per aconseguir-ho, generem un fitxer de text amb qualsevol nom (per exemple, manifest.txt) amb el contingut següent i, importantíssim, amb un salt de línia al final:

Main-Class: fitxerQueContéMètodeMain

Una vegada tenim el fitxer, l’hem d’incloure en l’arxiu .jar fent:

jar cmf manifest.txt nomArxiu.jar fitxer1.class fitxer2.class... directori1 directori2...

L’opció “mf” indica que s’indica el nom del fitxer de manifest i el nom del fitxer empaquetat en aquest ordre; podem invertir les opcions:

jar cfm nomArxiu.jar manifest.txt fitxer1.class fitxer2.class... directori1 directori2...

Un cop un conjunt de classes són empaquetades dins un fitxer .jar, aquest es pot afegir directament a l’entorn de treball, de manera que al fer-ho es considera que totes les seves classes formen part del programa que s’està generant. Gestionant un únic fitxer és possible gestionar-ne en realitat molts.

El fitxer de manifest pot contenir més informació. Deixem per a vosaltres la seva investigació.

Exemple de generació i utilització d'arxiu **.jar**

Netbeans genera automàticament fitxers JAR per als vostres projectes si useu l’opció Build de la barra d’eines. El fitxer es troba a la carpeta dist.

Considerem els fitxers ClasseC1.java, ClasseC2.java i AccesIntern.java que formen part del paquet “xxx.yyy.zzz” i el fitxer AccesExtern.java. Suposem que estem en una ubicació en què tenim el compilat AccesExtern.class i d’on penja l’estructura de directoris xxx/yyy/zzz amb els fitxers ClasseC1.class, ClasseC2.class i AccesIntern.class.

Per generar un fitxer JAR que contingui els quatre .class amb l’estructura de directoris indicada:

G:\>jar cf paquet.jar AccesExtern.class xxx/yyy/zzz

Aquesta ordre ens ha generat un arxiu JAR que conté totes les classes indicades i que podem utilitzar per distribuir la nostra aplicació. Per comprovar-ne la funcionalitat, podem moure el fitxer JAR generat a una altra ubicació, situar-nos-hi i executar.

Anar a la pàgina anterior:
Exercicis
Anar a la pàgina següent:
Activitats