Video introductiv: link
Pe parcursul laboratoarelor și temelor ați folosit structuri de date oferite de API-ul Java. În cadrul acestui laborator le vom aprofunda.
În pachetul java.util (pachet standard din JRE) există o serie de clase pe care le veti găsi folositoare. Collections Framework este o arhitectură unificată pentru reprezentarea şi manipularea colecţiilor. Ea conţine:
Colecţiile oferă implementări pentru următoarele tipuri:
Există o interfaţă, numită Collection, pe care o implementează majoritatea claselor ce desemnează colecţii din java.util. Explicaţii suplimentare găsiţi pe Java Tutorials - Collection
Exemplul de mai jos construieşte o listă populată cu nume de studenţi:
Collection names = new ArrayList(); names.add("Andrei"); names.add("Matei");
Colecţiile pot fi parcurse (element cu element) folosind:
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. for-each este foarte similar cu for
. Următorul exemplu parcurge elementele unei colecţii şi le afişează.
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[]
.
Fie următoarea porţiune de cod:
Collection c = new ArrayList(); c.add("Test"); Iterator it = c.iterator(); while (it.hasNext()) { String s = it.next(); // ERROR: next() returns an Object and it's needed an explicit cast to String String s = (String)it.next(); // OK }
Am definit o colecţie c
, de tipul ArrayList
(pe care îl vom examina într-o secţiune următoare). Apoi, am adăugat în colecţie un element de tipul String
. Am realizat o parcurgere folosind un iterator, şi am încercat obţinerea elementului nostru folosind apelul: String s = it.next()
;. Funcţia next
însă întoarce un obiect de tip Object
. Prin urmare apelul va eşua. Varianta corectă este String s = (String)it.next()
;.
Am fi putut preciza, din start, ce tipuri de date dorim într-o colecţie:
Collection<String> c = new ArrayList<String>(); c.add("Test"); c.add(2); // ERROR! Iterator<String> it = c.iterator(); while (it.hasNext()) { String s = it.next(); }
Mai multe detalii despre acest subiect găsiți in laboratorul de Genericitate
O listă este o colecţie care poate fi ordonată. Listele pot conţine elemente duplicate. Pe lângă operaţiile moştenite de la Collection
, interfaţa List conţine operaţii bazate pe poziţie (index), de exemplu: set, get, add la un index, remove de la un index.
List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Orange", "Grape")); fruits.add("Apple"); // metodă moștenită din Collection fruits.add(2, "Pear"); // [Apple, Orange, Pear, Grape, Apple] System.out.println(fruits.get(3)); // Grape fruits.set(1, "Cherry"); // [Apple, Cherry, Pear, Grape, Apple] fruits.remove(2); System.out.println(fruits); // [Apple, Cherry, Grape, Apple]
Alături de List
, este definită interfaţa ListIterator, ce extinde interfaţa Iterator
cu metode de parcurgere în ordine inversă.
List
posedă două implementări standard:
ArrayList
- implementare sub formă de vector. Accesul la elemente se face în timp constant: O(1)
LinkedList
- implementare sub formă de listă dublu înlănţuită. Prin urmare, accesul la un element nu se face în timp constant, fiind necesară o parcurgere a listei: O(n)
.Printre algoritmii implementaţi se numără:
sort
- realizează sortarea unei listebinarySearch
- realizează o căutare binară a unei valori într-o listă sortatăÎn general, algoritmii pe colecţii sunt implementaţi ca metode statice în clasa Collections.
Collection
cu clasa Collections
. Spre deosebire de prima, a doua este o clasă ce conţine exclusiv metode statice. Aici sunt implementate diverse operaţii asupra colecţiilor.
List<Integer> l = new ArrayList<Integer>(); l.add(5); l.add(7); l.add(9); l.add(2); l.add(4); Collections.sort(l); System.out.println(l);
Mai multe detalii despre algoritmi pe colecţii găsiţi pe Java Tutorials - Algoritmi pe liste
Rularea exemplului de sortare ilustrat mai sus arată că elementele din ArrayList
se sortează crescator. Ce se întâmplă când dorim să realizăm o sortare particulară pentru un tip de date complex? Spre exemplu, dorim să sortăm o listă ArrayList<Student>
după media anilor. Să presupunem că Student
este o clasă ce conţine printre membrii săi o variabilă ce reţine media anilor.
Acest lucru poate fi realizat folosind interfeţele:
Comparable | Comparator | |
Logica de sortare | Logica de sortare trebuie să fie în clasa ale cărei obiecte sunt sortate. Din acest motiv, această metodă se numeşte sortare naturală. | Logica de sortare se află într-o clasă separată. Astfel, putem defini mai multe metode de sortare, bazate pe diverse câmpuri ale obiectelor de sortat. |
Implementare | Clasa ale cărei instanţe se doresc a fi sortate trebuie să implementeze această interfaţă şi, evident, să suprascrie metoda compareTo() . | Clasa ale cărei instanţe se doresc a fi sortate nu trebuie să implementeze această interfaţă. Este nevoie de o alta clasă (poate fi şi internă) care să implementeze interfaţa Comparator . |
Metoda de comparare | int compareTo(Object o1) Această metodă compară obiectul curent (this) cu obiectul o1 şi întoarce un întreg. Valoarea întoarsă este interpretată astfel: 1. pozitiv – obiectul este mai mare decât o1 2. zero – obiectul este egal cu o1 3. negativ – obiectul este mai mic decât o1 | int compare(Object o1,Object o2) Această metodă compară obiectele o1 and o2 şi întoarce un întreg. Valoarea întoarsă este interpretată astfel: 1. pozitiv – o2 este mai mare decât o1 2. zero – o2 este egal cu o1 3. negativ – o2 este mai mic decât o1 |
Metoda de sortare | Collections.sort(List) Aici obiectele sunt sortate pe baza metodei compareTo() . | Collections.sort(List, Comparator) Aici obiectele sunt sortate pe baza metodei compare() din Comparator. |
Pachet | Java.lang.Comparable | Java.util.Comparator |
Exemplu Comparable:
public class Student implements Comparable<Student> { private String name; private String surname; public Student(String name, String surname) { this.name = name; this.surname = surname; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } @Override public int compareTo(Student o) { if (surname.equals(o.surname)) { return name.compareTo(o.name); } else { return surname.compareTo(o.surname); } } }
ArrayList<Student> students = new ArrayList<>(); // populate ArrayList with Student objects Collections.sort(students);
Exemplu implementare și folosire Comparator:
ArrayList<Integer> numbers = new ArrayList<>(); numbers.add(5); numbers.add(1); numbers.add(3623); numbers.add(13); numbers.add(7); Collections.sort(numbers, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); System.out.println(numbers); // se afișează [3623, 13, 7, 5, 1] // alternativ, putem sorta o colecție, folosind metoda sort() din interfața List, în acest mod: numbers.sort(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } });
Un Set (mulţime) este o colecţie ce nu poate conţine elemente duplicate. Interfaţa Set
conţine doar metodele moştenite din Collection
, la care adaugă restricţii astfel încât elementele duplicate să nu poată fi adăugate.
Avem trei implementări utile pentru Set:
(hash table)
; este implementarea cea mai performantă, însă nu avem garanţii asupra ordinii de parcurgere. Doi iteratori diferiţi pot parcurge elementele mulţimii în ordine diferită.HashSet
.HashSet
este că LinkedHashSet
menţine o listă dublu-înlănţuită peste toate elementele sale. Prin urmare (şi spre deosebire de HashSet
), elementele rămân în ordinea în care au fost inserate. O parcurgere a LinkedHashSet
va găsi elementele mereu în această ordine.
HashSet
, care se bazează pe o tabelă de dispersie, calculează codul de dispersie al elementelor pe baza metodei hashCode, definită în clasa Object
. De aceea, două obiecte egale, conform funcţiei equals
, trebuie să întoarcă acelaşi rezultat din hashCode
.
HashSet | LinkedHashSet | TreeSet | |
Funcționarea internă | Elementele se memorează într-o tabelă de dispersie | Elementele sunt păstrate cu ajutorul unei liste înlănțuite | Elementele se memorează într-un arbore de căutare |
Utilizarea | Se folosește când dorești să stochezi o listă de elemente fără a fi interesat de ordinea acestei memorări | Se folosește atunci când se dorește conservarea ordinii de la inserare | Se folosește când se dorește păstrarea elementelor într-o ordine stabilită cu ajutorul unui Comparator |
Ordinea | Ordinea elementelor este total aleatoare | Se conservă ordinea în care au fost introduse elementele | Se folosește ordinea stabilită cu ajutorul unui Comparator. Daca acesta nu este menționat, implicit elementele vor fi sortate crescător |
Complexitatea operațiilor | O(1) pentru toate operațiile de bază (inserare, ștergere, căutare) | O(1) pentru toate operațiile de bază (inserare, ștergere, căutare) | Deoarece este folosit un arbore în spate, operațiile se execută in O(log(N)) |
Performanța | Cel mai performant dintre cele 3 menționate | Performanța se află între cea a unui HashSet și a unui TreeSet deoarece în ciuda faptului că are complexitate O(1) la operațiile principale, folosește intern și liste înlănțuite pentru păstrarea ordinii de la inserare | Din cauza faptului că după fiecare operație de adăugare și ștergere trebuie să conserve ordinea elementelor, are cea mai proastă performanță dintre cele 3 menționate |
Compararea | Folosește equals() și hashCode() pentru a compara obiectele | Folosește equals() și hashCode() pentru a compara obiectele | Folosește compare() și compareTo() pentru a compara obiectele |
Explicaţii suplimentare găsiti pe Java Tutorials - Set.
Un Map este un obiect care mapează chei pe valori. Într-o astfel de structură nu pot exista chei duplicate. Fiecare cheie este mapată la exact o valoare. Map
reprezintă o modelare a conceptului de funcţie: primeşte
o entitate ca parametru (cheia), şi întoarce o altă entitate (valoarea).
Trei implementări pentru Map
sunt:
Particularităţile de implementare corespund celor de la Set
.
Exemplu de folosire:
class Student { String name; float avg; public Student(String name, float avg) { this.name = name; this.avg = avg; } public String toString() { return "[" + name + ", " + avg + "]"; } } public class Test { public static void main(String[] args) { Map<String,Student> students = new HashMap<String, Student>(); students.put("Matei", new Student("Matei", 4.90F)); students.put("Andrei", new Student("Andrei", 6.80F)); students.put("Mihai", new Student("Mihai", 9.90F)); System.out.println(students.get("Mihai")); // adaugăm un element cu aceeași cheie System.out.println(students.put("Andrei", new Student("", 0.0F))); // put(...) întoarce elementul vechi // si îl suprascrie System.out.println(students.get("Andrei")); // remove(...) returnează elementul șters System.out.println(students.remove("Matei")); // afișăm structura de date System.out.println(students); } }
Interfaţa Map.Entry desemnează o pereche (cheie, valoare) din map. Metodele caracteristice sunt:
O iterare obişnuită pe un map se va face în felul următor:
for (Map.Entry<String, Student> entry : students.entrySet()) System.out.println(entry.getKey() + " has the following average grade: " + entry.getValue().getAverage());
În bucla for-each
de mai sus se ascunde, de fapt, iteratorul mulţimii de perechi, întoarse de entrySet
.
Explicaţii suplimentare găsiţi pe Java Tutorials - Map.
Queue este o interfaţă ce defineşte operaţii specifice pentru cozi:
Implementări utilizate frecvente pentru Queue
:
PriorityQueue
: coadă cu priorităţi / heap
Deque este o interfaţă, care extinde interfața Queue
, ce defineşte operaţii specifice pentru cozi cu două capete, unul la început și celălalt la final. Având operații pentru ambele capete, rezultă faptul că o colecție de tip Deque poate fi folosită atât ca stivă, cât și drept coadă.
Operații specifice:
Implementări utilizate frecvente pentru Deque
:
LinkedList
: pe lângă List
, LinkedList
implementează şi Deque
(deci şi Queue
)ArrayDeque
: este mai rapidă decât LinkedList
, în caz ca este folosită drept coadăExplicaţii suplimentare găsiţi pe Java Tutorials - Queue, Deque
Vector
- operațiile prin care colecția este modificată (adăugare, ștergere) sunt sincronizate (detalii legate de sincronizări veți studia la APD, în anul 3), în timp ce operațiile la ArrayList (care este recomandat în locul lui Vector
) nu sunt sincronizate, permițând astfel programatorului să aibă mai mult control asupra operațiilor în codHashtable
- operațiile prin care colecția este modificată (adăugare, ștergere) sunt sincronizate, în timp aceste operatiile la HashMap (care este recomandat în locul lui Hashtable
) nu sunt sincronizateStack
- acesta reprezintă implementarea de operații specifice pentru stivă și extinde clasa Vector
, despre care am vorbit anterior. Colecția recomandată în locul acesteia este ArrayDeque
.
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.
Scheletul il puteți gasi pe github. Soluția trebuie încărcată pe devmind.