写在前面
面向对象编程中,多态算是一种泛化机制。你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。
在 Java 增加泛型类型之前,通用程序的设计就是利用继承实现的,例如:ArrayList 类只维护一个 Object 引用的数组,Object 为所有类基类。
public class BeforeGeneric { /** * 泛型之前的通用程序设计 */ static class ArrayList{ private Object[] elements=new Object[0]; public Object get(int i){ return elements[i]; } /** * 这里的实现,只是为了演示,不具有任何参考价值 * */ public void add(Object o){ int length = elements.length; Object[] newElements = new Object[length+1]; for(int i=0; i<length; i++){ newElements[i] = elements[i]; } newElements[length] = o; elements = newElements; } } public static void main(String[] args) { ArrayList stringValues = new ArrayList(); //可以向数组中添加任何类型的对象 stringValues.add(1); //问题1——获取值时必须强制转换 String str = (String) stringValues.get(0); //问题2——上述强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException System.out.println(str); } } 复制代码
面临的问题
1、当我们获取一个值的时候,必须进行强制类型转换。
2、当我们插入一个值的时候,无法约束预期的类型。假定我们预想的是利用 stringValues 来存放 String 集合,因为 ArrayList 只是维护一个 Object 引用的数组,我们无法阻止将 Integer 类型(Object 子类)的数据加入 stringValues。然而,当我们使用数据的时候,需要将获取的 Object 对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如 Integer),编译时我们不会收到任何的错误提示。但当我们运行程序时却会报异常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at generic.BeforeGeneric.main(BeforeGeneric.java:24)
这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。
什么是泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为 Object 类型。
使用泛型的好处
1、避免了类型强转的麻烦。
2、它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现 ClassCastException。
泛型的使用
泛型可以用在类、接口和方法上。
泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){} 复制代码
示例:
public class GenericMethodTest { // 泛型方法 printArray public static < E > void printArray( E[] inputArray ) { // 输出数组元素 for ( E element : inputArray ){ System.out.printf( "%s ", element ); } System.out.println(); } public static void main( String args[] ) { // 创建不同类型数组: Integer, Double 和 Character Integer[] intArray = { 1, 2, 3, 4, 5 }; Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 }; Character[] charArray = { 'H', 'E', 'L', 'L', 'O' }; System.out.println( "整型数组元素为:" ); printArray( intArray ); // 传递一个整型数组 System.out.println( "\n双精度型数组元素为:" ); printArray( doubleArray ); // 传递一个双精度型数组 System.out.println( "\n字符型数组元素为:" ); printArray( charArray ); // 传递一个字符型数组 } } 复制代码
输出如下所示:
整型数组元素为: 1 2 3 4 5 双精度型数组元素为: 1.1 2.2 3.3 4.4 字符型数组元素为: H E L L O 复制代码
泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
定义格式:
public class 类名 <泛型类型> { } 复制代码
示例:
public class Box<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box<Integer> integerBox = new Box<Integer>(); Box<String> stringBox = new Box<String>(); integerBox.add(new Integer(10)); stringBox.add(new String("菜鸟教程")); System.out.printf("整型值为 :%d\n\n", integerBox.get()); System.out.printf("字符串为 :%s\n", stringBox.get()); } } 复制代码
输出如下所示:
整型值为 :10 字符串为 :菜鸟教程 复制代码
泛型 K、T、V、E、N、? 的含义
如果你点开 JDK 中一些泛型类的源码,我们会看到下面这些代码:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { ... } public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... } 复制代码
上面这些泛型类定义中的泛型参数 E、K 和 V 都是什么意思呢?其实这些参数名称是可以任意指定,就想方法的参数名一样可以任意指定,但是我们通常会起一个有意义的名称,让别人一看就知道是什么意思。泛型参数也一样,E 一般是指元素,用来集合类中。
常见泛型参数名称有如下:
T Type 标识类型。
K,V 代表键值对中的 key 和 value。
E 代表 Element 通常在集合中使用。
N 代表 Number 数字。
? 无限通配符,表示的是未知类型,表示不关心或者不能确定实际操作的类型,一般配合容器类使用。
? 和 Object 的区别?
? 表示未知类型。Object 表示任意类型。 Object 在使用时需要强制转换为想要的类型。编译器只有在运行时才知道类型转换是否出现异常。
边界
当我们不指定或者不关心操作类型,但是又想进行一定范围限制的时候,我们可以通过添加上限或下限来起到限制作用。
上边界<? extends T>
- 定义了上限,只有读的能力。此方式表示参数化的类型可能是所指定的类型,或者是此类型的子类。如果传入的类型不是 T 或者 T 的子类,编辑不成功。
- 用于灵活读取,使得方法可以读取T 或 T 的任意子类型的容器对象。
示例:
public class Test { public static void printIntValue(List<? extends Number> list) { for (Number number : list) { System.out.print(number.intValue()+" "); } System.out.println(); } public static void main(String[] args) { List<Integer> integerList=new ArrayList<Integer>(); integerList.add(2); integerList.add(2); printIntValue(integerList); List<Float> floatList=new ArrayList<Float>(); floatList.add((float) 3.3); floatList.add((float) 0.3); printIntValue(floatList); } } 复制代码
下边界<? super T>
- 定义了下限,有读的能力以及部分写的能力,子类可以写入父类。此方式表示参数化的类型可能是指定的类型,或者是此类型的父类。如果传入的类型不是 T 或者 T 的父类,编辑不成功。
- 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
示例:
public class Test { public static void fillNumberList(List<? super Number> list) { list.add(new Integer(0)); list.add(new Float(1.0)); } public static void main(String[] args) { List<? super Number> list=new ArrayList(); list.add(new Integer(1)); list.add(new Float(1.1)); } } 复制代码
注意
- 要从泛型类取数据时,用 ? extends 通配符。
- 要往泛型类写数据时,用 ? super 通配符。
- 既要取又要写,就不用通配符(即 extends 与 super 都不用)。
- 不能同时声明泛型通配符上界和下界。
泛型擦除
泛型值存在于编译期,代码在进入虚拟机后泛型就会被擦除掉,这个特性叫做类型擦除。
- 如果泛型没有设置类型上限,那么将泛型转化成Object类型。
- 如果设置了类型上限,那么将泛型转化成他的类型上限。
- 如果有多个类型上限。使用第一个作为原始类型(T extends Demo1 & Demo2)原始类型为Demo1。