Warm greetings! This lesson introduces data streams, which are essentially continuous datasets. Think of a weather station or gaming application gathering data per second — both generate data streams! We will master handling these data streams using Kotlin, learning to access elements, slice segments, and convert these streams into strings for easier handling.
In Kotlin, data streams can be represented using arrays or lists. Kotlin's syntax and powerful type inference mean that we don't need explicit type annotations as in some other languages.
Consider a straightforward Kotlin class named DataStream
. This class encapsulates operations related to data streams in our program:
Kotlin1data class DataElement(val id: Int, val value: Int) 2 3class DataStream(private val data: List<DataElement>) { 4 // Methods will be added in later sections 5}
To use it, we create a sample data stream as an instance of our DataStream
class, where each element is a DataElement
object with two properties, id
and value
:
Kotlin1val stream = DataStream( 2 listOf( 3 DataElement(1, 100), 4 DataElement(2, 200), 5 DataElement(3, 300), 6 DataElement(4, 400) 7 ) 8)
To examine individual elements of a data stream, we use indexing. The get()
method we introduce below fetches the i
-th element from the data stream and returns a nullable type to handle cases where the index might be out of bounds:
Kotlin1data class DataElement(val id: Int, val value: Int) 2 3class DataStream(private val data: List<DataElement>) { 4 5 fun get(i: Int): DataElement? { 6 return data.getOrNull(i) 7 } 8}
Here, we can see the get()
method in action:
Kotlin1val stream = DataStream( 2 listOf( 3 DataElement(1, 100), 4 DataElement(2, 200), 5 DataElement(3, 300), 6 DataElement(4, 400) 7 ) 8) 9 10println(stream.get(2)) // It prints: DataElement(id=3, value=300) 11println(stream.get(-1)) // It prints: null
In essence, stream.get(2)
fetched us DataElement(id=3, value=300)
— the third element. Remember that Kotlin handles non-existent indices by returning null
.
Fetching a range of elements rather than a single one is facilitated by slicing. In Kotlin, we take advantage of the subList()
method to achieve similar results:
Kotlin1data class DataElement(val id: Int, val value: Int) 2 3class DataStream(private val data: List<DataElement>) { 4 5 fun get(i: Int): DataElement? { 6 return data.getOrNull(i) 7 } 8 9 fun slice(i: Int, j: Int): List<DataElement> { 10 // Ensure indices are within bounds 11 return data.subList(i.coerceAtLeast(0), j.coerceAtMost(data.size)) 12 } 13}
Here's a quick usage example:
Kotlin1val stream = DataStream( 2 listOf( 3 DataElement(1, 100), 4 DataElement(2, 200), 5 DataElement(3, 300), 6 DataElement(4, 400) 7 ) 8) 9 10println(stream.slice(1, 3)) 11// It prints: [DataElement(id=2, value=200), DataElement(id=3, value=300)]
In Kotlin, coerceAtLeast()
and coerceAtMost()
are functions that are used to restrict a value within a specified range.
-
coerceAtLeast(minimumValue)
ensures that the value is not less than theminimumValue
. If the original value is less, it is adjusted to be equal to theminimumValue
. -
coerceAtMost(maximumValue)
ensures that the value does not exceed themaximumValue
. If the original value is greater, it is adjusted to be equal to themaximumValue
.
In the context of the slice
function, i.coerceAtLeast(0)
ensures that the starting index is not less than 0, and j.coerceAtMost(data.size)
ensures that the ending index does not exceed the size of the list. This prevents index out-of-bounds errors.
To gain a clearer view of our data, we may wish to convert our data streams into strings. Kotlin provides intuitive string manipulation techniques without requiring additional libraries. Check out the string conversion in use:
Kotlin1data class DataElement(val id: Int, val value: Int) 2 3class DataStream(private val data: List<DataElement>) { 4 5 fun get(i: Int): DataElement? { 6 return data.getOrNull(i) 7 } 8 9 fun slice(i: Int, j: Int): List<DataElement> { 10 return data.subList(i.coerceAtLeast(0), j.coerceAtMost(data.size)) 11 } 12 13 fun joinToString(): String { 14 return data.joinToString(separator = ", ", prefix = "[", postfix = "]") 15 } 16}
Here's how it works:
Kotlin1val stream = DataStream( 2 listOf( 3 DataElement(1, 100), 4 DataElement(2, 200), 5 DataElement(3, 300), 6 DataElement(4, 400) 7 ) 8) 9 10println(stream.joinToString()) 11// It prints: [DataElement(id=1, value=100), DataElement(id=2, value=200), DataElement(id=3, value=300), DataElement(id=4, value=400)]
In our DataStream
class, we use the joinToString()
function to create a formatted string representation where:
- Separator: Elements in the data stream are separated by a comma and space (
,
) to clearly distinguish between eachDataElement
in the string representation. - Prefix: The entire sequence is wrapped with a starting square bracket (
[
), indicating the beginning of the list. - Postfix: A closing square bracket (
]
) is used to denote the end of the data stream list, providing a clear and consistent boundary for the string representation.
This provides a clean and readable string representation of our data stream without requiring any external libraries.
You can also use a lambda expression with the joinToString()
function to customize the format of each element in the resulting string. For example:
Kotlin1fun joinToCustomString(): String { 2 return data.joinToString(separator = ", ") { "{ 'id': ${it.id}, 'value': ${it.value} }" } 3}
In Kotlin, a lambda function is an anonymous function that can be treated as a value — you can pass it as an argument, return it from another function, or do anything else you would do with a normal object. Lambda expressions can capture variables from the surrounding scope, providing a succinct way to represent small function expressions. This allows you to easily define custom formatting logic directly within the joinToString()
call.
In this code, joinToString()
is called with a lambda expression that specifies how each DataElement
should be formatted. The lambda, defined within curly braces, takes each DataElement
(it
) and returns a string in the format "{ 'id': ${it.id}, 'value': ${it.value} }"
. This customization allows for a clear and readable string representation of each element within the data stream.
In later lessons, we'll expand data stream functionality to include more complex data stream handling capabilities.
In this lesson, we've explored data streams, discovered how to represent and manipulate them using native Kotlin data structures, especially lists, and encapsulated operations on data streams using Kotlin classes. We've also converted data streams to strings using a formatted string representation for easy readability. This section demonstrates how to use Kotlin's joinToString()
functionality to create string representations of data streams.
Now it's time to apply your newfound knowledge in the practice exercises that follow!