Is it possible to store String type elements in List<Integer >- Cast type to generic

This is actually an online bug I encountered. I share it with you here.

If you use reflection, it's very simple. After all, generics are only constrained at compile time and can't do anything at run time.

Think about it, if you don't use reflection, is there any way to do it?

Cause of problem

In the actual business of our company, there is a code similar to this logic. Finally, the article will release the getList() method for test construction:

    /**
     * Main business logic
     */
    public static void main(String[] args) {
        // Query the data list from the database without paying attention to the implementation details
        List<DataBO> list = getList();

        // Gets a collection of values for all "a" fields
        List<Integer> integerList = toList(list, "a");

        if (integerList.contains(1)) {
            System.out.println("The set contains 1 to process the corresponding logic");
        } else {
            System.out.println("The set does not contain 1, and the corresponding logic is processed");
        }
    }

    /**
     * This is a public tool method provided by the company to obtain the collection of values of a field of each object in the collection
     *
     * @param list Data object collection
     * @param key field
     * @return Set of values
     */
    public static <T> List<T> toList(List<DataBO> list, String key) {
        return list.stream()
                .filter(x -> x.get(key) != null)
                .map(x -> (T)x.get(key))
                .collect(Collectors.toList());
    }

The DataBO object is simplified as follows:

public class DataBO {

    /** A piece of data in the database. key is a column and value is a value */
    private Map<String, Object> map = new HashMap<>();

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

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

    @Override
    public String toString() {
        return "DataBO{" + "map=" + map + '}';
    }

}

Originally, my business requirement here is to take the values of all "a" fields in the list data and judge whether they contain 1.

It is known that the "a" field in the database is defined as int type, and it is confirmed that there is a piece of data stored in the "a" field is 1. But as soon as the code went online, there was a bug.

Find out how to go to the branch of "excluding 1"? There is no error. Is there any special treatment in the getList() method of the underlying service to filter out the data of database a=1?

Problem location

So I added some logs and printed out the elements of list and intergerList to see what was stored in them. So another version was launched online. After observing it, a magical thing appeared. There was clearly 1??! Why do you go to the branch "not including 1" below? Shit!

So I had to debug locally and found that in the set found in the database, the "a" field returned a string "1"! In the contents () method of ArrayList, the bottom layer uses equals() to compare whether it exists. "1".equals(1), the result must be false, so it is considered not to exist.

Well, although the "a" field of the database is defined as int type, the underlying service estimates that there is a bug. It converts the field of Integer type into String type and returns it to the upper service.

But it's always wrong. I clearly define variables of type list < Integer >. If so, even if the "a" field is not an Integer value, the toList() method should throw a Java Lang. ClassCastException is right. How can it go down normally? Why is a string stored in the object pointed to by the list < Integer > variable? Why is the toList() method Does the line map (x - > (T) x.get (key)) report an error?

Problem analysis

The obvious problem is that in the toList() method, the cast does not take effect. At the beginning, we said that java generics are only constrained at compile time, which is powerless at run time. Then we should first think of the generic erasure mechanism of java. Let's compile and decompile the demo class.

Decompilation shows that the mandatory type conversion in the original toList() method has been erased. Therefore, the returned object is not a List < integer > object, but a List object without generic restrictions. Obviously, there is a bug in this method. In fact, it is the wrong use of generic methods.

Problem repair

Originally, this online bug has been clarified here. If you just want to repair it quickly, it can be easily solved online. Change the set returned by the toList() method to list < string >, and then judge whether the set contains the string "1".

But we think that if other colleagues encounter this problem later, what should we do? They will also look confused. It's best to hope that the toList() method throws a Java Lang. ClassCastException, but also to achieve the effect of the original method, how to modify this method?

We can add a parameter to tell the method what type of value you want to return:

In this case, if the toList() method still returns the original list < integer >, an exception will be thrown:

Moreover, if the types of the previous and subsequent restrictions are inconsistent, errors will be reported at compile time, and generics will work:

This problem has been completely solved.

Supplement the getList() method used in this article to test the construction:

    /**
     * Check the database to obtain the collection of data objects
     *
     * @return Collection of data objects
     */
    public static List<DataBO> getList() {
        // This list is found from the database
        List<DataBO> list = new ArrayList<>();
        DataBO db1 = new DataBO();
        db1.set("a", "1");
        DataBO db2 = new DataBO();
        db2.set("a", 2);
        list.add(db1);
        list.add(db2);
        return list;
    }

Keywords: bug

Added by GarDouth on Sun, 30 Jan 2022 17:46:48 +0200