一.、提出背景 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
在Java SE 1.5之前,没有泛型(Generics)的情况下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器 可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
SE1.5中引入的一个新特性,其本质是参数化类型。参数类型可以用在类 、接口 和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java允许程序员构建一个元素类型为Object的Collection,其中的元素可以是任何类型。
假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?
答案是可以使用 Java 泛型 。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
不使用泛型式,容易引起的运行时错误
1 2 3 4 5 6 7 8 List arrayList = new ArrayList(); arrayList.add("aaaa" ); arrayList.add(100 ); for (int i = 0 ; i< arrayList.size();i++){ String item = (String)arrayList.get(i); Log.d("泛型测试" ,"item = " + item); }
毫无疑问,程序的运行结果会以崩溃结束:
1 java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。
1 2 3 List<String> arrayList = new ArrayList<String>(); ...
二、泛型类 泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。在类名后面添加了类型参数声明部分。
和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
如下实例演示了我们如何定义一个泛型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 字符串为 :菜鸟教程
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Generic generic = new Generic("111111" ); Generic generic1 = new Generic(4444 ); Generic generic2 = new Generic(55.55 ); Generic generic3 = new Generic(false ); Log.d("泛型测试" ,"key is " + generic.getKey()); Log.d("泛型测试" ,"key is " + generic1.getKey()); Log.d("泛型测试" ,"key is " + generic2.getKey()); Log.d("泛型测试" ,"key is " + generic3.getKey()); 运行结果: D/泛型测试: key is 111111 D/泛型测试: key is 4444 D/泛型测试: key is 55.55 D/泛型测试: key is false
注意:
泛型的类型参数只能是类类型,不能是简单类型。即类型参数必须为引用类型,不能是基本类型。
不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
1 2 if (ex_num instanceof Generic<Number>){ }
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
1 boolean ` `result = obj ``instanceof ` `Class
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
四、泛型接口 泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
1 2 3 4 public interface Generator <T > { public T next () ; }
当实现泛型接口的类,未传入泛型实参时:
1 2 3 4 5 6 7 8 9 10 11 class FruitGenerator <T > implements Generator <T > { @Override public T next () { return null ; } }
当实现泛型接口的类,传入泛型实参时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class FruitGenerator implements Generator <String > { private String[] fruits = new String[]{"Apple" , "Banana" , "Pear" }; @Override public String next () { Random rand = new Random(); return fruits[rand.nextInt(3 )]; } }
三、泛型通配符 1、类型通配符一般是使用? 代替具体的类型参数。
我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic与Generic实际上是相同的一种基本类型。那么问题来了,在使用Generic作为形参的方法中,能否使用Generic的实例传入呢?在逻辑上类似于Generic和Generic是否可以看成具有父子关系的泛型类型呢?
为了弄清楚这个问题,我们使用Generic这个泛型类继续看下面的例子:
1 2 3 public void showKeyValue (Generic<Number> obj) { Log.d("泛型测试" ,"key value is " + obj.getKey()); }
1 2 3 4 5 6 7 8 Generic<Integer> gInteger = new Generic<Integer>(123 ); Generic<Number> gNumber = new Generic<Number>(456 ); showKeyValue(gNumber);
通过提示信息我们可以看到Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
我们可以将上面的方法改一下:
1 2 3 public void showKeyValue1 (Generic<?> obj) { Log.d("泛型测试" ,"key value is " + obj.getKey()); }
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。重要说三遍!此处’?’是类型实参,而不是类型形参 ! 此处’?’是类型实参,而不是类型形参 !再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
例如 List 在逻辑上是List,List 等所有List<具体类型实参>的父类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.util.*; public class GenericTest { public static void main (String[] args) { List<String> name = new ArrayList<String>(); List<Integer> age = new ArrayList<Integer>(); List<Number> number = new ArrayList<Number>(); name.add("icon" ); age.add(18 ); number.add(314 ); getData(name); getData(age); getData(number); } public static void getData (List<?> data) { System.out.println("data :" + data.get(0 )); } } 运行结果: data :icon data :18 data :314
解析: 因为getData()方法的参数是List类型的,所以name,age,number都可以作为这个方法的实参,这就是通配符的作用
2、类型通配符上限通过形如List来定义,
如此定义就是通配符泛型值接受Number及其下层子类类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.*; public class GenericTest { public static void main (String[] args) { List<String> name = new ArrayList<String>(); List<Integer> age = new ArrayList<Integer>(); List<Number> number = new ArrayList<Number>(); name.add("icon" ); age.add(18 ); number.add(314 ); getUperNumber(age); getUperNumber(number); } public static void getData (List<?> data) { System.out.println("data :" + data.get(0 )); } public static void getUperNumber (List<? extends Number> data) { System.out.println("data :" + data.get(0 )); } } 运行结果: data :18 data :314
解析: 在(//1)处会出现错误,因为getUperNumber()方法中的参数已经限定了参数泛型上限为Number,所以泛型为String是不在这个范围之内,所以会报错
3、类型通配符下限通过形如 List 来定义,表示类型只能接受Number及其三层父类类型,如 Object 类型的实例。
五、泛型方法 你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。
实例 1 下面的例子演示了如何使用泛型方法打印不同字符串的元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class GenericMethodTest { public static < E > void printArray ( E[] inputArray ) { for ( E element : inputArray ){ System.out.printf( "%s " , element ); } } public static void main ( String args[] ) { 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
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
实例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public <T> T genericMethod (Class<T> tClass) throws InstantiationException , IllegalAccessException { T instance = tClass.newInstance(); return instance; }
1 Object obj = genericMethod(Class.forName("com.test.test" ));
5.1 泛型方法的基本用法 光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public class GenericTest { public class Generic <T > { private T key; public Generic (T key) { this .key = key; } public T getKey () { return key; } } public <T> T showKeyName (Generic<T> container) { System.out.println("container key :" + container.getKey()); T test = container.getKey(); return test; } public void showKeyValue1 (Generic<Number> obj) { Log.d("泛型测试" ,"key value is " + obj.getKey()); } public void showKeyValue2 (Generic<?> obj) { Log.d("泛型测试" ,"key value is " + obj.getKey()); } public static void main (String[] args) { } }
5. 2 类中的泛型方法 当然这并不是泛型方法的全部,泛型方法可以出现在任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class GenericFruit { class Fruit { @Override public String toString () { return "fruit" ; } } class Apple extends Fruit { @Override public String toString () { return "apple" ; } } class Person { @Override public String toString () { return "Person" ; } } class GenerateTest <T > { public void show_1 (T t) { System.out.println(t.toString()); } public <E> void show_3 (E t) { System.out.println(t.toString()); } public <T> void show_2 (T t) { System.out.println(t.toString()); } } public static void main (String[] args) { Apple apple = new Apple(); Person person = new Person(); GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>(); generateTest.show_1(apple); generateTest.show_2(apple); generateTest.show_2(person); generateTest.show_3(apple); generateTest.show_3(person); } }
5.3 泛型方法与可变参数 再看一个泛型方法和可变参数的例子:
1 2 3 4 5 public <T> void printMsg ( T... args) { for (T t : args){ Log.d("泛型测试" ,"t is " + t); } }
1 printMsg("111" ,222 ,"aaaa" ,"2323.4" ,55.55 );
5.4 静态方法与泛型 静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class StaticGenerator <T > { .... .... public static <T> void show (T t) { } }
5.5 泛型方法总结 泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
六. 泛型上下边界 在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
为泛型添加上边界,即传入的类型实参必须是指定类型的子类型
1 2 3 public void showKeyValue1 (Generic<? extends Number> obj) { Log.d("泛型测试" ,"key value is " + obj.getKey()); }
1 2 3 4 5 6 7 8 9 10 11 Generic<String> generic1 = new Generic<String>("11111" ); Generic<Integer> generic2 = new Generic<Integer>(2222 ); Generic<Float> generic3 = new Generic<Float>(2.4f ); Generic<Double> generic4 = new Generic<Double>(2.56 ); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4);
如果我们把泛型类的定义也改一下:
1 2 3 4 5 6 7 8 9 10 11 public class Generic <T extends Number > { private T key; public Generic (T key) { this .key = key; } public T getKey () { return key; } }
1 2 Generic<String> generic1 = new Generic<String>("11111" );
再来一个泛型方法的例子:
1 2 3 4 5 6 7 public <T extends Number> T showKeyName (Generic<T> container) { System.out.println("container key :" + container.getKey()); T test = container.getKey(); return test; }
通过上面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
有界的类型参数:可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
实例 2 下面的例子演示了”extends”如何使用在一般意义上的意思”extends”(类)或者”implements”(接口)。该例子中的泛型方法返回三个可比较对象的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class GenericMethodTest { public static <T extends Comparable<T>> T maximum (T x, T y, T z) { T max = x; if ( y.compareTo( max ) > 0 ){ max = y; } if ( z.compareTo( max ) > 0 ){ max = z; } return max; } public static void main ( String args[] ) { System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n" , 3 , 4 , 5 , maximum( 3 , 4 , 5 ) ); System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n" , 6.6 , 8.8 , 7.7 , maximum( 6.6 , 8.8 , 7.7 ) ); System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n" ,"pear" , "apple" , "orange" , maximum( "pear" , "apple" , "orange" ) ); } } 运行结果: 3 , 4 和 5 中最大的数为 5 6.6 , 8.8 和 7.7 中最大的数为 8.8 pear, apple 和 orange 中最大的数为 pear
七、关于泛型数组要提一下 看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组” 的。
也就是说下面的这个例子是不可以的:
1 List<String>[] ls = new ArrayList<String>[10 ];
而使用通配符创建泛型数组是可以的,如下面这个例子:
1 List<?>[] ls = new ArrayList<?>[10 ];
这样也是可以的:
1 List<String>[] ls = new ArrayList[10 ];
下面使用Sun 的一篇文档 的一个例子来说明这个问题:
1 2 3 4 5 6 7 List<String>[] lsa = new List<String>[10 ]; Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3 )); oa[1 ] = li; String s = lsa[1 ].get(0 );
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式 ,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
1 2 3 4 5 6 7 List<?>[] lsa = new List<?>[10 ]; Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3 )); oa[1 ] = li; Integer i = (Integer) lsa[1 ].get(0 );
八、特性:只在编译阶段有效 1 2 3 4 5 6 7 8 9 10 11 List<String> stringArrayList = new ArrayList<String>(); List<Integer> integerArrayList = new ArrayList<Integer>(); Class classStringArrayList = stringArrayList.getClass(); Class classIntegerArrayList = integerArrayList.getClass(); if (classStringArrayList.equals(classIntegerArrayList)){ Log.d("泛型测试" ,"类型相同" ); } 输出结果:D/泛型测试: 类型相同。
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
JVM里没有泛型,对于所有的泛型对于虚拟机来讲都是属于普通类。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型
九. 最后 本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。
参考资料: 版权声明:本文为CSDN博主「VieLei」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/s10461/article/details/53941091
https://www.runoob.com/java/java-generics.html
https://baike.baidu.com/item/java%E6%B3%9B%E5%9E%8B/511821?fr=aladdin
欢迎访问 chenyawei 的博客, 若有问题或者有好的建议欢迎留言,笔者看到之后会及时回复。 评论点赞需要github账号登录,如果没有账号的话请点击 github 注册, 谢谢 !
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !