Главная страница
Навигация по странице:

  • Таблица 24.11.

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница58 из 64
    1   ...   54   55   56   57   58   59   60   61   ...   64
    548 Глава 24 • Углубленное изучение коллекций queue ++= List("b", "c") // Queue(a, b, c)
    queue // Queue(a, b, c)
    queue.dequeue // a queue // Queue(b, c)
    Стеки
    Scala предоставляет изменяемый стек. Ниже представлен пример:
    val stack = new scala.collection.mutable.Stack[Int]
    stack.push(1) // Stack(1)
    stack // Stack(1)
    stack.push(2) // Stack(2, 1)
    stack // Stack(2, 1)
    stack.top // 2
    stack // Stack(2, 1)
    stack.pop // 2
    stack // Stack(1)
    Обратите внимание: в Scala нет поддержки неизменяемых стеков, поскольку данная функциональность уже реализована в списках. Операция push над неизменяемым стеком аналогична выражению a
    ::
    для списка. Операция pop
    — то же самое, что вызов head и tail для списка.
    Изменяемые ArraySeq
    Последовательный массив — это изменяемая последовательность фикси­
    рованного размера, которая хранит свои элементы внутри
    Array[AnyRef]
    В Scala он реализован в виде класса
    ArraySeq
    Данный класс обычно используется в случаях, когда вам нужен производи­
    тельный массив и притом необходимо создавать обобщенные экземпляры последовательности с элементами, тип которых не известен заранее и не может быть получен во время выполнения с помощью
    ClassTag
    . С этими проблемами, которые присущи массивам, вы познакомитесь чуть позже, в разделе 24.9.
    Хеш-таблицы
    Хеш­таблица сохраняет свои элементы в образующем ее массиве, помещая каждый в позицию в массиве, определяемую хеш­кодом этого элемента.
    На добавление элемента в хеш­таблицу всегда уходит одно и то же время,

    24 .8 . Конкретные классы изменяемых коллекций 549
    если только в массиве нет еще одного элемента с точно таким же хеш­кодом.
    Поэтому, пока помещенные в хеш­таблицу элементы имеют хорошее рас­
    пределение хеш­кодов, работа с ней выполняется довольно быстро. По этой причине типы изменяемых отображений и множеств по умолчанию в Scala основаны на хеш­таблицах.
    Хеш­множества и хеш­отображения используются точно так же, как и любые другие множества или отображения. Ниже представлены некоторые простые примеры:
    val map = collection.mutable.HashMap.empty[Int,String]
    map += (1 –> "make a web site")
    // Map(1 –> make a web site)
    map += (3 –> "profit!")
    // Map(1 –> make a web site, 3 –> profit!)
    map(1) // make a web site map.contains(2) // false
    Конкретный порядок обхода элементов хеш­таблицы не гарантируется.
    Выполняется простой обход элементов массива, на котором основана хеш­таблица, в порядке расположения его элементов. Чтобы получить гарантированный порядок обхода, следует воспользоваться связанными хеш­отображением или множеством вместо обычных. Связанные хеш­
    отображение или множество почти аналогичны обычным хеш­отображению или множеству, но включают связанный список элементов в порядке их до­
    бавления. Обход элементов такой коллекции всегда выполняется в том же порядке, в котором они добавлялись в нее изначально.
    Слабые хеш-отображения
    Слабое хеш­отображение представляет собой особую разновидность хеш­
    отображения, в которой сборщик мусора не следует по ссылкам от отобра­
    жения к хранящимся в нем ключам. Это значит, ключ и связанное с ним значение исчезнут из отображения, если на данный ключ нет другой ссылки.
    Слабые хеш­отображения используются для решения таких задач, как кэши­
    рование, когда нужно повторно задействовать результат затратной функции в случае повторного вызова функции в отношении того же самого ключа.
    Если ключи и результаты функции хранятся в обычном хеш­отображении, то оно может бесконечно разрастись и никакие ключи никогда не станут мусо­
    ром. Этой проблемы удается избежать с помощью слабого хеш­отображения.
    Как только объект ключа становится недоступным, связанная с ним запись удаляется из такого отображения. Слабые хеш­отображения реализованы

    550 Глава 24 • Углубленное изучение коллекций в Scala в виде обертки положенной в их основу Java­реализации java.util.We- akHashMap
    Совместно используемые отображения
    К совместно используемому отображению могут обращаться сразу несколько потоков. В дополнение к обычным операциям с
    Map это отображение предо­
    ставляет атомарные операции (табл. 24.11).
    Таблица 24.11. Операции в трейте concurrent .Map
    Что
    Что делает
    m.putIfAbsent(k,
    v)
    Добавляет привязку «ключ — значение» k
    –>
    v
    , кроме тех случаев, когда k
    уже определен в m
    m.remove(k,
    v)
    Удаляет запись для ключа k
    , если он в данный момент отображен на значение v
    m.replace(k,
    old,
    new)
    Заменяет значение, связанное с k
    , новым значением new
    , если ранее с ключом было связано значение old m.replace(k,
    v)
    Заменяет значение, связанное с k
    , значением v
    , если ранее этот ключ был связан с каким­либо значением
    Трейт scala.concurrent.Map определяет интерфейс для изменяемых отобра­
    жений с поддержкой конкурентного доступа. Стандартная библиотека пред­
    лагает две реализации этого трейта. Первая — это java.util.concurrent.Con- currentMap из Java, которую можно автоматически превратить в отображение языка Scala, используя стандартные для Java/Scala операции приведения коллекций (эти преобразования будут описаны в разделе 24.16). Вторая реализация,
    TrieMap
    , основана на HAMT без блокировок.
    Изменяемые битовые множества
    Изменяемое битовое множество похоже на неизменяемое, но отличается тем, что может быть изменено на месте. По сравнению с неизменяемым оно работает при обновлениях немного эффективнее, поскольку неизмененные
    Long
    ­значения копировать не нужно. Пример использования выглядит так:
    val bits = scala.collection.mutable.BitSet.empty bits += 1 // BitSet(1)
    bits += 3 // BitSet(1, 3)
    bits // BitSet(1, 3)

    24 .9 . Массивы 551
    24 .9 . Массивы
    Массивы в Scala — особая разновидность коллекции. С одной стороны, мас­
    сивы Scala в точности соответствуют массивам Java. То есть Scala­массив
    Ar ray[Int]
    представлен как Java­массив int[]
    ,
    Array[Double]
    — как double[]
    , а
    Ar ray[String]
    — как
    String[]
    . Но вместе с тем массивы Scala предостав­
    ляют гораздо больше, чем их Java­аналоги. Во­первых, массивы Scala могут быть обобщенными. То есть можно воспользоваться массивом
    Array[T]
    , где
    T
    является параметром типа или абстрактным типом. Во­вторых, массивы
    Scala совместимы со Scala­последовательностями, то есть туда, где требу­
    ется
    Seq[T]
    , можно передавать
    Array[T]
    . И наконец, массивы Scala также поддерживают все операции с последовательностями. Приведем несколько практических примеров:
    val a1 = Array(1, 2, 3)
    val a2 = a1.map(_ * 3) // Array(3, 6, 9)
    val a3 = a2.filter(_ % 2 != 0) // Array(3, 9)
    a3.reverse // Array(9, 3)
    Если учесть, что массивы Scala представлены точно так же, как массивы Java, то как эти дополнительные свойства могут поддерживаться в Scala?
    Ответ заключается в систематическом использовании неявных преобразова­
    ний. Массив не может претендовать на то, чтобы быть последовательностью, поскольку тип данных, представляющих настоящий массив, не является под­
    типом типа
    Seq
    . Вместо этого там, где массив будет использоваться в качестве последовательности
    Seq
    , он будет неявно обернут в подкласс класса
    Seq
    . Имя этого подкласса — scala.collection.mutable.ArraySeq
    . Вот как это работает:
    val seq: collection.Seq[Int] = a1 // ArraySeq(1, 2, 3)
    val a4: Array[Int] = seq.toArray // Array(1, 2, 3)
    a1 eq a4 // false
    Сеанс работы с интерпретатором показывает, что массивы совместимы с последовательностями благодаря неявному преобразованию из
    Array в
    ArraySeq
    . Преобразование в обратном направлении, из
    ArraySeq в
    Array
    , можно выполнить с использованием метода toArray
    , который определен в классе
    Iterable
    . В последней строке приведенного ранее диалога с интер­
    претатором показано, что заключенный в оболочку массив при его изъятии оттуда с помощью метода toArray возвращает копию исходного массива.
    Есть еще одно неявное преобразование, применимое к массивам. Оно просто
    «добавляет» к массивам все методы, применимые к последовательностям, но сами массивы в последовательности не превращает. «Добавляет» означает,

    552 Глава 24 • Углубленное изучение коллекций что массив заворачивается в другой объект типа
    ArrayOps
    , который поддер­
    живает все методы работы с последовательностями. Обычно этот
    ArrayOps
    ­
    объект существует весьма непродолжительное время — он становится недо­
    ступен после вызова метода для работы с последовательностью, и его место хранения может быть использовано повторно. Современные виртуальные машины зачастую вообще избегают создания таких объектов.
    Разница между этими двумя неявными преобразованиями массивов пока­
    зана в следующем примере:
    val seq: collection.Seq[Int] = a1 // ArraySeq(1, 2, 3)
    seq.reverse // ArraySeq(3, 2, 1)
    val ops: collection.ArrayOps[Int] = a1 // Array(1, 2, 3)
    ops.reverse // Array(3, 2, 1)
    Как видите, при вызове метода reverse в отношении объекта seq типа
    ArraySeq опять будет получен объект типа
    ArraySeq
    . Это не противоречит здравому смыслу, поскольку массивы
    ArraySeq
    , заключенные в оболочку, относятся к типам
    Seq
    , а вызов метода reverse в отношении любого
    Seq
    ­
    объекта вновь даст
    Seq
    ­объект. В то же время вызов метода reverse в от­
    ношении значения ops класса
    ArrayOps приведет к возвращению значения типа
    Array
    , а не
    Seq
    Приведенный ранее пример с
    ArrayOps был надуманным, предназначенным лишь для демонстрации разницы с
    ArraySeq
    . В обычной ситуации вы бы не стали определять значение класса
    ArrayOps
    , а просто вызвали бы в отноше­
    нии массива метод из класса
    Seq
    :
    a1.reverse // Array(3, 2, 1)
    Объект
    ArrayOps вставляется автоматически в ходе неявного преобразова­
    ния. Следовательно, показанная выше строка кода эквивалентна следующей строке, где метод intArrayOps является преобразованием, которое неявно вставлялось в предыдущем примере:
    intArrayOps(a1).reverse // Array(3, 2, 1)
    Возникает вопрос, касающийся способа выбора компилятором intArrayOps в показанной выше строке среди других неявных преобразований в
    ArraySeq
    Ведь оба преобразования отображают массив на тип, поддерживающий метод reverse
    , указанный во введенном коде. Ответ на данный вопрос — уровни приоритета этих двух преобразований. У преобразования
    ArrayOps более высокий приоритет, чем у
    ArraySeq
    . Первое преобразование определено в объекте
    Predef
    , а второе — в классе scala.LowPriorityImplicits
    , явля­
    ющемся суперклассом для
    Predef
    . Неявные преобразования в подклассах

    24 .9 . Массивы 553
    и подобъектах имеют приоритет над неявными преобразованиями в базовых классах. Следовательно, если применимы оба преобразования, то будет вы­
    брано то, которое определено в
    Predef
    . Очень похожая схема, рассмотренная в разделе 21.7, работает для строк.
    Теперь вы знаете, что массивы совместимы с последовательностями и могут поддерживать все операции, применяемые к последовательностям. А как на­
    счет обобщенности? В Java вы не можете воспользоваться записью
    T[]
    , где
    T
    представляет собой параметр типа. А как же тогда представлен имеющийся в Scala тип
    Array[T]
    ? Фактически такой обобщенный массив, как
    Array[T]
    , может во время выполнения программы стать любым из восьми примитив­
    ных типов массивов Java: byte[]
    , short[]
    , char[]
    , int[]
    , long[]
    , float[]
    , double[]
    , boolean[]
    — или же стать массивом объектов. Единственный об­
    щий тип, охва тывающий во время выполнения программы все эти типы, —
    AnyRef
    (или его аналог java.lang.Object
    ), следовательно, это именно тот тип, на который компилятор Scala отображает
    Array[T]
    . Во время выполнения программы, когда происходит обращение к элементу массива типа
    Array[T]
    или обновление этого элемента, производится ряд проверок на соответствие типам. Благодаря этому определяется действительный тип массива, а затем выполняется корректная операция уже над Java­массивом. Проверки на соответствие типам несколько замедляют операции над массивами. Можно ожидать, что обращения к обобщенным массивам будут в три­четыре раза медленнее обращений к простым массивам или массивам объектов. Сле­
    довательно, если требуется максимально высокая производительность, то предпочтение следует отдавать конкретным, а не обобщенным массивам.
    Но одного представления типа обобщенного массива недостаточно, дол­
    жен существовать также способ создания обобщенных массивов. А это еще более сложная задача, которая требует от вас оказать некоторую помощь.
    В качестве примера рассмотрим попытку создания метода, работающего с обобщениями и создающего массив:
    // Неправильно!
    def evenElems[T](xs: Vector[T]): Array[T] =
    val arr = new Array[T]((xs.length + 1) / 2)
    for i <- 0 until xs.length by 2 do arr(i / 2) = xs(i)
    arr
    Метод evenElems возвращает новый массив, состоящий из тех элементов используемого в качестве аргумента вектора xs
    , которые находятся в нем на четных позициях. В первой строке тела метода evenElems создается получа­
    емый в результате массив, имеющий тот же тип элементов, что и аргумент.
    Следовательно, в зависимости от фактического параметра типа для
    T
    это

    554 Глава 24 • Углубленное изучение коллекций может быть
    Array[Int]
    , или
    Array[Boolean]
    , или массив из других прими­
    тивных типов Java, или же массив какого­нибудь ссылочного типа. Но все эти типы имеют во время выполнения программы различные представления, поэтому возникает вопрос: как среда выполнения Scala собирается выбирать из них нужное? По сути, сделать это, основываясь на имеющейся инфор­
    мации, она не может, поскольку фактический тип, который соответствует параметру типа
    T
    , во время выполнения кода затирается. Поэтому при по­
    пытке скомпилировать показанный ранее код будет получено следующее сообщение об ошибке:
    2 | val arr = new Array[T]((xs.length + 1) / 2)
    | ˆ
    | No ClassTag available for T
    Здесь вам следует помочь компилятору, предоставив подсказку времени выполнения о том, какой параметр типа у evenElems
    . Эта подсказка прини­
    мает форму тега класса типа scala.reflect.ClassTag
    . Тег класса описывает заданный исполняемый класс, предоставляя исчерпывающую информацию о нем конструктору массива.
    Во многих случаях компилятор может создавать тег класса самостоятель­
    но. Именно так обстоит дело с конкретными типами наподобие
    Int или
    String
    . То же распространяется и на некоторые обобщенные типы наподо­
    бие
    List[T]
    , где для выстраивания предположения об исполняемом классе информации вполне достаточно; в данном примере исполняемым классом будет
    List
    Для полностью обобщенных случаев обычно практикуется передача тега класса с помощью контекстного ограничителя, рассмотренного в разде­
    ле 23.2. А вот как, используя этот ограничитель, можно исправить показанное ранее определение:
    // Этот код работает import scala.reflect.ClassTag def evenElems[T: ClassTag](xs: Vector[T]): Array[T] =
    val arr = new Array[T]((xs.length + 1) / 2)
    for i <- 0 until xs.length by 2 do arr(i / 2) = xs(i)
    arr
    В этом новом определении компилятор при создании
    Array[T]
    ищет тег класса для параметра типа
    T
    , то есть будет искать неявное значение типа
    ClassTag[T]
    . Если такое значение будет найдено, то тег класса будет исполь­
    зован для создания массива нужного вида. В противном случае вы увидете сообщение об ошибке, похожее на показанное ранее.

    24 .10 . Строки 555
    Вот как выглядит диалог с интерпретатором, в котором используется метод evenElems
    :
    evenElems(Vector(1, 2, 3, 4, 5)) // Array(1, 3, 5)
    evenElems(Vector("this", "is", "a", "test", "run")) // Array(this, a, run)
    В обоих случаях компилятор Scala автоматически создает тег класса для типа элемента (сначала
    Int
    , потом
    String
    ) и передает его неявному параметру мето­
    да evenElems
    . Компилятор может сделать то же самое для всех конкретных ти­
    пов, но не способен на это, если сам аргумент является еще одним параметром типа без признака класса. Например, следующий код не пройдет компиляцию:
    scala> def wrap[U](xs: Vector[U]) = evenElems(xs)
    1 |def wrap[U](xs: Vector[U]) = evenElems(xs)
    | ˆ
    | No ClassTag available for U
    Здесь метод evenElems получает тег класса для параметра типа
    U
    , но ничего не находит. Разумеется, решение в данном случае — потребовать еще один неяв­
    ный тег класса для
    U
    . Код, представленный ниже, уже пройдет компиляцию:
    def wrap[U: ClassTag](xs: Vector[U]) = evenElems(xs)
    Этот пример показывает также, что контекстное ограничение в определе­
    нии
    U
    — краткая форма неявного параметра, названного здесь evidence$1
    и имеющего тип
    ClassTag[U]
    24 .10 . Строки
    Как и массивы, строки не являются последовательностями в прямом смысле слова, но могут быть в них преобразованы и вдобавок поддерживают все операции с последовательностями. Ниже приводятся примеры операций, которые могут вызываться в отношении строк:
    val str = "hello"
    str.reverse // olleh str.map(_.toUpper) // HELLO
    str.drop(3) // lo str.slice(1, 4) // ell val s: Seq[Char] = str // hello
    Эти операции поддерживаются двумя неявными преобразованиями, рассмо­
    тренными в разделе 23.5. Первое, имеющее более низкий уровень приорите­
    та, отображает класс
    String на класс
    WrappedString
    , являющийся подклассом

    556 Глава 24 • Углубленное изучение коллекций immutable.IndexedSeq
    . Это преобразование было применено в последней строке предыдущего примера, в котором строка была преобразована в значе­
    ние типа
    Seq
    . Другое преобразование, с более высоким уровнем приоритета, отображает строку на объект
    StringOps
    , который добавляет к строкам все методы, применяемые к неизменяемым последовательностям. В предыду­
    щем примере это преобразование было неявно вставлено в вызовы методов reverse
    , map
    , drop и slice
    24 .11 . Характеристики производительности
    Как показали все предыдущие разъяснения, разным типам коллекций свойственны различные характеристики производительности. Именно это обстоя тельство становится главной причиной выбора конкретного типа коллекции из множества других типов. Характеристики производительности некоторых наиболее востребованных операций над коллекциями сведены в табл. 24.12 и 24.13.
    1   ...   54   55   56   57   58   59   60   61   ...   64


    написать администратору сайта