Programowanie Obiektowe (Java) 1
Transkrypt
Programowanie Obiektowe (Java) 1
Programowanie Obiektowe (Java) Wykład dziewiąty 1. Kontenery w Javie 5 Jednym z nowych elementów jakie zostały dodane do języka Java w jej najnowszym wydaniu (numer środowiska 1.5) są typy ogólne (ang. 1 import java.util.*; 2 3 class Element { 4 generics), które wzbogacają język Java o mechanizmu podobny do mechanizmu szablonów, znanego z języka C++. Są one stosowane między innymi w kontenerach. Dzięki nim możemy określić jakiej klasy obiekty będą przechowywane w kontenerze (lub jaki interfejs będą implementowały te obiekty). Dzięki temu możemy częściowo ograniczyć 1 rzutowanie w dół , które nie jest bezpieczne, jeśli nie stosujemy żadnego mechanizmu rozpoznawania typu podczas wykonania programu. Nie możemy 2 jednak, stosując typy ogólne , zupełnie pominąć rzutowania w dół, bowiem oprócz obiektów określonej klasy możemy w kontenerze przechowywać również obiekty klas dziedziczących po niej. Typy ogólne mogą być stosowane nie tylko w kontenerach, ale również w zwracanych przez nie iteratorach. Inną nowością, którą możemy stosować wraz z kontenerami jest nowa postać pętli „for”. Oto kod programu ilustrującego użycie list, w którym zastosowano opisane wcześniej nowe elementy Javy: private int value; 5 6 public String toString() { 7 8 return new Integer(value).toString(); } 9 10 public Element(int x) { 11 12 value=x; } 13 } 14 15 public class Listy { 16 public static void main(String[] args) { 17 ArrayList<Element> a = new ArrayList<Element>(); 18 LinkedList<Element> l = new LinkedList<Element>(); 19 Iterator<Element> it; 20 21 for(int i=0;i<20; i++) 22 a.add(new Element((int)(Math.random()*20))); 23 System.out.println(a); 24 for(Element e : a) 25 System.out.print(e+" "); 26 System.out.println(); 27 it = a.iterator(); 28 while(it.hasNext()) 29 System.out.print(it.next()+" "); 30 System.out.println(); 31 it=a.iterator(); 32 while(it.hasNext()) { 33 l.addFirst(it.next()); 34 it.remove(); 35 } 36 System.out.println(l); 37 while(!l.isEmpty()) 1 Korzystając w kontenerach z typów ogólnych sprawiamy, że kontener pamięta klasę przechowywanych obiektów. Stosowane są więc przez niego referencje konkretnej klasy, a nie klasy Object. 2 W języku polskim częściej spotyka się obecnie termin „typy generyczne”. 1 Programowanie Obiektowe (Java) 38 System.out.print(l.removeLast()+" "); 39 System.out.println(); 40 } 41 } Należy zaznaczyć, że nowa postać pętli for zapewnia większy stopień bezpieczeństwa korzystania z kontenerów, niż „zwykła” instrukcja for, jeśli tworzymy pętle zagnieżdżone. Innym udogodnieniem w korzystaniu z kontenerów jest automatyczne „opakowywanie” i „rozpakowywanie” typów podstawowych w odpowiadające im klasy. Ta technika pozwala na stworzenie kontenera, do którego będzie można dodawać wartości typu int bez dokonywania dodatkowych zabiegów, które zostaną za nas wykonane automatycznie. 2. Klasa Arrays W pakiecie java.util znajduje się klasa Arrays zawierająca wiele metod statycznych pozwalających na manipulowanie zawartością tablic. Metoda fill() wypełnia wszystkie elementy tablicy tą samą wartością. Metoda equals() służy do porównywania elementów tablicy. Została ona przeciążona dla typów podstawowych i obiektów klasy Object. Należy pamiętać, że w przypadku obiektów przechowywanych w tablicy, porównywane są ich referencje, a nie zawartość pól. Jeśli chcemy porównywać pola obiektów, to możemy zrobić to na dwa sposoby. Pierwszy polega na zaimplementowaniu przez klasę obiektów przechowywanych w tablicy interfejsu Comparable i jego metody compareTo(). Drugi polega na stworzeniu odrębnej klasy implementującej interfejs Comparator (należy zdefiniować jego metody equals() i compare()). Ten drugi sposób wykorzystujemy wówczas, gdy nie mamy dostępu do kodu klasy przechowywanych obiektów. Tablicę możemy posortować przy pomocy metody sort(), natomiast przeszukać ją możemy za pomocą metody binarySearch(). Kopiowanie tablic najlepiej przeprowadzić za pomocą metody arraycopy(), która jest metodą statyczną klasy System (nie należy ona do klasy Arrays). Na zakończenie należy zaznaczyć, że tablica jest jedynym 3 kontenerem , który „zna” klasę obiektów, które przechowuje (notabene tablice w Javie też są obiektami). 3. Strumienie Zbiór klas umożliwiających realizację operacji wejścia – wyjścia w Javie jest bardzo duży. Od wersji 1.4 jest on wzbogacony o pakiety klas umożliwiających wykonywanie operacji „niskopoziomowych” na danych znajdujących się w pliku lub innym zasobie. Te pakiety nie będą jednak przedstawione na tym wykładzie. Zajmiemy się jedynie operacjami „wysokopoziomowymi”, które umożliwiają nam klasy zgromadzone w pakiecie java.io. Pierwszą klasą z tego pakietu, jaka zostanie tu omówiona jest klasa File. 1 import java.io.*; 2 import java.util.*; 3 4 class Rekurencja { 5 private File sciezka; 6 7 public Rekurencja(String nazwa) { 8 9 sciezka = new File(nazwa); } 10 11 12 13 Klasa ta pozwala na przeprowadzanie takich operacji na plikach i katalogach, jak: tworzenie nowych plików i katalogów, przeglądanie katalogów, zmianę nazwy, usunięcie pliku, obsługę ścieżki do pliku lub katalogu, obsługę atrybutów plików. Oto krótki przykład jak wykorzystać obiekt takiej klasy do rekurencyjnego wypisania zawartości bieżącego katalogu: public void start() { walk(sciezka,""); } 14 15 private void walk(File plik,String indent) { 16 String[] lista; 17 File[] pliki; Klasa Rekurencja posiada pole sciezka, które jest klasy File. W konstruktorze tej 18 19 lista = plik.list(); klasy Rekurencja tworzony jest obiekt 20 pliki = plik.listFiles(); klasy 21 22 3 for(int i=0; i<lista.length;i++) System.out.println(indent+lista[i]); 23 indent+=" "; 24 for(int i=0; i<pliki.length;i++) File. Przez parametr nazwa przekazywana będzie ścieżka dostępu do pliku, z którym będzie związany ten obiekt (w przypadku tego programu będzie to katalog bieżący). Metoda start() rozpoczyna przeszukiwanie tego katalogu, wywołując metodę walk(). Ta To stwierdzenie jest prawdziwe dla wersji Javy wcześniejszych niż 5. 2 Programowanie Obiektowe (Java) 25 26 System.out.println(pliki[i].getName()+":"); 27 walk(pliki[i],indent); 28 29 ostatnia jest metodą rekurencyjną. Najpierw zapisuje ona referencję do tablicy nazw plików w zmiennej lokalnej lista, a następnie w referencji pliki if(pliki[i].isDirectory() && pliki[i].canRead()) { zapisuje adres tablicy obiektów klasy File związanych z tymi plikami. W pierwszej pętli for wypisywane są nazwy plików } } znajdujących się w bieżącym katalogu, w drugiej przeglądania jest tablica obiektów klasy File i sprawdzane jest, które z nich są 30 } 31 32 public class Katalogi { 33 34 Rekurencja r = new Rekurencja("."); 35 r.start(); 36 katalogami i czy można te katalogi przeczytać (czy program ma do tego uprawnienia). Dla każdego z tych obiektów wywoływana jest rekurencyjnie metoda walk(). Obiekt klasy Rekurencja public static void main(String[] args) { tworzony jest w klasie publicznej w metodzie main. Konstruktorowi tego } obiektu przekazywana jest nazwa katalogu bieżącego, czyli kropka (.). Jeśli za pomocą metod klasy File, chcielibyśmy wyświetlić pliki, których nazwy pasują do ustalonego przez nas wzorca, to do metody list() powinniśmy 37 } przekazać obiekt klasy, która implementuje interfejs FilenameFilter i zdefiniować jego metodę accept, która jest deklarowana następująco: boolean accept(File dir, String name); Klasa implementująca ten interfejs może być klasą anonimową. Klasy realizujące operacje wejścia – wyjścia w Javie zakładają, że zarówno źródłem jak i ujściem danych są strumienie. Strumień jest abstrakcją oznaczającą dowolny element mogący pobierać lub przyjmować dane. W Javie, w pakiecie java.io można wyróżnić trzy grupy klas. Pierwsza grupa związana jest z operacjami binarnymi (nazywanymi również bajtowymi), druga jest związana z operacjami znakowymi (uwzględnia kodowanie za pomocą standardu Unicode), natomiast trzecia grupa umożliwia zapis i odczyt obiektów. Podstawowymi klasami pozwalającymi na realizację 4 odczytu i zapisu bajtowego są klasy InputStream i OutputStream . Jak łatwo się domyślić metodą służącą do odczytania pojedynczego bajta w klasie InputStream jest metoda read(), natomiast do zapisu pojedynczego bajta w drugiej z wymienionych klas jest write(). Z tych klas wywodzą 5 się inne, które związane są z obsługą poszczególnych rodzajów źródeł i ujść. Te klasy zostały zestawione poniżej w tabelę : Klasa Sposób działania Sposób użycia ByteArrayInputStream Źródłem danych jest tablica znajdująca się w Parametrem wywołania konstruktora jest pamięci operacyjnej. referencja do tablicy z której będą pobierane dane. Powinno się używać razem z obiektami FilterInputStream. StringBuffferInputStream Wykonuje konwersję łańcucha znaków do Parametrem wywołania konstruktora jest obiektu klasy InputStream. referencja do obiektu klasy String. Również należy używać FilterInputStream. FileInputStream Umożliwia odczyt danych z pliku. z tym plikiem lub klasy Podobnie jak poprzednio powinno się używać FilterInputStream. Umożliwia odczyt danych nienazwanego (ang. pipe) klasy Argumentem wywołania konstruktora jest łańcuch znaków reprezentujący ścieżkę dostępu do pliku, lub obiekt klasy File związany z FileDescriptor. PipedInputStream obiektami z z obiektami klasy potoku Parametrem wywołania konstruktora tej klasy jest obiekt klasy PipedOutputStream. Obiekty tej klasy są używane w aplikacjach wielowątkowych, jako środki umożliwiające komunikację między wątkami. Reszta jak wyżej. 4 5 Klasy InputStream i OutputStream są klasami abstrakcyjnymi, więc wszędzie tam, gdzie będzie mowa o obiektach tych klas, będziemy mieć na myśli obiekty jej klas pochodnych. Tabela ta jest utworzona na podstawie „Thinking in Java” Bruce'a Eckela. 3 Programowanie Obiektowe (Java) Klasa SequenceInputStream Sposób działania Sposób użycia Pozwala połączyć pewną liczbę strumieni Argumentami wywołania są dwa obiekty klasy wejściowych w jeden strumień. InputStream lub kontener klasy Enumeration zawierający takie obiekty. Jest to klasa abstrakcyjna służąca za klasę bazową dla klas obiektów „dekoratorów”, obiektów klasy InputStream i pochodnych, FilterInputStream które zostaną opisane później. ByteArrayOutputStream Tworzy bufor w pamięci, do którego będą Parametr konstruktora jest opcjonalny i zapisane dane. określa początkowy rozmiar bufora. Obiekty tej klasy powinny być połączone z obiektami klasy FilterOutputStream. FileOutputStream Zapisuje dane do pliku. Argumentem wywołania konstruktora jest łańcuch znaków reprezentujący ścieżkę dostępu do pliku, lub obiekt klasy File związany z FileDescriptor. tym plikiem lub klasy Podobnie jak poprzednio należy używać z FilterOutputStream. Umożliwia zapis danych nienazwanego (ang. pipe) PipedOutputStream do obiektami klasy potoku Parametrem wywołania konstruktora tej klasy jest obiekt klasy PipedInputStream. Obiekty tej klasy są używane w aplikacjach wielowątkowych, jako środki umożliwiające komunikację między wątkami. Reszta jak wyżej. Jest to klasa abstrakcyjna służąca za klasę bazową dla klas obiektów „dekoratorów”, obiektów klasy OutputStream i pochodnych, FilterOutputStream które zostaną opisane później. W większości aplikacji napisanych w Javie, aby utworzyć reprezentację pojedynczego strumienia wejściowego lub wyjściowego tworzy się niej jeden lecz kilka obiektów. Pierwszy z tych obiektów jest obiektem którejś z klas wymienionych wyżej, natomiast pozostałe obiekty są 6 dekoratorami, które zwiększają funkcjonalność strumienia. Oto zestawienie tych dekoratorów : Klasa 6 Działanie Sposób użycia DataInputStream Umożliwia odczyt danych prostych typów w Argumentem wywołania konstruktora jest sposób niezależny od ich faktycznego zapisu w obiekt klasy InputStream. danym systemie. Jego metody, to np.: readByte(), readFloat(). BufferedInputStream Używany, w celu przyspieszenia operacji Argument konstruktora taki sam, jak wyżej. odczytu ze strumienia poprzez zbuforowanie Dodatkowy argument może określać rozmiar danych. bufora. Obiekty tej klasy nie dostarczają własnych interfejsów. LineNumberInputStream Śledzi numery wierszy w strumieniu Argument wywołania konstruktora jak wejściowym. Do jego metod należą: poprzednio. Zapewnia numerowanie wierszy. getLineNumber() i setLineNumber(int) PushBackInputStream Tworzy bufor o rozmiarze jednego bajta, Argument konstruktora jak wyżej. Używany w pozwalający na cofnięcie do strumienia tworzeniu kompilatorów i innych programów realizujących analizę leksykalną wejścia. ostatniego odczytanego baja. DataOutputStream Umożliwia zapis danych podstawowych typów Argumentem konstruktora jest obiekt klasy do strumienia niezależny od platformy. OutputStream. Przykładowe metody: writeByte(), writeFloat(). Na podstawie „Thinking in Java” Bruce'a Eckela. 4 Programowanie Obiektowe (Java) Klasa Działanie Formatuje dane wyjściowe, sposobie ich wyświetlania PrintStream Sposób użycia decydując o Argument konstruktora jak wyżej. Opcjonalny drugi argument typu boolean określa, czy bufor związany ze strumieniem opróżniany po każdej operacji zapisu. BufferOutputStream Buforuje dane do zapisu. Można wymusić ich Argumentem wywołania konstruktora jest zapisanie wywołując metodę flush() obiektu tej obiekt klasy OutputStream i opcjonalnie klasy. rozmiar bufora. Oto przykład realizujący bajtowy odczyt danych z pliku: 1 import java.io.*; 2 class Odczyt { 3 private FileInputStream fs; 4 5 public Odczyt(String s) { 6 try { 7 fs = new FileInputStream(new File(s)); 8 }catch(FileNotFoundException e) { 9 e.printStackTrace(); 10 System.err.println(e.getLocalizedMessage()); 11 }catch(Exception e) { 12 e.printStackTrace(); 13 System.err.println(e.getLocalizedMessage()); 14 15 } } 16 17 public void czytaj() throws IOException { 18 try { 19 int a; 20 while((a=fs.read())!=-1) System.out.print(a); 21 System.out.println(); 22 }catch(IOException e) { 23 e.printStackTrace(); 24 System.err.println(e.getLocalizedMessage()); 25 }catch(Exception e) { 26 e.printStackTrace(); 27 System.err.println(e.getLocalizedMessage()); 28 }finally { 29 fs.close(); 30 31 jest } } 32 } 33 5 Programowanie Obiektowe (Java) 34 public class OdczytBinarny { 35 public static void main(String[] args) { 36 Odczyt o = new Odczyt("OdczytBinarny.java"); 37 try { 38 o.czytaj(); 39 }catch(IOException e) { 40 e.printStackTrace(); 41 System.err.println(e.getLocalizedMessage()); 42 }catch(Exception e) { 43 e.printStackTrace(); 44 System.err.println(e.getLocalizedMessage()); 45 46 } } 47 } Odczyt i zapis znakowy realizowany jest przez inne klasy, których funkcjonalność odpowiada przedstawionym wyżej klasom. Do tych klas należą: Reader, Writer, FileReader, FileWriter, StringReader, StringWriter, CharArrayReader, CharArrayWriter, PipedReader i PipedWriter. Dodatkowo istnieją klasy umożliwiające konwersję strumieni z bajtowych na znakowe. Są to InputStreamReader i OutputStreamWriter. Klasy odczytu i zapisu znakowego posiadają również własne dekoratory: FilterReader, FilterWriter, BufferReader, BufferWriter, PrintWriter, LineNumberReader, PushBackReader i StreamTokenizer. Ostatni dekorator służy do rozpoznawania wzorców w odczytywanych ze strumienia danych i od wersji 1.4 Javy w dużej mierze został zastąpiony przez klasy realizujące rozpoznawanie wzorców (ang. pattern matching). Oto dwa przykłady użycia pokazujące w jaki sposób skorzystać ze znakowych operacji odczytu. Pierwszy czyta dane z pliku, natomiast drugi konwertuje strumień związany ze standardowym wejściem na strumień znakowy i wypisuje jego zawartość na ekran: 1 import java.io.*; 2 class Odczyt { 3 private FileReader fs; 4 5 public Odczyt(String s) { 6 try { 7 fs = new FileReader(s); 8 }catch(FileNotFoundException e) { 9 e.printStackTrace(); 10 System.err.println(e.getLocalizedMessage()); 11 }catch(Exception e) { 12 e.printStackTrace(); 13 System.err.println(e.getLocalizedMessage()); 14 15 } } 16 17 18 public void czytaj() throws IOException { try { 19 char a; 20 while((a=(char)fs.read())!=(char)-1) System.out.print(a); 6 Programowanie Obiektowe (Java) 21 System.out.println(); 22 }catch(IOException e) { 23 e.printStackTrace(); 24 System.err.println(e.getLocalizedMessage()); 25 }catch(Exception e) { 26 e.printStackTrace(); 27 System.err.println(e.getLocalizedMessage()); 28 }finally { 29 fs.close(); 30 } 31 } 32 } 33 34 public class OdczytZnakowy { 35 public static void main(String[] args) { 36 Odczyt o = new Odczyt("OdczytZnakowy.java"); 37 try { 38 o.czytaj(); 39 }catch(IOException e) { 40 e.printStackTrace(); 41 System.err.println(e.getLocalizedMessage()); 42 }catch(Exception e) { 43 e.printStackTrace(); 44 System.err.println(e.getLocalizedMessage()); 45 } 46 } 47 } import java.io.*; public class Input { public static void main(String[] args) { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try { System.out.println(br.readLine()); } catch(Exception e) { e.printStackTrace(); } } } 7 Programowanie Obiektowe (Java) Kolejny przykład pokazuje w jaki sposób użyć dekoratora BufferReader do odczytu pliku: 1 import java.io.*; 2 class Odczyt { 3 private BufferedReader br; 4 5 public Odczyt(String s) { 6 try { 7 br = new BufferedReader(new FileReader(s)); 8 }catch(FileNotFoundException e) { 9 e.printStackTrace(); 10 System.err.println(e.getLocalizedMessage()); 11 }catch(Exception e) { 12 e.printStackTrace(); 13 System.err.println(e.getLocalizedMessage()); 14 15 } } 16 17 public void czytaj() throws IOException { 18 try { 19 String a; 20 while((a=br.readLine())!=null) System.out.println(a); 21 System.out.println(); 22 }catch(IOException e) { 23 e.printStackTrace(); 24 System.err.println(e.getLocalizedMessage()); 25 }catch(Exception e) { 26 e.printStackTrace(); 27 System.err.println(e.getLocalizedMessage()); 28 }finally { 29 br.close(); 30 31 } } 32 } 33 34 public class OdczytBuforowany { 35 public static void main(String[] args) { 36 Odczyt o = new Odczyt("OdczytBuforowany.java"); 37 try { 38 39 40 o.czytaj(); }catch(IOException e) { e.printStackTrace(); 8 Programowanie Obiektowe (Java) 41 System.err.println(e.getLocalizedMessage()); 42 }catch(Exception e) { 43 e.printStackTrace(); 44 System.err.println(e.getLocalizedMessage()); 45 46 } } 47 } Opisane wyżej klasy służą do realizacji operacji wejścia – wyjścia na zasadzie dostępu sekwencyjnego, jeśli jest nam potrzebny dostęp swobodny, możemy skorzystać z metod klasy RandomAccesFile. Java standardowo zawiera również klasy umożliwiające zapis i odczyt plików skompresowanych. Istnieją również dwie klasy, związane ze strumieniami binarnymi (bajtowymi), które umożliwiają zapis i odczyt obiektów. Takie operacje nazywa się operacjami serializacji obiektów. Te klasy to ObjectInputStream i ObjectOutputStream. Pierwsza posiada metodę readObject(), druga writeObject(). Aby obiekt mógł być zapisany do strumienia jego klasa musi implementować interfejs Serializable, który nie deklaruje żadnej metody, a pełni tylko funkcję znacznika, wskazującego, że obiekt danej klasy może być zapisany lub odczytany ze strumienia. Należy pamiętać o tym, że metoda readObject() zwraca referencję klasy Object, a więc konieczne jest rzutowanie w dół, aby odtworzyć właściwą klasę obiektu. Jeśli nie chcemy, aby pewne informacje przechowywane w polach obiektu były zapisywane w pliku, to takie pole deklarujemy z użyciem słowa kluczowego transient. Inną metoda polega na implementowaniu interfejsu Externalizable zamiast Serializable i zdefiniowaniu jego metod writeExternal() i readExternal(), w których trzeba zawrzeć kod odpowiedzialny za zapis poszczególnych pól obiektu. 9