JavaRush/Courses/Kotlin SELF/Working with bytes: readByt...

Working with bytes: readBytes() / writeBytes()

Open

1. Introduction

When you’re just starting to work with files, you want simple happiness: “here’s a file, I want to copy it.” And at that moment you learn that files have streams, buffers, chunked read loops, EOF, and other wonderful words that are great for scaring beginners on Halloween. But the truth is that sometimes you don’t need that whole zoo: if the file is small, you can honestly load it entirely into memory as a byte array, and just as honestly write it to another file — without interpretation, without encodings, without complex loops.

Exactly for such scenarios Kotlin/JVM has a very handy pair: File.readBytes() and File.writeBytes(bytes). They do what their names promise: read and write bytes “as-is.” In essence, this is the shortest path from “I have a binary file” to “I have its copy.”

Why this looks like “methods” on File

When you first see code like File("a.bin").readBytes(), it’s easy to think: “Oh, so the File class has a readBytes() method.” Then a second question appears: “Why did the Java File class suddenly get these Kotlin methods?”

The answer is elegantly Kotlin-ish: these are extension functions.

Kotlin lets you “add” functions to types even if you don’t own their source code. From the outside they’re called as if they were ordinary methods, but in reality the compiler substitutes a call to a regular function. That’s the core idea of extensions: “call it like a member, but we didn’t change the class.”

Psychologically, this is convenient: file.readBytes() reads almost like a human phrase, rather than “call some utility from some package.”

Minimal example: read bytes and check the size

First, let’s make the smallest and most straightforward example: read the entire file and print how many bytes we got. Here we don’t even copy anything — we just make sure the ByteArray actually arrived.

import java.io.File

fun main() {
    val file = File("in.bin")
    val bytes: ByteArray = file.readBytes()

    println("bytesCount=${bytes.size}") // bytesCount=...
}

Note: bytes.size is the number of elements in a ByteArray, i.e., the number of bytes. These are not “characters,” not “lines,” not “letters.” It is “how many units of data are stored in the file.”

2. Full binary copy: “read → write”

Now we do what this was all about: “read → write.” The important point: we do not try to understand what’s inside the file. We don’t convert bytes to text and back. We move them like bricks: “took bricks from box A, put them into box B.”

import java.io.File

fun main() {
    val src = File("in.bin")
    val dst = File("copy.bin")

    val data = src.readBytes()
    dst.writeBytes(data)

    println("copied: ${src.length()} -> ${dst.length()} bytes")
    // copied: 123 -> 123 bytes
}

Here we use length() as a quick indicator of the file size on disk (in bytes). This is not a 100% proof that the contents are identical, but for a “first check” it works great.

3. Extract the copying into copyBinaryWhole(...)

If you write the “copy” directly in main, then after a couple of days your main will start to look like a vacation to-do list: everything seems important, but it’s impossible to read. So let’s extract the copying into a function.

Let’s make a utility for our learning console app (let it be a mini utility FileTool — we’ll develop it neatly in lectures about files).

import java.io.File

fun copyBinaryWhole(src: File, dst: File) {
    val data = src.readBytes()
    dst.parentFile?.mkdirs() // in case of "out/dir/copy.bin"
    dst.writeBytes(data)
}

fun main() {
    copyBinaryWhole(File("in.bin"), File("out/copy.bin"))
    println("Done") // Done
}

Notice dst.parentFile?.mkdirs(). This is a small bit of future-proofing: if the path is nested, directories might not exist. We’re not diving into I/O reliability and exception handling (that’s a separate big topic), but we’re already getting used to writing code that won’t crash “because of a small thing.”

4. Result validation: “looks like” vs “exactly”

Validating the result is not paranoia; it’s a normal engineering habit. Especially with files: copying can be interrupted, the file can be locked, the disk can run out of space, and you can accidentally mix up paths (and happily overwrite the wrong thing).

It helps to keep two levels of checks in mind:

Check What it proves Pros Cons
src.length() == dst.length()
“Sizes match” Fast, doesn’t load memory Contents may differ
src.readBytes().contentEquals(dst.readBytes())
“Byte-for-byte identical” Reliable Both files end up in memory

Check by size

Let’s start simple: after copying, compare the sizes.

import java.io.File

fun main() {
    val src = File("in.bin")
    val dst = File("copy.bin")

    dst.writeBytes(src.readBytes())

    val ok = src.length() == dst.length()
    println("sameSize=$ok") // sameSize=true
}

If sameSize=false, the copy clearly failed. If true, that’s a good sign — but not a guarantee.

Check by content: contentEquals

To compare two byte arrays by content, you must compare the content itself, not “object references.” Kotlin explicitly warns: for arrays you cannot use == as “content comparison,” because arrays are objects, and == for them does not do what many expect. That’s what contentEquals() is for.

import java.io.File

fun main() {
    val a: ByteArray = File("a.bin").readBytes()
    val b: ByteArray = File("b.bin").readBytes()

    println(a.contentEquals(b)) // true or false
}

Here’s the key idea: ByteArray is an array, and it is compared in a “special way.” Exactly the same rule applied to regular arrays; it’s just especially useful now because we’re working with binary data.

5. Why a == b does not work for ByteArray

A very typical beginner situation: “In Kotlin I’m used to == meaning value comparison. Why doesn’t it work here?” And Kotlin replies: “Because arrays are special.” == checks structural equality for many types, but not for arrays — arrays have separate functions like contentEquals/contentDeepEquals.

By analogy, a == b for arrays is like asking, “Is this the same package?” (the same object), while contentEquals is “Are the things inside the package the same and in the same order?” For files and bytes, we almost always care about the second.

6. Limits of the “whole file” approach: memory is not infinite

This may sound obvious, but it’s useful: RAM is finite. readBytes() reads the entire file into a ByteArray, which means that array must fit somewhere. If you read a 2–3 GB file this way, the program can very quickly meet an OutOfMemoryError (and that meeting is usually not a friendly one).

So “read all at once” is a tool with a comfort zone: small and medium files. In the next lecture we’ll move on to streams and chunked reading, but even now you can add simple protection: check the size before reading.

import java.io.File

private const val MAX_BYTES: Long = 10L * 1024 * 1024 // 10 MB

fun main() {
    val file = File("in.bin")

    if (file.length() > MAX_BYTES) {
        println("File is too large: ${file.length()} bytes") // File is too large: ...
        return
    }

    val bytes = file.readBytes()
    println("Loaded ${bytes.size} bytes") // Loaded ...
}

This is not an “ideal strategy,” but it’s an honest and understandable barrier: you explicitly tell the code what size you are willing to load all at once.

7. Diagram: full copy + verification

So the steps don’t get mixed up in your head, let’s lock the process in as a mini flowchart. Yes, programmers like diagrams, because they let you think once and then pretend for a long time that everything is obvious.

flowchart TD
    A[Have src file] --> B{Is size acceptable?}
    B -->|no| X[Report and exit]
    B -->|yes| C["src.readBytes()"]
    C --> D["dst.writeBytes(data)"]
    D --> E{Verify result}
    E -->|size| F[src.length == dst.length]
    E -->|content| G["data.contentEquals(dst.readBytes())"]

8. FileTool: interactive binary copy

Now we’ll assemble a small “practical” piece of code that performs copying and offers a verification option. We won’t go into command-line arguments (that will be on another day of the course), so we’ll make a simple interactive scenario via readln().

Verification choice model

We need some way to represent the choice “verify by size / verify by content / don’t verify.” For this lecture, a string is enough, but to avoid spreading string magic everywhere, let’s make a small normalization function.

fun normalizeMode(input: String): String =
    input.trim().lowercase()

We already know trim() and lowercase() from earlier string topics, so everything here should look familiar.

Copy + verify by size

First, implement the option that doesn’t require keeping two files in memory.

import java.io.File

fun copyWholeWithSizeCheck(src: File, dst: File): Boolean {
    dst.writeBytes(src.readBytes())
    return src.length() == dst.length()
}

Boolean is convenient here: the function communicates “success/failure” with a simple value. We’re not digging into error/exception design — our goal for now is to build a clear baseline scenario.

Copy + verify by content

Now the “ironclad” check: byte-for-byte comparison via contentEquals, which is designed specifically for comparing array contents.

import java.io.File

fun copyWholeWithContentCheck(src: File, dst: File): Boolean {
    val data = src.readBytes()
    dst.writeBytes(data)

    val copied = dst.readBytes()
    return data.contentEquals(copied)
}

Yes, this reads “twice”: once from src, and a second time from dst. That’s fine for a learning example, but keep in mind: for large files this can be expensive.

Assemble main: one scenario, three modes

Now let’s put this into a single main. It will be short, but “alive”: it will ask for paths and a verification mode.

import java.io.File

fun main() {
    print("Source file path: ")
    val src = File(readln().trim())

    print("Destination file path: ")
    val dst = File(readln().trim())

    print("Verify mode (none/size/content): ")
    val mode = readln().trim().lowercase()

    val ok = when (mode) {
        "size" -> copyWholeWithSizeCheck(src, dst)
        "content" -> copyWholeWithContentCheck(src, dst)
        else -> { dst.writeBytes(src.readBytes()); true }
    }

    println("OK=$ok") // OK=true (if everything worked)
}

Here when is used as an expression to produce ok. You’ve already covered when earlier in the course program, so the construct should read fine.

Nuance: why in none mode we still return true

The none/else branch in our main performs the copy and says “seems ok.” This is a deliberately simplified option: we don’t validate the result, so we “don’t know,” but for the “just copy it” scenario that’s exactly what the user wanted.

If you want a more honest contract, you could return not a Boolean, but, for example, a status string (“COPIED_WITHOUT_VERIFICATION”). But that starts pulling us into designing operation outcomes, and today our goal is to learn readBytes/writeBytes and understand the limits of the approach.

9. Typical mistakes when working with readBytes() / writeBytes()

Mistake #1: reading a binary file as text “out of habit.”
After days of readText() and strings, your hand automatically reaches to read everything “as a string,” and then you wonder about “garbage characters.” It’s important to ask yourself up front: “Is this text that someone will read with their eyes, or is it bytes (image/archive/executable)?” For binary data, text methods are almost always the wrong tool.

Mistake #2: assuming src.length() == dst.length() guarantees identical content.
Same size is good news, but it’s not proof. Two different files can have the same length. If you need “byte-for-byte identical,” you have to compare contents via contentEquals() (and remember that for arrays you cannot rely on ==).

Mistake #3: comparing ByteArray with == and getting weird results.
This is the classic trap: “But in Kotlin == compares values!” Arrays in Kotlin are a special case; they have dedicated content comparison functions.

Mistake #4: mindlessly reading huge files entirely.
readBytes() is convenient, but it’s not magic: it will load the entire file into memory. With large files, this becomes a memory and performance risk. Even at this step, it’s useful to get used to checking the size via length() and setting a limit, and for large files use a streaming approach (we’ll move to it later in the day’s plan).

Mistake #5: forgetting about destination directories.
Sometimes you write to a path like out/copy.bin, but the out folder doesn’t exist yet. writeBytes() doesn’t “create the world around it”; it just writes the file. So either create directories beforehand, or at least do dst.parentFile?.mkdirs() before writing, so you don’t hit errors for no reason.

1
Task
Kotlin SELF,  43 level,  1 lecture
Locked
File size
File size
1
Task
Kotlin SELF,  43 level,  1 lecture
Locked
Reference binary
Reference binary
1
Task
Kotlin SELF,  43 level,  1 lecture
Locked
Copy to a folder
Copy to a folder
1
Task
Kotlin SELF,  43 level,  1 lecture
Locked
Checking the copy
Checking the copy
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet