HashMap get/put

W

Wojtek

If I have the following:

HashMap<MyKey,MyValue> map = new HashMap<MyKey,MyValue>();
MyKey myKey = new MyKey();

Then I can put in a value by:

map.put(myKey, new MyValue() );

The compiler enforces the use of these two types.

However to do a get I can do the following and the compiler does not
complain:

map.get(myKey); // this is right
map.get(myKey.toString()); // this is wrong yet legal
map.get(new Long(20)); // this is wrong yet legal

All of these are legal according to the compiler. Why is it that the
compiler does not enforce type checking on the get()?

Or rather, why does the spec not say get(K key) instead of get(Object
o)

http://java.sun.com/javase/6/docs/api/java/util/HashMap.html#get(java.lang.Object)
 
S

Stefan Ram

Wojtek said:
All of these are legal according to the compiler. Why is it that the
compiler does not enforce type checking on the get()?

Setting with a wrong type can result in a Map that breaks
its contract. Therefore, it is forbidden. Getting with a
wrong type cannot do such harm, so, it is not forbidden.
After all, you can meaningfully and correctly detect at run
time that for a wrong key, there is no value in the map.
 
K

Ken

  Setting with a wrong type can result in a Map that breaks
  its contract. Therefore, it is forbidden. Getting with a
  wrong type cannot do such harm, so, it is not forbidden.
  After all, you can meaningfully and correctly detect at run
  time that for a wrong key, there is no value in the map.

I don't understand. If the type is not the same as the type used in
the map there is no chance of success. Clearly this is an error. I'm
of the mind of the OP.
 
S

Stefan Ram

Ken said:
I don't understand. If the type is not the same as the type used in
the map there is no chance of success.

Look at the following example:

public class Main
{ public static void main( final java.lang.String[] args )
{
final java.util.Map<java.lang.String,java.lang.String> value0
= new java.util.HashMap<java.lang.String,java.lang.String>();

final java.util.Map<java.lang.Object,java.lang.Integer> value1
= new java.util.HashMap<java.lang.Object,java.lang.Integer>();

final java.util.Map
<java.util.Map<java.lang.String,java.lang.String>,java.lang.String> map
= new java.util.HashMap
<java.util.Map<java.lang.String,java.lang.String>,java.lang.String>();

map.put( value0, "alpha" );

java.lang.System.out.println( map.get( value1 )); }}

alpha

»value1« has not the type used in the map, yet the »get« succeeds.
 
W

Wojtek

Stefan Ram wrote :
Setting with a wrong type can result in a Map that breaks
its contract. Therefore, it is forbidden. Getting with a
wrong type cannot do such harm, so, it is not forbidden.
After all, you can meaningfully and correctly detect at run
time that for a wrong key, there is no value in the map.

No it is not an error. Just will never retrieve ANY value if you use
the wrong type (see below).

This came about because I changed the key for a HashMap to one that is
more specific (such as String to MyKey). All the "put" statements were
caught. However ALL the get statements were not. I had to manually go
through the code to locate each occurrence and change it.

And of course I missed some and the code broke during runtime. I had
assumed that generics would solve this type of issue.


Caveat: Yes it is possible to get the same hashcode with two different
types, however it would be highly unlikely.
 
S

Stefan Ram

Look at the following example: (...)
final java.util.Map<java.lang.String,java.lang.String> value0
= new java.util.HashMap<java.lang.String,java.lang.String>(); (...)
»value1« has not the type used in the map, yet the »get« succeeds.

Above, both types were the same after type erasure and were
types of empty containers.

Here is a program with types that differ even after type
erasure and non-empty containers.

public class Main
{ public static void main( final java.lang.String[] args )
{
final java.util.ArrayList< java.lang.String >list0
= new java.util.ArrayList< java.lang.String >();
list0.add( "text" );

final java.util.LinkedList< java.lang.String >list1
= new java.util.LinkedList< java.lang.String >();
list1.add( "text" );

final java.util.Map
< java.util.ArrayList< java.lang.String >, java.lang.String >map
= new java.util.HashMap
< java.util.ArrayList< java.lang.String >, java.lang.String >();

map.put( list0, "value" );

java.lang.System.out.println( map.get( list1 )); }}

value
 
J

Joshua Cranmer

Or rather, why does the spec not say get(K key) instead of get(Object o)

I can think of some cases where it might be desirable to use gets with
other objects; this requires, of course, that the objects be designed
with this use case in mind, namely that the hash codes and equals work
correctly.

One example that came up in some code was where I had a parameter
accessible in two different fashions, and the conversion between the two
forms became a hotspot in the code.
 
L

Lew

Stefan Ram wrote :
No it is not an error. Just will never retrieve ANY value if you use the
wrong type (see below).

This came about because I changed the key for a HashMap to one that is
more specific (such as String to MyKey). All the "put" statements were
caught. However ALL the get statements were not. I had to manually go
through the code to locate each occurrence and change it.

And of course I missed some and the code broke during runtime. I had
assumed that generics would solve this type of issue.

Generics only handle what is declared generically. 'Map#get()' is not so
declared. Assumptions based on falsehood will yield false conclusions.

Caveat: Yes it is possible to get the same hashcode with two different
types, however it would be highly unlikely.

And this is relevant how? 'Map' retrieval is defined in terms of 'equals()',
not 'hashCode()'.

... formally, if this map contains a mapping from a key k to a value v
such that (key==null ? k==null : key.equals(k)),
then this method returns v; otherwise it returns null.
(There can be at most one such mapping.)

FWIW, if 'Map#get()' had been redefined generically, it would have broken
existing code without providing an improvement. Code that would have not
gotten a value still does not get a value without introducing a new error.
Maybe they erred in this, but they were trying to maintain backward compatibility.

It's a shame that the compiler didn't find your refactoring mistake for you,
but it was your mistake, not Java's.
 
K

Kevin McMurtrie

Wojtek <[email protected]> said:
If I have the following:

HashMap<MyKey,MyValue> map = new HashMap<MyKey,MyValue>();
MyKey myKey = new MyKey();

Then I can put in a value by:

map.put(myKey, new MyValue() );

The compiler enforces the use of these two types.

However to do a get I can do the following and the compiler does not
complain:

map.get(myKey); // this is right
map.get(myKey.toString()); // this is wrong yet legal
map.get(new Long(20)); // this is wrong yet legal

All of these are legal according to the compiler. Why is it that the
compiler does not enforce type checking on the get()?

Or rather, why does the spec not say get(K key) instead of get(Object
o)

http://java.sun.com/javase/6/docs/api/java/util/HashMap.html#get(java.lang.Obj
ect)

HashMap.java: public V get(Object key)

There's no generics declared on the key for reading.

Generics is extremely helpful for common source code mistakes but it is
by no means a powerful tool. There have been many, many long threads
about it here.
 
L

Lew

Kevin said:
Generics is extremely helpful for common source code mistakes but it is
by no means a powerful tool. There have been many, many long threads
about it here.

There may have been long threads about it, but generics most assuredly is a
powerful tool. Perhaps you just haven't learned to use it correctly yet.
 
M

Mike Schilling

Wojtek said:
If I have the following:

HashMap<MyKey,MyValue> map = new HashMap<MyKey,MyValue>();
MyKey myKey = new MyKey();

Then I can put in a value by:

map.put(myKey, new MyValue() );

The compiler enforces the use of these two types.

However to do a get I can do the following and the compiler does not
complain:

map.get(myKey); // this is right
map.get(myKey.toString()); // this is wrong yet legal
map.get(new Long(20)); // this is wrong yet legal

All of these are legal according to the compiler. Why is it that the
compiler does not enforce type checking on the get()?

Or rather, why does the spec not say get(K key) instead of
get(Object
o)

http://java.sun.com/javase/6/docs/api/java/util/HashMap.html#get(java.lang.Object)

I can picture (vaguely) looking something up with an object that's
equal to the original key but not of the same type. This makes at
least some sense with HashMaps, since it's possible, for objects T1 a
and T2 b, where T1 and T2 are any types at all, that a.equals(b) and
a.hashCode() == b.hashCode().

TreeMap is weirder:

Map<Integer, String> m = new TreeMap<Integer,String>();
m.put(2, "foo");
String val = m.get("bar");

This compiles just fine, but results in

Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer
at java.lang.String.compareTo(String.java:90)
at java.util.TreeMap.compare(TreeMap.java:1093)
at java.util.TreeMap.getEntry(TreeMap.java:347)
at java.util.TreeMap.get(TreeMap.java:265)
at WeirdMap.main(WeirdMap.java:11)

Since a String can't even be compared to the Integer keys.

Actually, it strikes me as odd that String.compare() is called rather
than Integer.compare(). I suppose that if you want precise control
over how comparisons are done, you need to pass in your own
Comparator.
 
L

Lew

Mike said:
I can picture (vaguely) looking something up with an object that's
equal to the original key but not of the same type. This makes at
least some sense with HashMaps, since it's possible, for objects T1 a
and T2 b, where T1 and T2 are any types at all, that a.equals(b) and
a.hashCode() == b.hashCode().

I don't think so, not in general nor commonly. Someone who codes disparate
types that way should be slapped, then fired.

Remember, to work properly 'equals()' must be reflexive, symmetric and
transitive. However, you are right that it's possible to violate that.
Possible, but stupid.

The common idiom for 'equals()' checks that the argument is of the same type
as the invoker.
 
W

Wojtek

Lew wrote :
And this is relevant how? 'Map' retrieval is defined in terms of 'equals()',
not 'hashCode()'.

The initial lookup up is done with a hashcode.

You could contrive a case where the hashcode is randomly generated for
a given object. It will never be found in the HashMap, yet the equals
would return true.
 
W

Wojtek

Lew wrote :
FWIW, if 'Map#get()' had been redefined generically, it would have broken
existing code without providing an improvement. Code that would have not
gotten a value still does not get a value without introducing a new error.
Maybe they erred in this, but they were trying to maintain backward
compatibility.

All sorts of code broke when generics were introduced. Every use of
each class which had generics needed to be changed. Also changing get()
would just have been done at that time.
It's a shame that the compiler didn't find your refactoring mistake for you,
but it was your mistake, not Java's.

Of course it was my mistake. That is what I am saying. And that is why
we have compilers which report mistakes, so that they may be caught
BEFORE they show up during runtime and "weird things" happen.
 
W

Wojtek

Stefan Ram wrote :
Above, both types were the same after type erasure and were
types of empty containers.

Here is a program with types that differ even after type
erasure and non-empty containers.
Um, I did a bit of editing in your example, removing the packages. For
some reason this makes it easier for me to read...

public class Main
{ public static void main( final String[] args )
{
final ArrayList<String> list0 = new ArrayList<String >();
list0.add( "text" );

final LinkedList<String > list1 = new LinkedList<String >();
list1.add( "text" );

final Map<ArrayList<String>,String> map = new
HashMap<ArrayList<String>,String >();

map.put( list0, "value" );

System.out.println( map.get( list1 ));
}
}

I do not see why this works or why this should work. Maybe because
"text" was added to both list0 and list1?
 
L

Lew

Wojtek said:
Um, I did a bit of editing in your example, removing the packages. For
some reason this makes it easier for me to read...

Amen to that!

Who puts FQNs into java.lang classes?

Stefan, your examples would be much, much easier to read if you'd
follow the coding conventions and also use simple type names where the
context is clear and for java.lang classes. Your lack of
conventionality turned that example into garbage.
 
D

Dave Searles

Patricia said:
You could not have a random hashcode that conforms to the Object
hashCode contract, because of the requirement that two objects that are
equal according to their equals methods have the same hashCode result.

Actually, you could if the equals method always returned false. Then
hashCode conforms. But equals may not, if the equals contract requires
that if a == b then a.equals(b). In that case, a random-but-set-at-birth
hashCode and equals equivalent to == is as close as you can get while
conforming to BOTH cotnracts (and java.lang.Object basically is set up
like that).
 
L

Lew

Patricia said:
That is correct if "disparate" is read as meaning distinct types that do
are not sufficiently related that objects of the two types might be equal..

Yes, that's what I meant. Your example of different implementations
for 'List' would not be "disparate" in this sense.

Lew:
Patricia:
It is possible to have equals methods that allow equality to an object
of a different type, not a subclass or superclass, that is still
reflexive, symmetric, and transitive.

Consider, for example, ArrayList and Stack. They both conform to the
java.util.List interface, including its documentation for equals:
"Returns true if and only if the specified object is also a list, both
lists have the same size, and all corresponding pairs of elements in the
two lists are equal."

These types are not completely disparate, but even to the degree to
which they are they adhere to the key requirements as you point out.
They also provide a strong argument for 'Map#get(Object)' instead of
'Map#get(E)'.
The really, really, basic rule about equals is 'Indicates whether some
other object is "equal to" this one.'. One would need a breach of that
rule to get a false match from HashMap's get.

The decision to leave get accepting Object means that one can still
probe a Map<Stack, String> with an ArrayList, and get a match if, only
if, the Map contains a key that has equal elements in the same order as
the ArrayList.

Lew:

Patricia:
The more general version of this rule is to test for types of objects
that might be logically equal to this one. A List equals() method should
test whether its argument is a List, not whether it is the same type as
the specific List implementation.

Excellent points that explain why Java might not have wanted to
genericize 'Map#get()'. They might've done better with some sort of
bounded wildcard argument, but 'Object' works just fine.

The key insight from your example is that for the purpose of
comparison, 'List' implementations are viewed at the level of the
interface type, and not at the level of the concrete type. At that
level, 'ArrayList' and 'Stack' are not disparate.
 
L

Lew

Wojtek said:
Lew wrote :

All sorts of code broke when generics were introduced. Every use of each
class which had generics needed to be changed. Also changing get() would
just have been done at that time.


Of course it was my mistake. That is what I am saying. And that is why
we have compilers which report mistakes, so that they may be caught
BEFORE they show up during runtime and "weird things" happen.

But you cannot expect the compiler to catch all your mistakes. So this was
one that it couldn't catch because in their infinite wisdom the Powers That Be
decreed that 'Map#get()' not be type parameterized. A few minutes with the
'Map' Javadocs before you refactored would've saved you some trouble. Then
you could've used Eclipse's Ctrl-Shift-G (find all references) or the
equivalent for your IDE and fixed the problem with even less effort than
letting the compiler find the error would have caused.

I think we're spoiled by how much help Java gives us to catch and prevent
error. We start whining like little brats when we reach one of those corners
that we have to sweep with our own effort.

You did the community a good service by alerting us to this particular corner.
 
M

Mike Schilling

But you cannot expect the compiler to catch all your mistakes. So
this was one that it couldn't catch because in their infinite wisdom
the Powers That Be decreed that 'Map#get()' not be type
parameterized.

Which we still don't have a good explanation for. The closest to a
real use case for get(Object) rather than get(K) we've seen is
Patricia's, and that could be made to work with get(K) simply enough
by letting K be List instead of ArrayList.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,480
Members
44,900
Latest member
Nell636132

Latest Threads

Top