Java泛型是什么?
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
即其本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。
泛型带来了哪些好处?
在没有泛型之前,当我们将一个对象放进集合中,集合会立刻忘记该对象的类型,它会把所有对象都当作Object类型来处理。所以从集合中取出对象的时候,我们通常需要进行强制类型转换,这种做法不仅造成代码的臃肿,而且容易引起异常。
在增加了泛型支持后:
- 集合可以记住元素的类型,并且在编译的时候检查元素类型,避免引起ClassCastException异常。将运行时的异常提前至了编译时。
- 所有的强制转换都是自动和隐式的,提高代码的重用率。
- 使代码更加简洁,程序更加健壮。
定义泛型
Java在接口、类或类的方法的声明中,声明一个泛型。多个参数时要用逗号隔开。
泛型的声明需要写在接口名、类名之后,方法的返回值之前。
public class Student<T> {
...
}
public static <K, V> void update(K k, V v) {
...
}
定义时要注意三点:
1. 泛型的类型参数只能是引用类型,不能是基本类型。
2. 使用尖括号 <> 声明一个泛型。
3. <>里可以使用T、E、K、V等字母。这些对编译器来说都是一样的,可以是任意字母。只是程序员习惯在特定情况下用不同字母来区分:
T : Type (类型)
E : Element(元素)
K : Key(键)
V : Value(值)
泛型接口
1)泛型接口的定义
修饰符 interface 接口名<声明自定义泛型> {
...
}
举个栗子~
public interface List<E> {
void add(E x);
Iterator<E> iterator();
...
}
public interface Map<K, V> {
Set<K> setSet();
V put(K key, V value);
2)泛型接口要注意的事项
- A. 接口上自定义泛型的具体数据类型是在实现一个接口的时候指定的。
interface Dao<T> {
public void add(T t);
}
public class Demo implements Dao<String> {
@Override
public void add(String t) {
...
}
...
}
- B. 如果接口上自定义的泛型,在实现接口的时候没有指定具体的数据类型,就默认为Object类型。
interface Dao<T> {
public void add(T t);
}
public class Demo implements Dao {
@Override
public void add(Object t) {
...
}
...
}
- C. 如果实现一个接口的时候,还不明确目前要操作的数据类型,要等到创建接口实现类对象的时候才去指定泛型的具体数据类型。该怎么实现呢?
interface Dao<T> {
public void add(T t);
}
public class Demo<T> implements Dao<T> {
@Override
public void add(T t) {
...
}
public static void main(String[] args) {
Demo<String> d = new Demo<String>();
}
}
总结起来就是,如果要延长接口自定义泛型 的具体数据类型,那么格式如下:
修饰符 class 类名<声明自定义泛型> implements 接口名<声明自定义泛型> {
...
}
3)逻辑子类并不是真实的子类
比如我们有一个List泛型接口List<E>
,此时如果为E形参传入String类型实参,则产生了一个新的类型List<String>
,可以把List想象成E被全部替换成String的特殊List子接口。所以虽然程序只定义了一个List接口,但实际使用的时候会产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。必须要指出:List<String>
绝不会被替换成真正的接口,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态生成无数个逻辑上的子类,但这种子类在物理上并不存在。
泛型类
1)泛型类的定义
修饰符 class 类名<声明自定义泛型> {
...
}
2)泛型类要注意的事项
A. 在类上自定义泛型的具体数据类型是在使用该类时创建对象的时候确定的。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不能增加泛型声明。例如为
Apple<T>
类定义构造器,构造器名依然是Apple,而不是Apple<T>
。B. 如果一个类在类上已经声明了自定义泛型,但是使用该类创建对象的时候没有指定泛型的具体数据类型,那么默认为Object类型。
- C. 在类上自定义泛型不能作用于静态的方法,如果静态的方法需要使用自定义泛型,那么需要在方法上自己声明使用。是因为非静态方法是在创建对象后才能调用的;静态方法不通过创建对象来调用。此时,静态方法上声明的与类上声明的不会冲突,因为此方法具体数据类型是在调用该方法的时候传入实参时才确定具体的数据类型的。
- D. 当创建了带泛型声明的父类之后,可以从该父类派生子类,要注意的是,当使用父类声明子类时不能再包含类型形参。
3)并不存在泛型类
看一个例子,下面的代码应该打印什么呢?
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
System.out.println(l1.getClass() == l2.getClass());
可能你会认为应该输出false,但实际上答案是true。因为不管泛型的实际类型参数是什么,它们在运行的时候总有同样的类。
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。
public class R<T> {
static T info; //代码错误
T age;
public void foo(T msg){}
public static void bar(T msg){} //代码错误
}
由于系统不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。例如这是错误的:
if(cs instanceof java.util.ArrayList<String>) {...}
//编译错误
泛型方法
前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些类型形参可被当成普通类型来用。在另外一些情况下,比如,想要在在静态内容(静态方法)中使用泛型,或者类(或者接口)没有定义成泛型,但是就想在其中某几个方法中运用泛型(比如接受一个泛型的参数等)但定义方法时想自己定义类型形参,等等,这也是可以的。Java5还提供了对泛型方法的支持。
1)泛型方法的定义
修饰符 <声明自定义泛型> 返回值类型 方法名(形参列表) {
...
}
举个栗子~
//需求: 定义一个方法可以接收任意类型的参数,而且返回值类型必须要与实参的类型一致。
public class Demo {
public static void main(String[] args) {
String str = getData("asd");
Integer i = getData(123);
}
public static <T> T getData(T t){
return t;
}
}
2)泛型方法要注意的事项
- A. 泛型方法的定义和普通方法定义不同的地方在于,需要在修饰符和返回类型之间加一个泛型类型参数的声明,表明在这个方法作用域中谁才是泛型类型参数。
- B. 类型参数的作用域
class A<T> { ... }
中T的作用域就是整个A;
public <T> func(...) { ... }
中T的作用域就是方法func;
类型参数也存在作用域覆盖的问题,可以在一个泛型类、接口中继续定义泛型方法,例如:
class A<T> {
// A已经是一个泛型类,其类型参数是T
public static <T> void func(T t) {
// 再在其中定义一个泛型方法,该方法的类型参数也是T
}
}
//当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,内部覆盖外部,外部的同名变量是不可见的。
//除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,因此一般正确的定义方式是这样的:
class A<T> {
public static <S> void func(S s) {
}
}
- C. 方法中的泛型参数无须显式传入实际类型参数,编译器会根据传入的实参类型自动推断类型参数。
例如:<T> void func(T t){ ... }
隐式调用object.func("name")
,根据”name”的类型String推断出类型参数T的类型是String。当然也可以显式指定,类型参数要写在尖括号中并放在方法名之前,例如:object.<String> func("String")
- D. 在使用泛型方法时应避免歧义,例如:
<T> void func(T t1, T t2){ ... }
如果这样调用的话object.func("name", 15);
会有很大隐患,T到底应该是String还是Integer存在歧义。
类型通配符
1)引入
为了说明通配符的作用,我们先看个例子:
List<Object> list1 = new ArrayList<String>();
List<Object> list2 = new ArrayList<Integer>();
上面的调用都是编译不通过的。这说明想实现一个既可以打印list1,又可以打印list2的方法是不可能的:
public static void fun(List<Object> list) {
...
}
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
fun(list1);//编译不通过
fun(list2);//编译不通过
如果把fun()方法的泛型参数去除,那么就OK了。即不使用泛型。
public static void fun(List list) {
...
}//会有一个警告
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
fun(list1);
fun(list2);
上面代码是没有错了,但会有一个警告。警告的原因是你没有使用泛型。Java希望大家都去使用泛型。你可能会说,这里根本就不能使用泛型~
通配符就是专门处理这一问题的。
public static void fun(List<?> list) {
...
}
上面代码中的“?”就是一个通配符,它只能在“<>”中使用。这时你可以向fun()方法传递List<String>
、List<Integer>
类型的参数了。当传递List类型的参数时,表示给“?”赋值为String;当传递List类型的参数给fun()方法时,表示给“?”赋值为Integer。
2)通配符的缺点
上面的问题是处理了,但通配符也有它的缺点。在上面例子中,List
List<? extends Number> list;
其中<? extends Number>
表示通配符的下边界,即“?”只能被赋值为Number或其子类型。
public static void fun(List<? extends Number> list) {
...
}
fun(new ArrayList<Integer>()); //ok
fun(new ArrayList<Double>()); //ok
fun(new ArrayList<String>()); //不ok
当fun()方法的参数为List<? extends Number>
后,说明你只能赋值给“?”Number或Number的子类型。虽然这多了一个限制,但也有好处,因为你可以用list的get()方法。就算你不知道“?”是什么类型,但你知道它一定是Number或Number的子类型。
所以:Number num = list.get(0)
是可以的。但是,还是不能调用list.add()方法。
4)带有下边界的通配符
List<? super Integer> list;
其中<? super Integer>
表示通配符的下边界,即“?”只能被赋值为Integer或其父类型。
public static void fun(List<? super Integer> list) {
...
}
fun(new ArrayList<Integer>()); //ok
fun(new ArrayList<Number>()); //ok
fun(new ArrayList<Object>()); //ok
fun(new ArrayList<String>()); //不ok
这时再去调用list.get()方法还是只能使用Object类型来接收:Object o = list.get(0)
。因为你不知道“?”到底是Integer的哪个父类。但是你可以调用list.add()方法了,例如:list.add(new Integer(100))是正确的。因为无论“?”是Integer、Number、Object,list.add(new Integer(100))都是正确的。
5)通配符小结
1. 方法参数带有通配符会更加通用;
2. 带有通配符类型的对象,被限制了与泛型相关方法的使用;
3. 上边界通配符:可以使用参数为泛型变量的方法。
4. 下边界通配符:可以使用返回值为泛型变量的方法;
6)应用实例
import java.util.ArrayList;
import java.util.List;
public class Demo {
public void fun1() {
Object[] objArray = new String[10]; //正确
//objArray[0] = new Integer(100);
//错误
//编译器不会报错,但是运行时会抛ArrayStoreException
//List<Object> objList = new ArrayList<String>();
//错误
//编译器报错,泛型引用和创建两端,给出的泛型变量必须相同
}
public void fun2() {
List<Integer> integerList = new ArrayList<Integer>();
print(integerList);
List<String> stringList = new ArrayList<String>();
print(stringList);
}
/*
* 其中的?就是通配符
* ?它表示一个不确定的类型,它的值会在调用时确定下来
* 通配符只能出现在左边,即不能在new时使用通配符
* List<?> list = new ArrayList<String>();
* 通配符好处:可以使泛型类型更加通用,尤其是在方法调用时形参使用通配符
*/
public void print(List<?> list) {
//list.add("hello");
//错误
//编译器报错,当使用通配符时,对泛型类中的参数为泛型的方法起到了副作用,不能再使用
Object s = list.get(0);//正确
}
public void fun3() {
List<Integer> intList = new ArrayList<Integer>();
print1(intList);
List<Long> longList = new ArrayList<Long>();
print1(longList);
}
/*
* 给通配符添加了限定:
* 只能传递Number或其子类型
* 子类通配符对通用性产生了影响,但使用形参更加灵活
*/
public void print1(List<? extends Number> list) {
//list.add(new Integer(100));
//错误
//编译器报错,说明参数为泛型的方法还是不能使用(因为?也可能为Long型)
Number number = list.get(0);//正确
}
public void fun4() {
List<Integer> intList = new ArrayList<Integer>();
print2(intList);
List<Number> numberList = new ArrayList<Number>();
print2(numberList);
List<Object> objList = new ArrayList<Object>();
print2(objList);
}
/*
* 给通配符添加了限定
* 只能传递Integer类型,或其父类型
*/
public void print2(List<? super Integer> list) {
list.add(new Integer(100)); //正确
Object obj = list.get(0); //正确
}
}
泛型的擦除与转换
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都被扔掉。比如一个List<String>
类型被转换为List,则该List对集合元素的类型检查变成了类型参数的上限(Object)。
下面这段程序示范了这种“擦除”:
class Applee<T extends Number> {
T size;
public Applee() {}
public Applee(T size) {
this.size = size;
}
public void setSize(T size) {
this.size = size;
}
public T getSize() {
return this.size;
}
}
public class ErasureTest {
public static void main(String[] args) {
Applee<Integer> a = new Applee<>(6);
//a的getSize()方法返回Integer对象
Integer as = a.getSize();
//把a对象赋给Applee变量,丢失尖括号里的类型信息
Applee b = a;
//b只知道size的类型是Number
Number size1 = b.getSize();
//下面代码编译错误
//Integer size2 = b.getSize();
}
}
我们定义了一个带泛型声明的Applee类,其类型形参的上限是Number,用来定义Applee类的size变量。然后创建了一个Applee对象a,传入了Integer作为类型形参的值,所以调用a的getSize方法返回Integer的值。当把a赋给一个不带泛型信息的b变量时,编译器就会丢失a对象的泛型信息,因为编译器不知道具体是Number的哪个子类。
从逻辑上来看,List<String>
是List的子类,如果直接把一个List对象赋给一个List<String>
对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个List<String>
,编译器仅仅提示“unchecked”未经检查的转换:
import java.util.ArrayList;
import java.util.List;
public class ErasureTest2 {
public static void main(String[] args) {
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
//下面代码引起警告“unchecked”
List<String> ls = list;
//但只要访问ls里的元素,就引起运行时异常
//System.out.println(ls.get(0));
}
}
上面程序中定义了List<Integer>
对象,这个List对象保留了集合元素的类型信息。当把这个List对象赋给一个List类型的list后,编译器就会丢失前者的泛型信息,这就是典型的“擦除”。Java又允许直接把List对象赋给一个List<T>
类型的变量,所以只会发出警告。但会引起运行时异常。下面代码也是同理:
public class ErasureTest2 {
public static void main(String[] args) {
List li = new ArrayList();
li.add(6);
li.add(9);
System.out.println((String)li.get(0));
}
}
泛型与数组
Java泛型有一条很重要的设计原则——
如果一段代码在编译时没有提出”unchecked”警告,则运行时不会引发ClassCastException异常。
正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。也就是说,能声明但不能创建。
List<String>[] las = new List<String>[10];
//这是错误的
List<String>[] las = new ArrayList[10];
//编译有unchecked警告
//编译器不保证这样做是安全的
创建元素类型是类型变量的数组对象也导致编译错误:
<T> T[] makeArray(Collection<T> coll) {
return new T[coll.size()];
}
由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器报错。
关于泛型的一些重点就是以上这些~