泛型的定义
泛型是Java编程语言中的一种特性,其主要作用就是允许代码在编译时进行检查,如果发生错误会给出提示,从而提高了代码的类型安全性和可重用性。
泛型时JDK1.5推出的新特性,java允许定义泛型类型、泛型接口、泛型方法,javaAPI中对一些类和接口的使用进行了一些调整。
package java.lang; public interface Comparable{ public int compareTo(Object o); } | package java.lang; public interface Comparable<T>{ public int compareTo(T o); } |
JDK1.5前 JDK1.5后
解释一下上述中<T> : 为形式泛型类型(formal generic type),可以来理解为一种“类型占位符”,就是我不确定你是什么类型,我先给你占个位置,到要使用的时候才能知道具体的类型。咋这个具体类型就叫做实际具体类型(actual concrete type)。替换泛型类型称为泛型实例化.(generic instantiation)。按照约定俗成惯例,像T或者E这样单个大写字母用于表示泛型类型。
注意:泛型类型时引用数据类型,而不是基本数据类型,不能使用int ,long或者char等基本数据类型来替换泛型类型。
泛型的优点
对于泛型的优点有以下几点:
1、类型安全 泛型确保了类型安全性,即在编译环节就进行错误检查。
2、代码重用 编写一次代码,就可以在多种类型数据上进行使用。
3、减少强制类型的转换 泛型消除在集合中使用强制转换类型的需求。
4、提高代码的可阅读性 泛型代码通常比非泛型代码更易于阅读和理解。
定义泛型类和接口
在java中,可以为类或者接口定义泛型,使用该类来创建对象(或者引用变量)时,必须使用具体的实际类型来替换它。
定义泛型类
public class ClassName<T1,T2,T3...,Tn>{
//属性
//方法
...
}
定义泛型接口
public interface InterfaceName<T1, T2, ..., Tn> {
// 接口体
}
在上述的定义中,ClassName 为类名, InterfaceName 接口名,尖括号里面的:T1,T2,T3....Tn 就是泛型参数类型,他们可以是任何的类型,包括基本类型(不包含基本数据类型哦)和自定义类型。
下面使用泛型类和接口进行一个举例:
//定义泛型类
public class Box<T>{
private T item;
public void setItem(T item){
this.item = item;
}
public T getItem(){
return item;
}
}
//定义泛型接口
public interface Storage<T>{
void add(T item);
T get(int index);
int size();
}
对于泛型类和接口的使用
// 使用泛型类
Box<Integer> integerBox = new Box<>();
integerBox.setItem(10);
int value = integerBox.getItem(); // value 的类型是 Integer
// 使用泛型接口
Storage<String> stringStorage = new Box<>();
stringStorage.add("Hello");
stringStorage.add("World");
String firstString = stringStorage.get(0); // firstString 的类型是 String
在例子中,我们还使用了泛型方法,将在下面进行分析,不要着急。
值得注意的还是那句话:在创建对象时,或者使用类或者接口声明引用变量时,必须指明具体的数据类型(就像上述代码中的String 和 Integer )。
泛型方法
在java中,泛型方法可以允许你编写可以与任何数据类型一起工作的方法,具体定义泛型方法如下:
public <T> returnType methodName(T parameter) {
// 方法体
}
在此方法中,<T>声明了一个类型参数T,它可以在方法返回类型和参数列表中使用。
public class GenericMethods {
// 定义一个泛型方法
public static <T> T getMiddleValue(T[] array) {
return array[array.length / 2];
}
public static void main(String[] args) {
// 使用泛型方法
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {"a", "b", "c", "d", "e"};
Integer middleInt = getMiddleValue(intArray);
String middleString = getMiddleValue(stringArray);
System.out.println("Middle integer: " + middleInt);
System.out.println("Middle string: " + middleString);
}
}
在这个例子中,getMiddleValue
方法是一个泛型方法,它接受一个泛型数组作为参数,并返回数组中间的元素。在main
方法中,我们分别使用getMiddleValue
方法处理整数数组和字符串数组,并打印出中间的元素。
可以将泛型指定为另外一种类型的子类型,这样的泛型类型称为受限的(bounded)。
public class BoundedTypeDemo {
public static void main(String[] args ) {
Rectangle rectangle = new Rectangle(2, 2);
Circle circle = new Circle(2);
System.out.println("Same area? " + equalArea(rectangle, circle));
}
public static <E extends GeometricObject> boolean equalArea(E object1, E object2) {
return object1.getArea() == object2.getArea();
}
}
原始类型和向后兼容
没有指定具体类型的泛型类或者泛型接口就是原始类型,用于早期的java版本,向后兼容。
GnericStack stack = new GnericStock( ); |
等价于
GnericStock <object> stack = new GnericStock< >( ); |
像这样不带类型参数的GnericStock 泛型称为原始类型(raw type),使用原始类型向后兼容java早期版本。
通配泛型
在Java中,统配泛型用于表示泛型类型的未知部分,允许我们在不指定具体类型的情况下使用泛型,有两种常见的通配泛型:
1、无界通配符(又叫非受限通配,unbounded wildcard)
使用 ? 表示,他表示任何类型,通常用于方法参数,以便接受任何类型的对象。它和 ? extends Obiect 是一样的。
public void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
2、有界通配符
- 上限受限通配(bounded wildcard) 使用<? extends T >表示它表示任何类型T 或者T的子类型。
public void printList(List<? extends Number> list) { for (Number n : list) { System.out.println(n); } }
- 下限受限通配(lower-bounded wildcard)使用< ? super T > 表示任何类型T 或者T的父类型。
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
// ...
}
泛型类型和通配类型之间的关系
消除泛型和对泛型的限制
编译器可以使用泛型信息,但这些信息在运行时是不可用的。这被称为类型消除。
泛型是使用一种叫做类型消除(擦除)的方法来实现的。编译器使用泛型类型信息来编译代码,但是随后会消除它。因此,泛型信息在运行时是不可用的。这种方法可以使泛型代码向后兼容使用原始类型的遗留代码。
泛型存在于编译时。一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型。例如:编译器检查左边的代码里泛型是否被正确使用,然后将它翻译成右边的代码
ArrayList<String> list = new ArrayList<>(); list.add(“peppa”); String name = list.get(0); | ArrayList list = new ArrayList(); list.add(“peppa”); String name = (String)list.get(0); |
当编译泛型类、接口和方法时,编译器用Object类型代替泛型类型.如下翻译:
public static <E> void print(E[] list){ for(int i = 0; i < list.length; i++) System.out.print(list[i] + “ “); System.out.println(); } | public static void print(Object[] list){ for(int i = 0; i < list.length; i++) System.out.print(list[i] + “ “); System.out.println(); } |
如果一个泛型类型是受限的,那么编译器就会使用该受限类型来替换它。例如:
public static <E extends Geometric> boolean equalArea(E o1,E o2){ return o1.getArea() == o2.getArea(); } | public static boolean equalArea(Geometric o1, Geometric o2){ return o1.getArea() == o2.getArea(); } |
非常需要注意的是,不管实际的具体类型是什么,泛型类是被它的所有实例所共享的。
ArrayList<String> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); |
尽管在编译时ArrayList<String>和ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList类会被加载到JVM中。list1和list2都是ArrayList的实例。因此:
Sout:list1 instanceof ArrayList //true Sout:list2 instanceof ArrayList //true |
然而表达式list1 instanceof ArrayList<String>是错误的。由于ArrayList<String>并没有在JVM中存储为单独一个类,所以,在运行时使用它是毫无意义的。
由于泛型类型在运行时被擦除,因此,对于如何使用泛型类型是有一些限制的:
限制1:不能使用new T()
运行时泛型T是不可用的。
限制2:不能使用new T[]
可以通过创建一个Object的数组,然后将它的类型转换为T[]来规避这个限制,
E[] elements = (E[])new Object[capacity];
类型转换到E[]会导致一个免检的编译警告,因为编译器无法确保在运行时类型转换是否能成功,这种类型的编译警告是使用java泛型的不足之处,也是无法避免的。
使用泛型类型创建泛型数组也是不允许的。
GenericStack<String>[] stack = new GenericStack<String>[10];//错误的
可以修改为下面代码来规避,但是会有编译警告
GenericStack<String>[] stack = (GenericStack<String>[])new GenericStack [10];
限制3:在静态上下文中不允许类的参数是泛型类型
由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。因此,在静态方法、数据域或者初始化的语句中,为类引用泛型类型参数是非法的。
限制4:异常类不能是泛型的