
该系列博文会告诉你如何从入门到进阶一步步地学习Java基础知识并上手进行实战接着了解每个Java知识点背后的实现原理更完整地了解整个Java技术体系形成自己的知识框架。一、泛型概述1、定义所谓泛型就是允许在定义类、接口、方法时使用类型形参这个类型形参或叫泛型将在声明变量、创建对象、调用方法时动态地指定即传入实际的类型参数也可称为类型实参。Java5改写了集合框架中的全部接口和类为这些接口、类增加了泛型支持从而可以在声明集合变量、创建集合对象时传入类型实参。2、泛型初体验一个被举了无数次的栗子代码语言javascriptAI代码解释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); }运行上述代码我们可以在控制台看到这样的错误信息代码语言javascriptAI代码解释java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.StringArrayList可以存放任意类型例子中添加了一个String类型添加了一个Integer类型再使用时都以String的方式使用因此程序崩溃了。为了解决类似这样的问题在编译阶段就可以解决泛型应运而生。3、泛型的特性先思考如下一段代码代码语言javascriptAI代码解释ListString sListnew ArrayListString(); ListInteger iListnew ArrayListInteger(); System.out.println(sList.getClass()iList.getClass());先不要看结果自己思考一下。结果代码语言javascriptAI代码解释true我们看到输出的结果为true通过上面的例子可以证明在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型只在编译阶段有效。在编译过程中正确检验泛型结果后会将泛型的相关信息擦出并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说泛型信息不会进入到运行时阶段。泛型的这一特性在下述文字中会有详解介绍小结泛型类型在逻辑上看以看成是多个不同的类型实际上都是相同的基本类型。二、泛型的使用泛型有三种使用方式分别为泛型类、泛型接口、泛型方法1、泛型类泛型类型用于类的定义中被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类如List、Set、Map。泛型的基本写法代码语言javascriptAI代码解释class 类名称 泛型标识可以随便写任意标识号标识指定的泛型的类型{ private 泛型标识 /*成员变量类型*/ var; ..... } }看不懂看不懂就对了下面我们来看一个栗子代码语言javascriptAI代码解释public class AppleT { //使用T类型定义变量 private T info; public Apple() {} //下面方法使用T类型定义构造器 public Apple(T info){ this.infoinfo; } public T getInfo() { return info; } public void setInfo(T info) { this.info info; } public static void main(String[] args) { //由于传给T形参的是String,所以构造器参数只能是String AppleString applenew AppleString(苹果); System.out.println(apple.getInfo()); //由于传给T形参的是Double,所以构造器参数只能是Double AppleDouble apple2new AppleDouble(5.56); System.out.println(apple2.getInfo()); } }这里的T可以写成任意符合常用的有如下几个符合表示不确定的 java 类型T (type) 表示具体的一个java类型K V (key value) 分别代表java键值中的Key ValueE (element) 代表Element2、泛型接口泛型接口与泛型类的定义及使用基本相同。下面是Java5改写后的List接口Map接口的代码片段代码语言javascriptAI代码解释public interface ListE{ //在该接口中E可以作为类型使用 //下面方法可以使用E作为参数类型 void add(E x); IteratorE iterator(); } //定义该接口时指定了两个泛型形参其参数名为K,V public interface MapK,V{ //在该接口中K,V完全可以作为类型使用 SetK keySet() V put(K key,V value); }下面我们来看一个泛型案例定义一个泛型接口代码语言javascriptAI代码解释//定义一个泛型接口 public interface GeneratorT { public T next(); }现在有一个类实现了这个泛型接口类的代码如下代码语言javascriptAI代码解释class FruitGeneratorT implements GeneratorT{ Override public T next() { return null; } }我们看到了这个类中也使用了泛型并未传入实际的参数未传入泛型实参时与泛型类的定义相同在声明类的时候需将泛型的声明也一起加到类中即class FruitGeneratorT implements GeneratorT如果不声明泛型如class FruitGenerator implements GeneratorT编译器会报错Unknown class代码语言javascriptAI代码解释public class FruitGenerator implements GeneratorString { private String[] fruits new String[]{Apple, Banana, Pear}; Override public String next() { Random rand new Random(); return fruits[rand.nextInt(3)]; } }这段代码也是实现了Generator接口不同的是传入了实际的类型String传入泛型实参时定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口GeneratorT但是我们可以为T传入无数个实参形成无数种类型的Generator接口。在实现类实现泛型接口时如已将泛型类型传入实参类型则所有使用泛型的地方都要替换成传入的实参类型即GeneratorTpublic T next();中的的T都要替换成传入的String类型。3、泛型通配符为什么要使用泛型通配符正如前面讲的当使用一个泛型类时包括声明变量和创建对象两种情况都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数编译器就会提出泛型警告。假设现在需要定义一个方法该方法里有一个集合形参集合形参的元素类型是不确定的那应该怎样定义呢考虑如下代码代码语言javascriptAI代码解释public void test(List c) { for(int i0;ic.size;i) { System.out.println(c.get(i)); } }上面程序当然没有问题这是一段最普通的遍历List集合的代码。问题是上面程序中List是一个有泛型声明的接口此处使用List 接口时没有传入实际类型参数这将引起泛型警告。为此考虑为List 接口传入实际的类型参数——因为List集合里的元素类型是不确定的泛型通配符的使用为了表示各种泛型List的父类可以使用类型通配符类型通配符是一个问号?将一个问号作为类型实参传给List集合写作List?意思是元素类型未知的List。这个问号?被称为通配符它的元素类型可以匹配任何类型。可以将上面方法改写为如下形式代码语言javascriptAI代码解释public void test(List? c) { for(int i0;ic.size;i) { System.out.println(c.get(i)); } }这样就不会出现警告了但这种带通配符的List仅表示它是各种泛型List的父类并不能将其他元素加入到其中例如将String放入其中List? cnew ArrayListString();//这段代码将报错因为程序无法确定c集合中元素的类型所以不能向其中添加对象。根据前面的ListE接口定义的代码可以发现add0方法有类型参数E作为集合的元素类型所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型所以程序无法将任何对象“丢进”该集合。唯一的例外是null它是所有引用类型的实例。设置类型通配符的上限现在想实现一个简单的绘图程序代码如下代码语言javascriptAI代码解释public abstract class Shape{ public abstract void draw(Canvas c); } //定义Shape的子类Circle public class Circle extends Shape{ //实现画图方法以打印字符串来模拟画图方法实现 public void draw(Canvas c) { System.out.println(在画布c上画一个圆); } } //定义Shape的子类Rectangle public class Rectangle extends Shape{ //实现画图方法以打印字符串来模拟画图方法实现 public void drawCanvas c { System.out.print1n(把一个矩形画在画布c上); } }上面定义了三个形状类其中Shape是一个抽象父类该抽象父类有两个子类Circle和Rectangle。接下来定义一个Canvas类该画布类可以画数量不等的形状Shape子类的对象那应该如何定义这个Canvas类呢考虑如下的Canvas实现类。代码语言javascriptAI代码解释//同时在画布上绘制多个形状 public void drawAll(ListShapeshapes) { for(Shape s:shapes) s.draw(this); }注意上面的drawAll()方法的形参类型是ListShape而ListCircle并不是ListShape的子类型因此下面代码将引起编译错误。代码语言javascriptAI代码解释ListCircle circleListnew ArrayListCircle(); Canvas cnew Canvas(); //不能把ListCircle当成ListShape使用所以下面代码引起编译错误 c.drawAll(circleList);这时我们可以通过通配符的上限来解决这个问题代码语言javascriptAI代码解释List? extends ShapeList? extends Shape是受限制通配符的例子此处的问号?代表一个未知的类型就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型也可以是Shape本身因此可以把Shape称为这个通配符的上限upper bound。设置类型通配符的下限除可以指定通配符的上限之外Java也允许指定通配符的下限通配符的下限用super类型的方式来指定通配符下限的作用与通配符上限的作用恰好相反。指定通配符的下限就是为了支持类型型变。比如Foo是Bar的子类当程序需要一个A? super Bar变量时程序可以将AFoo、AObject赋值给A? super Bar类型的变量这种型变方式被称为逆变。对于逆变的泛型集合来说编译器只知道集合元素是下限的父类型但具体是哪种父类型则不确定。因此这种逆变的泛型集合能向其中添加元素因为实际赋值的集合元素总是逆变声明的父类从集合中取元素时只能被当成Object类型处理编译器无法确定取出的到底是哪个父类的对象。4、泛型方法前面介绍了在定义类、接口时可以使用泛型形参在该类的方法定义和成员变量定义、接口的方法定义中这些泛型形参可被当成普通类型来用。在另外一些情况下定义类、接口时没有使用泛型形参但定义方法时想自己定义泛型形参这也是可以的Java5还提供了对泛型方法的支持。1、泛型方法的基本用法代码语言javascriptAI代码解释/** * 泛型方法的基本介绍 * param tClass 传入的泛型实参 * return T 返回值为T类型 * 说明 * 1public 与 返回值中间T非常重要可以理解为声明此方法为泛型方法。 * 2只有声明了T的方法才是泛型方法泛型类中的使用了泛型的成员方法并不是泛型方法。 * 3T表明该方法将使用泛型类型T此时才可以在方法中使用泛型类型T。 * 4与泛型类的定义一样此处T可以随便写为任意标识常见的如T、E、K、V等形式的参数常用于表示泛型。 */ public T T genericMethod(ClassT tClass)throws InstantiationException , IllegalAccessException{ T instance tClass.newInstance(); return instance; } Object obj genericMethod(Class.forName(com.test.test));2、类中的泛型方法泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的当泛型方法出现在泛型类中时我们再通过一个例子看一下代码语言javascriptAI代码解释//注意泛型类先写类名再写泛型泛型方法先写泛型再写方法名 //类中声明的泛型在成员和方法中可用 class A T, E{ { T t1 ; } A (T t){ this.t t; } T t; public void test1() { System.out.println(this.t); } public void test2(T t,E e) { System.out.println(t); System.out.println(e); } } Test public void run () { A Integer,String a new A(1); a.test1(); a.test2(2,ds); // 1 // 2 // ds } static class B T{ T t; public void go () { System.out.println(t); } }3、泛型方法和可变参数代码语言javascriptAI代码解释public class 泛型和可变参数 { Test public void test () { printMsg(dasd,1,dasd,2.0,false); print(dasdas,dasdas, aa); } //普通可变参数只能适配一种类型 public void print(String ... args) { for(String t : args){ System.out.println(t); } } //泛型的可变参数可以匹配所有类型的参数 public T void printMsg( T... args){ for(T t : args){ System.out.println(t); } } //打印结果 //dasd //1 //dasd //2.0 //false }4、静态方法与泛型静态方法有一种情况需要注意一下那就是在类中的静态方法使用泛型静态方法无法访问类上定义的泛型如果静态方法操作的引用数据类型不确定的时候必须要将泛型定义在方法上。即如果静态方法要使用泛型的话必须将静态方法也定义成泛型方法 。代码语言javascriptAI代码解释public class StaticGeneratorT { .... .... /** * 如果在类中定义使用泛型的静态方法需要添加额外的泛型声明将这个方法定义成泛型方法 * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如public static void show(T t){..},此时编译器会提示错误信息 StaticGenerator cannot be refrenced from static context */ public static T void show(T t){ } }总结泛型方法能使方法独立于类而产生变化以下是一个基本的指导原则无论何时如果你能做到你就该尽量使用泛型方法。也就是说如果使用泛型方法将整个类泛型化那么就应该使用泛型方法。另外对于一个static的方法而已无法访问泛型类型的参数。所以如果static方法要使用泛型能力就必须使其成为泛型方法。三、泛型的类型擦除1、什么是类型擦除还记得我们在文章开始介绍的代码吗我们现在再来看一下代码语言javascriptAI代码解释public class Test { public static void main(String[] args) { ArrayListString list1 new ArrayListString(); list1.add(abc); ArrayListInteger list2 new ArrayListInteger(); list2.add(123); System.out.println(list1.getClass() list2.getClass()); } }在这个例子中我们定义了两个ArrayList数组不过一个是ArrayListString泛型类型的只能存储字符串一个是ArrayListInteger泛型类型的只能存储整数最后我们通过list1对象和list2对象的getClass()方法获取他们的类的信息最后发现结果为true。这就是java的泛型擦除。下面我们再来看一个例子加深一下理解代码语言javascriptAI代码解释public class Test001 { public static void main(String[] args) throws Exception { ArrayListInteger listnew ArrayListInteger(); list.add(1); list.getClass().getMethod(add,Object.class).invoke(list, asd); for(int i0;ilist.size();i) { System.out.println(list.get(i)); } } }上面这段代码首先创建了一个ArrayList泛型类型实例化为Integer对象如果我们直接调用add()方法那么只能添加Integer类型的值但是现在我们利用反射发现可以往里面加入String类型的值这也说明了java的泛型擦除。定义Java在编译期间所有的泛型信息都会被擦掉这就是泛型擦除。正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的在生成的字节码中是不包含泛型中的类型信息的使用泛型的时候加上类型参数在编译器编译的时候会去掉这个过程成为类型擦除。2、类型擦除后保留的原始类型原始类型就是擦除去了泛型信息最后在字节码中的类型变量的真正类型无论何时定义一个泛型相应的原始类型都会被自动提供类型变量擦除并使用其限定类型无限定的变量用Object替换。例1、代码语言javascriptAI代码解释class PairT { private T value; public T getValue() { return value; } public void setValue(T value) { this.value value; } }Pair的原始类型为Object代码语言javascriptAI代码解释class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value value; } }因为在PairT中T 是一个无限定的类型变量所以用Object替换其结果就是一个普通的类如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair如PairString或PairInteger但是擦除类型后他们的就成为原始的Pair类型了原始类型都是Object。如果类型变量有限定那么原始类型就用第一个边界的类型变量类替换。例如代码语言javascriptAI代码解释public class PairT extends Comparable {}此时的原始类型就不再是Object而是Comparable了。在调用泛型方法时可以指定泛型也可以不指定泛型。在不指定泛型的情况下泛型变量的类型为该方法中的几种类型的同一父类的最小级直到Object在指定泛型的情况下该方法的几种类型必须是该泛型的实例的类型或者其子类代码语言javascriptAI代码解释public class Test { public static void main(String[] args) { /**不指定泛型的时候*/ int i Test.add(1, 2); //这两个参数都是Integer所以T为Integer类型 Number f Test.add(1, 1.2); //这两个参数一个是Integer以风格是Float所以取同一父类的最小级为Number Object o Test.add(1, asd); //这两个参数一个是Integer以风格是Float所以取同一父类的最小级为Object /**指定泛型的时候*/ int a Test.Integeradd(1, 2); //指定了Integer所以只能为Integer类型或者其子类 int b Test.Integeradd(1, 2.2); //编译错误指定了Integer不能为Float Number c Test.Numberadd(1, 2.2); //指定为Number所以可以为Integer和Float } //这是一个简单的泛型方法 public static T T add(T x,T y){ return y; } }其实在泛型类中不指定泛型的时候也差不多只不过这个时候的泛型为Object就比如ArrayList中如果不指定泛型那么这个ArrayList可以存储任意的对象。Object泛型代码语言javascriptAI代码解释public static void main(String[] args) { ArrayList list new ArrayList(); list.add(1); list.add(121); list.add(new Date()); }四、常见的泛型面试题Java中的泛型是什么 ? 使用泛型的好处是什么?这是在各种Java泛型面试中一开场你就会被问到的问题中的一个主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人 都知道在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全确保你只能把正确类型的对象放入 集合中避免了在运行时出现ClassCastException。Java的泛型是如何工作的 ? 什么是类型擦除 ?这是一道更好的泛型面试题。泛型是通过类型擦除来实现的编译器在编译时擦除了所有类型相关的信息所以在运行时不存在任何类型相关的信息。例如 List在运行时仅用一个List来表示。这样做的目的是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况你会 得到一些后续提问比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更 多信息。什么是泛型中的限定通配符和非限定通配符 ?这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符一种是它通过确保类型必须是T的子类来设定类型的上界另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化否则会导致编译错误。另一方面表 示了非限定通配符因为?可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。List? extends T和List ? super T之间有什么区别 ?这和上一个面试题有联系有时面试官会用这个问题来评估你对泛型的理解而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是 限定通配符的例子List? extends T可以接受任何继承自T的类型的List而List? super T可以接受任何T的父类构成的List。例如List? extends Number可以接受ListInteger或ListFloat。如何编写一个泛型方法让它能接受泛型参数并返回泛型类型?编写泛型方法并不困难你需要用泛型类型来替代原始类型比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下一个泛型方法可能会像这样:public V put(K key, V value) {return cache.put(key, value);}Java中如何使用泛型编写带有参数的类?这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型而且要使用JDK中采用的标准占位符。编写一段泛型程序来实现LRU缓存?对于喜欢Java编程的人来说这相当于是一次练习。给你个提示LinkedHashMap可以用来实现固定大小的LRU缓存当LRU缓存已经满 了的时候它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法该方法会被put() 和putAll()调用来删除最老的键值对。当然如果你已经编写了一个可运行的JUnit测试你也可以随意编写你自己的实现代码。你可以把ListString传递给一个接受ListObject参数的方法吗对任何一个不太熟悉泛型的人来说这个Java泛型题目看起来令人疑惑因为乍看起来String是一种Object所以 ListString应当可以用在需要ListObject的地方但是事实并非如此。真这样做的话会导致编译错误。如 果你再深一步考虑你会发现Java这样做是有意义的因为ListObject可以存储任何类型的对象包括String, Integer等等而ListString却只能用来存储Strings。ListObject objectList;ListString stringList;objectList stringList; //compilation error incompatible typesArray中可以用泛型吗?这可能是Java泛型面试题中最简单的一个了当然前提是你要知道Array事实上并不支持泛型这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array因为List可以提供编译期的类型安全保证而Array却不能。如何阻止Java中的类型未检查的警告?如果你把泛型和原始类型混合起来使用例如下列代码Java 5的javac编译器会产生类型未检查的警告例如List rawList new ArrayList()注意: Hello.java使用了未检查或称为不安全的操作;这种警告可以使用SuppressWarnings(“unchecked”)注解来屏蔽。