目录
- 为什么需要泛型(泛型的意义)
- 泛型类、泛型接口、泛型方法
- 类型参数的限定
- Java的泛型擦除
- 约束与局限
- 通配符
为什么需要泛型(泛型的意义)
1.相同代码可以适用多种数据类型
2.泛型中的类型在使用时指定,不需要手动进行强制类型转换。
//Fruit抽象类
public abstract class Fruit {
private String name;
public Fruit(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//继承Fruit的子类
public class Apple extends Fruit {
public Apple(String name) {
super(name);
}
}
//继承Fruit的子类
public class Orange extends Fruit {
public Orange(String name) {
super(name);
}
}
//测试类
public class TestFruit1 {
private Fruit fruit;
public TestFruit1(Fruit fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
TestFruit1 testFruit = new TestFruit1(apple);
System.out.println(testFruit.fruit.getName());
Orange orange = new Orange("Orange");
TestFruit1 testFruit = new TestFruit1(apple);
System.out.println(testFruit.fruit.getName());
}
}
这里定义了一个抽象的类Fruit,Apple和Orange类都继承至Fruit。我们在TestFruit中声明了一个Fruit类型的变量fruit。如果想使用fruit变量,我们需要实例化一个Fruit的子类实例并赋值给fruit变量,这看起来好像很正常。但是,如果这时我们需要一个String类型的fruit怎么办呢?显然fruit只能持有Fruit类型的变量。你可能会将TestFruit进行这样的修改:
//测试类
public class TestFruit2 {
private Object fruit;
public TestFruit2(Object fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
TestFruit2 testFruit = new TestFruit2(apple);
System.out.println(((Fruit)testFruit.fruit).getName());
TestFruit2 strFruit = new TestFruit2("String value");
}
}
上述的方式有一个坑,强制类型转换可能会出现类型转换异常。因为fruit可以接受任意类型的数据,而使用时需要将fruit转为具体类型才能使用,这可能出现赋值类型和转换类型不匹配的错误。下面使用泛型来实现:
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
TestFruit3<Fruit> testFruit1 = new TestFruit3<>(apple);
System.out.println(testFruit1.fruit.getName());
String a = "String value";
TestFruit3<String> testFruit2 = new TestFruit3<>(a);
System.out.println(testFruit2.fruit);
}
}
TestFruit3类使用了一个泛型参数T来声明fruit变量的类型,在使用时指定了fruit变量的类型,无需强制类型转换就可直接使用fruit变量。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节
泛型类、泛型接口、泛型方法
泛型类:
public class GenericClass<A,B> {
private A first;
private B second;
public GenericClass(A first,B second) {
this.first = first;
this.second = second;
}
public static void main(String[] args) {
GenericClass<String,Integer> twoGeneric = new GenericClass<>("first", 1);
System.out.println("first = " + twoGeneric.first);
System.out.println("second = " + twoGeneric.second);
}
}
GenericClass类中声明了两个泛型,说明泛型类是可以有多个类型变量的。
泛型接口:
//泛型接口,用于生成不同类型的对象
public interface Generator<T> { T next();}
//实现泛型接口,能够随机生成不同类型的Fruit对象
public class FruitGenerator implements Generator<Fruit> {
//类对象数组
//HongFuShi是继承至Apple的子类
private Class[] types = {Apple.class, Orange.class, HongFuShi.class};
private static Random rand = new Random(47);
public Fruit next() {
try {
//通过随机数获取类的类对象,再使用反射方法创建类的实例
return (Fruit) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
FruitGenerator fg = new FruitGenerator();
for (int i = 0; i < 3; i++) {
System.out.println(fg.next());
}
}
}
泛型化的Generator接口确保next()方法的返回值是参数的类型。
泛型方法
//普通方法,使用了类中声明的泛型参数T作为它的形参类型和返回值类型
private T generalMethod(T t) {
System.out.println("普通方法传入的值 t = " + t);
return t;
}
//泛型方法,声明了一个类型参数列表作为它的形参类型和返回值类型
private <T,K> T genericMethod(T... t, K k) {
System.out.println("泛型方法传入的值 t = " + t);
System.out.println("泛型方法传入的值 k = " + k);
return t;
}
上面两个方法展示了泛型方法与普通方法的区别:泛型方法需要使用"<>"来声明类型参数。类型参数可以出现在泛型方法的任意位置。类型参数可以有多个。类型参数也可以与可变参数结合使用。
类型参数的限定
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
TestFruit3<Fruit> testFruit1 = new TestFruit3<>(apple);
System.out.println(testFruit1.fruit.getName());
String a = "String value";
TestFruit3<String> testFruit2 = new TestFruit3<>(a);
//编译不过,应为String没有getName()方法
//System.out.println(testFruit2.fruit.getName());
}
}
上面例子中的类型参数T可以是任意的类型,但有时候我们的业务逻辑可能需要这个类型参数执行特定的任务。比如调用getName(),但显然当我们指定T的类型是String时它就没有这个方法。所以我们需要给类型参数T一个限定,让它能始终符合我们后续业务的要求。
//限定类型参数T必须是类型Fruit或Fruit的子类类型
public class TestFruit3<T extends Fruit,Serializable> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
TestFruit3<Fruit> testFruit1 = new TestFruit3<>(apple);
System.out.println(testFruit1.fruit.getName());
String a = "String value";
//
TestFruit3<String> testFruit2 = new TestFruit3<>(a);
//编译不过,应为String没有getName()方法
//System.out.println(testFruit2.fruit.getName());
}
//泛型方法的类型参数可以和所属类的类型参数同名
private <T extend String, K> T genericMethod(T... t, K k) {
System.out.println("泛型方法传入的值 t = " + t);
System.out.println("泛型方法传入的值 k = " + k);
return t;
}
}
从上述例子可以基本了解类型参数限定:1.限定类型可以有多个,但执会将第一个作为类型限定;2.需要使用限定符“extends”,指定类型参数的类型时类型参数的类型必须是限定类型或限定类型的子类类型。
Java的泛型擦除
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
Apple apple = new Apple("apple");
Class c1 = new TestFruit3<Fruit>(apple).getClass();
String a = "String value";
Class c2 = new TestFruit3<String>(a).getClass();
//返回结果会是什么?
System.out.println(c1 == c2);
//查看运行时的类型参数
System.out.println(Arrays.toString(c1.getTypeParameters()));
System.out.println(Arrays.toString(c2.getTypeParameters()));
}
}
TestFruit3<Fruit>和TestFruit3<String>很容易被认为是不同的类型。不同的类型在行为方面肯定不同,例如TestFruit3<Fruit>可以调用getName()方法,而TestFruit3<String>无法调用。但是TestFruit3<Fruit>和TestFruit3<String>在程序中被认为是相同类型。这是因为Java的泛型是使用擦除来实现的,在被编译后TestFruit3<Fruit>和TestFruit3<String>都变成了TestFruit3。
Java泛型是使用擦除来实现的,这意味着当我们使用泛型时,任何具体的类型信息都被擦除了,只知道诸如类型参数标识符和类型参数边界这类信息。
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
//编译错误,此时无法获取具体类型,擦除后的T将变成Object
fruit.getName();
}
}
public class TestFruit3<T extends Fruit> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
//编译通过,可以获取边界信息,擦除后的T将变成Fruit
fruit.getName();
}
}
约束与局限
1.不能用基本类型实例化类型参数
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
public static void main(String[] args) {
//编译报错
//TestFruit3<int> testFruit = new TestFruit3<>(1);
//编译通过
TestFruit3<Integer> testFruit = new TestFruit3<>(1);
}
}
类型参数T在擦除之后将被转为Object,而Object无法存储基本类型的值。解决这个问题可以使用包装器类型来替代或使用独立的类和方法处理。
2.运行时类型查询只适用于原始类型
if (testFruit instanceof TestFruit3<Integer>) {}//编译报错
if (testFruit instanceof TestFruit3<T>) {}//编译报错
if (testFruit instanceof TestFruit3) {}//编译通过
这也间接说明,TestFruit3<Integer>经过泛型类型擦除后的类型是TestFruit3。
3.不能创建参数化的数组
//编译报错
//TestFruit3<String>[] fruit3s = new TestFruit3<String>[10];
//编译通过
TestFruit3<String>[] fruit3s = new TestFruit3[10];
可以声明类型参数化的数组变量,但是不能使用new TestFruit3<String>[10]初始化这个变量。
//源码,假设可以编译通过
TestFruit3<String>[] fruit3s = new TestFruit3<String>[10];
//编译后经过擦除
TestFruit3[] fruit3s = new TestFruit3[10];
数组会记住它的元素类型,如果存储其他类型的元素,就会抛出一个ArrayStoreExecption异常。
Object[] objArray = fruit3s;
//运行时报错,只能存放TestFruit3
objArray[0] = "String value";
对于泛型类型,擦除会使这种机制失效:
objArray[0] = new TestFruit3<Integer>(1);
能够通过数组的存储检查,但是明显他和我们创建的数组TestFruit3<String>[]明显类型参数不匹配。当我们使用数组时,就会出错:
//会抛出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String的错误
System.out.println(fruit3s[1].fruit.length());
4.静态方法无法使用泛型类的类型参数
public class TestFruit3<T> {
private T fruit;
public TestFruit3(T fruit) {
this.fruit = fruit;
}
//编译报错
private static void staticMethod(T fruit) {
System.out.println(fruit);
}
//编译通过
private static <T> void staticMethod(T fruit) {
System.out.println(fruit);
}
}
原因在于类的静态成员会在类加载时被加载,而类的类型变量只有在类被实例化时才指定具体类型。如果我们在实例化前调用类的静态方法,此时类型变量的类型还不确定,程序将没办法继续执行。为了避免这个问题,Java编译时会检查这个错误。假设编译器不检查这个错误,经过类型擦除后会怎样呢:
//如果编译通过,类型变量被擦除
public class TestFruit3 {
private Object fruit;
public TestFruit3(Object fruit) {
this.fruit = fruit;
}
private static void staticMethod(Object fruit) {}
}
程序将无法工作,因为Object类型的成员将无法进行任何业务相关的逻辑。
5.不能实例化类型变量
public class TestFruit3<T> {
//编译不通过
private T fruit = new T();
}
类型擦除将T变成Object,而且,我们的本意肯定不希望调用new Object()。
6.不能抛出或捕获泛型类的实例
//泛型类甚至不能扩展Exception/Throwable
public class Problem<T> extends Exception{
//不能捕获泛型类的实例
public <T extends Throwable> void doWork(T t) {
try{
} catch(T e) {
}
}
}
//但是可以这样:
public class Problem {
public <T extends Throwable> void doWork(T t) throws T{
try{
} catch(Thrwoable e) {
throw t;
}
}
}
通配符
为什么需要通配符:
//打印泛型类
public static void print(TestFruit<Fruit> p) {
System.out.print(p)
}
public static void main(String[] args) {
//编译报错
//print(new TestFruit3<Apple>(new Apple()));
print(new TestFruit3<Fruit>(new Fruit()));
}
由上面的例子发现TestFruit3<Fruit>和TestFruit3<Apple>不能作为同一种类型使用,虽然Fruit和Apple之间存在继承关系。我们常用的List容器也是这样的:
//编译报错
List<Fruit> fruitList = new ArrayList<Apple>();
//数组可以记录定义的类型
Fruit[] fruit = new Apple[10];
数组是Java中完全定义的,可以在编译器和运行时进行类型检查,但泛型编译期和运行时系统都不知道我们使用了什么类型。
//打印泛型类
public static void print(TestFruit<?> p) {
System.out.print(p)
}
public static void main(String[] args) {
print(new TestFruit3<Apple>(new Apple()));
print(new TestFruit3<Fruit>(new Fruit()));
}
通配符“?”可以告诉编译器TestFruit的泛型可以是任意类型。这种通配符的使用方式叫无限顶通配符,它可以是任意的类型,它的意义在于说明我使用了一个带类型参数的TestFruit类来作为print方法的参数,而不能依据泛型进行任何的操作。
如果我们需要操作TestFruit的泛型,需要给他一个限定:
public static void print(TestFruit<? extends Fruit> p) {
System.out.print(p);
System.out.print(p.fruit.getName());
}
上面我们限定了通配符的类型上界,即只能是Fruit或其子类,所以它可以安全的调用Fruit提供的方法(读安全)。
通配符限定还有一种形式叫超类型限定:
public static void print2(TestFruit<? super Fruit> p) {
System.out.print(p);
//编译报错,无法操作
System.out.print(p.fruit.getName());
//编译通过,因为fruit至少是Fruit类型,它的子类实例当然可以安全的赋值给fruit变量
p.fruit = new Apple();
}
超类限定符可以保证传入的类型参数必须是Fruit本身或其超类,所以对于类型变量fruit来说可以安全的赋值。同样的道理,fruit不能保证传入的类型参数一定有getName()方法,即不能安全的读Fruit的成员。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。