10_Java泛型
本章章节
> 10.1为什么需要泛型
> 10.2泛型类
> 10.3 泛型接口
> 10.4泛型方法
> 10.5用泛型定义数组
> 10.6建立类型为泛型类的数组
> 10.7泛型类充当泛型的实例化类
> 10.8通配符
Java泛型(generics)是JDK 5中引入的一个新特性,它的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。
10.1 为什么需要泛型
现在我要设计一个可以表示坐标的点类,坐标由x和y组成。可是不同的人,可能想要知道的坐标不一样。有人想要坐标用整型表示;有人想要坐标用小数表示;有有人想要坐标用字符串表示,如下:
A这个人想要看到整型的坐标,例如:(10, 20)。所以我们要定义一个IntPoint类如下:
//IntPoint.java
public class IntPoint { private int x; private int y; public IntPoint(){ } public IntPoint(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } }
B这个人想要看到小数的坐标,例如(10.5, 20.6)。所以我们要定义一个DoublePoint类如下:
//DoublePoint.java
public class DoublePoint { private double x; private double y; public DoublePoint(){ } public DoublePoint(double x, double y) { this.x = x; this.y = y; } public double getX() { return x; } public void setX(double x) { this.x = x; } public double getY() { return y; } public void setY(double y) { this.y = y; } }
C这个人想要看到字符串的坐标。例如:("东经180度", y="北纬120度"),所以我们要定义一个StringPoint类如下:
//StringPoint.java
public class StringPoint { private String x; private String y; public StringPoint(){ } public StringPoint(String x, String y) { this.x = x; this.y = y; } public String getX() { return x; } public void setX(String x) { this.x = x; } public String getY() { return y; } public void setY(String y) { this.y = y; } }
使用过程如下:
//TestPoint.java
public class TestPoint { public static void main(String[] args) { IntPoint ptInt = new IntPoint(10, 20); System.out.println("(" + ptInt.getX() + "," + ptInt.getY() + ")"); DoublePoint ptDouble = new DoublePoint(10.5, 20.6); System.out.println("(" + ptDouble.getX() + "," + ptDouble.getY() + ")"); StringPoint ptString = new StringPoint("东经180度", "北纬120度"); System.out.println("(" + ptString.getX() + "," + ptString.getY() + ")"); } }
我们发现其实上面的三个类的基本形式都是一模一样的,除了表示坐标的x,y数据类型不一样而已。这样写出的代码冗余度太高,而且维护和扩展比较麻烦。比如我现在想要增加一个打印坐标的功能,我不得不在每一个坐标类都增加一个disp函数。那么有没有什么方法能帮我们做到只定义一个点类,就可以表示所有的可能呢?
第一种解决方法是利用Object类,因为Object是所有类的父类,所以将点类声明如下即可:
//ObjectPoint.java
public class ObjectPoint { private Object x; private Object y; public ObjectPoint() { } public ObjectPoint(Object x, Object y) { this.x = x; this.y = y; } 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; } }
使用过程如下:
//TestPoint.java
public class TestPoint { public static void main(String[] args) { ObjectPoint ptObj1 = new ObjectPoint(new Integer(10), new Integer(20)); //或ObjectPoint ptObj1 = new ObjectPoint(10, 20); int iV1 = Integer.parseInt(ptObj1.getX().toString()); //或采用int iV1 = (Integer)(ptObj1.getX());强制类型转换为Integer后再自动拆箱 int iV2 = Integer.parseInt(ptObj1.getY().toString()); System.out.println("(" + iV1 + "," + iV2 + ")"); ObjectPoint ptObj2 = new ObjectPoint(new Double(10.5), new Double(20.6)); //或ObjectPoint ptObj2 = new ObjectPoint(10.5, 20.6); double dV1 = Double.parseDouble(ptObj2.getX().toString()); double dV2 = Double.parseDouble(ptObj2.getY().toString()); System.out.println("(" + dV1 + "," + dV2 + ")"); ObjectPoint ptObj3 = new ObjectPoint(new String("东经180度"), new String("北纬120度")); //或ObjectPoint ptObj3 = new ObjectPoint("东经180度", "北纬120度"); String sV1 = ptObj3.getX().toString(); String sV2 = ptObj3.getY().toString(); System.out.println("(" + sV1 + "," + sV2 + ")"); } }
在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。比如写ObjectPoint ptObj1 = new ObjectPoint(10, 20);的时候,容易造成一些隐藏的问题,一不小心写成:ObjectPoint ptObj1 = new ObjectPoint(10.3, 20);编译的时候不报错,但是运行的时候就会有类型不匹配问题。所以Object是一种弱类型检测,执行的时候才知道具体的数据类型,所以不安全。
第二种方法是利用泛型(Generics),泛型的目的在于对参数类型本身进行参数化。它属于强类型检测(在使用的时候数据类型是确定的,编译器和执行器可以进行类型检测)。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
10.2 泛型类
声明包含泛型的类的格式如下:
[访问修饰符] class 类名<泛型1, 泛型2, …>{
[访问权限] 泛型类型标识 变量名称;
[访问权限] 构造方法([<泛型类型>] 参数名称){}
[访问权限] 泛型类型标识 getXxx(){}
[访问权限] 返回值类型声明 setXxx(泛型类型标识 变量名称){}
}
比如:
/* * 此处声明了一个包含泛型T的泛型类,T代表所有可能的类型 * 而T的实际类型在MyGenerics类实例化时指定。 */ class MyGenerics<T> { private T x; // x为泛型成员 public MyGenerics(T x) { // 构造方法的参数类型为泛型T this.x = x; } public void setX(T x) { // setX方法的参数类型为泛型T this.x = x; } public T getX() { // setX方法的返回类型为泛型T return this.x; } }
创建泛型类的实例时,可以使用一对尖括号指定泛型的真正类型,但是需要注意的是,尖括号中的类型必须是类类型,比如对于10,不能为int,而是Integer。
泛型对象实例化格式如下:
类名称<具体类> 对象名称 = new 类名称<具体类>();
比如:
//将T替换为Boolean,并调用一个参数 构造函数,传递的值为boolean类型 MyGenerics<Boolean> gen1 = new MyGenerics<Boolean>(true); gen1.setX(false); // 调用setX方法,传递boolean值 System.out.println(gen1.getX()); // 调用getX方法,返回boolean值 //将T替换为Integer,并调用一个参数 构造函数,传递的值为int类型 MyGenerics<Integer> gen2 = new MyGenerics<Integer>(10); gen2.setX(20); // 调用setX方法,传递int值 System.out.println(gen2.getX()); // 调用getX方法,返回int值
泛型使用的注意点:
1、泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
2、同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。如:gen2 = gen1,则会报错。
3、泛型的类型参数可以有多个。
4、泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上称为“有界类型”。
5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName("java.lang.String");
下面我们利用泛型来解决我们之前坐标的问题:
//GenericsPoint.java
public class GenericsPoint<T> { private T x; private T y; public GenericsPoint() { } public GenericsPoint(T x, T y) { this.x = x; this.y = y; } public T getX() { return x; } public void setX(T x) { this.x = x; } public T getY() { return y; } public void setY(T y) { this.y = y; } }
使用过程如下:
public class TestGenerics { public static void main(String[] args) { GenericsPoint<Integer> gen1 = new GenericsPoint<Integer>(10, 20); System.out.println("(" + gen1.getX() + "," + gen1.getY() + ")"); GenericsPoint<Double> gen2 = new GenericsPoint<Double>(10.5, 20.6); System.out.println("(" + gen2.getX() + "," + gen2.getY() + ")"); GenericsPoint<String> gen3 = new GenericsPoint<String>("东经180度", "北纬120度"); System.out.println("(" + gen3.getX() + "," + gen3.getY() + ")"); } }
需要注意的是,泛型与子类继承的限制:在泛型操作中,子类的泛型类型是无法使用父类的泛型类型接收的。
由前面的例子,我们知道,Object是所有类的父类,因此,所有的类型的实例都可赋值给声明为Object类型的变量。如:
Boolean f1 = new Boolean(true); Integer f2 = new Integer(1); Object f = f1; //OK f=f2; //OK
但在实例化泛型类时,将泛型指定为Object类型却不存在着和其他类型之间的兼容性:
GenericsPoint<Integer> gen1 = new GenericsPoint<Integer>(10, 20); GenericsPoint<Double> gen2 = new GenericsPoint<Double>(10.5, 20.6); GenericsPoint<Object> gen = gen1; // gen1和gen类型并不兼容,发生编译错误
除了可以定义包含一个泛型的类的定义之外,还可以包含多个泛型的类定义。其定义格式如下:
[修饰符] class类名<T1, T2, …> { [修饰符] T1 f1; [修饰符] T2 f2; //.... }
示例如下:
class MyGenerics<T1, T2> { private T1 x; private T2 y; public MyGenerics() { } public MyGenerics(T1 x, T2 y) { this.x = x; this.y = y; } public void setXY(T1 x, T2 y) { this.x = x; this.y = y; } public T1 getX() { return this.x; } public T2 getY() { return this.y; } }
此时MyGenerics泛型类的实例化格式:
给出泛型T1, T2的实际类型:
MyGenerics<Integer, String> gen1 = new MyGenerics<Integer, String>(1001, "张三");
也可以不给出T1, T2的实际类型。此时T1,T2将被默认为是Object类型:
MyGenerics gen2 = new MyGenerics();
但通常不会这么干。
在泛型类中的泛型成员不能直接实例化,其实例必须要通过方法的参数传递给泛型成员,看下面的代码:
public class MyGenerics<T> { private T[] array; //此处不能用new T[]实例化array public void setArray(T[] array) { this.array = array; } public T[] getArray() { return array; } }
可以通过方法的泛型参数,将数组的实例传递给类中的泛型数组。看下面代码:
String[] color = {"red", "green", "blue"}; MyGenerics<String> gen = new MyGenerics<String>(); gen.setArray(color); //向泛型成员array传递实际的字符串数组 String[] strs = gen.getArray();//读取泛型成员array的值,将其赋给字符串数组strs
10.3 泛型接口
在JDK1.5之后,不仅仅可以声明泛型类,也可以声明泛型接口,声明泛型接口和声明泛型类的语法类似,也是在接口名称后面加上<T>。
泛型接口定义格式:
[访问权限] interface 接口名称<泛型标识>{}
比如:
interface I<T1, T2>{ void fun1(T1 t); T2 fun2();
}
泛型接口实现的两种方式:
定义子类:在子类的定义上也声明泛型类型。
定义子类:如果实现接口的子类不想使用泛型声明,则在实现接口的时候直接指定好其具体的操作类型即可。如果不指定,则此时泛型会自动变为Object。
实现泛型接口时,类在定义时可以不声明泛型接口中的泛型,此时接口中的泛型也会自动变为Object类型。如下:
public class IC implements I { public void fun1(Object t) {…} //对应接口方法fun1的实现 public Object fun2() {…} //对应接口方法fun2的实现 }
如果需要在实现泛型接口时,保留接口中的泛型,类在定义时就必须要保留泛型接口中的泛型声明。如下:
public class IC<T1, T2> implements I<T1, T2>{ public void fun1(T1 t){…} //对接口方法getT1的实现 public T2 fun2(){…} //对接口方法getT2的实现 }
此时可以声明泛型接口的变量,并利用泛型实现类进行实例化。如下:
I<String, Integer> i = new IC<String, Integer>();
在实现泛型接口时,也可直接指定接口中的泛型的实际类型。如下:
//实现接口I时,直接指定泛型T1、T2的类型 class IC implements I<String, Integer> { //由于指定接口I中T1类型为String,fun1参数t的类型必须为String public void fun1(String t) {…} //由于指定接口I中T2类型为Integer,fun2返回类型必须为Integer public Integer fun2() {…} }
同理,当包含泛型的类有继承关系的时候,如需保留父类泛型,需要在声明时加入父类泛型。如下:
public class SubGeneric<T1, T2, T3> extends Generic<T1, T2> { private T3 f; public void setF(T3 f) { this.f = f; } public T3 getF() { return f; } }
如果不保留父类中的泛型声明,则继承下来的T1与T2自动变为Object类型。建议父类中的泛型声明在子类中都要保留。
如果在继承时,不想保留父类中的泛型,但也不想使用默认的Object类型,此时可以直接指定父类中的泛型。如下:
public class SubGeneric<T3> extends Generic<String, Object> { private T3 f; public void setF(T3 f) { this.f = f; } public T3 getF() { return f; } }
10.4 泛型方法
之前的所有的泛型除了可以为类中的属性制定类型之外,也可以定义方法,泛型方法所在的类中是否是泛型类本身是没有任何关系的。泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型,使用如下的格式定义泛型方法。
泛型方法的简单定义:
[访问权限] <泛型标识> 泛型标识 方法名称([泛型标识 参数名称])
调用泛型方法和调用普通方法没有任何不同,只需传递含有具体类型的实参即可。比如:
class Demo { public <T> T fun(T t) { // 可以接收任意类型的数据 return t; // 直接把参数返回 } } public class GenericsDemo { public static void main(String args[]) { Demo d = new Demo(); // 实例化 Demo 对象 String str = d.fun("张三"); // 传递字符串 int i = d.fun(30); // 传递数字,自动装箱 System.out.println(str); // 输出内容 System.out.println(i); // 输出内容 } }
可以将泛型方法和泛型类结合起来使用,如下范例:
class Info<T> { private T value; public Info(T value) { this.value = value; } public T getValue() { return this.value; } public void setValue(T value) { this.value = value; } public String toString() { return this.value.toString(); } } public class TestGenerics { public static <K> void conn(Info<K> a, Info<K> b) { System.out.println(a + " " + b); } public static void main(String[] args) { Info<Integer> infoInt1 = new Info<Integer>(10); Info<Integer> infoInt2 = new Info<Integer>(20); conn(infoInt1, infoInt2); Info<String> infoStr1 = new Info<String>("张三"); Info<String> infoStr2 = new Info<String>("李四"); conn(infoStr1, infoStr2); } }
10.5 用泛型定义数组
在使用泛型方法的时候,也可以传递或返回一个泛型定义的数组。例如:
public class TestGenerics { public static <T> T[] fun1(T... arr) { return arr; } public static <T> void fun2(T arr[]) { for (T val : arr) { System.out.print(val + " "); } System.out.println(); } public static void main(String[] args) { Integer i[] = fun1(1, 2, 3, 4, 5, 6); fun2(i); String str[] = fun1("aaa", "bbb", "ccc", "ddd"); fun2(str); } }
泛型定义的数组在接收数据的时候,必须保证数据的一致性。即传入的参数类型必须统一。假如修改传入的参数类型,使其与其他参数类型不一致,编译时就会报错。例如:
Integer i[] = fun1(1, 2, 3, 4, 5.0f, 6); // 编译报错
10.6 建立类型为泛型类的数组
除了可以用泛型去定义数组外,还可以定义泛型类的数组。如果要建立泛型类的数组,需要注意new关键字后面不要加入泛型的实际类型名。如下:
Generic<String>[] gs; //声明泛型类的数组 //先对泛型数组进行初始化 gs = new Generic[5]; //不要写成new Generic<String>[5] //再分别为每一个数组元素进行初始化 gs[0]=new Generic<String>(); //为第一个数组元素赋值 //…
实例如下:
class Info<T> { private T value; public Info(T value) { this.value = value; } public T getValue() { return this.value; } public void setValue(T value) { this.value = value; } public String toString() { return this.value.toString(); } } @SuppressWarnings("unchecked") public class TestGenerics { public static void main(String[] args) { Info<String>[] info = new Info[5]; for (int i = 0; i < info.length; i++) { info[0] = new Info<String>("hello" + i); System.out.println(info[0]); } } }
10.7 泛型类充当泛型的实例化类
之前所讲解的全部泛型操作,都是直接用一个实际的类来实例化的。Java中也允许用一个泛型类来实例化另一个泛型类。这种结构称为泛型嵌套。
首先定义了两个泛型类Info和Demo,如下所示:
class Info<T, V> { // 接收两个泛型类型 private T var; private V value; public Info(T var, V value) { this.setVar(var); this.setValue(value); } public void setVar(T var) { this.var = var; } public void setValue(V value) { this.value = value; } public T getVar() { return this.var; } public V getValue() { return this.value; } } class Demo<S> { private S info; public Demo(S info) { this.setInfo(info); } public void setInfo(S info) { this.info = info; } public S getInfo() { return this.info; } }
现在希望Demo类中info属性是Info类的这种类型。但是Info类本身使用的时候需要设置两个泛型。如下所示:
public class GenericsDemo { public static void main(String args[]) { Info<String, Integer> i = new Info<String, Integer>("张三", 30); Demo<Info<String, Integer>> d = new Demo<Info<String, Integer>>(i); System.out.println("内容一:" + d.getInfo().getVar()); System.out.println("内容二:" + d.getInfo().getValue()); } }
10.8 通配符
在开发中对象的引用传递是最常见的,但是在泛型类的操作中,进行引用传递的时候泛型类型必须匹配才可以传递,否则是无法传递的。例如:
class Info<T> { private T var; public void setVar(T var) { this.var = var; } public T getVar() { return this.var; } public String toString() { return this.var.toString(); } } public class GenericsDemo { public static void main(String args[]) { Info<String> i = new Info<String>(); i.setVar("xiaoxiao"); fun(i); } public static void fun(Info<Object> temp) { //接收Object泛型类型的Info对象 System.out.println("内容:" + temp); } }
由前面的知识我们知道,这里企图利用Info<Object>来接收Info<String>类型的变量,编译出错。
解决方法:
1. 泛型对象进行引用传递的时候,类型必须一致。将fun函数改成如下形式:
public static void fun(Info<String> temp){ System.out.println("内容:" + temp); }
2. 将fun()方法中Info参数的泛型取消掉(不建议):
public static void fun(Info temp){ System.out.println("内容:" + temp); }
3. 利用通配符:
public static void fun(Info<?> temp){ System.out.println("内容:" + temp); }
通配符“?”,表示可以接收任意的内容。但是使用“?”声明的对象,此时不能修改。如下:
public static void fun(Info<?> temp){ temp.setVar("star"); //不能修改,报错 System.out.println("内容:" + temp); }
所以使用“?”表示只能接收,不能修改。
10.8.1 受限泛型
之前设置泛型类型的时候,只要是类就可以任意设置。但是在Java的泛型中可以指定一个泛型的上限和下限。
在引用传递中,泛型操作中也可以设置一个泛型对象的范围上限和范围下限。范围上限使用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。而范围下限使用super关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object 类。
设置上限:
声明对象:类名称<? extends 类> 对象名称
定义类:[访问权限] 类名称<泛型标识 extends 类>{}
设置下限:
声明对象:类名称<? super 类> 对象名称
·设置上限实例(Info泛型类跟前面一样):
public class GenericsDemo { public static void main(String args[]) { Info<Integer> i1 = new Info<Integer>(); Info<Float> i2 = new Info<Float>(); i1.setVar(30); i2.setVar(30.1f); fun(i1); fun(i2); } public static void fun(Info<? extends Number> temp) { // 只能接收Number及其子类 System.out.println(temp); } }
如果你在main方法中增加下面几行,则编译报错,因为fun方法已经限制了上限,必须是Number类及其子类。如下:
Info<String> i3 = new Info<String>(); i3.setVar("xiaoxiao"); fun(i3); //报错
你还可以在声明Info的时候才用class Info<T extends Number>{}的形式,则此时利用Info<String> i3 = new Info<String>()就会报错。
定义泛型类时,如果写成下面的代码:
public class Info<T>{}
相当于下面的定义方式:
public class Info<T extends Object>{}
·设置下限实例:
当使用的泛型只能在本类及其父类类型上应用的时候,就必须使用泛型的范围下限来配置。如下:
public class GenericsDemo { public static void main(String args[]) { Info<String> i1 = new Info<String>(); // 声明String的泛型对象 Info<Object> i2 = new Info<Object>(); // 声明Object的泛型对象 i1.setVar("hello"); i2.setVar(new Object()); fun(i1); fun(i2); } public static void fun(Info<? super String> temp) {//只能接收String或Object类型泛型 System.out.print(temp + "、"); } }
在main方法中增加下面几行,则编译出错。因为fun已经设置了下限,则此时传递的必须是String类及其父类,如下:
Info<Integer> i3 = new Info<Integer>(); i3.setVar(30); fun(i3);
感谢阅读。如果感觉此章对您有帮助,却又不想白瞟