所谓泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态指定(即传入实际的类型参数,也可叫做类型实参)。
一、定义泛型接口、类
下面是Java 5改写的List接口、Iterator接口和Map接口:
//定义接口时,创建一个泛型形参,该形参名为E
public interface List<E>
{
//在接口里E可以作为类型使用
//下面方法可以使用E作为参数类型
void add(E x);
Iterator<E> iterator();
}
//定义接口时指定一个泛型形参,该形参名为E
public interface Iterator<E>
{
//在接口里完全可以作为类型使用
E next();
boolean hasNext();
...
}
//定义接口时指定两个泛型参数,其形参名为K,V
public interface Map<K,V>
{
//在接口里定义的K,V完全可以作为类型使用
Set<k> keySet()
V put(K key,V value)
...
}
泛型的实质:允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可以当作类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型。
例如使用List类型时,如果为E形参传入String类型实参,则产生一个新的类型:List
//List<String>等同于如下接口
public interface List<String>
{
void add(String x);
Iterator<String> iterator();
...
}
注:包含泛型声明的类型可以在定义变量、创建对象(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的主要使用场景)。下面定义一个Apple类,这个Apple类里就可以包含一个泛型声明。
//定义Apple类时使用泛型声明
public class Apple<T>
{
//使用T类型定义实例变量
private T info;
public Apple(){};
//下面使用T类型来定义构造器
public Apple(T info)
{
this.info=info;
}
public void setInfo(T info)
{
this.info=info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
//传给T形参的是String,所以构造器参数只能是String
Apple<String> a1=new Apple<>("苹果");
System.out.println(a1.getInfo());//苹果
////传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2=new Apple<>(5.67);
System.out.println(a2.getInfo());//5.67
}
}
上面程序定义了一个带泛型声明的Apple
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。
二、从泛型类派生子类
当创建了带泛型声明的接口、类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含泛型形参。
例如下面代码时错误的:
//定义类A继承Apple类,Apple类不能跟泛型形参
public class A extends Apple<T>{}
方法中的形参代表变量、常量、表达式等数据,本书把它们直接称为形参,或者称为数据形参。定义方法时可以声明数据形参,调用方法时必须为这些数据形参传入实际的数据;依次类时的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。
从Apple类派生一个子类:
//使用Aplle类为T形参传入String类型
public class A extends Apple<String>
调用方法时必须为所有数据形参传入参数值,与调用方法不同的是,使用类接口、接口时可以不为泛型形参传入实际的类型参数,即下面的代码是正确的:
//使用Aplle类时,没有为T形参传入类型参数
public class A extends Apple//省略泛型的形式称为原始类型
如果Apple
public class A1 extends Apple<String>
{
// 正确重写了父类的方法,返回值
// 与父类Apple<String>的返回值完全相同
public String getInfo()
{
return "子类" + super.getInfo();
}
/*
// 下面方法是错误的,重写父类方法时返回值类型不一致
public Object getInfo()
{
return "子类";
}
*/
public static void main(String[] args)
{
var a=new A1();
System.out.println(a.getInfo());
}
}
如果使用Aplle类时没有传入实际的类型(即使用原始类型),Java编译器可能发出警告:使用了未经检查或不安的操作————这就是泛型警告。如果需要看到该警告提示的更详细的信息,则可以通过为javac命令增加-Xlint:unchecked选项来实现。此时系统会把Apple
public class A2 extends Apple
{
//重写父类的方法
public String getInfo()
{
//下面重写将报错,原始类型是Object.错误: 不兼容的类型: Object无法转换为String
//return super.getInfo();//方法返回的值是Object
return super.getInfo().toString();
}
}
三、并不存在泛型类
可以把ArrayList
import java.util.*;
public class ArrayListTest
{
public static void main(String[] args)
{
//分别创建List<String>对象和List<Integer>对象
List<String> l1=new ArrayList<>();
List<Integer> l2=new ArrayList<>();
System.out.println(l1.getClass()==l2.getClass());//true
}
}
不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,再内存中也只有一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化块不允许使用泛型。下面程序演示这种错误:
import java.util.*;
public class R<T>
{
public T age;
//不能在静态变量声明中使用泛型形参
//public static T info;//错误: 无法从静态上下文中引用非静态 类型变量 T
public void foo(T msg){System.out.println(msg);}
//R.java:12: 错误: 不兼容的类型: String无法转换为T
/*{
age="二十";
}*/
//不能在静态方法中使用泛型形参
//public static void bar(T msg){}//错误: 无法从静态上下文中引用非静态 类型变量 T
public static void main(String[] args)
{
R<String> r=new R<>();
r.foo("测试泛型");
}
}
输出:测试泛型