Java 泛型

2021-04-09 18:13:18 浏览数 (1)

思考过程:

  1. (What)本质是什么?
  2. (Why)为什么会出现(新技术出现,一般有实际需求)?
  3. (When)解决什么问题?
  4. (How)如何使用?
  5. (Principle)主要原理是什么?
  6. (Key Point)有什么疑难点?
  7. (Effect)有什么影响?

What:泛型是什么?

泛型:参数化类型。类型在指定之前,是未知的,指定之后,范围就是固定的。

在J2SE 5.0中引入的这个对类型系统期待已久的增强允许类型或方法在提供编译时类型安全性的同时操作各种类型的对象。它将编译时类型安全性添加到集合框架中,并消除了强制转换的繁琐工作。

Why:为什么要引入泛型?

Java 集合框架中的数据元素,是Object类型,也就是可以是任意类型。

在使用集合数据时,需要显式地调用 强制类型转换。 1) 有可能引发ClassCastException 2) 问题暴露在运行时

使用泛型强制约束数据类型可以将 类型 转换问题暴露在编译期。

How:如何使用泛型?

命名约定:一般使用大写,简洁的单个字符表示。例如类的声明一般用E,泛型方法一般用T。

1)泛型类、泛型接口

2)泛型方法: 传入的参数 | 参数返回值,是泛型

泛型与子类型

规则要自洽,提出反例打破漏洞。

常见问题1: List<String>无法转换为List<Object>:

java.util.List<String> ls = new ArrayList<>(); java.util.List<Object> lo = ls;

一个List<String>可以赋值给List<Object>吗?

不可以。

在编译期间,编译器会报错:

// 错误: 不兼容的类型: List<String>无法转换为List<Object>

那么,编译器为什么规定,不允许这类情况发生呢?

难道String不是Object的子类吗?

难道一个List<String> 不是一个List<Object>吗?

那么,我们来看下面这种情况:

lo.add(new Object()); String s = ls.get(0);

我们把ls赋值给lo,然后通过lo,为ls增加新的元素ObjectA,如果修改成功,ls中,就不仅仅包含String这种对象了。

此时,如果我们获取ls的首个元素,得到的是ObjectA,无法转换为String。违反了List<String>的定义。

常见问题2: Foo是Bar的子类或者子接口,T<Foo> 不是 T<Bar> 的子类型

由上个问题引申出一个结论:

如果Foo是Bar的子类或者子接口,那么,T<Foo> 是 T<Bar> 子类型吗?
不是。当然了,这个结论还是非常违反直觉。

为什么?常见的误解点在哪里?

那就是,我们往往认为集合(Collection)内部的元素类型是不可变的。而事实上,它是可变的。

由此引发的问题同上。

常见问题3:一个未知类型的集合,在没有指定类型之前,是不能添加任意对象的

Collection<?> c = new ArrayList<String>(); c.add(new Object()); // Compile time error

一个未知类型的集合,在没有指定类型之前,是不能添加对象的,因为类型是未知的,会引发编译错误。

常见问题4: 一个有边界的未知类型的集合,在没有指定类型之前,是不能添加子类对象的

public void addRectangle(List<? extends Shape> shapes) { // Compile-time error! shapes.add(0, new Rectangle()); }

因为List<? extends Shape> shapes 此时类型,并没有指定具体的类型。Shape子类也有可能是Circle。

通配符(Wildcards)与泛型的上下边界

为啥会有通配符出现呢?

因为Collection<Object> 不等价于 Collection。

示例一种的Collection中的对象可以是任意类型,而示例二中Collection中的对象必须是Object类型,不能是Object的任意子类。

示例一

void printCollection(Collection c) { Iterator i = c.iterator(); for (k = 0; k < c.size(); k ) { System.out.println(i.next()); } }

示例二

void printCollection(Collection<Object> c) { for (Object e : c) { System.out.println(e); } }

这么一来,就抹杀了示例一代码的通用性。

那咋搞?所以泛型里引入了通配符, 用”?”表示,代表“未知类型的集合”

void printCollection(Collection<?> c) { for (Object e : c) { System.out.println(e); } }

Bounded Wildcards(有界通配符)

有界通配符是因为什么需求出现的呢?

是为了让泛型支持多态。

示例代码:假如你要实现一个绘制程序:

public abstract class Shape { public abstract void draw(Canvas c); } public class Circle extends Shape { private int x, y, radius; public void draw(Canvas c) { ... } } public class Rectangle extends Shape { private int x, y, width, height; public void draw(Canvas c) { ... } }

这些类使用canvas绘制:

public class Canvas { public void draw(Shape s) { s.draw(this); } }

假如Canvas类有个方法,绘制画布上所有的形状。传入一个List<Shape>作为参数列表。

public void drawAll(List<Shape> shapes) { for (Shape s: shapes) { s.draw(this); } }

现在,我们知道,这个方法传入的参数,必须是一个List<Shape>类型的列表,其他的List<Circle>参数类型,都无法调用这个方法。

那咋办呢?这就引入了有界通配符,来完成这件事,使得可以传入一个列表,是List<Circle> 或者是List<Rectangle>.

public void drawAll(List<? extends Shape> shapes) { ... }

泛型方法与类型推断

啥是泛型方法?为啥不直接用泛型类或者通配符?

举个例子:假如给一个集合追加参数:

static void fromArrayToCollection(Object[] a, Collection<?> c) { for (Object o : a) { c.add(o); // compile-time error } }

这显然不行,因为Collection<?> c的类型没有没有明确被指定。

那怎么办呢?使用泛型方法解决这个问题:

static <T> void fromArrayToCollection(T[] a, Collection<T> c) { for (T o : a) { c.add(o); // Correct } }

泛型方法如何实现类型推断的?

示例,泛型方法会以使用,相关联的参数的共同父类,作为推断类型。不同的集合类型,例如数组和Collection或者其他对象,不能成功推断出类型,会报出编译时错误。

Object[] oa = new Object[100]; Collection<Object> co = new ArrayList<Object>(); // T inferred to be Object fromArrayToCollection(oa, co); String[] sa = new String[100]; Collection<String> cs = new ArrayList<String>(); // T inferred to be String fromArrayToCollection(sa, cs); // T inferred to be Object fromArrayToCollection(sa, co); Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection<Number> cn = new ArrayList<Number>(); // T inferred to be Number fromArrayToCollection(ia, cn); // T inferred to be Number fromArrayToCollection(fa, cn); // T inferred to be Number fromArrayToCollection(na, cn); // T inferred to be Object fromArrayToCollection(na, co); // compile-time error fromArrayToCollection(na, cs);

那么,什么时候使用通配符?

什么时候使用泛型方法?

为了理解这个问题,我们再来看一个例子。

通配符

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

泛型方法也能替代上述代码:

interface Collection<E> { public <T> boolean containsAll(Collection<T> c); public <T extends E> boolean addAll(Collection<T> c); // Hey, type variables can have bounds too! }

但是,在containsAll和addAll中,类型参数T只使用一次

。返回类型不依赖于类型参数,也不依赖于方法的任何其他参数(在本例中,只有一个参数)。

这告诉我们类型参数正用于多态性;它的唯一效果是允许在不同的调用站点使用各种实际参数类型。如果是这样,就应该使用通配符。

通配符被设计成支持灵活的子类型,也就是多态。

泛型方法允许使用类型参数来表示方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。

如果不存在这种依赖关系,则不应使用泛型方法。

泛型方法和通配符,可以同时使用的。

class Collections { public static <T> void copy(List<T> dest, List<? extends T> src) { ... }

参数src只要是T的子类就可以,而desc必须是T类型的。其实我们也可以把上述代码写作:

class Collections { public static <T, S extends T> void copy(List<T> dest, List<S> src) { ... }

T既是第一个参数的类型,还是第二个参数类型的边界值。S自己只用了一次。其实是可以简化的,简化后(第一版)参数类型之间的关系,十分明确。

通配符还有一个优点,即它们可以在方法签名之外使用,例如字段、局部变量和数组的类型等。

示例如下:加入我们想在Canvas的drawAll方法中保存传入的参数列表,就可以用通配符的形式声明该 嵌套列表字段。

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>(); public void drawAll(List<? extends Shape> shapes) { history.addLast(shapes); for (Shape s: shapes) { s.draw(this); } }

泛型与遗留的代码交互,引发的UncheckedExceptionWarnning

在适当的泛型代码中,集合总是伴随着类型参数。当使用类似集合的泛型类型而不使用类型参数时,它称为原始类型。

原始类型类似于通配符类型,这是一个精心设计的决定,允许泛型与预先存在的遗留代码进行互操作。

泛型类由其所有调用共享

这句话什么意思呢?还是要举个例子。

List <String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());

上述代码会打印什么呢?答案是true。

为什么呢?

因为所有泛型类的实例,在运行时,使用的是同一份runtime class 文件。不管标签里的type类型是什么。

泛型类,对于所有可能的标签,展现出的行为,是一致的。

静态 变量和方法,在类的实例之间,也是共享的。这也是不能在静态方法,或者initailizer中引用类型参数的原因。

Casts and InstanceOf

编译泛型类时使用了类型擦除, 运行时不存在类型变量。这意味着它们在时间和空间上都不需要性能开销,这很好。

但是,这也意味着您不能在类型转换中可靠地使用它们。所以,InstanceOf和强制类型转换,都与Type Parameter 没有关系。

Arrays 不能用Type-Parameter声明数组类型

<T> T[] makeArray(T t) { return new T[100]; // Error. }

同样地,编译泛型类时使用了类型擦除, 运行时不存在类型变量,所以无法确定实际的数组类型将是什么。

解决这些限制的方法是使用类文本作为Runtime-Token。

https://docs.oracle.com/javase/tutorial/extra/generics/literals.html

这个位置看得略烦躁,之后再看

Array与Collection嵌套:

Principle 泛型 的实现原理

public interface List<T> { void add(T x); Iterator<T> iterator(); } interface Iterator<E> { E next(); boolean hasNext(); }

如果T是Integer,代码就与下方一样吗?

public interface IntegerList {

void add(Integer x);

Iterator<Integer> iterator();

}

看起来是这么个意思,但这是来自直觉的误解。

如果T是其他任何可能的类型,代码中就要存在如此多的代码copy吗??

———

泛型并不是这样扩展的,它与普通类一样,都是编译一次,产生一份二进制文件。不管是源文件,还是二进制,不管是在磁盘上,还是内存中,都只有一份代码。

-------

擦除和转换

public String loophole(Integer x) { List<String> ys = new LinkedList<String>(); List xs = ys; xs.add(x); // Compile-time unchecked warning,我们在列表中插入一个整数,并尝试获取一个String类型的数据。 return ys.iterator().next(); // 如果忽略此警告并尝试执行此代码,则它将在尝试传入Integer类型的参数x而崩溃,抛出ClassCastException }

在运行时,此代码的行为如下:

public String loophole(Integer x) { List ys = new LinkedList; List xs = ys; xs.add(x); return(String) ys.iterator().next(); // run time error,会出现ClassCastException }

泛型是由Java编译器作为一种叫做擦除的前端转换来实现的。您可以(几乎)将其视为源到源的转换,从而将代码的泛型版本转换为非泛型版本。

因此,即使存在未经检查的警告,Java虚拟机的类型安全性和完整性永远不会受到威胁。

基本上,擦除可以除去(或擦除)所有泛型类型信息。尖括号之间的所有类型信息都被抛出,因此,例如,List<String>之类的参数化类型将转换为List。类型变量的所有剩余使用都将替换为类型变量(通常为对象)的上限。并且,当生成的代码类型不正确时,将插入到适当类型的转换,如在最后一行的漏洞中。

Effet:

第一是泛化。可以用T代表任意类型。Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

第二是类型安全。泛型的一个主要目标就是提高Java程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。

第三是消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

第四是向后兼容。支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。

0 人点赞