目录
什么是泛型
一般的类和方法只能使用具体的类型,而通过泛型能对类型实现参数化,使类和方法适用多种类型。
例如:当我们要实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值时我们可以借助Object类来完成:
class MyArray { public Object[] obj = new Object[10]; public void setVal(int pos, Object val) { obj[pos] = val; } public Object getVal(int pos) { return obj[pos]; } } public class Test { public static void main(String[] args) { MyArray myArray = new MyArray(); myArray.setVal(0, 1); myArray.setVal(1, 1.5); myArray.setVal(2, "haha"); int val1 = (int)myArray.getVal(0);//需要进行强制类型转换 double val2 = (double)myArray.getVal(0);//需要进行强制类型转换 String val3 = (String)myArray.getVal(0);//需要进行强制类型转换 } }
虽然使用Object类也能让当前数组可以存放任何数据,但取数组时却不好确定取到哪种数据类型 。而泛型把类型作为参数传递,能指定当前的容器,要持有什么类型的对象,同时让编译器去做检查,实现需要什么类型,就传入什么类型。class MyArray<T> { public T[] obj = (T[])new Object[10]; public void setVal(int pos, T val) { obj[pos] = val; } public T getVal(int pos) { return obj[pos]; } } public class Test { public static void main(String[] args) { MyArray<Integer> myArray = new MyArray();//指定类型为Integer myArray.setVal(0, 1); //myArray.setVal(1, 1.5);//类型不匹配,编译报错 //myArray.setVal(2, "haha");//类型不匹配,编译报错 int val1 = myArray.getVal(0); } }
通过对比可以发现泛型可以帮助我们在存放元素的时候进行类型检查,在取出元素的时候进行类型转换。
泛型的基本语法
• 泛型类: class 泛型类名称 < 类型形参列表 > { // 这里可以使用类型参数 }class Demo1<T> { T t; } class Demo2<T1, T2> { T1 t1; T2 t2; }
• 定义一个泛型类引用和实例化一个泛型类对象:泛型类 < 类型实参 > 变量名 = new 泛型类 < 类型实参 > ( 构造方法实参 )MyArray<Integer> list = new MyArray<Integer>();
• 泛型方法 :方法限定符 < 类型形参列表 > 返回值类型 方法名称 ( 形参列表 ){...}public class Test { //静态的泛型方法需要在static后用<>声明泛型类型参数 public static<T> void test() { } }
类名后的 <T> 代表占位符,表示当前类是一个泛型类类型形参一般使用一个大写字母表示,常用的名称有:• E 表示 Element(元素,在集合中使用)• K 表示 Key(键)• V 表示 Value(值)• N 表示 Number(数值类型)• T 表示 Type(Java类)• ? 表示不确定的java类
注:
• 泛型只能接受类,所有的基本数据类型必须使用包装类;
• 泛型类并不会改变方法的返回值(不用考虑重载)
泛型的擦除机制
我们已经知道泛型是在编译时根据传递的数据类型进行类型检查和转换,其实在编译过程中泛型还会将所有的T替换成 Object,这种机制叫做擦除机制。擦除机制是在编译级别实现的,编译器生成的字节码在运行期间并不包含泛型的类型信息。
其实前面 T[] obj = (T[])new Object 这种写法是非常危险的。当 getval() 方法返回的是T[],且测试代码中用 Integer[] 数组来接收时在运行时就会得到 ClassCastException 错误。
class MyArray<T> { public T[] obj = (T[])new Object[10]; public void setVal(int pos, T val) { obj[pos] = val; } public T[] getVal(int pos) { return obj; } } public class Test { public static void main(String[] args) { MyArray<Integer> myArray = new MyArray(); myArray.setVal(0, 1); Integer[] val1 = myArray.getVal(0);//将Object[]分配给Integer[]引用,程序报错 } }
注:数组在运行时存储和检查类型信息,泛型在编译时检查类型错误。
泛型的上界
在定义泛型类时,可以通过类型边界对传入的类型变量做一定的约束。
语法: class 泛型类名称 < 类型形参 extends 类型边界 > { ... }其中 类型参数必须是 类型边界或 其子类 定(可以看成当泛型有类型边界时,擦除机制只会擦除到它的类型边界)当类型形参为 E,类型边界为 Number 时:class Demo<E extends Number> { } public class Test { public static void main(String[] args) { Demo<Integer> demo1 = new Demo<>();//Integer是Number的子类 Demo<Number> demo2 = new Demo<>();//Number是Number本身 //Demo<String> demo3 = new Demo<>();//error:String不是Number的子类或者Number本身 } }
注:如果没有指定类型边界 ,可以视为 E extends Object泛型的上界还可以是一个接口,这时候的类型参数就必须要有实现这个接口的方法。当我们要写一个泛型类求数组的最大值时:class Person implements Comparable<Person> { public int age; public Person (int age) { this.age = age; } @Override public int compareTo(Person o) { return this.age - o.age; } } class Max<T extends Comparable<T>> { public T findMax(T[] arr) { T max = arr[0]; for (int i = 0; i < arr.length; i++) { if (max.compareTo(max) < 0) { max = arr[i]; } } return max; } } public class Test { public static void main(String[] args) { Max<Person> max = new Max<>(); Person[] per = {new Person(10), new Person(20), new Person(30)}; Person person = max.findMax(per); } }
通配符
通配符(?)在形参中使用,可以用于扩充参数的范围。
class A<T> { private T a; public void setA(T a) { this.a = a; } public T getA() { return a; } } public class Test { public static void fun(A<?> a) {//此时使用通配符"?"描述的是它可以接收任意泛型类型 //a.setA(10);//error:'?'类型不确定,无法进行修改 System.out.println(a.getA()); } public static void main(String[] args) { A<Integer> a1 = new A<>(); a1.setA(10); A<String> a2 = new A<>(); a2.setA("ABC"); fun(a1); fun(a2); } }
泛型存在泛型上界,通配符除了有上界还有下界。
•通配符的上界
语法: ?extends 上界类
传递的参数只能是上界类或其子类
class Grandfather { } class Father extends Grandfather { } class Son extends Father { } class A<T> { private T a; public void setA(T a) { this.a = a; } public T getA() { return a; } } public class Test { public static void fun(A<? extends Father> a) { } public static void main(String[] args) { A<Grandfather> a1 = new A<>(); a1.setA(new Grandfather()); A<Father> a2 = new A<>(); a2.setA(new Father()); A<Son> a3 = new A<>(); a3.setA(new Son()); //fun(a1);//error:不是Father或其子类 fun(a2);//是Father本身 fun(a3);//是Father的子类 } }
此时无法在 fun 函数中对 a 进行添加元素,因为 a 接收的是 Father 和他的子类,此时无法确定存储的元素应该是哪个类。虽然无法在 fun 函数中 进行添加元素,但是在其中可以获取元素。public static void fun(A<? extends Father> a) { //a.setA(new Father());//error:?接受的类型可能为Father或Father的子类,可能发生不安全的向下转型 //a.setA(new Son());//error Father father = a.getA();//a一定是father或其子类,可能发生向上转型 }
•通配符的下界语法: ?super 下界类
传递的参数只能是下界类或其父类
class Grandfather { } class Father extends Grandfather { } class Son extends Father { } class A<T> { private T a; public void setA(T a) { this.a = a; } public T getA() { return a; } } public class Test { public static void fun(A<? super Father> a) { } public static void main(String[] args) { A<Grandfather> a1 = new A<>(); a1.setA(new Grandfather()); A<Father> a2 = new A<>(); a2.setA(new Father()); A<Son> a3 = new A<>(); a3.setA(new Son()); fun(a1);//是Father的父类类 fun(a2);//是Father本身 //fun(a3);//error:是Father的子类 } }
与通配符的上界相反,通配符的下界在 fun 函数中不能进行读取数据,只能写入数据(写入的数据只能是下界类及其父类)。public static void fun(A<? super Father> a) { //Father father = a.getA();//error:'?'接受的类型可能为Father或Father的父类,可能发生危险的向下转型 //Grandfather grandfather = a.getA();//error a.setA(new Father());//可以添加Father本身,a可能是Father或其本身,可能发生向上转型 a.setA(new Son());//可以添加Father的子类 }
包装类
在 Java 中,由于基本类型不是继承自 Object ,为了在泛型代码中可以支持基本类型, Java 给每个基本类型都对应了一个包装类型。下面是基本数据类型及其所对应的包装类:
基本数据类型 包装类 byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean 基本类型与包装类型的相互转换叫作装箱和拆箱:
int a = 10; // 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中 Integer i1 = Integer.valueOf(a);//手动装箱 Integer i2 = new Integer(a);//手动装箱 Integer i3 = a; // 自动装箱 Integer i4 = (Integer)a; // 自动装箱 // 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中 int j1 = i1.intValue();//手动拆箱 int j2 = ii; // 自动拆箱 int j3 = (int)i1; // 自动拆箱