In aceasta lucrare de
laborator vor fi acoperite urmatoarele probleme:
- Specificarea
comportamentului claselor (metode si constructori):
- Semnaturile metodelor si returnarea
valorilor (in Java)
- Constructorii - functiile pentru initializarea obiectelor
- Supraincarcarea numelor metodelor si constructorilor – polimorfismul static
- Relatii intre clase: Asocierea si utilizarea
- Studiu de caz: Clasele
Mesaj si Pachet
- Structura de baza: campuri, constructori, metode
- Supraincarcarea numelor. Relatii intre clase
- Studiu de caz: Clasele Grupa, DatePersonale si
SituatieCurs si clasa Student actualizata
- Exercitii in laborator
Dupa invocare
(apelare) metodele obiectelor
efectueaza sarcini (in general utilizand argumentele si valorile
campurilor obiectului) care se pot
finaliza prin returnarea unei valori.
Definitia unei metode contine 2 parti: semnatura (antetul, declaratia) si corpul (blocul, segmentul, secventa de instructiuni a
implementarii).
Semnatura specifica:
- numele metodei,
- lista de parametri formali
(numarul, ordinea, tipul si numele lor),
- tipul valorii returnate,
- specificatori ai unor proprietati
explicite (modificatori ai proprietatilor implicite).
Daca metoda
nu returneaza nici o valoare, tipul
valorii returnate este declarat void.
Tipul valorii returnate poate fi unul dintre cele 8 tipuri primitive Java (byte, short, int, long, float,
double, boolean si char), sau unul dintre cele
3 tipuri referinta (tablourile, clasele si interfetele Java).
Corpul metodei contine secventa de instructiuni
care specifica pasii necesari
indeplinirii sarcinilor (evaluari expresii, atribuiri, decizii, iteratii,
apeluri metode). Returnarea valorilor
este specificata in codul metodelor prin instructiunea return urmata de o expresie care poate fi evaluata la o valoare de tipul declarat in semnatura.
In laborator: Pentru exemplul de mai jos: 1. Identificati numele
metodelor. 2. Incercati sa
determinati metodele
definite de programator si metodele bibliotecilor Java. 3. Identificati metodele care
sunt definite (cu semnatura si corp) si metodele care
sunt apelate. 4. Identificati numele si tipul
parametrilor si valorile argumentelor in fiecare caz. 5. Identificati tipul valorilor returnate
in fiecare caz. 6. Identificati instructiunile
return si comparati tipul expresiilor cu tipul declarat. |
import javax.swing.JOptionPane; // clasa de biblioteca (package) Java, externa // dar accesibila codului care urmeaza public class DialogUtilizator01 { // clasa definita de utilizator (declaratia) // corpul clasei: public String nextLine(String text) { // metode Java (operatii) return JOptionPane.showInputDialog(text); // returneaza o valoare tip String } public int nextInt(String text) { // returneaza o valoare tip int return Integer.parseInt(JOptionPane.showInputDialog(text)); } public void println(String text) { // nu returneaza nici o valoare JOptionPane.showMessageDialog(null, text); } } |
In documentatia (API-ul) claselor Java pot fi gasite detalii privind clasa JOptionPane.
In laborator: 1. Lansati mediul BlueJ. Inchideti proiectele anterioare (cu Ctrl+W sau Project si Close). 2. Creati un nou proiect numit dialog (cu Project, apoi New Project…,
selectati D:/, apoi POO2007, apoi numarul
grupei, apoi scrieti dialog). 3. Creati o noua clasa, numita DialogUtilizator01,
cu New Class… 4. Double-click pe noua clasa (deschideti editorul)
si inlocuiti
codul cu cel de sus. 5. Compilati codul apoi creati un
obiect din noua clasa. |
In laborator: 1. Executati metoda nextLine() dandu-i ca parametru “Introduceti
numele dumneavoastra”. 2. Inspectati valoarea
returnata. 3. Executati si metodele nextInt()
si println() si urmariti
efectul lor. |
Atentie! Nu uitati: Daca bara
de stare a executiei este activa () verificati cu Alt+Tab
daca a aparut o fereastra Java (in spatele ferestrelor
vizibile). |
Clasa Java pentru testarea clasei anterior definite:
public class RunDialogUtilizator01 { public static void main(String[] args) { DialogUtilizator01 d01 = new DialogUtilizator01(); // obiect testat String linie = d01.nextLine("Introduceti numele dumneavoastra"); // metoda testata d01.println("Buna ziua " + linie + ". Bine ai venit in lumea Java!"); } } |
In laborator: 1. Tot in proiectul dialog, creati o noua clasa numita RunDialogUtilizator01 2. Double-click pe noua clasa (deschideti editorul)
si inlocuiti
codul cu cel de sus. 3. Compilati codul si executati
metoda main() a noii clase (right-click pe clasa si
selectare main()). |
Constructorul
Java este un tip special de functie, care
- are acelasi nume cu numele clasei in care este
declarat,
- este utilizat pentru a initializa orice nou obiect de acel tip (stabilind valorile
campurilor/ atributelor obiectului, in momentul crearii lui dinamice),
- nu
returnează nici o valoare,
- are aceleasi niveluri de accesibilitate, reguli de implementare a corpului si reguli de supraincarcare a numelui ca si metodele obisnuite.
In Java nu este neaparat necesara scrierea unor constructori pentru clase, deoarece un constructor implicit este
generat automat de sistemul de executie
(DOAR) pentru o clasa care nu declara explicit constructori. Acest
constructor nu face nimic (nici o initializare, implementarea lui continand un bloc de cod vid: { }). De aceea, orice initializare dorita explicit impune
scrierea unor constructori.
Un exemplu de clasa similara celei anterioare, dar care defineste explicit
un constructor:
import java.util.Scanner; // clasa de biblioteca (package) Java public class DialogUtilizator02 { // clasa definita de utilizator private Scanner sc; // camp Java (atribut) private String prompt; public DialogUtilizator02(String nume) { // constructor (initializator) this.sc = new Scanner(System.in); this.prompt = nume + "> "; } public String nextLine(String text) { // metode Java (operatii) System.out.print(this.prompt + text); return this.sc.nextLine(); } public int nextInt(String text) { System.out.print(this.prompt + text); return this.sc.nextInt(); } public void println(String text) { System.out.println(text); } } |
In documentatia (API-ul) claselor Java pot fi gasite detalii privind clasa Scanner.
In laborator: 1. Tot in proiectul dialog,
creati o
noua clasa numita DialogUtilizator02 2. Intrati in codul clasei (in editor), inlocuiti-i codul cu cel dat,
apoi compilati-l.
3. Creati un obiect nou, numit d02,
pasandu-i constructorului valoarea "test".
4. Executati metoda nextLine()
dandu-i ca parametru "Grupa :
". Ce apare in
Terminal Window? 5. Inspectati valoarea returnata. 6. Executati si metodele nextInt()
si println() si urmariti efectul lor. |
Java suporta supraincarcarea
numelor (name
overloading) pentru metode si constructori.
Astfel, o clasa poate avea orice numar
de metode cu acelasi nume cu conditia ca listele lor de parametri sa fie diferite.
In mod similar, o clasa poate avea orice numar de constructori (acestia avand toti
acelasi nume - identic cu numele clasei) cu conditia ca listele lor de parametri sa fie diferite. De exemplu, codul clasei
anterioare poate fi completat cu constructorul:
public DialogUtilizator02() { // constructor (initializator) this.sc = new Scanner(System.in); this.prompt = "IMPLICIT" + "> "; // echivalent cu: this.prompt = this("IMPLICIT "); } |
In laborator: 1. Intrati in codul clasei DialogUtilizator02 (in editor), adaugati
constructorul, apoi recompilati. 2. Creati un obiect nou folosind noul
constructor. Ce observati? 3. Executati-i metoda println()
dandu-i ca parametru "POO". Ce apare in Terminal Window? 4. Creati un obiect nou folosind
primul constructor, caruia ii pasati "EXPLICIT". 5. Executati-i metoda println()
dandu-i ca parametru "POO". Ce apare in Terminal Window? |
In laborator: 1. Concepeti si editati codul unei metode
noi a clasei DialogUtilizator02, cu semnatura: public void println() care
nu primeste parametru si afiseaza in Terminal Window (folosind System.out.println())
textul: "Nu am primit nici
un parametru". 2. Recompilati clasa si creati un obiect nou folosind noul constructor. 3. Executati noua metoda println(). Ce
apare in Terminal Window? 4. Executati din nou vechea metoda println(), cu parametru "x". Ce
apare in Terminal Window? |
Legătura este o cale
între obiectele care se cunosc (văd)
unul pe altul (îşi pot transmite mesaje – apelurile de metode), pentru aceasta avand referinte unul
către celălalt.
Fie clasele Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Point { private int x; private int y;
public Point(int abscisa, int ordonata) { x = abscisa; y = ordonata; } public void moveTo(int abscisaNoua, int ordonataNoua) { x = abscisaNoua; y = ordonataNoua; } public void moveWith(int deplasareAbsc, int deplasareOrd) { x = x + deplasareAbsc; y = y + deplasareOrd; } public int getX() { return x; } public int getY() { return y; } } |
|
1 2 3 4 5 6 7 8 9 |
public class UtilizarePoint { private static Point punctA; // referinta, legatura catre un obiect Point
public static void main(String[] args) { punctA = new Point(3, 4); // alocare si initializare atribut punctA punctA.moveTo(3, 5); // trimitere mesaj moveTo() catre punctA punctA.moveWith(3, 5); // trimitere mesaj moveWith() catre punctA } } |
Un obiect sau o clasa “vede” un alt obiect daca are o
referinta catre el, si astfel ii
poate apela metodele. Se spune ca exista o legatura intre obiectul care are referinta catre obiectul referit.
De exemplu, clasa UtilizarePoint are o referinta punctA catre un obiect al clasei Point.
Fiecărei familii de legături între obiecte ale aceleiasi clase ii corespunde o relaţie între clasele acelor obiecte.
Asocierea este o relatie care exprimă un cuplaj (o dependenta) redus
între clase (clasele asociate rămânând relativ independente). Clasele Point si UtilizarePoint sunt de exemplu intr-o relatie de asociere (cu navigabilitate) unidirectionala:
Clasa UtilizarePoint are un atribut punctA de tip Point care permite clasei UtilizarePoint sa trimita mesaje unui
obiect (pointA) al clasei Point.
|
private Point punctA; // atribut de tip Point |
Clasa Point in schimb nu are
nici o referinta catre clasa UtilizarePoint care sa ii permita
trimiterea de mesaje (invocari de metode).
Asocierile unidirectionale pot fi considerate relatii de utilizare. Ele se reprezinta prin sageti indreptate pe directia catre care exista referinta (catre
care se pot trimite mesaje). Clasa RunDialogUtilizator01
utilizeaza un obiect al clasei DialogUtilizator01:
Clasa Mesaj01 incapsuleaza un obiect de tip String care reprezinta un mesaj de la
utilizatorul curent (regrupand textul mesajului
cu metodele prin care este controlat accesul la acesta):
public class Mesaj01 { private String text; public Mesaj01(String text) { // constructor cu parametru this.text = text; } public String getText() { // obtinerea valorii campului return this.text; } public String toString() { return ("Mesaj: " + this.text); } public void display() { System.out.println(this.toString()); } public boolean equals(Object obj) { return this.text.equals(((Mesaj01)obj).text); } } |
In cazul clasei Mesaj01, supraincarcarea numelui constructorului
ar insemna crearea unui constructor
suplimentar, de exemplu unul care
nu primeste nici un parametru:
public Mesaj01() { this(""); } // corpul este echivalent cu: { this.text = ""; } |
Pentru a exemplifica relatia de utilizare intre clase va fi creata o clasa Pachet02 care incapsuleaza
un obiect Mesaj02 (regrupand mesajul
si sursa lui cu metodele prin care este controlat accesul la acestea):
public class Pachet02 { private Mesaj02 mesaj; private String sursa; public Pachet02(Mesaj02 mesaj, String sursa) { this.mesaj = mesaj; this.sursa = sursa; } public Mesaj02 getMesaj() { return this.mesaj; } public String getSursa() { return this.sursa; } public String toString() { return ("Pachetul de la " + this.sursa + " contine: " + this.mesaj); } public boolean equals(Object obj) { return (this.mesaj.equals(((Pachet02)obj).mesaj)) && (this.sursa.equals(((Pachet02)obj).sursa)); } } |
In cazul clasei Pachet02, supraincarcarea
numelui constructorului ar insemna crearea
unui constructor suplimentar, de exemplu unul cu semnatura:
public Pachet02(Mesaj02 mesaj) |
Sa presupunem ca dorim sa modificam codul clasei Student care abstractiza un student real, introducand detalii suplimentare referitoare la Student ca persoana, pe langa campul nume.
De exemplu, putem inlocui campul nume de
tip String cu
un camp date de un tip nou, DatePersonale, care va fi o noua clasa ce va contine pe
langa un camp nume de tip String si campurile initiale si prenume de tip String si anNastere de tip int. Codul
noii clasei va fi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public
class DatePersonale { // Campuri ascunse private String nume; private String initiale; private String prenume; private int anNastere; // Constructori public DatePersonale(String n, String i, String p, int an) { nume
= new String(n); // copiere „hard” a obiectelor primite
ca parametri, initiale
= new String(i); // adica se copiaza obiectul camp cu camp,
prenume
= new String(p); // nu doar referintele ca pana acum anNastere
= an; } // Interfata publica si implementarea ascunsa public String getNume() {
return (nume); } public String getPrenume() {
return (prenume); } public int getAnNastere() { return (anNastere); } public String toString() { // forma „String” a campurilor obiectului return (nume + " " + initiale
+ " " + prenume + "
(" + anNastere +
")"); } } |
De asemenea, presupunem ca dorim sa modificam codul
clasei Student regrupand elementele pereche ale campurilor
cursuri si rezultate (care sunt tablouri) in obiecte ale unei clase
noi, SituatieCurs. Vom
inlocui in clasa Student
tablourile cursuri cu elemente de tip String si rezultate cu elemente de tip int, cu un
singur tablou, cursuri, cu elemente de tip SituatieCurs.
Cele doua clase noi
pot fi reprezentate in UML ( - = acces private, + = acces public) astfel:
Codul Java al noii clasei va fi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public
class SituatieCurs { // Campuri ascunse private int nota = 0; // initializare implicita private String denumire; // Constructor public SituatieCurs(String d) { denumire = new String(d); } // copiere „hard”
// se initializeaza doar denumire // Interfata publica si implementarea ascunsa public void notare(int n) { nota = n; } // se adauga
nota public int nota() {
return(nota); } // se returneaza nota public String toString() { //
forma „String” a campurilor if
(nota==0) return ("Disciplina
" + denumire + " nu a
fost notata"); else
return("Rezultat la disciplina " + denumire + ": " + nota); } } |
In cele doua clase, se observa ca se foloseste copierea
“hard” a obiectelor primite ca parametri, adica crearea unor copii ale obiectelor argument, camp cu
camp, nu doar copierea referintelor.
In clasa SituatieCurs nu am utilizat metode de tip getCamp() si setCamp(). Metoda nota()ar fi putut fi denumita getNota() iar metoda notare() ar fi putut fi denumita setNota().
Printre metodele declarate regasim si toString(), cu
scopul de a returna sub forma de String informatiile pe care le incapsuleaza obiectul caruia i se aplica. Nota initiala 0 inseamna notei (notarea poate incepe doar cu 1). De aceea toString() returneaza diferit pentru valori
nule/nenule.
Vom rescrie acum codul Java al clasei Student pentru a incorpora schimbarile anuntate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/** // Campuri ascunse private DatePersonale date; private SituatieCurs[] cursuri; private int numarCursuri = 0; // initializare
implicita // Constructori public Student(String nume, String initiale, String prenume, int
anNastere) { date
= new DatePersonale(nume, initiale, prenume, anNastere); // copiere „hard” cursuri
= new SituatieCurs[10]; //
se initializeaza doar date si cursuri } // Interfata publica
si implementarea ascunsa (include punct intrare
program) public void addCurs(String nume) { // se adauga un
nou curs cursuri[numarCursuri++]
= new SituatieCurs(nume); } public void notare(int numarCurs, int nota) { cursuri[numarCurs].notare(nota); // se adauga
nota cursului specificat }
public String toString()
{ //
forma „String” a campurilor String s = "Studentul " + date + " are urmatoarele
rezultate:\n"; for (int i=0; i<numarCursuri; i++) s = s
+ cursuri[i].toString() +
"\n"; return (s); } public static void main(String[] args) { //
Crearea unui nou Student, initializarea campurilor noului obiect Student st1 = new Student("Xulescu", "Ygrec",
"Z.", 1987); st1.addCurs("CID"); st1.addCurs("MN"); st1.notare(0,
8); // Utilizarea informatiilor privind
Studentul System.out.println(st1.toString()); // afisarea
formei „String” a campurilor } |
Regasim copierea
“hard” a obiectelor si metoda toString(). Tabloul
cursurilor este initial gol, rand pe rand fiind adaugate cursuri noi (si e
incrementat numarul lor).
In UML avem asocierile (clasa Student utilizeaza DatePersonale si SituatieCurs):
Clasa are o metoda main() cu rolul de a
testa lucrul cu obiectele Student. Executia ei conduce la:
Studentul Xulescu A. Ygrec (1987) are
urmatoarele rezultate: Rezultat la disciplina CID: 8 Disciplina MN nu a fost notata |
Sa presupunem ca dorim sa scriem codul unei clase noi numita Grupa care sa abstractizeze o grupa de studenti in cadrul programului care gestioneaza informatii
intr-o universitate, facultate, etc.
Clasa va avea campuri
separate pentru serie, de care tine, si numar, care o identifica (cele doua ar putea forma o alta
clasa, de exemplu InfoGrupa) si un tablou pentru referinte spre studenti.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public
class Grupa { // Campuri ascunse private int numar; private String serie; private Student[] studenti; // Constructori public Grupa(int nr, String sr) { // se initializeaza
doar serie si numar numar
= nr; serie = new String(sr); // copiere „hard”
pentru serie } // Interfata publica
si implementarea ascunsa (include punct intrare
program) public void addStudenti(Student[] st) { // se adauga tabloul
de studenti studenti
= new Student[st.length]; System.arraycopy (st, 0, studenti, 0, st.length); // copiere „hard”
a tabloului } public String toString() { String g = "\nRezultatele grupei
" + numar + serie + ":\n"; for
(int i=0; i<studenti.length;
i++) g = g + studenti[i].toString()
+ "\n"; return (g); } public static void main(String[] args) { // Crearea
unui nou Student, initializarea campurilor noului obiect Student st1 = new Student("Xulescu", "A.",
"Ygrec", 1987); st1.addCurs("CID"); st1.addCurs("MN"); st1.notare(0,
8); //
Crearea unui nou Student, initializarea campurilor noului obiect Student st2 = new Student("Zulescu", "B.",
"Ics", 1988); st2.addCurs("CID"); st2.addCurs("MN"); st2.notare(1,
9); //
Crearea unei noi Grupe, initializarea campurilor noului obiect Grupa g1 = new Grupa(424, "A"); Student[] st = {st1, st2}; g1.addStudenti(st); //
Utilizarea informatiilor privind Grupa System.out.println(g1.toString()); } } |
Asocierile in UML (clasa
Grupa utilizeaza Student):
Pentru copiere
„hard” a tablourilor (element cu element), clasa System
ofera metoda arraycopy(), care copiaza
un subtablou din tabloul sursa src
, de lungime length
,
incepand de la index srcPos
, in tabloul destinatie dest
, la index destPos
.
Semnatura arraycopy() este:
|
Metoda main() testeaza lucrul cu obiectele Grupa si Student. Executia ei are ca efect:
Rezultatele
grupei 424A: Studentul Xulescu A. Ygrec (1987) are
urmatoarele rezultate: Rezultat la disciplina CID: 8
Disciplina MN nu a fost notata Studentul Zulescu B. Ics (1988) are
urmatoarele rezultate: Disciplina CID nu a fost notata Rezultat la disciplina MN: 9 |
In laborator: 1. Se deschide BlueJ. 2. Se creeaza un proiect Java numit
ModelareStudenti.
3. Se creeaza in acest proiect
clasele cu numele DatePersonale, SituatieCurs,
Student si Grupa. 4. Se copiaza in interiorul lor
codurile din sectiunea 3.4. 5. Se testeaza executia metodelor main() din clasele Student si Grupa. |
- modificare metoda notare() (in clasa SituatieCurs)
pentru a nu
permite notarea in afara gamei 1-10
avertizand utilizatorul in cazul unei note in
afara gamei
- adaugare in clasa Student a unei metode nota()
care returneaza
nota pentru indexul primit
cursului ca parametru
- adaugare in clasa Student a unei metode medieCurenta()
care returneaza
media notelor curente
- ...
- bonusuri
pentru propuneri interesante de exercitii in laborator!