Pe parcursul laboratoarelor și temelor ați folosit structuri de date oferite de API-ul Java. În cadrul acestui laborator le vom aprofunda.
Aspectele urmărite sunt:
Aspectele bonus urmărite sunt:
În Java există o separare fundamentală între:
int, double, boolean, char etc.);java.lang.Object.
Obiectele de tip wrapper oferă funcționalități suplimentare sub forma unor metode (ex. rotateLeft(), toHex() etc.), însă, spre deosebire de tipurile primitive, acestea sunt gestionate de Garbage Collector, ceea ce poate face execuția mai lentă.
Din aceste motive, Java a ales să păstreze tipurile primitive pentru a evita costurile suplimentare ale obiectelor, mai ales în calculele numerice intensive.
numNodes == -1) puteți folosi clasele de tip wrapper care permit și starea null (ex. numNodes == null).
Pentru a permite utilizarea valorilor primitive în contexte ce necesită obiecte (de exemplu, în colecții despre care vom vorbi mai jos), Java oferă clase wrapper dedicate fiecărui tip primitiv.
| Tip primitiv | Clasă wrapper corespunzătoare |
|---|---|
void | java.lang.Void |
boolean | java.lang.Boolean |
char | java.lang.Character |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
Un obiect wrapper poate fi construit:
Float pi = new Float(3.14); Float pi2 = new Float("3.14");
Dacă șirul nu poate fi convertit într-o valoare numerică validă, se afișează o eroare la run-time de tip NumberFormatException.
Toate clasele numerice (Integer, Double, Float etc.) au la dispoziție metode pentru conversia valorii interne în alte forme primitive:
Double size = new Double(32.76); double d = size.doubleValue(); // 32.76 float f = size.floatValue(); // 32.76f long l = size.longValue(); // 32L int i = size.intValue(); // 32
Aceste metode sunt echivalente cu operațiile de conversie explicită (cast) între tipurile primitive.
Începând cu Java 5, conversia între tipurile primitive și wrapper se face automat:
int primitiveValue = 42; Integer objectValue = primitiveValue; // autoboxing automat de la int → Integer Integer anotherObject = new Integer(99); int anotherPrimitive = anotherObject; // unboxing automat de la Integer → int Integer a = 10; Integer b = 20; int sum = a + b; // a și b sunt unboxed automat, apoi rezultatul e autoboxed dacă e atribuit unui Integer
Compilatorul inserează conversiile în mod implicit, făcând codul mai concis și mai lizibil.
Colecțiile sunt structuri de date esențiale care permit gruparea, organizarea și manipularea eficientă a obiectelor. În Java, colecțiile oferă o modalitate standardizată de a lucra cu seturi, liste, cozi și mapări, reprezentând una dintre cele mai puternice părți ale limbajului.
La nivel de bază, Java oferă tablouri (arrays), despre care am învățat în laboratoarele trecute că sunt structuri fixe, rapide, dar inflexibile. Ele au o dimensiune fixă și nu pot crește sau micșora dinamic, ceea ce devine o limitare serioasă în aplicațiile reale.
Colecțiile au fost introduse pentru a răspunde acestor limitări, oferind structuri dinamice capabile să se redimensioneze automat și să ofere operații avansate de căutare, filtrare și sortare.
| Etapă | Soluție oferită | Limitări |
|---|---|---|
| Java 1.0 | Vector și Hashtable | Lipsă de tipizare, design inconsistent |
| Java 1.2 | Collections Framework | Introduce interfețele Collection și Map |
| Java 5+ | Generics, autoboxing, unboxing | Colecții tipizate și mai sigure |
Colecțiile moderne permit:
Frameworkul de colecții se bazează pe două ierarhii paralele, definite în pachetul java.util:
| Ierarhie | Interfață rădăcină | Ce reprezintă | Exemple |
|---|---|---|---|
| Colecții de elemente | Collection | Grup de obiecte independente | List, Set, Queue |
| Mapări cheie–valoare | Map | Asocieri între chei unice și valori | HashMap, TreeMap |
Map face parte din framework, nu extinde Collection deoarece logica sa (chei și valori separate) diferă conceptual de cea a colecțiilor simple.
Fiecare colecție are un scop diferit:
Prin folosirea ierarhiilor de mai sus, avem următoarele beneficii:
Interfața Collection<E> definește un comportament comun pentru toate colecțiile care gestionează elemente individuale. Ea nu dictează cum sunt stocate datele, ci ce operații sunt posibile.
| Metodă | Descriere | Observații |
|---|---|---|
boolean add(E e) | Adaugă un element | Returnează false dacă e duplicat (în cazul Set) |
boolean remove(E e) | Elimină un element existent | Poate arunca UnsupportedOperationException |
boolean contains(E e) | Verifică dacă elementul există | Compară prin equals() |
int size() | Numărul elementelor | |
boolean isEmpty() | Verifică dacă e goală | |
Iterator<E> iterator() | Permite parcurgerea elementelor | Folosit pentru bucle controlate |
ConcurrentModificationException.
| Metodă | Descriere |
|---|---|
addAll(Collection c) | adaugă toate elementele unei alte colecții |
removeAll(Collection c) | elimină toate elementele dintr-o altă colecție |
containsAll(Collection c) | verifică dacă toate elementele există |
Un iterator este un obiect care permite traversarea unei colecţii şi modificarea acesteia (ex: ştergere de elemente) în mod selectiv. Puteţi obţine un iterator pentru o colecţie, apelând metoda sa iterator(). Interfaţa Iterator este următoarea:
public interface Iterator<E> { boolean hasNext(); E next(); void remove(); // optional }
Exemplu de folosire a unui iterator:
Collection<Double> col = new ArrayList<Double>(); Iterator<Double> it = col.iterator(); while (it.hasNext()) { Double backup = it.next(); // apelul it.next() trebuie realizat înainte de apelul it.remove() if (backup < 5.0) { it.remove(); } }
Apelul metodei remove() a unui iterator face posibilă eliminarea elementului din colecţie care a fost întors la ultimul apel al metodei next() din acelaşi iterator.
În exemplul anterior, toate elementele din colecţie mai mici decât 5 for fi şterse la ieşirea din bucla while.
Această construcţie permite (într-o manieră expeditivă) traversarea unei colecţii.
Collection collection = new ArrayList(); for (Object o : collection) System.out.println(o);
Construcţia for-each se bazează, în spate, pe un iterator, pe care îl ascunde. Prin urmare nu putem şterge elemente în timpul iterării.
În această manieră pot fi parcurşi şi vectori oarecare. De exemplu, collection ar fi putut fi definit ca Object[].
import java.util.*; public class CollectionExample { public static void main(String[] args) { // Folosim Collection cu ArrayList ca implementare Collection<String> fruits = new ArrayList<>(); // Diamond operator <> // Adăugăm elemente fruits.add("Apple"); fruits.add("Banana"); fruits.add("Cherry"); // Verificăm dimensiunea și dacă este goală System.out.println("Dimensiune: " + fruits.size()); System.out.println("Este goală? " + fruits.isEmpty()); // Verificăm dacă un element există System.out.println("Conține 'Banana'? " + fruits.contains("Banana")); // Iterăm prin elemente folosind Iterator System.out.println("Elemente în colecție:"); Iterator<String> it = fruits.iterator(); while (it.hasNext()) { System.out.println(it.next()); } // Eliminăm un element fruits.remove("Apple"); System.out.println("Colecție după eliminare: " + fruits); } }
<>:
Collection<String> fruits = new ArrayList<>(); // corect, specificat doar în stânga Collection<String> fruits = new ArrayList<String>(); // corect, dar nepreferat
Vom învăța mai multe despre acesta în laboratorul despre genericitate.
<E>, <T>, <K, V>. Vom înțelege în următoarele laboratoare la ce se referă exact, însă pentru acest laborator este suficient să știți că sunt niște notații pentru a semnala că o colecție folosește un tip omogen (ex. Collection<E> → Collection poate folosi doar un tip omogen denumit E care poate fi un obiect).
Un Set este o colecție fără duplicate. Elementele sunt comparate folosind metodele equals() și hashCode().
Caracteristici:
HashSet)null (în unele implementări)HashSet – rapid, dar neordonatLinkedHashSet – păstrează ordinea de inserareTreeSet – menține ordinea naturală sau definită de un Comparator| Implementare | Structură internă | Ordine elemente | Permite duplicate? | Complexitate adăugare / căutare / ștergere | Avantaje | Dezavantaje | Când se folosește |
|---|---|---|---|---|---|---|---|
| HashSet | Bazat pe HashMap intern (folosește hashing pentru distribuție rapidă a elementelor) | ❌ Ordine imprevizibilă | ❌ Nu | Aproximativ O(1) (constantă amortizată) | Foarte rapid pentru operații de bază (add, remove, contains) | Nu păstrează ordinea inserării; performanța depinde de funcția hashCode() | Când performanța este prioritară și ordinea nu contează (ex: filtrarea duplicatelor, verificări rapide de apartenență) |
| LinkedHashSet | Extinde HashSet dar menține o listă dublu înlănțuită a elementelor pentru ordinea inserării | ✅ Ordinea inserării | ❌ Nu | Aproximativ O(1) | Păstrează ordinea de inserare, performanță foarte bună | Ușor mai lent decât HashSet (suplimentar pentru menținerea listei) | Când e nevoie de viteză dar și de o ordine previzibilă (ex: cache, istorice, afisări ordonate) |
| TreeSet | Implementat prin TreeMap intern (arbore roșu-negru) | ✅ Ordine naturală (sau definită de un Comparator) | ❌ Nu | O(log n) | Menține elementele sortate, permite căutări bazate pe ordine (headSet, tailSet, etc.) | Mai lent decât HashSet și LinkedHashSet | Când ai nevoie ca elementele să fie sortate sau să poți naviga în intervale (ex: rapoarte, topuri, ordonări lexicografice) |
hashCode() sau equals() după inserarea elementului, comportamentul colecției devine imprevizibil. Vom vorbi despre aceste pe larg în secțiunile de mai jos.
| Subinterfață | Descriere | Metode specifice | Implementări principale |
|---|---|---|---|
SortedSet | Elemente sortate | subSet(), headSet(), tailSet() | TreeSet, ConcurrentSkipListSet |
NavigableSet | Căutare a elementelor apropiate | higher(), lower(), ceiling(), floor() | TreeSet, ConcurrentSkipListSet |
ConcurrentSkipListSet oferă o variantă thread-safe, bazată pe o skip list, potrivită pentru medii concurente, dar care depășește scopul acestui laborator.
import java.util.*; public class HashSetExample { public static void main(String[] args) { // HashSet Set<String> cities = new HashSet<>(); cities.add("Bucharest"); cities.add("Cluj"); cities.add("Bucharest"); // ignorat (duplicat) System.out.println(cities); // ordinea nu este garantată System.out.println("Conține Cluj? " + cities.contains("Cluj")); // LinkedHashSet Set<Integer> numbers = new LinkedHashSet<>(); numbers.add(3); numbers.add(1); numbers.add(2); System.out.println(numbers); // păstrează ordinea de inserare → [3, 1, 2] // TreeSet Set<String> names = new TreeSet<>(); names.add("Ana"); names.add("Ion"); names.add("Maria"); System.out.println(names); // sortat natural → [Ana, Ion, Maria] } }
List este o colecție ordonată ce permite elemente duplicate și acces prin index.
Caracteristici:
ArrayList – acces rapid, inserări lente în mijlocLinkedList – inserări rapide, acces lent la elementeVector – versiune veche, sincronizată (de obicei evitată)| Metodă | Descriere | Exemple |
|---|---|---|
void add(int index, E e) | Inserează la poziția indicată | list.add(2, “Hello”) |
E get(int index) | Returnează elementul | list.get(0) |
E set(int index, E e) | Înlocuiește un element | |
void remove(int index) | Elimină elementul la index |
Iterator. Evitați modificarea listei în timpul iterării, deoarece poate genera ConcurrentModificationException.
| Implementare | Structură internă | Ordine elemente | Permite duplicate? | Acces prin index | Complexitate (add / get / remove) | Avantaje | Dezavantaje | Când se folosește |
|---|---|---|---|---|---|---|---|---|
| ArrayList | Bazat pe array dinamic (redimensionat automat) | ✅ Ordinea inserării | ✅ Da | ✅ Da | add → amortizat O(1) get → O(1) remove → O(n) | Acces foarte rapid prin index, eficient la citire | Inserările/ștergerile în mijloc sunt lente; cost mare la redimensionare | Când ai multe citiri și parcurgeri, dar puține inserări în mijloc |
| LinkedList | listă dublu înlănțuită | ✅ Ordinea inserării | ✅ Da | ⚠️ Lent (traversare secvențială) | add/remove → O(1) la capete get → O(n) | Inserare/ștergere rapidă oriunde în listă | Acces lent la elemente; overhead de memorie mai mare | Când ai multe inserări/ștergeri, dar nu acces frecvent prin index |
Vector nu a fost inclusă, deoarece nu mai este folosită în proiectele moderne.
import java.util.*; public class ArrayListExample { public static void main(String[] args) { // ArrayList List<String> fruits = new ArrayList<>(); fruits.add("Apple"); fruits.add("Banana"); fruits.add("Apple"); // duplicatele sunt permise System.out.println(fruits); // [Apple, Banana, Apple] System.out.println("Primul element: " + fruits.get(0)); // LinkedList List<String> tasks = new LinkedList<>(); tasks.add("Wake up"); tasks.add("Make coffee"); tasks.addFirst("Stretch"); System.out.println(tasks); // [Stretch, Wake up, Make coffee] tasks.removeLast(); System.out.println(tasks); // [Stretch, Wake up] } }
Queue modelează o coadă de așteptare. Elementele sunt procesate în ordinea în care sunt adăugate (FIFO).
Caracteristici:
PriorityQueue - permite inserarea într-o coadă păstrând o ordine specificăArrayDeque - permite inserarea și ștergerea din ambele capete
ArrayDeque.LinkedList implementează interfața Deque care extinde Queue, deoarece o listă înlănțuită permite inserarea și ștergerea de la ambele capete, similar cu un ArrayDeque.
| Metodă | Descriere | Comportament la gol |
|---|---|---|
boolean offer(E e) | Încearcă să adauge element | Returnează `false` dacă nu e loc |
E poll() | Scoate și returnează elementul din față | null dacă e goală |
E peek() | Returnează elementul din față fără a-l elimina | null dacă e goală |
| Implementare | Structură internă | Ordine elemente | Permite duplicate? | Acceptă null? | Complexitate (add / poll / peek) | Avantaje | Dezavantaje | Când se folosește |
|---|---|---|---|---|---|---|---|---|
| LinkedList | Listă dublu înlănțuită | ✅ Ordinea inserării (FIFO) sau LIFO (prin Deque) | ✅ Da | ✅ Da | add → O(1) poll → O(1) peek → O(1) | Flexibilă — poate fi folosită și ca List, Queue sau Stack | Acces aleator lent (O(n)); overhead de memorie mai mare | Când ai nevoie de o coadă simplă, ușor de convertit în listă sau stivă |
| ArrayDeque | Buffer circular dinamic | ✅ Ordinea inserării (FIFO) sau LIFO | ✅ Da | ❌ Nu | add → amortizat O(1) poll → O(1) peek → O(1) | Mai rapidă decât LinkedList; foarte eficientă pentru stive și cozi | Nu este thread-safe; nu acceptă null | Când vrei o coadă sau stivă performantă într-un singur fir de execuție |
| PriorityQueue | Heap binar (min-heap) | ⚠️ Ordine bazată pe prioritate, nu pe inserare | ✅ Da | ❌ Nu | add → O(log n) poll → O(log n) peek → O(1) | Menține automat ordinea elementelor prin comparator | Nu păstrează ordinea de inserare; nu este thread-safe | Când vrei procesarea elementelor în funcție de prioritate |
Queue cum ar fi: ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue, DelayQueue, SynchronousQueue, LinkedTransferQueue, doar că cele mai folosite implementări sunt cele din tabelul de mai sus.
import java.util.*; public class Main { public static void main(String[] args) { // PriorityQueue Queue<Integer> pq = new PriorityQueue<>(); pq.add(5); pq.add(1); pq.add(3); while (!pq.isEmpty()) { System.out.println(pq.poll()); // scoate în ordine: 1, 3, 5 } // ArrayDeque (Stack) Deque<String> stack = new ArrayDeque<>(); stack.push("A"); stack.push("B"); stack.push("C"); System.out.println(stack.pop()); // C (comportament de stivă) System.out.println(stack); // [B, A] } }
Map<K, V> asociază chei unice cu valori. Este baza pentru implementarea tabelelor de asociere, cache-urilor și bazelor de date simple în memorie.
| Metodă | Descriere |
|---|---|
V put(K key, V value) | Adaugă o pereche cheie–valoare |
V get(K key) | Returnează valoarea asociată cheii |
V remove(K key) | Elimină o pereche din map |
int size() | Numărul perechilor |
put() o suprascrie și returnează vechea valoare.HashSet, implementările de tip hash (ex: HashMap, LinkedHashMap) necesită ca obiectele să implementeze metodele equals() și hashCode(). Alte implementări, cum ar fi TreeMap, folosesc ordinea naturală sau un Comparator și nu se bazează pe hashCode().
| Metodă | Returnează | Conținut |
|---|---|---|
keySet() | Set<K> | Toate cheile unice |
values() | Collection<V> | Toate valorile (pot fi duplicate) |
entrySet() | Set<Map.Entry<K,V» | Perechi cheie–valoare |
| Implementare | Structură internă | Ordine elemente | Permite duplicate? | Acceptă null? | Complexitate (put / get / remove) | Avantaje | Dezavantaje | Când se folosește |
|---|---|---|---|---|---|---|---|---|
| HashMap | Table hash cu liste înlănțuite/bucket-uri sau red-black tree după coliziuni | ❌ Nicio ordine garantată | ❌ Nu (cheile sunt unice) | ✅ Chei și valori | put → O(1) amortizat get → O(1) remove → O(1) | Foarte rapidă; implementare standard | Nu păstrează ordinea de inserare | Când ai nevoie de un map rapid, fără grijă pentru ordine |
| LinkedHashMap | HashMap + listă dublu înlănțuită pentru ordinea inserării | ✅ Ordinea inserării sau LRU dacă este activată | ❌ Nu | ✅ Chei și valori | put → O(1) amortizat get → O(1) remove → O(1) | Păstrează ordinea inserării sau accesului; utilă pentru cache LRU | Ușor mai lentă și mai multă memorie decât HashMap | Când ai nevoie de map rapid, dar ordonat după inserare sau acces |
| TreeMap | Red-Black Tree (arbore echilibrat) | ✅ Ordine naturală sau Comparator | ❌ Nu | ❌ Chei ✅ Valori | put → O(log n) get → O(log n) remove → O(log n) | Ordine sortată; suportă subseturi și navigare | Mai lentă decât HashMap; nu permite chei null | Când ai nevoie de un map sortat sau de intervale de chei |
TreeMap implementează interfețele SortedMap și NavigableMap, ceea ce îi permite să mențină cheile într-o ordine sortată și să ofere metode pentru navigarea în arbore (de exemplu: subMap(), headMap(), tailMap(), higherKey(), lowerKey()).
import java.util.*; public class MapExample { public static void main(String[] args) { // HashMap Map<String, Integer> map = new HashMap<>(); map.put("Apple", 10); map.put("Banana", 20); map.put("Cherry", 30); map.put("Apple", 40); // suprascrie valoarea pentru cheie "Apple" System.out.println("HashMap: " + map); // Iterare for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // Output HashMap (aleator pe baza metodei hashCode(): // Banana -> 20 // Apple -> 40 // Cherry -> 30 // LinkedHashMap LinkedHashMap<String, Integer> linkedMap = new LinkedHashMap<>(); linkedMap.put("Apple", 10); linkedMap.put("Banana", 20); linkedMap.put("Cherry", 30); System.out.println("LinkedHashMap: " + linkedMap); // Iterare for (Map.Entry<String, Integer> entry : linkedMap.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // Output LinkedHashMap (ordonat pe baza inserării): // Apple -> 10 // Banana -> 20 // Cherry -> 30 // TreeMap TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("Apple", 10); treeMap.put("Banana", 20); treeMap.put("Cherry", 30); System.out.println("TreeMap: " + treeMap); // Iterare for (Map.Entry<String, Integer> entry : treeMap.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // Output TreeMap (ordonat lexicografic dupa cheia String): // Apple -> 10, // Banana -> 20 // Cherry -> 30 // SubMap System.out.println("SubMap (A-C): " + treeMap.subMap("Apple", "Cherry")); // Output SubMap: SubMap (A-C): {Apple=10, Banana=20} } }
subMap(“Apple”, “Cherry”) returnează toate cheile între “Apple” (inclusiv) și “Cherry” (exclusiv).
List.of()) sunt imutabile și aruncă UnsupportedOperationException la add() sau remove().
// Listă imutabilă List<String> immutableList = List.of("Apple", "Banana", "Cherry"); immutableList.add("Orange"); // eroare // Listă mutabilă List<String> mutableList = new ArrayList<>(List.of("Apple", "Banana", "Cherry")); mutableList.add("Orange"); // funcționează
Sortarea elementelor simple (ex. String, Integer, Float etc.) într-un ArrayList este directă, dar pentru obiecte complexe, cum ar fi o listă ArrayList<Student> sortată după media anilor, avem nevoie de Comparable sau Comparator.
| Comparable | Comparator | |
|---|---|---|
| Logica de sortare | În clasa obiectului (sortare naturală) | Într-o clasă separată, permițând mai multe strategii |
| Implementare | Clasa implementează Comparable și suprascrie compareTo() | O altă clasă (sau internă) implementează Comparator și metoda compare() |
| Metoda de comparare | int compareTo(T o) – returnează negativ, zero sau pozitiv | int compare(T o1, T o2) – returnează negativ, zero sau pozitiv |
| Metoda de sortare | Collections.sort(List) | Collections.sort(List, Comparator) sau List.sort(Comparator) |
| Pachet | java.lang.Comparable | java.util.Comparator |
class Student implements Comparable<Student> { private String name; private String surname; public Student(String name, String surname) { this.name = name; this.surname = surname; } // Ordonare naturală @Override public int compareTo(Student o) { return surname.equals(o.surname) ? name.compareTo(o.name) : surname.compareTo(o.surname); } } public class Main { public static void main(String[] args) { ArrayList<Student> students = new ArrayList<>(); // Sortare folosind metoda sort() din Collections Collections.sort(students); } }
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(5, 1, 3623, 13, 7)); // Sortare descrescătoare folosind Comparator anonim Collections.sort(numbers, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); System.out.println(numbers); // [3623, 13, 7, 5, 1] // Alternativ, folosind metoda sort() din List numbers.sort(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } });
sort() din Collections sau metoda sort() din List pentru a ordona un tip de colecție.sort() din List apelează metoda de sortare din Collections care folosește un algoritm foarte eficient numit TimSort.
| Aspect | Comparable | Comparator |
|---|---|---|
| Când se folosește | Când clasa are o ordine „naturală” clară și vrem o singură metodă de sortare implicită | Când vrem sortări multiple sau dinamice sau clasa nu poate implementa Comparable |
| Exemplu | Sortarea alfabetică a unui `Student` după `surname` și `name` | Sortarea unei liste de `Student` după media anilor, apoi vârstă, apoi nume |
| Avantaje | Simplu, nu necesită clase externe | Flexibil, permite definirea mai multor strategii de sortare |
| Limitări | Poate exista o singură metodă de sortare „standard” per clasă | Necesită clasă separată sau comparator anonim |
Comparable.Comparator.
În Java, metodele hashCode() și equals() sunt fundamentale pentru colecțiile care folosesc hashing, cum ar fi HashSet, HashMap sau LinkedHashMap. Înțelegerea lor corectă este esențială pentru a evita comportamente neașteptate.
Metoda hashCode() returnează un număr întreg care reprezintă obiectul. Acest număr este folosit de colecțiile bazate pe hash pentru a determina bucket-ul în care va fi plasat obiectul. Astfel, accesul la elemente devine rapid, aproape constant în timp.
Dacă două obiecte sunt considerate egale prin equals(), hashCode-ul lor trebuie să fie identic. Dacă hashCode-ul este diferit, obiectele sunt sigur inegale.
hashCode() trebuie să fie consistentă: dacă obiectul nu se modifică, valoarea returnată trebuie să fie aceeași.hashCode() după ce obiectul a fost adăugat într-o colecție poate cauza pierdere de elemente sau comportament neașteptat.
Definirea metodelor pentru hash:
public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Student other = (Student) obj; return age == other.age && name.equals(other.name); } @Override public int hashCode() { return Objects.hash(name, age); } }
Adăugarea în HashSet:
Set<Student> students = new HashSet<>(); students.add(new Student("Alice", 20)); students.add(new Student("Alice", 20)); // Nu se adaugă, equals() returnează true System.out.println(students.size()); // Afișează 1
click dreapta pe cod → Generate… → equals() and hashCode() de unde selectăm toate câmpurile care ne interesează.hashCode() cu un algoritm diferit față de cel din Objects.hash() recomandat de IntelliJ, însă alegeți algoritmul cu grijă, astfel încât să nu penalizați performanța aplicației pentru use-case-ul aplicației voastre.
Java oferă suport nativ pentru operații aritmetice pe tipuri primitive (int, long, float, double). Pentru operații mai complexe, cum ar fi funcții trigonometrice, radăcini pătrate sau generarea de numere aleatoare, clasa java.lang.Math oferă metode statice gata de folosit. Aceasta nu poate fi instanțiată, fiind un utilitar de tip static.
Pentru aritmetica întregilor, împărțirea la zero aruncă o excepție de tip ArithmeticException. În schimb, operațiile pe numere zecimale (double/float) nu aruncă excepții, ci pot returna valori speciale precum POSITIVE_INFINITY, NEGATIVE_INFINITY sau NaN.
Tabelul de mai jos sintetizează principalele metode din clasa Math:
Tipurile primitive long și double au limite în ceea ce privește dimensiunea și precizia. Dacă ai nevoie de numere foarte mari sau de zecimale cu precizie arbitrară, poți folosi clasele BigInteger și BigDecimal din pachetul java.math.
BigInteger permite operații aritmetice pe numere întregi de orice dimensiune, fără să apară overflow.BigDecimal permite operații pe numere zecimale cu precizie controlată și este ideal pentru calcule financiare sau științifice.BigInteger b1 = new BigInteger("9223372036854775807"); BigInteger b2 = new BigInteger("2"); System.out.println(b1.add(b2)); // 9223372036854775809 BigDecimal bd1 = new BigDecimal("1.0"); BigDecimal bd2 = new BigDecimal("3"); BigDecimal result = bd1.divide(bd2, 100, BigDecimal.ROUND_UP); System.out.println(result); // 0.3333...3334 (100 cifre)
Aceste clase oferă metode pentru adunare, scădere, înmulțire, împărțire și control precis al rotunjirii.
Java include mai multe clase pentru manipularea timpului și a datelor:
java.time.LocalDate – reprezintă doar data, fără timp.java.time.LocalTime – reprezintă doar timpul, fără dată.java.time.LocalDateTime – combină data și ora.java.time.ZonedDateTime – reprezintă un moment exact într-un fus orar, luând în calcul ajustările de vară.
Crearea instanțelor se poate face fie cu valorile numerice folosind of(), fie prin parsarea stringurilor folosind parse(). De asemenea, metoda now() returnează momentul curent.
Reprezintă o dată fără timp (ex: 2023-03-31). Ideal pentru evenimente sau zile calendaristice.
Puteți crea un obiect folosind metoda now() pentru data curentă sau of() pentru o dată specifică.
| Metodă | Funcționalitate | Exemplu |
|---|---|---|
LocalDate.now() | Data curentă | LocalDate today = LocalDate.now(); |
LocalDate.of(year, month, day) | Creează o dată specifică | LocalDate piDay = LocalDate.of(2023, 3, 14); |
LocalDate.parse(String, DateTimeFormatter) | Parsează un string | LocalDate valentine = LocalDate.parse(“02/14/23”, shortUS); |
plus() / minus() | Adaugă sau scade unități de timp | today.plus(1, ChronoUnit.WEEKS); |
LocalDate today = LocalDate.now(); LocalDate reminder = today.plus(1, ChronoUnit.WEEKS); System.out.println(reminder); // o săptămână de la azi
Reprezintă o oră fără dată (ex: 07:15). Util pentru alarme sau ore de evenimente recurente.
| Metodă | Funcționalitate | Exemplu |
|---|---|---|
LocalTime.now() | Ora curentă | LocalTime now = LocalTime.now(); |
LocalTime.of(hour, minute) | Creează o oră specifică | LocalTime alarm = LocalTime.of(7, 15); |
parse(String, DateTimeFormatter) | Parsează un string | LocalTime sunset = LocalTime.parse(“2020”, military); |
LocalTime alarm = LocalTime.of(7, 15); System.out.println(alarm); // 07:15
Reprezintă data și timpul împreună, fără fus orar. Este util pentru programări și timestamp-uri locale.
| Metodă | Funcționalitate | Exemplu |
|---|---|---|
LocalDateTime.now() | Data și ora curentă | LocalDateTime now = LocalDateTime.now(); |
LocalDateTime.of(year, month, day, hour, minute) | Creează un obiect specific | LocalDateTime appointment = LocalDateTime.of(2023,5,4,7,0); |
plus() / minus() | Manipulare date și timp | appointment.plusDays(2); |
LocalDateTime appointment = LocalDateTime.of(2023,5,4,7,0); LocalDateTime nextAppointment = appointment.plusHours(3); System.out.println(nextAppointment); // 2023-05-04T10:00
Reprezintă un moment exact într-un fus orar, incluzând ajustările de vară. Este esențial când aplicația se folosește în mai multe zone.
| Metodă | Funcționalitate | Exemplu |
|---|---|---|
ZonedDateTime.now() | Data și ora curentă cu fus orar | ZonedDateTime now = ZonedDateTime.now(); |
atZone(ZoneId) | Atașează un fus orar la LocalDateTime | ZonedDateTime ny = appointment.atZone(ZoneId.of(“America/New_York”)); |
withZoneSameInstant(ZoneId) | Convertire instantanee într-un alt fus | ZonedDateTime paris = ny.withZoneSameInstant(ZoneId.of(“Europe/Paris”)); |
LocalDateTime piLocal = LocalDateTime.parse("2023-03-14T01:59"); ZonedDateTime piCentral = piLocal.atZone(ZoneId.of("America/Chicago")); ZonedDateTime piParis = piCentral.withZoneSameInstant(ZoneId.of("Europe/Paris")); System.out.println(piParis); // 2023-03-14T07:59+01:00[Europe/Paris]
Permite formatări și parsări personalizate pentru date și timp. Aceleași formate pot fi folosite și pentru parsarea string-urilor.
| Caracteristică | Funcționalitate | Exemplu |
|---|---|---|
ofPattern(String) | Creează un format personalizat | DateTimeFormatter shortUS = DateTimeFormatter.ofPattern(“MM/dd/yy”); |
format(TemporalAccessor) | Formatează o dată sau oră | shortUS.format(today); |
parse(String) | Parsează un string conform formatului | LocalDate valentine = LocalDate.parse(“02/14/23”, shortUS); |
DateTimeFormatter military = DateTimeFormatter.ofPattern("HHmm"); LocalTime sunset = LocalTime.parse("2020", military); System.out.println(sunset); // 20:20 DateTimeFormatter appointment = DateTimeFormatter.ofPattern("h:mm a MM/dd/yy z"); ZonedDateTime dentist = ZonedDateTime.parse("10:30 AM 11/01/23 EST", appointment); System.out.println(dentist); // 2023-11-01T10:30-04:00[America/New_York] LocalTime t = LocalTime.now(); System.out.println(withSeconds.format(t)); // ex: 09:17:34 System.out.println(military.format(t)); // ex: 0917
Puteți crea formate foarte variate pentru afișări personalizate folosind caracterele din DateTimeFormatter.
Clasele Instant și ChronoUnit permit manipularea timestamp-urilor precise, ideale pentru logging sau urmărirea evenimentelor:
Instant time1 = Instant.now(); Instant time2 = Instant.now(); System.out.println(time1.isAfter(time2)); // false System.out.println(time1.plus(3, ChronoUnit.DAYS)); // 3 zile mai târziu
Instant este similar cu clasa veche Date, dar mult mai consistent și integrat cu java.time.
java.util, java.text și java.time pentru mai multe utilitare care vă pot fi de folos. De exemplu, vă puteți uita la java.util.Random pentru generarea de numere aleatoare.
Map sunt HashMap (neordonat, nesortat), TreeMap (map sortat) și LinkedHashMap (map ordonat)1. În cadrul acestui exercițiu, veți implementa o clasă numită Student, care are patru membri:
Clasa Student va implementa interfața Comparable<Student>, folosită la sortări, prin implementarea metodei compareTo. În metoda compareTo, studenții vor fi comparați mai întâi după medie, apoi după numele de familie, apoi după prenume. După implementarea clasei, sortați elementele listei “students” din metoda main folosind metoda Collections.sort().
2. Adăugați lista “copyStudents” într-un PriorityQueue (cu ajutorul metodei Collection.addAll), care folosește un Comparator (utilizați constructorul PriorityQueue) sau o funcție anonimă. Elementele vor fi sortate crescător după id.
3. Suprascrieți metodele equals și hashCode în clasa Student (puteți folosi generatorul de cod din IntelliJ). După aceasta, adăugați în lista asociată studentilor din “studentMap” patru materii aleatorii. Pentru a obține materiile aleatorii, urmăriți indicațiile din codul din funcția main.
4. Extindeți clasa LinkedHashSet<Integer> cu o clasă în care se vor putea adăuga doar numere pare. Metoda add va fi suprascrisă astfel încât să nu permită adăugarea de numere impare în colecție. Efectuați aceeași operațiune și pentru clasele TreeSet și HashSet. Observați diferențele privind ordinea de inserare a elementelor între cele trei clase menționate.