Java Generics Tutorial – Example Class, Interface, Methods, Wildcards and much more

Genrics is one of the core feature of Java programming and it was introduced in Java 5. If you have been working on Java Collections and with version 5 or higher, I am sure that you have used it. Using generics with collection classes is very easy but it provides a lot more features than just creating the type of collection and we will try to learn features of generics in this article. Understanding generics can become confusing sometimes if we go with jargon words, so I would try to keep it simple and easy to understand.

泛型详解:

java-generics-tutorial

We will look into below topics of generics in this tutorial.

  1. Java Generics Example
  2. Generics with Class and Interfaces
  3. Generics Type Naming Convention
  4. Generics in Methods and Constructors
  5. Generics Bounded Type Parameters
  6. Generics and Inheritance
  7. Generic Classes and Subtyping
  8. Generics Wildcards
    1. Generics Upper Bounded Wildcard
    2. Generics Unbounded Wildcard
    3. Generics Lower bounded Wildcard
  9. Subtyping using Generics Wildcard
  10. Type Erasure

Java Generics Example

Generics was added in Java 5 to provide compile-time type checking and removing risk ofClassCastException that was common while working with collection classes. The whole collection framework was re-written to use generics for type-safety. Let’s see how generics help us using collection classes safely.

1
2
3
4
5
6
7
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
 
for(Object obj : list){
        String str=(String) obj; //type casting leading to ClassCastException at runtime
}

Above code compiles fine but throws ClassCastException at runtime because we are trying to cast Object in the list to String whereas one of the element is of type Integer. After Java 5, we use collection classes like below.

1
2
3
4
5
6
7
List<String> list1 = new ArrayList<String>(); // java 7 ? List<String> list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); //compiler error
 
for(String str : list1){
     //no type casting needed, avoids ClassCastException
}

Notice that at the time of list creation, we have specified that the type of elements in the list will be String. So if we try to add any other type of object in the list, the program will throw compile time error. Also notice that in for loop, we don’t need type casting of the element in the list, hence removing the ClassCastException at runtime.

Generics with Class and Interfaces

We can define our own classes and interfaces with generics type. A generic type is a class or interface that is parameterized over types. We use angle brackets (<>) to specify the type parameter.

To understand the benefit, lets say we have a simple class as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.journaldev.generics;
 
public class GenericsTypeOld {
 
    private Object t;
 
    public Object get() {
        return t;
    }
 
    public void set(Object t) {
        this.t = t;
    }
 
        public static void main(String args[]){
        GenericsTypeOld type = new GenericsTypeOld();
        type.set("Pankaj");
        String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
    }
}

Notice that while using this class, we have to use type casting and it can produce ClassCastException at runtime. Now we will use generics to rewrite the same class with generics type as shown below.

GenericType.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.journaldev.generics;
 
public class GenericsType<T> {
 
    private T t;
     
    public T get(){
        return this.t;
    }
     
    public void set(T t1){
        this.t=t1;
    }
     
    public static void main(String args[]){
        GenericsType<String> type = new GenericsType<>();
        type.set("Pankaj"); //valid
         
        GenericsType type1 = new GenericsType(); //raw type
        type1.set("Pankaj"); //valid
        type1.set(10); //valid and autoboxing support
    }
}

Notice the use of GenericsType class in the main method. We don’t need to do type-casting and we can remove ClassCastException at runtime. If we don’t provide the type at the time of creation, compiler will produce a warning that “GenericsType is a raw type. References to generic type GenericsType<T> should be parameterized”. When we don’t provide type, the type becomes Object and hence it’s allowing both String and Integer objects but we should always try to avoid this because we will have to use type casting while working on raw type that can produce runtime errors.

Tip: We can use @SuppressWarnings("rawtypes") annotation to suppress the compiler warning, check out java annotations tutorial.

Also notice that it supports java autoboxing.

Comparable interface is a great example of Generics in interfaces and it’s written as:

1
2
3
4
5
6
package java.lang;
import java.util.*;
 
public interface Comparable<T> {
    public int compareTo(T o);
}

In similar way, we can use generics in our interfaces and classes. We can also have multiple type parameters as in Map interface. Again we can provide parameterized value to a parameterized type also, for examplenew HashMap<String, List<String>>(); is valid.

Generics Type Naming Convention

Naming convention helps us understanding code easily and having a naming convention is one of the best practices of java programming language. So generics also comes with it’s own naming conventions. Usually type parameter names are single, uppercase letters to make it easily distinguishable from java variables. The most commonly used type parameter names are:

  • E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
  • K – Key (Used in Map)
  • N – Number
  • T – Type
  • V – Value (Used in Map)
  • S,U,V etc. – 2nd, 3rd, 4th types

注:上述即为泛型类型的命名约定,了解一下即可

Generics in Methods and Constructors

Sometimes we don’t want whole class to be parameterized, in that case we can use generics type in methods also. Since constructor is a special kind of method, we can use generics type in constructors too.

Here is a class showing example of generics type in method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.journaldev.generics;
 
public class GenericsMethods {
 
    //Generics in method泛型方法:注意格式signature
    public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){
        return g1.get().equals(g2.get());
    }
     
    public static void main(String args[]){
        GenericsType<String> g1 = new GenericsType<>();
        g1.set("Pankaj");
         
        GenericsType<String> g2 = new GenericsType<>();
        g2.set("Pankaj");
         
        boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);//当调用方法时,在方法名前的尖括号中放入具体的类型
        //大多数情况下,方法调用中可以省略<String>参数:GenericsMethods.isEqual(g1, g2);
    //above statement can be written simply as
        isEqual = GenericsMethods.isEqual(g1, g2);
        //This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets.
        //Compiler will infer the type that is needed
    }
}

Notice the isEqual method signature showing syntax to use generics type in methods. Also notice how to use these methods in our java program. We can specify type while calling these methods or we can invoke them like a normal method. Java compiler is smart enough to determine the type of variable to be used, this facility is called as type inference.

Generics Bounded Type Parameters

Suppose we want to restrict the type of objects that can be used in the parameterized type, for example in a method that compares two objects and we want to make sure that the accepted objects are Comparables. To declare a bounded type parameter, list the type parameter’s name, followed by the extends keyword, followed by its upper bound(上界), similar like below method.

1
2
3
public static <T extends Comparable<T>> int compare(T t1, T t2){
        return t1.compareTo(t2);
    }

注:此处可能会有一些奇怪,为什么使用extends而不是implements呢?毕竟,Comparable是一个接口。该符号<T extends BoundingType>表示T是绑定类型的子类型(subtype).T和绑定类型可以是类,也可以是接口。选择关键字extends是因为它更接近子类的概念。

The invocation of these methods is similar to unbounded method except that if we will try to use any class that is not Comparable, it will throw compile time error.

Bounded type parameters can be used with methods as well as classes and interfaces.

Generics supports multiple bounds also, i.e <T extends A & B & C>. In this case A can be an interface or class. If A is class then B and C should be interfaces. We can’t have more than one class in multiple bounds.

Generics and Inheritance

We know that Java inheritance allows us to assign a variable A to another variable B if A is subclass of B. So we might think that any generic type of A can be assigned to generic type of B, but it’s not the case. Lets see this with a simple program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.journaldev.generics;
 
public class GenericsInheritance {
 
    public static void main(String[] args) {
        String str = "abc";
        Object obj = new Object();
        obj=str; // works because String is-a Object, inheritance in java
         
        MyClass<String> myClass1 = new MyClass<String>();
        MyClass<Object> myClass2 = new MyClass<Object>();
        //myClass2=myClass1; // compilation error since MyClass<String> is not a MyClass<Object>
        obj = myClass1; // MyClass<T> parent is Object
    }
     
    public static class MyClass<T>{}
 
}

We are not allowed to assign MyClass<String> variable to MyClass<Object> variable because they are not related, in fact MyClass<T> parent is Object.注:此处把握一点:无论S和T有什么关系,通常MyClass<S>和MyClass<T>没有任何关系

下面Post一段java core上的介绍来进一步认识为什么要这样做:

转换成原始类型后会出错:

Generic Classes and Subtyping(子类型化)

We can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.

For example, ArrayList<E> implements List<E> that extends Collection<E>, so ArrayList<String> is a subtype of List<String> and List<String> is subtype of Collection<String>.

The subtyping relationship is preserved as long as we don’t change the type argument, below shows an example of multiple type parameters.

1
2
interface MyList<E,T> extends List<E>{
}

The subtypes of List<String> can be MyList<String,Object>, MyList<String,Integer> and so on.

Generics Wildcards(通配符)

Question mark (?) is the wildcard in generics and represent an unknown type. The wildcard can be used as the type of a parameter, field, or local variable and sometimes as a return type. We can’t use wildcards while invoking a generic method or instantiating a generic class. In following sections, we will learn about upper bounded wildcards, lower bounded wildcards, and wildcard capture.

Generics Upper Bounded Wildcard

Upper bounded wildcards are used to relax the restriction on the type of variable in a method. Suppose we want to write a method that will return the sum of numbers in the list, so our implementation will be something like this.

1
2
3
4
5
6
7
public static double sum(List<Number> list){
        double sum = 0;
        for(Number n : list){
            sum += n.doubleValue();
        }
        return sum;
    }

Now the problem with above implementation is that it won’t work with List of Integers or Doubles because we know that List<Integer> and List<Double> are not related, this is when upper bounded wildcard is helpful. We use generics wildcard with extends keyword and the upper bound class or interface that will allow us to pass argument of upper bound or it’s subclasses types.也就是说传入的参数只能是我们限定的上界参数类型或其子类,而不能是限定参数类型的超类

The above implementation can be modified like below program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.journaldev.generics;
 
import java.util.ArrayList;
import java.util.List;
 
public class GenericsWildcards {
 
    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>();
        ints.add(3); ints.add(5); ints.add(10);
        double sum = sum(ints);
        System.out.println("Sum of ints="+sum);
    }
 
    public static double sum(List<? extends Number> list){
        double sum = 0;
        for(Number n : list){
            sum += n.doubleValue();
        }
        return sum;
    }
}

It’s similar like writing our code in terms of interface, in above method we can use all the methods of upper bound class Number. Note that with upper bounded list, we are not allowed to add any object to the list except null. If we will try to add an element to the list inside the sum method, the program won’t compile.(不允许向列表中添加元素)注:参照java core相应篇章理解更加清晰,此处不在赘述

Generics Unbounded Wildcard(无限定通配符)

Sometimes we have a situation where we want our generic method to be working with all types, in this case unbounded wildcard can be used. Its same as using <? extends Object>.

1
2
3
4
5
public static void printData(List<?> list){
        for(Object obj : list){
            System.out.print(obj + "::");
        }
    }

We can provide List<String> or List<Integer> or any other type of Object list argument to the printDatamethod. Similar to upper bound list, we are not allowed to add anything to the list.

Generics Lower bounded Wildcard

Suppose we want to add Integers to a list of integers in a method, we can keep the argument type as List<Integer> but it will be tied up with Integers whereas List<Number> and List<Object> can also hold integers, so we can use lower bound wildcard to achieve this. We use generics wildcard (?) with superkeyword and lower bound class to achieve this.

We can pass lower bound or any super type of lower bound as an argument in this case, java compiler allows to add lower bound object types to the list.

1
2
3
public static void addIntegers(List<? super Integer> list){
        list.add(new Integer(50));
    }

Subtyping using Generics Wildcard

1
2
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

Type Erasure(类型擦除)

Generics was added to provide type-checking at compile time and it has no use at run time, so java compiler uses type erasure feature to remove all the generics type checking code in byte code and insert type-casting if necessary. Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.

For example if we have a generic class like below;

1
2
3
4
5
6
7
8
9
10
11
12
public class Test<T extends Comparable<T>> {
 
    private T data;
    private Test<T> next;
 
    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }
 
    public T getData() { return this.data; }
}

The Java compiler replaces the bounded type parameter T with the first bound interface, Comparable, as below code:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
 
    private Comparable data;
    private Test next;
 
    public Node(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }
 
    public Comparable getData() { return data; }
}

Further Reading:
Generics doesn’t support sub-typing, so List<Number> numbers = new ArrayList<Integer>(); will not compile, learn why generics doesn’t support sub-typing.

We can’t create generic array, so List[] array = new ArrayList[10] will not compile, read why we can’t create generic array?.

Thats all for generics in java, generics is a really vast topic and requires a lot of time to understand and use it effectively. This post here is an attempt to provide basic details of generics and how can we use it to extend our program with type-safety.

为了进一步理解泛型相关的类型擦除,此处特引用一篇博文辅助理解,博主讲解的还是比较清晰的:(ps:转载于http://www.infoq.com/cn/articles/cf-java-generics/)

Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。对于泛型概念的引入,开发社区的观点是褒贬不一。 从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。而从不 好的地方来说,为了保证与旧有版本的兼容性,Java泛型的实现上存在着一些不够优雅的地方。当然这也是任何有历史的编程语言所需要承担的历史包袱。后续 的版本更新会为早期的设计缺陷所累。  

开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List<Object>作为形式参数,那么如果 尝试将一个List<String>的对象作为实际参数传进去,却发现无法通过编译。虽然从直觉上来说,Object是String的父类, 这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。本文试图对Java泛型做一个概括性的说明。

类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译 器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在 编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的 地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

  • 泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
  • 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是 无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说, 它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上 界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

class MyString implements Comparable<String> {
    public int compareTo(String str) {        
        return 0;    
    }
} 

当类型信息被擦除之后,上述类的声明变成了class MyString implements Comparable。但是这样的话,类MyString就会有编译错误,因为没有实现接口Comparable声明的int compareTo(Object)方法。这个时候就由编译器来动态生成这个方法。

实例分析

了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的List<Object>和List<String>为例来具体分析:

public void inspect(List<Object> list) {    
    for (Object obj : list) {        
        System.out.println(obj);    
    }    
    list.add(1); //这个操作在当前方法的上下文是合法的。 
}
public void test() {    
    List<String> strs = new ArrayList<String>();    
    inspect(strs); //编译错误 
}  

这段代码中,inspect方法接受List<Object>作为参数,当在test方法中试图传入 List<String>的时候,会出现编译错误。假设这样的做法是允许的,那么在inspect方法就可以通过list.add(1)来向 集合中添加一个数字。这样在test方法看来,其声明为List<String>的集合中却被添加了一个Integer类型的对象。这显然是 违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。 

通配符与上下界

在使用泛型类的时候,既可以指定一个具体的类型,如List<String>就声明了具体的类型是String;也可以用通配符?来表 示未知类型,如List<?>就声明了List中包含的元素类型是未知的。 通配符所代表的其实是一组类型,但具体的类型是未知的。List<?>所声明的就是所有类型都是可以的。但是List<?>并不 等同于List<Object>。List<Object>实际上确定了List中包含的是Object及其子类,在使用的时候 都可以通过Object来进行引用。而List<?>则其中所包含的元素类型是不确定。其中可能包含的是String,也可能是 Integer。如果它包含了String的话,往里面添加Integer类型的元素就是错误的。正因为类型未知,就不能通过new ArrayList<?>()的方法来创建一个新的ArrayList对象。因为编译器无法知道具体的类型是什么。但是对于 List<?>中的元素确总是可以用Object来引用的,因为虽然类型未知,但肯定是Object及其子类。考虑下面的代码:

public void wildcard(List<?> list) {
    list.add(1);//编译错误 
}  

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

因为对于List<?>中的元素只能用Object来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范 围。 如List<? extends Number>说明List中可能包含的元素类型是Number及其子类。而List<? super Number>则说明List中包含的是Number及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number>的时候,就可以使用Number类的intValue等方法。

类型系统

在Java中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如String继承自Object。根据Liskov替换原则, 子类是可以替换父类的。当需要Object类的引用的时候,如果传入一个String对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用 的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的。 String[]可以替换Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的List<String>是 不能替换掉List<Object>的。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:

  • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>是 Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
  • 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。
  • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把List<Object>改成List<?>即可。List<String>是List<?>的子类型,因此传递参数时不会发生错误。

开发自己的泛型类

泛型类与一般的Java类基本相同,只是在类和接口定义上多出来了用<>声明的类型参数。一个类可以有多个类型参数,如 MyClass<X, Y, Z>。 每个类型参数在声明的时候可以指定上界。所声明的类型参数在Java类中可以像一般的类型一样作为方法的参数和返回值,或是作为域和局部变量的类型。但是 由于类型擦除机制,类型参数并不能用来创建对象或是作为静态变量的类型。考虑下面的泛型类中的正确和错误的用法。

class ClassTest<X extends Number, Y, Z> {    
    private X x;    
    private static Y y; //编译错误,不能用在静态变量中    
    public X getFirst() {
        //正确用法        
        return x;    
    }    
    public void wrong() {        
        Z z = new Z(); //编译错误,不能创建对象    
    }
}  

最佳实践

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

  • 在代码中避免泛型类和原始类型的混用。比如List<String>和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
  • 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
  • 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类 即可。
  • 不要忽视编译器给出的警告信息。
posted @ 2014-04-21 18:06  CoolRandy  阅读(756)  评论(0)    收藏  举报