Chapter 3. Working with Arrays

Topics in This Chapter A1

In this chapter, you will learn how to work with arrays in Scala. Java, JavaScript, Python, and C++ programmers usually choose an array or its close relation (such as lists or vectors) when they need to collect a bunch of elements. In Scala, there are other choices (see Chapter 13), but for now, I’ll assume you are impatient and just want to get going with arrays.

Key points of this chapter:

  • Use an Array if the length is fixed, and an ArrayBuffer if the length can vary.

  • Use () to access elements.

  • Use for elem <- arr do ... to traverse the elements.

  • Use for elem <- arr yield ... to transform into a new array.

  • Scala and Java arrays are interoperable; with ArrayBuffer, use scala.jdk.CollectionConverters.

3.1 Fixed-Length Arrays

If you need an array whose length doesn’t change, use the Array type in Scala. For example,

val strings = Array("Hello", "World")
  // An Array[String] of length 2—the type is inferred
val moreStrings = Array.ofDim[String](5)
  // A string array with five elements, all initialized with null
val nums = Array.ofDim[Int](10)
  // An array of ten integers, all initialized with zero

Images Note

In Java, arrays are created with the new operator. However, in Scala 3, the use of the new operator is discouraged, as you will see in Chapter 5. It is possible to call new Array[Int](10) to create an array of ten integers. But it is also a bit confusing since Array[Int](10) (without the new) is an array of length 1, containing the integer 10.

Use the () operator to access or modify the elements of an array:

strings(0) = "Goodbye"
  // strings is now Array("Goodbye", "World")

Why not [] as in Java, JavaScript, C++, or Python? Scala takes the point of view that an array is like a function that maps the index values to the elements.

Inside the JVM, a Scala Array is implemented as a Java array. The arrays in the preceding example have the type java.lang.String[] inside the JVM. An array of Int, Double, or another equivalent of the Java primitive types is a primitive type array. For example, Array(2,3,5,7,11) is an int[] in the JVM.

3.2 Variable-Length Arrays: Array Buffers

Java has ArrayList, Python has list, and C++ has vector for arrays that grow and shrink on demand. The equivalent in Scala is the ArrayBuffer.

import scala.collection.mutable.ArrayBuffer
val b = ArrayBuffer[Int]()
  // An empty array buffer, ready to hold integers
b += 1
  // ArrayBuffer(1)
  // Add an element at the end with +=
b ++= Array(1, 2, 3, 5, 8)
  // ArrayBuffer(1, 1, 2, 3, 5, 8)
  // You can append any collection with the ++= operator
b.dropRightInPlace(3)
  // ArrayBuffer(1, 1, 2)
  // Removes the last three elements

Adding or removing elements at the end of an array buffer is an efficient (“amortized constant time”) operation.

You can also insert and remove elements at an arbitrary location, but those operations are not as efficient—all elements after that location must be shifted. For example:

b.insert(2, 6)
  // ArrayBuffer(1, 1, 6, 2)
  // Inserts before index 2
b.insertAll(2, Array(7, 8, 9))
  // ArrayBuffer(1, 1, 7, 8, 9, 6, 2)
  // Inserts the elements from another collection
b.remove(2)
  // ArrayBuffer(1, 1, 8, 9, 6, 2)
b.remove(2, 3)
  // ArrayBuffer(1, 1, 2)
  // The second parameter tells how many elements to remove

Sometimes, you want to build up an Array, but you don’t yet know how many elements you will need. In that case, first make an array buffer, then call

b.toArray
  // Array(1, 1, 2)

Conversely, call a.toBuffer to convert the array a to an array buffer.

3.3 Traversing Arrays and Array Buffers

In Java and C++, there are several syntactical differences between arrays and array lists/vectors. Scala is much more uniform. Most of the time, you can use the same code for both.

Here is how you traverse an array or array buffer with a for loop:

for i <- 0 until a.length do
  println(s"$i: ${a(i)}")

The until method is similar to the to method, except that it excludes the last value. Therefore, the variable i goes from 0 to a.length - 1.

In general, the construct

for i <- range do

makes the variable i traverse all values of the range. In our case, the loop variable i assumes the values 0, 1, and so on until (but not including) a.length.

To visit every second element, let i traverse

0 until a.length by 2
  // Range(0, 2, 4, ...)

To visit the elements starting from the end of the array, traverse

a.length -1 to 0 by -1
  // Range(..., 2, 1, 0)

Images Tip

Instead of 0 until a.length or a.length -1 to 0 by -1, you can use a.indices or a.indices.reverse.

If you don’t need the array index in the loop body, visit the array elements directly:

for elem <- a do
  println(elem)

This is similar to the “enhanced” for loop in Java, the “for in” loop in JavaScript or Python, or the “range-based” for loop in C++. The variable elem is set to a(0), then a(1), and so on.

3.4 Transforming Arrays

In the preceding sections, you saw how to work with arrays just like you would in other programming languages. But in Scala, you can go further. It is easy to take an array (or array buffer) and transform it in some way. Such transformations don’t modify the original array but yield a new one.

Use a for comprehension like this:

val a = Array(2, 3, 5, 7, 11)
val result = for elem <- a yield 2 * elem
  // result is Array(4, 6, 10, 14, 22)

The for/yield loop creates a new collection of the same type as the original collection. If you started with an array, you get another array. If you started with an array buffer, that’s what you get from for/yield.

The result contains the expressions after the yield, one for each iteration of the loop.

Oftentimes, when you traverse a collection, you only want to process the elements that match a particular condition. This is achieved with a guard: an if inside the for. Here we double every even element, dropping the odd ones:

for elem <- a if elem % 2 == 0 yield elem / 2

Keep in mind that the result is a new collection—the original collection is not affected.

Images Note

Some programmers with experience in functional programming prefer map and filter to for/yield and guards:

a.filter(_ % 2 == 0).map(_ / 2)

That’s just a matter of style—the for/yield loop does exactly the same work. Use whichever you find easier.

Suppose we want to remove all negative elements from an array buffer of integers. A traditional sequential solution might traverse the array buffer and remove unwanted elements as they are encountered.

var n = b.length
var i = 0
while i < n do
  if b(i) >= 0 then i += 1
  else
    b.remove(i)
    n -= 1

That’s a bit fussy—you have to remember not to increment i when you remove the element, and to decrement n instead. It is also not efficient to remove elements from the middle of the array buffer. This loop unnecessarily moves elements that will later be removed.

In Scala, the obvious solution is to use a for/yield loop and keep all non-negative elements:

val nonNegative = for elem <- b if elem >= 0 yield elem

The result is a new array buffer. Suppose that we want to modify the original array buffer instead, removing the unwanted elements. Then we can collect their positions:

val positionsToRemove = for i <- b.indices if b(i) < 0 yield i

Now remove the elements at those positions, starting from the back:

for i <- positionsToRemove.reverse do b.remove(i)

Alternatively, remember the positions to keep, copy them over, and then shorten the buffer:

val positionsToKeep = for i <- b.indices if b(i) >= 0 yield i
for j <- positionsToKeep.indices do b(j) = b(positionsToKeep(j))
b.dropRightInPlace(b.length - positionsToKeep.length)

The key observation is that it is better to have all index values together instead of seeing them one by one.

3.5 Common Algorithms

It is often said that a large percentage of business computations are nothing but computing sums and sorting. Fortunately, Scala has built-in functions for these tasks.

Array(1, 7, 2, 9).sum
  // 19
  // Works for ArrayBuffer too

In order to use the sum method, the element type must be a numeric type: either an integral or floating-point type or BigInteger/BigDecimal.

Similarly, the min and max methods yield the smallest and largest element in an array or array buffer.

ArrayBuffer("Mary", "had", "a", "little", "lamb").max
  // "little"

The sorted method sorts an array or array buffer and returns the sorted array or array buffer, without modifying the original:

val b = ArrayBuffer(1, 7, 2, 9)
val bSorted = b.sorted
  // b is unchanged; bSorted is ArrayBuffer(1, 2, 7, 9)

You can also supply a comparison function, but then you should use the sortWith method:

val bDescending = b.sortWith(_ > _) // ArrayBuffer(9, 7, 2, 1)

See Chapter 12 for the function syntax.

You can sort an array or array buffer in place:

val a = Array(1, 7, 2, 9)
a.sortInPlace()
  // a is now Array(1, 2, 7, 9)

For the min, max, and sortInPlace methods, the element type must have a comparison operation. This is the case for numbers, strings, and other types with a “given” Ordering object.

Finally, if you want to display the contents of an array or array buffer, the mkString method lets you specify the separator between elements. A second variant has parameters for the prefix and suffix. For example,

a.mkString(" and ")
  // "1 and 2 and 7 and 9"
a.mkString("<", ",", ">")
  // "<1,2,7,9>"

Contrast with toString:

a.toString
  // "[I@b73e5"
  // This is the useless toString method for arrays, straight from Java
b.toString
  // "ArrayBuffer(1, 7, 2, 9)"

3.6 Deciphering Scaladoc

There are lots of useful methods on arrays and array buffers, and it is a good idea to browse the Scala documentation to get an idea of what’s there.

Since the Array class compiles directly to a Java array, most of the useful array methods are found in the ArrayOps and ArraySeq classes.

Scala has a rich type system, so you may encounter some strange-looking syntax as you browse the Scala documentation. Fortunately, you don’t have to understand all nuances of the type system to do useful work. Use Table 3–1 as a “decoder ring.”

For now, you’ll have to skip methods that use types such as Option or PartialFunction that we haven’t discussed yet.

Table 3–1 Scaladoc Decoder Ring

Scaladoc

Explanation

def count(p: (A) => Boolean): Int

This method takes a predicate, a function from A to Boolean. It counts for how many elements the function is true. For example, a.count(_ > 0) counts how many elements of a are positive.

def insert(@deprecatedName("n", "2.13.0") index: Int, elem: A): Unit

Ignore the @deprecatedName annotation. At one point, the index parameter was called n. You don’t care.

def appendAll(xs: IterableOnce[A]): Unit

The xs parameter can be any collection with the IterableOnce trait, a trait in the Scala collections hierarchy. Other common traits that you may encounter in Scaladoc are Iterable and Seq. All Scala collections implement these traits, and the difference between them is academic for library users. Simply think “any collection” when you see one of these.

def combinations(n: Int): Iterator[ArrayBuffer[A]]

This method computes a potentially large result and therefore returns it as an iterator instead of a collection, so that you can visit the elements one by one instead of placing them in a collection. For now, just call toArray or toBuffer on the result.

def copyToArray[B >: A] (xs: Array[B]): Unit

Note that the function copies an ArrayBuffer[A] into an Array[B]. Here, B is allowed to be a supertype of A. For example, you can copy from an ArrayBuffer[Int] to an Array[Any].

At first reading, just ignore the [B >: A] and mentally replace B with A.

def sorted[B >: A] (implicit ord: Ordering[B]): ArrayBuffer[A]

The element type A must have a supertype B for which an “implicit” or “given” object of type Ordering[B] exists. Such an ordering exists for numbers and strings, as well as for classes that implement the Java Comparable interface. We will discuss the mechanism in detail in Chapter 19. With other implicit types such as Numeric in sum[B >: A](implicit num: Numeric[B]): B, follow your intuition. To form a sum, there must be some way of adding the values.

def ++[B >: A](xs: Array[_ <: B])(implicit evidence$23: ClassTag[B]): Array[B]

This method concatenates two arrays. The second array can be a subtype of the first. The “implicit” of type ClassTag is a formality that is required to construct the resulting array in the JVM. You can safely ignore any such ClassTag parameters.

def stepper[S <: Stepper[_]](implicit shape: StepperShape[A, S]): S with EfficientSplit

Here, you just have to admit defeat. This method is required for a technical reason that is only of interest to the Scala library implementors.

3.7 Multidimensional Arrays

Like in Java, JavaScript, or Python, multidimensional arrays are implemented as arrays of arrays. For example, a two-dimensional array of Double values has the type Array[Array[Double]]. To construct such an array, use the ofDim method:

val matrix = Array.ofDim[Double](3, 4) // Three rows, four columns

To access an element, use two pairs of parentheses:

matrix(row)(column) = 42

You can make ragged arrays, with varying row lengths:

val triangle = Array.ofDim[Array[Int]](10)
for i <- triangle.indices do
  triangle(i) = Array.ofDim[Int](i + 1)

3.8 Interoperating with Java

Since Scala arrays are implemented as Java arrays, you can pass them back and forth between Java and Scala.

This works in almost all cases, except if the array element type isn’t an exact match. In Java, an array of a given type is automatically converted to an array of a supertype. For example, a Java String[] array can be passed to a method that expects a Java Object[] array. Scala does not permit this automatic conversion because it is unsafe. (See Chapter 17 for a detailed explanation.)

Suppose you want to invoke a Java method with an Object[] parameter, such as java.util.Arrays.binarySearch(Object[] a, Object key):

val a = Array("Mary", "a", "had", "lamb", "little")
java.util.Arrays.binarySearch(a, "beef") // Does not work

This does not work because Scala will not convert an Array[String] into an Array[Object]. You can force the conversion like this:

java.util.Arrays.binarySearch(a.asInstanceOf[Array[Object]], "beef")

Images Note

This is just an example to show how to overcome element type differences. If you want to carry out binary search in Scala, do it like this:

import scala.collection.Searching.*
val result = a.search("beef")

The result is Found(n) if the element was found at position n or InsertionPoint(n) if the element was not found but should be inserted before position n.

If you call a Java method that receives or returns a java.util.List, you could, of course, use a Java ArrayList in your Scala code—but that is unattractive. Instead, import the conversion methods in scala.jdk.CollectionConverters. Then you can call the asJava method to convert any sequence (such as a Scala buffer) into a Java list.

For example, the java.lang.ProcessBuilder class has a constructor with a List<String> parameter. Here is how you can call it from Scala:

import scala.jdk.CollectionConverters.*
import scala.collection.mutable.ArrayBuffer
val command = ArrayBuffer("ls", "-al", "/usr/bin")
val pb = ProcessBuilder(command.asJava) // Scala to Java

The Scala buffer is wrapped into an object of a Java class that implements the java.util.List interface.

Conversely, when a Java method returns a java.util.List, you can convert it into a Buffer:

import scala.jdk.CollectionConverters.*
import scala.collection.mutable.Buffer
 val cmd: Buffer[String] = pb.command().asScala // Java to Scala
  // You can’t use ArrayBuffer—the wrapped object is only guaranteed to be a Buffer

If the Java method returns a wrapped Scala buffer, then the implicit conversion unwraps the original object. In our example, cmd == command.

Exercises

1. Write a code snippet that sets a to an array of n random integers between 0 (inclusive) and n (exclusive).

2. Write a loop that swaps adjacent elements of an array of integers. For example, Array(1, 2, 3, 4, 5) becomes Array(2, 1, 4, 3, 5).

3. Repeat the preceding assignment, but produce a new array with the swapped values. Use for/yield.

4. Given an array of integers, produce a new array that contains all positive values of the original array, in their original order, followed by all values that are zero or negative, in their original order.

5. How do you compute the average of an Array[Double]?

6. How do you rearrange the elements of an Array[Int] so that they appear in reverse sorted order? How do you do the same with an ArrayBuffer[Int]?

7. Write a code snippet that produces all values from an array with duplicates removed. (Hint: Look at Scaladoc.)

8. Suppose you are given an array buffer of integers and want to remove all but the first negative number. Here is a sequential solution that sets a flag when the first negative number is called, then removes all elements beyond.

var first = true
var n = a.length
var i = 0
while i < n do
  if a(i) >= 0 then i += 1
  else
    if first then
      first = false
      i += 1
    else
      a.remove(i)
      n -= 1

This is a complex and inefficient solution. Rewrite it in Scala by collecting positions of the negative elements, dropping the first element, reversing the sequence, and calling a.remove(i) for each index.

9. Improve the solution of the preceding exercise by collecting the positions that should be moved and their target positions. Make those moves and truncate the buffer. Don’t copy any elements before the first unwanted element.

10. Make a collection of all time zones returned by java.util.TimeZone.getAvailableIDs that are in America. Strip off the "America/" prefix and sort the result.

11. Import java.awt.datatransfer.* and make an object of type SystemFlavorMap with the call

val flavors = SystemFlavorMap.getDefaultFlavorMap().asInstanceOf[SystemFlavorMap]

Then call the getNativesForFlavor method with parameter DataFlavor.imageFlavor and get the return value as a Scala buffer. (Why this obscure class? It’s hard to find uses of java.util.List in the standard Java library.)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.50.206