Wejście/wyjście w Javie.
Problemy :
rozmaitość obsługiwanych żródeł danych i celów ( konsola klawiatura, inne urządzenia, pliki, pamięć, łącza itp.)
konieczność zachowania przenośności na różne platformy
System wejścia/wyjścia jest oparty na pojęciu strumienia (znakowego lub binarnego ) .
Realizowany przez hierarchię klas - na pierwszy rzut oka bardzo rozbudowaną, ale i tak w miarę prosty w wyniku wykorzystania jednego z rodzajów wzorców projektowych - dekoratorów.
Na szczęście większość z nich jest przeznaczona do bardziej złożonych działań - wykorzystanie systemu do wykonywania podstawowych zadań ( drukowanie wyników , wczytywanie danych, współpraca z plikami ) jest stosunkowo proste.
Podstawowe klasy hierarchii to : InputStraem ( wejście binarne), OutputStream ( wyjście binarne), Reader(wejście znakowe) i Writer (wyjście znakowe). Podstwowe metody to : read - dla wejścia i write - dla wyjścia.
Dla ułatwienia podzieliłem wykład na dwie części :
strumienie standardowe ( tekstowy wydruk wyników na ekranie i wczytywanie danych z klawiatury)
wykorzystanie plików ( tekstowy i binarny zapis/odczyt).
Zgodnie z przyjętym założeniem przekazuję tylko podstawowe informacje umożliwiające efektywne wykorzystanie strumieni ( większość z tych działań można zrealizować na wiele sposobów - zazwyczaj będę ograniczał się do jednego).
Standardowe strumienie znakowe
Drukowanie wyników
Przy prostych wydrukach wystarczy wykorzystanie standardowego strumienia out ( obiekt klasy PrintStream) z klasy System z podstawowymi metodami : print, println i printf ( wydruk z formatowaniem). Jeśli zależy nam na efektywności i przenośności lepiej jest definiować własny strumień klasy PrintWriter , np. :
PrintWriter wyj= new PrintWriter( new BufferedWriter(new OutputStreamWriter (System.out)),true);
Do bardziej złożonych wydruków ( np. tabele, wykazy) należy stosować metodę printf.
Parametr format zawiera bezpośredni tekst i opis sposobu drukowania argumentów.
Ogólna postać formatu dla pojedynczego argumentu :
%[argument_index$][flags][width][.precision]conversion
Elementy w nawiasach [] są opcjonalne.
argument_index numer argumentu ( od 1) , jeśli jest opuszczony drukuje kolejny (liczone są tylko nieindeksowane) element, indeks < oznacza poprzedni argument ( umożliwia wielokrotny wydruk jednego argumentu)
flags ciąg znaków modyfikujących sposób wydruku ( znaczenie zależy od typu drukowanego elementu)
width minimalna szerokość pola wydruku
precision górne ograniczenie szerokości pola wydruku ( znaczenie zależy od typu drukowanego elementu)
conversion sposób formatowania elementu ( zbiór poprawnych konwersji zależy od typu elementu)
Formaty dla róznych typów argumentów :
- boolean
%[argument_index$][flags][width][.precision]conversion
konwersja : b lub B (wydruk wielkimi literami)
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
precyzja - maksymalna szerokość pola (brak oznacza brak ograniczeń)
flaga - '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
- char
%[argument_index$][flags][width]conversion
konwersja : c lub C (wydruk wielkimi literami)
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
flaga - '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
Uwagi : aby wydrukować znak procentu podajemy %% - format bezargumentowy
%n - oznacza niezależny od platformy znak nowej linii - bezargumentowy
- typy całkowite ( byte, short,int,long)
%[argument_index$][flags][width]conversion
konwersja : d
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
flagi: '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
'0' uzupełnianie pola 0 a nie spacją
'+' znak jest zawsze drukowany
' ' dla liczb dodatnich zamiast znaku drukowana spacja
'(' liczby ujemne są drukowane w ( ) ale bez znaku -
- typy rzeczywiste (float, double)
%[argument_index$][flags][width][.precision]conversion
konwersja : e lub E postać naukowa (wykładnicza)
f postać stałoprzecinkowa
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
precyzja - liczba miejsc po kropce dziesiętnej uzupełniana 0 lub obcinana przez zaokrąglenie ( domyślnie 6)
flaga - '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
'#' wymusza druk kropki dziesiętnej nawet jeśli część ulamkowa jest 0
- napisy (również obiekty ze zdefiniowaną metodą toString)
%[argument_index$][flags][width][.precision]conversion
konwersja : s lub S (wydruk wielkimi literami)
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
precyzja - maksymalna szerokość pola (brak oznacza brak ograniczeń)
flaga - '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
- data/czas (klasy Calendar i Date)
%[argument_index$][flags][width][.precision]conversion
konwersja : t lub T (pierwszy znak , konwersja jest zawsze dwuznakowa)
po szczegółowy opis znaczenia drugiego znaku odsyłam do helpa
szerokość - minimalna szerokość pola ( uzupełniana spacjami)
precyzja - maksymalna szerokość pola (brak oznacza brak ograniczeń)
flaga - '-' wyrównanie do lewego brzegu pola ( domyślne wyrównanie do prawego)
Uwagi :
- jeżeli wydruk nie kończy się znakiem nowej linii należy użyć metody flush( ) do natychmiastowego wypisania na ekranie
- podobne metody ma metoda String.format(...) szczególnie przydatna do przeciążania metody toString dla obiektów ( ale nie tylko )
Wczytywanie danych
W wielu wypadkach do wczytywania danych wystarczą proste klasy ( InputStream lub Reader ) jednak wygodniejsze ( i szybsze ) jest użycie buforowanego wejścia ( BufferedReader), a najbardziej elastyczne jest wykorzystanie klasy dzielącej zawartość strumienia na proste elementy tzw. tokeny ( StreamTokenizer ).
Deklaracja zmiennych :
BufferedReader wej= new BufferedReader( new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(wej); // argument typu Reader
Uwaga : praktycznie wszystkie metody wejściowe zgłaszają wyjątek IOException, który należy obsłużyć lub przekazać do metody wywołującej.
Podstawowe metody klasy BufferedReader :
public int read()throws IOException
wczytanie pojedynczego znaku ( -1 gdy koniec strumienia)
public int read(char[] cbuf,int off,int len)throws IOException
wczytanie ciągu znaków do fragmentu ( można opuścić off i len) tablicy znakowej ( wynik - liczba wczytanych znaków lub -1 - próba czytania na końcu strumienia )
public String readLine()throws IOException
wynikiem jest zawartość wczytanej linii tekstu
public long skip(long n) throws IOException
pomiń n znaków
public boolean ready()throws IOException
true jeśli w strumieniu są jakieś znaki
public int available()throws IOException
liczba znaków oczekujących w strumieniu ( jest to metoda klasy InputStream dostępna dzięki użyciu strumienia System.in)
Aby wczytać wartość liczbową używamy metody readln, a następnie odpowiedniej metody parse ( Boolean, Int , Double ...). Niestety metody te są wrażliwe ( zgłaszają NumberFormatException) na niepotrzebne znaki ( nawet spacje) więc trzeba je samemu usuwać . Tej wady nie ma StreamTokenizer.
Uwaga: warto przeanalizować możliwości klasy Scanner.
Klasa ta może rozpoznawać : nazwy(słowa), liczby, stringi i komentarze. Typowy sposób wykorzystania to :
ustawienie parametrów do analizy strumienia
void |
commentChar(int ch) definiuje znak będący początkiem komentarza wierszowego |
void |
eolIsSignificant(boolean flag) Określa czy nowa linia jest traktowana jako token |
void |
lowerCaseMode(boolean fl) wymusza zmianę znaków słowa na małe litery |
void |
ordinaryChar(int ch)
|
void |
ordinaryChars(int low,
int hi) |
void |
quoteChar(int ch)
|
void |
resetSyntax()
|
void |
slashSlashComments(boolean flag)
|
void |
slashStarComments(boolean flag)
|
void |
whitespaceChars(int low,
int hi) |
void |
wordChars(int low,
int hi) |
odczytywanie i przetwarzanie kolejnych tokenów, przez wywołanie :
wej.nextToken( )
wynikiem jest typ tokena ( można go również odczytać z pola wej.ttype )
TT_EOF - koniec strumienia
TT_EOL - koniec linii ( tylko gdy ustawiony jest .....)
TT_NUMBER - wartość liczbowa, umieszczona w polu val (typu double)
TT_WORD - nazwa(napis) , umieszczony w polu sval ( typu String).
Dodatkowe metody :
int |
lineno()
|
void |
pushBack()
|
Uwaga: nie trzeba zamykać strumieni zbudowanych na bazie strumieni standardowych ( out, in, err).
W przypadku prowadzenia analizy danych zapisanych w strumieniu przez wiele różnych metod bardzo przydatna jest możliwość zwrotu (lub wręcz wstawienia ) do strumienia ostatnio odczytanego znaku( lub ciągu znaków). Taką możliwość daje metoda unread( ..) klasy PushBackReader.
Przykład :
import java.util.*;
import java.io.*;
public class ComparatorPublikacji implements Comparator<Publikacja>,Serializable
{ public int compare(Publikacja p1,Publikacja p2)
{ return p1.autor.compareTo(p2.autor)!=0 ? p1.autor.compareTo(p2.autor) : p1.tytul.compareTo(p2.tytul);}
}
// interfejs utworzony tylko po to, żeby przekazywać strumieni jako parametry metod
import java.io.*;
public interface Strumien
{ PrintWriter wyj= new PrintWriter( new BufferedWriter(new OutputStreamWriter(System.out)),true);
BufferedReader wej= new BufferedReader( new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(wej); // argument typu Reader
}
import java.io.*;
public abstract class Publikacja implements Comparable, Cloneable, Serializable, Strumien
{ String tytul;
String autor;
int rok;
Publikacja()
{ wyj.print(" Podaj Tytul Autora i Rok "); wyj.flush();
try {st.nextToken();
tytul=st.sval;
st.nextToken();
autor=st.sval;
while(st.nextToken()!=st.TT_NUMBER);
rok=(int)st.nval;
}catch(IOException e) {System.out.println("Publikacja Blad wczytywania");}
}
public Publikacja clone()
{ try{ Publikacja p=(Publikacja)super.clone();
p.tytul= new String(tytul);
p.autor=new String(autor);
return p;
} catch(CloneNotSupportedException e){return null;}
}
public String toString()
{ return String.format("%-30s %-15s %6d ", tytul,autor,rok); }
public boolean equals(Publikacja p)
{ return p!=null && tytul.equals(p.tytul) && autor.equals(p.autor) ; }
public int hashCode()
{ return tytul.hashCode() & autor.hashCode(); }
public int compareTo(Object p1)
{ Publikacja p=(Publikacja)p1;
return (tytul.compareTo(p.tytul)!=0 ? tytul.compareTo(p.tytul) : autor.compareTo(p.autor));
}
}
import java.io.*;
public class Ksiazka extends Publikacja
{ String wydawnictwo;
public Ksiazka()
{ super();
try{ wyj.print(" Podaj wydawnictwo ");wyj.flush();
st.nextToken();
wydawnictwo=st.sval;
} catch(IOException e){ System.out.println(" Ksiazka Blad wejscia ");}
}
public String toString()
{ return super.toString() + String.format(" Ksiazka wydana przez %s ",wydawnictwo);}
}
import java.io.*;
public class Artykol extends Publikacja
{ String czasopismo;
public Artykol()
{super();
wyj.print(" w czasopismie ");wyj.flush();
try{ st.nextToken();
czasopismo=st.sval;
}catch(IOException e) { System.out.println(" Artykol Blad wejscia ");}
}
public String toString()
{ return super.toString() + String.format(" Artykol w : %s",czasopismo);}
}
import java.util.*;
import java.io.*;
public class test implements Strumien
{
SortedSet<Publikacja> s = new TreeSet<Publikacja>();
public void druk(Collection<Publikacja> s)
{ for(Publikacja el:s)
wyj.printf("%s %n" ,el);
wyj.println();
}
public void nowaPublikacja()
{ String odp="";
while(!(odp.equals("Ksiazka")||odp.equals("Artykol")))
{ wyj.println(" Ksiazka czy Artykol ");
try{ st.nextToken();
odp=st.sval;
} catch(IOException e) { System.out.println(" nowa Publikacja Blad wejscia ");return;}
}
s.add(odp.equals("Ksiazka")? new Ksiazka() : new Artykol());
}
public void druk1(Iterator<Publikacja> it)
{ while(it.hasNext())
System.out.println(it.next()+" , ");
System.out.println();
}
public void test()
{ char odp = ' ';
do { wyj.println(" Co chcesz robic : Dodaj publikacje, Wypisz liste, Koniec ");
try { st.nextToken(); }
catch(IOException e){ System.out.println(" Blad menu "); return;}
odp=st.sval.toLowerCase().charAt(0);
switch(odp)
{case 'd' : nowaPublikacja();break;
case 'w' : druk(s); break;
case 'k' : break;
default : wyj.printf(" Nie ma takiej opcji %n");
}
} while (odp!='k');
}
}
Strumienie plikowe
Do czego wykorzystujemy pliki :
przygotowywanie dużych zestawów danych wejściowych
do zapisu obszernych wyników
do przechowywania zgromadzonych danych między kolejnymi uruchomieniami aplikacji
do pracy z wielkimi ( nie mieszczącymi się w pamięci operacyjnej ) zbiorami danych.
1 i 2 - pliki tekstowe
3 i 4 - pliki binarne ( do 4 najlepsze są pliki o dostępie bezpośrednim).
Uwaga : strumienie plikowe trzeba po wykorzystaniu zamykać używając metody close( ).
Przy wykorzystaniu plików tekstowych mamy dwa rozwiązania :
przekierowanie strumieni standardowych
Przez wywołanie metody z klasy System :
static void |
setErr(PrintStream err) |
static void |
setIn(InputStream in) |
static void |
setOut(PrintStream out) |
System.setIn("dane.txt");
System.setOut("wyniki.txt");
PrintWriter wyj= new PrintWriter( new BufferedWriter(new OutputStreamWriter(System.out)),true);
BufferedReader wej= new BufferedReader( new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(wej);
wykorzystanie klas : FileReader i FileWriter
Konstruktory ( analogiczne dla FileWriter):
public FileReader(String fileName) throws FileNotFoundException
public FileReader(File file) throws FileNotFoundException
public FileWriter(String fileName) throws FileNotFoundException
public FileWriter(File file) throws FileNotFoundException
public FileWriter(String fileName, boolean append) throws FileNotFoundException
public FileWriter(File file, boolean append) throws FileNotFoundException
np:
PrintWriter wyj= new PrintWriter( new BufferedWriter(new FileWriter("wyniki.txt")),true);
lub prościej
PrintWriter wyj= new PrintWriter("wyniki.txt");
BufferedReader wej= new BufferedReader( new FileReader("dane.txt"));
StreamTokenizer st = new StreamTokenizer(wej);
Wykorzystanie tak jak to opisano w poprzednim wykładzie.
Przy plikach binarnych wykorzystujemy klasy : FileInputStream i FileOutputStream ( dla prostych danych ) lub ObjectInputStream i ObjectOutputStream ( dla obiektów).
Konstruktory ( analogiczne dla FileOutputStream + opcjonalny parametr append):
public FileInputStream(String fileName) throws FileNotFoundException
public FileInputStream(File file) throws FileNotFoundException
Np.
FileInputString wej = new FileInputString("baza.dta");
FileOutputString wyj = new FileOutputString("baza.dta");
Podstawowe metody raead/write są przeciążone dla wszystkich typów prostych i ciągów bajtów.
Do pracy z obiektami powinniśmy korzystać z klas ObjectInputStream i ObjectOutputStream.
np.
ObjectInputStream wejOb= new(ObjectInputStream(wej));
ObjectOutputStream wyjOb= new(ObjectOutputStream(wyj));
Dodatkowe metody to :
public final Object readObject( ) throws IOException,ClassNotFoundException
public final void writeObject(Object obj) throws IOException
Uwaga: jeśli obiekt ma pola typu obiektowego należy wykorzystać mechanizm serializacji\deserializacji. Każda z klas występujących w grafie zapisywanych obiektów musi implementować interfejs Serializable. Po zapisie i odczycie mamy odtworzony cały graf obiektów ( identyczne obiekty składowe nie są powielane).
Przy pracy bezpośrednio z plikiem ( plik jako wielka tablica w pamięci zewnętrznej ) wykorzystujemy klasę RandomAccesFile ( nie jest to podejście strumieniowe).
Konstruktory:
public RandomAccessFile(String name,String mode)throws FileNotFoundException
public RandomAccessFile(File file,String mode)throws FileNotFoundException
Tryby dostępu to :
r - tylko odczyt
rw - odczyt i zapis
rws, rwd - fizyczny zapis zmian po każdej modyfikacji
Podstawowe metody to przciążone read/write i close.
Dodatkowe metody udostępniane przez klasę :
long getFilePointer( ) - daj bieżącą pozycję kursora pliku
long length( ) - długość (wielkość) pliku
void seek ( long pos) - ustaw kursor na pozycję
void setLength(long newLength) - zmień wielkość pliku
void skipBytes(int n ) - przesuń kursor o n bajtów
Jeśli potrzebujemy odczytać informacje o pliku , manipulować plikami lub kartotekami powinniśmy wykorzystać klasę File.
Podstawowy konstruktor : File( String nazwa)
Wybrane ( proste metody) :
boolean |
canRead() można czytać z pliku. |
boolean |
canWrite() można zapisywać do pliku. |
boolean |
createNewFile() utwórz nowy ( pusty ) plik. |
static File |
createTempFile(String prefix,
String suffix)
|
boolean |
delete() Skasuj plik. |
boolean |
isDirectory() Czy to kartoteka. |
boolean |
isFile() czy to plik. |
long |
length() wielkość pliku. |
boolean
|
Przykład wykorzystania plików ( pomijam klasy, które nie uległy zmianie ).
import java.util.*;
import java.io.*;
public class test implements Strumien
{
public void test() throws IOException,ClassNotFoundException
{ PrintWriter plikWyj= new PrintWriter("wyniki.txt");
Spis spis=new Spis();
spis.wczytaj();
char odp = ' ';
do { wyj.println(" Co chcesz robic : Dodaj publikacje, Wypisz liste, Save, Restore, Koniec ");
st.nextToken(); }
odp=st.sval.toLowerCase().charAt(0);
switch(odp)
{case 'd' : spis.nowaPublikacja(st,true);break;
case 'w' : plikWyj.printf("%s %n",spis); break;
case 's' : spis.save(); break;
case 'r' : spis=spis.restore(); break;
case 'k' : break;
default : wyj.printf(" Nie ma takiej opcji %n");
}
} while (odp!='k');
if(plikWyj != null) plikWyj.close();
}
}
import java.util.*;
import java.io.*;
public class Spis implements Strumien,Serializable
{ SortedSet<Publikacja> s = new TreeSet<Publikacja>();
public String toString()
{ String wynik=new String();
for(Publikacja el:s)
wynik=wynik+String.format("%s %n" ,el);
return wynik;
}
public void nowaPublikacja(StreamTokenizer st,boolean standard)throws IOException
{ String odp="";
while(!(odp.equals("Ksiazka")||odp.equals("Artykol")))
{ if(standard) {wyj.println(" Ksiazka czy Artykol ");}
st.nextToken();
odp=st.sval;
}
s.add(odp.equals("Ksiazka")? new Ksiazka(st,standard) : new Artykol(st,standard));
}
public void save()throws IOException
{ ObjectOutputStream plik = new ObjectOutputStream(new BufferedOutputStream( new FileOutputStream("baza.dta")));
plik.writeObject(this);
plik.close();
}
public Spis restore()throws IOException, ClassNotFoundException
{ ObjectInputStream plik = new ObjectInputStream(new BufferedInputStream( new FileInputStream("baza.dta")));
Spis tmp=(Spis)plik.readObject();
plik.close();
return tmp;
}
public void wczytaj() throws IOException
{ StreamTokenizer plikWej=new StreamTokenizer(new FileReader("dane.txt"));
while(plikWej.nextToken()!=plikWej.TT_EOF)
{plikWej.pushBack();
nowaPublikacja(plikWej,false);
}
}
}
import java.io.*;
public abstract class Publikacja implements Comparable, Cloneable, Serializable, Strumien
{ String tytul;
String autor;
int rok;
Publikacja(StreamTokenizer st,boolean standard)throws IOException
{ if(standard){wyj.print(" Podaj Tytul Autora i Rok "); wyj.flush();}
st.nextToken();
tytul=st.sval;
st.nextToken();
autor=st.sval;
while(st.nextToken()!=st.TT_NUMBER);
rok=(int)st.nval;
}
public Publikacja clone()
{ try{ Publikacja p=(Publikacja)super.clone();
p.tytul= new String(tytul);
p.autor=new String(autor);
return p;
} catch(CloneNotSupportedException e){return null;}
}
public String toString()
{ return String.format("%-30s %-15s %6d ", tytul,autor,rok); }
public boolean equals(Publikacja p)
{ return p!=null && tytul.equals(p.tytul) && autor.equals(p.autor) ; }
public int hashCode()
{ return tytul.hashCode() & autor.hashCode(); }
public int compareTo(Object p1)
{ Publikacja p=(Publikacja)p1;
return (tytul.compareTo(p.tytul)!=0 ? tytul.compareTo(p.tytul) : autor.compareTo(p.autor));
}
}
import java.io.*;
public class Ksiazka extends Publikacja
{ String wydawnictwo;
public Ksiazka(StreamTokenizer st, boolean standard) throws IOException
{ super(st,standard);
if(standard) {wyj.print(" Podaj wydawnictwo ");wyj.flush();}
st.nextToken();
wydawnictwo=st.sval;
}
public String toString()
{ return super.toString() + String.format(" Ksiazka wydana przez %s ",wydawnictwo);}
}
import java.io.*;
public class Artykol extends Publikacja
{ String czasopismo;
public Artykol(StreamTokenizer st, boolean standard) throws IOException
{super(st,standard);
if( standard) {wyj.print(" w czasopismie ");wyj.flush();}
st.nextToken();
czasopismo=st.sval;
}
public String toString()
{ return super.toString() + String.format(" Artykol w : %s",czasopismo);}
}