java 泛型

java之泛型

1.概述

泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。

直接来看一个简单的泛型类吧。

public class People<T> {
    T first;

    public T getFirst() {
        return first;
    }

    public People(T first) {
        this.first = first;
    }
}

在这个泛型类中,与一个普通的java类的区别在:

  • 类名后多了
  • 变量first的类型是T。

这里这个T就是类型参数,T可以是Java的8种基本数据类型,也可以是自己定义的类。这里使用类型参数T,可以使得People这个类与它所能操作的数据类型不在绑定在一起,这个类可以用于多种数据类型,实现了代码的复用,降低了耦合。

关于泛型类的使用大家可以自行学习,这个比较简单,网上也有很多资料,这里就不再赘述了。

2.泛型的基本原理

泛型是java 5引入的一个新特性,那么在java 5以前,java程序员是怎么表示可变类型的呢?答案是使用Object类。

public class People {
    Object first;

    public Object getFirst() {
        return first;
    }

    public People(Object first) {
        this.first = first;
    }
}

这就是java 5以前java程序员表示可变类型的方法,同时,这也是java泛型内部的实现原理。

People people = new People(5);
Integer first = (Integer) people.getFirst();
//java5以前这样使用可变类型的类

对于泛型类,java编译器会将泛型代码转换为普通的非泛型代码,就像上面的代码这样,将类型参数T擦除,转换为Object,插入必要的类型转换。这个过程称为类型擦除。原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换T。

也就是说,java泛型是通过类型擦除实现的,类定义的类型参数T最后都会被替换为Object,在程序运行过程中,不知道泛型的实际类型参数。这么设计是为了兼容java 5以前的代码,因此java的泛型会有一些限制,这在后面再说。

3.泛型的好处

泛型的好处除了前面提到的代码重用,还有就是拥有很好的安全性,以及很好的可读性。

People people = new People("people");
int first = (Integer) people.getFirst();

如上,使用Object时,java编译器对于这样的类型转换的错误是检测不到的,类型转换错误只能在程序运行时才会被抛出。

但使用泛型后,一旦出现上面的类型转换错误后,一些开发环境就可以检测出这个错误,进而提示类型错误。或者在编译时,编译器也会提示。这称之为类型安全。除此之外,还可以省去强制类型转换,再加上明确的类型信息,使代码的可读性更好。

  • 泛型方法:类可以是泛型的,那么方法也可以是泛型的,并且泛型方法与所在的类是否是泛型类无关。
public static <T> T indexOf(T[] arr){
    return arr[arr.length/2];
}

类型变量放在修饰符后面,在返回类型前面。

  • 泛型接口:接口也可以是泛型的,java里面的Comparable和Comparator接口都是泛型的。
public interface Comparable<T> {
	public int compareTo(T o);
}
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

实现泛型接口时应该指定具体的类型。例如:

public final class Integer extends Number implements Comparable<Integer> {
    //这表明Integer只能与Integer对象比较
}
4.类型参数的限定

java 支持限定类型参数的上限,也就是说参数必须是给定的上界类型或其子类型。上界的限定通过extends关键字来表示,这个上界可以是某个具体的类,或某个具体的接口,或其他的类型参数。

限定类型后,如果类型使用错误 ,编译器会提示。指定边界后,类型擦除时就不会转化为Object了,而是会转化为他的边界类型。

public class People<T extends Comparable<T>>	//可以解读为T必须实现了Comparable接口
											//并且可以与相同类型的元素进行比较

在java泛型中,无论T与S有什么关系,通常,People< T>与People< S>是没有联系的。因此不能因为T是S的子类,就将People< T>的对象赋值给People< S>的变量。

5.通配符

1.举个例子,Integer是Number的子类,现在要让People的对象赋值给People的变量。根据上面的说明,我们知道用一般的方法是行不通的。我们可以使用通配符类型来解决这个问题,通配符的写法如下:

public static void addAll(People<? extends Number> p)//<?extends T>这种形式称为有限定通配符

我们先来解释一下这个称为有限定的通配符吧,

用来实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体的类型是未知的(用?来表示),只知道这个具体的类型是E或者E的某个子类型。

在上面的这行代码,可以解释为:任何泛型People类型,它的类型参数是Number的子类,如People

我们来看看怎么使用通配符解决之前的问题。

People<Integer> intPeople = new People<Integer>();
People<? extends Number> numberPeople = intPeople;//这里是不会报错的

2.除此之外,还有无限定通配符<?>。先来看看它的写法,

public void swap(People<?> p ,int i,int j)//使用通配符的
    
public <T> void swap(People<T> p ,int i,int j)//普通的类型参数

通配符形式更为简洁,但需要注意的是,通配符不是类型变量,不能作为类型来使用,通俗来讲就是只能读,不能写。

People<Integer> intPeople = new People<Integer>();
People<? extends Number> numberPeople = intPeople;
        
Integer a = 200;
numberPeople.setFirst(a);			//错误!
numberPeople.setFirst((Number) a);	 //错误!
numberPeople.setFirst((Object) a);	 //错误!

? extends Number 表示是Number的某个子类型,但不知道具体的类型,如果允许写入,java就无法保证类型安全性,所以干脆禁止。

大部分情况下,这种限制是好的,但这又会出现一些问题。

public void swap(People<?> p, int i, int j) {//交换索引为i,j的元素的位置
    Object tem = p.get(i);
    p.set(i, p.get(j));
    p.set(j, tem);
}

这个代码看上去是正确的,但java编译器会报错,两行set语句都是错误的,可以借助带类型参数的泛型方法,来解决上面的这个问题。

private <T> void swapInternal(People<T> p, int i, int j) {
    T tem = p.get(i);
    p.set(i, p.get(j));
    p.set(j, tem);
}

public void swap(People<?> p, int i, int j) {
    swapInternal(p, i, j);
}

上面的这种写法在java容器类中就有类似的,公共的api是通配符形式,形式更简单,但内部调用带类型参数的方法。

3.java 还提供了一种通配符的限定<? super T> ,称为--超类型通配符,这个刚好跟<? extends T>相反。

  • 表示该通配符所代表的类型是T类型及其子类。
  • 表示该通配符所代表的类型是T类型及其超类。

    具体的就不在这里介绍了,大家可以自行了解。

    直观地讲,带有超类型限定的通配符<? super T>可以向泛型对象写入,带有子类型限定的通配符<? extends T>可以从泛型对象读取。

    需要注意的是,对于<? super T>在java源码中有一种常见的应用。我们先来看一段代码。

public final class LocalDate implements ChronoLocalDate {
}
//这里我只截取java源码中与本节有关的代码
public interface ChronoLocalDate extends Comparable<ChronoLocalDate> {
}

从上面的这段代码中,可以看到LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable ,也就是LocalDate实现了Comparable,而不是Comparable

因此假设如果我们要处理类似前面这种继承关系的类的话,比如Father类实现了Comparable,Son类继承了Father类。需要注意的是Son并没有重新实现Comparable接口。

那么如果我们要实现一个泛型方法来找出List中最小的Son对象,我们可以这样定义。

public static <T extends Comparable<? super T>> T min(List<T> list) {
	//比较方法的逻辑        
}

这意味着T可以实现Comparable<? super T>,而不仅仅是Comparable。也就是:类型 T 必须实现 Comparable 接口,并且这个接口的类型是 T 或 T 的任一父类。

假设类型参数是Son 的话,那声明就会是这样的<Son extends Comparable<? super Son>>,它意味着一个Son类可以实现Comparable

4.最后总结一下。

  • 和 一般用于读取,使得方法可以读取T和T的任意子类型的容器对象。
  • 用于写入和比较,使得对象可以写入父类型的容器;使得子类型的对象可以应用父类型的比较方法。
6.局限性

java泛型是通过类型擦除实现的,类型参数在编译的时候会被Object替换掉。这就使得Java泛型存在了一定的局限性和限制 。

1.使用泛型类、方法和接口

在使用泛型类、方法和接口时,需要注意的几点是:

  • 基本类型不能用于实例化类型参数
  • 运行时类型信息不适用于泛型
  • 类型擦除可能会引发一些冲突

类型信息是一个对象,他的类型为Class,Class本身就是一个泛型类,每个类的类型对象可以通过类名.class的方式引用,也可以通过对象.getClass()方法获得。

但是类型对象与泛型无关,所以java不支持类似People.class的写法,只有People.class。泛型类得到的类型对象总是原始类型。

People<Integer> integerPeople = new People<Integer>();
People<String> stringPeople = new People<String>();
if(integerPeople.getClass() == stringPeople.getClass())//这是TRUE的,

两次getClass()都返回People类型。

2.定义泛型类、方法和接口

在定义泛型类、方法和接口时,需要注意的几点是:

  • 不能通过类型参数创建对象
  • 泛型类的类型参数不能用于静态变量和方法

java不允许使用类型参数来创建对象,例如下面这样的方法都是错误的。

T a = new T();
T[] arr = new T[10];   //这两个方法都是错误的

也不允许使用类型参数在静态变量和静态方法中使用。

private static T a;		//错误
public static T geta(); //错误
posted @ 2019-03-30 14:50  再吃一颗苹果ch  阅读(157)  评论(0编辑  收藏  举报