Java-Java5.0泛型解读
概述
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型类
我们先看一个简单的类的定义
package com.xgj.master.java.generics;
public class GenericClass {
private String str;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
这是我们最常见的做法,但是这样做有一个坏处 GenericClass类中只能装入String类型的元素,如果我们以后要扩展该类,比如转入Integer类型的元素,就不想要从重写,代码得不到复用,我们使用泛型可以很好地解决这个问题。
如下代码所示:
package com.xgj.master.java.generics;
/**
*
* @ClassName: GenericClass
* @Description: 泛型类
* @author: Mr.Yang
* @date: 2017年8月31日 下午3:35:38
* @param <T>
*/
public class GenericClass<T> {
// T stands for "Type"
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
这样GenericClass类便可以得到复用,我们可以将T替换成任何我们想要的类型:
假设我们有一个User类
GenericClass<String> string = new GenericClass<String>();
GenericClass<User> user = new GenericClass<User>();
另外一个示例:
package com.xgj.master.java.generics;
import org.junit.Test;
public class NormalClass {
@Test
public void test(){
Point point = new Point();
point.setX(100); // int -> Integer -> Object
point.setY(20);
int x = (Integer) point.getX(); // 必须向下转型
int y = (Integer) point.getY();
System.out.println("This point is:" + x + ", " + y);
point.setX(25.4); // double -> Integer -> Object
point.setY("字符串");
// 必须向下转型
double m = (Double) point.getX();
// 运行期间抛出异常 java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double
double n = (Double) point.getY();
System.out.println("This point is:" + m + ", " + n);
}
/**
*
* @ClassName: Point
* @Description: 一般内部类
* @author: Mr.Yang
* @date: 2017年8月31日 下午8:23:31
*/
class Point {
Object x = 0;
Object y = 0;
public Object getX() {
return x;
}
public void setX(Object x) {
this.x = x;
}
public Object getY() {
return y;
}
public void setY(Object y) {
this.y = y;
}
}
}
上面的代码中,设置值的时候不会有任何问题,但是取值时,要向下转型,因向下转型存在着风险,而且编译期间不容易发现,只有在运行期间才会抛出异常,所以要尽量避免使用向下转型。
那么,有没有更好的办法,既可以不使用重载(有重复代码),又能把风险降到最低呢?
可以使用泛型类(Java Class),它可以接受任意类型的数据。所谓“泛型”,就是“宽泛的数据类型”,任意的数据类型。
package com.xgj.master.java.generics;
import org.junit.Test;
public class GenericClass2 {
@Test
public void test() {
Point<Integer, Integer> point = new Point<Integer, Integer>();
point.setX(200);
point.setY(400);
Integer x = point.getX();
Integer y = point.getY();
System.out.println("This point is:" + x + ", " + y);
Point<Double, String> point2 = new Point<Double, String>();
point2.setX(25.4);
point2.setY("字符串");
double m = point2.getX();
String n = point2.getY();
System.out.println("This point is:" + m + ", " + n);
}
/**
*
*
* @ClassName: Point
*
* @Description: 泛型类, T1 T2仅表示类型
*
* @author: Mr.Yang
*
* @date: 2017年8月31日 下午8:20:26
*
* @param <T1>
* @param <T2>
*/
class Point<T1, T2> {
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}
}
与普通类的定义相比,上面的代码在类名后面多出了 <T1, T2>
,T1, T2 是自定义的标识符,也是参数,用来传递数据的类型,而不是数据的值,我们称之为类型参数。在泛型中,不但数据的值可以通过参数传递,数据的类型也可以通过参数传递。T1, T2 只是数据类型的占位符,运行时会被替换为真正的数据类型。
传值参数(我们通常所说的参数)由小括号包围,如 (int x, double y),类型参数(泛型参数)由尖括号包围,多个参数由逗号分隔,如 <T>
或 <T, E>
。
类型参数需要在类名后面给出。一旦给出了类型参数,就可以在类中使用了。类型参数必须是一个合法的标识符,习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。
泛型类在实例化时必须指出具体的类型,也就是向类型参数传值,格式为:
className variable<dataType1, dataType2> = new className<dataType1, dataType2>();
也可以省略等号右边的数据类型,但是会产生警告,即:
className variable<dataType1, dataType2> = new className();
因为在使用泛型类时指明了数据类型,赋给其他类型的值会抛出异常,既不需要向下转型,也没有潜在的风险,比上个例子的自动装箱和向上转型要更加实用。
泛型方法
我们可以编写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
定义泛型方法的规则如下:
所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前, 比如
public static <E> void printArray( E[] inputArray ){}
每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
-
泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是short, int, double, long, float, byte, char等原始类型。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。
那如何声明一个泛型方法呢? 声明一个泛型方法很简单,只要在返回类型前面加上一个类似
<K, V>
的形式就可以啦。
比如:
package com.xgj.master.java.generics;
/**
*
* @ClassName: Compare
* @Description: 内部类,泛型类
* @author: Mr.Yang
* @date: 2017年8月31日 下午4:01:39
* @param <K>
* @param <V>
*/
public class ComPare<K,V> {
private K key;
private V value;
/**
*
* @Title:ComPare
* @Description:构造函数
* @param key
* @param value
* @return
*/
public ComPare(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
package com.xgj.master.java.generics;
/**
*
* @ClassName: GenericMethod
* @Description: 泛型方法演示
* @author: Mr.Yang
* @date: 2017年8月31日 下午3:54:23
*/
public class GenericMethod {
/**
*
* @Title: cofer
* @Description: 泛型方法
* @param c1
* @param c2
* @return
* @return: boolean
*/
public static <K,V> boolean cofer(ComPare<K,V> c1, ComPare<K,V> c2){
return c1.getKey().equals(c2.getKey()) && c1.getValue().equals(c2.getValue());
}
// 泛型方法的调用
public static void main(String[] args) {
ComPare<Integer,String> c1 = new ComPare<>(1, "dog");
ComPare<Integer, String> c2 = new ComPare<>(2, "cat");
boolean different = GenericMethod.<Integer,String>cofer(c1, c2);
System.out.println("c1 compares c2,and the result is " + different);
// 在Java1.7/1.8可以利用type inference,让Java自动推导出相应的类型参数
boolean different2 = GenericMethod.cofer(c1, c2);
System.out.println("自动推导 c1 compares c2,and the result is " + different2);
}
}
运行结果:
c1 compares c2,and the result is false
自动推导 c1 compares c2,and the result is false
第二个示例:
package com.xgj.master.java.generics;
public class GenericMethod2 {
public static void main(String[] args) {
// 实例化泛型类
Point<Integer, Integer> p1 = new Point<Integer, Integer>();
p1.setX(10);
p1.setY(20);
p1.printPoint(p1.getX(), p1.getY());
Point<Double, String> p2 = new Point<Double, String>();
p2.setX(25.4);
p2.setY("字符串");
p2.printPoint(p2.getX(), p2.getY());
}
}
/**
*
*
* @ClassName: Point
*
* @Description: 定义泛型类
*
* @author: Mr.Yang
*
* @date: 2017年8月31日 下午7:59:47
*
* @param <T1>
* @param <T2>
*/
class Point<T1, T2> {
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
/**
*
*
* @Title: printPoint
*
* @Description: 定义泛型方法
*
* @param x
* @param y
*
* @return: void
*/
public <T1, T2> void printPoint(T1 x, T2 y) {
T1 m = x;
T2 n = y;
System.out.println("This point is:" + m + ", " + n);
}
}
上面的代码中定义了一个泛型方法 printPoint(),既有普通参数,也有类型参数,类型参数需要放在修饰符后面、返回值类型前面。一旦定义了类型参数,就可以在参数列表、方法体和返回值类型中使用了。
与使用泛型类不同,使用泛型方法时不必指明参数类型,编译器会根据传递的参数自动查找出具体的类型。泛型方法除了定义不同,调用就像普通方法一样。
注意:泛型方法与泛型类没有必然的联系,泛型方法有自己的类型参数,在普通类中也可以定义泛型方法。
泛型方法 printPoint() 中的类型参数 T1, T2 与泛型类 Point 中的 T1, T2 没有必然的联系,也可以使用其他的标识符代替:
public static <V1, V2> void printPoint(V1 x, V2 y){
V1 m = x;
V2 n = y;
System.out.println("This point is:" + m + ", " + n);
}
泛型接口
在Java中也可以定义泛型接口, 示例如下:
package com.xgj.master.java.generics;
public class GenericInterfaceDemo {
public static void main(String arsg[]) {
Info<String> obj = new InfoImp<String>("xiaogongjiang");
System.out.println("Length Of String: " + obj.getVar().length());
}
}
/**
*
*
* @ClassName: Info
*
* @Description: 定义泛型接口
*
* @author: Mr.Yang
*
* @date: 2017年8月31日 下午8:06:06
*
* @param <T>
*/
interface Info<T> {
public T getVar();
}
/**
*
*
* @ClassName: InfoImp
*
* @Description: 泛型接口实现类
*
* @author: Mr.Yang
*
* @date: 2017年8月31日 下午8:06:13
*
* @param <T>
*/
class InfoImp<T> implements Info<T> {
private T var;
/**
*
*
* @Title:InfoImp
*
* 定义泛型构造方法
*
* @param var
*/
public InfoImp(T var) {
this.setVar(var);
}
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
}
边界符
在上面的代码中 , 类型参数可以接受任意的数据类型,只要它是被定义过的。但是,很多时候我们只需要一部分数据类型就够了,用户传递其他数据类型可能会引起错误。例如,编写一个泛型函数用于返回不同类型数组(Integer 数组、Double 数组、Character 数组等)中的最大值
package com.xgj.master.java.generics;
public class CountGreater {
public static <T> int countGreaterThan(T[] array, T element){
int count = 0 ;
// 遍历数组
for (T t : array) {
if (t > element) { // 编译报错
++count;
}
}
return count;
}
}
但是这样很明显是错误的,因为除了short, int, double, long, float, byte, char等原始类型,其他的类并不一定能使用操作符>,所以编译器报错,那怎么解决这个问题呢?答案是使用边界符, 通过 extends 关键字可以限制泛型的类型.
public interface Comparable<T> {
public int comparable(T t);
}
做个类似下面这样的声明,这样就等于告诉编译器类型参数T代表的都是实现了Comparable接口的类,这样等于告诉编译器它们都至少实现了compareTo方法。
public class CountGreater {
public static <T extends Comparable<T>> int countGreaterThan(T[] array, T element){
int count = 0 ;
// 遍历数组
for (T t : array) {
if (t.comparable(element) > 0) {
++count;
}
}
return count;
}
}
extends 后面可以是类也可以是接口。但这里的 extends 已经不是继承的含义了,应该理解为 T 是继承自 Comparable类的类型,或者 T 是实现了 XX 接口的类型。
通配符
在了解通配符之前,我们首先必须要澄清一个概念,还是借用我们上面定义的GenericClass类,假设我们添加一个这样的方法:
public void test(GenericClass<Number> n){/***/}
那么现在GenericClass<Number> n
允许接受什么类型的参数?
我们是否能够传入GenericClass<Integer>
或者GenericClass<Double>
呢?
答案是否定的,虽然Integer和Double是Number的子类,但是在泛型中GenericClass<Integer>
或者GenericClass<Double>
与GenericClass<Number>
之间并没有任何的关系。
这一点非常重要,接下来我们通过一个完整的例子来加深一下理解。
首先我们先定义几个简单的类,下面我们将用到它:
class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
我们创建了一个泛型类Reader,然后在f1()中当我们尝试Fruit f = fruitReader.readExact(apples);编译器会报错,因为List<Fruit>
与List<Apple>
之间并没有任何的关系。
如下:
package com.xgj.master.java.generics;
import java.util.Arrays;
import java.util.List;
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Orange> oranges = Arrays.asList(new Orange());
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f1() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
// The method readExact(List<Fruit>) in the type
// GenericReading.Reader<Fruit> is not applicable for the arguments (List<Apple>)
Fruit f = fruitReader.readExact(apples); // 编译报错
}
public static void main(String[] args) {
f1();
}
}
但是按照我们通常的思维习惯,Apple和Fruit之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:
package com.xgj.master.java.generics;
import java.util.Arrays;
import java.util.List;
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Orange> oranges = Arrays.asList(new Orange());
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f2() {
CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(oranges);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f2();
}
}
这样就相当与告诉编译器, fruitReader的readCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。
PECS原则
上面我们看到了类似<? extends T>
的用法,利用它我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?
比如
package com.xgj.master.java.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// The method add(capture#1-of ? extends Fruit) in the type List<capture#1-of ? extends Fruit>
// is not applicable for the arguments (Apple)
// flist.add(new Apple()); 编译报错
// flist.add(new Orange());编译报错
// flist.add(new Fruit());编译报错
// flist.add(new Object());编译报错
flist.add(null); // 虽然可以添加null ,但是没有意义
// We Know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List
List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
- 当我们尝试add一个Apple的时候,flist可能指向
new ArrayList<Orange>();
- 当我们尝试add一个Orange的时候,flist可能指向
new ArrayList<Apple>();
- 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错。
所以对于实现了<? extends T>
的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。
如果我们要add元素应该怎么做呢?可以使用<? super T>
:
package com.xgj.master.java.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericWriting {
static List<Apple> apples = new ArrayList<Apple>();
static List<Fruit> fruit = new ArrayList<Fruit>();
static <T> void writeExact(List<T> list, T item) {
list.add(item);
}
static void f1() {
writeExact(apples, new Apple());
writeExact(fruit, new Apple());
}
static <T> void writeWithWildcard(List<? super T> list, T item) {
list.add(item);
}
static void f2() {
writeWithWildcard(apples, new Apple());
writeWithWildcard(fruit, new Apple());
}
public static void main(String[] args) {
f1();
f2();
}
}
这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List<? super Apple> list
,它可以有下面几种含义:
List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();
根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:
“Producer Extends” – 如果你需要一个只读List,用它来produce
T,那么使用? extends T
。“Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用
? super T
。
如果需要同时读取以及写入,那么我们就不能使用通配符了。
如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++)
dest.set(i, src.get(i));
}
}
类型擦除
Java泛型中最令人苦恼的地方或许就是类型擦除了. 如果在使用泛型时没有指明数据类型,那么就会擦除泛型类型.
因为在使用泛型时没有指明数据类型,为了不出现错误,编译器会将所有数据向上转型为 Object,所以在取出坐标使用时要向下转型.
比如
public class Demo {
public static void main(String[] args){
Point p = new Point(); // 类型擦除
p.setX(10);
p.setY(20.8);
int x = (Integer)p.getX(); // 向下转型
double y = (Double)p.getY();
System.out.println("This point is:" + x + ", " + y);
}
}
class Point<T1, T2>{
T1 x;
T2 y;
public T1 getX() {
return x;
}
public void setX(T1 x) {
this.x = x;
}
public T2 getY() {
return y;
}
public void setY(T2 y) {
this.y = y;
}
}
因为在使用泛型时没有指明数据类型,为了不出现错误,编译器会将所有数据向上转型为 Object,所以在取出坐标使用时要向下转型,和不使用泛型没什么两样。
另外一个例子
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
这意味着不管我们声明Node<String>
还是Node<Integer>
,到了运行期间,JVM统统视为Node<Object>
。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题。
在Java中不允许创建泛型数组
Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
实际上对于这个问题,还可以采用Factory和Template两种设计模式解决.
- 我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息.
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
=> { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }
上一篇: 理解OpenGL拾取模式(OpenGL Picking)
下一篇: 常见MYSQL技巧 (51)