Java泛型

泛型(Generic Type)是Java中重要的一部分。在J2SE 5.0之后新增。在使用Java标准库中的内容的时候,经常会遇到泛型。这里将泛型总结一下

什么是泛型

  • 讨论一个内容的时候,首先会来说什么是什么。在官方的文档中说到

A generic type is a generic class or interface that is parameterized over types.

  • 泛型又可以称作参数化类型,这是在Java SE新增添的特性。一对尖括号,中间包含类型信息。将类型独立成参数,在使用的时候才指定实际的类型

如果没有泛型会怎么样?我们考虑以下几种情况:

  1. 你实现了一个存储整数类型(Integer)的列表,这时候你又需要存储字符串(String)的列表,两种列表逻辑行为完全一样,只是存储的类型不同

  2. 为了保证列表的通用性,你将列表的类型改为了Object,这样就不用为类型修改代码了。但是每次从列表中取对象的时候都需要强制转换,而且很很容易出错

    • 有了泛型之后,可以将逻辑相同类型不同的代码独立出来,由编译器负责进行类型转换

泛型的声明

泛型方法(Generic Method)
  • 泛型方法是在普通方法声明上加入了泛型
public static < E > void printArray( E[] inputArray ){
    for ( E element : inputArray ){        
        System.out.printf( "%s ", element );
    }
    System.out.println();
}
  • 调用:
// Create arrays of Integer, Double and Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "Array integerArray contains:" );
printArray( intArray  ); // pass an Integer array
System.out.println( "\nArray doubleArray contains:" );
printArray( doubleArray ); // pass a Double array
System.out.println( "\nArray characterArray contains:" );
printArray( charArray ); // pass a Character array
  • 输出:

Array integerArray contains:
1 2 3 4 5 6
Array doubleArray contains:
1.1 2.2 3.3 4.4
Array characterArray contains:
H E L L O

Java泛型方法的声明格式如下:

[权限] [修饰符] [泛型] [返回值] [方法名]  (    [参数列表]   ) {}
public static  < E >   void   printArray( E[] inputArray ) {}
  • 泛型的声明,必须在方法的修饰符(public,static,final,abstract等)之后,返回值声明之前。可以声明多个泛型,用逗号隔开。泛型的声明要用<>包裹

泛型方法的使用有两种:

类型推导
  • 以声明键值对的例子来说,通常的写法会有一长串,不免有些痛苦
Map<String, List<String>> m = new HashMap<String, List<String>>();
  • 我们可以构造一个泛型方法作为静态工厂,来完成这一操作
public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
Map<String, List<String>> m = newInstance();
  • 编译器在编译代码的时候推导出了K, V分别对应的类型。当然,编译器的推导能力也是有限的,这里也就不过多讨论了
指定类型
  • 不赘述
泛型类(Generic Class)
  • 泛型类和普通类的声明一样,只是在类名后面加上了类型表示。就像泛型方法,泛型类可以有一个或多个类型表示,用逗号进行分隔
public class Box<T> {
  private T t;
  public void add(T t) {
    this.t = t;
  }
  public T get() {
    return t;
  }
  public static void main(String[] args) {
     Box<Integer> integerBox = new Box<Integer>();
     Box<String> stringBox = new Box<String>();

     integerBox.add(new Integer(10));
     stringBox.add(new String("Hello World"));
     System.out.printf("Integer Value :%d\n\n", integerBox.get());
     System.out.printf("String Value :%s\n", stringBox.get());
  }
}

Integer Value :10
String Value :Hello World

  • 在泛型类上声明的类型,可以被用到类中任何表示类型的地方

  • 泛型类只能通过以指定类型的方式进行使用。在之后的Java版本中,加入了类型推导功能,可以将后面的泛型类型省略,但是还是需要保留尖括号

List<String> list = new ArrayList<String>(); // 普通的写法
List<String> list = new ArrayList<>(); // 省略的写法
泛型接口(Generic Interface)
  • 泛型接口是在声明接口的时候指定,类在继承接口的时候需要补充泛型类型
public interface Info<T> {
    public T getInfo();
}
  • 然后定义一个类实现这个接口
public InfoImp implements Info<String> {
    public String getInfo() {
        return "Hello World!";
    }
}
  • 可以发现实现接口里的方法需要使用具体的类型

泛型接口的一般格式:

[访问权限] interface [接口名] <泛型标识> {}
  • 当然,我们可以实现泛型接口的时候不指名泛型类型,这样这个类就需要定义为泛型类
public InfoImp<T> implements Info<T> {
    public T getInfo() {
        return null;
    }
}

泛型标识与泛型通配符

  • 理论上泛型标识可以使用任意的字母或字母序列,可以考虑以下的例子,但是不推荐这样使用
public static <STRING> STRING sayHello(STRING word){
    System.out.println("Hello " + word);
    return a;
}
  • 但是Java内部有一套自己的规范,这样在阅读JDK代码的时候会更加明确泛型标识的含义
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类型
S、U、V - 2nd、3rd、4th types
  • 说到泛型标识符,再说一说泛型通配符。常用的泛型通配符有三种
任意类型 “<”?>
  • “<”?> 可以理解为泛型中的 Object,为什么这么说呢?因为任意类型的通配符可以接受任意类型的泛型。下面的例子表示出了这种关系
Box<?> box = new Box<String>();
  • 类似于将后面的类型转换到前面的类型。但是
class Box<?> {} //错误的泛型类定义
public static <?> void sayHello(? helloString) {} //错误的泛型方法定义
interface Box<?> {} //错误的泛型接口定义
上限类型 “<”? extends 类>
  • “<”? extends 类> 表示泛型只能使用这个类或这个类的子类。举个例子
public static <T extends String> void sayHello(T helloString) {}
  • 在该方法中调用sayHello(“xiaoming”);是正确的,但是调用sayHello(2333);就是错误的

  • 考虑一种更通用的情况

public static void printList(List<? extends String> list){
    for(String str:list){
        System.out.println(str);
    }
}
  • 这个例子指定了泛型类的具体类型的范围。在JDK中经常可以看到这样的使用方法
下限类型 “<”? super 类>
  • 同上限方法,”<”? super 类> 表示泛型只能使用这个类或这个类的父类

类型擦除

  • 泛型只在编译时有效,编译成字节码的过程中会进行类型擦除的操作。当然并不是所有的泛型类型都被擦除,有些也会保留下来

  • 一个简单的类型擦除的例子:

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
   String s = it.next();
}
  • 我们对其编译,然后再反编译,反编译引擎用的CFR:
ArrayList arrayList = new ArrayList();
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
    String string = (String)iterator.next();
}
  • 从上面的结果可以看出,泛型类上面的类型被去掉了,但是增加了一个类型强制转换。解释器默认认为里面的类型都会是String。这是因为在编译的时候会进行类型检查,如果发现使用的类型与泛型声明类型不符,编译是不会通过的

  • 那能不能绕过这个检查呢?这个时候就需要使用反射来进行操作了。就上面的例子来说,ArrayList可以放入任意类型,所以使用反射只要保证类型强制转换不出问题,程序还是可以使用的

在Java文档中提到,类型擦除主要进行以下工作:

  1. 将泛型中的所有类型参数更换成类型界限,或者无界的类型替换成Object。所以生成的字节码只包含普通类、接口和方法

  2. 为了确保类型安全,必要时插入强制类型转换

  3. 生成桥接方法保持扩展泛型类型中的多态性

可变参数

  • 使用泛型方法可以使用可变参数:
public class Main {
    public static <T> void out(T... args) {
        for (T t : args) {
            System.out.println(t);
        }
    }
    public static void main(String[] args) {
        out("findingsea", 123, 11.11, true);
    }
}

new T()?

  • 能不能在泛型方法(类)中创建泛型类型的实例呢?答案是不可以的
public static <E> void append(List<E> list) {
    E elem = new E();  // 编译错误
    list.add(elem);
}
  • 不过可以使用反射来创建实例
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
  • 因为类型擦除的缘故,部分类型信息会丢失,我们在运行时不会获取到相应的类型,所以也就无法将该类型实例化成对象

泛型和数组

在Java中,直接创建泛型数组是非法的(要注意的一点是,Java中没有所谓的泛型数组一说)。泛型设计的初衷是为了简化程序员类型转换的操作,保证类型安全。数组是协变的,如果Sub为Super的子类型,那么数组Sub[]就是Super[]的子类型。这样做就很难保证存储上的安全

  • 但在实际使用过程中,往往需要创建泛型数组:
public static <E> E[] newArray(int n) {
    return new E[n];
}
  • 这个时候运行程序,会抛出异常

Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E

  • 在这种情况下,可以使用 List 来代替数组
public static <E> List<E> newList() {
    return new ArrayList<E>();
  • 但是,Java不是生来就有List的,如果遇到必须使用数组的情况该怎么办?

  • 在这里可以参考Java里聚合类型的实现,以一个简单的例子说明

public class Stack<E> {
    private E[] elements;
    private int size = 0;

    public Stack() {
        elements = new E[10];
    }

    public void push(E e) {
        elements[size++] = e;
    }

    public E pop() {
        E result = elements[--size];
        elements[size] = null;
        return result;
    }
}
  • 上面的例子和java.util.Stack还是有区别的,只是为了说明如何处理泛型数组问题

  • 同样运行的时候会出错

Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E

  • 一种方法是在创建泛型数组的时候创建一个Object数组,然后转换成E数组
elements = (E[]) new Object[10];
  • 第二种方法将数组类型改为Object,在弹出元素的时候进行转换
public class Stack<E> {
    private Object[] elements;
    ...
    public E pop() {
        E result = (E) elements[--size];
        ...
    }
}
  • 泛型数组的返回和传递
package generics;

public class GenericsArray {
  /**
   * 返回一个泛型数组
   * @param ts
   * @return
   */
  @SafeVarargs
  public static <T> T[] getArray(T...ts){
     return ts;
  }
  /**
   * 为方法传递一个泛型数组
   * @param param
   */
  public static <T> void print(T[] param){

     System.out.println("the Param is:");

     for(T c:param){

       System.out.print(c);

    }
  }
  public static void main(String[] args){

     Integer[] arr = getArray(1,2,3,4,5);

     System.out.println(arr);

     print(arr);
  }
}

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值