Table of Contents

Laboratorul 1: Limbajul Java

Obiective

Scopul acestui laborator este familiarizarea studenților cu noțiunile de bază ale programării în Java.

Aspectele urmărite sunt:

Aspectele bonus urmărite sunt:

  • În acest laborator există mai multe secțiuni marcate [Optional]. Aceste secțiuni cuprind informații bonus care vă pot fi prezentate în timpul laboratorului sau pe care le puteți aprofunda în afara acestuia, ele nefiind necesare pentru laboratoarele viitoare sau pentru teme.
  • De asemenea, veți întâlni câteva secțiuni marcate [Nice to know]. Vă recomandăm ca acestea să aibă prioritate în parcurgerea secțiunilor de tip [Optional], deoarece vă pot oferi informații bonus care să fie și foarte probabil utile pentru teme sau laboratoare viitoare.
  • Verificați că ați instalat toate utilitarele din laboratorul 0. Dacă ați întâmpinat probleme sau aveți nevoie de clarificări discutați cu laborantul.

📈 De la C la Java

În cadrul următoarelor secțiuni vă vom prezenta concepte familiare din C, dar transpuse în limbajul Java.

Variabile

Variabilele în Java respectă formatul tradițional: tip_de_date nume_variabilă = valoare. Valoarea unei variabile poate să fie un literal (adică o valoare efectivă cum ar fi 10, 43.9, 'c' etc.) sau o altă variabilă.

De asemenea, este bine să reținem diferența dintre asignare, inițializare și declarare:

int x; // declarare
x = 10; // asignare
 
int y = 20; // declarare cu inițializare
 
y = x; // de asemenea asignare

Tipuri primitive

Conform POO, orice este un obiect, însă din motive de performanță, Java suportă și tipurile de bază, care nu sunt clase.

boolean isValid = true;
 
char nameInitial = 'L';
 
byte hexCode = (byte)0xdeadbeef;
 
short age = 23;
 
int credit = -1;
 
long userId = 169234;
 
float percentage = 0.42f;
 
double money = 99999;

Vom învăța în laboratoarele următoare ce reprezintă obiectele, clasele și de ce primitivele sunt mai performante decât obiectele.

Tip Memorie Interval binar Interval de valori
byte 8 biți [-27→27-1] [-128→127]
short 16 biți [-215→215-1] [-32768→32767]
int 32 biți [-231→231-1] [-2_147_483_648→2_147_483_647]
long 64 biți [-263→263-1] [-9_223_372_036_854_775_808→9_223_372_036_854_775_807]
char 16 biți [-215→215-1] ['\u0000'→'\uffff'] (Unicode)
boolean 1 bit* [0, 1] [false, true]
float 32 biți IEEE 754: 1 bit semn, 8 biți exponent, 23 biți mantisă [1,4e-045→3,4e+038] (precizie 7 zecimale)
double 64 biți IEEE 754: 1 bit semn, 11 biți exponent, 52 biți mantisă [4,9e-324→1,8e+308] (precizie 15 zecimale)

Conversia între tipuri primitive în Java

Când folosim literali cum ar fi 345, 24.5, 'c', este important să știm următoarele:

Drept urmare avem următorul cod:

int a = 345; // valid
short b = (short)20; // necesită "explicit casting"
 
double d = 24.5; // valid
float f = 35.4f; // de asemenea valid
float f2 = (float)35.4; // necesită "explicit casting"

În Java, conversia între tipuri primitive se poate face implicit sau explicit, în funcție de risc și compatibilitate între tipuri.

Implicit Casting (Widening)

Implicit Java convertește automat un tip mai mic într-un tip mai mare, fără pierdere de date. Se aplică tipurilor numerice byte → short → int → long → float → double și char → int.

byte b = 10;
int i = b;      // byte -> int
double d = i;   // int -> double, devine "10.0"
Explicit Casting (Narrowing)

Se folosește explicit atunci când convertim un tip mai mare într-un tip mai mic, unde există risc de pierdere de date. Necesită casting manual prin (tipDestinatie):

double pi = 3.14;
int intPi = (int) pi; // partea zecimală se pierde -> devine "3"

  • Folosiți implicit casting atunci când este sigur și nu pierdeți date.
  • Folosiți explicit casting doar când sunteți siguri că pierderea de informație este acceptabilă sau necesară.

[Optional] Despre primitive

[Optional] Despre primitive

Mărimea tipului char

Probabil ați observat că char nu are mărimea de 8 biți ca în C. Java permite stocarea caracterelor Unicode în char, iar pentru acestea este nevoie de mai multă memorie, dar astfel se permite stocarea mai multor caractere speciale (ex. emoji-uri, alfabet chirilic etc.). C în comparație permite stocarea caracterelor ASCII în char, fiind limitat de memorie.

Mărimea tipului boolean

Tipul boolean stochează teoretic un bit. În realitate, memoria este procesată mereu în bank-uri de câte 8 biți, deci nu se poate folosi doar un singur bit. Însă, JVM-ul folosește mai multe trucuri pentru a economisi memorie, cum ar fi folosirea măștilor pe biți pentru a stoca mai multe valori de tip boolean într-un octet.

Mărimea tipurilor primitive în funcție de arhitectură

Față de C, memoria alocată fiecărui tip primitiv în Java are o valoare fixă indiferent de arhitectură. Acest lucru se datorează JVM-ului care acționează ca o mașină virtuală peste sistemul de operare, deci poate procesa memoria într-un mod consecvent.

Afișarea la output în Java

În Java, afișarea pe consolă se face cu obiectul standard System.out. Acesta oferă mai multe metode, dintre care cele mai folosite sunt print(), println() și printf().

Momentan nu am învățat ce sunt metodele și obiectele, dar este important să înțelegem că putem folosi una din următoarele comenzi pentru a afișa mesaje în consolă:

  • System.out.println()
  • System.out.print()
  • System.out.printf()

println() și print()

Metoda print() afișează text sau valori pe consolă fără să adauge o linie nouă la final. Dacă apelăm mai multe instrucțiuni print(), acestea vor continua pe aceeași linie.

Metoda println(), în schimb, afișează textul și adaugă automat un caracter de sfârșit de linie \n sau \r\n în funcție de cum aveți setat IntelliJ-ul (LF sau CRLF pe care îl găsiți în fereastra de IntelliJ în dreapta jos).

System.out.print("Hello");
System.out.print(" World");
System.out.println("!");

Output:

Hello World!
Concatenarea și afișarea variabilelor

În Java, putem combina șiruri de caractere cu variabile folosind operatorul +. Acest lucru este mai simplu decât în C, unde trebuia să folosim formate.

int varsta = 20;
String nume = "Ana";
 
System.out.println("Numele este " + nume + " și are " + varsta + " ani.");

Output:

Numele este Ana și are 20 ani.

  • Șirurile de caractere sunt delimitate de (ex. “Ana are mere” → șir de caractere).
  • Orice tip de variabilă (int, double, boolean etc.) poate fi transformat automat într-un șir dacă îl concatenați cu un șir de caractere folosind operatorul +.

printf()

Dacă vrem mai mult control asupra afișării, putem folosi printf(), asemănător cu printf din C. Această metodă permite specificarea de formate:

int x = 10;
double pi = Math.PI;
 
System.out.printf("x = %d, pi = %.2f%n", x, pi);

Output:

x = 10, pi = 3.14

În general veți prefera printf doar dacă doriți să controlați precizia, alinierea și stilul valorilor afișate. Însă recomandarea este să folosiți print și println fiind mult mai lizibile.

Expresii și declarații

Java folosește expresii și declarații pentru a descrie activitățile unui program. Expresiile produc valori, iar declarațiile execută acțiuni.

Expresii

O expresie produce un rezultat (valoare) la evaluare. Valoarea poate fi numerică, de tip referință sau void (metode fără valoare de retur).

int sum = 5 + 3;            // expresie numerică
String s = "Hi" + " there"; // concatenare String
Object o = new Object();    // expresie care returnează referință

Veți învăța despre obiectul String și operatorul new în laboratoarele următoare.

Operatorii și precedența

Operatorii combină sau alterează expresii. Precedența determină ordinea în care se evaluează operatorii, aceasta fiind fixă și reprezentată de un tabel de precedență în următoarea ordine crescătoare (de la cel mai important, la cel mai puțin important):

  1. Paranteze: (, )
  2. Unar: ++, , +, -, !
  3. Multiplicare, împărțire, rest: *, /, %
  4. Adunare și scădere: +, -
  5. Shiftare pe biți: » (signed right shift), « (signed left shift), »> (unsigned right shift), «< (unsigned left shift)
  6. Comparare: <, >, , >=
  7. Egalitate: ==, !=
  8. AND/OR/XOR pe biți: &, |, ^
  9. Condiționale: &&, ||
  10. Ternar: ?:
  11. Atribuire: = și asignare: +=, -=, /=, *=, %=

Declarații și atribuiri

Reamintin că declarația definește valori (ex. int i;), atribuirea oferă valori (ex. i = 5) și de asemenea putem folosi atribuirea și în expresii (ex. j = (i = 5);j și i vor avea valoarea 5).

Structuri de control

Structurile de control determină fluxul de execuție al unui program. În Java, cele mai comune sunt: condiționale (if, switch) și bucle (for, while, do-while).

Instrucțiunea if/else

Permite alegerea între două sau mai multe ramuri, în funcție de o condiție booleană:

int age = 20;
 
if (age >= 60) {
    System.out.println("Senior");
} else if (age >= 18) {
    System.out.println("Adult");
} else {
    System.out.println("Minor");
}

Pentru o singură instrucțiune în blocul unei ramuri, acoladele {} sunt opționale.

Înlocuirea if/else cu operator ternar

Operatorul ternar este o formă scurtă de if - else care returnează o valoare.

System.out.println(age >= 18 ? "Adult" : "Minor");

Formatul folosit de operatorul ternar este:

condiție ? expresie1 : expresie2

Puteți să folosiți o expresie ternară înăuntrul unei expresii, astfel simulând un comportament de tipul if - else if - else:

condiție if ? expresie if : (condiție else if ? expresie else if : expresie else)

Mai mult, puteți simula oricâte condiții else if folosind regula de mai sus. Acum putem transforma codul de mai sus din secțiunea if/else astfel:

System.out.println(age >= 60 ? "Senior" : (age >= 18 ? "Adult" : "Minor"));

  • Condiția trebuie mereu să returneze o valoare de tip boolean.
  • Deși putem simula lanțuri if - else if - else, nu este recomandat să facem asta pentru că reducem lizibilitatea codului. Operatorul ternar este preferat doar atunci când înlocuim structuri if - else.

Instrucțiunea switch

Selectează între mai multe opțiuni posibile pe baza valorii unei expresii:

int day = 3;
char dayInitial;
 
switch(day) {
    case 1: dayName = 'M'; break;
    case 2: dayName = 'T'; break;
    case 3: dayName = 'W'; break;
    case 4: dayName = 'T'; break;
    case 5: dayName = 'F'; break;
    default: dayName = 'W'; break;
}

  • break oprește executarea cazurilor ulterioare.
  • default este opțional și se execută dacă niciun case nu se potrivește.

Java 14+ permite switch expressions pentru sintaxă mai compactă:

char size = switch(value) {
    case 1, 2, 3 -> 'S'; // S -> small
    case 4 -> 'M';       // M -> medium
    case 5, 6 -> 'L';    // L -> large
    default -> 'U';      // U -> unknown
};
Bucle repetitive

Execută blocul atât timp cât condiția este adevărată:

int count = 5;
while(count > 0) {
    System.out.println(count); // 5 4 3 2 1
    count--;
}
System.out.println("Done!");

Execută blocul cel puțin o dată, apoi verifică condiția:

int count = 10;
do {
    System.out.println(count); // 10
    count--;
} while(count > 10);

Bucle de tip „counting loop”, ideale pentru iterații cunoscute:

for(int i = 0; i < 5; i++) {
    System.out.println(i); // 0 1 2 3 4 
}

Puteți avea mai multe variabile în inițializare și incrementare:

for(int x = 0, y = 10; x < y; x++, y--) {
    System.out.println("(" + x + "), (" + y + ")"); // (0, 10) (1, 9) (2, 8) (3, 7) (4, 6)
}

Iterează prin colecții sau array-uri:

int[] numbers = {1, 2, 3, 4};
for(int n : numbers) {
    System.out.print(n + " "); // 1 2 3 4
}

Oprește complet bucla în care se află și continuă execuția după buclă. Dacă instrucțiunea break se află în mai multe bucle se va opri bucla cea mai apropiată:

for (int j = 0; j < 2; j++) {
    for (int i = 0; i < 10; i++) {
        if (i == 5) break; // bucla "i" se oprește când "i" este 5
        System.out.println(i);
    }
}

Sare peste restul instrucțiunilor din iterația curentă și trece la următoarea iterație a buclei:

for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) continue; // nu se execută pentru i par
    System.out.println(i); // afișează doar numerele impare
}

break și continue se folosesc doar în interiorul buclelor (for, while, do-while) sau în switch (doar pentru break).

Array-uri și matrice

Vectorii sunt utilizați pentru a stoca mai multe valori într-o singură variabilă. Un vector este de fapt o matrice (array) unidimensională.

Exemplu definire vector de String-uri cu valorile deja cunoscute
String[] cars = { "Volvo", "BMW", "Ford" };
Exemplu creare și populare vector cu valori de la 1 la 20
int[] intArray = new int[20];
for (int i = 0; i < intArray.length; i++) {
	intArray[i] = i + 1;
}

De asemenea, structura pentru o matrice este similară cu cea din C:

int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6}
};
System.out.println(matrix[1][2]); // 6

  • Înainte să populăm vectorul, trebuie declarat (int[] intArray) și alocată memorie pentru 20 elemente de tip int (new int[20]).
  • Pentru a accesa lungimea vectorului, folosim câmpul length păstrat în vector.
  • Indexarea elementelor într-un array începe de la 0.
  • Vom afla în laboratoarele următoare ce înseamnă new și cum se realizează alocarea memoriei în Java.

Comentarii

În Java, comentariile sunt fragmente de text ignorate de compilator, folosite pentru a explica codul.

// Acesta este un comentariu pe o linie
int x = 10; // putem comenta și după cod
/* Acesta este un comentariu
   pe mai multe linii */
int x = 10;

JavaDoc

Ce este?

JavaDoc reprezintă o specificație care explică scopul sau înțelesul elementului căruia îi este atașat. Acesta se poate atașa fie unei clase, fie unei metode. Codul pe care îl scriem nu este complet dacă nu are acest tip de documentație, deoarece, cu toate că următoarea persoană poate să își dea seama ce face o bucată de cod, aceasta nu o să aibă nicio informație legată de utilitate sau despre direcția de dezvoltare din viitor. Fără o astfel de documentație un programator nu poate lua decizii informate despre cum să interacționeze cu codul.

Cum trebuie să fie această documentație?

Atunci când scriem documentația trebuie să ținem cont de 3 aspecte. Aceasta trebuie să fie clară, completă și concisă.

Structura

Un JavaDoc trebuie să fie ușor de citit și astfel recomandăm următoarea structură:

Această structură este o sugestie care trebuie adaptată în funcție de unde este folosită.

Block tags

Block tag Descriere Exemplu
@param NumeParametru Ne oferă informații legate de parametrii metodelor. Dacă anumite valori nu sunt acceptate drept argument (ex. null), acestea trebuie menționate în documentație. @param start începutul intervalului de căutare
{@link} Este utilizat pentru a face o legătură cu o componentă deja existentă printr-un link. Este folosit pentru a fixa o referință. Extinde funcționalitatea {@link metodaMeaSuper(String)} pentru a fi utilizată pe date de tip Float.
{@code} Folosit pentru a referi părți de cod fără a fi formatată precum text HTML. {@code HashList} reprezintă o structură de date unde datele sunt de tipul cheie-valoare.

Exemplu de JavaDoc în cod:

/**
* Returns an image that represents a solved sudoku. 
* This method always returns immediately, whether or not the 
* image exists. It is a similar implementation to {@link solveTetris}
* located in the same suite of games. 
*
* @param  path an absolute path to the location of the starting image
* @param  name the name of the image that represents the solved sudoku
*/
public Image solveSudoku(String path, String name) {
    try {
        return solve();
    } catch (Exception e) {
        return null;
    }
}

Coding style for beginners

De ce să respectăm un coding style?

Un aspect foarte important în momentul în care trebuie să scrieți cod în Java este legat de modul în care scrieți, mai exact de organizarea codului în interiorul unor clase cu funcționalități bine definite. Poate cel mai important motiv al respectării acestor reguli este faptul că vă va fi de ajutor în momentele în care faceți debugging sau testing pe o sursă. Primii pași pentru a avea un cod cât mai lizibil și ușor de urmărit sunt următorii:

Exemplu de cod care nu respectă coding style-ul
public class BadStyle{
public static void main(String[]args){
int x=10;int y=20;
if(x>0) System.out.println("Pozitiv");else{System.out.println("Negativ");}
for(int i=0;i<y;i++){System.out.print(i+",");}
System.out.println("x="+x+" y="+y);
} }

  • Mai multe detalii privind Coding Style-ul, precum și documentația necesară le găsiți pe pagina dedicată.
  • Fiți receptivi la recomandările laborantului, deoarece dobândirea unui Coding Style se face prin exercițiu și feedback. Există diverse reguli de Coding Style în Java și este foarte probabil să vă întâlniți cu ele pe măsură ce parcurgeți laboratoarele și exercițiile din acestea.

🌎 World of Java

În secțiunile următoare vă vom prezenta particularități ale limbajului Java și al Java Development Kit (JDK).

Codul sursă în proiectele Java va fi scris în fișiere cu extensia .java, mai exact în interiorul claselor pe care le vom defini în aceste fișiere (mai multe detalii despre clase în laboratoarele viitoare).

Compilatorul Java (javac)

Compilatorul Java este instrumentul care transformă codul sursă scris de programator (salvat în fișiere .java) în bytecode (salvat în fișiere .class). Acest bytecode este un format intermediar pe care Mașina Virtuală Java (JVM) îl poate executa. În cadrul JDK, compilatorul este disponibil prin utilitarul de linie de comandă javac.

Scris în Java

Compilatorul javac este o aplicație scrisă chiar în limbajul Java (click me). Asta înseamnă că el poate fi rulat pe orice calculator unde există instalat JVM (Java Virtual Machine). Nu contează dacă sistemul de operare este Windows, Linux sau macOS, același program funcționează peste tot.

Datorită acestei proprietăți, limbajul Java este considerat un limbaj portabil, adică o aplicație Java pot fi scrisă o singură dată și rulată oriunde.

Conversia în bytecode

Când scriem cod sursă într-un fișier .java, acesta nu este înțeles direct de calculator. Compilatorul transformă instrucțiunile Java într-un format intermediar numit bytecode, care se salvează într-un fișier .class.

Main.java
public class Main {
   public static void main(String[] args) {
     System.out.println("Hello World");
   }
}

De exemplu fișierul Main.java de mai sus va fi converit în fișierul Main.class. Acest fișier conține bytecode, iar la execuție va fi rulat de JVM.

  • Rețineți că fișierele .class nu sunt cod “nativ” al calculatorului, ci un cod universal pe care JVM îl poate interpreta.
  • Vom folosi exemplul de cod de mai sus pentru a prezenta proprietățile compilatorului Java și pentru a înțelege mai bine structura unui program de tipul “Hello World”.

Restricții privind fișierele

În Java există câteva reguli stricte legate de fișierele .java:

Dacă fișierul de mai sus s-ar numi Dog.java, am primi o eroare la compilare, deoarece clasa publică dinăuntrul fișierului se cheamă “Main” (public class Main).

Q: De ce există această convenție de nume?

A: Pentru a facilita organizarea fișierelor în proiecte Java. O să vedeți că în proiectele care sunt scrise într-un limbaj care folosește paradigma OOP există foarte multe fișiere, iar această restricție ne ajută să navigăm mai ușor proiectul.

Organizare pe pachete

Java folosește pachete pentru a organiza clasele, ele fiind de fapt foldere dintr-un sistem de fișiere.

Main.java
package mypackage.entrypoint
 
public class Main {
   public static void main(String[] args) {
     System.out.println("Hello World");
   }
}

De exemplu, pentru codul de mai sus ne dăm seama că fișierul Main.java se află la calea src/mypackage/entrypoint/Main.java.

  • Codul sursă în proiectele Java trebuie scris în folderul src.
  • Putem crea oricâte pachete dorim, scopul lor este doar de a grupa logic clasele în grupuri și de a evita conflictele de nume în proiecte mari.

Exemplu de compilare

Compilarea unui fișier

Pentru fișierul Main.java care se află în folderul src rulăm din terminal următoarea comandă:

$ cd src/
$ javac Main.java

În urma rulării vom avea un fișier Main.class:

$ ls
Main.class Main.java
Compilarea mai multor fișiere

Prin înșiruirea fișierelor Java putem compila mai multe surse în același timp, rezultând mai multe fișiere .class în cazul în care nu există erori:

$ javac Main.java Dog.java Cat.java
$ ls
Main.class Dog.class Cat.class Main.java Dog.java Cat.java

[Optional] Alte funcționalități ale javac

[Optional] Alte funcționalități ale javac

Recompilare inteligentă

javac verifică timpii de modificare ai fișierelor sursă și recompilează automat doar clasele necesare.

Lucru cu biblioteci externe

Compilatorul poate folosi atât fișiere .java, cât și fișiere .class deja compilate, dacă acestea se află în classpath.

Integrare cu unelte de build

În proiecte mari se folosesc instrumente precum Gradle sau Maven pentru a organiza dependențele și procesele de compilare, dar în spatele lor rulează tot compilatorul javac.

Mașina virtuală Java (JVM)

JVM este componenta care face posibilă execuția aplicațiilor Java pe orice platformă. Ea primește bytecode-ul generat de compilator prin javac și îl rulează într-un mediu controlat, gestionând resursele, securitatea și performanța aplicației. Astfel, programatorul scrie o singură dată codul, iar JVM se ocupă de adaptarea la sistemul de operare și hardware.

După ce am compilat codul nostru în bytecode, putem porni o instanță de JVM folosind comanda java, de unde putem încărca proiectul nostru astfel:

$ ls
Main.class Main.java
$ java Main
Hello World!

  • Utilitarul java acceptă doar fișiere .class, deci comanda java Main se traduce în java Main.class, dar dacă încercați să rulați java Main.class veți avea erori.
  • Nu folosiți java .\Main sau java ./Main, scrieți direct java Main ca să nu aveți erori.

Reamintim că metoda main permite stocarea mai multor parametrii din linia de comandă. Pentru a rula o aplicație Java cu argumente din linia de comandă folosiți următoarea structură:

$ java MyApp arg1 arg2

Acestea pot fi accesate în program prin args[0], args[1] etc. din cadrul metodei main:

public class Main {
    // Presupunem că s-a rulat comanda "java Main Hello World"
    public static void main(String[] args) {
        if (args.length == 2) {
            System.out.println(args[0] + " " + args[1]); // Hello World
        } else {
            System.out.println("Wrong number of parameters!");
        }
    }
}

Puteți specifica mai ușor parametrii din linia de comandă direct din IntelliJ urmărind acest tutorial de la JetBrains.

Datorită pasului de compilare pentru transformarea fișierelor din cod Java în bytecode, dar și a JVM-ului, care folosește un mecanism de interpretare a bytecode-ului, Java este considerat un limbaj compilat și interpretat. Pentru mai multe detalii, consultați secțiunea [Optional] de mai jos.

[Optional] JVM și JIT în detaliu

[Optional] JVM și JIT în detaliu

1. Încărcarea claselor (Class Loader)

JVM folosește un mecanism de încărcare dinamică a claselor. Când o aplicație are nevoie de o clasă, Class Loader-ul o caută, o încarcă în memorie și o verifică. Acest proces are mai multe etape:

  • Loading: clasa este adusă în memorie din fișierul .class sau dintr-o arhivă .jar.
  • Linking: clasa este verificată (Bytecode Verifier) pentru a preveni erori sau cod malițios.
  • Initialization: sunt inițializate variabilele statice și blocurile static.

Această abordare de tip “lazy loading” înseamnă că doar clasele efectiv utilizate în execuție sunt aduse în memorie, ceea ce optimizează consumul de resurse.

2. Interpretarea și compilarea dinamică (Interpreter și JIT)

Inițial, bytecode-ul este interpretat, adică instrucțiunile sunt executate una câte una, ceea ce oferă portabilitate dar nu maximă performanță. Pentru a accelera aplicația, JVM include Just-In-Time Compiler (JIT), care:

  1. Detectează “hot spots” (secvențe de cod executate frecvent).
  2. Traduce bytecode-ul acelor secvențe în cod mașină nativ, direct optimizat pentru platforma curentă.
  3. Stochează rezultatele pentru reutilizare, astfel încât ulterior execuția devine mult mai rapidă (caching).

JIT compilează codul frecvent utilizat și îl stochează în memorie ca acesta să fie returnat direct, astfel se evită recompilarea fiecărei linii prin rularea interpretorului.

Practic, aplicațiile Java combină avantajele limbajelor interpretate (portabilitate, flexibilitate) cu performanțe apropiate de cele native datorită JIT.

3. Gestionarea memoriei

Vom învăța în laboratoarele următoare cum JVM gestionează memoria, dar pentru moment este suficient să știm că JVM se ocupă de gestionarea memoriei din Heap și Stack și că există un mecanism inteligent de gestionare a referințelor numit Garbage Collector.

4. Securitate și izolare

Fiecare clasă încărcată este verificată pentru a respecta regulile limbajului Java (de exemplu, să nu acceseze direct memoria). JVM oferă un mediu sandbox, prevenind accesul neautorizat la resursele sistemului.

5. Portabilitate și abstractizare de platformă

Unul dintre cele mai mari avantaje ale JVM este că ascunde detaliile legate de arhitectura procesorului și de sistemul de operare. Același bytecode Java poate fi rulat pe Windows, Linux sau macOS fără modificări, atâta timp cât există o implementare de JVM disponibilă pentru platforma respectivă.

Rularea programelor Java

O aplicație Java standalone are nevoie de cel puțin o clasă care să conțină metoda main(). Aceasta este punctul de intrare al programului și este prima metodă executată de JVM.

Metoda main are următoarea structură:

public static void main(String[] args)

  • Vom învăța mai multe despre public, static și String în următoarele laboratore. În acest lab vom scrie tot codul mereu în metoda main.
  • Reamintim că metoda main se află mereu într-o clasă.

Expresiile și declarațiile trebuie scrise mereu în interiorul unei clase, deoarece Java este un limbaj OOP pur. Limbajele OOP și imperative (ex. C++) permit scrierea codului în afara claselor. Mai mult, codul pe care îl vom scrie în acest lab se va afla mereu în metoda principală main.

Organizarea unui proiect Java

Înainte de a începe orice implementare, trebuie să vă gandiți cum grupați logica întregului program pe unități. Știm din secțiunile prezentate anterior că avem două metode de a grupa fișierele, concret prin pachete și clase.

Elementele care se regăsesc în același grup trebuie să fie conectate în mod logic, pentru o ușoară implementare și înțelegere ulterioară a codului. În cazul Java, aceste grupuri logice se plasează în pachete și se reflectă pe disc conform ierarhiei din cadrul proiectului. Reamintim că pachetele pot conține atât alte pachete, cât și fișiere sursă.

 Organizarea pachetelor in Java

Următorul pas este delimitarea entităților din cadrul unui grup, pe baza unor trăsături individuale. În cazul nostru, aceste entități vor fi clasele care vor conține cod efectiv Java.

Pentru un exemplu de creare a unui proiect, adăugare de pachete și fișiere sursă, consultați laboratorul trecut.

Vă recomandăm să parcurgeți următoarele două secțiuni [Nice to know], deoarece acestea vă vor învăța mai multe despre structura proiectelor Java și despre adăugarea dependențelor. Următoarele laboratoare nu necesită informații despre aceste concepte și temele pot fi făcute fără să cunoașteți detaliile menționate, dar cu siguranță vă pot ajuta pentru anumite scenarii de debug sau la personalizarea proiectului prin dependențe extra.

[Nice to know] Executabile JAR

Un JAR (Java ARchive) este un fișier comprimat (similar cu un .zip) care conține clase compilate .class, resurse (imagini, fișiere de configurare) și metadate necesare pentru rularea sau distribuirea unei aplicații Java.

După ce am compilat clasele proiectului nostru putem crea un fișier .jar folosind comanda:

$ jar cf app.jar *.class

Un JAR poate conține un fișier special numit MANIFEST.MF (în folderul META-INF/), unde se definește punctul de intrare al aplicației:

Main-Class: MainApp

Dacă fișierul manifest conține o definiție de tipul Main-Class, aplicația va putea fi lansată astfel:

$ java -jar app.jar

[Nice to know] Sisteme de build și management de proiecte

Un sistem de build și management de proiect este responsabil cu facilitarea compilării și rulării unui proiect. Proiectele Java folosesc trei sisteme de build:

În următoarele subsecțiuni vom face referire la conceptul de dependență. O dependență este o bibliotecă externă de care aplicația are nevoie pentru a funcționa. În loc să rescrii de la zero funcționalități complexe (ex: parsarea fișierelor JSON, lucrul cu baze de date, manipularea șirurilor de caractere), poți folosi aceste biblioteci gata făcute.

IntelliJ build system

Structura proiectelor cu IntelliJ build system este următoarea:

Cum funcționează build-ul?
  1. La salvarea fișierelor sau rularea programului, IntelliJ apelează automat Java Compiler javac.
  2. Fișierele .java din src/ sunt compilate în .class și puse în out/.
Cum adăugăm dependențe?

În acest sistem de build se preferă linking-ul executabilelor .jar ca dependențe. Deci este necesar să descărcați dependența voastră ca .jar și să urmăriți pașii din acest tutorial pentru a integra dependența cu proiectul vostru.

Maven

Structura proiectelor Maven este următoarea:

Cum funcționează build-ul?
  1. La salvarea fișierelor sau rularea programului, IntelliJ apelează una din cele trei comenzi în funcție de butonul apăsat:
    • mvn compile: generează fișierele .class.
    • mvn test: rulează testele din src/test/java.
    • mvn package: generează un executabil .jar care conține tot codul sursă al proiectului fără teste.
  2. Maven folosește pom.xml pentru a descărca dependențele necesare din repository-uri externe.
  3. Fișierele .java din src/ sunt compilate în .class și puse în target/.
Cum adăugăm dependențe?

Deoarece Maven folosește formatul XML, va trebui în pom.xml să introducem label-ul <dependencies> (care va fi închis cu un alt label </dependencies>) în care înșiruim dependențele pe care le dorim. Dependențele pot fi luate de pe Maven Repository central unde vi se oferă și structura XML a dependenței pe care trebuie să o includeți.

De exemplu, să încercăm să adăugăm dependența Jackson de pe Maven Central care ne permite să lucrăm cu obiecte JSON la teme. Inițial pom-ul nostru arată astfel:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.company</groupId>
    <artifactId>nume-proiect</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
</project>

Adăugăm după label-ul </properties>, label-ul <dependencies> și dăm paste la codul XML din link-ul de mai sus, după care închidem array-ul de dependențe cu </dependencies>:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>org.company</groupId>
    <artifactId>nume-proiect</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <properties>
        <maven.compiler.source>25</maven.compiler.source>
        <maven.compiler.target>25</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
 
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.20.0</version>
        </dependency>
    </dependencies>
</project>

Acum avem instalat Jackson versiunea 2.20.0 în proiectul nostru. Putem verifica că proiectul este configurat corect cu noua dependență rulând din nou procesul de build cu mvn build.

  • Dacă dorim să schimbăm versiunea dependenței putem să schimbăm doar valoarea label-ului <version>.
  • Maven permite mult mai multe opțiuni de configurare pentru build pe care le puteți aprofunda aici.

Pentru toate sistemele de build avem în proiect folderul .idea care este folosit de IntelliJ pentru a gestiona proiectul cu setările corecte atunci când îl deschideți. Oricând doriți să deschideți un proiect IntelliJ asigurați-vă că acel folder există.

[Optional] Java din linia de comandă

JShell este un instrument introdus în Java 9 care oferă un mediu interactiv (REPL – Read-Eval-Print-Loop). El permite rularea de instrucțiuni Java fără a mai crea fișiere .java și fără a compila manual cu javac. Este foarte util pentru testarea rapidă a expresiilor, a fragmentelor de cod sau pentru învățare.

Pentru a porni mediul interactiv scrieți în terminal jshell:

$ jshell
jshell> int x = 10;
x ==> 10
 
jshell> x * x
$1 ==> 100
 
jshell> System.out.println("Salut, JShell!");
Salut, JShell!

Funcționalități utile ale JShell:

[Optional] System properties în Java

Java oferă o modalitate portabilă de a configura aplicațiile prin System Properties, concret perechi cheie-valoare transmise la pornirea JVM. System properties sunt utile pentru a configura aplicația Java la momentul pornirii, fără a modifica codul sursă.

Acestea se trimit din linia de comandă folosind opțiunea -D:

$ java -Dsurname=Popescu -Dname=Ion Main

Pentru a citi valorile în cod folosim instrucțiunea System.getProperty(“nume_proprietate”):

public class Main {
    public static void main(String[] args) {
        String surname = System.getProperty("surname");
        String name = System.getProperty("name");
        System.out.println(surname + " " + name); // Popescu Ion
    }
}

Cazuri de utilizare frecvente:

În Java se pot citi variabile de mediu, dar Oracle recomandă folosirea system properties, fiind o soluție mai structurată pentru configurare.

[Optional] Classpath

Classpath-ul este lista de directoare și fișiere .jar unde compilatorul javac și JVM-ul java caută clasele necesare pentru compilare și execuție.

Fără classpath, JVM nu știe unde să găsească clasele tale sau bibliotecile externe.

Compilarea și rularea unui proiect Java fără pachete se face astfel:

$ javac Hello.java
$ java Hello

Dar în momentul în care introducem pachete, trebuie să specificăm classpath-ul. De exemplu avem fișierul Main la calea ~/Desktop/project/src/main/java/org/example/Main.java:

package org.example;
public class Main { 
    public static void main(String[] args){ 
        System.out.println("Hi"); 
    } 
}

Ne aflăm în directorul ~/Desktop/project/ și pentru a compila sursele folosim comanda:

$ pwd
~/Desktop/project/
$ javac src\main\java\org\example\Main.java

În urma compilării vom avea un fișier .class la locația ~/Desktop/project/src/main/java/org/example. Contrar așteptărilor, următoarele două comenzi nu funcționează:

$ java src.main.java.org.example.Main
Error: Could not find or load main class src.main.java.org.example.Main
Caused by: java.lang.NoClassDefFoundError: src/main/java/org/example/Main (wrong name: org/example/Main)
$ java src/main/java/org/example/Main
Error: Could not find or load main class src.main.java.org.example.Main
Caused by: java.lang.NoClassDefFoundError: src/main/java/org/example/Main (wrong name: org/example/Main)

Ca să putem rula fișierul nostru trebuie să îi spunem JVM-ului unde se află pachetele noastre folosind classpath:

$ java -cp src/main/java org.example.Main
Hi

Prin comanda de mai sus i-am spus JVM-ului să caute pachetele org și example începând cu src/main/java.

Dacă foloseam o bibliotecă externă .jar cum ar fi jackson.jar care se află la calea ~/Downloads/jackson.jar am fi referențiat-o compilatorului și JVM-ului astfel:

$ pwd
~/Desktop/project
$ javac -cp ~/Downloads/jackson.jar src\main\java\org\example\Main.java
$ java -cp "src/java/main:~/Downloads/jackson.jar" org.example.Main
Hi

În comanda javac de mai sus am specificat unde trebuie compilatorul să găsească biblioteca externă (~/Downloads/jackson.jar).

În comanda java am specificat unde trebuie JVM-ul să caute pachetele clasei main (org și example) și am specificat unde se află biblioteca externă (~/Downloads/jackson.jar). Ambele căi sunt concatenate într-un singur șir de caractere, folosind : ca separator (src/java/main + : + ~/Downloads/jackson.jarsrc/java/main:~/Downloads/jackson.jar).

Regulile generale pentru folosirea classpath-ului corect sunt:

  • -cp: referențiează folderul deasupra folder-ului org.
  • Clasa pe care o rulați trebuie să fie referențiată folosind numele ei complet (ex. org.example.Main).
  • Putem referenția mai multe căi folosind ca separator : pe Linux/macOS sau ; pe Windows.

Java 9 a introdus modules, care permit împărțirea aplicațiilor în unități logice cu control al dependențelor și al vizibilității. Ele ajută la organizarea codului și la optimizarea aplicației, dar nu sunt necesare pentru programele obișnuite. În majoritatea cazurilor puteți continua să folosiți doar pachete și claspath-ul clasic. Pentru detalii despre module puteți consulta acest ghid de pe Baeldung.

Summary

Exerciții - TO BE CHANGED

În cadrul laboratorului și la teme vom lucra cu ultima versiune stabila de Java. Când consultați documentația uitați-vă la cea pentru această versiune.

Prerequisites

Fiind un laborator introductiv și cu multe concepte destul de greu de verificat automat cu LambdaChecker, punctajul de săptămâna aceasta este opțional/bonus. Cu toate acestea, încercați să rezolvați toate exercițiile și să puneți cât mai multe întrebări asistenților, pentru a vă însuși cât mai bine cunoștințele.

Task 1 (3p)

  1. Creați pachetul lab1, unde adăugați codul din secțiunea Exemplu de implementare. Rulați codul din IDE.
  2. Folosind linia de comandă, compilați și rulați codul din exemplu
  3. Mutați codul într-un pachet task1, creat în pachetul lab1. Folosiți-vă de IDE, de exemplu Refactor → Move pentru IntelliJ. Observați ce s-a schimbat în fiecare fișier mutat.

Task 2 (5p)

Creați un pachet task2 (sau alt nume pe care îl doriți să îl folosiți). În el creați clasele:

Task 3 (1p)

  1. Creați două obiecte Student cu aceleași date în ele. Afișați rezultatul folosirii equals() între ele. Discutați cu asistentul despre ce observați și pentru a vă explica mai multe despre această metodă.

Metoda equals este folosită în Java pentru a compara dacă două obiecte sunt egale în ceea ce privește informațiile încapsulate în acestea. Mai precis, se compară referințele celor două obiecte. Dacă acestea indică spre aceeași zonă de memorie atunci equals va returna true, altfel va returna false. Veți învăța în laboratorul 3 mai multe despre cum se folosește această funcție pentru a verifica egalitatea dintre două obiecte.

Exemplu de folosire:

if (obj1.equals(obj2)) {
    // do stuff
}

Task 4 (1p)

  1. Adăugați modificatorul de acces 'private' tuturor variabilelor claselor Student și Internship (e.g. private String name;)
  2. Rezolvați erorile de compilare adăugând metode getter și setter acestor variabile.
    1. Ce ați făcut acum se numește încapsulare (encapsulation) și este unul din principiile de bază din programarea orientată pe obiecte. Prin această restricționare protejați accesarea și modificarea variabilelor.

Pentru a vă eficientiza timpul, folosiți IDE-ul (IntelliJ) pentru a generarea aceste metode: Code → Generate… → Getters and Setters

Resurse și linkuri utile