In this line:
val animals = ArrayBuffer.empty[Any]
// ↑↑↑
You explicitly tell Scala that your animals
ArrayBuffer
can contain Any
object. As a result, you can only use methods of Any
which are not that many.
It is, for example, perfectly legal to put an Int
into animals
, since you told Scala that the contents of animals
can be anything. However, Int
doesn't have a name
method, and thus your code would blow up at runtime if you tried to get the name of an Int
. In order to prevent your code from blowing up at runtime, Scala rather decides to not allow you to write that code at all in the first place.
So, you need to tell Scala what kinds of things you want to put into the animals
. Unfortunately, we only have two explicit nominal types at our disposal here: Dog
and Cat
. However, when you type animals
as ArrayBuffer[Dog]
, then you can't put Sally in there, and if you type it as ArrayBuffer[Cat]
, then you can't put Harry in there.
So, we need to type animals
as something that allows both Dog
s and Cat
s.
In Scala 3, you could use a Union Type:
val animals = ArrayBuffer.empty[Dog | Cat]
Alas, Scala 3 is still quite far away.
Another way would be to use a Compound type with structural refinement:
val animals = ArrayBuffer.empty[{val name: String}]
This allows your code to work, but it may not necessarily do what you want: this allows any object that has a val
named name
of type String
, not just Dog
s and Cat
s. In particular, you could put something in animals
which is not an animal, as long as it has a name.
The best way would probably be to introduce an abstract supertype (let's call it Pet
) that defines an abstract val name
that gets overridden by Cat
and Dog
, and then type animals
as ArrayBuffer[Pet]
:
trait Pet {
val name: String
}
class Dog(val name: String) extends Pet
class Cat(val name: String) extends Pet
val animals = ArrayBuffer.empty[Pet]