Detaljno objašnjenje Java NIO Buffer-a (bafera) i Channel-a (kanala)
Da ponovo podsetimo na razliku između IO i NIO:
- Može se jednostavno reći: IO je orijentisan na tokove, NIO je orijentisan na blokove (bafer)
- IO sistem orijentisan na tokove obrađuje podatke jedan bajt ujednom.
- IO sistem orijentisan na blokove (bafer) obrađuje podatke u obliku blokova.
NIO se uglavnom sastoji iz dve ključne komponente:
- Buffer (bafer)
- Channel (kanal)
U NIO-u se podaci ne obrađuju na način toka, već se obrada podataka obavlja kombinovanjem Buffer-a i Channel-a.
Da to jednostavno razumemo:
Channel (kanal) možemo uporediti sa železnicom, a Buffer (bafer) sa vozom (koji prevozi teret).
Naš NIO jednostavno prenosi Buffer sa podacima kroz Channel kako bi obradio podatke!
Uvek imajte na umu: Channel ne radi sa podacima, on je samo odgovoran za transport podataka. Sa podacima radi Buffer.
- Channel --> transport
- Buffer --> podaci
U poređenju sa tradicionalnim IO, tok je jednosmeran. Za NIO, sa konceptom Channel-a, naše čitanje i pisanje su dvosmerni (voz na pruzi može iz Guangdžou u Peking, a onda se prirodno može i iz Pekinga vratiti u Guangdžou)!
Buffer (bafer)
Da vidimo šta je vredno pažnje kod Buffer-a.
Buffer je apstraktna klasa za bafer:

Među njima, ByteBuffer je najčešće korišćena implementaciona klasa (za čitanje i pisanje bajt-podataka u kanalu).

Šta obično radimo kada dobijemo bafer? Vrlo jednostavno, čitanje podataka iz bafera / pisanje podataka u bafer. Dakle, ključne metode bafera su put i get:

Klasa Buffer održava 4 ključne promenljive koje pružaju informacije o nizu koji sadrži. To su:
- Kapacitet (Capacity) maksimalan broj elemenata podataka koje bafer može da primi. Kapacitet se postavlja pri kreiranju bafera i ne može se promeniti. (Razlog zašto se ne može promeniti je jednostavan — u pozadini je niz.)
- Granica (Limit) ukupan broj podataka u baferu, predstavlja koliko podataka ukupno ima u trenutnom baferu.
- Pozicija (Position) pozicija sledećeg elementa koji će biti pročitan ili zapisan. Position se automatski ažurira odgovarajućim
get()iput()funkcijama. - Oznaka (Mark) pozicija za podsetnik. Koristi se za beleženje pozicije poslednjeg čitanja/pisanja.
Prvo da pokažemo kako se kreira bafer i kako se menjaju vrednosti ključnih promenljivih.
// Kreiranje bafera
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// Pregled vrednosti 4 ključne promenljive na početku
System.out.println("Na početku-->limit--->"+byteBuffer.limit());
System.out.println("Na početku-->position--->"+byteBuffer.position());
System.out.println("Na početku-->capacity--->"+byteBuffer.capacity());
System.out.println("Na početku-->mark--->" + byteBuffer.mark());
System.out.println("--------------------------------------");
// Dodavanje nekih podataka u bafer
String s = "Chenmo Wang Er";
byteBuffer.put(s.getBytes());
// Pregled vrednosti 4 ključne promenljive nakon put-a
System.out.println("Nakon put-a-->limit--->"+byteBuffer.limit());
System.out.println("Nakon put-a-->position--->"+byteBuffer.position());
System.out.println("Nakon put-a-->capacity--->"+byteBuffer.capacity());
System.out.println("Nakon put-a-->mark--->" + byteBuffer.mark());Rezultat izvršenja:
Na početku-->limit--->1024
Na početku-->position--->0
Na početku-->capacity--->1024
Na početku-->mark--->java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
--------------------------------------
Nakon put-a-->limit--->1024
Nakon put-a-->position--->14
Nakon put-a-->capacity--->1024
Nakon put-a-->mark--->java.nio.HeapByteBuffer[pos=14 lim=1024 cap=1024]Sada želim da uzmem podatke iz bafera, kako? NIO nam daje flip() metod. Ovaj metod može da promeni pozicije position i limit!
Na osnovu prethodnog koda, primenimo flip().
// flip() metod
byteBuffer.flip();
System.out.println("Nakon flip() metoda-->limit--->"+byteBuffer.limit());
System.out.println("Nakon flip() metoda-->position--->"+byteBuffer.position());
System.out.println("Nakon flip() metoda-->capacity--->"+byteBuffer.capacity());
System.out.println("Nakon flip() metoda-->mark--->" + byteBuffer.mark());Da vidimo kako će se promeniti vrednosti 4 ključna svojstva:

Nakon poziva flip(), limit postaje vrednost trenutnog position-a (14), a position se resetuje na 0. To znači da možete čitati podatke upravo upisane u bafer počevši od početne pozicije, sve do pozicije koju određuje limit. capacity ostaje nepromenjen (1024).
Pretpostavimo da imamo ByteBuffer početnog kapaciteta 1024.
Početno stanje:
position = 0
limit = 1024
capacity = 1024Nakon dodavanja podataka "Chenmo Wang Er":
Pošto "Chenmo Wang Er" koristi ASCII kodiranje, gde svaki karakter zauzima 1 bajt, sa ukupno 14 karaktera zauzima 14 bajtova.
position = 14
limit = 1024
capacity = 1024Nakon poziva flip() metoda:
position = 0
limit = 14
capacity = 1024Grafički prikazano, to izgleda ovako.

Kada prebacimo u režim čitanja, možemo čitati podatke iz bafera:
// Kreiranje niza bajtova veličine limit() (jer postoji samo limit toliko podataka za čitanje)
byte[] bytes = new byte[byteBuffer.limit()];
// Učitavanje pročitanih podataka u naš niz bajtova
byteBuffer.get(bytes);
// Ispis podataka
System.out.println(new String(bytes, 0, bytes.length));Rezultat ispisa:
Chenmo Wang ErZatim da ispišemo vrednosti ključnih promenljivih:

Ako posle čitanja želite ponovo da pišete podatke u bafer, koristite clear() metod. Ovaj metod će „očistiti” bafer — podaci zapravo nisu obrisani, samo su zaboravljeni.

Channel (kanal)
Channel (kanal) je samo odgovoran za prenos podataka, ne radi direktno sa podacima. Sve operacije nad podacima se obavljaju preko Buffer-a! Kanali se obično dele u dve glavne kategorije: kanali za fajlove i kanali za sokete.
FileChannel: kanal za fajl I/O, podržava čitanje, pisanje i dodavanje u fajlove. FileChannel omogućava prenos podataka na bilo kojoj poziciji u fajlu, podržava napredne funkcije kao što su zaključavanje fajlova i memorijski mapirani fajlovi. FileChannel ne može da se postavi u neblokirajući režim, pa je pogodan samo za blokirajuće operacije sa fajlovima.
SocketChannel: kanal za TCP soket I/O. SocketChannel podržava neblokirajući režim i može se koristiti sa Selector-om (o kom će biti reči u nastavku) za postizanje efikasne mrežne komunikacije. SocketChannel omogućava povezivanje sa udaljenim hostom i prenos podataka.
Uparujući sa njim je ServerSocketChannel: kanal za slušanje TCP soket konekcija. Kao i SocketChannel, ServerSocketChannel podržava neblokirajući režim i može se koristiti sa Selector-om. ServerSocketChannel je zadužen za slušanje novih zahteva za konekcijom; kada primi zahtev, može da kreira novi SocketChannel za obradu prenosa podataka.
DatagramChannel: kanal za UDP soket I/O. DatagramChannel podržava neblokirajući režim, može slati i primati datagram pakete, i pogodan je za nepovezanu, nepouzdanu mrežnu komunikaciju.
U ovom poglavlju ćemo se uglavnom baviti FileChannel-om; SocketChannel, ServerSocketChannel i DatagramChannel će biti obrađeni u narednim poglavljima.
Kanal za fajlove FileChannel
Kanal se može otvoriti na sledeći način.
FileChannel.open(Paths.get("docs/prateci-tutorijal.md"), StandardOpenOption.WRITE);Ovde koristimo Paths, što će takođe biti objašnjeno kasnije.
①. Korišćenje FileChannel-a u kombinaciji sa ByteBuffer-om za implementaciju kopiranja fajlova:
try (FileChannel sourceChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger.txt"), StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger1.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (sourceChannel.read(buffer) != -1) {
buffer.flip();
destinationChannel.write(buffer);
buffer.clear();
}
}Kreiramo ByteBuffer kapaciteta 1024 kao bafer. U petlji čitamo podatke iz FileChannel-a izvornog fajla u bafer. Kada read() metod vrati -1, to znači da smo stigli do kraja fajla.
Nakon čitanja podataka, pozivamo flip() metod kako bismo u baferu pripremili podatke za pisanje. Zatim upisujemo sadržaj bafera u FileChannel ciljnog fajla (write() metod). Nakon završetka pisanja, pozivamo clear() metod da resetujemo bafer kako bismo ga ponovo iskoristili u sledećoj iteraciji.
②. Korišćenje memorijski mapiranih fajlova (MappedByteBuffer) za implementaciju kopiranja fajlova (direktna operacija nad baferom):
try (FileChannel sourceChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger.txt"), StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.READ)) {
long fileSize = sourceChannel.size();
MappedByteBuffer sourceMappedBuffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
MappedByteBuffer destinationMappedBuffer = destinationChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);
for (int i = 0; i < fileSize; i++) {
byte b = sourceMappedBuffer.get(i);
destinationMappedBuffer.put(i, b);
}
}MappedByteBuffer je klasa u Java NIO koja nasleđuje java.nio.ByteBuffer. MappedByteBuffer se koristi za predstavljanje memorijski mapiranog fajla — deo ili ceo fajl se mapira u memoriju kako bi se čitanje i pisanje u fajl obavljalo direktno kroz memoriju. Ovaj pristup može poboljšati performanse fajl I/O-a jer operativni sistem može direktno prenositi podatke između memorije i diska, bez dodatnog kopiranja podataka kroz Java aplikaciju.
Obično se koristi zajedno sa FileChannel-om; MappedByteBuffer objekat se može kreirati pozivom map() metoda FileChannel-a. map() metod prihvata tri parametra: režim mapiranja (FileChannel.MapMode), početna pozicija mapiranja i dužina mapiranja.
Režimi mapiranja uključuju režim samo za čitanje (READ_ONLY), režim za čitanje i pisanje (READ_WRITE) i privatni režim (PRIVATE).
Podešavamo MappedByteBuffer izvornog fajla u režim samo za čitanje (READ_ONLY), a MappedByteBuffer ciljnog fajla u režim za čitanje i pisanje (READ_WRITE).
U petlji bajt-po-bajt čitamo podatke iz MappedByteBuffer-a izvornog fajla i upisujemo ih u MappedByteBuffer ciljnog fajla. Na ovaj način se implementira funkcija kopiranja fajla. Kopiranje fajla pomoću memorijski mapiranih fajlova (MappedByteBuffer) može biti brže nego korišćenjem ByteBuffer-a.
Treba imati na umu da pri radu sa fajlovima pomoću MappedByteBuffer-a izmene podataka možda neće odmah biti upisane na disk. Pozivom force() metoda MappedByteBuffer-a podaci se odmah upisuju nazad na disk.
③. Prenos podataka između kanala pomoću transfer() metoda (direktna operacija nad baferom):
try (FileChannel sourceChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger.txt"), StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(Paths.get("logs/javabetter/itwanger3.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.READ)) {
sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
} catch (IOException e) {
throw new RuntimeException(e);
}transferTo() metod FileChannel-a je efikasan metod za prenos fajlova koji omogućava da se deo ili ceo sadržaj fajla direktno prebaci iz izvornog kanala fajla u ciljni kanal (obično drugi kanal fajla ili mrežni kanal). Ovaj način prenosa izbegava višestruko kopiranje podataka fajla između korisničkog prostora i prostora jezgra, čime se poboljšavaju performanse prenosa fajlova.
transferTo() metod prihvata sledeća tri parametra:
- position: pozicija u izvornom fajlu od koje počinje prenos.
- count: broj bajtova koje treba preneti.
- target: ciljni kanal koji prima podatke.
Treba napomenuti da transferTo() metod možda neće moći da prenese sve zahtevane bajtove u jednom koraku. U praksi će možda biti potrebno koristiti petlju kako bi se osiguralo da su svi bajtovi preneti.
public class FileChannelTransferToLoopExampleWithPaths {
public static void main(String[] args) {
Path sourcePath = Paths.get("logs/itwanger/paicoding.txt");
Path destinationPath = Paths.get("logs/itwanger/paicoding_copy.txt");
// Korišćenje try-with-resources iskaza da se osigura ispravno zatvaranje resursa kanala
try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(destinationPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
long position = 0;
long count = sourceChannel.size();
// Prenos u petlji dok se svi bajtovi ne prenesu
while (position < count) {
long transferred = sourceChannel.transferTo(position, count - position, destinationChannel);
position += transferred;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}Pored toga, transferTo() metod na donjem nivou koristi mogućnost nula-kopiranja (zero-copy) koju pruža operativni sistem (kao što je sendfile() sistemski poziv na Linux-u), što može značajno poboljšati performanse prenosa fajlova. Međutim, različiti operativni sistemi i JVM implementacije mogu uticati na dostupnost i performanse nula-kopiranja, pa stvarne performanse mogu zavisiti od okruženja.
Nula-kopiranje (Zero-Copy) je tehnika za optimizaciju performansi prenosa podataka koja minimalizuje opterećenje CPU-a i memorije tokom prenosa. U tradicionalnom procesu prenosa, podaci se obično moraju više puta kopirati između korisničkog prostora i prostora jezgra, što dovodi do dodatnog opterećenja CPU-a i memorije. Tehnologija nula-kopiranja izbegava ove suvišne operacije kopiranja i time ostvaruje efikasniji prenos podataka.
U Javi se tehnologija nula-kopiranja prvenstveno primenjuje u fajl i mrežnom I/O-u. Metodi transferTo() i transferFrom() klase FileChannel koriste nula-kopiranje i omogućavaju efikasan prenos podataka između kanala za fajlove i mrežne kanale. Za detalje pogledajte: Duboka analiza principa Linux IO-a i implementacija nekoliko mehanizama nula-kopiranja
Direktni i indirektni bafer
Razlika između direktnih i indirektnih bafera uglavnom leži u načinu na koji se čuvaju u memoriji. Evo kratkog pregleda i razlika između direktnih i indirektnih bafera:
Indirektni bafer:
- Dodeljuje se u heap memoriji JVM-a
- Pod upravljanjem sakupljača smeća (garbage collector)
- Prilikom čitanja i pisanja potrebno je kopirati podatke iz heap memorije u lokalnu memoriju operativnog sistema, a zatim izvršiti I/O operaciju
- Kreiranje:
ByteBuffer.allocate(int capacity)
Direktni bafer:
- Dodeljuje se u lokalnoj memoriji operativnog sistema
- Nije pod upravljanjem sakupljača smeća
- Prilikom čitanja i pisanja radi direktno u lokalnoj memoriji, izbegavajući kopiranje podataka i poboljšavajući performanse
- Kreiranje:
ByteBuffer.allocateDirect(int capacity) - Takođe, prethodno pomenuti
FileChannel.map()metod vraća direktni bafer tipa MappedByteBuffer.
Razlika između ByteBuffer.allocate i ByteBuffer.allocateDirect.

Razlika između direktnih i indirektnih bafera.

Indirektni bafer se čuva unutar JVM-a; podaci se moraju kopirati iz aplikacije (Java) u indirektni bafer, zatim u bafer jezgra, i konačno poslati uređaju (disk/mreža). Za direktan bafer, podaci se mogu direktno kopirati iz aplikacije (Java) u bafer jezgra, bez prolaska kroz indirektni bafer JVM-a.
Asinhroni kanal za fajlove AsynchronousFileChannel
AsynchronousFileChannel je klasa asinhronog kanala za fajlove uvedena u Java 7, koja pruža asinhrone operacije čitanja, pisanja, otvaranja i zatvaranja fajlova.
Asinhroni kanal za fajlove se može otvoriti pomoću metoda AsynchronousFileChannel.open(), koji prihvata Path objekat i skup opcija otvaranja (kao što su StandardOpenOption.READ, StandardOpenOption.WRITE, itd.) kao parametre.
Path file = Paths.get("example.txt");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE);AsynchronousFileChannel pruža dva načina za asinhronu operaciju:
①. Future način: koristi Future objekat za praćenje završetka asinhrone operacije. Kada pozovemo asinhronu operaciju (kao što je read() ili write()), ona odmah vraća Future objekat. Možemo koristiti taj objekat da proverimo da li je operacija završena i da dobijemo rezultat operacije. Ovaj način je pogodan za scenarije u kojima nije potrebno odmah izvršiti druge operacije po završetku.
Na primer:
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> result = fileChannel.read(buffer, position);
while (!result.isDone()) {
// Izvršavanje drugih operacija
}
int bytesRead = result.get();
System.out.println("Bytes read: " + bytesRead);②. CompletionHandler način: koristi objekat koji implementira CompletionHandler interfejs za obradu završetka asinhrone operacije. Potrebno je da pružimo implementacionu klasu CompletionHandler-a i preradimo completed() i failed() metode, kojima se redom obrađuju slučajevi uspeha i neuspeha operacije. Kada se asinhrona operacija završi, sistem automatski poziva odgovarajući metod. Ovaj način je pogodan za scenarije u kojima je potrebno odmah izvršiti druge operacije po završetku.
Na primer:
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("Bytes read: " + result);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.err.println("Read failed");
exc.printStackTrace();
}
});Pogledajmo kompletan primer, u Future formi.
Path path = Paths.get("docs/prateci-tutorijal.md");
try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
while (true) {
Future<Integer> result = fileChannel.read(buffer, position);
while (!result.isDone()) {
// Ovde se mogu izvršavati drugi zadaci, npr. obrada drugih I/O operacija
}
int bytesRead = result.get();
if (bytesRead <= 0) {
break;
}
position += bytesRead;
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
}
}U ovom primeru koristimo metod AsynchronousFileChannel.read() za pokretanje asinhrone operacije čitanja. Ovaj metod vraća objekat Future<Integer> koji predstavlja rezultat operacije čitanja. Možemo pozvati isDone() metod da proverimo da li je asinhrona operacija završena. Po završetku, možemo pozvati get() metod da dobijemo stvarni broj pročitanih bajtova.
Zatim da pogledamo formu sa CompletionHandler interfejsom:
public static void readAllBytes(Path path) throws IOException, InterruptedException {
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
AtomicLong position = new AtomicLong(0);
CountDownLatch latch = new CountDownLatch(1);
fileChannel.read(buffer, position.get(), null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer bytesRead, Object attachment) {
if (bytesRead > 0) {
position.addAndGet(bytesRead);
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.print(new String(data));
buffer.clear();
fileChannel.read(buffer, position.get(), attachment, this);
} else {
latch.countDown();
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("Error: " + exc.getMessage());
latch.countDown();
}
});
latch.await();
}U readAllBytes metodu koristimo metod
AsynchronousFileChannel.open()da otvorimo asinhroni kanal za fajlove u režimu čitanja.Kreiramo ByteBuffer veličine 1024 za čuvanje podataka pročitanih iz fajla.
Koristimo promenljivu position tipa AtomicLong da beležimo trenutnu poziciju čitanja u fajlu. Početna vrednost je 0.
Kreiramo objekat CountDownLatch koji se koristi za obaveštavanje glavne niti kada se asinhrona operacija završi. Početna vrednost je 1.
Pokrećemo asinhronu operaciju čitanja pomoću metoda
fileChannel.read(). Parametri ovog metoda obuhvataju: bafer za čuvanje podataka, trenutnu poziciju čitanja, dodatni objekat (u ovom primeru nije potreban, pa se prosleđuje null) i objekat koji implementira CompletionHandler interfejs, koji će biti pozvan kada se operacija čitanja završi.CompletionHandler interfejs ima dva metoda:
completed()ifailed(). Kada se operacija čitanja uspešno završi, poziva secompleted()metod; ako operacija čitanja ne uspe, poziva sefailed()metod.U
completed()metodu prvo proveravamo da li je bytesRead (broj bajtova pročitanih u ovom koraku) veći od 0. Ako je veći od 0, to znači da ima još podataka za čitanje.
- Ažuriramo promenljivu position tako što je povećavamo za bytesRead.
- Okrećemo bafer (
flip()) kako bismo mogli da čitamo iz njega. - Kreiramo novi niz bajtova čija veličina odgovara ograničenju bafera (
limit()). - Uzimamo podatke iz bafera i čuvamo ih u novokreiranom nizu bajtova.
- Konvertujemo niz bajtova u string i ispišemo ga.
- Čistimo bafer kako bismo mogli da nastavimo sa čitanjem više podataka.
- Ponovo pozivamo fileChannel.read() metod da nastavimo čitanje podataka iz fajla.
Ako je bytesRead manji ili jednak 0, to znači da smo pročitali sve podatke iz fajla. U tom slučaju treba da: pozovemo
latch.countDown()metod kako bismo obavestili glavnu nit da je asinhrona operacija završena. Zatvorimo fileChannel.Ako operacija čitanja ne uspe, u
failed()metodu ćemo ispisati poruku o grešci i pozvatilatch.countDown()metod da obavestimo glavnu nit.Na kraju pozivamo
latch.await()metod da sačekamo završetak asinhrone operacije. Glavna nit će ovde blokirati dok brojač latch-a ne postane 0.
Rezime
Buffer i Channel u Java NIO su ključne komponente NIO sistema. Buffer čuva podatke i pruža operacije čitanja i pisanja nad podacima. Ima više tipova, kao što su ByteBuffer, CharBuffer, IntBuffer itd., radi podrške različitim tipovima podataka.
Channel predstavlja vezu sa I/O uređajem (kao što je fajl ili soket). Pruža mogućnost čitanja podataka iz izvornog uređaja u Buffer i pisanja podataka iz Buffer-a u ciljni uređaj. Channel može biti samo za čitanje, samo za pisanje ili istovremeno za oboje; detaljno smo obradili kanal za fajlove FileChannel i asinhroni kanal za fajlove AsynchronousFileChannel.
Ukratko, NIO koristi komponente Buffer i Channel za efikasan prenos podataka, čime se poboljšavaju performanse I/O operacija.
Referentni link: https://www.zhihu.com/question/29005375/answer/667616386, obradio: Chenmo Wang Er
