Skip to the content.

5.3 NIO.2

© Γιάννης Κωστάρας


<- Δ ->

Στο προηγούμενο μάθημα μάθαμε τις βασικές κλάσεις για επικοινωνία με τις μονάδες μόνιμης αποθήκευσης. Στην έκδοση 1.4 η γλώσσα εισήγαγε το NIO (New I/O) ενώ στην έκδοση 7 το NIO.2, δηλ. ένα νέο πλούσιο ΑΡΙ για το σκοπό αυτό.

I/O NIO.2
java.io java.nio.file
File Path
File Paths
- FileSystems
- FileSystem
- FileStore

Το NIO ΑΡΙ είναι non-blocking, σε αντίθεση με το blocking API του προηγούμενου μαθήματος που περιμένει να διαβαστούν/γραφτούν όλα τα δεδομένα προτού μπορέσει να εκτελέσει άλλες εντολές. Οι κλάσεις NIO δουλεύουν με κανάλια (channels) και προσωρινές μνήμες (buffers) αντί για ροές (streams). Αν ένα νήμα (thread) ζητήσει να διαβάσει ή να γράψει σε ένα κανάλι τότε θα του επιστραφεί ότι υπάρχει στο κανάλι ή την προσωρινή μνήμη ενώ παράλληλα μπορεί να εκτελέσει άλλες εντολές. Επίσης, μπορεί να μετακινηθεί στον buffer μπρος/πίσω (σ’ αντίθεση με τις ροές που τα δεδομένα μεταφέρονται προς μια μόνο κατεύθυνση), οπότε μπορούμε να έχουμε ταυτόχρονα ανάγνωση και εγγραφή.

Ας δούμε πώς “μεταφράζονται” τα παραδείγματα του προηγούμενου μαθήματος στο ΝΙΟ.2.

java.nio.file.Path & java.nio.file.Paths

Η κλάση Path είναι αντίστοιχη της File (αν και δεν την αντικαθιστά πλήρως).

jshell> import java.nio.file.*

jshell> Path path = Paths.get("test.txt");
path ==> test.txt

jshell> Path path = Paths.get("C:/temp/test.txt");    // absolute path
path ==> C:/temp/test.txt

jshell> Path path = Paths.get("temp/test.txt");    // relative path
path ==> temp/test.txt

jshell> Path path = Paths.get("C:","temp","test.txt");    
path ==> C:/temp/test.txt

jshell> Path path = Paths.get("C:/temp/../temp/test.txt").normalize();    
path ==> C:/temp/test.txt

jshell> Path path = Paths.get(URI.create("file:///home/user/test.txt"));
path ==> /home/user/test.txt

jshell> Path path = FileSystems.getDefault().getPath("C:\\temp\\test.txt");
path ==> C:\temp\test.txt

jshell> Path path = Path.of("C:\\temp\\test.txt");
path ==> C:\temp\test.txt

jshell> Path path = Paths.get(System.getProperty("user.home"), "downloads", "test.txt");
path ==> /Users/MyMacBook/downloads/test.txt

jshell> Path path1 = Paths.get("C:");    
path1 ==> C:

jshell> Path path2 = Paths.get("temp/test.txt");    
path2 ==> temp/test.txt

jshell> path1.equals(path2)
$1 ==> false

jshell> path1.compareTo(path2) // το path1 < path2 (λεξικογραφικά)
$2 ==> -49

jshell> path1.resolve(path2) // συνένωση
$3 ==> C:/temp/test.txt

jshell> path1.relativize(path2)  // requires all paths to be either absolute or relative
$4 ==> ../temp/test.txt

jshell> path
path ==> C:/temp/test.txt

jshell> path.endsWith("test.txt")
$5 ==> true

jshell> path.resolveSibling("file.txt")	// συνένωση
$6 ==> C:/temp/file.txt

jshell> path.getFileName()
$7 ==> test.txt

jshell> path.getNameCount()
$8 ==> 3

jshell> path.getName(1)
$9 ==> temp

jshell> path.getParent()
$10 ==> C:/temp

jshell> path.getRoot()
$11 ==> null

jshell> Paths.get("/Users/").getRoot()
$12 ==> /

jshell> path.subpath(1,2); // beginIndex, endIndex; index starts from 0
$13 ==> temp

jshell> path.toString()
$14 ==> "C:/temp/test.txt"

jshell> path.toUri()
$15 ==> file:///C:/temp/test.txt

jshell> path.isAbsolute()
$16 ==> true

jshell> path.toAbsolutePath()
$17 ==> /C:/temp/test.txt

jshell> Path path = Paths.get("/","tmp","test.txt");    
path ==> /tmp/test.txt

jshell> path.toRealPath()
$18 ==> /private/tmp/test.txt

jshell> path.startsWith("/")
$19 ==> true

jshell> File f = path.toFile()	// Path ==> File
f ==> /tmp/test.txt

java.nio.file.Files

jshell> Files.exists(path)
$16 ==> true

jshell> Files.notExists(path)    // !Files.exists() != Files.notExists(), π.χ. λόγω δικαιωμάτων πρόσβασης
$17 ==> false

jshell> Files.isDirectory(path)
$18 ==> false 

jshell> Files.isRegularFile(path)
$19 ==> true

jshell> Files.isReadable(path)
$20 ==> true

jshell> Files.isWritable(path)
$21 ==> true

jshell> Files.isExecutable(path)
$22 ==> false

jshell> Files.isHidden(path)
$23 ==> false

jshell> Files.isSymbolicLink(path)
$22 ==> false

jshell> Files.probeContentType(path)
$23 ==> "text/plain"

jshell> Files.size(path)
$24 ==> 0

jshell> Path p = Paths.get("/tmp/test.txt")
p ==> /tmp/test.txt

jshell> Files.isSameFile(path, p); // άλλος τρόπος πέραν του Path.exists() χωρίς να ελέγχει αν τα αρχεία/φάκελοι υπάρχουν.
$25 ==> true

Προσοχή! Η τελευταία εντολή ελέγχει τις δυο διαδρομές καλώντας την equals() πράγμα που σημαίνει ότι αν π.χ. η μια διαδρομή είναι απόλυτη (absolute) κι η άλλη σχετική (relative) παρόλο που μπορεί να δείχνουν στο ίδιο αρχείο/φάκελο, το αποτέλεσμα θα είναι false.

jshell> Files.getAttribute(path, "creationTime")
$26 ==> 2019-12-30T21:40:04Z

jshell> Files.getAttribute(path, "lastModifiedTime")
$27 ==> 2019-12-30T21:40:04Z

jshell> Files.getAttribute(path, "size")
$28 ==> 0

jshell> Files.getAttribute(path, "isDirectory")
$29 ==> false

jshell> Files.getAttribute(path, "dos:hidden") // δουλεύει μόνο στα Windows
|  Exception java.lang.UnsupportedOperationException: View 'dos' not available

Το 2ο όρισμα της getAttribute είναι της μορφής view:attribute όπου view μπορεί να είναι: basic, dos, posix με την basic να είναι η εξ’ορισμού. Άλλος τρόπος να διαβάσουμε τα γνωρίσματα (attributes) αρχείων και καταλόγων είναι:

jshell> import java.nio.file.attribute.*;

jshell> BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
attr ==> null

jshell> attr.size()
$30 ==> 0

jshell> attr.creationTime()
$31 ==> 2019-02-19T19:52:50Z

jshell> attr.lastAccessTime()
$32 ==> 2019-02-19T19:52:50Z

jshell> attr.lastModifiedTime()
$33 ==> 2019-02-19T19:52:50Z

jshell> attr.isDirectory()
$34 ==> false

jshell> attr.isRegularFile()
$35 ==> true

jshell> attr.isSymbolicLink()
$36 ==> false

jshell> attr.isOther()
$37 ==> false

jshell> FileTime fileTime = FileTime.fromMillis(System.currentTimeMillis());
fileTime ==> 2019-02-19T21:10:44.927Z

jshell> Files.setLastModifiedTime(path, fileTime);

jshell> Files.getLastModifiedTime(path)
$38 ==> 2019-02-19T21:10:44Z

DosFileAttributes

PosixFileAttributes

jshell> PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class);

jshell> attr.owner().getName()
$13 ==> "john"

jshell> attr.group().getName()
$14 ==> "staff"

jshell> attr.permissions().toString()
$15 ==> "[OWNER_WRITE, GROUP_READ, OTHERS_READ, OWNER_READ]"

jshell> Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
permissions ==> [OWNER_READ, OWNER_WRITE, GROUP_READ, OTHERS_READ]

jshell> Files.setPosixFilePermissions(path, permissions);

jshell> FileSystem fs = FileSystems.getDefault();
fs ==> sun.nio.fs.MacOSXFileSystem@50d0686

jshell> for (FileStore store : fs.getFileStores()) {
   ...> try {
   ...> long totalSpace = store.getTotalSpace() / 1024;
   ...> long usedSpace = (store.getTotalSpace() - store.getUnallocatedSpace()) / 1024;
   ...> long availableSpace = store.getUsableSpace() / 1024;
   ...> boolean isReadOnly = store.isReadOnly();
   ...> System.out.println("--- " + store.name() + " --- " + store.type());
   ...> System.out.println("Total space: " + totalSpace);
   ...> System.out.println("Used space: " + usedSpace);
   ...> System.out.println("Available space: " + availableSpace);
   ...> System.out.println("Is read only? " + isReadOnly);
   ...> } catch (IOException e) {
   ...> System.err.println(e);
   ...> }
   ...> }
--- /dev/disk0s2 --- hfs
Total space: 233591456
Used space: 226233780
Available space: 7101676
Is read only? false
--- devfs --- devfs
Total space: 228
Used space: 228
Available space: 0
Is read only? false
--- map -hosts --- autofs
Total space: 0
Used space: 0
Available space: 0
Is read only? false
--- map auto_home --- autofs
Total space: 0
Used space: 0
Available space: 0
Is read only? false
--- /dev/disk1s0 --- cd9660
Total space: 575606
Used space: 575606
Available space: 0
Is read only? true

Δημιουργία

Δημιουργία καταλόγου:

jshell> Path newdir = Paths.get("/tmp/newdir");
newdir ==> /tmp/newdir

jshell> Files.createDirectory(newdir);    //  throws exception if directory exists

jshell> Files.exists(newdir)
$1 ==> true

jshell> Path newdir2 = Paths.get("/tmp/newdir2");
newdir2 ==> /tmp/newdir2

jshell> Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
perms ==> [OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE]

jshell> FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
attr ==> java.nio.file.attribute.PosixFilePermissions$1@72d818d1

jshell> import java.nio.file.attribute.*;

jshell> Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
perms ==> [OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, GROUP_READ, GROUP_EXECUTE]

jshell> FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms);
attr ==> java.nio.file.attribute.PosixFilePermissions$1@72d818d1

jshell> Files.createDirectory(newdir2, attr);

jshell> Files.exists(newdir2)
$2 ==> true

jshell> Path newdir3 = FileSystems.getDefault().getPath("/tmp/", "test", "newdir3");
newdir3 ==> /tmp/test/newdir3

jshell> Files.createDirectories(newdir3);  // creates intermediate directories too, if they do not exist

jshell> Files.exists(newdir3)
$3 ==> true

Δημιουργία αρχείου:

jshell> Path newfile = Paths.get("/tmp/", "newfile");
newfile ==> /tmp/newfile

jshell> Files.createFile(newfile);

Εμφάνιση καταλόγου

jshell> Path directoryPath = Paths.get("C:/temp");
directoryPath ==> C:\temp

jshell> try (DirectoryStream<Path> ds = Files.newDirectoryStream(directoryPath)) {
   ...> for (Path file : ds) {
   ...> System.out.println(file.getFileName());
   ...> }
   ...> }catch(IOException e) {
   ...> System.err.println(e);
   ...> }
test.txt

Μπορούμε να χρησιμοποιήσουμε και χαρακτήρες μπαλαντέρ (globbing):

Π.χ.

try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, "*.{png,jpg,bmp}")) {
...

Μπορούμε να φιλτράρουμε τ’ αποτελέσματα (π.χ. μόνο αρχεία):

jshell> DirectoryStream.Filter<Path> fileFilter = new DirectoryStream.Filter<Path>() {
   ...> @Override
   ...> public boolean accept(Path path) throws IOException { 
   ...> return (Files.isRegularFile(path));
   ...> }
   ...> };
fileFilter ==> 1@617faa95

jshell> try (DirectoryStream<Path> ds = Files.newDirectoryStream(path, fileFilter)) {
...

Για αναδρομική προσπέλαση καταλόγων, το ΝΙΟ.2 προσφέρει τη διεπαφή FileVisitor και την υλοποίησή της SimpleFileVisitor. Ένα παράδειγμα χρήσης φαίνεται παρακάτω:

jshell> Files.walkFileTree(directoryPath, new SimpleFileVisitor<Path>() {
   ...> @Override
   ...> public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws 
   ...> IOException {
   ...> System.out.println("d " + dir.toAbsolutePath());
   ...> return FileVisitResult.CONTINUE;
   ...> }
   ...> @Override
   ...> public FileVisitResult visitFile(Path dir, BasicFileAttributes attrs) throws
   ...> IOException {
   ...> System.out.println("f " + dir.toAbsolutePath());
   ...> return FileVisitResult.CONTINUE;
   ...> }
   ...> });
d C:\temp
f C:\temp\test.txt
$3 ==> C:\temp

Άλλο ένα παράδειγμα, εύρεση μόνο των φακέλων:

jshell> Files.walkFileTree(directoryPath, new SimpleFileVisitor<Path>() {  
   ...>  @Override
   ...>  public FileVisitResult postVisitDirectory(Path dir, IOException e) throws
   ...>  IOException {
   ...>  System.out.println("d " + dir.toAbsolutePath());
   ...>  return FileVisitResult.CONTINUE;
   ...>  }
   ...>  });
d C:\temp
$4 ==> C:\temp

Η διεπαφή διαθέτει τις εξής μεθόδους:

public interface FileVisitor {
   FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs);
   FileVisitResult visitFile(Path dir, BasicFileAttributes attrs);
   FileVisitResult postVisitDirectory(Path dir, IOException e);
   FileVisitResult visitFileFailed(Path dir, IOException e);
}

Ας δούμε ένα ακόμα παράδειγμα, εύρεση ενός αρχείου:

jshell>  Path searchedFile = Paths.get("test.txt");
searchedFile ==> test.txt

jshell> Files.walkFileTree(directoryPath, new SimpleFileVisitor<Path>() {
   ...>  @Override
   ...>  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws
   ...>  IOException {
   ...>  return FileVisitResult.CONTINUE;
   ...>  }
   ...>  @Override
   ...> public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
   ...> return FileVisitResult.CONTINUE;
   ...> }
   ...>
   ...> @Override
   ...> public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws
   ...> IOException {
   ...>     Path name = file.getFileName();
   ...>     if (name != null && name.equals(searchedFile)) {
   ...>         System.out.println("Searched file was found: " + searchedFile +
   ...>         " in " + file.toRealPath().toString());
   ...>         return FileVisitResult.TERMINATE;
   ...>     } else {
   ...>         return FileVisitResult.CONTINUE;
   ...>     }
   ...> }
   ...> @Override
   ...> public FileVisitResult visitFileFailed(Path file, IOException exc)
   ...> throws IOException {
   ...> //report an error if necessary
   ...> return FileVisitResult.CONTINUE;
   ...> }
   ...> });
Searched file was found: test.txt in C:\temp\test.txt
$5 ==> C:\temp

Ανάγνωση αρχείου

jshell> import java.nio.charset.*;

jshell> List<String> linesOfContent = Files.readAllLines(path, StandardCharsets.UTF_8);
linesOfContent ==> []

jshell> byte[] content = Files.readAllBytes(path)
content ==> byte[87] { -50, -93, -50, -75, 32, -50, -77, -50, ... -75, -49, -127, -50, -82 }

jshell> String s = new String(content, "UTF-8");
s ==> "Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή"

jshell> try (BufferedReader reader = Files.newBufferedReader(path, Charset.forName("UTF-8"))) {
   ...> String line = null;
   ...> while ((line = reader.readLine()) != null) {
   ...> System.out.println(line);
   ...> }
   ...> } catch (IOException e) {
   ...> System.err.println(e);
   ...> }
Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή

Εγγραφή αρχείου

jshell> List<String> lines = Arrays.asList(
   ...> new String[]{"Σε γνωρίζω από την κόψη", "του σπαθιού την τρομερή"});
lines ==> [Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή]

jshell> Files.write(path, lines, Charset.forName("UTF-8"), StandardOpenOption.APPEND)
$1 ==> /tmp/test.txt

jshell> List<String> linesOfContent = Files.readAllLines(path, StandardCharsets.UTF_8);
linesOfContent ==> [Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή]

jshell> try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) {
   ...> for (String line : lines) {
   ...> outputStream.write((line + System.lineSeparator()).getBytes(StandardCharsets.UTF_8));
   ...> }
   ...> }
   
jshell> String s = "Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή"
s ==> "Σε γνωρίζω από την κόψη, του σπαθιού την τρομερή"

jshell> byte[] bytes = s.getBytes("UTF-8")
bytes ==> byte[87] { -50, -93, -50, -75, 32, -50, -77, -50, ... -75, -49, -127, -50, -82 }

jshell> Files.write(newfile, bytes)
$2 ==> /tmp/newfile  

jshell> try (BufferedWriter writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"), StandardOpenOption.APPEND)) {
   ...> writer.write(text);
   ...> } catch (IOException e) {
   ...> System.err.println(e);
   ...> }

Υπάρχουν οι εξής επιλογές:

StandardOperation. :

Αντιγραφή αρχείου/φακέλου

Στο προηγούμενο μάθημα και στο μάθημα με τις εξαιρέσεις είδαμε έναν τρόπο αντιγραφής ενός αρχείου. Η Files.copy() κάνει την ίδια δουλειά:

jshell> path // το πηγαίο αρχείο πρέπει να υπάρχει
path ==> /tmp/test.txt

jshell> Path backup = Paths.get("/tmp/test.bak")
backup ==> /tmp/test.bak

jshell> Files.copy(path, backup) 
$1 ==> /tmp/test.bak

jshell> Files.copy(path, dest)
|  Exception java.nio.file.FileAlreadyExistsException: /tmp/test.bak

jshell> Files.copy(path, dest, StandardCopyOption.REPLACE_EXISTING);
$2 ==> /tmp/test.bak

Η Files.copy() δουλεύει και για φακέλους αλλά να θυμάστε ότι δεν αντιγράφει υπο-φακέλους. Θα πρέπει να την χρησιμοποιήσετε για ν’ αντιγράψετε κάθε υπο-φάκελο.

Ένας γρήγορος τρόπος αντιγραφής αρχείων είναι και ο παρακάτω:

jshell> File sourceFile = path.toFile() // το πηγαίο αρχείο πρέπει να υπάρχει
sourceFile ==> /tmp/test.txt

jshell> File destFile = Paths.get("/tmp/backup.txt").toFile()
destFile ==> /tmp/backup.txt

jshell> Files.createFile(destFile.toPath());  // καλό είναι να υπάρχει τ' αρχείο προορισμού
$1 ==> /tmp/backup.txt

jshell> import java.nio.channels.*

jshell> if (sourceFile.exists() && destFile.exists()) {
   ...> try (FileChannel srcChannel = new FileInputStream(sourceFile).getChannel();
   ...> FileChannel sinkChannel = new FileOutputStream(destFile).getChannel()) {
   ...> srcChannel.transferTo(0, srcChannel.size(), sinkChannel);
   ...> } catch (IOException e) {
   ...> e.printStackTrace();
   ...> }
   ...> }

Μετονομασία/μετακίνηση αρχείου/φακέλου

jshell> Path path = Files.createFile(Paths.get("C:/temp/test.txt")); // το αρχείο πρέπει να υπάρχει
path ==> C:\temp\test.txt

jshell> Path targetPath = Paths.get("C:/Users/john/"); // must be a directory
targetPath ==> C:\Users\john
 
jshell> Files.move(path, targetPath.resolve(path.getFileName()), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); 
$1 ==> C:\Users\john\test.txt

Η παράμετρος StandardCopyOption.ATOMIC_MOVE εγγυάται ότι αν αποτύχει η μετακίνηση τότε το πηγαίο αρχείο/φάκελος παραμένει άθικτο/ς.

Δουλεύει και για φακέλους αν θέλετε μόνο να μετονομάσετε το φάκελο. Δε δουλεύει αν θέλετε να μετακινήσετε το φάκελο σε άλλο σύστημα αρχείων ή drive.

Εάν μετακινήσετε (move) μια συντόμευση (symbolic link), μετακινείται η ίδια η συντόμευση, όχι το αρχείο που δείχνει η συντόμευση. Είναι σημαντικό να σημειωθεί ότι στην περίπτωση της αντιγραφής (copy) που είδαμε προηγούμενα, αντιγράφεται το αρχείο που δείχνει η συντόμευση κι όχι η ίδια η συντόμευση. (Περισσότερα για τις συντομεύσεις παρακάτω).

Διαγραφή αρχείου/φακέλου

jshell> Files.deleteIfExists(path)
$1 ==> true

Οι εντολές deleteIfExists(), delete() εγείρουν εξαιρέσεις σε περίπτωση που κάτι πάει στραβά:

jshell> try {
   ...> Files.delete(path);
   ...> } catch (IOException x) {
   ...> System.out.println("Deletion failed");
   ...> }	
Deletion failed

Οι ακόλουθες εξαιρέσεις μπορούν να εγερθούν: NoSuchFileException, DirectoryNotEmptyException, IOException, SecurityException.

Στην περίπτωση διαγραφής καταλόγου (φακέλου), θα πρέπει να είναι άδειος.

Στην περίπτωση μιας συντόμευσης (symbolic link), διαγράφεται η συντόμευση, όχι το αρχείο που δείχνει η συντόμευση.

Στα συστήματα Unix, υπάρχει η δυνατότητα δημιουργίας συνδέσμων (links) και μάλιστα hard links και soft ή symbolic links. Στα Windows είναι οι αντίστοιχες συντομεύσεις (shortcuts). Το hard link είναι στην ουσία ένα αντίγραφο ενός αρχείου (δεν υπάρχουν hard links για φακέλους). Το soft link είναι απλά ένας δείκτης σ’ ένα αρχείο ή φάκελο. Θα πρέπει να υπάρχει το αρχείο για να δημιουργηθεί ένα soft link γι’ αυτό. Αντιθέτως, ο “στόχος” που δείχνει ένα soft link μπορεί και να μην υπάρχει με αποτέλεσμα το soft link να είναι “τζούφιο, άδειο”, να μη δείχνει πουθενά (π.χ. επειδή το αρχείο ή ο φάκελος στο οποίο έδειχνε διαγράφηκε). Αντιθέτως, αν διαγράψετε το αρχείο που δείχνει το hard link, τότε το hard link συνεχίζει να υπάρχει ως αντίγραφο του αρχικού αρχείου.

Στο Unix/Linux δημιουργούμε ένα hard link με την εντολή mklink ενώ ένα soft link με την εντολή ln -s (παρέχοντας πρώτα το αρχείο/φάκελο προορισμού και μετά το soft link).

Π.χ. η συντόμευση /usr/tmp -> ../var/tmp δηλώνει ότι το /usr/tmp δείχνει στο ../var/tmp. Εν τέλει, που δείχνει αυτή η συντόμευση; /usr/../var/tmp η οποία αποτιμάται στη διαδρομή /var/tmp.

Η κλάση java.nio.file.Files παρέχει τις ακόλουθες μεθόδους:

* createLink() 
* createSymbolicLink()
* isSymbolicLink()
* readSymbolicLink()

Ας δούμε μερικά παραδείγματα:

jshell> Path slink = Paths.get("C:/temp/slink.txt")
slink ==> C:\temp\slink.txt

jshell> Files.createSymbolicLink(slink, path)

jshell> Files.isSymbolicLink(slink)
$1 ==> true

jshell> Files.readSymbolicLink(slink);
$2 ==>

Να σημειωθεί ότι πολλές μέθοδοι δέχονται ως παράμετρο αν θα πρέπει ν’ ακολουθήσουν τις συντομεύσεις ή όχι (LinkOption.NOFOLLOW_LINKS).

Κανάλια

Στην αρχή του μαθήματος είπαμε ότι το ΝΙΟ είναι non-blocking. Αυτό το επιτυγχάνει χάρις στις νέες ασύγχρονες κλάσεις που εισάγει στη βιβλιοθήκη java.nio.channels όπως φαίνεται στην ακόλουθη εικόνα:

Εικόνα 5.3.1 Διάγραμμα κλάσεων ιεραρχίας Καναλιών (Channels)

Στην εικόνα προσέξτε την ιεραρχία διαμάντι μεταξύ των διεπαφών Channel, ReadableByteChannel, WritableByteChannel, ByteChannel που, όπως είδαμε στα μαθήματα της 2ης εβδομάδας, είναι κάτι που πρέπει να αποφεύγεται. Στην συγκεκριμένη περίπτωση καθώς δεν υπάρχουν επικαλυπτώμενες μέθοδοι, δεν υπάρχει κάποιο πρόβλημα.

Θα επικεντρωθούμε στα κανάλια διαχείρισης αρχείων (AsynchronousFileChannel και FileChannel), αλλά υπάρχουν και AsynchronousSocketChannel, AsynchronousServerSocketChannel, AsynchronousDatagramChannel για ασύγχρονη διαχείριση TCP και UDP sockets. Όπως είπαμε, με τον όρο ασύγχρονο Ι/Ο εννοούμε Ι/Ο (Input/Output) που δεν μπλοκάρει περιμένοντας δεδομένα από τη ροή ή το κανάλι επικοινωνίας. Όταν δεν υπάρχουν δεδομένα να διαχειριστεί, ελευθερώνει τον επεξεργαστή ώστε να εκτελέσει άλλες ενέργειες και αναλαμβάνει πάλι δράση όταν υπάρχουν δεδομένα προς επεξεργασία (δέχεται κάποια διακοπή - interrupt).

Σ’ ένα εισαγωγικό μάθημα σαν κι αυτό, δε θα εντρυφήσουμε στα κανάλια, ένα παράδειγμα χρήσης των οποίων είδαμε πιο πάνω στην αντιγραφή αρχείου. Τα κανάλια δουλεύουν με ByteBuffers τα οποία ξεφεύγουν από τις ανάγκες ενός εισαγωγικού μαθήματος της γλώσσας. Παρόλ’ αυτά θα δώσουμε εδώ μια μικρή εισαγωγή κι ένα παράδειγμα χρήσης.

Ένας java.nio.ByteBuffer, όπως λέει και τ’ όνομά του, είναι μια προσωρινή/ενδιάμεση θέση μνήμης για αποθήκευση bytes, ή πιο σωστά μια όψη (view) σε μια ακολουθία από bytes. Υπάρχουν δυο κατηγορίες: οι bytebuffers που χρησιμοποιούν μια συστοιχία (array) και εκείνοι που χρησιμοποιούν απευθείας ένα μέρος της μνήμης σωρού (heap).

Όπως βλέπετε στο παρακάτω σχήμα, ένας ByteBuffer συστοιχίας διαθέτει κάποιους δείκτες:

Ισχύει: 0 <= mark <= position <= limit <= capacity

Εικόνα 5.3.2 ByteBuffer συστοιχίας

Διαβάστε περισσότερα στις πηγές (π.χ. [Schuller, 2012]).

Στο προηγούμενο μάθημα είδαμε ένα παράδειγμα αποθήκευσης (σειριοποίησης) μιας ιεραρχίας κλάσεων (Track) σ’ ένα αρχείο με τη χρήση της ObjectOutputStream. Το μέγεθος του αρχείου tracks.dump είναι ~300 bytes.

Ένα AirTrack όμως καταλαμβάνει 25 bytes στη μνήμη:

και αντίστοιχα ένα LandTrack καταλαμβάνει 21 bytes.

Track airTrack = new AirTrack(5000, 10000, 20.0, -20.0);    // 25 bytes 
Track landTrack = new LandTrack(20, 10.0, -25.0);           // 21 bytes 
List<Track> tracks = new ArrayList<>();
tracks.add(airTrack);
tracks.add(landTrack);

Επιπλέον θα πρέπει ν’ αποθηκεύσουμε πόσα ίχνη θ’ αποθηκεύσουμε στο αρχείο, στο παράδειγμά μας 2 ίχνη, ένα αέρος και ένα εδάφους, που είναι άλλος ένας ακέραιος, δηλ. άλλα 4 bytes. Άρα συνολικά χρειαζόμαστε έναν ByteBuffer μεγέθους 25 + 21 + 4 = 50 bytes.

ByteBuffer buffer = ByteBuffer.allocate(50);    // 25 + 21 + 4 bytes
// try (RandomAccessFile store = new RandomAccessFile(new File("./tracks.dump"), "rw");
//      FileChannel channel = store.getChannel()) {
try (SeekableByteChannel channel = (Files.newByteChannel(Paths.get("./tracks.dump"),
     EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE)))) {
     channel.position(0);
     buffer.putInt(tracks.size()); // 4 bytes
     for (Track track : tracks) {
         System.out.println(track.toString());
         buffer.put((byte) track.getTrackType().ordinal());  // 1 byte
         buffer.putDouble(track.getX());     // 8 bytes
         buffer.putDouble(track.getY());     // 8 bytes
         buffer.putInt(track.getSpeed());    // 4 bytes
         if (track.getTrackType() == Track.TrackType.AIR) {
             buffer.putInt(((AirTrack) track).getHeight());  // 4 bytes
         }
	 }
    buffer.flip();  // limit = position; position = 0; <-- don't forget this!!!
    int nOfBytes = channel.write(buffer);
    System.out.println("Bytes written: " + nOfBytes);
    buffer.clear();
} catch (IOException ex) {
    System.err.println(ex);
}

Το μέγεθος του αρχείου tracks.dump είναι πλέον μόνο 50 bytes.

Για να διαβάσετε το αρχείο tracks.dump:

// try (RandomAccessFile store = new RandomAccessFile(new File("./tracks.dump"), "r");
//      FileChannel channel = store.getChannel()) {
try (SeekableByteChannel channel = (Files.newByteChannel(Paths.get("./tracks.dump"),
     EnumSet.of(StandardOpenOption.READ)))) {
     channel.position(0);
//   buffer.flip();      // limit = position; position = 0;
//   buffer.rewind();    // position = 0; mark discarded
//   buffer.clear();     // position =0; limit = capacity; mark discarded
    int nbytes;
    do {
        nbytes = channel.read(buffer);
    } while (nbytes != -1 && buffer.hasRemaining());
    System.out.println("Bytes read: " + nbytes);
    buffer.flip(); // limit = position; position = 0; <-- don't forget this!!!
    int numOfTracks = buffer.getInt();
    for (int i = 0; i < numOfTracks; i++) {
        byte type = buffer.get();      // 1 byte
        Track.TrackType trackType = Track.TrackType.values()[type];
        Track t = null;
        double x = buffer.getDouble();      // 8 bytes
        double y = buffer.getDouble();      // 8 bytes
        int speed = buffer.getInt();        // 4 bytes
        if (trackType == Track.TrackType.AIR) {
            int height = buffer.getInt();  // 4 bytes
            t = new AirTrack(height, speed, x, y);
        } else if (trackType == Track.TrackType.LAND) {
            t = new LandTrack(speed, x, y);
        }
        if (t != null) {
            System.out.println(t.toString());
        }
    }
} catch (IOException ex) {
    System.err.println(ex);
}

Υπάρχουν πολλοί τρόποι να λάβουμε ένα κανάλι, είδαμε έναν πιο πάνω κι άλλον έναν παρακάτω:

ByteBuffer buffer = ByteBuffer.allocateDirect(50);
try (FileChannel rdr = (new FileInputStream("./tracks.dump")).getChannel()) {
   while (rdr.read(buffer) > 0) {
     // Do something with the buffer
     buffer.clear();
   }
} catch (IOException ex) {
   System.err.println(ex);
}

Περίληψη

Σ’ αυτό το μάθημα είδαμε κάποιες από τις δυνατότητες του NIO.2. Μιλήσαμε για τις δυο βασικές κλάσεις java.nio.file.Path και java.nio.file.Files, για symbolic links και είδαμε ένα παράδειγμα καναλιών (Channels) με τη χρήση ByteBuffers. Δεν αναφερθήκαμε στις δυνατότητες που παρέχει το NIO.2 API να ανιχνεύουμε αλλαγές στο σύστημα αρχείων (java.nio.file.WatchService).

Πέρα από τα αρχεία, μπορείτε να αποθηκεύσετε τα δεδομένα σας και σε βάσεις δεδομένων, που είναι το θέμα του επόμενου μαθήματος.

Ασκήσεις

  1. Επιλύστε την άσκηση 1 του προηγούμενου μαθήματος χρησιμοποιώντας το ΝΙΟ.2 ΑΡΙ.

  2. Να γράψετε ένα πρόγραμμα σε Java που να διαβάζει ένα αρχείο HTML ως χαρακτήρες από την είσοδό του και να εμφανίζει στην έξοδό του το κείμενο της εισόδου χωρίς τις ετικέτες (tags) HTML. Π.χ. έστω το αρχείο HTML:

<html>
<body>
<h1>Κεφάλαιο 1</h1> 
Σ' αυτό το κεφάλαιο θα μιλήσουμε για τη γλώσσα προγραμματισμού Java.
</body>
</html>

τότε η έξοδος του προγράμματος θα είναι:

Κεφάλαιο 1
Σ' αυτό το κεφάλαιο θα μιλήσουμε για τη γλώσσα προγραμματισμού Java.

Πηγές

  1. Baeldung (2019), Guide to Java FileChannel
  2. Friesen J. (2015), Java I/O, NIO and NIO.2, APress.
  3. Gupta L. Working With Buffers – Java NIO 2.0
  4. Jenkov, Java NIO Buffer
  5. Leonard A. (2011), Pro Java 7 NIO.2, Apress.
  6. Schuller P. (2012), “The Java ByteBuffer – a crash course”, JavaCodeGeeks.
  7. Java Notes for Professionals
  8. Java NIO Buffer

<- Δ ->