JavaRush/Courses/Kotlin SELF/How to think in bytes and why ByteArray is needed

How to think in bytes and why ByteArray is needed

Open

1. A file on disk is bytes

Today we’ll make an important “mindset shift”: we’ll stop perceiving a file as a “string on disk” and start perceiving it as what it really is — a sequence of bytes. It sounds dry, but in practice it saves hours of pain when you first try to “read an image as text” and you get… well, let’s call it poetry of machine origin.

When you’re used to readText(), it’s easy to start believing that a file “consists of letters”. In reality, a file on disk consists of bytes — small numbers stored one after another. “Text” appears only when we agree on how to interpret those bytes as characters. And that agreement is the main reason why the same set of bytes sometimes turns into normal text and sometimes into “garbled characters”.

Imagine a simple diagram:

flowchart LR
    A[File on disk] --> B[Bytes: 0..255]
    B --> C{Interpret as text?}
    C -- yes --> D[Decoding by encoding rules]
    D --> E[String: characters]
    C -- no --> F[Binary data: image/archive/audio...]

The main idea: a string is an interpretation, and bytes are reality.

2. Text and bytes: when readText() is not suitable

In previous lectures we read files as text, and everything was fine: you read lines, trim spaces, parse numbers — great. But as soon as you take a file that by meaning is not text (for example, PNG or ZIP), trying to “read it as a string” turns into meaningless mush. And this is not a “Kotlin error”. It’s an error in our idea: we applied a text lens to data where there is no text.

It helps to keep a simple comparison in mind:

What we have What is in the file on disk How to read it Why
.txt, .csv, .json (usually) bytes that are encoded characters readText(), readLines() we want to get characters
.png, .zip, .pdf, .mp3 bytes that represent the structure of the format readBytes(), streams we want the data as-is

In Kotlin/JVM there is a separate category of arrays for primitives (like IntArray, DoubleArray, and our hero ByteArray). This is not a “regular Array<Byte>”, but a more efficient special type.

3. ByteArray is a container for “raw” data

If up to this point you’ve lived in the String world, then ByteArray is your entry into the world of “data without interpretation”. The good news: in terms of behavior it’s very similar to arrays you’ve already studied. It has size, index access via [], and it works great in loops. The bad news: it doesn’t try to be convenient “text”, so you need to think a bit more carefully with it (but we’ll manage — we’ve already survived null and try/catch, so we can’t be scared).

Let’s start with the simplest thing: create a byte array and look at what’s inside.

fun main() {
    val bytes = byteArrayOf(1, 2, 3, 4)

    println("size=${bytes.size}")   // size=4
    println("first=${bytes[0]}")    // first=1
}

ByteArray supports regular indexing (zero-based indices) — this is a general rule for arrays in Kotlin.

Now an example with an “empty” array of the required size — this already looks like a buffer, which we’ll soon use when working with files:

fun main() {
    val buffer = ByteArray(8 * 1024) // 8 KB

    buffer[0] = 10
    buffer[1] = 20

    println("bufferSize=${buffer.size}")  // bufferSize=8192
    println("first=${buffer[0]}")         // first=10
}

Pay attention to the style: we create the array once, and then change its contents. This is an important habit for I/O: reusing a buffer is often better than creating new arrays for every little thing.

4. Useful nuances: Byte, buffer, and array comparison

The Byte type in Kotlin: it’s “small and a bit grumpy”

Before this we had Int and Long, which behave “as expected”: big numbers, all good. Byte is a small integer of size 1 byte, and in Kotlin it is signed: range -128..127. This sometimes surprises beginners, because a “byte in a file” is often thought of as 0..255. But in Kotlin the Byte type is exactly like this.

Let’s look at an example:

fun main() {
    val a: Byte = 120
    val b: Byte = 127

    println("a=$a") // a=120
    println("b=$b") // b=127
}

And now — careful, a demonstration of “why you can’t just take 200 and shove it into Byte”:

fun main() {
    val x = 200

    val asByte: Byte = x.toByte()
    println("x=$x")             // x=200
    println("asByte=$asByte")   // asByte=-56
}

This is not a bug: that’s how the conversion works, because Byte physically cannot store 200 within its range, so the value gets “trimmed” to what fits into 8 bits. For now you don’t need to dive deep into binary arithmetic — it’s just important to remember the practical rule: a byte is small, and during conversions data can be “lost”.

Buffer: a ByteArray that works like a “bucket”

The word “buffer” sounds like something from systems programming, accessible only to people who drink coffee without sugar and write drivers. In reality, a buffer is just a fixed-size array that we use as a “bucket”: scoop up a portion of data, process it, scoop up the next.

We’re not touching file streams yet, but we can practice the “in portions” idea on a regular array: simulate copying data in chunks.

fun main() {
    val source = byteArrayOf(10, 20, 30, 40, 50, 60, 70)
    val chunkSize = 3

    var i = 0
    while (i < source.size) {
        val end = minOf(i + chunkSize, source.size)
        println("chunk: ${source.copyOfRange(i, end).contentToString()}")
        i += chunkSize
    }
    // chunk: [10, 20, 30]
    // chunk: [40, 50, 60]
    // chunk: [70]
}

What matters here is not the copyOfRange function itself, but the idea: the last chunk is almost always smaller than the normal portion size. This is exactly what will later become critical when reading files: the last portion of data is rarely exactly 8 KB or 16 KB — it’s “cut off by the end of the file”.

Comparing ByteArray: why == compares the wrong thing

There’s one classic moment where beginners (and sometimes experienced people on Monday morning) fall into a trap: comparing arrays. Intuitively you want to do: “if two byte arrays are the same, then a == b should be true”. But arrays have a peculiarity: the == operator for them often means comparing references, not contents. That is, “is it the same object?” instead of “is it the same data?”.

For data arrays, we almost always need a content comparison — and for that there is contentEquals(...).

fun main() {
    val a = byteArrayOf(1, 2, 3)
    val b = byteArrayOf(1, 2, 3)

    println(a == b)                // false (usually)
    println(a.contentEquals(b))    // true
}

In practice, this is super important for the file topic: if you make a “file copy”, checking “the copy equals the original” is exactly a byte-for-byte check, not “do they reference the same object”.

5. Mini-upgrade to the practical app: text → bytes

We’re trying to develop one application gradually. Up to this point we confidently saved data as text: we built report strings and wrote them to a file. Today we’ll add a small but conceptually important step: we’ll learn to obtain a byte representation of our data. This is preparation for working with binary data (copying, archiving, etc.), but without diving into future topics.

Suppose we have a function that builds a report text (in previous days you might have written it using StringBuilder or just concatenation). Now we’ll make a function that returns a ByteArray instead.

fun reportText(total: Int): String {
    return "Total expenses: $total"
}

fun reportBytes(total: Int): ByteArray {
    val text = reportText(total)
    return text.encodeToByteArray()
}

fun main() {
    val bytes = reportBytes(total = 1200)
    println("bytes=${bytes.size}") // bytes=20 (may differ)
}

What’s important to understand at the level of today’s lecture:

String and ByteArray are different “forms” of data. A string is characters (in the language’s memory), while ByteArray is specific bytes that you can already write “as-is” or pass to low-level APIs. The encodeToByteArray() function and its inverse decodeToString() provide a bridge between these worlds.

And let’s show the reverse conversion — purely as a demonstration that bytes can be text, if you know for sure that these are bytes of text:

fun main() {
    val original = "Hello, bytes!"
    val bytes = original.encodeToByteArray()
    val restored = bytes.decodeToString()

    println(original)  // Hello, bytes!
    println(restored)  // Hello, bytes!
}

A very careful idea: not all bytes are text. These bytes are text because we made them that way ourselves. If these were the bytes of a PNG image, decodeToString() would be about the same as trying to read a barcode with your eyes without a scanner: technically possible, but the meaning slips away.

6. A future pipeline: “data → bytes → storage”

To avoid confusion later (when real files, streams, and buffering appear), it helps to lock in the mental pipeline in advance:

flowchart TD
    A[Application data: numbers, strings, structures] --> B[String report / format]
    B --> C[ByteArray: bytes]
    C --> D["File/archive/copy (later)"]

Today we stand confidently at the ByteArray step. The next lectures of the day will teach how to properly read/write those bytes to a file and move them in chunks — but for now our goal is to stop confusing text and binary and start living calmly with the idea that “bytes are normal”.

7. Common mistakes: how to recognize them by symptoms

Mistake #1: reading a binary file using readText() “just to take a look”.
This almost always ends with “garbage” in the console and the false feeling that the file is corrupted. In reality, only our interpretation is corrupted. If a file is not text by meaning, your default model should be “bytes”, i.e., ByteArray, not String.

Mistake #2: thinking that Byte stores the range 0..255.
In Kotlin, Byte is signed, so during conversions like 200.toByte() you’ll get a negative number. Nothing “broke” — it just “didn’t fit”. The fix is not panic, but careful understanding of types: Byte is small, and data can be lost.

Mistake #3: comparing byte arrays with == and being surprised.
When you compare two sets of data (especially if later you’ll check “does the file copy match the original”), you need a content comparison: contentEquals. Otherwise you’re not comparing data, but “is it the same object”, which is almost never what you wanted.

Mistake #4: creating new arrays inside every step instead of reusing a buffer.
Even without files, it’s clear that chunk-based work almost always implies reusing the same container (buffer). If in the future you start reading large data and constantly create new ByteArray, you’ll just make your program do garbage-collection (GC) fitness — and give yourself a profiling adventure.

Mistake #5: trying to convert any bytes back into a string via decodeToString().
The “bytes ↔ string” conversion only makes sense when the bytes really are encoded text. For images, archives, and other binary formats, this action doesn’t add meaning — only noise.

1
Task
Kotlin SELF,  43 level,  0 lecture
Locked
Four bytes
Four bytes
1
Task
Kotlin SELF,  43 level,  0 lecture
Locked
Strange Byte
Strange Byte
1
Task
Kotlin SELF,  43 level,  0 lecture
Locked
Text in bytes
Text in bytes
1
Task
Kotlin SELF,  43 level,  0 lecture
Locked
Two comparisons
Two comparisons
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet