Java基础之泛型

时间:2023-02-16 12:06:47

1.泛型概念的提出(为什么需要泛型)?

  当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。因此取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。

List list = new ArrayList();
list.add(1);
list.add(2);
list.add("五号");//一不小心插入了String
for (Object object : list){
//取出“五号”时报ClassCastException
Integert = (Integer)object;
}
/**
* 上面是一个集合没有使用泛型时出现的错误,
* 那么当我们使用泛型时,该错误就会避免,例如:
*/

List<Integer>list = newArrayList<Integer>();
list.add(1);
list.add(2);
list.add("五号");//插入String,提示:编译错误

2 认识泛型

(1)Java的参数化类型被称为泛型,即允许我们在创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素。
(2)泛型允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定。例如:

// 定义一个接口
interface Money<E>{
Eget(intindex);
boolean add(E e);
}
// 定义一个类
public class Apple<T>{
private T info;
public Apple(T info) {
this.info = info;
}
public T getInfo(){
return this.info;
}
public void setInfo(T info){
this.info = info;
}
public static void main(String[] args) {
Apple<String>ap1 = new Apple<String>("小苹果");
System.out.println(ap1.getInfo());
Apple<Double>ap2 = new Apple<Double>(1.23);
System.out.println(ap2.getInfo());
}
}

(3)注意:在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间。不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class),例如下面的程序将输出true。

static List<String> l1 = new ArrayList<String>();
static List<Double> l2 = new ArrayList<Double>();
System.out.println(l1.getClass() == l2.getClass());// true

3 类型通配符

  如果我们想定义一个方法,这个方法的参数是一个集合形参,但是集合形参的元素类型是不确定的。可能会想到下面两种定义方法:

/**
* test1可以使用,但是会报泛型警告;
*/

public void test1(List l){ }
/**
* test2在传入非List<Object>时无法使用,会报“test2(List<Object>)对于参数(List<String>)不适用”;
*/

public void test2(List<Object> l){ }
/**
* 为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),
* 将一个问号作为类型实参传给List集合,写作List<?>,
* 那么我们就可以这样定义上面的方法:
*/

public void test3(List<?> l){ }

4 PECS原则

4.1 错误使用描述

List<? extends Foo> list1 = new ArrayList<Foo>();
List<? extends Foo> list2 = new ArrayList<Foo>();

/* Won't compile */
list2.add(new Foo()); //(1)error 1
list1.addAll(list2); //(2)error 2

(1)error 1:

add(capture<? extends Foo>) in List cannot be applied to add(Foo)

(2)error 2:

addAll(java.util.Collection<? extends capture<? extends Foo>>) in List cannot be applied to addAll(java.util.List<capture<? extends Foo>>)

4.2 PECS法则

  Java泛型可以有多种写法,主要是 extends 和 super 关键字。比如:

ashMap< T extends String>;
HashMap< ? extends String>;
HashMap< T super String>;
HashMap< ? super String>;

4.2.1 ? extends

List<Apple> apples = new ArrayList<>();
List<? extends Fruit> fruits = apples; //works, apple is a subclass of Fruit.
fruits.addAll(apples); //compile error
fruits.add(new Strawberry()); //compile error

(1)存入数据

  • fruits是一个Fruit子类的List,由于Apple是Fruit的子类,因此将apples赋给fruits是合法的。
  • 编译器会阻止将Strawberry类加入fruits。在向fruits中添加元素时,编译器会检查类型是否符合要求。因为编译器只知道fruits是Fruit某个子类的List,但并不知道这个子类具体是什么类,为了类型安全,只好阻止向其中加入任何子类。
  • 那么可不可以加入Fruit呢?很遗憾,也不可以。事实上,不能往一个使用了? extends的数据结构里写入任何的值。

(2)读取数据:由于编译器知道它总是Fruit的子类型,因此我们总可以从中读取出Fruit对象:

Fruit fruit = fruits.get(0);

4.2.2 ? super

List<Fruit> fruits = new ArrayList<>();
List<? super Apple> apples = fruits; //works, Fruit is a superclass of Apple.
apples.add(new Apple()); //work
apples.add(new RedApple()); //work
apples.add(new Fruit()); //compile error
apples.add(new Object()); //compile error

(1)存入数据

  • fruits是一个Apple超类(父类,superclass)的List。
  • 出于对类型安全的考虑,我们可以加入Apple对象或者其任何子类(如RedApple)对象(因为编译器会自动向上转型),但由于编译器并不知道List的内容究竟是Apple的哪个超类,因此不允许加入特定的任何超类型。

(2)读取数据:编译器在不知道是什么类型的情况下只能返回Object对象,因为Object是任何Java类的最终祖先类。

Object fruit = apples.get(0);

4.3 总结

  • 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends);
  • 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super);
  • 如果既要存又要取,那么就不要使用任何通配符。

5 泛型方法和类型通配符的区别

(1)大多数时候都可以使用泛型方法来代替类型通配符。例如对于Java的Collection接口中两个方法的定义:

public interface Collection<E>{
boolean containsAll(Collection<?>c);
booleanaddAll(Collection<? extends E> c);
...
}

(2)上面集合中的两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示:

public interface Collection<E>{
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}

(3)上面两个方法中类型形参T只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
(4)泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

6 参考链接

Java泛型中的PECS原则