引言
泛型在java中使用范围很广,可能我们不会总去自己构造泛型类或者泛型接口,但是在使用集合中的一些类库例如List、Map时,就会无形间用到它。作为jdk1.5之后提供的功能,泛型在实现上很有特点,这些特点导致我们在使用时必须能够理解它的实现机制,从这篇文章开始,我们由浅入深地介绍泛型。
作为泛型系列的第一篇文章,我们只需要从大体上了解泛型的作用、泛型的简单定义(泛型类、泛型接口和泛型方法)即可。
泛型的作用
保证类型安全
在java1.5之前,如果我们想让一个类去持有任意类型的对象,可以通过声明Object类的对象来实现,看下面的例子:
public class ObjectHolder {
private Object a;
public ObjectHolder(Object a) {
this.a = a;
}
public Object getA() {
return a;
}
public void setA(Object a) {
this.a = a;
}
public static void main(String[] args) {
ObjectHolder oh = new ObjectHolder("a string");
String s = (String) oh.getA();
oh.setA(1);
Integer integer = (Integer) oh.getA();
}
}
ObjectHolder中声明了一个Object类型的a作为被持有的对象,这样,我们可以向其传入任何引用类型的对象。在main方法中,我们传入的是String类型的对象,之后通过get方法得到它,然后再通过set方法将持有的对象设置为Integer类型,再通过get方法得到它。
这样,我们可以任意放入和改变ObjectHolder类持有的对象的类型。但是,因为a在定义时就是Object类型的,所以get方法的返回值肯定是Object类型,因此,如果我们想得到a的真正类型,就得使用强制类型转换来实现。所以,当我将main方法这样修改一下:
public static void main(String[] args) {
ObjectHolder oh = new ObjectHolder("a string");
Integer integer = (Integer) oh.getA();
}
这段代码能够编译通过,但是会在程序运行期间出现问题:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at person.andy.concurrency.generation.ObjectHolder.main(ObjectHolder.java:26)
因为我们进行了不正确的类型转换。
所以使用ObjectHolder虽然可以实现持有任何类型对象的目的,但是会带来运行期间出现类型转换异常的风险,这个可能出现的错误不会在编译期间被发现。
泛型的出现,可以解决这个问题:
public class GenericHolder1<T> {
private T t;
public GenericHolder1(T t) {
this.t = t;
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
GenericHolder1<String> genericHolder1 = new GenericHolder1<>("we");
String t = genericHolder1.getT();
//这样是不能通过编译的。
//Integer i = genericHolder1.getT();
}
这是泛型类定义的语法,在类名后面使用尖括号<>将类型参数(这里是T)包围。在创建泛型类的对象时,我们必须指明要持有的对象的类型。正如GenericHolder1
在通过get方法获取t时,没有进行强制类型转换,返回值直接就是String。
被注释的代码是不能编译通过的,因为编译器知道getT方法返回的是String类型。
通过这样一个简单的泛型类,我们实现了与ObjectHolder相同的功能,并且在编译期间保证了类型安全,运行期不会出现错误的类型转换。
当然,泛型的作用不仅仅是解决ObjectHolder的问题,我们只是借这个例子引出泛型以及泛型的优点。下面看使用泛型的另一个例子:
代码重用
有时候一个方法需要能返回多个对象,而return语句只能返回单个对象,我们可以通过创建一个对象并用这个对象打包想要返回的每个对象来解决这个问题。每次需要一个对象组合的时候,我们都可以创建一个特定的类。但是有了泛型之后,我们就可以不用重复的工作。
首先,定义一个打包两个对象的泛型类:
public class Tuple2<A,B> {
public final A a1;
public final B a2;
public Tuple2(A a, B b) { a1 = a; a2 = b; }
public String rep() { return a1 + ", " + a2; }
@Override
public String toString() {
return "(" + rep() + ")";
}
public static void main(String[] args) {
Tuple<String,Integer> tuple = new Tuple2<>("a string",12);
String a1 = tuple2.a1;
Integer a2 = tuple2.a2;
}
}
通过继承创建更多的任意组合的泛型类:
public class Tuple3<A, B, C> extends Tuple2<A, B> {
public final C a3;
public Tuple3(A a, B b, C c) {
super(a, b);
a3 = c;
}
@Override
public String rep() {
return super.rep() + ", " + a3;
}
}
public class Tuple4<A, B, C, D>
extends Tuple3<A, B, C> {
public final D a4;
public Tuple4(A a, B b, C c, D d) {
super(a, b, c);
a4 = d;
}
@Override
public String rep() {
return super.rep() + ", " + a4;
}
}
然后我们可以这样使用:
public static void main(String[] args) {
Tuple2<String,Integer> tuple2 = new Tuple2<>("tuple2",2);
Tuple3<String,Integer, SimpleObject> tuple3 = new Tuple3<>("tuple3",3,new SimpleObject());
Tuple4<String,Integer, SimpleObject,Object> tuple4 = new Tuple4<>("tuple3",3,new SimpleObject(),new Object());
System.out.println(tuple2);
System.out.println(tuple3);
System.out.println(tuple4);
}
输出如下:
(tuple2, 2)
(tuple3, 3, person.andy.concurrency.classload.SimpleObject@511d50c0)
(tuple3, 3, person.andy.concurrency.classload.SimpleObject@60e53b93, java.lang.Object@5e2de80c)
上面举的两个例子只说明了泛型的一部分作用,实际上,泛型的作用不仅这些,我们这里不再一一详细介绍。
上面的两个例子用到的都是泛型类,我们应该知道了怎么去定义一个简单的泛型类了。接下来来看泛型接口和泛型方法。
泛型接口
泛型接口的定义形式与泛型类基本一致。不过泛型接口需要我们实现之后才能使用。
jdk本身就提供了很多泛型接口,例如Supplier、BiFunction等。我们可以实现这些接口,然后在实现类中指定具体类型或者不指定,例如下面的Fibonacci实现了Supplier接口并且指定了精确类型为Integer。
public class Fibonacci implements Supplier<Integer> {
private int count = 0;
@Override
public Integer get() {
return fib(count++);
}
private int fib(int n){
if(n < 2) {
return 1;
}
return fib(n-2) + fib(n-1);
}
public static void main(String[] args) {
Stream.generate(new Fibonacci()).limit(18).map(n ->n +" ").forEach(System.out::println);
}
}
这里,我们指定了精确类型(这里就是Integer),如果指定了精确类型,那么所有用到泛型参数的地方,都会被替换为我们指定的精确类型。
当然,我们也可以选择不去指定精确类型:
public class Fibonacci1<T> implements Supplier<T> {
@Override
public T get() {
return null;
}
}
当没有指定精确类型时,get方法的返回值类型还是泛化类型T。
泛型方法
除了类和接口,方法也能被泛化。要定义泛型方法,需要将泛型参数列表放置在返回值之前,例如下面的例子:
public class GenericMethods {
public <T> T getObject(T t){
return t;
}
public static void main(String[] args) {
Integer object = genericMethods.getObject(1);
String qwe = genericMethods.getObject("qwe");
SimpleObject object1 = genericMethods.getObject(new SimpleObject());
}
}
getObject方法是一个泛型方法,有一个泛型参数T,它的参数和返回值是被泛化的,我们在调用这个方法的时候,同样不需要进行强制类型转换,返回的直接就是我们需要的类型。
在泛型类中定义泛型方法有些需要注意的地方:
public class GenericMethods2 {
static class Animal {
@Override
public String toString() {
return "Animal";
}
}
static class Dog extends Animal {
@Override
public String toString() {
return "Dog";
}
}
static class Fruit {
@Override
public String toString() {
return "Fruit";
}
}
static class GenericClass<T> {
public void show01(T t) {
System.out.println(t.toString());
}
public <T> void show02(T t) {
System.out.println(t.toString());
}
public <K> void show03(K k) {
System.out.println(k.toString());
}
}
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Fruit fruit = new Fruit();
GenericClass<Animal> genericClass = new GenericClass<>();
//泛型类在初始化时限制了参数类型
genericClass.show01(dog);
// genericClass.show01(fruit);
//泛型方法的参数类型在使用时指定
genericClass.show02(dog);
genericClass.show02(fruit);
genericClass.<Animal>show03(animal);
genericClass.<Animal>show03(dog);
genericClass.show03(fruit);
// genericClass.<Dog>show03(animal);
}
}
在上面的示例中,GenericClass有三个方法,show01、show02和show03。show01实际上不是一个泛型方法,因为他的声明中没有给出类型变量,也就是没有
小结
这篇文章,我们简单介绍了泛型的使用,知道了泛型类、泛型接口和泛型方法怎样定义,并且知道了不管是泛型类、泛型接口还是泛型方法,都会在编译期间保证类型安全,避免了运行期强制类型转换可能出现的问题。但是,泛型的实现机制是怎样的,它怎样保证类型安全,我们将在下一节详细介绍。