Do you have a deep understanding of Java generics?

generic paradigm

Generics provide a way to convey a collection type to the compiler, which can check and constrain its type once the compiler knows the type of the collection element.

Before there are no generics:

/**
 * Iterate Collection, note that only String types can exist in Collection
 */
public static void forEachStringCollection(Collection collection) {
    Iterator iterator = collection.iterator();
    while (iterator.hasNext()) {
        String next = (String) iterator.next();
        System.out.println("next string : " + next);
    }
}

This is the program after using generics:

public static void forEachCollection(Collection<String> collection) {
  Iterator<String> iterator = collection.iterator();
  while (iterator.hasNext()) {
    String next = iterator.next();
    System.out.println("next string : " + next);
  }
}

Until there are no generics, we can only tell the method caller through more intuitive method naming and doc comments, and the forEachStringCollection method can only receive collections of element type String.However, this is only a "convention". If the user passes in a collection whose elements are not of type String, the code will not fail during compilation and the ClassCastException exception will be thrown only at runtime, which is not friendly to the caller.

With generics, doc comments for methods can be transferred to method signatures: forEachCollection (Collection <String> collection), where method callers know a Collection <String> is required at a glance, and compilers can check for type constraints at compile time.It should be noted that the compiler's checks are also very easy to bypass. How do you bypass them?See below.

Voice-over: Code is the best comment.

Generics and Type Conversion

Think about whether the following code is legal:

List<String> strList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
objList.add("Public Number:Coder Little Black"); // Code 1
objList = strList; // Code 2

Say nothing but answer directly.

Code 1 is clearly legal.Object type is the parent of String type.

So why is Code 2 not legal?

In Java, assignment of object types is actually assignment of reference addresses, that is, assuming that code 2 assigns successfully, objList and strList variables refer to the same address.What's wrong with that?

If you add a non-String element to the objList at this point, it is equivalent to adding a non-String element to the strList.Clearly, the List <String> strList is destroyed here.Therefore, the Java compiler would consider code 2 illegal and safe.

Voice-over: Maybe it's different from most people's intuition, that's why we haven't considered the problem thoroughly enough, and the reason here is more important than the result.

Generic wildcards

As we already know, code 2 above is illegal.Then, let's think about two ways to do this:

public static void printCollection1(Collection c) {}

public static void printCollection2(Collection<Object> c) {}

What is the difference between the two methods?

The printCollection1 method supports collections of any element type, while the printCollection2 method can only receive collections of Object type.Although String is a subclass of Object, Collection <String> is not a subclass of Collection <Object> and does the same thing as Code 2.

Take another look at this method:

public static void printCollection3(Collection<?> c) {}

What is the difference between the printCollection 3 and the two methods above?How do you understand on the printCollection 3 method?

? Represents any type, indicating that the printCollection3 method receives any type of collection.

Okay, so here's the problem again. Look at the following code:

List<?> c = Lists.newArrayList(new Object());
Object o = c.get(0);
c.add("12"); // Compilation error

Why compile errors?

We can assign any type of set to a List<?> C variable.However, what is the parameter type of the add method?, which indicates an unknown type, so it is safe to call the add method with a programming error.

The get method returns the elements in the collection. Although the element type in the collection is unknown, it is Object type regardless of the type, so it is safe to use Object type to receive.

Bounded wildcards

public static class Person extends Object {}

public static class Teacher extends Person {}

// Only know that this generic type is a subclass of Person, which one does not know
public static void method1(List<? extends Person> c) {}

// Only know that this generic type is the parent of Teacher, and which one does not know
public static void method2(List<? super Teacher> c) {}

Think about the results of running the code as follows:

public static void test3() {
  List<Teacher> teachers = Lists.newArrayList(new Teacher(), new Teacher());
  // method1 handles a subclass of Person and Teacher is a subclass of Person
  method1(teachers);
}


// Only know that this generic type is a subclass of Person, which one does not know
public static void method1(List<? extends Person> c) {
  // Person subclass, to Person, security
  Person person = c.get(0);
  c.add(new Person()); //Code 3, compilation error
}

Why did code 3 compile errors?

method1 only knows that this generic type is a subclass of Person, and which one does not know.If Code 3 compiles successfully, then one of the above codes adds a Person element to List <Teacher> teachers.At this point, a ClassCastException exception will probably be thrown in subsequent operations with List <Teacher> teachers.

Let's look at the following code again:

public static void test4() {
  List<Person> teachers = Lists.newArrayList(new Teacher(), new Person());
  // method1 handles a subclass of Person and Teacher is a subclass of Person
  method2(teachers);
}

// Only know that this generic type is the parent of Teacher, and which one does not know
public static void method2(List<? super Teacher> c) {
  // Which one doesn't know and can only be received with Object
  Object object = c.get(0); // Code 4
  c.add(new Teacher()); // Code 5, no error
}

The method2 generic type is the parent of the Teacher, and there are many parent classes of the Teacher, so Code 4 can only receive with Object.Subclasses inherit the parent class, so it is safe to add a Teacher object to the collection.

Best Practices: PECS Principles

PECS:producer extends, consumer super.

  • Producer, production data, use <? Extends T>
  • Consumer, consumer data, use <? Super T>

How do you understand that?Let's go directly to the code:

/**
 * producer - extends, consumer´╝Ź super
 */
public static void addAll(Collection<? extends Object> producer,
                          Collection<? super Object> consumer) {
    consumer.addAll(producer);
}

Some students may say, what can I do with this principle?

That's okay. I sometimes can't remember it.Fortunately, there is one method in JDK: java.util.Collections#copy, which is a good way to illustrate PECS principles.Every time I want to use it and I can't remember it, I can see it at a glance.

// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src){}

Voice-over: There's a lot of knowledge and a lot of clutter. We should index our brains, encounter problems, and use the index to quickly find solutions

More secure generic checking

Some of the above checks are compile-time, and it's easy to trick the compiler into checking:

public static void test5() {
  List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
  List copy = list;
  copy.add("a");
  List<Integer> list2 = copy;
}

The test5 method deceives the compiler and runs successfully.

When will you make a mistake?ClassCastException exceptions are thrown when the program reads elements in list2.

Java provides us with the java.util.Collections#checkedList method, which checks for type matches when add s is called.

public static void test6() {
  List<Integer> list = Collections.checkedList(Arrays.asList(1, 2, 3, 4, 5), Integer.class);
  List copy = list;
  // Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
  copy.add("a");
}

Voice-over: This is the idea of fail-fast, which errors immediately when type inconsistencies are found in add, instead of continuing with a potentially problematic program

Type Error

We know that the compiler erases generics, so what do you mean by generic erasing?Is unification changed to Object?

Generic erase follows the following rules:

  • If generic parameters are unbound, the compiler replaces them with Object s.
  • If generic parameters are bounded, the compiler replaces them with boundary types.
public class TypeErasureDemo {
    public <T> void forEach(Collection<T> collection) {}

    public <E extends String> void iter(Collection<E> collection) {}
}

Use the javap command to view Class file information:

From the Class file information, you can see that the compiler replaces the generic of the forEach method with Object and the generic of the iter method with String.

Generics and method overloads

Now that you know the generic erase rules, let's see what problems generics encounter when they encounter method overloads.

Read the following code:

// first group
public static void printArray(Object[] objs) {}

public static <T> void printArray(T[] objs) {}
// Group 2
public static void printArray(Object[] objs) {}

public static <T extends Person> void printArray(T[] objs) {}

Does both of the above methods constitute a heavy load?

  • The first group: Generics are erased, that is, at run time, T[] is actually Object[], so the first group does not constitute an overload.

  • The second group: <T extends Person>indicates that the received method is a subclass of Person and constitutes an overload.

Resolving generics using ResolvableType

The Spring framework provides org.springframework.core.ResolvableType to elegantly resolve generics.

A simple example of use is as follows:

public class ResolveTypeDemo {

    private static final List<String> strList = Lists.newArrayList("a");

    public <T extends CharSequence> void exchange(T obj) {}

    public static void resolveFieldType() throws Exception {
        Field field = ReflectionUtils.findField(ResolveTypeDemo.class, "strList");
        ResolvableType resolvableType = ResolvableType.forField(field);
        // class java.lang.String
        System.out.println(resolvableType.getGeneric(0).resolve());
    }

    public static void resolveMethodParameterType() throws Exception {
        Parameter[] parameters = ReflectionUtils.findMethod(ResolveTypeDemo.class, "exchange", CharSequence.class).getParameters();
        ResolvableType resolvableType = ResolvableType.forMethodParameter(MethodParameter.forParameter(parameters[0]));
        // interface java.lang.CharSequence
        System.out.println(resolvableType.resolve());
    }

    public static void resolveInstanceType() throws Exception {
        PayloadApplicationEvent<String> instance = new PayloadApplicationEvent<>(new Object(), "hi");
        ResolvableType resolvableTypeForInstance = ResolvableType.forInstance(instance);
        // class java.lang.String
        System.out.println(resolvableTypeForInstance.as(PayloadApplicationEvent.class).getGeneric().resolve());
    }
}

Generic and JSON Deserialization

Recently you saw a code that uses Jackson to convert JSON to Map.

public class JsonToMapDemo {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    public static <K, V> Map<K, V> toMap(String json) throws JsonProcessingException {
        return (Map) OBJECT_MAPPER.readValue(json, new TypeReference<Map<K, V>>() {
        });
    }

    public static void main(String[] args) throws JsonProcessingException {
        // {"1":{"id":1}}
        String json = "{\"1\":{\"id\":1}}";
        Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
        });
        userIdMap.forEach((integer, user) -> {
            System.out.println(user.getId());
        });

    }

    @Data
    public static class User implements Serializable {
        private static final long serialVersionUID = 8817514749356118922L;
        private int id;
    }
}

Run the main method, although the code ends normally.But what's wrong with this code?Let's look at the following code:

public static void main(String[] args) {
  // {"1":{"id":1}}
  String json = "{\"1\":{\"id\":1}}";
  Map<Integer, User> userIdMap = toMap(json);
  userIdMap.forEach((integer, user) -> {
    // Source code will error
    // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    System.out.println(user.getId());
  });
}

Why should I report ClassCastException?Let's explore Debug.

Debug reveals that the key of a Map <Integer, User > userIdMap object is actually a String type, while value is a LinkedHashMap.It's understandable that the above code is written without knowing what K and V are.Write correctly as follows:

public static void main(String[] args) throws JsonProcessingException {
  // {"1":{"id":1}}
  String json = "{\"1\":{\"id\":1}}";
  Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
  });
  userIdMap.forEach((integer, user) -> {
    System.out.println(user.getId());
  });
}

Welcome to the WeChat Public Number: Coder XiaoHei

Tags: Java JSON Programming JDK

Posted on Wed, 08 Jan 2020 21:51:53 -0800 by britt15