Java karakterni tokovi: priča o Reader-u i Writer-u
Priča o karakternim tokovima Reader i Writer kreće od njihovog dijagrama relacija među klasama — pogledajmo sliku.

Karakterni tok je ulazno-izlazni tok namenjen čitanju i pisanju podataka u obliku karaktera. Za razliku od bajt-tokova, karakterni tokovi čitaju i pišu podatke u jedinicama karaktera, a ne u jedinicama bajtova. Često se koriste za obradu tekstualnih informacija.
Ako bajt-tokom direktno čitate kineski tekst, možete naići na problem nečitljivih znakova (mojibake), kao u sledećem primeru:
// FileInputStream je ulazni tok za operacije nad fajlom
FileInputStream inputStream = new FileInputStream("a.txt");//sadržaj je "Chenmo Wang Er je glup X"
int len;
while ((len=inputStream.read())!=-1){
System.out.print((char)len);
}Pogledajmo rezultat izvršavanja:
Rezultat: æ²é»çäºæ¯å» XPogledajte snimak:

Razlog pojave nečitljivih znakova je to što u bajt-toku jedan karakter obično čini više bajtova, a različita karakterna kodiranja koriste različit broj bajtova. Ako koristimo pogrešno karakterno kodiranje ili ako pri čitanju i pisanju ne obradimo ispravno konverziju karakternih kodiranja, kineski znakovi koji se pročitaju postaće nečitljivi.
Na primer, kada podrazumevanim karakternim kodiranjem (kao u gornjem primeru) čitamo tekstualni fajl koji sadrži kineske znakove, pojaviće se nečitljivi znakovi, jer je podrazumevano karakterno kodiranje obično ASCII, koji može da predstavi samo engleske znakove, a ne može ispravno da raščlani kineske znakove.
Kako onda bajt-tokom ispravno pročitati kineski tekst? Pogledajte primer.
try (FileInputStream inputStream = new FileInputStream("a.txt")) {
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, len));
}
}Zašto ovaj način radi?
Zato što smo za dekodiranje koristili klasu String — pregledom izvornog koda new String(bajt bytes[], int offset, int length) vidimo da ovaj konstruktor ima funkciju dekodiranja:
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}Ako nastavimo da pratimo metod defaultCharset() koji poziva StringCoding.decode(), videćemo da je podrazumevano kodiranje UTF-8. Kod je ispod:
public static Charset defaultCharset() {
if (defaultCharset == null) {
synchronized (Charset.class) {
if (cs != null)
defaultCharset = cs;
else
defaultCharset = forName("UTF-8");
}
}
return defaultCharset;
}
static char[] decode(byte[] ba, int off, int len) {
String csn = Charset.defaultCharset().name();
try {
// use charset name decode() variant which provides caching.
return decode(csn, ba, off, len);
} catch (UnsupportedEncodingException x) {
warnUnsupportedCharset(csn);
}
}U Javi su uobičajena karakterna kodiranja ASCII, ISO-8859-1, UTF-8, UTF-16 itd. Od njih, ASCII i ISO-8859-1 mogu predstaviti samo deo znakova, dok UTF-8 i UTF-16 mogu predstaviti sve Unicode znakove, uključujući kineske znakove.
Kada pomoću new String(bajt bytes[], int offset, int length) pretvaramo bajt-tok u string, Java prema pravilima UTF-8 dekodira po 3 bajta u jedan kineski znak, čime se kineski tekst ispravno dekodira.
Iako bajt-tokovi imaju način da reše problem nečitljivih znakova, to nije dovoljno neposredno, pa su uvedeni karakterni tokovi, namenjeni isključivo obradi teksta (audio, slike, video i sl. su netekstualni fajlovi).
Iz drugog ugla: karakterni tok = bajt-tok + tabela kodiranja
01. Karakterni ulazni tok (Reader)
java.io.Reader je superklasa (roditeljska klasa) karakternih ulaznih tokova i definiše neke njihove zajedničke metode:
close(): zatvara ovaj tok i oslobađa sistemske resurse vezane za njega.
read(): čita jedan karakter iz ulaznog toka.
read(char[] cbuf): čita određeni broj karaktera iz ulaznog toka i smešta ih u niz karakteracbuf.
FileReader je podklasa klase Reader i služi za čitanje karakterih podataka iz fajla. Njene glavne osobine su:
- Putanja fajla za čitanje može se zadati putem konstruktora.
- Svaki put može pročitati jedan ili više karaktera.
- Mogu se čitati znakovi iz Unicode skupa karaktera; navođenjem karakternog kodiranja vrši se konverzija između skupova znakova.
1) Konstruktor klase FileReader
FileReader(File file): kreira novi FileReader, sa parametrom File objekat.
FileReader(String fileName): kreira novi FileReader, sa parametrom ime fajla.
Primer koda:
// Kreiraj objekat toka pomoću File objekta
File file = new File("a.txt");
FileReader fr = new FileReader(file);
// Kreiraj objekat toka pomoću imena fajla
FileReader fr = new FileReader("b.txt");2) FileReader čita karakterne podatke
①. Čitanje karaktera: metod read pri svakom pozivu čita jedan karakter i vraća pročitani karakter (kao tip int); kada stigne do kraja fajla, vraća -1. Primer koda:
// Kreiraj objekat toka pomoću imena fajla
FileReader fr = new FileReader("abc.txt");
// Definiši promenljivu za čuvanje podataka
int b;
// Čitaj u petlji
while ((b = fr.read())!=-1) {
System.out.println((char)b);
}
// Zatvori resurse
fr.close();②. Čitanje karaktera zadate dužine: read(char[] cbuf, int off, int len), pri čemu se rezultat smešta u niz karaktera. Ovde cbuf označava niz karaktera u koji se smešta rezultat čitanja, off je početna pozicija za smeštanje rezultata, a len je broj karaktera koje treba pročitati. Primer koda:
File textFile = new File("docs/dogovor.md");
// Primer za FileReader
// try-with-resources FileReader
try(FileReader reader = new FileReader(textFile);) {
// read(char[] cbuf)
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer, 0, buffer.length)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}U ovom primeru FileReader čita karakterne podatke iz fajla i smešta ih u niz karaktera veličine 1024. Pri svakom čitanju pročita se len karaktera, a zatim se pomoću konstruktora String taj deo pretvara u string i ispisuje.
FileReader implementira interfejs AutoCloseable, pa se može koristiti iskaz try-with-resources za automatsko zatvaranje resursa, čime se izbegava zamorno ručno zatvaranje.
02. Karakterni izlazni tok (Writer)
java.io.Writer je superklasa (roditeljska klasa) klasa karakternih izlaznih tokova i može upisivati zadate karakterne informacije u odredište. Pogledajmo neke zajedničke metode koje definiše:
write(int c)upisuje jedan karakter.
write(char[] cbuf)upisuje niz karaktera.
write(char[] cbuf, int off, int len)upisuje deo niza karaktera, gde je off početni indeks, a len broj karaktera.
write(String str)upisuje string.
write(String str, int off, int len)upisuje deo stringa, gde off određuje početnu poziciju podstringa koji se upisuje u str, a len određuje dužinu podstringa koji se upisuje.
flush()ispira bafer ovog toka.
close()zatvara ovaj tok, ali ga prvo ispire.
Klasa java.io.FileWriter je podklasa klase Writer i služi za upisivanje karaktera u fajl.
1) Konstruktor klase FileWriter
FileWriter(File file): kreira novi FileWriter, sa parametrom File objekat koji se čita.FileWriter(String fileName): kreira novi FileWriter, sa parametrom ime fajla koji se čita.
Primer koda:
// Prvi način: kreiraj objekat toka pomoću File objekta
File file = new File("a.txt");
FileWriter fw = new FileWriter(file);
// Drugi način: kreiraj objekat toka pomoću imena fajla
FileWriter fw = new FileWriter("b.txt");2) FileWriter upisuje podatke
①. Upisivanje karaktera: metod write(int b) pri svakom pozivu može upisati jedan karakter. Primer koda:
FileWriter fw = null;
try {
fw = new FileWriter("output.txt");
fw.write(72); // upisuje ASCII kod karaktera 'H'
fw.write(101); // upisuje ASCII kod karaktera 'e'
fw.write(108); // upisuje ASCII kod karaktera 'l'
fw.write(108); // upisuje ASCII kod karaktera 'l'
fw.write(111); // upisuje ASCII kod karaktera 'o'
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}U ovom primeru koda prvo se kreira FileWriter objekat fw i zadaje putanja fajla za upis "output.txt". Zatim se metodom fw.write() u fajl upisuju bajtovi — ovde su to ASCII kodovi karaktera 'H', 'e', 'l', 'l', 'o'. Na kraju, u bloku finally zatvara se FileWriter objekat i oslobađaju resursi.
Imajte na umu da metod write(int b) upisuje jedan bajt, a ne jedan karakter. Ako je potrebno upisivati karaktere, koristite write(char cbuf[]) ili write(String str).
②. Upisivanje niza karaktera: metod write(char[] cbuf) upisuje zadati niz karaktera u izlazni tok. Primer koda:
FileWriter fw = null;
try {
fw = new FileWriter("output.txt");
char[] chars = {'H', 'e', 'l', 'l', 'o'};
fw.write(chars); // Upisuje niz karaktera u fajl
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}③. Upisivanje zadatog niza karaktera: metod write(char[] cbuf, int off, int len) upisuje deo zadatog niza karaktera u izlazni tok. Primer koda je ispod (ponovljeni deo preskačem, pogledajte gore):
fw = new FileWriter("output.txt");
char[] chars = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'};
fw.write(chars, 0, 5); // Upisuje prvih 5 karaktera niza u fajlMetodom fw.write() u fajl se upisuje prvih 5 karaktera niza.
④. Upisivanje stringa: metod write(String str) upisuje zadati string u izlazni tok. Primer koda:
fw = new FileWriter("output.txt");
String str = "Chenmo Wang Er";
fw.write(str); // Upisuje string u fajl⑤. Upisivanje zadatog stringa: metod write(String str, int off, int len) upisuje deo zadatog stringa u izlazni tok. Primer koda (u try-with-resources obliku) je ispod:
String str = "Chenmo Wang Er je zaista zgodan!";
try (FileWriter fw = new FileWriter("output.txt")) {
fw.write(str, 0, 5); // Upisuje prvih 5 karaktera stringa u fajl
} catch (IOException e) {
e.printStackTrace();
}[Napomena] Ako se resursi ne zatvore, podaci ostaju samo u baferu i ne snimaju se u fajl.
3) Zatvaranje (close) i ispiranje (flush)
Pošto FileWriter ima ugrađen bafer ByteBuffer, ako se izlazni tok ne zatvori, znakovi ne mogu biti upisani u fajl.

Ali kad jednom zatvorite objekat toka, ne možete više upisivati podatke. Ako želimo i da upisujemo podatke i da nastavimo da koristimo tok, potreban nam je metod flush.
flush: ispire bafer; objekat toka se i dalje može koristiti.
close: prvo ispire bafer, a zatim obaveštava sistem da oslobodi resurse. Objekat toka se više ne može koristiti.
flush je prilično zanimljiv — hajde da to osetimo kroz jedan komad koda:
// Izvor, odnosno ulazni tok [tok za čitanje], čita fajl a.txt
FileReader fr=new FileReader("abc.txt"); // fajl a.txt mora postojati, inače se javlja izuzetak FileNotFoundException
// Odredište, odnosno izlazni tok
FileWriter fw=new FileWriter("b.txt"); // sistem će automatski kreirati b.txt jer je to izlazni tok!
int len;
while((len=fr.read())!=-1){
fw.write(len);
}
// Obratite pažnju: ovde se tok ne zatvara metodom close; u razvoju se tako ne radi, ali je ovde izostavljeno radi boljeg shvatanja uloge flush-aKakav je rezultat izvršavanja? Odgovor je: fajl b.txt je i dalje prazan, nema u njemu ništa.

Razlog smo već ranije spomenuli. Tako je sa programiranjem — dok ga ne otkucaš, nikad ga ne naučiš!!! Zato obavezno kucajte, kucajte što više!!!
Ako u gorenavedeni kod dodate sledeće tri linije, fajl b.txt će uspešno prekopirati podatke iz izvornog fajla!
fr.close();
fw.flush();
fw.close();Metod flush() znači "isprazni keš"; služi da isprazni tok podataka iz bafera. Pri radu sa tokovima, podaci se prvo pročitaju u memoriju, a zatim se upisuju u fajl.
Možete to ponovo doživeti pomoću sledećeg primera koda:
// Kreiraj objekat toka pomoću imena fajla
FileWriter fw = new FileWriter("fw.txt");
// Ispiši podatke, preko flush
fw.write('i'); // Ispiši prvi karakter
fw.flush();
fw.write('p'); // Nastavi da ispisuješ drugi karakter, upis uspeo
fw.flush();
// Ispiši podatke, pa close
fw.write('z'); // Ispiši prvi karakter
fw.close();
fw.write('t'); // Nastavi da ispisuješ drugi karakter, [greška] java.io.IOException: Stream closed
fw.close();Napomena: čak i kada flush metodom upišete podatke, na kraju operacije ipak morate pozvati close metodu da biste oslobodili sistemske resurse. Naravno, možete koristiti i try-with-resources pristup.
4) Nastavak upisa i prelazak u novi red u FileWriter
Nastavak upisa i prelazak u novi red: postupak je sličan kao kod FileOutputStream operacija, evo koda:
// Kreiraj objekat toka pomoću imena fajla; omogućuje nastavak upisa
FileWriter fw = new FileWriter("fw.txt",true);
// Ispiši string
fw.write("Chenmo Wang Er");
// Ispiši znak za novi red
fw.write("\r\n");
// Ispiši string
fw.write("je glup X");
// Zatvori resurse
fw.close();Izlaz je prikazan ispod:
Izlaz:
Chenmo Wang Er
je glup X5) Kopiranje tekstualnog fajla
Evo koda:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CopyFile {
public static void main(String[] args) throws IOException {
// Kreiraj objekat ulaznog toka
FileReader fr=new FileReader("aa.txt");//ako fajl ne postoji, biće bačen java.io.FileNotFoundException
// Kreiraj objekat izlaznog toka
FileWriter fw=new FileWriter("copyaa.txt");
/*Posao koji obavlja kreiranje izlaznog toka:
* 1. poziva sistemske resurse da bi kreirao fajl
* 2. kreira objekat izlaznog toka
* 3. usmerava objekat izlaznog toka na fajl
* */
// Kopiranje tekstualnog fajla, čita se po jedan karakter
copyMethod1(fr, fw);
// Kopiranje tekstualnog fajla, čita se po jedan niz karaktera
copyMethod2(fr, fw);
fr.close();
fw.close();
}
public static void copyMethod1(FileReader fr, FileWriter fw) throws IOException {
int ch;
while((ch=fr.read())!=-1) {//čitanje podataka
fw.write(ch);//upis podataka
}
fw.flush();
}
public static void copyMethod2(FileReader fr, FileWriter fw) throws IOException {
char chs[]=new char[1024];
int len=0;
while((len=fr.read(chs))!=-1) {//čitanje podataka
fw.write(chs,0,len);//upis podataka
}
fw.flush();
}
}03. Obrada IO izuzetaka
Tokom učenja možemo imati naviku da samo prosleđujemo (propagiramo) izuzetak, ali u stvarnom razvoju se preporučuje korišćenje bloka try...catch...finally za obradu izuzetaka. Formatirani kod je ispod:
// Deklariši promenljivu
FileWriter fw = null;
try {
// Kreiraj objekat toka
fw = new FileWriter("fw.txt");
// Ispiši podatke
fw.write("Erge je zaista zgodan"); // upisujemo string
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}Ili direktno koristite try-with-resources pristup.
try (FileWriter fw = new FileWriter("fw.txt")) {
// Ispiši podatke
fw.write("Erge je zaista zgodan"); // upisujemo string
} catch (IOException e) {
e.printStackTrace();
}U ovom kodu, try-with-resources će nakon izvršenja bloka try automatski zatvoriti FileWriter objekat fw, pa nije potrebno ručno zatvaranje toka. Ako u bloku try dođe do izuzetka, tok se takođe automatski zatvara i izuzetak se prosleđuje. Zato upotreba try-with-resources-a čini kod jednostavnijim, sigurnijim i čitljivijim.
04. Rezime
Writer i Reader su apstraktne klase u Java I/O-u namenjene karakternom ulazu i izlazu; one pružaju niz metoda za čitanje i upis karakterih podataka. Razlika je u tome što Writer služi za upis karakterih podataka u izlazni tok, dok Reader služi za čitanje karakterih podataka iz ulaznog toka.
Uobičajene podklase klasa Writer i Reader su FileWriter i FileReader, kojima se karakterni tok može upisivati u fajl, odnosno čitati iz fajla.
Pri karakternom ulazu i izlazu pomoću Writer-a i Reader-a potrebno je obratiti pažnju na pitanje karakternog kodiranja.
