原文链接,点击跳转
泛型
概念
描述:
- 泛型是程序设计语言的一种特性,在编写代码类的时候不指定具体类型,用一个参数变量表示,在具体实例化使用时才声明指定类型。比如 ArrayList,在定义 ArrayList 类的时候是不知道要保存什么样的对象,所以编写该类代码时使用 ArrayList,用一个参数变量来表示要保存的对象类型。当实例化 ArrayList 用来保存对象时,声明列表要保存的具体对象类型,比如该列表用来保存字符串对象,实例化时使用 new ArrayList<String>,指定了具体类型。
意义:
- 泛型本质就是
参数化类型
或类型参数化
,用参数变量来表示要传入的类型,并且该参数变量也只可以代表类。主要作用就是在编译时保证类型安全,提高代码的重用率,将运行时期可能发生的异常提前到编译时期暴露。泛型的类型参数在编译时会被消除,不能直接使用基本值类型作为泛型类型参数。
举例描述
不使用泛型
-
在没有使用泛型之前,通常使用 Object 类来表示可变的类型。使用 Object 来接收对象在进行类型转换时存在安全风险,容易导致 ClassCastException 异常。
public static void main(String[] args) { List list = new ArrayList(); list.add("cat"); list.add("dog"); list.add(18); for (Object o : list) { String s = (String) o; System.out.println(s); } }
没有使用泛型时,使用 Object 类接收对象,之后获取元素进行向下转型得到加入时的类型元素,再进行后续操作。这里本想在列表中加入字符串对象,但不小心添加了一个整数类型的对象,该代码在编译时不会报错,但在运行时进行类型转换的时候报 ClassCastException 异常。
使用泛型
-
使用泛型在编译期间就能确定类型,从而保证类型安全。
public static void main(String[] args) { List<String> list = new ArrayList(); list.add("cat"); list.add("dog"); // list.add(18); // 如果加入这行代码,在编译时便报错。 for (Object o : list) { String s = (String) o; System.out.println(s); } }
使用泛型在编译期间就进行确定了类型,当不小心传入其他类型的对象时,在编译期间就会报错,不会遗留到运行时,在运行时遍历元素向下转型便不会出现 ClassCastException 异常。提高代码的重用率。
泛型的使用
主要有泛型接口、泛型类、泛型方法、泛型通配符和类型擦除。
只有使用尖括号 <类型参数符号> 的形式才是声明泛型,如 <T>,单独的类型参数符号表示使用类型参数,如 T。
声明泛型的位置是在类或接口名的后面,如:public class GenericClass<T>;或在方法返回值之前,如: public <T> T genericMethod(T t)。
一般使用 T、E、K、V 符号表示泛型参数。T 代表 Type、E 代表 Element、K 代表 Key,V 代表 Value,<K, V> 通常用于键值对的表示。
泛型接口
修饰符 interface接口名<代表泛型的变量> { }
- 例:
public interface GenericInterface<T> { void show(T t); }
当实现类实现该接口时有两种方式:
- 实现类实现接口,同时指定泛型类型。
- 实现类实现接口但不指定泛型,这个类也就成了泛型类。
泛型类
修饰符 class 类名<代表泛型的变量> { }
- 例:
public class GenericClass<T> { public void show(T t){ ArrayList<T> ts = new ArrayList<>(); ts.add(t); System.out.println("实例化时指定类型"); } }
实例化 GenericClass 时指定具体类型参数,之后该实例化对象调用方法时只能传入指定的类型对象。
泛型方法
泛型方法相对于泛型类来说使用粒度更小。泛型类指定的泛型可以在整个类中使用,若多个方法使用该泛型时,在泛型类实例化并指定类型后,这些方法就只能使用指定类型。而泛型方法指定的泛型只针对该方法,只能在该方法中使用,可以在调用该方法时指定具体类型,这样就与泛型类对象无关,代码更加灵活。
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
-
例:
public class GenericClass<T> { public T method(T t){ System.out.println("该方法不是泛型方法,实例化时指定类型参数,该方法只能传入该类型对象:" + t); return t; } // 这里泛型方法使用的参数与泛型类一样(可以不一样),但是并不是同一个,两者毫无关联。 // 泛型方法使用的 T 都是声明泛型方法<T>中的T,而不是泛型类中T。 public <T> T genericMethod(T t){ System.out.println("该方法是泛型方法,使用方法时指定类型参数,与泛型类指定的类型参数无关:" + t); return t; } public static void main(String[] args) { GenericClass<String> stringGenericClass = new GenericClass<>(); stringGenericClass.method("string"); stringGenericClass.genericMethod(1); } }
类型通配符
类型通配符:
类型通配符一般是使用 ?
代替具体的类型实参。类型通配符是类型实参,而不是类型形参。使用时一般用在方法参数中,表示可以接受该类所有类型的泛型变量。
当在一个不是泛型类中使用泛型类的对象作为方法参数传入时,需要明确指定泛型类中类型参数,否则编译会报错。
-
例:
// public void show(List<T> list){ // 报错 public void show(List<String> list){ for (Object o : list) { System.out.println(o); } }
以上代码在传入泛型类参数时需要指定类型实参(<T> 是类型形参,<String> 是类型实参),但是这样限制了代码的通用性。本来该方法传参是可以使用所有类型实参的泛型类对象,而不是某个具体类型实参的泛型类对象,但是定义该方法时参数需要指定类型实参,导致该方法使用非常局限。
-
例:
public class OrdinaryClass { public void show(List<String> list){ for (Object o : list) { System.out.println(o); } } public static void main(String[] args) { ArrayList<String> strings = new ArrayList<>(); ArrayList<Integer> integers = new ArrayList<>(); OrdinaryClass ordinaryClass = new OrdinaryClass(); ordinaryClass.show(strings); // ordinaryClass.show(integers); // 加入该代码编译报错。 } }
基于以上考虑,便提出使用 ? 通配符来表示任意泛型类的类型实参,这样保证了定义方法传入泛型类作为参数时,该方法能接收所有类型实参的泛型类对象,只与泛型类有关,与泛型类中的类型实参无关,提高代码重用性。
-
例:
public class OrdinaryClass { public void show(List<?> list){ for (Object o : list) { System.out.println(o); } } public static void main(String[] args) { ArrayList<String> strings = new ArrayList<>(); ArrayList<Integer> integers = new ArrayList<>(); OrdinaryClass ordinaryClass = new OrdinaryClass(); ordinaryClass.show(strings); ordinaryClass.show(integers); // 加入该代码不会报错 } }
通配符上限:
<? extend T>:表示只能匹配类型参数为 T 类型或其子类。
- 如:List<? extend Number> 能接收类型参数为 Number 及其子类。
通配符下限:
<? super T>:表示只能匹配类型参数为 T 类型或其父类。
- 如:List<? super Integer> 能接收类型参数为 Integer 及其父类。
类型擦除
在泛型内部,无法获得任何有关泛型参数类型的信息,泛型只在编译阶段有效,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”。
泛型类型在逻辑上可看成是多个不同的类型,但是其实质都是同一个类型。因为泛型是在JDK5之后才出现的,需要处理 JDK5之前的非泛型类库。擦除的核心动机是它使得泛化的客户端可以用非泛化的类库实现,反之亦然,这经常被称为"迁移兼容性"。
- 例:
输出:public static void main(String[] args) throws NoSuchFieldException { ArrayList<String> strings = new ArrayList<>(); ArrayList<Integer> integers = new ArrayList<>(); System.out.println(strings.getClass().getSimpleName()); System.out.println(integers.getClass().getSimpleName()); System.out.println(strings.getClass() == integers.getClass()); }
ArrayList
ArrayList
true
所有泛型类型参数,若没有设置泛型上限,则编译之后统一擦除为Object类型,若设置了泛型上限,则编译之后统一擦除为相应的泛型上限。
无上限类型擦除,擦除后实际类型为 Object。
- 例:
输出:Objectpublic class GenericClass<T> { private T var; public static void main(String[] args) throws NoSuchFieldException { GenericClass<String> stringGenericClass = new GenericClass<>(); Class<? extends GenericClass> aClass = stringGenericClass.getClass(); Field var = aClass.getDeclaredField("var"); System.out.println(var.getType().getSimpleName()); } }
有上限类型擦除,擦除后实际类型为上限类型。
- 例:
输出:Numberpublic class GenericClass<T extends Number> { private T var; public static void main(String[] args) throws NoSuchFieldException { GenericClass<Integer> stringGenericClass = new GenericClass<>(); Class<? extends GenericClass> aClass = stringGenericClass.getClass(); Field var = aClass.getDeclaredField("var"); System.out.println(var.getType().getSimpleName()); } }