Java baferisani tokovi: Kvalitet čitanja i pisanja u Java IO se drastično poboljšava
Java baferisani tokovi predstavljaju omotač oko bajt tokova i karakternih tokova — povećavaju efikasnost I/O operacija otvaranjem bafera u memoriji. Java realizuje baferisanje bajt tokova putem klasa BufferedInputStream i BufferedOutputStream, a baferisanje karakternih tokova putem klasa BufferedReader i BufferedWriter.
Princip rada baferisanih tokova je da se podaci prvo upišu u bafer, a kada se bafer napuni, jednom se upišu u fajl ili izlazni tok; ili kada je bafer prazan, jednom se učita određena količina podataka iz fajla ili ulaznog toka. Na taj način se smanjuje broj sistemskih I/O operacija, povećava efikasnost I/O sistema, i samim tim raste i brzina izvršavanja programa.
01,Baferisani bajt tokovi
BufferedInputStream i BufferedOutputStream spadaju u baferisane bajt tokove i predstavljaju nadogradnju osnovnih bajt tokova InputStream i OutputStream. Bajt tokove smo već detaljno obradili ranije — možete otvoriti ovaj link da ponovite gradivo.
1)Konstruktori
BufferedInputStream(InputStream in): kreira novi baferisani ulazni tok — obratite pažnju da je tip parametra InputStream.BufferedOutputStream(OutputStream out): kreira novi baferisani izlazni tok — obratite pažnju da je tip parametra OutputStream.
Primer koda:
// Kreiramo baferisani bajt ulazni tok — prvo deklarišemo bajt tok
FileInputStream fps = new FileInputStream(b.txt);
BufferedInputStream bis = new BufferedInputStream(fps)
// Kreiramo baferisani bajt ulazni tok (u jednom koraku)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt"));
// Kreiramo baferisani bajt izlazni tok (u jednom koraku)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));2)Efikasnost baferisanih tokova
Testiraćemo efikasnost baferisanih tokova kopiranjem velikog fajla od preko 370 MB. Radi poređenja, prvo ćemo to uraditi pomoću osnovnih tokova. Kod izgleda ovako:
// Beležimo početno vreme
long start = System.currentTimeMillis();
// Kreiramo objekte tokova
try (FileInputStream fis = new FileInputStream("py.mp4");//exe fajl je dovoljno veliki
FileOutputStream fos = new FileOutputStream("copyPy.mp4")){
// Čitamo i pišemo podatke
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
}
// Beležimo završno vreme
long end = System.currentTimeMillis();
System.out.println("Vreme kopiranja običnim tokovima:"+(end - start)+" ms");Nažalost, moja mašina je prilično slaba, pa je i posle 10 minuta još uvek kopirala. Pređimo na baferisane tokove. Kod izgleda ovako:
// Beležimo početno vreme
long start = System.currentTimeMillis();
// Kreiramo objekte tokova
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){
// Čitamo i pišemo podatke
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
}
// Beležimo završno vreme
long end = System.currentTimeMillis();
System.out.println("Vreme kopiranja baferisanim tokovima:"+(end - start)+" ms");Potrebno je samo 8016 milisekundi. Kako da bude još brže?
Možemo preći na čitanje i pisanje pomoću niza — o tome smo već govorili ranije. Kod izgleda ovako:
// Beležimo početno vreme
long start = System.currentTimeMillis();
// Kreiramo objekte tokova
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){
// Čitamo i pišemo podatke
int len;
byte[] bytes = new byte[8*1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
}
// Beležimo završno vreme
long end = System.currentTimeMillis();
System.out.println("Vreme kopiranja baferisanim tokovima uz niz:"+(end - start)+" ms");Sada je još brže — samo 521 milisekunda.
3)Zašto su baferisani bajt tokovi toliko brzi?
Tradiconalni Java IO radi u blokirajućem režimu — njegovo stanje rada je „čitaj/piši, čekaj, čitaj/piši, čekaj...".
Baferisani bajt tokovi rešavaju upravo taj problem: čita se više odjednom i piše se više odjednom, čime se smanjuje učestalost čitanja i pisanja — prosto se prostor menja za vreme.
- Smanjenje broja sistemskih poziva: pri korišćenju baferisanih bajt tokova, podaci se ne upisuju odmah na disk ili u izlazni tok, već prvo u bafer; kada se bafer napuni, jednom se upisuju na disk ili u izlazni tok. Time se smanjuje broj sistemskih poziva, pa raste efikasnost I/O operacija.
- Smanjenje broja disk operacija čitanja i pisanja: pri korišćenju baferisanih bajt tokova, kada je potrebno pročitati podatke, baferisani tok prvo čita iz bafera; ako u baferu nema dovoljno podataka, jednom se učita određena količina sa diska ili iz ulaznog toka. Slično, pri upisu podaci se prvo smeste u bafer, a kada se bafer napuni, jednom se ispišu na disk ili u izlazni tok. Time se smanjuje broj disk operacija, pa raste efikasnost I/O operacija.
- Povećanje efikasnosti prenosa podataka: pošto se podaci prenose u blokovima, smanjuje se broj prenosa, čime raste efikasnost prenosa podataka.
Pogledajmo metod read klase BufferedInputStream:
public synchronized int read() throws IOException {
if (pos >= count) { // Ako je trenutna pozicija stigla do kraja bafera
fill(); // Napuni bafer
if (pos >= count) // Ako je i posle punjenja pozicija na kraju, čitanje je završeno
return -1; // Vraća -1 kao znak da je čitanje završeno
}
return getBufIfOpen()[pos++] & 0xff; // Vraća bajt na trenutnoj poziciji i povećava poziciju za 1
}Ovaj kod se sastoji iz dva glavna dela:
fill(): ovaj metod napuni baferbufdo kraja.getBufIfOpen()[pos++] & 0xff: vraća bajt na trenutnoj poziciji čitanjapos(getBufIfOpen()vraća niz buffer, tipa bajt) i nad njim radi bitwise AND sa 0xff. Cilj je da se pročitani bajt b tretira kao neoznačeni (unsigned) bajt, jer je Java tip bajt označen (signed); AND sa 0xff pretvara ga u neoznačeni celi broj u opsegu od 0 do 255.
bajt & 0xFFćemo detaljnije obraditi malo kasnije.
Pogledajmo sada i metod read klase FileInputStream:

U ovom kodu, metod read0() je nativni metod — njegovu implementaciju pruža sam operativni sistem, a ne Java jezik. Implementacija read0() metoda se može razlikovati na različitim operativnim sistemima, ali funkcija je uvek ista: čitanje jednog bajta.
Pogledajmo sada i metod write(bajt b[], int off, int len) klase BufferedOutputStream:
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) { // Ako je broj bajtova za upis veći ili jednak dužini bafera
/* Ako tražena dužina prelazi veličinu izlaznog bafera,
prvo isprazni bafer, a zatim upiši podatke direktno.
Tako se izbegava problem kaskade kod baferisanih tokova. */
flushBuffer(); // Prvo isprazni bafer
out.write(b, off, len); // Podatke upiši direktno u izlazni tok
return;
}
if (len > buf.length - count) { // Ako je broj bajtova za upis veći od preostalog prostora
flushBuffer(); // Prvo isprazni bafer
}
System.arraycopy(b, off, buf, count, len); // Kopira podatke u bafer
count += len; // Ažurira brojač
}Prvo, metod proverava da li je broj bajtova za upis veći ili jednak dužini bafera. Ako jeste, prvo ispira (flush) podatke iz bafera na disk, a zatim direktno upisuje podatke u izlazni tok. Ovo se radi kako bi se izbegao problem kaskade kod baferisanih tokova — kada bafer nije dovoljno velik da primi sve podatke za upis, može doći do kaskadnog ispiranja, što smanjuje efikasnost.
Problem kaskade (Cascade Problem) javlja se u nizu baferisanih tokova (Buffered Stream) kada bafer nije dovoljno velik da primi sve podatke za upis, pa se podaci dele na više delova i raspoređuju po različitim baferima, te je na kraju potrebno svaki bafer isprati pojedinačno — što dovodi do pada performansi.
Zatim, ako je broj bajtova za upis manji od dužine bafera, proverava se da li preostali prostor u baferu može da primi te bajtove. Ako ne može, prvo se podaci iz bafera ispiraju na disk. Nakon toga, metod System.arraycopy() kopira podatke za upis u bafer i ažurira se brojač count.
Konačno, ako je broj bajtova za upis manji od dužine bafera, a u baferu još uvek ima prostora, podaci se direktno kopiraju u bafer i ažurira se brojač count.
Drugim rečima, flush se radi tek kada se buf napuni — podaci se istovremeno ispiraju na disk, podrazumevano po 8192 bajta.
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}Ako buf nije pun, pisanje u njega se nastavlja.
Uporedimo to sa metodom write klase FileOutputStream — koji je takođe nativni metod i može da upiše samo jedan bajt u jednom pozivu.

Kada se BufferedOutputStream i BufferedInputStream koriste zajedno, broj operacija čitanja i pisanja se drastično smanjuje. Posebno byte[] bytes = new byte[8*1024] — to znači da bafer ima prostor od 8 × 1024 bajta, pa se efikasnost čitanja i pisanja značajno povećava.
4)bajt & 0xFF
Tip bajt se obično koristi za čuvanje binarnih podataka, npr. pri čitanju i pisanju fajlova, mrežnom prenosu i sličnim scenarijima. U tim situacijama, promenljiva tipa bajt služi za čuvanje svakog pojedinačnog bajta u toku podataka, čime se omogućavaju operacije čitanja i pisanja.
Tip bajt je označen (signed), odnosno njegov opseg vrednosti je od -128 do 127. Ako želimo neoznačenu (unsigned) vrednost bajta, moramo koristiti bajt & 0xFF za konverziju.
Razlog je što je 0xFF neoznačeni celi broj čija je binarna reprezentacija 11111111. Kada se vrednost tipa bajt podvrgne bitwise AND sa 0xFF, rezultat je neoznačeni celi broj u opsegu od 0 do 255.
0xff je heksadekadni broj, što odgovara binarnom 11111111. Operator & znači: ako je odgovarajući bit oba operanda 1, rezultat je 1, inače je 0. Pošto 0xff ima osam jedinica, pretvaranje pojedinačnog bajt-a u int zapravo znači vršenje AND operacije (&) između bajt-a i vrednosti 255 tipa int.
Na primer, ako imamo promenljivu b tipa bajt sa vrednošću -1, onda je rezultat b & 0xFF jednak 255. Tako se označena vrednost tipa bajt pretvara u neoznačeni celi broj.
Operacija & je način računanja nad binarnim podacima — samo kada su oba bita 1 rezultat je 1, inače je 0. U gornjem izračunavanju getBufIfOpen()[pos++] & 0xff, bajt ima 8 bita, 0xFF je heksadekadna vrednost 255 i predstavlja tip int, a int ima 32 bita.
Ako je getBufIfOpen()[pos++] jednako -118, njegova reprezentacija u izvornom obliku (true form) je:
00000000 00000000 00000000 10001010Inverzni oblik (ones' complement) je:
11111111 11111111 11111111 11110101Komplement dvojke (two's complement) je:
11111111 11111111 11111111 111101100xFF predstavlja heksadekadni zapis broja 255; izvorni, inverzni i komplementni oblik su isti, a binarni zapis je:
00000000 00000000 00000000 11111111Rezultat AND operacije između 0xFF i -118 je:
00000000 00000000 00000000 11110110Nakon vraćanja u izvorni oblik:
00000000 00000000 00000000 10001010To odgovara int vrednosti 138. Dakle, kada se bajt vrednost -118 podvrgne AND operaciji sa 0xFF, rezultat se menja iz -118 u int vrednost 138, pri čemu donjih 8 bita ostaju potpuno jednaka onima kod bajt vrednosti -118.
Uzgred, da pojasnimo izvorni oblik, inverzni oblik i komplement dvojke.
①. Izvorni oblik (true form)
Izvorni oblik se sastoji od znaka i apsolutne vrednosti — prvi bit označava znak, a ostali preostalu vrednost. Na primer, za 8-bitni binarni broj:
[+1]izv = 0000 0001
[-1]izv = 1000 0001Prvi bit je znak. Pošto je prvi bit rezervisan za znak, opseg 8-bitnog binarnog broja je:
[1111 1111 , 0111 1111]odnosno
[-127 , 127]②. Inverzni oblik (ones' complement)
Inverzni oblik se određuje na sledeći način:
- Za pozitivne brojeve inverzni oblik je isti kao izvorni.
- Za negativne brojeve se izvorni oblik zadržava, znak ostaje nepromenjen, a preostali bitovi se invertuju.
Na primer:
[+1] = [00000001]izv = [00000001]inv
[-1] = [10000001]izv = [11111110]invVidimo da ako inverzni oblik predstavlja negativan broj, ljudski mozak ne može intuitivno da pročita njegovu vrednost — obično se prvo mora pretvoriti u izvorni oblik pa tek onda izračunati.
③. Komplement dvojke (two's complement)
Komplement dvojke se određuje na sledeći način:
- Za pozitivne brojeve komplement dvojke je isti kao izvorni.
- Za negativne brojeve se izvorni oblik zadržava, znak ostaje nepromenjen, preostali bitovi se invertuju, a zatim se dodaje 1. (Drugim rečima, inverzni oblik + 1.)
[+1] = [00000001]izv = [00000001]inv = [00000001]kompl
[-1] = [10000001]izv = [11111110]inv = [11111111]komplI za negativne brojeve, komplement dvojke se ne može intuitivno pročitati — obično je potrebno pretvoriti ga u izvorni oblik pre nego što se izračuna vrednost.
Iz gore navedenog zaključujemo:
- Za pozitivne brojeve: izvorni oblik, inverzni oblik i komplement dvojke su isti.
- Za negativne brojeve: izvorni oblik, inverzni oblik i komplement dvojke se razlikuju.
02,Baferisani karakterni tokovi
Klasa BufferedReader nasleđuje iz klase Reader i pruža nekoliko pogodnih metoda — na primer, metod readLine() može pročitati čitavu liniju teksta odjednom, umesto znak po znak.
Klasa BufferedWriter nasleđuje iz klase Writer i pruža nekoliko pogodnih metoda — na primer, metod newLine() upisuje sistemski specifičan separator linija.
1)Konstruktori
BufferedReader(Reader in): kreira novi baferisani ulazni tok — obratite pažnju da je tip parametra Reader.BufferedWriter(Writer out): kreira novi baferisani izlazni tok — obratite pažnju da je tip parametra Writer.
Primer koda:
// Kreiramo baferisani karakterni ulazni tok
BufferedReader br = new BufferedReader(new FileReader("b.txt"));
// Kreiramo baferisani karakterni izlazni tok
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));2)Metodi specifični za baferisane karakterne tokove
Osnovni metodi baferisanih karakternih tokova pozivaju se na isti način kao kod običnih karakternih tokova, pa to nećemo ponavljati — pogledajmo metode koji su specifični za baferisane karakterne tokove.
- BufferedReader:
String readLine(): čita jednu liniju teksta, vraća null kada više nema sadržaja. - BufferedWriter:
newLine(): prelazak u novi red, separator linija je definisan sistemom.
Primer koda za metod readLine():
// Kreiramo objekat toka
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// Definišemo nisku koja čuva pročitanu liniju teksta
String line = null;
// Čitamo u petlji, na kraju vraća null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// Oslobađamo resurse
br.close();Sada pogledajmo primer za metod newLine():
// Kreiramo objekat toka
BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
// Upisujemo podatke
bw.write("Chen");
// Prelazak u novi red
bw.newLine();
bw.write("mo");
bw.newLine();
bw.write("Wang");
bw.newLine();
bw.write("Er");
bw.newLine();
// Oslobađamo resurse
bw.close();03,Vežba sa baferisanim karakternim tokovima
Pogledajte ovu pesmu koju sam napisao:
Učitelju Cen, prijatelju Danqiu, nalivajte vinom, neka pehari ne staju!
Ne vidiš li kako voda Žute reke teče s neba, juri ka moru i nikad se ne vraća?
Zvona, bubnjevi i delikatese od žada — sve je to bez vrednosti; samo želim da zauvek ostanem pijan i nikad se ne probudim.
Kad život pruža radost, treba je živeti punim plućima; ne dozvoli da zlatni pehar stoji prazan pod mesecom.
Zakoljmo ovce i volove za gozbu i veselje — treba popiti trista pehara!
Ne vidiš li na sjajnom ogledalu u dvorani sede vlasi — ujutru crne kao svila, uveče bele kao sneg?
Daću vam jednu pesmu, molim vas, prisluhnite.
Nebo mi je dalo talenat koji će sigurno doći na svoje; hiljadu zlatnika, potrošeno, vratiće se opet.
Da li ste uživali?
Verovatno ste i sami primetili — ovo je „Poziv na vino" (Jiang Jin Jiu) koju je napisao Li Bai, a ne ja, Wang Er. 😝
Međutim, redosled linija je izmešan — srećom, sve sam ih numerisao. Kako da ih složimo u pravilnom redosledu?
Pogledajmo implementaciju u kodu:
// Kreiramo mapu za čuvanje teksta — ključ je redni broj, vrednost je linija teksta
HashMap<String, String> lineMap = new HashMap<>();
// Kreiramo objekte tokova — izvor
BufferedReader br = new BufferedReader(new FileReader("logs/test.log"));
// odredište
BufferedWriter bw = new BufferedWriter(new FileWriter("logs/test1.txt"));
// Čitamo podatke
String line;
while ((line = br.readLine())!=null) {
// Raščlanjujemo tekst
if (line.isEmpty()) {
continue;
}
String[] split = line.split(Pattern.quote("."));
// Upisujemo u mapu
lineMap.put(split[0], split[1]);
}
// Oslobađamo resurse
br.close();
// Prolazimo kroz mapu
for (int i = 1; i <= lineMap.size(); i++) {
String key = String.valueOf(i);
// Uzimamo tekst iz mape
String value = lineMap.get(key);
// Upisujemo spojeni tekst
bw.write(key+"."+value);
// Prelazak u novi red
bw.newLine();
}
// Oslobađamo resurse
bw.close();Sve znanje koje se ovde koristi smo već obradili ranije — na primer HashMap, podela niski, kao i baferisani karakterni tokovi koje smo upravo naučili.
Pogledajmo rezultat:
1. Ne vidiš li kako voda Žute reke teče s neba, juri ka moru i nikad se ne vraća?
2. Ne vidiš li na sjajnom ogledalu u dvorani sede vlasi — ujutru crne kao svila, uveče bele kao sneg?
3. Kad život pruža radost, treba je živeti punim plućima; ne dozvoli da zlatni pehar stoji prazan pod mesecom.
4. Nebo mi je dalo talenat koji će sigurno doći na svoje; hiljadu zlatnika, potrošeno, vratiće se opet.
5. Zakoljmo ovce i volove za gozbu i veselje — treba popiti trista pehara!
6. Učitelju Cen, prijatelju Danqiu, nalivajte vinom, neka pehari ne staju!
7. Daću vam jednu pesmu, molim vas, prisluhnite.
8. Zvona, bubnjevi i delikatese od žada — sve je to bez vrednosti; samo želim da zauvek ostanem pijan i nikad se ne probudim.