OPERACJE I/O: CZYTANIE I ZAPISYWANIE Aby pobrać dane, program otwiera strumień (stream) do źródła informacji (pliku, pamięci, soketu) i sekwencyjnie czyta te informacje. W ten sam sposób odbywa się wysyłanie informacji do zewnętrznych lokalizacji – poprzez otwarcie strumienia i sekwencyjnego wprowadzania danych. Bez względu na miejsce zapisu lub odczytu danych oraz na typ tych danych algorytm sekwencyjnego odczytu i zapisu jest taki sam: STRUMIENIE ZNAKOWE Reader i Writer to abstrakcyjne superklasy dla strumieni znakowych java.io. Reader zapewnia API i częściową implementację dla readers – strumieni zapisujących 16-bitowe znaki. Podklasy klas Reader i Writer implementują strumienie specjalne i dzielą się na dwie kategorie: - te które czytają lub zapisują do danych typu sinks (pokazane na szaro) - te które przeprowadzają niektóre rodzaje przetwarzania danych (pokazane na biało) STRUMIENIE BAJTÓW Aby czytać i zapisywać bajty 8-bitowe, programy powinny korzystać ze strumieni bajtów, potomków klas InputStream OutputStream dostarczają interfejs API. i OutputStream. InputStream i Reader I InputStream definiują podobne API ale dla innych typów danych. Na przykład Reader zawiera metody do czytania znaków i tablic znaków: int read() int read(char cbuf[]) int read(char cbuf[], int offset, int length) InputStream definiuje te same metody ale do czytania bajtów i tablic bajtów: int read() int read(byte cbuf[]) int read(byte cbuf[], int offset, int length) Ponadto zarówno Reader jak i InputStream zapewniają metody do oznaczania położenia w strumieniu, zmiany wejść i repetowania aktualnego położenia. Writer i OutputStream są bardzo podobne. Writer definiuje metody zapisu znaków i tablic znaków: int write(int c) int write(char cbuf[]) int write(char cbuf[], int offset, int length) OutputStream definiuje te same metody w odniesieniu do bajtów: int write(int c) int write(byte cbuf[]) int write(byte cbuf[], int offset, int length) Wszystkie streams--readers, writers, input streams, and output streams są automatycznie otwierane przy ich tworzeniu. POSŁUGIWANIE SIĘ STRUMIENIAMI Strumienie I/O: JAK KORZYSTAĆ ZE STRUMIENI PLIKOWYCH Strumienie plikowe są prawdopodobnie najłatwiejszymi do zrozumienia rodzajami strumieni. Wszystkie strumienie: FileReader, FileWriter, FileInputStream, i FileOutputStream czytają lub zapisują do pliku w rodzimym systemie plików. import java.io.*; public class Copy { public static void main(String[] args) throws IOException { File inputFile = new File("farrago.txt"); File outputFile = new File("outagain.txt"); FileReader in = new FileReader(inputFile); FileWriter out = new FileWriter(outputFile); int c; while ((c = in.read()) != -1) out.write(c); in.close(); out.close(); } } Powyższy program jest bardzo prosty. Otwiera on FileReader do pliku farrago.txt oraz otwiera FileWriter do pliku outagain.txt. Program czyta znaki tak długo, aż skończą się dane w pliku wejściowym i zapisuje te dane. Poniżej pokazano kod wykonujący tworzenie file reader: File inputFile = new File("farrago.txt"); FileReader in = new FileReader(inputFile); Kod ten tworzy obiekt typu File – klasy użytkowej dostarczanej przez java.io. Po uruchomieniu w aktualnym katalogu powinna się znaleźć kopia pliku farrago.txt w zbiorze outagain.txt. Oto zawartość tego pliku: So she went into the garden to cut a cabbage-leaf, to make an apple-pie; and at the same time a great she-bear, coming up the street, pops its head into the shop. 'What! no soap?' So he died, and she very imprudently married the barber; and there were present the Picninnies, and the Joblillies, and the Garyalies, and the grand Panjandrum himself, with the little round button at top, and they all fell to playing the game of catch as catch can, till the gun powder ran out at the heels of their boots. Samuel Foote 1720-1777 JAK KORZYSTAĆ ZE STRUMIENI TYPU PIPE Strumienie typu PIPE są używane do łączenia wyjścia jednego wątku do wejścia innego. PipedReader i PipedWriter implementują komponenty wejścia i wyjścia. Bez strumieni typu Pipe programy musiałyby przechowywać gdzieś swoje rezultaty pomiędzy swoimi krokami, tak jak poniżej: Korzystając ze strumieni typu Pipe programy mogą wyprowadzać wyjście jednej metody do wejścia innej: Poniższy program implementuje powyższy schemat: import java.io.*; public class RhymingWords { public static void main(String[] args) throws IOException { FileReader words = new FileReader("words.txt"); // do the reversing and sorting Reader rhymedWords = reverse(sort(reverse(words))); // write new list to standard out BufferedReader in = new BufferedReader(rhymedWords); String input; while ((input = in.readLine()) != null) System.out.println(input); in.close(); } public static Reader reverse(Reader source) throws IOException { BufferedReader in = new BufferedReader(source); PipedWriter pipeOut = new PipedWriter(); PipedReader pipeIn = new PipedReader(pipeOut); PrintWriter out = new PrintWriter(pipeOut); new ReverseThread(out, in).start(); return pipeIn; } public static Reader sort(Reader source) throws IOException { BufferedReader in = new BufferedReader(source); PipedWriter pipeOut = new PipedWriter(); PipedReader pipeIn = new PipedReader(pipeOut); PrintWriter out = new PrintWriter(pipeOut); new SortThread(out, in).start(); return pipeIn; } } Połączenie utworzone w programie kreuje łańcuch typu pipe: Klasa SequenceInputStream tworzy pojedynczy strumień z wielu wejść źródłowych. Oto przykładowy program: import java.io.*; public class Concatenate { public static void main(String[] args) throws IOException { ListOfFiles mylist = new ListOfFiles(args); SequenceInputStream s = new SequenceInputStream(mylist); int c; while ((c = s.read()) != -1) System.out.write(c); s.close(); } } import java.util.*; import java.io.*; public class ListOfFiles implements Enumeration { private String[] listOfFiles; private int current = 0; public ListOfFiles(String[] listOfFiles) { this.listOfFiles = listOfFiles; } public boolean hasMoreElements() { if (current < listOfFiles.length) return true; else return false; } public Object nextElement() { InputStream in = null; if (!hasMoreElements()) throw new NoSuchElementException("No more files."); else { String nextElement = listOfFiles[current]; current++; try { in = new FileInputStream(nextElement); } catch (FileNotFoundException e) { System.err.println("ListOfFiles: Can't open " + nextElement); } } return in; } } Praca z filtrami łańcuchów Pakiet java.io dostarcza zbiór klas abstrakcyjnych, które definiują i częściowo implementują filtry strumieni. Filtry te filtrują dane przeczytane lub zapisywane do pliku. Filtry strumieni to klasy FilterInputStream, FilterOutputStream, FilterInputStream, i FilterOutputStream. Większość filtrów strumieni dostarczanych przez pakiet java.io to podklasy klas FilterInputStream i FilterOutputStream. Są one wypisane poniżej: - DataInputStream i DataOutputStream - BufferedInputStream i BufferedOutputStream - LineNumberInputStream - PushbackInputStream - PrintStream Aby użyć filtrowanego wejścia lub wyjścia łańcucha, dowiązuje się filtrowany łańcuch do innego wejścia lub wyjścia podczas jego tworzenia: BufferedReader d = new BufferedReader(new DataInputStream(System.in)); String input; while ((input = d.readLine()) != null) { ... //do something interesting here } JAK UŻYWAĆ KLAS DATAINPUTSTREAM I DATAOUTPUTSTREAM Przykładowy program czyta i zapisuje dane sformatowane w kolumny oddzielone tabulatorami. Kolumny zawierają ceny sprzedaży, ilość towarów i opis towarów. 19.99 12 9.99 8 Java T-shirt Java Mug Klasa DataOutputStream, tak jak inne strumienie filtrowane, musi być dowiązana do innej klasy OutputStream. W tym przypadku jest dowiązana do klasy FileOutputStream, która jest ustawiona do zapisu do pliku invoice1.txt: DataOutputStream out = new DataOutputStream( new FileOutputStream("invoice1.txt")); Następnie, DataIODemo korzysta z metod zapisu klasy DataOutputStream do zapisu danych: for (int i = 0; i < prices.length; i ++) { out.writeDouble(prices[i]); out.writeChar('\t'); out.writeInt(units[i]); out.writeChar('\t'); out.writeChars(descs[i]); out.writeChar('\n'); } out.close(); Następnie, klasa DataIODemo otwiera strumień DataInputStream do właśnie zapisywanego pliku: DataInputStream in = new DataInputStream( new FileInputStream("invoice1.txt")); DataInputStream musi też być dowiązany do innej klasy InputStream. try { while (true) { price = in.readDouble(); in.readChar(); //throws out the tab unit = in.readInt(); in.readChar(); //throws out the tab char chr; desc = new StringBuffer(20); char lineSep = System.getProperty("line.separator").charAt(0); while ((chr = in.readChar() != lineSep) { desc.append(chr); } System.out.println("You've ordered " + unit + " units of " + desc + " at $" + price); total = total + unit * price; } } catch (EOFException e) { } System.out.println("For a TOTAL of: $" + total); in.close(); Kiedy przeczytane są wszystkie dane, DataIODemo wyświetla wyrażenie reasumujące szyk i należną całkowitą wartość i zamyka strumień. Należy zwrócić uwagę na pętle używaną przez DataIODemo do czytania danych ze strumienia DataInputStream. Zwykle, gdy dane są czytane, można zobaczyć taką pętlę: while ((input = in.read()) != null) { ... } Metoda odczytu zwraca wartość null, która oznacza osiągnięcie końca pliku. Wiele metod DataInputStream nie potrafi tego zrobić. Po uruchomieniu programu DataIODemo na wyjściu pojawia się następująca sewencja: You've ordered 12 units of Java T-shirt at $19.99 You've ordered 8 units of Java Mug at $9.99 You've ordered 13 units of Duke Juggling Dolls at $15.99 You've ordered 29 units of Java Pin at $3.99 You've ordered 50 units of Java Key Chain at $4.99 For a TOTAL of: $892.8800000000001 SERIALIZACJA OBIEKTÓW Dwa strumienie w java.io - ObjectInputStream i ObjectOutputStream są strumieniami bajtów i pracują jak inne strumienie wejścia-wyjścia. Jakkolwiek, są one specjalne gdyż mogą czytać i zapisywać obiekty. Kluczem do zapisywania obiektów jest zapisywanie ich stanów w serializowanej formie odpowiedniej do rekonstrukcji tego obiektu takim, jakim był po odczytaniu. W ten sposób czytanie i zapisywanie obiektów jest procesem nazywanym serializacją. Serializacja obiektów jest niezbędna do budowy prawie wszystkich aplikacji. Z serializacji obiektów można korzystać na następujące sposoby: - Remote Method Invocation (RMI) - komunikacja pomiędzy obiektami i soketami - Lightweight persistence – archiwacja obiektów w celu ich użycia w późniejszym wywołaniu tego samego programu Praca z plikami o losowym dostępie Strumienie o sekencyjnym dostępie muszą zapisywać swoje dane sekwencyjnie. Chociaż takie strumienie są niezwykle przydatne, są one konsekwencją zastosowania medium sekwencyjnego, takiego jak papier i taśma magnetyczna. Pliki o losowym dostępie zapewniają niesekwencyjny, lub też losowy, dostep do swoich zawartości. Do czego przydatne są pliki o losowym dostępie? Rozważmy format archiwizacji ZIP: Przypuśćmy że chcemy rozpakować wybrany plik z zarchiwum ZIP. Korzystając ze strumienia sekwencyjnego, należałoby: - otworzyć archiwum - przeszukać archiwum i zlokalizować pożądany plik - rozpakować plik - zamknąć archiwum Posługując się takim algorytmem, średnio trzeba przeczytać połowę archiwum zanim znajdzie się poszukiwany zbiór. Ten sam plik można rozpakować bardziej efektywnie, korzystając z usługi wyszukiwania pliku o losowym dostępie i posługując się algorytmem: - otworzyć archiwum - przeszukać katalog archiwum i zlokalizować wpis dla szukanego pliku - wyszukać (od tyłu) pozycję pliku - rozpakować plik - zamknąć archiwum Algorytm ten jest bardziej efektywny z powodu tego, że odczytywany jest jedynie katalog archiwum i plik przeznaczony do rozpakowania.