Scopul acestui laborator este introducerea studenților în concepte mai avansate privind proiectarea claselor în Java, precum și familiarizarea acestora cu mecanismele de agregare, moștenire și polimorfism, alături de modul în care acestea pot fi utilizate pentru refolosirea și extinderea cu ușurință a codului existent.
Aspectele urmărite sunt:
Aspectele bonus urmărite sunt:
În programarea orientată pe obiecte, agregarea și compunerea sunt două tipuri de relații has-a (un obiect are alt obiect). Aceste relații apar atunci când o clasă conține o referință către alt obiect.
Un obiect conținut poate fi inițializat în mai multe moduri:
class Car { private Engine engine = new Engine(); ... }
class Car { private Engine engine; // Bloc de inițializare - se execută înainte de constructor { engine = new Engine(); } public Car() { ... } ... }
class Car { private Engine engine; Car(Engine engine) { this.engine = engine; } ... }
class Car { private Engine engine; public Engine initializeEngine() { if (engine == null) { engine = new Engine(); } return engine; } }
Agregarea reprezintă o relație slabă între obiecte. Obiectul container primește dependența din exterior, dar nu îi controlează ciclul de viață. Obiectul conținut poate exista independent.
Exemplu real: Un departament are angajați, dar angajații pot exista și fără acel departament.
public class Department { private String name; private Employee[] employees; public Department(String name, Employee[] employees) { this.name = name; this.employees = employees; // dependența vine din exterior → agregare } }
Compunerea reprezintă o relație puternică între obiecte. Obiectul container creează și gestionează obiectele interne. Dacă obiectul container este distrus, obiectele conținute dispar și ele.
Exemplu real: O casă are camere. Camerele nu au sens să existe fără casă.
public class House { private Room[] rooms; public House(int numberOfRooms) { rooms = new Room[numberOfRooms]; // obiectele sunt create în interior → compunere for (int i = 0; i < numberOfRooms; i++) { rooms[i] = new Room("Camera " + (i + 1)); } } }
| Caracteristică | Agregare | Compunere |
|---|---|---|
| Tip relație | Slabă | Puternică |
| Ciclul de viață | Independent | Dependent |
| Creează obiectele? | Nu | Da |
| Exemplu | Departament–Angajat | Casă–Cameră |
new în interiorul clasei, de obicei este vorba de compunere.class Engine { int power; Engine(int power) { this.power = power; } } class Car { private Engine engine; public Car(Engine engine) { // Se clonează motorul → compunere (nu agregare) this.engine = new Engine(engine.power); } }
Moștenirea, numită și derivare, este un mecanism care permite refolosirea și extinderea codului unei clase existente.
Clasa copil poate prelua funcționalitatea clasei părinte și poate adăuga sau modifica comportamente, creând versiuni specializate ale claselor de bază.
Drept urmare, putem spune că prin moștenire clasa copil preia toții membrii (metodele și câmpurile) clasei părinte pentru a îi extinde sau folosi direct.
has-a, la moștenire vom folosi relația is-a.
Exemplu: Dacia is a Car → clasa Dacia moștenește toate proprietățile și metodele lui Car.
Avantajele principale ale moștenirii sunt:
În Java, folosim cuvântul cheie extends:
class Cat extends Mammal { ... }
Prin folosirea moștenirii putem crea lanțuri de moștenire. De exemplu să presupunem că avem clasa Cat care moștenește Mammal care moștenește la rândul ei clasa Animal:
Animal (clasă de bază) ↓ Mammal (clasă copil pentru Animal) ↓ Cat (clasă copil pentru Mammal)
Într-un proiect Java, aceste moșteniri multiple ar arăta astfel:
class Animal { float weight = 10.2f; void eat() { System.out.println("The animal is eating"); } void sleep() { System.out.println("The animal is sleeping"); } }
class Mammal extends Animal { int heartRate = 90; void breathe() { System.out.println("The mammal is breathing"); } }
public class Cat extends Mammal { boolean longHair = true; void purr() { System.out.println("The cat is purring"); } }
class Main { public static void Main(String args[]) { // Se pot accesa toate metodele și câmpurile moștenite de către Cat Cat cat = new Cat(); cat.purr(); cat.breathe(); cat.eat(); cat.sleep(); System.out.println("Cat weight: " + cat.weight); // Se pot accesa toate metodele și câmpurile moștenite de către Mammal Mammal mammal = new Mammal(); mammal.breathe(); mammal.eat(); mammal.sleep(); mammal.weight = 14.5f; System.out.println("Mammal weight: " + mammal.weight); // Se pot accesa doar metodele și câmpurile proprii ale clasei Animal pentru că se oprește lanțul de moștenire Animal animal = new Animal(); animal.eat(); animal.sleep(); animal.weight = 45.3f; System.out.println("Animal weight: " + animal.weight); } }
Mammal moștenește weight și eat() din Animal.Cat moștenește tot din Mammal și Animal la care se adaugă metoda proprie purr().
Reamintim specificatorii de acces prezentați în laboratorul trecut în contextul moștenirii membrilor:
| Default | Private | Protected | Public | |
|---|---|---|---|---|
| Aceeași clasă | Da | Da | Da | Da |
| Același pachet, subclasă | Da | Nu | Da | Da |
| Același pachet, non-subclasă | Da | Nu | Da | Da |
| Pachet diferit, subclasă | Nu | Nu | Da | Da |
| Pachet diferit, non-subclasă | Nu | Nu | Nu | Da |
Constructorii nu se moștenesc, însă inițializarea obiectelor urmează un lanț logic: mai întâi se construiesc membrii clasei părinte, apoi cei ai subclasei, până la clasa curentă.
Pentru exemplul de mai sus inițializarea s-ar întâmpla astfel:
Animal ↓ Mammal ↓ Cat
class Mammal extends Animal { ... public Mammal() { // Nu avem erori pentru că "Animal" și membrii lui au fost inițializați deja System.out.println("Animal inherited field value is: " + weight); } ... }
Vom descrie acest mecanism în cadrul secțiunii următoare despre super.
Keyword-ul super este folosit în constructorul unei subclase pentru a inițializa partea moștenită a obiectului și pentru a accesa membri (metode sau variabile) ai clasei părinte.
class Parent { int value; Parent(int x) { value = x; System.out.println("Parent constructor"); } } class Child extends Parent { Child(int x) { super(x); // apelul constructorului părinte trebuie să fie prima linie System.out.println("Child constructor, value = " + value); } }
Pentru a asigura ordinea inițializării pe lanțul de moștenire, instrucțiunea super() sau super(…) trebuie să fie prima instrucțiune din fiecare constructor definit în clasa copil.
super(…) către părinte → părintele apelează super(…) către bunic).super() este injectată automat de către compilator la începutul corpului din constructori.super(…) nu se va mai injecta super()super(…): class Parent { Parent(int x) { System.out.println("Parent constructor: " + x); } } class Child extends Parent { Child() { // Apel explicit, deoarece "super()" ar apela "Parent()" care nu există super(10); System.out.println("Child constructor"); } }
Keyword-ul super poate fi folosit și pentru accesul membrilor din părinte. Acest lucru este util când vrem să accesăm membrii ascunși sau suprascriși.
class Parent { int value = 100; void show() { System.out.println("Parent value: " + value); } } class Child extends Parent { int value = 200; // shadowing void show() { System.out.println("Child value: " + value); System.out.println("Parent value via super: " + super.value); // accesăm membrul ascuns } }
super este similar cu this, dar this face referire la instanța curentă, iar super la instanța părinte.super nu poate fi folosit în metode statice pentru că memoria statică este inițializată înaintea instanțelor.super() sau super(…) garantează că subclasa nu folosește membri ai părintelui înainte ca aceștia să fie inițializați.
În Java, o clasă poate fi declarată folosind cuvântul cheie final, ceea ce înseamnă că nu mai poate fi moștenită. Se folosește atunci când vrem să blocăm moștenirea pentru a proteja implementarea sau pentru motive de securitate/perfomanță (optimizări făcute de JVM).
public final class MathUtils { public static int square(int x) { return x * x; } }
Încercarea de a moșteni o clasă final produce eroare de compilare:
class MyMath extends MathUtils { } // Eroare: "cannot inherit from final class"
java.lang.Math este final).
| Caracteristică | Agregare/Compunere | Moștenire |
|---|---|---|
| Relație | has-a | is-a |
| Expunerea metodei | doar obiectul conținut | toate metodele părintelui |
| Control asupra obiectului | containerul folosește obiectul extern | copilul extinde comportamentele părintelui |
| Exemplu | Car has an Engine | Dog is an Animal |
În general, întrebarea has-a sau is-a este suficientă pentru a determina tipul de relație, însă există obiecte pentru care relația nu poate fi stabilită ușor folosind această întrebare.
De exemplu, obiectele Button și Toolbar:
Toolbar poate conține mai multe Button-uri → has-a (agregare)Toolbar e un buton specializat → is-a (moștenire)Pentru aceste situații este bine să folosim următoarea distincție:
Este mai bine să includem obiecte existente în clase și să le delegăm funcționalități, în loc să creem subclase pentru a le modifica comportamentul. Moștenirea ar trebui folosită doar când clasa noastră chiar reprezintă un tip al obiectului părinte, deoarece poate încălca încapsularea și limitează reutilizarea codului.
În Java, upcasting și downcasting sunt operații care permit conversia între tipuri în ierarhia de moștenire a claselor.
Upcasting-ul este conversia unui obiect de tip derivat într-un obiect de tipul clasei de bază și are următoarele proprietăți:
Pentru ierarhia:
public class Animal { public void makeSound() { System.out.println("Animal makes a sound!"); } public void eat() { System.out.println("Animal eats!"); } }
public class Dog extends Animal { public void wagTail() { System.out.println("Dog moves tail!"); } }
Avem exemplul de upcasting:
public class Main { public static void main(String[] args) { Dog myDog = new Dog(); Animal animal = myDog; // Upcasting automat Animal anotherAnimal = new Dog(); // Upcasting automat animal.makeSound(); // Funcționează animal.eat(); // Funcționează // EROARE de compilare - metoda "wagTail()" nu există în referința de tip "Animal" // animal.wagTail(); } }
Dog myDog = new Dog(); se alocă memorie pentru un obiect de tip Dog (conform instanței).Animal animal = myDog; se face upcast la referința de tip Animal.animal.wagTail(); compilatorul nu știe încă ce memorie va fi alocată, deci nu se va uita la instanță, ci la referință.Animal, metoda wagTail() nu este găsită și vom avea o eroare.Dog compilatorul nu poate vedea asta.
Upcasting-ul permite tratarea diferitelor subclase într-un mod uniform.
De exemplu dacă avem clasa Shelter unde vrem să apelăm metoda eat() a fiecărui animal definit ar trebui să avem următoarea structură:
public class Shelter { public void feedAnimal(Dog dog) { dog.eat(); } public void feedAnimal(Cat cat) { cat.eat(); } public void feedAnimal(Rabbit rabbit) { rabbit.eat(); } }
public class Main { public static void main(String[] args) { Shelter shelter = new Shelter(); Dog dog = new Dog(); Cat cat = new Cat(); Rabbit rabbit = new Rabbit(); // Overloading shelter.feedAnimal(dog); shelter.feedAnimal(cat); shelter.feedAnimal(rabbit); } }
Prin folosirea mecanismului de overloading putem apela metoda feedAnimal() pentru fiecare animal, doar că această abordare nu este scalabilă.
Putem folosi în schimb mecanismul de upcasting pentru a scurta semnificativ codul:
public class Shelter { public void feedAnimal(Animal animal) { animal.eat(); } }
public class Main { public static void main(String[] args) { Shelter shelter = new Shelter(); Dog dog = new Dog(); Cat cat = new Cat(); Rabbit rabbit = new Rabbit(); // Upcast la Animal shelter.feedAnimal(dog); shelter.feedAnimal(cat); shelter.feedAnimal(rabbit); } }
Prin upcasting, convertim fiecare tip copil în tipul părinte Animal la pasarea în metodă, deoarece orice Dog/Cat/Rabbit este și un Animal.
Downcasting-ul este mecanismul prin care se face conversia explicită de la clasa de bază la o clasă derivată și are următoarele proprietăți:
instanceof pentru a verifica tipul înainte.Pentru ierarhia de clase de mai sus avem exemplul:
Animal animal = new Dog(); // Upcasting if (animal instanceof Dog) { ((Dog) animal).wagTail(); // Downcasting explicit }
animal pentru a forța compilatorul să vadă animal ca fiind o referință de tip Dog.ClassCastException.
Downcasting-ul permite accesarea funcționalităților specifice subclasei atunci când avem o referință de tipul clasei părinte (upcast). Este necesar atunci când vrem să folosim metode sau câmpuri care nu sunt definite în clasa de bază, dar există în subclasa reală.
Fără downcast, nu am fi putu accesa metoda wagTail() din exemplul de mai sus.
Folosirea instanceof este considerată bad practice, deoarece:
instanceof sau getClass(), deoarece acest stil de programare este anti-OOP și veți fi depunctați semnificativ.
Pentru a rezolva această problemă putem folosi mecanismul de suprascriere prezentat în continuare.
Polimorfismul reprezintă abilitatea unui obiect să se comporte ca un alt obiect din ierarhia de moștenire. Este strâns legat de suprascrierea metodelor și permite tratamentul uniform al obiectelor de tipuri diferite printr-o interfață comună sau o clasă de bază.
Există două tipuri principale:
Suprascrierea se referă la redefinirea unei metode moștenite pentru a schimba comportamentul acesteia în subclasa curentă.
Reguli de bază pentru suprascriere:
class Animal { public void makeSound() { System.out.println("Animal makes sound"); } public void eat() { System.out.println("Animal eating"); } public final void die() { System.out.println("Dying!"); } } class Dog extends Animal { // Suprascriem metoda din părinte @Override public void makeSound() { System.out.println("Bark!"); } public void wagTail() { System.out.println("Dog wags tail"); } // Nu suprascrie nici o metodă }
@Override este opțional, dar recomandat pentru claritate și verificarea erorilor de compilare.
Este important să înțelegem că suprascrierea nu ne garantează că metoda aleasă va fi întotdeauna cea din copil. Deoarece JVM se uită la tipul instanței și nu la referință, putem avea mai multe comportamente definite:
public class Main { public static void main(String[] args) { Animal a = new Animal(); // tipul instanței lui "a" este "Animal" a.makeSound(); // se va apela "makeSound()" din "Animal" Dog d = new Dog(); // tipul instanței lui "d" este "Dog" d.makeSound(); // se va apela "makeSound()" din "Dog" Animal anotherAnimal = new Dog(); // tipul instanței lui "anotherAnimal" este "Dog" anotherAnimal.makeSound(); // se va apela "makeSound()" din "Dog" a = new Dog(); // am schimbat instanța în "Dog" a.makeSound(); // acum se va apela "makeSound()" din "Dog" } }
În Java, o metodă declarată folosind cuvântul cheie final nu poate fi suprascrisă în nicio subclasă. Aceasta este o modalitate de a proteja comportamentul unei metode astfel încât să nu fie modificat prin moștenire.
class Animal { public final void die() { System.out.println("Animal died."); } } class Dog extends Animal { // Eroare de compilare: "cannot override final method" @Override public void die() { System.out.println("Dog died."); } }
final?
private sunt implicit final din perspectiva moștenirii. Acestea nu pot fi suprascrise deoarece nu sunt vizibile în subclase.
Reamintim că supraîncărcarea este mecanismul în care putem defini mai multe metode cu același nume în aceeași clasă, dar cu parametri diferiți (număr sau tip).
Pentru mai multe detalii despre supraîncărcare reluați laboratorul 2 sau consultați imaginea de mai jos.
În afară de primitive, totul în Java este un obiect, iar acest lucru este ilustrat prin faptul că orice clasă moștenește clasa Object.
Deci ierarhia de moștenire pentru animale din exemplul anterior nu era chiar completă, dar o putem corecta astfel:
Object (clasa moștenită de toate obiectele) ↓ Animal (clasă de bază definită de programator) ↓ Mammal (clasă copil pentru Animal) ↓ Cat (clasă copil pentru Mammal)
extends Object atunci când scriem o clasă, deoarece acest lucru se va întâmpla automat dacă clasa noastră nu moștenește deja o altă clasă, astfel se respectă principiul moștenirii unei singure clase.
Motivul pentru care există clasa Object este să ofere un set minim și comun de comportamente tuturor obiectelor, indiferent ce reprezintă ele. Astfel, orice instanță în Java are automat metode de bază precum:
toString() – descrierea obiectului ca textequals(Object o) – comparație logicăhashCode() – folosit în colecții (HashMap, HashSet etc.)getClass() – află tipul obiectului la runtimeclone() – copiere superficialăfinalize() – cleanup (deprecated)
Object căpătăm și un polimorfism universal care va fi util atunci când învățăm colecții în laboratoarele următoare.toString() și equals(Object o).
Cu ajutorul metodei toString(), care este deja implementată în mod predefinit pentru fiecare clasă în Java, putem obține o reprezentare a unui obiect ca String.
În cazurile claselor implementate de utilizator, este de recomandat să suprascriem metoda toString() pentru a afișa detaliile de interes ale clasei.
Un exemplu de implementare a metodei toString():
public class Student { private String name; private int averageGrade; public Student(String name, int averageGrade) { this.name = name; this.averageGrade = averageGrade; } public String toString() { return "Nume student: " + name + "\nMedia studentului: " + averageGrade; } }
Folosirea metodei toString():
Student st1 = new Student("Ilie Popescu", 5); // Nu trebuie să specificăm "st1.toString()", compilatorul va adăuga automat apelul de metodă System.out.println(st1);
st1.toString(), doar că nu este recomandat din motive de lizibilitate.toString(), se va printa adresa obiectului în memorie (pentru clasa Student am avea o adresă de tipul Student@412).
toString(): click dreapta oriunde în cod → Generate… → toString() → alegeți câmpurile care doriți să fie printate.
Metoda equals(Object o) este folosită pentru a compara corect două obiecte. Dacă am folosi operatorul == pentru verificarea egalității am ajunge să verificăm de fapt egalitatea referințelor (adică a adreselor de memorie). Prin metoda equals putem verifica dacă două obiecte diferite au aceeași stare internă conform unor criterii specifice.
Una din problemele cele mai des întâlnite este suprascrierea corectă a metodei equals. La fel ca metoda toString(), această metodă are un comportament default în care se verifică doar dacă referințele sunt egale.
class Person { String name; } class Main { public static void main(String[] args) { Person p1 = new Person(); Person p2 = new Person(); System.out.println(p1.equals(p2)); // false, pentru că p1 și p2 sunt obiecte diferite } }
Mai jos putem vedea un exemplu de suprascriere incorectă a acestei metode.
public class Car { private String name; private int horsePower; private int year; public Car(String name, int horsePower, int year) { this.name = name; this.horsePower = horsePower; this.year = year; } public boolean equals(Car c) { System.out.println("Car"); return true; } public boolean equals(Object o) { System.out.println("Object"); return false; } }
public class Dacia extends Car { ... }
Prima metodă este o supraîncărcare a metodei equals iar a doua metodă este suprascrierea metodei equals.
Car a = new Car(); Dacia b = new Dacia(); int c = 10; a.equals(a); // afișează Car a.equals(b); // afișează Car deoarece se face upcasting de la Dacia la Car a.equals(c); // afișează Object deoarece se face upcasting de la Int la Object
Problema care se poate observa este că putem pasa ca argumente metodei equals si tipuri de date diferite de Car, lucru ce ar putea arunca excepții de cast sau când vrem să accesăm anumite proprietăți din instanță. Mai jos este modul corect de a suprascrie metoda equals.
public class Car { private String name; private int horsePower; private int year; public Car(String name, int horsePower, int year) { this.name = name; this.horsePower = horsePower; this.year = year; } @Override public boolean equals(Object o) { // Verificăm dacă obiectul o este de tipul "Car" if (!(o instanceof Car car)) return false; // Verificăm câmpurile claselor să fie identice return horsePower == car.horsePower && year == car.year && Objects.equals(name, car.name); } }
De reținut că folosirea instanceof nu este recomandată, însă în acest caz este singurul mod prin care ne putem asigura ca instanța de obiect trimisă metodei este de tip Car. Se poate folosi și metoda getClass() însă nici aceasta nu este recomandată din aceleași motive.
==.equals(Object o): click dreapta oriunde în cod → Generate… → equals() and hashSet() → alegeți getClass sau instanceof (nu este relevant) → alegeți câmpurile folosite pentru egalitate.
Keyword-ul super poate fi folosit de Java în clasele copil pentru a accesa metoda originală din clasa părinte atunci când metoda este suprascrisă.
class Parent { void sayHello() { System.out.println("Hello from Parent"); } } class Child extends Parent { @Override void sayHello() { super.sayHello(); // apelăm metoda din părinte System.out.println("Hello from Child"); } }
Când rulează o aplicație Java, memoria este împărțită în mai multe zone principale:
| Zonă de memorie | Ce conține | Importanță |
|---|---|---|
| Heap | Obiectele create la runtime | unde trăiesc obiectele |
| Stack | Variabile locale + referințe | gestionat per thread |
| Metaspace | Informații despre clase + metode + tabele pentru dynamic dispatch (vtable) + variabile și metode statice | suportă OOP și polimorfismul |
Când o clasă este încărcată de JVM, metainformațiile ei sunt puse în Metaspace:
VTable este o tabelă internă introdusă de JVM pentru a face posibil polimorfismul dinamic.
class Animal { void makeSound() { System.out.println("generic"); } static void info() { System.out.println("Animal class"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("woof"); } static void info() { System.out.println("Dog class"); } }
Când se încarcă clasele, JVM construiește două VTables în Metaspace, doar pentru metodele non-statice:
Animal vtable: - makeSound() -> Animal.makeSound Dog vtable: - makeSound() -> Dog.makeSound (override)
| Concept | Ce face | Legătura cu memoria |
|---|---|---|
| Upcasting (safe) | tratezi Dog ca Animal | DOAR schimbă tipul referinței, obiectul rămâne în Heap, legătura cu VTable se păstrează |
| Downcasting (risky) | tratezi Animal ca Dog | JVM verifică în Metaspace dacă tipul real are VTable pentru Dog |
Animal a = new Dog(); // upcast a.makeSound(); // JVM folosește vtable din Metaspace → Dog.makeSound Animal.info(); // apelează Animal.info() din Metaspace Dog.info(); // apelează Dog.info() din Metaspace
| Caracteristică | Metode de instanță | Metode static |
|---|---|---|
| Rezolvare | runtime (dinamic) | compile-time (static binding) |
| Instrucțiune JVM | invokevirtual | invokestatic |
| Depind de instanță | Da | Nu |
| Apar în VTable | Da | Nu |
| Pot fi suprascrise | Da | Nu, doar ascunse (method hiding) |
Metodele static aparțin clasei, nu obiectelor, deci JVM nu are motiv să le caute într-un VTable la runtime. Ele sunt apelate direct prin numele clasei, nu prin instanță.
invokevirtual și invokestatic sunt instrucțiuni din bytecode-ul de Java folosite pentru a rezolva și apela metodele corecte.
Apelurile către metode static sunt rezolvate folosind Constant Pool, o zonă de memorie care se află tot în Metaspace și care este asociată fiecărei clase care conține referințe simbolice către metode, câmpuri și clase.
static în Metaspace, unde este stocată logica clasei.invokestatic citește adresa metodei direct din Constant Pool, fără polimorfism, fără VTable.
invokestatic este stocată în Constant Pool.
Relații între obiecte:
Upcasting:
Downcasting:
Suprascrierea:
Supraîncărcarea:
Cuvântul cheie super:
this se referă la instanța clasei curente
Veți proiecta o clasă Form care va avea câmpul privat color (String).
Clasa va avea, de asemenea:
Din clasa Form derivați clasele Square, Triangle, Circle:
Clasele vor avea:
Adăugați o metodă equals(Object o) în clasa Triangle.
Justificați criteriul de echivalență ales.
Hint: Puteți genera automat metoda, cu ajutorul IDE. Selectați câmpurile considerate și analizați în ce fel va fi suprascrisă metoda equals.
Creați un vector de obiecte Form și populați-l cu obiecte de tip Triangle, Circle și Square (upcasting).
Parcurgeți acest vector și apelați metoda toString() pentru elementele sale. Ce observați?
Adăugați:
Implementarea metodelor constă în afișarea bazei și înălțimii, razei, respectiv laturii.
Parcurgeți vectorul de la exercițiul anterior și, folosind downcasting la clasa corespunzătoare, apelați metodele specifice fiecărei clase:
Pentru a stabili tipul obiectului curent folosiți operatorul instanceof.
Afișați dimensiunile formelor din vectorul creat fără a folosi operatorul instanceof.
Afișați perimetrul fiecărui obiect din vectorul creat utilizând exclusiv funcția printPerimeter, modificând doar ce se afla in corpul funcției, lasând antetul identic.