Home Java javaTutorial Java generics explained

Java generics explained

Apr 30, 2017 am 10:03 AM
java

Introduction

Generics are a very important knowledge point in Java. Generics are widely used in the Java collection class framework. In this article, we will look at the design of Java generics from scratch, which will involve wildcard processing and annoying type erasure.

Generic basics

Generic class

We first define a simple Box class:

public class Box {
    private String object;
    public void set(String object) { this.object = object; }
    public String get() { return object; }
}
Copy after login

This is the most common approach. A disadvantage of this is that only String type elements can be loaded into Box now. If we need to load other types of elements such as Integer in the future, we must rewrite another Box, and the code will be complicated. When it comes to reuse, using generics can solve this problem very well.

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}
Copy after login

In this way, our Box class can be reused, and we can replace T with any type we want:

Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
Copy after login

Generic method

After reading about generic classes, let’s take a look at generic methods. Declaring a generic method is very simple, just add a form like in front of the return type:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}
public class Pair<K, V> {
    private K key;
    private V value;
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}
Copy after login

We can call generic methods like this:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
Copy after login

Or use type inference in Java1.7/1.8 to let Java automatically deduce the corresponding type parameters:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
Copy after login

Boundary character

Now we want to implement such a function to find the number of elements in a generic array that is greater than a specific element. We can implement it like this:

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}
Copy after login

But this is obviously wrong, because except for primitive types such as short, int, double, long, float, byte, char, etc., other classes may not be able to use the operator >, so the compiler reports an error, so how to solve this problem Woolen cloth? The answer is to use boundary characters.

public interface Comparable<T> {
    public int compareTo(T o);
}
Copy after login

Making a statement similar to the following is equivalent to telling the compiler that the type parameter T represents classes that implement the Comparable interface, which is equivalent to telling the compiler that they all implement at least the compareTo method.

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}
Copy after login

Wildcard

Before understanding wildcards, we must first clarify a concept, or borrow the Box class we defined above, assuming we add a method like this:

public void boxTest(Box<Number> n) { /* ... */ }
Copy after login

So what types of parameters does Box n allow to accept now? Can we pass in Box or Box? The answer is no. Although Integer and Double are subclasses of Number, there is no relationship between Box or Box and Box in generics. This is very important. Next, let’s deepen our understanding through a complete example.

First we define a few simple classes, which we will use below:

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
Copy after login

In the following example, we create a generic class Reader, and then in f1() when we try Fruit f = fruitReader.readExact(apples); the compiler will report an error because there is a gap between List and List It doesn't have any relationship.

public class GenericReading {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit = Arrays.asList(new Fruit());
    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }
    static void f1() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        // Errors: List<Fruit> cannot be applied to List<Apple>.
        // Fruit f = fruitReader.readExact(apples);
    }
    public static void main(String[] args) {
        f1();
    }
}
Copy after login

But according to our usual thinking habits, there must be a connection between Apple and Fruit, but the compiler cannot recognize it. So how to solve this problem in generic code? We can solve this problem by using wildcard characters:

static class CovariantReader<T> {
    T readCovariant(List<? extends T> list) {
        return list.get(0);
    }
}
static void f2() {
    CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
    Fruit f = fruitReader.readCovariant(fruit);
    Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
    f2();
}
Copy after login

This is equivalent to telling the compiler that the parameters accepted by the readCovariant method of fruitReader only need to be a subclass of Fruit (including Fruit itself), so that the relationship between the subclass and the parent class is also related.

PECS principle

Above we saw usage similar to , using which we can get elements from the list, so can we add elements to the list? Let’s try it:

public class GenericsAndCovariance {
    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error: can&#39;t add any type of object:
        // flist.add(new Apple())
        // flist.add(new Orange())
        // flist.add(new Fruit())
        // flist.add(new Object())
        flist.add(null); // Legal but uninteresting
        // We Know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
}
Copy after login

The answer is no, the Java compiler does not allow us to do this, why? We might as well consider this issue from the perspective of the compiler. Because List flist itself can have multiple meanings:

List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
Copy after login
  • When we try to add an Apple, flist may point to new ArrayList();

  • ## When we try to add an Orange, flist may point to new ArrayList();

  • # When we try to add a Fruit, the Fruit can be any type of Fruit, and flist may only want a specific type of Fruit. The compiler cannot recognize it and will report an error.
  • Therefore, the collection class that implements

    What should we do if we want to add elements? You can use :

    public class GenericWriting {
        static List<Apple> apples = new ArrayList<Apple>();
        static List<Fruit> fruit = new ArrayList<Fruit>();
        static <T> void writeExact(List<T> list, T item) {
            list.add(item);
        }
        static void f1() {
            writeExact(apples, new Apple());
            writeExact(fruit, new Apple());
        }
        static <T> void writeWithWildcard(List<? super T> list, T item) {
            list.add(item)
        }
        static void f2() {
            writeWithWildcard(apples, new Apple());
            writeWithWildcard(fruit, new Apple());
        }
        public static void main(String[] args) {
            f1(); f2();
        }
    }
    Copy after login

      这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List list,它可以有下面几种含义:

    List<? super Apple> list = new ArrayList<Apple>();
    List<? super Apple> list = new ArrayList<Fruit>();
    List<? super Apple> list = new ArrayList<Object>();
    Copy after login

      当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。

      根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:

    • “Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用? extends T。

    • “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用? super T。

    • 如果需要同时读取以及写入,那么我们就不能使用通配符了。

      如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

    public class Collections {
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            for (int i=0; i<src.size(); i++)
                dest.set(i, src.get(i));
        }
    }
    Copy after login

     类型擦除

      Java泛型中最令人苦恼的地方或许就是类型擦除了,特别是对于有C++经验的程序员。类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型。

      说了这么多,那么泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:

    public class Node<T> {
        private T data;
        private Node<T> next;
        public Node(T data, Node<T> next) }
            this.data = data;
            this.next = next;
        }
        public T getData() { return data; }
        // ...
    }
    Copy after login

      编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:

    public class Node {
        private Object data;
        private Node next;
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
        public Object getData() { return data; }
        // ...
    }
    Copy after login

      这意味着不管我们声明Node还是Node,到了运行期间,JVM统统视为Node。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:

    public class Node<T extends Comparable<T>> {
        private T data;
        private Node<T> next;
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
        public T getData() { return data; }
        // ...
    }
    Copy after login

      这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:

    public class Node {
        private Comparable data;
        private Node next;
        public Node(Comparable data, Node next) {
            this.data = data;
            this.next = next;
        }
        public Comparable getData() { return data; }
        // ...
    }
    Copy after login

      上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。

      问题一

      在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:

    List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error
    Copy after login

      为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。

      我们先来看一下下面这个例子:

    Object[] strings = new String[2];
    strings[0] = "hi";   // OK
    strings[1] = 100;    // An ArrayStoreException is thrown.
    Copy after login

      对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:

    Object[] stringLists = new List<String>[];  // compiler error, but pretend it&#39;s allowed
    stringLists[0] = new ArrayList<String>();   // OK
    // An ArrayStoreException should be thrown, but the runtime can&#39;t detect it.
    stringLists[1] = new ArrayList<Integer>();
    Copy after login

      假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayList()和new ArrayList()的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉。

      如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:

    public class ErasedTypeEquivalence {
        public static void main(String[] args) {
            Class c1 = new ArrayList<String>().getClass();
            Class c2 = new ArrayList<Integer>().getClass();
            System.out.println(c1 == c2); // true
        }
    }
    Copy after login

      问题二

      继续复用我们上面的Node的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。

    public class Node<T> {
        public T data;
        public Node(T data) { this.data = data; }
        public void setData(T data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    public class MyNode extends Node<Integer> {
        public MyNode(Integer data) { super(data); }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }
    Copy after login

      看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:

    public class Node {
        public Object data;
        public Node(Object data) { this.data = data; }
        public void setData(Object data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    public class MyNode extends Node {
        public MyNode(Integer data) { super(data); }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }
    Copy after login

      实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException异常,提示String无法转换成Integer:

    MyNode mn = new MyNode(5);
    Node n = mn; // A raw type - compiler throws an unchecked warning
    n.setData("Hello"); // Causes a ClassCastException to be thrown.
    // Integer x = mn.data;
    Copy after login

      如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)方法,所以只能调用父类Node的setData(Object data)方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException到底是怎么抛出的?

      实际上Java编译器对上面代码自动还做了一个处理:

    class MyNode extends Node {
        // Bridge method generated by the compiler
        public void setData(Object data) {
            setData((Integer) data);
        }
        public void setData(Integer data) {
            System.out.println("MyNode.setData");
            super.setData(data);
        }
        // ...
    }
    Copy after login

      这也就是为什么上面会报错的原因了,setData((Integer) data);的时候String无法转换成Integer。所以上面第2行编译器提示unchecked warning的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Node n = mn就好了,这样编译器就可以提前帮我们发现错误。

      问题三

      正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:

    public static <E> void append(List<E> list) {
        E elem = new E();  // compile-time error
        list.add(elem);
    }
    Copy after login

      但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:

    public static <E> void append(List<E> list, Class<E> cls) throws Exception {
        E elem = cls.newInstance();   // OK
        list.add(elem);
    }
    Copy after login

      我们可以像下面这样调用:

    List<String> ls = new ArrayList<>();
    append(ls, String.class);
    Copy after login

      实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。

      问题四

      我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayList和ArrayList的之间的区别:

    public static <E> void rtti(List<E> list) {
        if (list instanceof ArrayList<Integer>) {  // compile-time error
            // ...
        }
    }
    => { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }
    Copy after login

      和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:

    public static void rtti(List<?> list) {
        if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
            // ...
        }
    }
    Copy after login

                   

    The above is the detailed content of Java generics explained. For more information, please follow other related articles on the PHP Chinese website!

    Statement of this Website
    The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

    Hot AI Tools

    Undresser.AI Undress

    Undresser.AI Undress

    AI-powered app for creating realistic nude photos

    AI Clothes Remover

    AI Clothes Remover

    Online AI tool for removing clothes from photos.

    Undress AI Tool

    Undress AI Tool

    Undress images for free

    Clothoff.io

    Clothoff.io

    AI clothes remover

    Video Face Swap

    Video Face Swap

    Swap faces in any video effortlessly with our completely free AI face swap tool!

    Hot Tools

    Notepad++7.3.1

    Notepad++7.3.1

    Easy-to-use and free code editor

    SublimeText3 Chinese version

    SublimeText3 Chinese version

    Chinese version, very easy to use

    Zend Studio 13.0.1

    Zend Studio 13.0.1

    Powerful PHP integrated development environment

    Dreamweaver CS6

    Dreamweaver CS6

    Visual web development tools

    SublimeText3 Mac version

    SublimeText3 Mac version

    God-level code editing software (SublimeText3)

    Java Spring Interview Questions Java Spring Interview Questions Aug 30, 2024 pm 04:29 PM

    In this article, we have kept the most asked Java Spring Interview Questions with their detailed answers. So that you can crack the interview.

    Break or return from Java 8 stream forEach? Break or return from Java 8 stream forEach? Feb 07, 2025 pm 12:09 PM

    Java 8 introduces the Stream API, providing a powerful and expressive way to process data collections. However, a common question when using Stream is: How to break or return from a forEach operation? Traditional loops allow for early interruption or return, but Stream's forEach method does not directly support this method. This article will explain the reasons and explore alternative methods for implementing premature termination in Stream processing systems. Further reading: Java Stream API improvements Understand Stream forEach The forEach method is a terminal operation that performs one operation on each element in the Stream. Its design intention is

    TimeStamp to Date in Java TimeStamp to Date in Java Aug 30, 2024 pm 04:28 PM

    Guide to TimeStamp to Date in Java. Here we also discuss the introduction and how to convert timestamp to date in java along with examples.

    Java Program to Find the Volume of Capsule Java Program to Find the Volume of Capsule Feb 07, 2025 am 11:37 AM

    Capsules are three-dimensional geometric figures, composed of a cylinder and a hemisphere at both ends. The volume of the capsule can be calculated by adding the volume of the cylinder and the volume of the hemisphere at both ends. This tutorial will discuss how to calculate the volume of a given capsule in Java using different methods. Capsule volume formula The formula for capsule volume is as follows: Capsule volume = Cylindrical volume Volume Two hemisphere volume in, r: The radius of the hemisphere. h: The height of the cylinder (excluding the hemisphere). Example 1 enter Radius = 5 units Height = 10 units Output Volume = 1570.8 cubic units explain Calculate volume using formula: Volume = π × r2 × h (4

    PHP vs. Python: Understanding the Differences PHP vs. Python: Understanding the Differences Apr 11, 2025 am 12:15 AM

    PHP and Python each have their own advantages, and the choice should be based on project requirements. 1.PHP is suitable for web development, with simple syntax and high execution efficiency. 2. Python is suitable for data science and machine learning, with concise syntax and rich libraries.

    PHP: A Key Language for Web Development PHP: A Key Language for Web Development Apr 13, 2025 am 12:08 AM

    PHP is a scripting language widely used on the server side, especially suitable for web development. 1.PHP can embed HTML, process HTTP requests and responses, and supports a variety of databases. 2.PHP is used to generate dynamic web content, process form data, access databases, etc., with strong community support and open source resources. 3. PHP is an interpreted language, and the execution process includes lexical analysis, grammatical analysis, compilation and execution. 4.PHP can be combined with MySQL for advanced applications such as user registration systems. 5. When debugging PHP, you can use functions such as error_reporting() and var_dump(). 6. Optimize PHP code to use caching mechanisms, optimize database queries and use built-in functions. 7

    Create the Future: Java Programming for Absolute Beginners Create the Future: Java Programming for Absolute Beginners Oct 13, 2024 pm 01:32 PM

    Java is a popular programming language that can be learned by both beginners and experienced developers. This tutorial starts with basic concepts and progresses through advanced topics. After installing the Java Development Kit, you can practice programming by creating a simple "Hello, World!" program. After you understand the code, use the command prompt to compile and run the program, and "Hello, World!" will be output on the console. Learning Java starts your programming journey, and as your mastery deepens, you can create more complex applications.

    How to Run Your First Spring Boot Application in Spring Tool Suite? How to Run Your First Spring Boot Application in Spring Tool Suite? Feb 07, 2025 pm 12:11 PM

    Spring Boot simplifies the creation of robust, scalable, and production-ready Java applications, revolutionizing Java development. Its "convention over configuration" approach, inherent to the Spring ecosystem, minimizes manual setup, allo

    See all articles