This is an old revision of the document!


Laboratorul 12: Programare Avansată Java

Obiective

Serializare și Deserializare

Ce este serializarea?

Serializarea reprezintă procesul prin care un obiect Java este transformat într-o reprezentare transferabilă, de regulă o secvență de bytes sau un format text, astfel încât să poată fi:

  • salvat pe disc
  • transmis prin rețea
  • stocat temporar (cache)
  • restaurat ulterior în memorie

Operația inversă se numește deserializare și presupune refacerea obiectului original din această reprezentare.

De ce avem nevoie de serializare?

Serializarea este folosită în special pentru:

  • salvarea stării unei aplicații
  • persistarea temporară a obiectelor
  • transfer de date între procese sau servicii
  • sisteme de cache distribuit
  • mecanisme de rollback / snapshot

Serializarea nu înlocuiește o bază de date. Este un mecanism de transport sau stocare temporară, nu unul de persistență relațională.

Serializarea nativă Java

Interfața Serializable

Pentru ca un obiect să poată fi serializat de JVM folosind mecanismul nativ, clasa sa trebuie să implementeze interfața: java.io.Serializable.

public class User implements Serializable {
    private String name;
    private int age;
}

Această interfață:

  • este un marker interface (deci nu definește metode),
  • este verificată de JVM la runtime,
  • permite folosirea claselor ObjectOutputStream și ObjectInputStream.

Dacă un obiect nu este serializabil, JVM aruncă NotSerializableException.

Exemplu complet: serializare și deserializare

1. Clasa model

import java.io.Serializable;
 
public class Person implements Serializable {
    private String name;
    private int age;
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

2. Serializarea unui obiect

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
 
public class SerializeDemo {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Ana", 30);
 
        ObjectOutputStream out =
            new ObjectOutputStream(new FileOutputStream("person.ser"));
 
        out.writeObject(p);
        out.close();
 
        System.out.println("Object serialized.");
    }
}

3. Deserializarea unui obiect

import java.io.FileInputStream;
import java.io.ObjectInputStream;
 
public class DeserializeDemo {
    public static void main(String[] args) throws Exception {
        ObjectInputStream in =
            new ObjectInputStream(new FileInputStream("person.ser"));
 
        Person p = (Person) in.readObject();
        in.close();
 
        System.out.println("Deserialized: " + p);
    }
}

Output

Output

Object serialized.
Deserialized: Ana (30)

Câmpuri transient

Nu toate câmpurile unui obiect ar trebui serializate. Exemple tipice:

  • parole
  • token-uri
  • conexiuni
  • cache-uri

Pentru a exclude un câmp de la serializare folosim cuvântul cheie transient.

public class Account implements Serializable {
    private String username;
    private transient String password;
}

Deoarece am declarat password ca fiind transient, acesta nu este salvat, iar după deserializare devine null.

Versionarea serializării

Câmpul serialVersionUID identifică versiunea unei clase serializate.

private static final long serialVersionUID = 1L;

Acest identificator este folosit de JVM pentru a verifica dacă o clasă este compatibilă cu obiectul serializat, astfel putem garanta compatibilitatea operațiilor.

Dacă nu este declarat explicit:

  • JVM generează unul automat.
  • orice mică modificare va genera un nou UID.

Serializare modernă: Jackson (JSON)

Ce este Jackson?

Jackson este o bibliotecă externă foarte populară care permite:

  • serializarea obiectelor Java în JSON,
  • deserializarea JSON-ului în obiecte Java.

Spre deosebire de serializarea nativă:

  • nu folosește format binar,
  • nu necesită Serializable,
  • este independentă de JVM.

De ce este Jackson preferat în aplicațiile moderne?

De ce este Jackson preferat în aplicațiile moderne?

  • JSON este lizibil și ușor de debugat,
  • este interoperabil cu alte limbaje,
  • permite control fin prin adnotări,
  • este mult mai sigur decât serializarea nativă.

Este standardul de facto pentru:

  • API-uri REST,
  • microservicii,
  • testare și integrare.

Exemplu cu Jackson

1. Dependința Jackson

Într-un proiect real (Maven), Jackson se adaugă astfel:

pom.xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.1</version>
</dependency>

Multe proiecte au deja această dependență când se folosește Spring Boot pentru crearea unui API.

2. Clasa model

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
 
public class User {
 
    private String name;
    private int age;
 
    @JsonIgnore
    private String password;
 
    public User() {
        // constructor fără parametri NECESAR pentru deserializare
    }
 
    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
 
    public String getName() {
        return name;
    }
 
    public int getAge() {
        return age;
    }
 
    @JsonProperty("years")
    public void setAge(int age) {
        this.age = age;
    }
 
    public String getPassword() {
        return password;
    }
 
    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

  • constructorul fără parametri este obligatoriu pentru Jackson
  • @JsonIgnore exclude câmpul din JSON
  • @JsonProperty permite maparea numelor diferite din JSON

3. Serializare: obiect → JSON

import com.fasterxml.jackson.databind.ObjectMapper;
 
public class JacksonSerializeDemo {
 
    public static void main(String[] args) throws Exception {
 
        ObjectMapper mapper = new ObjectMapper();
 
        User user = new User("Ana", 30, "secret123");
 
        String json = mapper.writeValueAsString(user);
 
        System.out.println("JSON rezultat:");
        System.out.println(json);
    }
}

output

output

{"name":"Ana","age":30}

Câmpul password nu apare deoarece are adnotarea @JsonIgnore.

4. Deserializare: JSON → obiect

import com.fasterxml.jackson.databind.ObjectMapper;
 
public class JacksonDeserializeDemo {
 
    public static void main(String[] args) throws Exception {
 
        ObjectMapper mapper = new ObjectMapper();
 
        String json = """
        {
          "name": "Ion",
          "years": 25,
          "password": "shouldBeIgnored"
        }
        """;
 
        User user = mapper.readValue(json, User.class);
 
        System.out.println("Obiect deserializat:");
        System.out.println(user);
    }
}

output

output

User{name='Ion', age=25}

  • yearsage datorită @JsonProperty
  • password este ignorat
  • Jackson a folosit constructorul fără parametri + setter-ele

5. Serializare în fișier și citire din fișier

Scriere în fișier

mapper.writeValue(new File("user.json"), user);

Citire din fișier

User user = mapper.readValue(new File("user.json"), User.class);

Adnotări în Java

Ce sunt adnotările?

Adnotările sunt metadate atașate elementelor din codul Java (clase, metode, câmpuri, parametri).

Ele nu modifică direct logica programului, ci oferă informații suplimentare care pot fi interpretate de:

  • compilator
  • JVM
  • framework-uri
  • tool-uri externe (JUnit, Jackson, Spring etc.)

Cu alte cuvinte, adnotările spun ce este codul, nu ce face.

De ce folosim adnotări?

Adnotările au apărut pentru a elimina:

  • configurații externe (XML)
  • cod repetitiv (boilerplate)
  • convenții fragile bazate pe nume

Astăzi, ele sunt fundamentale în:

  • serializare/deserializare (Jackson)
  • testare (JUnit)
  • dependency injection (Spring)
  • validare
  • ORM (Hibernate)

Sintaxa unei adnotări

@AnnotationName
public class Example { }

Adnotările pot avea parametri:

@JsonProperty("user_name")
private String name;

sau mai mulți:

@MyAnnotation(value = "test", enabled = true)

Unde pot fi aplicate adnotările?

Prin @Target putem controla unde este permisă adnotarea:

  • TYPE – clase, interfețe
  • FIELD – câmpuri
  • METHOD – metode
  • PARAMETER – parametri
  • CONSTRUCTOR
  • LOCAL_VARIABLE

Exemplu:

@Target(ElementType.FIELD)

Când sunt disponibile adnotările? (Retention)

@Retention definește cât timp există adnotarea:

@Retention(RetentionPolicy.RUNTIME)

Tipuri:

  • SOURCE – doar la compilare (ex: @Override)
  • CLASS – în bytecode, dar nu la runtime
  • RUNTIME – disponibilă prin reflection (cea mai importantă)

Jackson, JUnit și Spring folosesc RUNTIME.

Adnotări built-in importante

@Override

@Override
public String toString() { ... }
  • verifică dacă metoda chiar suprascrie una din părinte
  • previne erori de semnătură

@Deprecated

@Deprecated
public void oldMethod() { }
  • marchează cod care nu mai trebuie folosit
  • IDE-ul afișează warning

@SuppressWarnings

@SuppressWarnings("unchecked")
  • dezactivează warning-uri ale compilatorului
  • trebuie folosit cu atenție

Adnotări și Jackson

Jackson folosește adnotări pentru a controla serializarea:

@JsonIgnore
private String password;
@JsonProperty("years")
private int age;

Aceste adnotări:

  • sunt citite prin reflection
  • influențează comportamentul ObjectMapper

Crearea unei adnotări custom

1. Definirea adnotării

import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Important {
    String value() default "";
}

  • adnotările folosesc @interface
  • metodele NU au corp
  • pot avea valori default

2. Utilizarea adnotării

public class Person {
 
    @Important("primary identifier")
    private String name;
 
    private int age;
}

Citirea adnotărilor cu Reflection

import java.lang.reflect.Field;
 
public class AnnotationDemo {
 
    public static void main(String[] args) throws Exception {
 
        Class<Person> clazz = Person.class;
 
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Important.class)) {
                Important imp = field.getAnnotation(Important.class);
                System.out.println(field.getName() + " -> " + imp.value());
            }
        }
    }
}

output

output

name -> primary identifier

[Optional] Când sunt procesate adnotările?

Cele trei momente de procesare

Java definește trei momente distincte în care adnotările pot fi citite sau folosite:

1. La compilare (compile-time)

Adnotările cu:

@Retention(RetentionPolicy.SOURCE)

sunt:

  • disponibile doar în timpul compilării
  • eliminate complet din bytecode
  • folosite de compilator sau tool-uri

Exemplu:

@Override

Rol:

  • verificare de corectitudine
  • warning-uri / erori de compilare

Nu există acces la aceste adnotări la runtime.

2. După compilare, înainte de rulare (build-time / processing)

Unele adnotări sunt procesate prin Annotation Processors (APT – Annotation Processing Tool).

Acestea:

  • rulează după compilare
  • pot genera cod
  • pot valida structura codului

Exemple reale:

  • Lombok (@Getter, @Setter)
  • MapStruct
  • AutoValue

  • Conceptual există un pas de preprocesare, dar nu la nivelul JVM, ci la nivelul build-ului.
  • Acesta NU este un preprocesor ca în C (#define), ci un mecanism controlat de compilator.

3. La runtime (execuție)

Adnotările cu:

@Retention(RetentionPolicy.RUNTIME)

sunt:

  • păstrate în bytecode
  • disponibile prin Reflection
  • interpretate de framework-uri

Exemple:

  • Jackson (@JsonProperty)
  • JUnit (@Test)
  • Spring (@Autowired)

Acesta este cel mai important caz pentru Java modern.

Reflection în Java

Ce este Reflection?

Reflection este mecanismul prin care un program Java poate:

  • inspecta structura claselor la runtime
  • accesa câmpuri, metode, constructori
  • crea obiecte dinamic
  • invoca metode fără a le cunoaște la compilare

Cu alte cuvinte, Reflection permite unui program să se auto-analizeze și să se auto-manipuleze în timpul execuției.

De ce există Reflection?

Reflection a fost introdus pentru a permite:

  • framework-uri generice (Spring, Hibernate, Jackson, JUnit)
  • extensibilitate fără modificarea codului
  • comportament configurabil prin metadate (adnotări)

Fără Reflection:

  • multe framework-uri moderne nu ar fi posibile
  • codul ar fi rigid și greu de extins

Reflection ≠ programare normală

Reflection nu înlocuiește programarea clasică. Este:

  • mai lentă
  • mai greu de citit
  • mai periculoasă

De aceea este folosită:

  • rar
  • controlat
  • în infrastructură, nu în logica de business

Obiectul Class – punctul de intrare

Orice operație de Reflection pornește de la un obiect de tip Class<?>.

Există trei modalități principale de a obține un Class:

Class<String> c1 = String.class;
Class<?> c2 = "hello".getClass();
Class<?> c3 = Class.forName("java.lang.String");

Toate cele trei referințe indică aceeași clasă la runtime.

Inspectarea structurii unei clase

Presupunem următoarea clasă:

public class Person {
    private String name;
    private int age;
 
    public Person(String name, int age) { }
 
    public String getName() { return name; }
}

Câmpuri

Field[] fields = Person.class.getDeclaredFields();
 
for (Field f : fields) {
    System.out.println(f.getName());
}

output

output

name
age

Metode

Method[] methods = Person.class.getDeclaredMethods();
 
for (Method m : methods) {
    System.out.println(m.getName());
}

Constructori

Constructor<?>[] constructors = Person.class.getDeclaredConstructors();
 
for (Constructor<?> c : constructors) {
    System.out.println(c);
}

Accesarea membrilor private

Reflection permite accesarea membrilor private, ocolind encapsularea.

Person p = new Person("Ana", 30);
 
Field field = Person.class.getDeclaredField("name");
field.setAccessible(true);
 
String value = (String) field.get(p);
System.out.println(value);

Acest lucru rupe principiile OOP și trebuie folosit doar în infrastructură sau framework-uri.

Invocarea metodelor prin Reflection

Method m = Person.class.getDeclaredMethod("getName");
String result = (String) m.invoke(p);
 
System.out.println(result);

Reflection permite:

  • apelarea metodelor necunoscute la compilare
  • decuplarea codului

Crearea obiectelor dinamic

Constructor<Person> ctor =
    Person.class.getConstructor(String.class, int.class);
 
Person p = ctor.newInstance("Ion", 25);

Acesta este mecanismul folosit de:

  • Jackson
  • Hibernate
  • Spring

Reflection și Adnotări

Reflection este mecanismul prin care adnotările RUNTIME sunt citite.

Exemplu:

@Retention(RetentionPolicy.RUNTIME)
@interface Info {
    String value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Info {
    String value();
}

Citire la runtime:

Info info = Person.class.getAnnotation(Info.class);
System.out.println(info.value());

Reflection în framework-uri reale

Reflection este folosit pentru:

  • scanarea claselor
  • identificarea adnotărilor
  • apelarea metodelor automat

Exemple:

  • JUnit → caută metode cu @Test
  • Jackson → caută câmpuri și constructori
  • Spring → injectează dependențe

Costuri și riscuri

Reflection:

  • este mai lentă decât apelurile directe
  • elimină verificările de tip la compilare
  • poate produce erori doar la runtime

Reflection este o unealtă de framework, nu de business logic. În practică, o vom folosi doar la testarea codului nostru.

Assertions

Ce sunt assertions?

Assertions sunt afirmații folosite pentru a verifica ipoteze interne ale programului în timpul execuției. Ele exprimă condiții care ar trebui să fie mereu adevărate dacă programul funcționează corect.

Aserțiile nu sunt mecanisme de tratare a erorilor și nu înlocuiesc excepțiile.

Scopul assertions

Assertions sunt folosite pentru:

  • detectarea bug-urilor logice
  • validarea presupunerilor interne
  • documentarea implicită a codului

Sintaxa de bază

assert condition;

Sau cu mesaj:

assert condition : "Mesaj de eroare";

Dacă condition este false, JVM aruncă un AssertionError.

Exemplu simplu

public int divide(int a, int b) {
    assert b != 0 : "b must not be zero";
    return a / b;
}

Dacă b este 0, programul va eșua doar dacă assertions sunt activate.

Activarea assertions

Assertions sunt dezactivate implicit.

Se activează la rulare:

java -ea Main

Dacă assertions nu sunt activate, codul assert nu se execută deloc.

Assertions vs Excepții

Assertions Excepții
pentru bug-uri interne pentru erori de runtime
pot fi dezactivate sunt mereu active
nu se folosesc pentru input user validează input extern
poo-ca-cd/laboratoare/programare-avansata-java.1768164831.txt.gz · Last modified: 2026/01/11 22:53 by florian_luis.micu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0