前阵子给公司新人培训Java 基础相关的一些点,系统整理了一下泛型相关的知识点。特来分享一下。但愿能让一些对泛型不熟悉的同窗彻底掌握Java 泛型的相关知识点。java
开始以前,先给你们来一道测试题。程序员
List<String> strList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); System.out.println(strList.getClass() == integerList.getClass());
请问,上面代码最终结果输出的是什么?熟悉泛型的同窗应该可以答出来,而对泛型有所了解,可是了解不深刻的同窗可能会答错。编程
泛型概述数组
泛型的定义和使用安全
通配符 ?app
List<String>
传递给一个接受List<Object>
参数的方法吗?最先的“泛型编程”的概念起源于C++的模板类(Template),Java 借鉴了这种模板理念,只是二者的实现方式不一样。C++ 会根据模板类生成不一样的类,Java 使用的是类型擦除的方式。
Java1.5 发行版本中增长了泛型(Generic)。jvm
有不少缘由促成了泛型的出现,而最引人注意的一个缘由,就是为了建立容器类。-- 《Java 编程思想》ide
容器就是要存放要使用的对象的地方。数组也是如此,只是相比较的话,容器类更加的灵活,具备更多的功能。全部的程序,在运行的时候都要求你持有一大堆的对象,因此容器类算得上最须要具备重用性的类库之一了。测试
看下面这个例子,ui
public class AutoMobile { } /** * 重用性很差的容器类 */ public class Holder1 { private AutoMobile a; public Holder1(AutoMobile a) { this.a = a; } //~~ } /** * 想要在java5 以前实现可重用性的容器类 * @author Richard_yyf * @version 1.0 2019/8/29 */ public class Holder2 { private Object a; public Holder2(Object a) { this.a = a; } public Object getA() { return a; } public void setA(Object a) { this.a = a; } public static void main(String[] args) { Holder2 h2 = new Holder2(new AutoMobile()); AutoMobile a = (AutoMobile) h2.getA(); h2.setA("Not an AutoMobile"); String s = (String) h2.getA(); h2.setA(1); Integer x = (Integer) h2.getA(); } } /** * 经过泛型来实现可重用性 * 泛型的主要目的是指定容器要持有什么类型的对象 * 并且由编译器来保证类型的正确性 * * @author Richard_yyf * @version 1.0 2019/8/29 */ public class Holder3WithGeneric<T> { private T a; public Holder3WithGeneric(T a) { this.a = a; } public T getA() { return a; } public void setA(T a) { this.a = a; } public static void main(String[] args) { Holder3WithGeneric<AutoMobile> h3 = new Holder3WithGeneric<>(new AutoMobile()); // No class cast needed AutoMobile a = h3.getA(); } }
经过上述对比,咱们应该能够理解类型参数化具体是什么个意思。
在没有泛型以前,从集合中读取到的每个对象都须要进行转换。若是有人不当心插入了类型错误的对象,在运行时的转换处理就会出错。这显然是不可忍受的。
泛型的出现,给Java带来了不同的编程体验。
Holder<AutoMobile>
这个类型显化的效果,程序员可以一目了然猜想出这个容器类持有的数据类型。泛型按照使用状况能够分为 3 种。
定义格式:
public class 类名 <泛型类型1,...> { ... }
尖括号 <>
中的 字母 被称做是类型参数,用于指代任何类型。咱们常看到<T>
的写法,事实上,T 只是一种习惯性写法,若是你愿意。你能够这样写。
public class Test<Hello> { Hello field1; }
但出于规范和可读性的目的,Java 仍是建议咱们用单个大写字母来表明类型参数。常见的如:
定义格式:
public <泛型类型> 返回类型 方法名(泛型类型 变量名) { ... }
注意事项:
<T>
中的T
被称为类型参数,而方法中的 T
被称为参数化类型,它不是运行时真正的参数。/** * 泛型类与泛型方法的共存现象 * @author Richard_yyf * @version 1.0 2019/8/29 */ public class GenericDemo2<T> { public void testMethod(T t){ System.out.println(t.getClass().getName()); } public <T> T testMethod1(T t){ return t; } public static void main(String[] args) { GenericDemo2<String> t = new GenericDemo2<>(); t.testMethod("generic"); Integer integer = 1; Integer i = t.testMethod1(integer); } }
泛型方法始终以本身定义的类型参数为准
固然,现实场景下千万不要去做死写出这么难以阅读的代码。
泛型接口和泛型类差很少。
定义格式:
public interface 接口名<泛型类型> { ... }
Demo
public interface GenericInterface<T> { void show(T t); } public class GenericInterfaceImpl<String> implements GenericInterface<String>{ @Override public void show(String o) { } }
除了用 <T>
表示泛型外,还有 <?>
这种形式。? 被称为通配符。
为何要引进这个概念呢?先来看下下面的Demo.
public class GenericDemo2 { class Base{} class Sub extends Base{} public void test() { // 继承关系 Sub sub = new Sub(); Base base = sub; List<Sub> lsub = new ArrayList<>(); // 编译器是不会让下面这行代码经过的, // 由于 Sub 是 Base 的子类,不表明 List<Sub>和 List<Base>有继承关系。 List<Base> lbase = lsub; } }
在现实编码中,确实有这样的需求,但愿泛型可以处理某一范围内的数据类型,好比某个类和它的子类,对此 Java 引入了通配符这个概念。
因此,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
<?>
被称做无限定的通配符<? extends T>
被称做有上限的通配符<? super T>
被称做有下限的通配符<?>
无限定通配符常常与容器类配合使用,它其中的 ? 其实表明的是未知类型,因此涉及到 ? 时的操做,必定与具体类型无关。
// Collection.java public interface Collection<E> extends Iterable<E> { boolean add(E e); } public class GenericDemo3 { /** * 测试 无限定通配符 <?> * @param collection c */ public void testUnBoundedGeneric(Collection<?> collection) { collection.add(123); collection.add("123"); collection.add(new Object()); // 你只能调用 Collection 中与类型无关的方法 collection.iterator().next(); collection.size(); } }
无需关注 Collection 中的真实类型,由于它是未知的。因此,你只能调用 Collection 中与类型无关的方法。
有同窗可能会想,<?>
既然做用这么眇小,那么为何还要引用它呢?
我的认为,提升了代码的可读性,程序员看到这段代码时,就可以迅速对此创建极简洁的印象,可以快速推断源码做者的意图。
(用的不多,可是要理解)
为了接下去的说明方便,先定义一下几个类。
class Food {} class Fruit extends Food {} class Apple extends Fruit {} class Banana extends Fruit {} // 容器类 class Plate<T> { private T item; public Plate(T item) { this.item = item; } public T getItem() { return item; } public void setItem(T item) { this.item = item; } }
<?>
表明着类型未知,可是咱们的确须要对于类型的描述再精确一点,咱们但愿在一个范围内肯定类别,好比类型 T 及 类型 T 的子类均可以放入这个容器中。
在这个体系中,上限通配符 Plate<? extends Fruit>
覆盖下图中蓝色的区域。
边界让Java不一样泛型之间的转换更容易了。但不要忘记,这样的转换也有必定的反作用。那就是容器的部分功能可能失效。
public void testUpperBoundedBoundedGeneric() { Plate<? extends Fruit> p = new Plate<>(new Apple()); // 不能存入任何元素 p.setItem(new Fruit()); // error p.setItem(new Apple()); // error // 读出来的元素须要是 Fruit或者Fruit的基类 Fruit fruit = p.getItem(); Food food = p.getItem(); // Apple apple = p.getItem(); }
<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。好比下面例子里两个set()方法,插入Apple和Fruit都报错。
缘由是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。多是Fruit?多是Apple?也多是Banana,RedApple,GreenApple?
若是你须要一个只读容器,用它来produce T,那么使用<? extends T> 。
相对应的,还有下限通配符 <? super T>
对应刚才那个例子,Plate<? super Fruit>
覆盖下图中红色的区域。
public void testLowerBoundedBoundedGeneric() { // Plate<? super Fruit> p = new Plate<>(new Food()); Plate<? super Fruit> p = new Plate<>(new Fruit()); // 存入元素正常 p.setItem(new Fruit()); p.setItem(new Apple()); // 读取出来的东西,只能放在Object中 Apple apple = p.getItem(); // error Object o = p.getItem(); }
由于下界规定了元素的最小粒度的下限,其实是放松了容器元素的类型控制。既然元素是Fruit的基类,往里面存比Fruit粒度小的类均可以。可是往外读取的话就费劲了,只有全部类的基类Object能够装下。但这样一来元素类型信息就都丢失了。
PECS - Producer Extends Consumer Super
泛型是 Java 1.5 版本才引进的概念,在这以前是没有泛型的概念的,但显然,泛型代码可以很好地和以前版本的代码很好地兼容。
这是由于,泛型信息只存在于代码编译阶段,在进入 JVM 以前,与泛型相关的信息会被擦除掉
专业术语叫作 类型擦除。
List<String> strList = new ArrayList<String>(); List<Integer> integerList = new ArrayList<Integer>(); System.out.println(strList.getClass() == integerList.getClass());
==== output ===== true =================
打印的结果为 true 是由于 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
/** * 类型擦除 相关类 * * @author Richard_yyf * @version 1.0 2019/8/29 */ public class EraseHolder<T> { T data; public EraseHolder(T data) { this.data = data; } public static void main(String[] args) { EraseHolder<String> holder = new EraseHolder<>("hello"); Class clazz = holder.getClass(); System.out.println("erasure class is:" + clazz.getName()); Field[] fs = clazz.getDeclaredFields(); for ( Field f:fs) { // 那咱们可不能够说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢? System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); } EraseHolder2<String> holder2 = new EraseHolder2<>("hello"); clazz = holder2.getClass(); fs = clazz.getDeclaredFields(); for ( Field f:fs) { System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); } } static class EraseHolder2<T extends String> { T data; public EraseHolder2(T data) { this.data = data; } } }
利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不容许的操做限制。
public class EraseReflectDemo { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(23); // can't add here // 由于泛型的限制 boolean add(E e); list.add("123"); // error // 利用反射能够绕过编译器去调用add方法 // 又由于类型擦除时 boolean add(E e); 等同于 boolean add(Object e); try { Method method = list.getClass().getDeclaredMethod("add", Object.class); method.invoke(list, "test"); method.invoke(list, 42.9f); } catch (Exception e) { e.printStackTrace(); } for (Object o : list) { System.out.println(o); } } }
==== output ===== 23 test 42.9 =================