¿Por qué ConcurrentHashMap evita claves y valores nulos?

141

El JavaDoc de ConcurrentHashMapdice esto:

Me gusta Hashtablepero HashMapno me gusta , esta clase no permite nullser utilizada como clave o valor.

Mi pregunta: ¿por qué?

2da pregunta: ¿Por qué no Hashtablepermite nulo?

He usado muchos HashMaps para almacenar datos. Pero al cambiar a ConcurrentHashMapMe metí en problemas varias veces debido a NullPointerExceptions.

Marcel
fuente
1
Creo que es una inconsistencia extremadamente molesta. EnumMap tampoco permite nulo. Obviamente, no hay limitación técnica que no permita claves nulas. para un Mapa <K, V>, simplemente un campo de tipo V proporcionará soporte para claves nulas (probablemente otro campo booleano si desea diferenciar entre valor nulo y ningún valor).
RAYO
66
Una mejor pregunta es "¿por qué HashMap permite una clave nula y valores nulos?". O posiblemente, "¿por qué Java permite que nulo habitara en todos los tipos?", O incluso "¿por qué Java tiene nulos en absoluto?".
Jed Wesley-Smith

Respuestas:

220

Del autor de ConcurrentHashMapsí mismo (Doug Lea) :

La razón principal por la que no se permiten nulos en ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) es que las ambigüedades que pueden ser apenas tolerables en mapas no concurrentes no se pueden acomodar. La principal es que si map.get(key)regresa null, no puede detectar si la clave se asigna explícitamente a nullla clave no está asignada. En un mapa no concurrente, puede verificar esto a través de map.contains(key), pero en uno concurrente, el mapa puede haber cambiado entre llamadas.

Bruno
fuente
77
Gracias, pero ¿qué pasa con tener nulo como clave?
AmitW
2
por qué no usar Optionals como valores internamente
benez
2
@benez Optionales una característica de Java 8, que no estaba disponible en ese entonces (Java 5). Podrías usar Optionals ahora, de hecho.
Bruno
@AmitW, creo que el ans es igual, es decir, ambigüedades. Por ejemplo, suponga que un hilo hace que una clave sea nula y almacena un valor en su contra. Luego, otro hilo cambió otra clave a nulo. Cuando el segundo hilo intente agregar un nuevo valor, será reemplazado. Si el segundo subproceso intenta obtener el valor, obtendrá el valor para una clave diferente, la modificada por la primera. Tal situación debe ser evitada.
Dexter
44

Creo que es, al menos en parte, para que pueda combinar containsKeyy geten una sola llamada. Si el mapa puede contener nulos, no hay forma de saber si getestá devolviendo un valor nulo porque no había una clave para ese valor, o simplemente porque el valor era nulo.

¿Por que eso es un problema? Porque no hay una forma segura de hacerlo tú mismo. Toma el siguiente código:

if (m.containsKey(k)) {
   return m.get(k);
} else {
   throw new KeyNotPresentException();
}

Como mes un mapa concurrente, la clave k puede eliminarse entre las llamadas containsKeyy get, lo que hace que este fragmento devuelva un valor nulo que nunca estuvo en la tabla, en lugar de lo deseado KeyNotPresentException.

Normalmente lo resolverías sincronizando, pero con un mapa concurrente que, por supuesto, no funcionará. Por lo tanto, la firma de gettuvo que cambiar, y la única forma de hacerlo de una manera compatible con versiones anteriores era evitar que el usuario inserte valores nulos en primer lugar, y continuar usándolo como un marcador de posición para "clave no encontrada".

Alice Purcell
fuente
Puedes hacer map.getOrDefault(key, NULL_MARKER). Si es así null, el valor era null. Si vuelve NULL_MARKER, el valor no estaba presente.
Oliv
@Oliv Solo a partir de Java 8. Además, puede que no haya un marcador nulo sensible para ese tipo.
Alice Purcell
@AlicePurcell, "pero con un mapa concurrente que, por supuesto, no funcionará", por eso, puedo sincronizar de manera similar en la versión concurrente, por lo que me pregunto por qué no funcionará. ¿Puedes elaborar esto?
Samshers
@samshers No hay operaciones en un mapa simultáneo sincronizado, por lo que necesitaría sincronizar externamente todas las llamadas, en ese momento no solo ha perdido todos los beneficios de rendimiento de tener un mapa concurrente, sino que ha dejado una trampa para los futuros mantenedores que naturalmente esperaría poder acceder de manera segura a un mapa concurrente sin sincronizar.
Alice Purcell
@AlicePurcell, genial. Aunque técnicamente es posible, definitivamente será una pesadilla de mantenimiento y los usuarios posteriores no esperarán que tengan que sincronizarse en la versión concurrente.
samshers
4

Josh Bloch diseñado HashMap; Doug Lea diseñado ConcurrentHashMap. Espero que eso no sea calumnioso. En realidad, creo que el problema es que los nulos a menudo requieren envoltura para que el nulo real pueda significar sin inicializar. Si el código del cliente requiere valores nulos, entonces puede pagar el costo (ciertamente pequeño) de envolver los valores nulos.

Tom Hawtin - tackline
fuente
2

No se puede sincronizar en un nulo.

Editar: Esto no es exactamente por qué en este caso. Inicialmente pensé que estaba ocurriendo algo interesante al bloquear cosas contra actualizaciones concurrentes o usar el monitor de objetos para detectar si algo se modificó, pero al examinar el código fuente parece que estaba equivocado: se bloquean usando un "segmento" basado en un máscara de bits del hash.

En ese caso, sospecho que lo hicieron para copiar Hashtable, y sospecho que Hashtable lo hizo porque en el mundo de la base de datos relacional, null! = Null, por lo que usar un nulo como clave no tiene sentido.

Paul Tomblin
fuente
¿Eh? No se realiza ninguna sincronización en las claves y los valores de un mapa. Eso no tendría ningún sentido.
Tobias Müller
Hay otros tipos de bloqueo realizados. Eso es lo que lo hace "concurrente". Para hacer eso, necesita un Objeto para colgar.
Paul Tomblin
2
¿Por qué no hay un objeto especial internamente que pueda usarse para sincronizar valores nulos? por ejemplo, "Private Object NULL = new Object ();". Creo que he visto esto antes ...
Marcel
¿A qué otro tipo de bloqueo te refieres?
Tobias Müller
En realidad, ahora que miro el código fuente gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/ ... Tengo serias dudas al respecto. Parece que usa bloqueo de segmento, no bloqueo en artículos individuales.
Paul Tomblin
0

ConcurrentHashMap es seguro para subprocesos. Creo que no permitir claves y valores nulos fue parte de garantizar que sea seguro para subprocesos.

Kevin Crowell
fuente
0

Supongo que el siguiente fragmento de la documentación de la API da una buena pista: "Esta clase es totalmente interoperable con Hashtable en programas que dependen de la seguridad de su hilo pero no de sus detalles de sincronización".

Probablemente solo querían hacer que sea ConcurrentHashMaptotalmente compatible / intercambiable Hashtable. Y como Hashtableno permite claves y valores nulos.

Tobias Müller
fuente
2
¿Y por qué Hashtable no admite nulos?
Marcel
Al mirar su código, no veo una razón obvia por la que Hashtable no permita valores nulos. ¿Tal vez fue solo una decisión de API de cuando se creó la clase? HashMap tiene un manejo especial para el caso nulo interno que Hashtable no tiene. (Siempre arroja NullPointerException.)
Tobias Müller
-2

No creo que rechazar el valor nulo sea una opción correcta. En muchos casos, queremos poner una clave con valor nulo en el mapa actual. Sin embargo, al usar ConcurrentHashMap, no podemos hacer eso. Sugiero que la próxima versión de JDK pueda soportar eso.

yinhaomin
fuente
1
¿Has pensado en competir por Javachampion?
BlackBishop
Use Opcional si desea un comportamiento nulo en sus claves.
Alice Purcell