Gdy już wiemy jak tworzyć wygodnie obiekty reprezentujące dane musimy jeszcze poznać dostępne w Scali sposoby przechowywania obiektów. Do dyspozycji mamy całą gamę różnych klas kontenerowych, przyjrzymy się im dokładniej, bo mają one ciekawe właściwości, a poza tym z praktycznego punktu widzenia są bardzo istotne. Listing 4 zawiera przykłady użycia różnych pojemników na obiekty.
Listing 4. Przykład użycia w Scali tablic i klas kontenerowych
1 object Collections extends Application { 2 //tablice 3 println("* * * tablice * * *") 4 val tablica: Array[String] = new Array[String](3) 5 tablica(0) = "Ala" 6 tablica(1) = "ma" 7 tablica(2) = "kota" 8 9 //dostęp swobodny do elementów tablicy 10 print(tablica(0) + " " + tablica(1) + " " + tablica(2)) 11 12 //for 1: stara, dobra pętla for w wersji Scala 13 for(i <- 0 until 2) 14 yield print(tablica(i) + " ") 15 println 16 17 //for 2: wersja nieco uproszczona 18 for(s <- tablica) 19 yield print(s + " ") 20 println 21 22 //foreach: typowo funkcyjne podejście 23 tablica.foreach(s => print(s + " ")) 24 println 25 26 //listy 27 println("* * * listy * * *") 28 val lista1 = "ala"::"ma"::"kota"::Nil 29 val lista2 = List("miś", "ma", "uszy") 30 val lista = lista1:::lista2 31 lista.filter(s => s.length <= 3).foreach(println) 32 33 //tuple 34 println("* * * tuple * * *") 35 import java.util.Date 36 val osoba = ("Miś", "Uszatek", new Date(), 120) 37 println("imię: " + osoba._1 + ", data dodania do bazy: " 38 + osoba._3 + ", wzrost: " + osoba._4 + "cm") 39 40 //set 41 println("* * * zbiory * * *") 42 import collection.mutable.HashSet 43 val zbior = HashSet[String]("ala","ma") 44 zbior += "kota"; zbior += "ma" 45 zbior.foreach(s => print(s + " ")) 46 println 47 48 //mapa 49 println("* * * mapy * * *") 50 import scala.collection.mutable.HashMap 51 val mapa = HashMap[Int, String](1->"ala") 52 mapa(2) = "ma" 53 mapa+= 3->"kota" 54 mapa.values.foreach(s => print(s + " ")) 55 }
Pierwszą rzeczą, o której trzeba pamiętać, to fakt, że zarówno tablice jak i klasy kontenerowe przechowują obiekty o określonym typie.
Tablice mają zawsze ustaloną wielkość. Dostęp do obiektów w tablicy można zrealizować na kilka sposobów. Pierwszy przykład to prosty dostęp sekwencyjny (Listing 4, linia 10), następnie wykorzystujemy dwie odmiany pętli for, która zgodnie z funkcyjnym charakterem Scali ma nietypową składnię, barwnie zwaną po angielsku For-Comprehensions (linie 13 i 18). Najciekawszy jest ostatni przykład, użycie metody foreach, która jako argument przyjmuje inną funkcję (linia 23). W naszym przypadku jest to tylko print, ale może tam umieścić dowolnie złożoną funkcję, także anonimową, czyli nie mającą nazwy, zdefiniowaną bezpośrednio wewnątrz foreach.
Podobnym do tablicy kontenerem jest lista. Różni się ona od tablicy dwoma bardzo ważnymi cechami: lista nie ma a priori ustalonej wielkości i jest obiektem niezmiennym. Jeśli modyfikujemy listę dodając do niej nowy element, to tak naprawdę tworzymy nowy obiekt reprezentujący powiększoną listę. Poruszanie się poprzez elementy listy jest realizowane podobnie jak dla tablicy, przy pomocy for lub metody foreach. Listing 4 demonstruje także bardzo przydatną metodę, filter, która zwraca listę obiektów spełniających umieszczony w jej argumencie warunek logiczny (linia 31).
Następnym kontenerem jest tupla. Tupla jest bardzo użyteczna, gdyż potrafi przechowywać obiekty różnych typów. Jest to wygodne na przykład wtedy, gdy jakaś metoda musi zwrócić kilka wartości. Programista Java od razu utworzyłby do tego celu klasę JavaBean, tutaj widzimy jak prosto można zbudować tuplę przechowującą dane o osobie (linia 36).
Żadna przyzwoita biblioteka kontenerów nie mogłaby się obejść bez zbioru (Set) i mapy (Map), zwanej czasem tablicą asocjacyjną. Zbiór się charakteryzuje tym, że nie może zawierać duplikatów, ponadto nie zachowuje kolejności dodawania obiektów. Mapa z kolei przechowuje nie pojedyncze obiekty, ale pary klucz – wartość. Oba te kontenery mają dwie wersję – niezmienną oraz modyfikowalną. Nazwy klas w obu przypadkach są identyczne, różni się tylko nazwa pakietu.
Operacje na zbiorze i mapie można wykonywać bardzo wygodnie dzięki przeciążonym operatorom +, -, +=, itp. które pozwalają dodawać lub usuwać obiekty.
Kolekcje niezmienne oraz możliwość przekazywania jako argumentów metod innych metod podkreślają funkcyjną naturę Scali. Programista Javy, C# czy innego języka typowo imperatywnego może odczuwać na początku pewien dyskomfort związany z używaniem zachowujących się na sposób funkcyjny metod foreach czy filter. Przy odrobinie wprawy można przekonać się jednak, że paradygmat programowania funkcyjnego ma swoje zalety i ułatwia pisanie kodu.
Twórcy Scali najwyraźniej wzięli sobie do serca dziesiąte prawo Philipa Greenspuna mówiące, że każdy dostatecznie złożony program w C, Fortranie, itp. zawiera byle jak zaprojektowaną, pełną błędów, wolno działającą implementację fragmentów języka Lisp (lub innego języka funkcyjnego) i postanowili odpowiedni fragment Lispa zaimplementować możliwie poprawnie.