Abhishek Agarwal
Abhishek Agarwal

Reputation: 1248

How to create a Hashmap in Scala with key as String and value as another String or another Hashmap

I want to create a variable in Scala which can have data of the following format:

"one" -> "two",
"three -> {"four" -> "five", "six -> "seven"},
"eight" -> {"nine" -> { "ten" -> "eleven", "twelve" -> "thirteen"},
           "fourteen" -> {"fifteen" -> "sixteen}
           }

I tried creating a Java HashMap using:

var requiredVar = new HashMap[String, Object]()

I am able to do something like :

var hm = new HashMap[String, String]
hm.put("four","five")
requiredVar.put("three",hm)

But if I try to add :

requiredVar.get("three").put("six","seven")

I get the error that

value put is not a member of Object

How can I get this done?

I have tried something like native to Scala as well:

val a = Map("one" -> "two" , "three" -> Map("four"->"five"))
a.get("three").put("six"->"seven")

but get the following error:

error: value put is not a member of Option[Any]

Upvotes: 0

Views: 684

Answers (1)

anqit
anqit

Reputation: 1090

In the first case, when using Java, you get the error because the compiler doesn't know that the value that retrieved from requiredVar is a Map. You declare requiredVar to be a HashMap[String, Object], so the compiler will only know anything retrieved from the map is an Object, nothing more specific.

Specifically, in your case:

requiredVar.get("three").put("six","seven")

requiredVar.get("three") returns an Object, which doesn't have a put() method.

You are running into a similar issue in the Scala version of your code as well. When you create the Map:

val a = Map("one" -> "two" , "three" -> Map("four"->"five"))

the compiler must infer the types of the keys and values, which it is doing by finding the closest common ancestor of all the values, which for a String and another Map, is Any, Scala's equivalent to Java's Object. So when you try to do

a.get("three").put("six"->"seven")

a.get("three") is returning an Option[Any], which doesn't have a put method. By the way, Scala's Map.get returns an Option, so that if the key is not present in the map, a None is returned instead an exception being thrown. You can also use the more concise method a("three"), which returns the value type directly (in this case Any), but will throw an exception if the key is not in the map.

There are a few ways I can think of try to achieve what you want to do.

1) Casting

If you are absolutely sure that the value you are retrieving from the map is another Map instead of a String, you can cast the value:

requiredVar.get("three").asInstanceOf[HashMap[String, String]].put("six","seven")

This is a fairly brittle approach, as if the value is a String, then you will get a runtime exception thrown.

2) Pattern Matching

Rather than casting arbitrarily, you can test the retrieved value for its type, and only call put on values you know are maps:

requiredVar.get("three") match {
  case m: HashMap[String, String] => m.put("six", "seven")
  case _ => // the value is probably a string here, handle this how you'd like
}

This allows you to guard against the case that the value is not a map. However, it is still brittle because the value type is Any, so in the case _ case, you don't actually know the value is a String, and would have to pattern match or cast to know for sure and use the value as a String

3) Create a new value type

Rather than rely on a top type like Object or Any, you can create types of your own to use as the value type. Something like the following could work:

import scala.collection.mutable.Map

sealed trait MyVal 
case class StringVal(s: String) extends MyVal
case class MapVal(m: Map[String, String]) extends MyVal

object MyVal {
  def apply(s: String): StringVal = StringVal(s)
  def apply(m: Map[String, String]): MapVal = MapVal(m)
}

var rv = Map[String, MyVal]()
rv += "one" -> MyVal("two")
rv += "three" -> MyVal(Map[String, String]("four" -> "five"))

rv.get("three") match {
  case Some(v) => v match {
    case MapVal(m) => m("six") = "seven"
    case StringVal(s) =>  // handle the string case as you'd like
  }
  case None => // handle the key not being present in the map here
}

The usage may look similar, but the advantage now is that the pattern match on the rv.get("three") is complete.

4) Union types

If you happen to be using a 3.x version of Scala, you can use a union type to specify exactly what types of values you will have in your map, and achieve something like the above option much less verbosely:

import scala.collection.mutable.Map

val rv: Map[String, String | Map[String, String]] = Map()
rv += "one" -> "two"
rv += "three" -> Map[String, String]("four" -> "five")
rv.get("three") match {
  case Some(m: Map[String, String]) => m += "six" -> "seven"
  case Some(s: String) => // handle string values
  case None => // handle key not present
}

One thing to note though, with all of the above options, is that in Scala, it is preferable to use immutable collections, instead of mutable versions like HashMap or scala.collection.mutable.Map (which is by default a HashMap under the hood). I would do some research about immutable collections and try to think about how you can redesign your code accordingly.

Upvotes: 4

Related Questions