Kotlic Collections 01 - Collection, List, Map, Set

Dmitry Klokov·2021년 1월 26일
0

KOTLIN

목록 보기
4/6
post-thumbnail

Collection types



The Kotlin Standard Library provides implementations for basic collection types: sets, lists, and maps. A pair of interfaces represent each collection type:

  • A read-only interface that provides operations for accessing collection elements.
  • A mutable interface that extends the corresponding read-only interface with write operations: adding, removing, and updating its elements.

Note that altering a mutable collection doesn't require it to be a var: write operations modify the same mutable collection object, so the reference doesn't change. Although, if you try to reassign a val collection, you'll get a compilation error.

The read-only collection types are covariant. This means that, if a Rectangle class inherits from Shape, you can use a List<Rectangle> anywhere the List<Shape> is required. In other words, the collection types have the same subtyping relationship as the element types. Maps are covariant on the value type, but not on the key type.

In turn, mutable collections aren't covariant; otherwise, this would lead to runtime failures. If MutableList<Rectangle>was a subtype of MutableList<Shape>, you could insert other Shape inheritors (for example, Circle) into it, thus violating its Rectangle type argument.

Get familiar with collections


Collection

List

As you see, in some aspects lists are very similar to arrays. However, there is one important difference: an array's size is defined upon initialization and is never changed; in turn, a list doesn't have a predefined size; a list's size can be changed as a result of write operations: adding, updating, or removing elements.

In Kotlin, the default implementation of List is ArrayList which you can think of as a resizable array.

Set

Set<T> stores unique elements; their order is generally undefined. null elements are unique as well: a Set can contain only one null. Two sets are equal if they have the same size, and for each element of a set there is an equal element in the other set.

MutableSet is a Set with write operations from MutableCollection.

The default implementation of SetLinkedHashSet – preserves the order of elements insertion. Hence, the functions that rely on the order, such as first() or last(), return predictable results on such sets.

An alternative implementation – HashSet – says nothing about the elements order, so calling such functions on it returns unpredictable results. However, HashSet requires less memory to store the same number of elements.

Map

Map<K, V> is not an inheritor of the Collection interface; however, it's a Kotlin collection type as well. A Map stores key-value pairs (or entries); keys are unique, but different keys can be paired with equal values. The Map interface provides specific functions, such as access to value by key, searching keys and values, and so on.

MutableMap is a Map with map write operations, for example, you can add a new key-value pair or update the value associated with the given key.

The default implementation of MapLinkedHashMap – preserves the order of elements insertion when iterating the map. In turn, an alternative implementation – HashMap – says nothing about the elements order.

Constructing Collections


Constructing from elements

The most common way to create a collection is with the standard library functions listOf<T>(), setOf<T>(), mutableListOf<T>(), mutableSetOf<T>(). If you provide a comma-separated list of collection elements as arguments, the compiler detects the element type automatically. When creating empty collections, specify the type explicitly.

The same is available for maps with the functionsmapOf() and mutableMapOf(). The map's keys and values are passed as Pair objects (usually created with to infix function).

Note that the to notation creates a short-living Pair object, so it's recommended that you use it only if performance isn't critical. To avoid excessive memory usage, use alternative ways. For example, you can create a mutable map and populate it using the write operations. The apply() function can help to keep the initialization fluent here.

Empty collections

Initializer functions for lists

For lists, there is a constructor that takes the list size and the initializer function that defines the element value based on its index.

Note: List() & listOf()

Concreate type constructors

To create a concrete type collection, such as an ArrayList or LinkedList, you can use the available constructors for these types. Similar constructors are available for implementations of Set and Map.

Copying

To create a collection with the same elements as an existing collection, you can use copying operations. Collection copying operations from the standard library create shallow copy collections with references to the same elements. Thus, a change made to a collection element reflects in all its copies.

Collection copying functions, such as toList(), toMutableList(), toSet() and others, create a snapshot of a collection at a specific moment. Their result is a new collection of the same elements. If you add or remove elements from the original collection, this won't affect the copies. Copies may be changed independently of the source as well. These functions can also be used for converting collections to other types, for example, build a set from a list or vice versa.

Alternatively, you can create new references to the same collection instance. New references are created when you initialize a collection variable with an existing collection. So, when the collection instance is altered through a reference, the changes are reflected in all its references.

Collection initialization can be used for restricting mutability. For example, if you create a List reference to a MutableList, the compiler will produce errors if you try to modify the collection through this reference.

Invoking functions on other collections

Collections can be created in result of various operations on other collections. For example, filtering a list creates a new list of elements that match the filter:

Mapping produces a list of a transformation results:

Association produces maps:

Iterators


For traversing collection elements, the Kotlin standard library supports the commonly used mechanism of iterators – objects that provide access to the elements sequentially without exposing the underlying structure of the collection. Iterators are useful when you need to process all the elements of a collection one-by-one, for example, print values or make similar updates to them.

Iterators can be obtained for inheritors of the Iterable<T> interface, including Set and List, by calling the iterator() function. Once you obtain an iterator, it points to the first element of a collection; calling the next() function returns this element and moves the iterator position to the following element if it exists.

Note : Once the iterator passes through the last element, it can no longer be used for retrieving elements; neither can it be reset to any previous position. To iterate through the collection again, create a new iterator.

Another way to go through an Iterable collection is the well-known for loop. When using for on a collection, you obtain the iterator implicitly. So, the following code is equivalent to the example above:

Finally, there is a useful forEach() function that lets you automatically iterate a collection and execute the given code for each element. So, the same example would look like this:

List iterators

For lists, there is a special iterator implementation: ListIterator. It supports iterating lists in both directions: forwards and backwards. Backward iteration is implemented by the functions hasPrevious() and previous(). Additionally, the ListIterator provides information about the element indices with the functions nextIndex() and previousIndex().

Having the ability to iterate in both directions, means the ListIterator can still be used after it reaches the last element.

Mutable iterators

For iterating mutable collections, there is MutableIterator that extends Iterator with the element removal function remove(). So, you can remove elements from a collection while iterating it.

In addition to removing elements, the MutableIterator can also insert and replace elements while iterating the list.

Ranges and Progressions


Kotlin lets you easily create ranges of values using the rangeTo() function from the kotlin.ranges package and its operator form ... Usually, rangeTo() is complemented by in or !in functions:

Integral type ranges (IntRange, LongRange, CharRange) have an extra feature: they can be iterated over. These ranges are also progressions of the corresponding integral types. Such ranges are generally used for iteration in the for loops:

To iterate numbers in reverse order, use the downTo function instead of ..:

It is also possible to iterate over numbers with an arbitrary step (not necessarily 1). This is done via the step function:

To iterate a number range which does not include its end element, use the until function:

Range

A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values which are both included in the range. Ranges are defined for comparable types: having an order, you can define whether an arbitrary instance is in the range between two given instances. The main operation on ranges is contains, which is usually used in the form of in and !in operators.

To create a range for your class, call the rangeTo() function on the range start value and provide the end value as an argument. rangeTo() is often called in its operator form ...

Progression

As shown in the examples above, the ranges of integral types, such as Int, Long, and Char, can be treated as arithmetic progressions of them. In Kotlin, these progressions are defined by special types: IntProgression, LongProgression, and CharProgression.

Progressions have three essential properties: the first element, the last element, and a non-zero step. The first element is first, subsequent elements are the previous element plus a step. Iteration over a progression with a positive step is equivalent to an indexed for loop in Java/JavaScript.

for (int i = first; i <= last; i += step) {
	// ...
}

When you create a progression implicitly by iterating a range, this progression's first and last elements are the range's endpoints, and the step is 1.

for (i in 1..10) print(i)

To define a custom progression step, use the step function on a range.

for (i in 1..8 step 2) print(i)

The last element of the progressions is calculated this way:

  • For a positive step: the maximum value not greater than the end value such that:
    (last - first) % step == 0.

  • For a negative step: the minimum value not less than the end value such that:
    (last - first) % step == 0.

Thus, the last element is not always the same as the specified end value.

for (i in 1..9 step 3) print(i) // the last element is 7

To create a progression for iterating in reverse order, use downTo instead of .. when defining the range for it.

for (i in 4 downTo 1) print(i)

Progressions implement Iterable<N>, where N is Int, Long, or Char respectively, so you can use them in various collection functions like map, filter, and other.

println( (1..10).filter { it % 2 == 0 } )
profile
Power Weekend

0개의 댓글