In-depth understanding of java generics-PECS rule

Speaking of generics, everyone should be familiar with it. The most commonly used is List<T> list. If it is not for the encapsulation of the method, the use of reflection, and the direct writing of the generic class, in fact, there is nothing to pay special attention to, the lack of generics, the compiler will naturally remind, and it will do.

Well, let's first introduce a simple generic.

     /**
         * 这个Integer可以写成int么?为什么?
         */
        List<Integer> list = new ArrayList<>();

We know that to create a collection, it is necessary to define a generic type, of course, it can also be defined vaguely, such as List<T> list and so on. Can this Integer be written as an int? Of course, this is not possible. As shown in the figure below, we can see that generics are generally inherited from Object, which means it is a wrapper class. Int is just a basic type and cannot be used as a generic.

Okay, let’s go on to say that many partners who use <T> and <?> these two types of generics should have encountered them. They may be at a loss for a certain period of time. These two buddies are representatives of types. What's the difference?

This, it’s actually very simple to say, <T> (of course, <E> is the same, all are replaced by a letter) logo, you can see that the letter <T> is fixed, and a letter is replaced A wrapper class, that is, the generic is a class, for example, T represents Integer, then as long as you use List<T> or other uses that contain T, it represents Integer.

<?> Generic is a question mark, in fact it is also very clear, that is, I don't know what type will be passed, and any type can be represented.

We know that the use of generics is generally the encapsulation of methods, using reflection and other techniques to extract the common parts of the code. At this time, <T>, <?> are generally used. Some partners may not understand it yet. Let's expand. .

Java generics is a new feature introduced in J2 SE1.5. Its essence is a parameterized type, which means that the data type being manipulated is designated as a parameter (type parameter). This parameter type can be used in classes , interfaces, and In the creation of methods, they are called generic classes, generic interfaces, and generic methods.

I’m not very good at explaining the concept, just make a copy, and let’s talk about using it directly below.

Create a few classes first (you can skip)

/**
 * 动物(祖父类级别)
 */
@Data
public class Animal {

    /**
     * 判断条件,默认为false
     */
    private Boolean flag = false ;

    /**
     * 名字
     */
    private String name;
    /**
     * 重量
     */
    private String weight;

}

/**
 * 鸟类
 */
@Data
public class Bird extends Animal{
    /**
     * 飞多高
     */
    private String flyHeight;

}

/**
 * 鹰
 */
@Data
public class Eagle extends Bird{

    /**
     * 住在山上
     */
    private String mountain;
    /**
     * 吃肉
     */
    private String meat;

}

/**
 * 鹦鹉
 */
@Data
public class Parrot extends Bird {

    /**
     * 住的笼子
     */
    private String cage;

    /**
     * 吃米饭
     */
    private String rice;
}

Let’s write a step-by-step look at it. Let’s start with a simple one, which is to pass in an object Animal, judge according to the rules, and then assign the name. Without generics, the common part of the extraction method is that a Bird will handle it. A Parrot will also handle the incoming one. Write a simple method setAnimalName(Animal animal) to implement the next logic.

  /**
     * 设置动物名称
     * 无泛型,入参直接写父类名称,子类也可以传进来处理
     * 不过要处理的类型最好是父类的,不然要特殊处理(用反射)
     * @param animal
     */
    private static void setAnimalName(Animal animal){
        if(animal.getFlag()){
            animal.setName("animal");
        }
    }

    public static void main(String[] args) {
        Eagle eagle = new Eagle();
        eagle.setMeat("beef");
        eagle.setName("jake");

        Parrot parrot = new Parrot();
        parrot.setFlag(true);
        parrot.setCage("red-cage");

        setAnimalName(eagle);
        setAnimalName(parrot);
        System.out.println(" eagle:"+ JSON.toJSONString(eagle)+
                 "\n parrot:"+JSON.toJSONString(parrot));

    }

You can look at the printout. Although generics are not used, the inheritance relationship can be achieved:

 eagle:{"flag":false,"meat":"beef","name":"jake"}
 parrot:{"cage":"red-cage","flag":true,"name":"animal"}

Some friends always feel that this should use generics, right? Why is it useless? Of course, it can be used, let's take a look.

   /**
     * 使用泛型
     * @param t
     */
    private static <T extends Animal> void setAnimalName(T t){
        if(t.getFlag()){
            t.setName("animal");
        }
    }

Does it feel a bit interesting? The result is the same, so I won’t post it.

Let's extend it. If I don't have an inheritance relationship, I only know that there is a field name and a field flag in the passed object. At this time, of course, I can only use generics, which is to use simple reflection and recursion. Let's take a look.

    /**
     * 使用泛型
     * @param t
     */
    private static <T> void setAnimalName(T t){
        Class<?> temp = t.getClass();
        try {
            //flag 可能在父类中,使用clzz = clzz.getSuperclass() 递归
            Method method = getMethod(temp,"getFlag");
            Object invoke = method.invoke(t);
            if(invoke instanceof Boolean && (Boolean) invoke){
                Method setNameMethod = getMethod(temp,"setName",String.class);
                //设置名称
                setNameMethod.invoke(t, "animal");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 递归获取方法
     * @param temp
     * @param methodName
     * @return
     */
    private static Method getMethod(Class<?> temp,String methodName,Class<?>... parameterTypes){
        Method method = null ;
        while (method == null && !temp.getName().equalsIgnoreCase("java.lang.Object")){
            try {
                method = temp.getDeclaredMethod(methodName,parameterTypes);
            } catch (NoSuchMethodException e) {
                //源码中抛出了异常,所以还要捕获处理掉
            }
            temp = temp.getSuperclass();
        }
        return method;
    }
}

That’s all for the commonly used generic processing. Let’s take a look at PECS. Simply put, the Producer uses extends and the Consumer uses super. If the code looks a bit awkward, you can first look at the following figure. .

 /**
     * PECS理解
     */
    static public void test(){

        //继承Bird 的类是有多个的,这个类不是固定的,所以使用?,使用T会报错
        List<? extends Bird> pList = new ArrayList<>();

        pList = Arrays.asList(new Bird(),new Bird());
//        pList.add(new Bird());//报错
//        pList.add(new Eagle());//报错
//        pList.add(new Parrot());//报错
//        pList.add(new Animal()); //报错
        Bird bird = pList.get(0);
        System.out.println("sList的大小:"+pList.size() +"\n bird"+bird);

        List<? super Bird> sList = new ArrayList<>();
        sList.add(new Bird());
        sList.add(new Parrot());
        sList.add(new Eagle());
//        Object o = sList.get(0);  //返回的是Object,没啥意义

        System.out.println("sList的大小:"+sList.size());
    }
    

The above is the operation code of PECS. If you look at it directly, it is a bit dizzy. Below is a picture for everyone to take a look. If it is wrong, you can point it out.

 

Summary of PECS principles:

  • If you want to read data of type T from the collection and cannot write to it, you can use the? Extends wildcard; (Producer Extends)
  • If you want to write data of type T from the collection and do not need to read it, you can use the? Super wildcard; (Consumer Super)
  • If you want to save and retrieve, then don't use any wildcards.

 Then look at a classic application JDK 8 Collections.copy()

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

As you can see, src is the data source, assigned to dest, which is equivalent to src producing (Producer) data, and dest consumes (Customer) data. dest reassigns the data, the data type is T or a subclass of T, and of course it is also a subclass of the parent class of T. While src performs data removal, the type of the data returned is all T (there may be subclasses of T, but it is also T when it is up-converted). This source code is not difficult, but the ideas in it are still very classic. Everyone can take a look. This source code is not very cumbersome, everyone should be able to understand it, so I won’t write comments.

No sacrifice,no victory~

Guess you like

Origin blog.csdn.net/zsah2011/article/details/109130371