Scala 2.8 mutable.Set null handling
Using scala, 2.8:
import scala.collection.mutable
import mutable.MultiMap
val m = new mutable.HashMap[String, mutable.Set[String]] with MultiMap[String, String]
m.addBinding("key", null)
m exists { _._2 contains null }
prints false
m exists { _._2开发者_StackOverflow isEmpty }
prints false
m("key").size
prints 1
How do I find the first key (or any) key that was added via an addBinding call with a value of null?
In Scala 2.7.7, you get a NullPointer straight away when adding null:
scala> val s = new scala.collection.mutable.HashSet[String]
s: scala.collection.mutable.HashSet[String] = Set()
scala> s += null
java.lang.NullPointerException
at scala.collection.mutable.FlatHashTable$class.elemHashCode(FlatHashTable.scala:144)
at scala.collection.mutable.HashSet.elemHashCode(HashSet.scala:31)
at scala.collection.mutable.FlatHashTable$class.addEntry(FlatHashTable.scala:66)
at scala.collection.mutable.HashSet.addEntry(HashSet.scala:31)
at scala.collection.mutable.HashSet.$plus$eq(HashSet.s...
Using Scala 2.8.0.RC7, that is no longer the case. However, as Randall has observed, the behaviour is not entirely consistent either:
scala> import scala.collection.mutable.HashSet
import scala.collection.mutable.HashSet
scala> val s = new HashSet[String]
s: scala.collection.mutable.HashSet[String] = Set()
scala> s += null
res0: s.type = Set()
scala> s += null
res1: s.type = Set()
scala> s.size
res2: Int = 2
scala> s.head
java.util.NoSuchElementException: next on empty iterator
at scala.collection.Iterator$$anon$3.next(Iterator.scala:29)
at scala.collection.Iterator$$anon$3.next(Iterator.scala:27)
at scala.collection.mutable.FlatHashTable$$anon$1.next(FlatHashTable.scala:176)
at scala.collection.IterableLike$class.head(IterableLike.scala:102)
at scala.collection.mutable.HashSet.head(HashSet.scala:38)
at .(:8)
Most of the work when adding an entry is done in
FlatHashTable
When you add an entery, addEntry
is invoked, which looks like this:
/** The actual hash table.
*/
@transient protected var table: Array[AnyRef] = new Array(initialCapacity)
def addEntry(elem: A) : Boolean = {
var h = index(elemHashCode(elem))
var entry = table(h)
while (null != entry) {
if (entry == elem) return false
h = (h + 1) % table.length
entry = table(h)
}
table(h) = elem.asInstanceOf[AnyRef]
tableSize = tableSize + 1
if (tableSize >= threshold) growTable()
true
}
null.asInstanceOf[AnyRef]
just gives you back null
, and the elemHashCode(elem)
returns zero if the elem is
null
. So in this case, the element with index zero is 'initialized' with null
(which is the value it already had), but the tableSize is increased. This explains the results that are seen in the REPL.
If you call head
on the Set, ultimately the iterator in FlatHashTable
is called, as can be seen from the stacktrace. This is implemented as follows:
iterator = new Iterator[A] {
private var i = 0
def hasNext: Boolean = {
while (i < table.length && (null == table(i))) i += 1
i < table.length
}
def next(): A =
if (hasNext) { i += 1; table(i - 1).asInstanceOf[A] }
else Iterator.empty.next
}
hasNext
will return false, since the element that we stored in the Array is null, therefore, the call to next
will call Iterator.empty.next, which throws an exception.
Since adding null to a HashSet is not handled consistently in Scala 2.8, this indeed looks like a bug. Also, as already has been remarked, it provides another good reason to stay away from using null where possible.
Edit Since I couldn't find a ticket for this, I added it.
Perhaps this is a bug, but it is what I observe in Scala 2.8.RC3:
scala> import scala.collection.mutable.HashSet
import scala.collection.mutable.HashSet
scala> val hs1 = new HashSet[String]
hs1: scala.collection.mutable.HashSet[String] = Set()
scala> hs1 += null
res0: hs1.type = Set()
scala> hs1.size
res1: Int = 1
scala> hs1.contains(null)
res2: Boolean = false
精彩评论