1

I have a class named Myclass, which is just a wrapper of a HashMap, I want to be able to store the possible key/value pair listed below:

  • KEY_A -> MyClassA
  • KEY_LIST_B -> List<MyClassB>
  • KEY_C -> List<MyClassC>

Here is my code :

public class Main {
    public static void main(String[] args) {
        MYClass myClass = new MYClass();

        myClass.set(MyEnum.KEY_A, new MyClassA());
        myClass.set(MyEnum.KEY_LIST_B, new ArrayList<>(Arrays.asList(new MyClassB())));
        myClass.set(MyEnum.KEY_C, new ArrayList<>(Arrays.asList(new MyClassC())));

        MyClassA a = (MyClassA) myClass.get(MyEnum.KEY_A);
        List<MyClassB> listB = (List<MyClassB>) myClass.get(MyEnum.KEY_LIST_B);//Unchecked cast
        List<MyClassC> listC = (List<MyClassC>) myClass.get(MyEnum.KEY_C);//Unchecked cast
    }

    public static class MYClass {
        private final HashMap<MyEnum, Object> map;

        public MYClass() { map = new HashMap<>(); }

        public Object get(MyEnum key) { return map.get(key); }

        public void set(MyEnum key, Object value) { map.put(key, value); }
    }

    public static class MyClassA {}
    public static class MyClassB {}
    public static class MyClassC {}
    public enum MyEnum {KEY_A, KEY_LIST_B, KEY_C}
}
  1. How can I design (signature of these methods) the get() and set() methods of MyClass to be able to store the key/value pair listed earlier and avoid the Unchecked cast?

  2. Why this line does not have Unchecked cast warning even if the cast is not safe ?

    MyClassA a = (MyClassA) myClass.get(MyEnum.KEY_A);

Abdo21
  • 498
  • 4
  • 14
  • 2
    (2) Because if that cast is not possible then it will fail with a `ClassCastException` at run-time. The "unchecked cast" warning is related to generics, because it's possible to change the generic type arguments via casts without getting a cast exception at run-time (e.g., you inserted a `List` value but then cast the object to a `List` when you retrieve the value, allowing you to add `Integer` to the `List`). – Slaw Aug 21 '22 at 22:25
  • 1
    (1) You can do the casting internally and use a generic psuedo-enum [like this](https://stackoverflow.com/a/12605285/1553851). – shmosel Aug 21 '22 at 22:28
  • @Slaw Thanks for your reply, I thought the warning was related to `ClassCastException`. – Abdo21 Aug 21 '22 at 22:41
  • @shmosel Thanks for your reply, I don't understand what you mean by 'internal casting' and 'generic pseudo-enumeration' can you please show me some code how can I use it – Abdo21 Aug 21 '22 at 22:54
  • @shmosel's link already gives an example. That is the only way you might be able to accomplish something type-safe. – Louis Wasserman Aug 21 '22 at 23:02
  • Thanks @LouisWasserman, There are many answers, I don't know which one to choose and how, although I don't think the question is the same because I'm not asking about generic enumeration, in my case all keys belong to the same enumeration (`MyEnum` ) only the value and the return type can be changed. – Abdo21 Aug 21 '22 at 23:17
  • If you cannot change the enum, then there is nothing whatsoever you can do to improve matters. Fundamentally, the types of `function(KEY_A)` and `function(KEY_B)` must always be the same, if the types of `KEY_A` and `KEY_B` are the same. – Louis Wasserman Aug 22 '22 at 00:11

1 Answers1

2

A typical solution would be to use a generic "key class" that carries information regarding the value type. Instances of the key would have a reference to the value type's Class. Note your enum is close to this solution, but unfortunately you can't have a generic enum where each constant has different type arguments.

Here's an example:

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Container {

  public static final Key<Foo> FOO_KEY = new Key<>(Foo.class);
  @SuppressWarnings({"rawtypes", "unchecked"}) 
  public static final Key<List<Bar>> BAR_LIST_KEY = new Key(List.class);
  @SuppressWarnings({"rawtypes", "unchecked"})
  public static final Key<List<Baz>> BAZ_LIST_KEY = new Key(List.class);

  private final Map<Key<?>, Object> map = new HashMap<>();

  public <T> void put(Key<T> key, T value) {
    map.put(key, value);
  }

  public <T> T get(Key<T> key) {
    var value = map.get(key);
    return value == null ? null : key.getValueType().cast(value);
  }

  public static final class Key<T> {

    private final Class<T> valueType;

    private Key(Class<T> valueType) {
      this.valueType = valueType;
    }

    public Class<T> getValueType() {
      return valueType;
    }
  }
}

I personally would be okay with this implementation, as the (suppressed) warnings only occur inside Container. Code which uses the Container class won't encounter unchecked cast or raw type warnings.

The reason for the @SuppressWarnings annotations is because a Class cannot legitimately represent a parameterized type. In other words, you can have a Class<List> but not a Class<List<Foo>>1. However, if you want to avoid even suppressing these warnings, then the only approach I can think of is to create wrapper classes for your generic value types. Such as:

public record MyClassBList(List<MyClassB> list) {}

And then have a Key<MyClassBList>.

As for why:

MyClassA a = (MyClassA) myClass.get(MyEnum.KEY_A);

Does not cause an "unchecked cast" warning to be omitted, that's because there's no generics involved. If the above cast is not possible at run-time, then a ClassCastException would be thrown. But when generics are involved, then the cast might succeed even if you "change" the type arguments. For instance:

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

List<String> stringList = new ArrayList<>();
stringList.add("Hello, World!");
map.put("key", stringList);

// This results in an "unchecked cast" warning, but the cast
// **will** succeed at run-time.
List<Integer> integerList = (List<Integer>) map.get("key");
integerList.add(0); // okay, because you've said it's a List<Integer>

// Now 'stringList' has a String element AND an Integer element
for (String element : stringList) {
  // this will fail at run-time (after the cast to List<Integer>
  // was successful) because the second iteration will result in
  // trying to cast an Integer to a String
  System.out.println(element);
}

And that mess of a situation is why they warn you about "unchecked casts". The Container example above should not be able to result in such problems due to how put and get are defined, unless you deliberately and explicitly try to break things (e.g., by using raw types, casting "hacks", etc.).


1. You technically can have a reference to a Class<List<Foo>>, by doing something similar to what I did with Key above, but it would not actually represent a List<Foo>.

Slaw
  • 37,820
  • 8
  • 53
  • 80