Chad Adams Chad Adams - 4 months ago 10
Java Question

Java Compile-Time Type Checking For Polymorphic Collections

I'm trying to write a collection that's 100% type safe. I have it all working properly the only issue I'm struggling with is this.

AttributeMap map = new AttributeMap();

map.put("special", 100);

map.put("running", false);

int special = map.get("special");

boolean running = map.get("running");

System.out.println("special value: " + special + " running value: " + running);

// not caught at compilation time, caught at run-time
boolean test = map.get("special");

// caught at compilation time
boolean test2 = map.get("special", Integer.class);


Output without the run-time error

special value: 100 running value: false


The value from the map becomes the value I choose. Example special should be a type integer, because that's what I put into the map is there a way to check this error at compile-time so it doesn't become a run-time error?

Before I post this code, this is going to look overly complicated you may ask why not just do this?

private Map<Object, Object> attributes = new HashMap<>();


Yes that would do the same as what I'm doing, but that fails to catch every cast at compilation time. I'm trying to keep track of my type I put in as a value and retrieve it as the same type so it can be caught at compilation time.

Here's my class so far.

AttributeMap

package com.vltr.collection.attr;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
* A specialized {@link Map} that ensures type safely upon a generic map.
*
* @author Vult-R
*/
public final class AttributeMap {

/**
* The map contains attributes.
*/
private Map<AttributeKey<?, ?>, Object> attributes;

private Set<AttributeKey<?, ?>> set = new HashSet<>();

/**
* Creates a new {@link AttributeMap}.
*
* @param attributes
* The map of attributes.
*/
public AttributeMap(Map<AttributeKey<?, ?>, Object> attributes) {
this.attributes = attributes;
}

/**
* Creates an empty {@link AttributeMap}.
*/
public AttributeMap() {
this.attributes = new HashMap<AttributeKey<?, ?>, Object>();
}

/**
* Places a new {@link AttributeKey} into the map.
*
* @param key
* The key to be used.
*
* @param value
* The value to map.
*
* @param clazz
* The class type associated between the key and value.
*/
@SuppressWarnings("unchecked")
public <K, V> void put(K key, V value) {
put(new AttributeKey<K, V>(key, (Class<V>) value.getClass()), value);
}

/**
* A wrapper function for placing a new {@link AttributeKey} into the map.
*
* @param key
* The key to be used.
*
* @param value
* The value to map.
*
* @param clazz
* The class type associated between the key and value.
*/
private <K, V> void put(AttributeKey<K, V> key, V value) {
attributes.put(key, value);
set.add(key);
}

/**
* A wrapper function for retrieving a value.
*
* @param key
* The key mapped to a value.
*
* @throws AttributeException
* If an error occurs while trying to retrieve a value.
*
* @return The associated value.
*/
private <K, V> V get(AttributeKey<K, V> key) throws AttributeException {

V type = null;

AttributeKey<K, V> k = getFromSet(key);

try {
type = (V) key.getClazz().cast(attributes.get(key));
} catch (ClassCastException ex) {
throw new AttributeException(key, attributes.get(key).getClass());
}

if (key.getClazz() != k.getClazz()) {
System.out.println("not the same");
}

return type;

}

/**
* Gets a value for retrieving a value
*
* @param key
* The key mapped to a value.
*
* @param clazz
* The class type associated between the key and value.
*
* @throws AttributeException
* If an error occurs while trying to retrieve a value.
*
* @return The associated value.
*/
public <K, V> V get(K key, Class<V> clazz) {
return get(new AttributeKey<K, V>(key, clazz));
}

/**
* Gets a value for retrieving a value
*
* @param key
* The key mapped to a value.
*
* @param clazz
* The class type associated between the key and value.
*
* @throws AttributeException
* If an error occurs while trying to retrieve a value.
*
* @return The associated value.
*/
public <K, V> V get(K key) {

final AttributeKey<K, V> k = new AttributeKey<K, V>(key, getFromSet(new AttributeKey<K, V>(key, null)).getClazz());

return get(k);
}

/**
* Removes a {@code key} and associated value from the map.
*
* @param key
* The key and its associated value to remove.
*/
public <K, V> void remove(AttributeKey<K, V> key) {
attributes.remove(key);
set.remove(key);
}


AttributeKey

/**
* Removes a {@code key} and associated value from the map.
*
* @param key
* The key and its associated value to remove.
*/
public <K, V> void remove(K key) {

final AttributeKey<K, V> ak = new AttributeKey<K, V>(key, getFromSet(new AttributeKey<K, V>(key, null)).getClazz());

remove(ak);
}

/**
* Sets a {@code key} and its associated {@code value}.
*
* @param key
* The key to set.
*
* @param value
* The value to set.
*/
public <K, V> void set(AttributeKey<K, V> key, V value) {
attributes.put(key, value);
}

/**
* Clears all keys and associated values from this map.
*/
public void clear() {
attributes.clear();
}

/**
* Determines if a {@code key} with associated {@code clazz} type exists
* within this map.
*
* @param key
* The key to check.
*
* @param clazz
* The clazz to check.
*
* @return {@code true} If this map contains a specified key and its correct
* class type. {@code false} Otherwise.
*/
public <K, V> boolean containsKey(K key, Class<V> clazz) {
return attributes.containsKey(new AttributeKey<K, V>(key, clazz));
}

/**
* Determines if a value exists within this map.
*
* @param value
* The value to check.
*
* @return {@code true} If this map contains this specified value.
* {@code false} Otherwise.
*/
public boolean containsValue(Object value) {
return attributes.containsValue(value);
}

/**
* Retrieves the undlying {@link #entrySet()} from this map.
*
* @return The {@link #entrySet()}.
*/
public Set<Entry<AttributeKey<?, ?>, Object>> entrySet() {
return attributes.entrySet();
}

@SuppressWarnings("unchecked")
private <K, V> AttributeKey<K, V> getFromSet(AttributeKey<K, V> key) {
for(AttributeKey<?, ?> k : set) {
if (k.getKey() == key.getKey()) {
return (AttributeKey<K, V>) k;
}
}
return null;
}

/**
* Determines if this attribute map equals another attribute map.
*
* @param o
* The object to check.
*
* @return {@code true} If this map equals another attribute set,
* {@code false} Otherwise. *
*/
public boolean equals(Object o) {
return attributes.equals(o);
}

/**
* Retrieves the hash code for this attribute map.
*
* @return The hash code.
*/
public int hashCode() {
return attributes.hashCode();
}

/**
* Determines if this attribute map is empty.
*
* @return {@true} If this map is empty, {@code false} Otherwise.
*/
public boolean isEmpty() {
return attributes.isEmpty();
}

/**
* Retrieves the underlying {@link #keySet()} from this map.
*
* @return The {@link #keySet()}.
*/
public Set<AttributeKey<?, ?>> keySet() {
return attributes.keySet();
}

/**
* Gets the size of this map.
*
* @return The size.
*/
public int size() {
return attributes.size();
}

public int setSize() {
return set.size();
}

/**
* Gets the values of this map.
*
* @return The values.
*/
public Collection<Object> values() {
return attributes.values();
}

}


package com.vltr.collection.attr;

/**
* Represents a wrapper that wraps a {@link Map}s key value. This class will help enforce the correct type.
*
* @author Vult-R
*/
public final class AttributeKey<K, V> {

/**
* The key that will be used.
*/
private final K key;

/**
* The class type associated with this key.
*/
private final Class<V> clazz;

/**
* Creates a new {@link AttributeKey}.
*
* @param key
* The key that will be used.
*
* @param clazz
* The associated class type.
*/
public AttributeKey(K key, Class<V> clazz) {
this.key = key;
this.clazz = clazz;
}

/**
* Gets the key for this attribute.
*
* @return The key.
*/
public K getKey() {
return key;
}

/**
* Gets the associated class type for this attribute.
*
* @return The class type.
*/
public Class<V> getClazz() {
return clazz;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
AttributeKey<?, ?> other = (AttributeKey<?, ?>) obj;
if (key == null) {
if (other.key != null) {
return false;
}
} else if (!key.equals(other.key)) {
return false;
}
return true;
}

@Override
public String toString() {
return key.getClass().getSimpleName();
}
}

package com.vltr.collection.attr;

/**
* The {@link RuntimeException} implementation specifically for {@link Attribute}s.
*
* @author Seven
*/
public final class AttributeException extends RuntimeException {

private static final long serialVersionUID = 1L;

/**
* Creates a new {@link AttributeException}.
*
* @param key
* The key or this attribute.
*
* @param value
* The value for this attribute.
*/
public AttributeException(AttributeKey<?, ?> key, Object value) {
super(String.format("Invalid value type: %s for [key=%s], only accepts type of %s", value.getClass().getSimpleName(), key.getKey().toString(), key.getClazz().getClass().getSimpleName()));
}

/**
* Creates a new {@link AttributeException}.
*
* @param key
* The key which contains an error.
*/
public AttributeException(AttributeKey<?, ?> key) {
super(String.format("Could not retrieve a value for [key= %s]", key.getKey()));
}

public AttributeException(AttributeKey<?, ?> key, Class<?> clazz) {
super(String.format("Could not cast [key= %s] from [type= %s] to [type= %s]. ", key.getKey(), key.getClazz().getSimpleName(), clazz.getSimpleName()));
}

}


I can catch this problem by doing this.

map.put("special", 100);

// correct
int special = map.get("special", Integer.class);

// incorrect and caught at compile-time
boolean special = map.get("special", Integer.class);


Though I don't want to specify the second parameter I want to hide that. Is that possible?

Answer

You can't do this.

Java does not support compile-time type checking for polymorphic generic collections. You can add anything to a Collection<?> but when retrieving you always get back Object and have to cast to the appropriate type, which will always involve a runtime check.

The compiler was trying to tell you this but you disabled the warnings with @SuppressWarnings("unchecked"), which is like putting black tape over the temperature warning light in your car and then being surprised when the engine overheats.

You say:

I can catch this problem by doing this.

map.put("special", 100);
// correct
int special = map.get("special", Integer.class);

// incorrect and caught at compile-time     
boolean special = map.get("special", Integer.class);

Though I don't want to specify the second parameter I want to hide that. Is that possible?

Think this through. The put calls could (will) have happened far, far away (i.e. not in the current source file, possibly something compiled last year). The compiler has no idea what types are contained in the Map at runtime for any specific key. In fact on two different executions a given key could map to values of completely different types. How is the compiler, when it is compiling the source, supposed to know the value type associated with a key in the future? Or that the type will always be the same one?

From a comment by the OP:

Though making 100% type-safe collections using a map is possible. See here https://github.com/atomicint/aj8/tree/master/server/src/main/java/org/apollo/game/attribute

Notice in AttributeMap.java:

@SuppressWarnings("unchecked")  
public <T> T get(AttributeKey<T> key) { 
    ...

All that code does is push the runtime check into AttributeMap<>#.get(), and it also resorts to @SuppressWarnings("unchecked"). It just hides the runtime check so your code doesn't have to hide it. The runtime check and potential ClassCastException are still there, and this is most definitely NOT any more type safe.

Comments