Curs 4
Organizarea fisierelor .java si .class
Organizarea fisierelor sursa (.java)
Organizarea fisierelor cuextensia .class
Necesitatea organizarii fisierelor
Setarea caii de cautare (CLASSPATH)
Arhive JAR
Organizarea fisierelor sursa (.java)
Orice aplicatie nebanala trebuie sa fie construita folosind o organizare ierarhica a surselor si fisierelor .class ale sale. Este recomandat ca strategia de organizare a fisierelor sursa sa respecte urmatoarele conventii:
Codul sursa al claselor si interfetelor sa se gaseasca în fisiere ale caror nume sa fie numele scurt al claselor/interfetelor si care sa aiba extenisa .java.
Atentie
Este obligatoriu ca o clasa/interfata publica sa se gaseasca într-un fisier având numele clasei(interfetei) si extenisa .java. Din acest motiv într-un fisier sursa nu pot exista doua clase publice. Pentru clasele care nu sunt publice acest lucru nu este obligatoriu ci doar recomandat. Intr-un fisier sursa pot exista oricâte clase care nu sunt publice.
Fisierele sursa trebuie sa se gaseasca în directoare care sa reflecte numele pachetelor în care se gasesc clasele si interfetele din acele fisiere sursa. Cu alte cuvinte un director va contine surse pentru clase si interfete din acelassi pachet iar numele directorului va fi chiar numele pachetului. Daca numele pachetelor sunt formate din mai multe unitati lexicale separate prin punct, atunci acestea trebuie de asemenea sa corespunda unor directoare ce vor descrie calea spre fisierele sursa ale caror clase/interfete fac parte din pachetele respective.
Vom clarifica modalitatea de organizare a fisierelor sursa ale unei aplicatii printr-un exemplu concret. Sa presupunem ca dorim crearea unui program java care sa reprezinte diverse notiuni matematice din domenii diferite cum ar fi geometrie, algebra, analiza, etc. Pentru a simplifica lucrurile sa presupunem ca dorim sa cream, clase care sa descrie urmatoarele notiuni: poligon, cerc, poliedru, sfera, grup, functie. O prima varianta ar fi sa construim câte o clasa java pentru fiecare si sa le plasam în acelasi director împreuna cu un program care sa le foloseasca, însa, având în vedere posibila extindere a aplicatiei cu noi reprezentari de notiuni matematice, aceasta abordare ar fi ineficienta.
O abordare eleganta ar fi aceea în care clasele care descriu notiuni din acelasi domeniu sa se gaseasca în pachete separate si directoare separate. Ierarhia fisierelor sursa ar fi:
/matematica
/surse
/geometrie
/plan
Poligon.java
Cerc.java
/spatiu
Poliedru.java
Sfera.java
/algebra
Grup.java
/analiza
Functie.java
Matematica.java
Clasele descrise în fisierele de mai sus trebuie declarate în pachete denumite corespunzator cu numele directoarelor în care se gasesc:
Poligon.java package geometrie.plan;
public class Poligon { . . . }
Cerc.java package geometrie.plan;
public class Cerc { . . . }
Poliedru.java package geometrie.spatiu;
public class Poliedru { . . . }
Sfera.java package geometrie.spatiu;
public class Sfera { . . . }
Grup.java package algebra;
public class Grup { . . . }
Functie.java package analiza;
public class Functie { . . . }
Matematica.java este clasa principala a aplicatiei.
Numele lung al unei clase trebuie sa descrie calea spre acea clasa în cadrul fisierelor sursa ale unei aplicatii.
Organizarea fisierelor .class
In urma compilarii fisierelor sursa vor fi generate unitati de compilare pentru fiecare clasa si interfata din fisierele sursa. Pentru fiecare clasa/interfata va fi generat un fisier cu extensia .class si cu numele clasei/interfetei respective.
Ca si la fisierele .java, un fisier .class trebuie sa se gaseasca într-o serie de directoare care sa reflecte numele pachetului din care face parte. Initial, în urma compilarii fisierele sursa si unitatile de compilare (.class) se gasesc în acelasi director, însa ele pot fi apoi organizate separat.
Revenind la exemplul de mai sus, putem avea urmatoarea organizare:
/matematica
/clase
/geometrie
/plan
Poligon.class
Cerc.class
/spatiu
Poliedru.class
Sfera.class
/algebra
Grup.class
/analiza
Functie.class
Matematica.class
Crearea acestei structuri ierarhice poate fi facuta automat de catre compilator. In directorul aplicatiei (matematica) cream subdirectorul clase si dam comanda:
javac -sourcepath surse surse/Matematica.java -d clase
sau
javac -classpath surse surse/Matematica.java -d clase
Necesitatea organizarii fisierelor
Organizarea fisierelor sursa este necesara deoarece în momentul când compilatorul întâlneste un nume de clasa el trebuie sa poata identifica acea clasa, ceea ce înseamna ca trebuie sa gaseasca fiserul sursa care o contine.
Similar, fisierele .class sunt organizate astfel pentru a da posibilitatea interpretorului sa gaseasca o anumita clasa în timpul executiei programului. Insa aceasta organizare nu este suficienta deoarece specifica numai partea finala din calea catre fisierele .java si .class : /matematica/clase/geometrie/plan/Poligon.class. Pentru aceasta, atât la compilare cât si la interpretare trebui specificata lista de directoare în care se gasesc fisierele aplicatiei. Aceasta lista se numeste cale de cautare (classpath).
Definitie
O cale de cautare este o lista de directoare sau arhive în care vor fi cautate fisierele necesare unei aplicatii. Fiecare director din calea de cautare este directorul imediat superior structurii de directoare formate de organizarea claselor în directoare corespunzatoare pachetelor, astfel încât compilatorul si interpretorul sa poata construi calea completa spre clasele aplicatiei. Implicit calea de cautare este formata doar din directorul curent.
Sa consideram clasa principala a aplicatiei Matematica.java:
import geometrie.plan.*;
import algebra.Grup;
import analiza.Functie;
public class Matematica {
public static void main(String args[]) {
Poligon a = new Poligon();
geometrie.spatiu.Sfera = new geometrie.spatiu.Sfera();
//...
}
}
Identificarea unei clase referite în program se face în felul urmator:
La directoarele aflate în calea de cautare se adauga subdirectoarele specificate în import sau în numele lung al clasei
In directoarele formate este cautat un fisier cu numele clasei. In cazul în care nu este gasit nici unul sau sunt gasite mai multe va fi semnalata o eroare.
Setarea caii de cautare (CLASSPATH)
Se poate face în doua modalitati:
Setarea variabile de mediu CLASSPATH (nerecomandat)
UNIX:
SET CLASSPATH = cale1 : cale2 : ...
Recomandat sa apara si directorul curent .
DOS shell (Windows 95/NT):
SET CLASSPATH = cale1 ; cale2 ; ...
Recomandat sa apara si directorul curent .
Folosirea optiunii -classpath la compilarea si interpretarea programelor
javac - classpath <cale de cautare> <fisier.java>
java - classpath <cale de cautare> <fisier.class>
Lansarea în executie a aplicatiei noastre, din directorul aplicatiei, s-ar putea face astfel:
java -classpath clase Matematica.java
O organizare eficienta a fisierelor aplicatiei ar arata astfel:
/matematica
/surse
/clase
compile.bat (javac -sourcepath surse surse/Matematica.java -d clase)
run.bat (java -classpath clase Matematica.java)
Arhive JAR (Java ARchive)
Definitie
Sunt arhive în format ZIP folosite pentru compresarea mai multor fisere în unul singur. Diferenta consta în faptul ca un fisier JAR contine, pe lânga fiserele arhivate, si un director denumit META-INF, ce contine diverse informatii auxiliare.
Un fisier JAR poate fi creat folosind utilitarul jar sau metode ale pachetului java.util.jar.
Beneficii
portabilitate - este singurul format de arhivare independent de platforma
compresarea fisierelor est optimizata pentru fisiere de tip class
minimizarea timpului de incarcare a unui applet : daca appletul (fisiere class, resurse, etc) este compresat intr-o arhiva jar, el poate fi incarcat intr-o singura tranzactie HTTP, fara a fi deci nevoie de a se deschide o conexiune noua pt. fiecare fisier.
securitate - arhivele JAR pot fi "semnate" electronic
mecanismul pentru lucrul cu fisiere JAR este parte integrata a platformei Java.
Folosirea utilitarului jar
Arhivatorul jar se gaseste în subdirectorul bin al directorului în care este instalat mediul Java. In tabelul de mai jos sunt sumarizate operatiile principale:
Operatie Comanda
Crearea unei arhive jar cf nume-arhiva fisier(e)-intrare
Vizualizare continutului unei arhive jar tf nume-arhiva
Extragerea continutului unei arhive jar xf nume-arhiva
Extragerea doar a unor fisiere dintr-o arhiva jar xf nume-arhiva fisier(e)-arhivate
Executarea unei aplicatii împachetate într-un fisier jar ( JDK 1.1) jre -cp app.jar ClasaPrincipala
Executarea unei aplicatii împachetate într-un fisier jar ( JDK 1.2)
Alicatia trebuie compresata in asa fel incat interpretorul sa stie unde se gaseste clasa principala java -jar app.jar
Deschiderea într-un browser a unui applet compresat într-un fisier jar <applet code=NumeClasaApplet.class
archive="NumeArhiva.jar"
width=latime height=înaltime>
</applet>
Exemple:
arhivarea a doua fisiere class: jar cf classes.jar A.class B.class
arhivarea tuturor fisierelor din directorul curent: jar cvf allfiles.jar *
Executarea aplicatiilor împachetate într-o arhiva JAR
Pentru a rula o aplicatie împachetata într-o arhiva JAR trebuie sa facem cunoscuta interpretorului numele clasei principale a aplicatiei. Sa consideram urmatorul exemplu, în care dorim sa arhivam clasele : GraphEditor.class, Graph.class, Edge.class, Vertex.class , clasa principala fiind GraphEditor. Vom scrie:
jar cvfm editor.jar *.class
In urma acestei comenzi vom obtine arhiva editor.jar. Daca vom încerca sa lansam în executie aceasta arhiva prin comanda java -jar editor.jar vom obtine urmatoarea eroare:
"Failed to load Main-Class manifest from editor.jar"
. Aceasta înseamna ca în fiserul Manifest.mf ce se gaseste în directorul META-INF trebuie sa înregistram clasa principala a aplicatiei. Acest lucru îl vom face în doi pasi:
se creeaza un fisier cu un nume oarecare (ex: mymanifest ) în care vom scrie:
Main-Class : GraphEditor.java
adaugam aceasta completare la fisierul manifest al arhivei editor.jar:
jar cvfm editor.jar mymanifest
Ambele operatii puteau fi executate într-un singur pas:
jar cvfm editor.jar mymanifest *.class
Fisiere JAR executabile
Pe sistemele Win32, platforma Java 2 va asocia extensiile .jar cu interpretorul java, ceea ce înseamna cs facând dublu-click pe o arhiva jar va fi lansata în executie aplicatia împachetata în acea arhiva (daca exista o clasa principala).
Curs 4
Colectii
Ce sunt colectiile ?
Interfetele de baza care descriu colectii
Collection
Set
List
Map
SortedSet
SortedMap
Implementari ale colectiilor
Folosirea eficienta a colectiilor
Algoritmi
Iteratori si enumerari
Exemplu: gestionarea angajatilor unei companii
Ce sunt colectiile ?
Definitie
O colectie este un obiect care grupeaza mai multe elemente într-o singura unitate. Prin colectii vom avea acces la tipuri de date cum ar fi vectori, multimi, tabele de dispersie, etc. Colectiile sunt folosite pentru memorarea si manipularea datelor, precum si pentru transmiterea datelor de la o metoda la alta.
In Java colectiile sunt tratate intr-o maniera unitara, fiind organizate intr-o arhitectura ce cuprinde:
Interfete: tipuri abstracte de date ce descriu colectiile. Interfetele permit utilizarea colectiilor independent de detaliile implementarilor.
Implementari: implementari concrete ale interfetelor ce descriu colectii. Aceste clase reprezinta tipuri de date reutilizabile.
Algoritmi: metode care efectueaza diverse operatii utile cum ar fi cautarea, sortarea definite pentru obiecte ce implementeaza interfete ce descriu colectii. Acesti algoritmi se numesc si polimorfici deoarece aceeasi metoda poate fi folosita pe implementari diferite ale unei colectii. Aceste tipuri de algoritmii descriu notiunea de functionalitate reutilizabila.
Dintre avantajele oferite de utilizarea colectiilor amintim:
Reducerea efortului de programare: prin punerea la dispozitia programatorului a unui set de tipuri de date si algoritmi ce modeleaza structuri si operatii des folosite in aplicatii.
Cresterea vitezei si calitatii programului: implementarile efective ale colectiilor sunt de inalta performanta si folosesc algoritmi cu timp de lucru optim. In scrierea unei aplicatii putem sa ne concentram eforturile asupra problemei in sine si nu asupra modului de reprezentare si manipulare a informatiilor.
Interfete ce descriu colectii
Interfetele ce descriu colectii reprezinta nucleul mecanismului de lucru cu colectii. Scopul acestor interfete este de a permite utilizarea colectiilor independent de modul lor de implementare. Ierarhia lor este prezentata in imaginea de mai jos:
Mai jos sunt descrise structurile de date modelate de aceste interfete:
Collection
Collection descrie un grup de obiecte numite si elementele sale. Unele implementari ale acestei interfete permit existenta elementelor duplicate, alte implementari nu. Unele au elementele ordonate, altele nu. Modeleaza o colectie la nivelul cel mai general. In JDK nu exista nici o implementare directa a acestei interfete, ci exista doar implementari ale unor subinterfete mai concrete cum ar fi Set sau List.
Set
Modeleaza notiunea de multime în sens matematic. O multime nu poate avea elemente duplicate.
List
Descrie liste (secvente) de elemente indexate. Listele pot contine duplicate si permit un control precis asupra pozitiei unui element prin intermediul indexului acelui element.
O clasa independenta ce implementeaza o functionalitate asemanatoare este clasa Vector.
Map
Implementarile acestei interfete sunt obiecte ce asociaza fiecarui element o cheie unica. Nu pot contine asadar chei duplicate si fiecare chei este asociata la un singur element.
O clasa independenta ce implementeaza o functionalitate asemanatoare este clasa HashTable.
SortedSet
Este asemanatoare cu interfata Set la care se adauga faptul ca elementele dintr-o astfel de colectie sunt ordonate ascendent. Pune la dispozitie operatii care beneficiaza de avantajul ordonarii elementelor.
SortedMap
Este asemanatoare cu interfata Map la care se adauga faptul ca multimea cheilor dintr-o astfel de colectie este ordonata ascendent.
Interfata Collection
Interfata Collection modeleaza un grup de obiecte numite elemente. Scopul acestei interfete este de a folosi colectii la un nivel de maxima generalitate. In definitia interfetei vom observa ca metodele se impart in trei categorii.
public interface Collection {
// Operatii de baza la nivel de element
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(Object element); // Optional
boolean remove(Object element); // Optional
Iterator iterator();
// Operatii la nivel de colectie
boolean containsAll(Collection c);
boolean addAll(Collection c); // Optional
boolean removeAll(Collection c); // Optional
boolean retainAll(Collection c); // Optional
void clear(); // Optional
// Operatii de conversie in vector
Object[] toArray();
Object[] toArray(Object a[]);
}
Interfata Set
Modeleaza notiunea de multime în sens matematic. O multime nu poate avea elemente duplicate. Defineste aceleasi metode ca interfata Collection. Doua dintre clasele care ofera implementari concrete ale acestei interfete sunt HashSet si TreeSet. (vezi "Implementari")
Interfata List
Interfata List descrie liste (secvente) de elemente indexate. Listele pot contine duplicate si permit un control precis asupra pozitiei unui element prin intermediul indexului acelui element. In plus fata de elementele definite de interfata Collection avem metode pentru:
acces pozitional
cautare (aflarea indexului unui element)
iterare ordonata
extragerea unei subliste
Definitia interfetei este data mai jos:
public interface List extends Collection {
// Acces pozitional
Object get(int index);
Object set(int index, Object element); // Optional
void add(int index, Object element); // Optional
Object remove(int index); // Optional
abstract boolean addAll(int index, Collection c); // Optional
// Cautare
int indexOf(Object o);
int lastIndexOf(Object o);
// Iterare
ListIterator listIterator();
ListIterator listIterator(int index);
// Extragere sublista
List subList(int from, int to);
}
Doua clase care implementeaza interfata List sunt ArrayList si Vector.
Interfata Map
Implementarile acestei interfete sunt obiecte ce asociaza fiecarui element o cheie unica. Nu pot contine asadar chei duplicate si fiecare chei este asociata la un singur element.
Definitia interfetei este data mai jos:
public interface Map {
// Operatii de baza la nivel de element
Object put(Object key, Object value);
Object get(Object key);
Object remove(Object key);
boolean containsKey(Object key);
boolean containsValue(Object value);
int size();
boolean isEmpty();
// Operatii la nivel de colectie
void putAll(Map t);
void clear();
// Vizualizari ale colectiei
public Set keySet();
public Collection values();
public Set entrySet();
// Interfata pentru manipularea unei inregistrari
public interface Entry {
Object getKey();
Object getValue();
Object setValue(Object value);
}
}
Clase care implementeaza interfata Map sunt HashMap, TreeMap si Hashtable.
Interfata SortedSet
Este asemanatoare cu interfata Set la care se adauga faptul ca elementele dintr-o astfel de colectie sunt ordonate ascendent conform ordinii lor naturale, sau conform cu ordinea data de un comparator specificat la crearea colectiei.
Este subclasa a interfetei Set, oferind metode suplimentare pentru:
extragere de subliste
aflarea primului/ultimului element din lista
aflarea comparatorului folosit pentru ordonare
Definitia interfetei este data mai jos:
public interface SortedSet extends Set {
// Subliste
SortedSet subSet(Object fromElement, Object toElement);
SortedSet headSet(Object toElement);
SortedSet tailSet(Object fromElement);
// Capete
Object first();
Object last();
Comparator comparator();
}
Interfata SortedMap
Este asemanatoare cu interfata Map la care se adauga faptul ca multimea cheilor dintr-o astfel de colectie este ordonata ascendent conform ordinii naturale, sau conform cu ordinea data de un comparator specificat la crearea colectiei.
Este subclasa a interfetei Map, oferind metode suplimentare pentru:
extragere de subtabele
aflarea primei/ultimei chei
aflarea comparatorului folosit pentru ordonare
Definitia interfetei este data mai jos:
public interface SortedMap extends Map {
SortedMap subMap(Object fromKey, Object toKey);
SortedMap headMap(Object toKey);
SortedMap tailMap(Object fromKey);
Object first();
Object last();
Comparator comparator();
}
Implementari ale colectiilor
Clasele de baza care implementeaza interfete ce descriu colectii sunt prezentate in tabelul de mai jos. Numele lor este de forma <Implementare><Interfata>, unde 'implementare' se refera la structura de date folosita.
Implementari
Interfete Set HashSet TreeSet
List ArrayList LinkedList
Map HashMap TreeMap
JDK 1.2 furnizeaza câte doua clase ce implementeaza fiecare tip de colectie, în fiecare caz prima implementare fiind cea de baza, care va fi in general folosita. Acestea sunt: HashSet, ArrayList si HashMap.
Clasele care descriu colectii au multe trasaturi comune cum ar fi:
permit elementul null
sunt serializabile
au definita metoda clone
au definita metoda toString, care returneaza o reprezentare ca sir de caractere a colectiei respective
permit crearea iteratorilor pentru parcurgere
implementarea interfetelor este indirecta, în sensul ca au o
Implementarea interfetelor este indirecta în sensul ca aceste clase au superclase abstracte care ofera implementari concrete pentru majoritatea metodelor definite de interfete. Cele mai importante superclase sunt AbstractCollection si AbstractMap, din care sunt apoi extinse clasele abstracte AbstractList si AbstractSet,respectiv AbstractMap.
Clasele prezentate in tabelul de mai sus sunt extensii concrete ale claselor abstracte aminitite.
Singura interfata care nu are nici o implementare este Collection.
Folosirea eficienta a colectiilor
Dupa cum am vazut, fiecare interfata ce descrie o colectie are câte doua implementari, dintre care una este de baza, fiind folosita in 90% din cazuri. De exemplu, interfata List este implementata de clasele ArrayList si LinkedList, prima fiind cea mai folosita. De ce exista atunci si clasa LinkedList ?
Raspunsul consta in faptul ca implementari diferite ale interfetelor pot oferi performante mai bune in functie de situatie, prin realizarea unor compromisuri între spatiul necesar pentru reprezentarea datelor, rapiditatea regasirii acestora si timpul necesar actualizarii colectiei in cazul unor modificari.
Sa consideram urmatoarele exemple ce creeaza o lista folosind ArrayList, respectiv LinkedList si executa diverse operatii pe ea.
//exemplul 1
import java.util.*;
public class List1 {
public static void main(String(args[]) {
List lst = new ArrayList();
//List lst = new LinkedList();
final int N = 25000;
for(int i=0; i < N; i++)
lst.add(new Integer(i));
//*
}
}
//exemplul 2 - List2
Adaugam la exemplul 1 (*) urmatoarea secventa
for(int i=0; i < N; i++)
lst.get(i);
//exemplul 3 - List3
Adaugam la exemplul 1 (*) urmatoarea secventa
for(int i=0; i < N; i++)
lst.remove(0);
Timpii aproximativi de rulare a acestor programe sunt dati in tabelul de mai jos:
ArrayList LinkedList
List1 (add) 0.4 0.4
List2 (get) 0.4 21.3
List3 (remove) 6.9 0.4
Asadar, adaugarea elementelor este rapida pentru ambele tipuri de liste. ArrayList ofera acces in timp constant la elementele sale si din acest motiv folosirea lui "get" este rapida, în timp ce pentru LinkedList este extrem de lenta, deoarece intr-o lista inlantuita accesul la un element se face prin parcurgerea secventiala a listei pâna la elementul respectiv.
La eliminarea elementelor din lista folosirea lui ArrayList este lenta deoarece elementele ramase sufera un proces de reindexare (shift la stânga) in timp ce pentru LinkedList este rapida si se face prin simpla schimbare a unor legaturi.
Deci, ArrayList se comporta bine pentru cazuri in care avem nevoie de regasirea unor elemente la pozitii diferite in lista, iar LinkedList functioneaza optim atunci când facem multe operatii de editare(stergeri, inserari) în corpul listei.
De asemenea, ArrayList foloseste mai eficient spatiul de memerie decât LinkedList, deoarece aceasta din urma are nevoie de o structura de date auxiliara pentru memorare unui nod. Nodurile sunt reprezentate prin instante ale unei clase interne, având forma:
class Entry {
Object element;
Entry next;
Entry previous;
}
Concluzia nu este ca una din aceste clase este mai "buna" decât cealalta, ci ca exista diferente substantiale in reprezentarea si comportamentul diferitelor implementari ale colectiilor si ca alegerea unei clase pentru reprezentarea unei colectii trebuie sa se faca în functie de natura problemei ce trebuie rezolvata. Acest lucru este valabil pentru toate tipurile de colectii. De exemplu, HashSet si TreeSet sunt doua modalitati de reprezentare a multimilor. Prima se bazeaza pe folosirea unei tabele de dispersie, a doua pe folosirea unei structuri arborescente.
Algoritmi
Algorimtii polimorfici descrisi în aceasta sectiune sunt metode definite în clasa Collections care permit efectuarea unor operatii utile cum ar fi cautarea, sortarea,etc. Caracterisiticile principale ale algoritmilor sunt:
sunt metode statice
au un singur argument de tip colectie
apelul lor va fi de forma Collections.algoritm([colectie])
majoritatea opereaza pe liste dar si pe colectii arbitrare
Metodele mai importante din clasa Collections sunt date in tabelul de mai jos:
sort Sorteaza ascendent o lista referitor la ordinea sa naturala sau la ordinea data de un comparator
shuffle Amesteca elementele unei liste - opusul lui sort
binarySearch Efectueaza o cautare binara a unui element într-o lista ordonata
reverse Inverseaza ordinea elementelor dintr-o lista
fill Populeaza o lista cu un element
copy Copie elementele unei liste in alta
min Returneaza minimul dintr-o colectie
max Returneaza maximul dintr-o colectie
enumeration Returneaza o enumerare a elementelor dintr-o colectie
Iteratori si enumerari
Enumerarile si iteratorii descriu modalitati pentru parcurgerea secventiala a unei colectii. Ei sunt descrisi de obiecte ce implementeaza interfetele Enumeration, respectiv Iterator sau ListIterator. Toate clasele care implementeaza colectii au metode ce returneaza o enumerare sau un iterator pentru parcurgerea elementelor lor. Metodele acestor interfete sunt date in tabelul de mai jos, semnificatiile lor fiind evidente:
Enumeration Iterator ListIterator
boolean hasMoreElements()
Object nextElement() boolean hasNext()
Object next()
void remove() boolean hasNext(),hasPrevious()
Object next(), previous()
void add(Object o) void remove()
void set(Object o)
Iteratorii simpli permit eliminarea elementului curent din colectia pe care o parcurg, cei ordonati (de tip ListIterator) permit si inserarea unui element la pozitia curenta, respectiv modificarea elementului curent.
Iteratorii sunt preferati enumerarilor datorita posibilitatii lor de a actiona asupra colectiei pe care o parcurg prin metode de tip remove, add, set dar si prin faptul ca denumirile metodelor sunt mai concise.
In exemplul de mai jos punem într-un vector numerele de la 1 la 10, le amestecam, dupa care le parcurgem element cu element folosind un iterator.
import java.util.*;
class TestIterator{
public static void main(String args[]) {
ArrayList a = new ArrayList();
for(int i=0; i<10; i++) a.add(new Integer(i));
Collections.shuffle(a); //amestecam elementele colectiei
System.out.println("Vectorul amestecat: " + a);
System.out.println("Parcurgem vectorul element cu element:");
System.out.println("\nvarianta 1: cu while");
Iterator it = a.iterator();
while (it.hasNext())
System.out.print(it.next() + " ");
System.out.println("\nvarianta 2: cu for");
for(it=a.iterator(); it.hasNext(); )
System.out.print(it.next() + " ");
}
}
Exemplu
In exemplul de mai jos vom folosi clasa HashMap pentru a tine evidenta angajatilor unei companii. Vom folosi mecanismul serializarii pentru salvarea informatiilor intr-un fisier, respectiv pentru restaurarea lor.
//clasa Angajat
import java.io.Serializable;
class Angajat implements Serializable {
String cod;
String nume;
int salariu;
public Angajat(String cod, String nume, int salariu) {
this.cod=cod;
this.nume=nume;
this.salariu=salariu;
}
public String toString() {
return cod + "\t" + nume + "\t" + salariu;
}
}
//clasa Personal
import java.io.*;
import java.util.*;
class Personal implements Serializable {
HashMap personal = new HashMap();
String fisier=null;
boolean salvat=false;
void load(String fis) throws IOException{
ObjectInputStream in=null;
this.fisier=fis;
try {
in=new ObjectInputStream(new FileInputStream(fisier));
personal = (HashMap)in.readObject();
} catch(FileNotFoundException e) {
System.out.println("Fisierul " + fisier + " nu exista!");
} catch(ClassNotFoundException e) {
System.out.println("Eroare la incarcarea datelor!");
}finally {
if (in != null)
in.close();
}
}
void saveAs(String fis) throws IOException{
ObjectOutputStream out=null;
try {
out=new ObjectOutputStream(new FileOutputStream(fis));
out.writeObject(personal);
salvat=true;
System.out.println("Salvare reusita in fisierul " + fis);
}catch(IOException e) {
System.out.println("Salvarea nu a reusit!");
}finally {
if (out != null)
out.close();
}
}
void save() throws IOException {
if (fisier == null) fisier="personal.txt";
saveAs(fisier);
}
Angajat getAngajat(String argumente) {
String cod="", nume="";
int salariu=0;
try {
StringTokenizer st=new StringTokenizer(argumente);
cod = st.nextToken();
nume = st.nextToken();
salariu = Integer.parseInt(st.nextToken());
}catch(NoSuchElementException e) {
System.out.println("Argumente incomplete!");
}catch(NumberFormatException e) {
System.out.println("salariul trebuie sa fie numeric!");
}
return new Angajat(cod, nume, salariu);
}
boolean add(String argumente) {
Angajat a=getAngajat(argumente);
if (personal.containsKey(a.cod)) {
System.out.println("Mai exista un angajat cu acest cod!");
return false;
}
personal.put(a.cod, a);
salvat=false;
return true;
}
boolean delete(String cod) {
if (personal.remove(cod) == null) {
System.out.println("Nu exista nici un angajat cu acest cod!");
return false;
}
salvat=false;
return true;
}
void update(String argumente) {
Angajat a=getAngajat(argumente);
delete(a.cod);
add(argumente);
}
void list() {
Iterator it=personal.values().iterator();
while (it.hasNext()) System.out.println((Angajat)(it.next()));
}
void executaComenzi() {
String linie, comanda, argumente;
try {
BufferedReader stdin=new BufferedReader(new InputStreamReader(System.in));
while (true) {
linie = stdin.readLine().trim();
StringTokenizer st=new StringTokenizer(linie);
comanda=st.nextToken();
argumente="";
while (st.hasMoreTokens()) argumente += st.nextToken() + " ";
argumente=argumente.trim();
if (comanda.startsWith("exit")) break;
else if (comanda.startsWith("add")) add(argumente);
else if (comanda.startsWith("del")) delete(argumente);
else if (comanda.startsWith("update")) update(argumente);
else if (comanda.startsWith("list")) list();
else if (comanda.startsWith("load")) load(argumente);
else if (comanda.startsWith("saveAs")) saveAs(argumente);
else if (comanda.startsWith("save")) save();
else System.out.println("what ?");
}
if (!salvat) save();
System.out.println("bye...");
}catch (IOException e) {
System.out.println("Eroare I/O:" + e);
e.printStackTrace();
}
}
}
//clasa principala GestiuneAngajati
public class GestiuneAngajati {
public static void main(String args[]) {
Personal p = new Personal();
p.executaComenzi();
}
}
Curs 4
Interfete
Ce este o interfata ?
Definirea unei interfete
Implementarea unei interfete
Exemplu de interfata
Diferente între o interfata si o clasa abstracta
Mostenire multipla prin intermediul interfetelor
Utilitatea interfetelor
Crearea grupurilor de constante
Transmiterea metodelor ca parametri (call-back)
Interfata FilenameFilter
Ce este o interfata ?
Interfetele duc conceptul de clasa abstracta cu un pas înainte prin eliminarea oricarei implementari a metodelor, punând în practica unul din conceptele POO de separare a modelului unui obiect (interfata) de implementarea sa. Asadar, o interfata poate fi privita ca un protocol de comunicare între obiecte.
O interfata Java defineste un set de metode dar nu specifica nici o implementare pentru ele. O clasa care implementeaza o interfata trebuie obligatoriu sa specifice implementari pentru toate metodele interfetei, supunându-se asadar unui anumit comportament.
Definitie
O interfata este o colectie de metode fara implementare si declaratii de constante
Definirea unei interfete
Definirea unei interfete se face prin intermediul cuvântului cheie interface:
[public] interface NumeInterfata
[extends SuperInterfata1 [,extends SuperInterfata2...]]
{
//corpul interfetei:constane si metode abstracte
}
O interfata poate avea un singur modificator: public. O interfata publica este accesibila tuturor claselor indiferent de pachetul din care fac parte. O interfata care nu este publica este accesibila doar claselor din pachetul din care face parte interfata.
O clasa poate extinde oricâte interfete. Acestea se numesc superinterfete si sunt separate prin virgula (vezi "Mostenirea multipla prin intermediul interfetelor").
Corpul unei interfete contine:
constante: acestea pot fi sau nu declarate cu modificatorii public, static si final care sunt impliciti; nici un alt modificator nu poate aparea în declaratia unei variabile a unei interfete Constantele dintr-o interfata trebuie obligatoriu initializate.
interface NumeInterfata {
int MAX = 100; //echivalent cu
public static final MAX = 100;
int MAX; //ilegal - fara initializare
private int x = 1; //ilegal
}
metode fara implementare: acestea pot fi sau nu declarate cu modificatorul public care este implicit; nici un alt modificator nu poate aparea în declaratia unei metode a unei interfete.
interface NumeInterfata {
void metoda(); //echivalent cu
public void metoda();
protected void metoda2(); //ilegal
Atentie
Variabilele unei interfete sunt implicit publice chiar daca nu sunt declarate cu modificatorul public.
Variabilele unei interfete sunt implicit constante chiar daca nu sunt declarate cu modificatorii static si final.
Metodele unei interfete sunt implicit publice chiar daca nu sunt declarate cu modificatorul public.
In variantele mai vechi de Java era permis si modificatorul abstract în declaratia interfetei si în declaratia metodelor, însa a fost eliminat deoarece atât interfata cât si metodele sale sunt implicit abstracte.
Implementarea unei interfete
Se face prin intermediul cuvântului cheie implements:
class NumeClasa implements NumeInterfata sau
class NumeClasa implements Interfata1, Interfata2...
O clasa poate implementa oricâte interfete. (vezi "Mostenirea multipla prin intermediul interfetelor").
O clasa care implementeaza o interfata trebuie obligatoriu sa specifice cod pentru toate metodele interfetei. Din acest motiv, odata creata si folosita la implementarea unor clase, o interfata nu mai trebuie modificata , în sensul ca adaugarea unor metode noi sau schimbarea signaturii metodelor existente va duce la erori în compilarea claselor care o implementeaza.
Modificarea unei interfete implica modificarea tuturor claselor care implementeaza acea interfata!
Implementarea unei interfete poate sa fie si o clasa abstracta.
Exemplu de interfata
interface Instrument {
//defineste o metoda fara implementare
void play();
}
class Pian implements Instrument {
//clasa care implementeaza interfata
//trebuie obligatoriu sa implementeze metoda play
public void play() {
System.out.println("Pian.play()");
}
}
class Vioara implements Instrument {
//clasa care implementeaza interfata
//trebuie obligatoriu sa implementeze metoda play
public void play() {
System.out.println("Vioara.play()");
}
}
public class Muzica { //clasa principala
static void play(Instrument i) {
//metoda statica care porneste un instrument generic
//ce implementeaza interfata Instrument
i.play();
}
static void playAll(Instrument[] e) {
for(int i = 0; i < e.length; i++)
play(e[i]);
}
public static void main(String[] args) {
Instrument[] orchestra = new Instrument[2];
int i = 0;
orchestra[i++] = new Pian();
orchestra[i++] = new Vioara();
playAll(orchestra);
}
}
Se observa ca folosind interfata Instrument putem adauga noi clase de instrumente fara a schimba codul metodelor play si playAll din clasa principala întrucât acestea primesc ca parametru un instrument generic.
Atentie
O interfata nu este o clasa, dar orice referinta la un obiect de tip interfata poate primi ca valoare o referinta la un obiect al unei clase ce implementeaza interfata respectiva (upcast). Din acest motiv interfetele pot fi privite ca tipuri de date.
Diferente între o interfata si o clasa abstracta
La prima vedere o interfata nu este altceva decât o clasa abstacta în care toate metodele sunt abstracte (nu au nici o implementare). Asadar o clasa abstracta nu ar putea înlocui o interfata ?
Raspunsul la intrebare este Nu. Deosebirea consta în faptul ca unele clase sunt fortate sa extinda o anumita clasa (de exemplu orice applet trebuie sa fie subclasa a clasei Applet) si nu ar mai putea sa extinda o clasa abstracta deoarece în Java nu exista decât mostenire simpla. Fara folosirea interfetelor nu am putea forta clasa respectiva sa respecte un anumit protocol.
La nivel conceptual diferenta consta în:
extinderea unei clase abstracte forteaza o relatie între clase
implementarea unei interfete specifica doar necesitatea implementarii unor anumie metode
Mostenire multipla prin intermediul interfetelor
Interfetele nu au nici o implementare si nu ocupa spatiu de memorie la instantierea lor. Din acest motiv nu reprezinta nici o problema ca anumite clase sa implementeze mai multe interfete sau ca o interfata sa extinda mai multe interfete (sa aiba mai multe superinterfete)
class NumeClasa implements Interfata1, Interfata2, ...
interface NumeInterfata extends Interfata1, Interfata2, ...
O interfata mosteneste atât constantele cât si declaratiile de metode de la superinterfetele sale. O clasa mosteneste doar constantele unei interfete.
Exemplu de clasa care implementeaza mai multe interfete:
interface Inotator {
void inoata();
}
interface Zburator {
void zboara();
}
class Luptator {
public void lupta() {}
}
class Erou extends Luptator implements Inotator, Zburator {
public void inoata() {}
public void zboara() {}
}
Exemplu de interfata care extinde mai multe interfete :
interface Monstru {
void ameninta();
}
interface MonstruPericulos extends Monstru {
void distruge();
}
interface Mortal {
void omoara();
}
interface Vampir extends MonstruPericulos, Mortal {
void beaSange();
}
class Dracula implements Vampir {
public void ameninta() {}
public void distruge() {}
public void omoara();
public void beaSange() {}
}
Atentie
O clasa nu poate avea decât o superclasa
O clasa poate implementa oricâte interfete
O clasa mosteneste doar constantele unei interfete
O clasa nu poate mosteni implementari de metode dintr-o interfata
Ierarhia interfetelor este independenta de ierarhia claselor care le implementeaza
Utilitatea interfetelor
O interfata defineste un protocol ce poate fi implementat de orice clasa, indiferent de ierarhia de clase din care face parte. Interfetele sunt utile pentru:
definirea unor similaritati între clase independente fara a forta artificial o legatura între ele.
asigura ca toate clasele care implementeaza o interfata pun la dipozitie metodele specificate în interfata; de aici rezulta posibilitatea implementarii unitare a unor clase prin mai multe modalitati.
specificarea metodelor unui obiect fara a deconspira implementarea lor (aceste obiecte se numesc anonime si sunt folosite la livrarea unor pachete cu clase catre alti programatori: acestia pot folosi clasele respective dar nu pot vedea implementarile lor efective)
definirea unor grupuri de constante
transmiterea metodelor ca parametri (tehnica Call-Back) (vezi "Transmiterea metodelor ca parametri").
Crearea grupurilor de constante
Deoarece orice variabila a unei interfete este implicit declarata cu public, static si final interfetele reprezinta o metoda convenabila de creare a unor grupuri de constante, similar cu enum din C++.
public interface Luni {
int IAN=1, FEB=2, ..., DEC=12;
}
Folosirea acestor constante se face prin expresii de genul NumeInterfata.constanta :
if (luna < Luni.DEC)
luna ++
else
luna = Luni.IAN;
Transmiterea metodelor ca parametri (call-back)
Transmiterea metodelor ca parametri se face în C++ cu ajutorul pointerilor. In Java aceasta tehnica este implementata prin intermediul interfetelor. Vom ilustra acest lucru prin intermediul unui exemplu.
Explorarea unui graf
In fiecare nod trebuie sa se execute prelucrarea informatiei din el prin intermediul unei functii primite ca parametru.
interface functie {
public int executie(int arg);
}
class Graf {
//...
void explorare(functie f) {
//...
if explorarea a ajuns in nodul v
f.executie(v.valoare);
//...
}
}
//Definim doua functii
class f1 implements functie {
public int executie(int arg) {
return arg+1;
}
}
class f2 implements functie {
public int executie(int arg) {
return arg*arg;
}
}
public class TestCallBack {
public static void main(String args[]) {
Graf G = new Graf();
G.explorare(new f1());
G.explorare(new f2());
}
}
Interfata FilenameFilter
Instantele claselor ce implementeaza aceasta interfata sunt folosite pentru a crea filtre pentru fisiere si sunt primite ca argumente de metode care listeaza continutul unui director, cum ar fi metoda list a clasei File.
Aceasta interfata are o singura metoda accept care specifica criteriul de filtrare si anume, testeaza daca numele fisierului primit ca parametru îndeplineste conditiile dorite de noi.
Definitia interfetei:
public interface FilenameFilter {
// Metode
public boolean accept( File dir, String numeFisier );
}
Asadar orice clasa de specificare a unui filtru care implementeza interfata FilenameFilter trebuie sa implementeze metoda accept a acestei interfete. Aceste clase mai pot avea si alte metode, de exemplu un constructor care sa primeasca criteriul de filtrare, adica masca dupa care se filtreaza fisierele. In general, o clasa de specificare a unui filtru are urmatorul format:
class DirFilter implements FilenameFilter {
String filtru;
//constructorul
DirFilter(String filtru) {
this.filtru = filtru;
}
//implementarea metodei accept
public boolean accept(File dir, String nume) {
//elimin informatiile despre calea fisierului
String f = new File(nume).getName();
if (filtrul este indeplinit)
return true;
else
return false;
}
}
Metodele cele mai uzuale ale clasei String folosite pentru filtrarea fisierelor sunt:
boolean endsWith(String s)
//testeaza daca un sir se termina cu sirul specificat s
int indexOf(String s)
//testeaza daca un sirul are ca subsir sirul specificat s
//returneaza 0=nu este subsir, >0=pozitia subsirului
Instantele claselor pentru filtrare sunt primite ca argumente de metode de listare a continutului unui director. O astfel de metoda este metoda list a clsei File:
String[] list (FilenameFilter filtru )
Observati ca aici interfata este folosita ca un tip de date, ea fiind substituita cu orice clasa care o implementeaza. Acesta este un exemplu tipic de transmitere a unei functii (functia de filtrare accept) ca argument al unei metode.
Listarea fisierelor din directorul curent care au extensia .java
import java.io.*;
public class DirList2 {
public static void main(String[] args) {
try {
File director = new File(".");
String[] list;
list = director.list(new FiltruExtensie("java"));
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
} catch(Exception e) {
e.printStackTrace();
}
}
}
class FiltruExtensie implements FilenameFilter {
String extensie;
FiltruExtensie (String extensie) {
this.extensie = extensie;
}
public boolean accept (File dir, String nume) {
return ( nume.endsWith("." + extensie) );
}
}
Exemplu de folosire a claselor anonime
In cazul în care nu avem nevoie de filtrarea fisierelor dintr-un director decât o singura data, pentru a evita crearea unei noi clase care sa fie folosita pentru filtrare putem apela la o clasa interna anonima, aceasta situatie fiind un exemplu tipic de folosire a acestora.
import java.io.*;
public class DirList3 {
public static void main(String[] args) {
try {
File director = new File(".");
String[] list;
//folosim o clasa anonima pentru specificarea filtrului
list = director.list(new
FilenameFilter() {
public boolean accept (File dir,String nume)
return ( nume.endsWith(".java"));
}
} );
for(int i = 0; i < list.length; i++)
System.out.println(list[i]);
} catch(Exception e) {
e.printStackTrace();
}
}
}
Curs 4
Serializarea obiectelor
Ce este serializarea ?
Serializarea obiectelor
Clasa ObjectOutputStream
Clasa ObjectInputStream
Obiecte serializabile
Implementarea interfetei Serializable
Personalizarea serializarii obiectelor
Implementarea interfetei Externalizable
Controlul serializarii (cuvântul cheie transient)
Exemplu de folosire a serializarii
Folosirea serializarii pentru copierea obiectelor
Ce este serializarea ?
Definitie
Serializarea este o metoda ce permite transformarea unui obiect într-o secventa de octeti din care sa poata fi refacut ulterior obiectul original. Cu alte cuvinte, serializarea permite salvarea într-o maniera unitara a datelor împreuna cu signatura unui obiect pe un mediu de stocare a informatiei extern programului. Procesul invers de citirea a unui obiect serializat pentru a-i reface starea originala se numeste deserializare. Intr-un cadru mai larg, prin serializare se întelege procesul de scriere/citire a obiectelor.
Utilitatea serializarii consta în urmatoarele aspecte:
Compensarea diferentelor între sisteme de operare, adica putem crea un obiect pe o masina Windows, îl serializam, apoi îl trimitem prin retea catre o masina UNIX unde va fi corect reconstruit. In acest fel comunicarea între sisteme diferite se realizeaza unitar, independent de reprezentarea datelor, ordinea octetilor sau alte detalii specifice sistemelor repective.
Permite persistenta obiectelor, ceea ce înseamna ca durata de viata a unui obiect nu este determinata de executia unui program în care acesta este definit - obiectul poate exista si între apelurile programelor care îl folosesc. Acest lucru se realizeaza prin serializarea obiectului si scrierea lui pe disc înainte de terminarea unui program, apoi, la relansarea programului, obiectul va fi citit de pe disc si starea lui refacuta. Acest tip de persistenta a obiectelor se numeste persistenta usoara, întrucât ea trebuie efectuata explicit de catre programator si nu este realizeazata automat de catre sistem.
RMI (Remote Method Invocation) - comunicarea obiectelor prin socket-uri: este o modalitate prin care obiectele de pe o alta masina se comporta ca si când ar exista pe masina pe care ruleaza programul nostru. Atunci când este trimis un mesaj catre un obiect "remote" (de pe alta masina), serializarea este necesara pentru transportul argumentelor prin retea si pentru returnarea valorilor.
Java Beans - sunt componente grafice definite de utilizator si care pot fi folosite la fel ca si componentele grafice standard. Orice componenta Bean are o stare initiala a informatiilor sale, stare care este specificata la definirea sa. Atunci când ea este folosita într-un program aceasta stare trebuie încarcata de undeva, ceea ce înseamna ca aceste componente trebuie serializate si salvate pe disc.
Un aspect important al serializarii este ca nu salveaza doar imaginea unui obiect ci si toate referintele la alte obiecte pe care acesta le contine. Acesta este un proces recusiv de salvare a datelor, întrucât celelalet obiectele referite de obiectul care se serializeaza pot referi la rândul lor alte obiecte, s.a.md. Asadar obiectele care construiesc starea altui obiect formeaza o întreaga retea de obiecte, ceea ce înseamna ca un algoritm de salvare a starii unui obiect nu este facil.
In cazul în care starea unui obiect este formata doar din valori ale unor variabile de tipuri primitive, atunci salvarea datelor înapsulate în acel obiect se poate face si prin salvarea pe rând a datelor, folosind clasa DataOutputStream, pentru ca apoi sa fie restaurate prin metode ale clasei DataInputStream, dar, asa cum am vazut, o asemenea abordare nu este în general suficienta, deoarece pot aparea probleme cum ar fi : datele obiectului pot fi instante ale altor obiecte, unele câmpuri fac referinta la acelasi obiect, etc.
Serializarea obiectelor se realizeaza prin intermediul fluxurilor definite de clasele ObjectOutputStream (pentru salvare) si ObjectInputStream (pentru restaurare).
Serializarea obiectelor
Serializarea obiectelor se realizeaza prin intermediul fluxurilor definite de clasele ObjectOutputStream (pentru salvare) si ObjectInputStream (pentru restaurare). Acestea sunt fluxuri de procesare ceea ce înseamna ca ele vor fi folosite împreuna cu alte fluxuri pentru citirea/scrierea efectiva a datelor pe mediul extern pe care va fi salvat sau de pe care va fi restaurat un obiect serializat.
Mecanismul implicit de serializare a unui obiect va salva numele clasei obiectului, signatura clasei obicetului, valorile tuturor câmpurile serializabile ale obiectului (vezi "Controlul serializarii"). Referintele la alte obiecte serializabile din cadrul obiectului curent vor duce automat la serializarea acestora iar referintele multiple catre un acelasi obiect sunt codificate utilizând un algoritm care sa poata reface "reteaua de obiecte" la aceeasi stare ca atunci când obiectul original a fost salvat.
Clasele ObjectInputStream si ObjectOutputStream implementeaza indirect interfetele DataInput, respectiv DataOutput, interfete ce declara metode de tipul readXXX, respectiv writeXXX pentru scrierea/citirea datelor primitive. Pe lânga aceste metode vor exista si metode pentru scrierea/citirea obiectelor.
Metodele pentru serializarea obiectelor sunt:
private void readObject(ObjectInputStream stream) //salvare obiect
throws IOException,ClassNotFoundException;
private void writeObject(ObjectOutputStream stream) //refacere obiect
throws IOException;
Aceste metode contin apeluri catre metodele implicite de seializare a obiectelor:
final void defaultWriteObject()
throws IOException
final void defaultReadObject()
throws IOException,ClassNotFoundException,NotActiveException
Clasele care necesita o serializare speciala trebuie sa supradefineasca metodele writeObject si readObject (obligatoriu pe amândoua!) pentru a implementa metode specifice de serializare. (vezi "Personalizarea serializarii obiectelor").
Clasa ObjectOutputStream
Scrierea obiectelor pe un flux este un proces extrem de simplu. Exemplul de mai jos, afla timpul curent în milisecunde construind un obiect de tip Date si îl salveaza într-un fisier theTime:
FileOutputStream out = new FileOutputStream("theTime");
ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();
s.close();
Asadar metoda pentru scrierea unui obiect este writeObject, responsabila cu serializarea completa a obiectului. Deoarece implementeaza interfata DataOutput, pe lânga metoda de scriere a obiectelor, clasa pune la dispozitie si metode de tipul writeXXX pentru scrierea tipurilor de date primitive, astfel încât apeluri ca cele de mai jos sunt permise :
FileOutputStream out = new FileOutputStream("t.tmp");
ObjectOutputStream s = new ObjectOutputStream(out);
s.writeInt(12345);
s.writeDouble(12.345);
s.writeUTF("Sir de caractere");
s.flush();
s.close();
Metoda writeObject arunca exceptii de tipul NotSerializableException daca obiectul primit ca argument nu este serializabil. Vom vedea în continuare ca un obiect este serializabil daca este instanta a unei clase ce implementeaza interfata Serializable.
Clasa ObjectInputStream
Odata ce au fost scrise obiecte si tipuri primitive de date pe un flux, citirea acestora si reconstruirea obiectelor salvate se va face printr-un flux de intrare de tip ObjectInputStream. Acesta este de asemenea un flux de procesare si va trebui asociat cu un flux pentru citirea efectiva a datelor, de exemplu FileInputStream (pentru date salvate într-un fisier).
FileInputStream in = new FileInputStream("theTime");
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();
Asadar, metoda pentru citirea unui obiect serializat si refacerea starii lui este readObject. Clasa ObjectInputStream implementeaza interfata DataInput, deci, pe lânga metoda de citire a obiectelor clasa pune la dispozitie si metode de tipul readXXX pentru citirea tipurilor de date primitive.
FileInputStream in = new FileInputStream("t.tmp");
ObjectInputStream s = new ObjectInputStream(in);
int n = s.readInt();
double d = s.readDouble(12.345);
String sir = s.readUTF();
Atentie
Ca si la celelate fluxuri de date (care implemeteaza interfata DataInput) citirea dintr-un flux de obiecte trebuie sa se faca exact în ordinea în carea acestea au fost scrise.
Trebuie observat ca metoda readObject returneaza un obiect de tipul Object si nu de tipul corespunzator obiectului citit, conversia la acest tip trebuind sa se faca explicit:
Date date = s.readObject(); // ilegal
Date date = (Date)s.readObject(); // corect
Obiecte serializabile
Un obiect este serializabil daca si numai daca clasa din care face parte implementeaza interfata Serializable. Asadar, daca dorim ca instantele unei clase sa poata fi serializate, clasa respectiva trebuie sa implementeze interfata Serializable. Aceasts interfata este mai deosebita, în sensul ca nu contine nici o declaratie de metoda, singurul ei scop fiind de a identifica clasele ale caror obiecte sunt serializabile.
Implementarea interfetei Serializable
Definitia completa a interfetei Serializable este:
package java.io;
public interface Serializable {
// nimic!
}
Crearea claselor ale caror instante sunt serializabile este extrem de facila: la clasa respectiva trebuie sa adaugam în declaratia ei ca implementeze interfata Serializable si nimic mai mult:
public class ClasaSerializabila implements Serializable {
//putem sa nu scriem nici o metoda deoarece
//interfata nu declara nici o metoda!
}
Asadar, clasa poate sa nu contina nici o metoda, ea va contine totusi metode altfel nu ar avea nici un rost, dar metodele vor fi specifice scopului pentru care ea a fost creata si nu vor avea legatura cu serializarea.
Asa cum am vazut, serializarea implicita a obiectelor oricarei clase este definita în metoda defaultWriteObject a clasei ObjectOutputStream care va salva toate datele necesare reconstruirii obiectului : numele clasei, signatura, valorile variabilelor membre si obiectele referite de acestea. In majoritatea cazurilor aceasta metoda de serializare este suficienta, însa o clasa poate avea nevoie de mai mult control asupra serializarii.
Personalizarea serializarii obiectelor
Personalizarea serializarii se realizeaza prin supradefinirea (într-o clasa serializabila!) a metodelor writeObject si readObject, modificând astfel actiunea lor implicita.
Metoda writeObject controleaza ce date sunt salvate si este uzual folosita pentru a adauga informatii suplimentare la cele scrise implicit de metoda defaultWriteObject.
Metoda readObject controleaza modul în care sunt restaurate obiectele, citind informatiile salvate si, eventual, modifcând starea obiectelor citite astfel încât ele sa corespunda anumitor cerinte.
Aceste metode trebuie obligatoriu sa aiba urmatorul format:
private void writeObject(ObjectOutputStream stream)
throws IOException
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException
De asemenea, uzual, primul lucru pe care trebuie sa îl faca aceste metode este apelul la metodele standard de serializare a obiectelor defaultWriteObject, respectiv defaultReadObject si abia apoi sa execute diverse operatiuni suplimentare. Forma lor generala este:
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
// personalizarea serializarii
}
private void readObject(ObjectInputStream s)
throws IOException,ClassNotFoundException {
s.defaultReadObject();
// personalizarea deserializarii
. . .
// actualizarea starii obiectului (daca e necesar)
}
Metodele writeObject si readObject sunt responsabile cu serializarea clasei în care sunt definite, serializarea superclasei sale fiind facuta automat (si implicit). Daca însa o clasa trebuie sa-si coordoneze serializarea proprie cu serializarea superclasei sale, atunci trebuie sa implementeze interfata Externalizable.
Implementarea interfetei Externalizable
Pentru un control complet, explicit, al procesului de serializare, o clasa trebuie sa implementeze interfata Externalizable. Pentru instante ale acestor clase doar numele clasei este salvat automat pe un flux de obiecte, clasa fiind responsabila cu scrierea si citirea membrilor sai si trebuie sa se coordoneze cu superclasele ei.
Definitia interfetei Externalizable este:
package java.io;
public interface Externalizable extends Serializable {
public void writeExternal(ObjectOutput out)
throws IOException;
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException;
}
Asadar, aceste clase trebuie sa implementeze obligatoriu metodele writeExternal si readExternal în care se va face serializarea completa a obiectelor si coordonarea cu superclasa ei.
Controlul serializarii
Exista cazuri când dorim ca unele variabile membre sau sub-obiecte ale unui obiect sa nu fie salvate automat în procesul de serializare. Acestea sunt cazuri comune atunci când respectivele câmpuri reprezinta informatii confidentiale, cum ar fi parole, sau variabile auxiliare pe care nu are rost sa le salvam. Chiar declarate ca private în cadrul clasei aceste câmpuri participa la serializare. O modalitate de a controla serializare este implementarea interfetei Externalizable, asa cum am vazut anterior. Aceasta metoda este însa incomoda atunci când clasele sunt greu de serializat iar multimea câmpurilor care nu trebuie salvate este redusa.
Pentru ca un câmp sa nu fie salvat în procesul de serializare atunci el trebuie declarat cu modificatorul transient si trebuie sa fie ne-static. De exemplu, declararea unei parole ar trebui facuta astfel:
transient private String parola; //ignorat la serializare
Atentie
Modificatorul static anuleaza efectul modificatorului transient;
static transient private String parola; //participa la serializare
De asemenea, nu participa la serializare sub-obiectele neserializabile ale unui obiect, adica cele ale caror clase nu au fost declarate ca implementând interfata Serializable (sau Externalizable).
Exemplu: (câmpurile marcate 'DA' participa la serializare, cele marcate 'NU', nu participa)
class A { ... }
class B implements Serializable { ... }
public class Test implements Serializable {
private int x; // DA
transient public int y; // NU
static int var1; // DA
transient static var2; // DA
A a; // NU
B b1; // DA
transient B b2; // NU
}
Atunci când o clasa serializabila deriva dintr-o alta clasa, salvarea câmpurilor clasei parinte se va face doar daca si aceasta este serializabila. In caz contrar, subclasa trebuie sa salveze explicit si câmpurile mostenite.
Ex1: class Parinte implements Serializable {
int x;
}
class Fiu extends Parinte implements Serializable {
int y;
}//La serializarea obiectelor de tip Fiu se salveaza atât x cât si y.
Ex2: class Parinte {
int x;
}
class Fiu extends Parinte implements Serializable {
int y;
}//Serializarea nu decurge normal.
Exemplu de folosire a serializarii
import java.io.*;
public class TestSerializare {
public static void main(String args[]) throws IOException {
MyObject obj = new MyObject(10, 20, 30);
//salvam obiectul in fisierul "fisier.tmp"
FileOutputStream fout = new FileOutputStream("fisier.tmp");
ObjectOutputStream sout = new ObjectOutputStream(fout);
sout.writeObject(obj);
sout.flush();
sout.close();
fout.close();
System.out.println("A fost salvat obiectul " + obj);
System.out.println("Restauram...");
FileInputStream fin = new FileInputStream("fisier.tmp");
ObjectInputStream sin = new ObjectInputStream(fin);
try {
obj = (MyObject) sin.readObject();
} catch (ClassNotFoundException e) {}
sin.close();
fin.close();
System.out.println("A fost restaurat obiectul " + obj);
}
}
class MyObject implements Serializable {
int x; //este salvat
transient private int y; //nu este salvat
transient static int z; //nu este salvat
public MyObject(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public String toString() {
return new String("x=" + x + ", y=" + y + ", z=" + z);
}
}
Rezultatul acestui program va fi :
A fost salvat obiectul x=10, y=20, z=30
Restauram...
A fost restaurat obiectul x=10, y=0, z=30
Folosirea serializarii pentru copierea obiectelor
Se stie ca nu putem copia un obiect prin instructiunea de atribuire. O secventa de forma:
MyObject o1 = new MyObject(10, 20, 30);
MyObject o2 = o1;
nu face decât sa declare obiectul o2 ca fiind o referinta la obiectul o1 si prin urmarea orice schimbare într-unul din cele doua obiecte se va reflecta si în celalalt.
O posibilitate de a face o copie unui obiect este folosirea metodei clone() a clasei Object.
MyObject o1 = new MyObject(10, 20, 30);
MyObject o2 = (MyObject) o1.clone();
Conversia la clasa MyObject este necesara deoarece metoda clone() returneaza un obiect de tip Object. Deficienta acestei metode este ca nu functioneaza corect decât atunci când clasa clonata nu are câmpuri referinta ca alte obiecte, obiectele referite nemaifiind copiate la rândul lor.
O metoda clone() care sa realizeze o copie efectiva a unui obiect, împreuna cu copierea tuturor obiectelor referite de câmpurile acelui obiect poate fi implementata prin mecanismul serializarii astfel:
public Object clone() {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(this);
out.close();
ByteArrayInputStream bin = new ByteArrayInputStream();
ObjectInputStream in = new ObjectInputStream(bin);
Object ret = in.readObject();
in.close();
return ret;
} catch (Exception e) {
System.out.println(e);
return null;
}
}
Curs 4
Pachete
Crearea unui pachet
Denumirea unui pachet
Folosirea membrilor unui pachet
Importul unei clase sau interfete
Importul unui pachet (importul la cerere)
Pachetele JDK
Crearea unui pachet
Definitie
Un pachet este o colectie de clase si interfete înrudite. Sunt folosite pentru gasirea si utilizarea mai usoara a claselor, pentru a evita conflictele de nume si pentru a controla accesul la anumite clase. In alte limbaje de programare pachetele se numesc librarii.
Toate clasele si interfetele Java apartin la diverse pachete, grupate dupa functionalitatea lor: clasele de baza se gasesc în pachetul java.lang, clasele pentru intrari/iesiri sunt în java.io, clasele pentru grafica în java.awt, cele pentru construirea applet-urile în java.applet, etc. Crearea unui pachet se realizeaza prin scriere la începutul fisierelor sursa ce contin clasele si interfetele pe care dorim sa le grupam într-un pachet a instructiunii: package NumePachet; Sa consideram un exemplu: presupunem ca avem doua fisiere sursa Graf.java si Arbore.java Graf.java Arbore.java
package grafuri;
class Graf {...}
class GrafPerfect extends Graf {...} package grafuri;
class Arbore {...}
class ArboreBinar extends Arbore {...}
Clasele Graf, GrafPerfect, Arbore, ArboreBinar vor face parte din acelasi pachet grafuri.
Instructiunea package actioneaza asupra întregului fisier sursa la începutul caruia apare. Cu alte cuvinte nu putem specifica faptul ca anumite clase dintr-un fisier sursa apartin unui pachet iar altele altui pachet.
Daca nu este specificat un anumit pachet, clasele unui fisier sursa vor face parte din pachetul implicit (care nu are nici un nume). In general, pachetul implicit este format din toate clasele si intefetele directorului curent.
Este recomandabil ca toate clasele si intefetele sa fie plasate în pachete. Pachetul implicit este folosit doar pentru aplicatii mici sau la începutul dezvoltarii unei aplicatii.
Denumirea unui pachet
Exista posibilitatea ca doi programatori care lucreaza la un proiect comun sa foloseasca acelasi nume pentru unele din clasele lor. De asemenea, se poate ca una din clasele unei aplicatii sa aiba acelasi nume cu o clasa a mediului Java. Acest lucru este posibil atât timp cât clasele cu acelasi nume se gasesc în pachte diferite, ele fiind diferentiate prin prefixarea lor cu numele pachetelor. Asadar numele complet al unei clase este format din numele pachetului la care apartine + numele sau:
numePachet.NumeClasa
Ex: java.lang.String (java.lang=pachet, String=clasa)
De exemplu sa presupunem ca în aplicatia noastra folosim o clasa numita Stack:
package my_package;
class Stack { ... }
Clasa Stack exista deja în pachetul java.util. Diferentierea între cele doua clase se va face prin specificarea numelui complet al clasei, adica numelePachetului.NumeleClasei:
java.util.Stack s1 = new java.util.Stack();
my_package.Stack s2 = new my_package.Stack();
Ce se întâmpla însa când doi programatori care lucreaza la un proiect comun folosesc clase cu acelasi nume ce se gasesc în pachete cu acelasi nume ? Pentru a evita acest lucru companiile folosesc inversul domeniului lor Internet în denumirea pachetelor implementate în cadrul companiei, cum ar fi com.company.numePachet. In cadrul unei aceeasi companii conflictele de nume trebuie rezolvate prin diverse conventii de uz intern.De exemplu, adresa mea de e-mail este acf@infoiasi.ro, ceea ce înseamna ca domeniul meu Internet este infoiasi.ro. Pachetele create de mine ar trebui denumite ro.infoiasi.NumePachet. Pentru a rezolva conflicte cu alti programatori din acelasi domeniu cu mine pachetele s-ar putea numi: ro.infoiasi.acf.NumePachet.
Folosirea membrilor unui pachet
Conform specificatiilor de acces ale unei clase si ale mebrilor ei doar clasele publice si membrii declarati publici ai unei clase sunt accesibili în afara pachetului în care acestea se gasesc. (vezi "Specificatori de acces pentru membrii unei clase")
Pentru a folosi o clasa publica dintr-un pachet sau pentru a apela o metoda publica a unei clase public a unui pachet exista trei solutii:
specificarea numelui complet al clasei
importul clasei respective
importul întregului pachet în care se gaseste clasa
Specificarea numelui complet al calsei se face, asa cum am vazut, prin prefixarea numelui clasei cu numele pachetului: numePachet.NumeClasa. Aceasta metoda este recomandata doar pentru cazul în care folosirea acelei clase se face o singura data sau foarte rar. De exemplu ar fi extrem de neplacut sa scriem de fiecare data când vrem sa declaram un sir de caractere sau sa folosim un obiect grafic secvete de genul;
java.lang.String s = "neplacut";
java.awt.Rectangle r = new java.awt.Rectangle();
java.awt.Circle c = new java.awt.Circle();
In aceste situatii vom importa (include) clasa respective sau întreg pachet din care face parte in aplicatia noastra. Acest lucru se realizeaza prin instructiunea import, care trebuie sa apara la începutul fisierelor sursa, imediat dupa instructiunea package.
Importul unei clase sau interfete
Se face printr-o instructiune import în care specificam numele clasei (interfetei) pe care dorim sa o folosim dintr-un pachet:
import java.awt.Rectangle;
Din acest moment vom putea folosi în clasele fisierului în care am plasat instructiunea de import numele scurt al clasei Rectangle
Rectangle r = new Rectangle(0,0,100,100);
Aceasta abordare este eficienta în cazul în care nu avem nevoie decât de acea clasa sau doar de câteva clase din pachetul respectiv. Daca în exemplul nostru am avea nevoie si de clasele Circle, Line, Point, Polygon ar trebui sa avem câte o instructiune de import pentru fiecare dintre ele:
import java.awt.Rectangle;
import java.awt.Circle;
import java.awt.Line;
import java.awt.Point;
import java.awt.Polygon;
In aceasta situatie ar fi recomandat importul întregului pachet si nu al fiecarei clase în parte.
Importul unui pachet (importul la cerere)
Se face printr-o instructiune import în care specificam numele pachetului ale carui clase si interfete dorim sa le folosim dintr-un pachet, urmat de simbolul '*'. Se mai numeste import la cerere deoarece încarcarea claselor se face dinamic în momentul apelarii lor. Este cel mai uzual tip de import.
import java.awt.*;
Din acest moment vom putea folosi în clasele fisierului în care am plasat instructiunea de import numele scurt al tuturor claselor pachetului importat:
Rectangle r = new Rectangle();
Circle c = new Circle(); ...
Atentie
* nu are semnificatia uzuala de wildcard (masca) si nu poate fi folosit decât ca atare.
import java.awt.C*; //eroare de compilare
In cazul în care sunt importate doua pachete care contin o clasa cu acelasi nume atunci referirea la ea trebuie facuta folosind numele complet al clasei respective.
//Stack.java
package my_package;
class Stack { ... }
//alt fisier sursa
import java.util.*;
import my_package.*;
...
Stack s = new Stack(); //ilegal -> conflict de nume
java.util.Stack s1 = new java.util.Stack(); //corect
my_package.Stack s2 = new my_package.Stack();//corect
Mediul Java importa automat trei pachete pentru toate fisierele sursa:
pachetul java.lang
pachetul curent
pachetul implicit (fara nume)
Pachetele JDK
Limbajul Java se bazeaza pe o serie de biblioteci (pachete) cu ajutorul carora se pot construi aplicatiile. Exista deci un set de clase deja implementate, ceea ce reduce timpul de dezvoltare a unui program. Cele mai importante sunt:
java.applet suport pt scrierea de appleturi
java.awt suportul pentru grafica(Abstract Windowing Toolkit)
java.beans suport pentru scrierea de componente reutilizabile
java.io intrari/iesiri, acces la fisiere
java.lang clasele de baza ale limbajului
java.math operatii matematice
java.net acces la retea
java.rmi executie la distanta (Remote Message Interface)
java.security mecanisme de securitate : criptare, autentificare
java.sql interogari SQL
java.text suport pentru formatarea textelor
java.util clase utile : Vector, Stack, Random, etc
Pachetul java.lang contine elementele de baza ale limbajului si este importat automat.