一、泛型概述
1.1 泛型的设计背景
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。
因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection<E>,List<E>,ArrayList<E> 这个<E>就是类型参数,即泛型。
泛型:标签。
举例:
中药店,每个抽屉外面贴着标签;
超市购物架上很多瓶子,每个瓶子装的是什么,有标签;
1.2 泛型的理解
泛型在Java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
“泛型”的字面意思就是 广泛的类型。类、接口和方法代码可以同应用于非常广泛的类型,代码与它们可以操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,而且可以提高代码的可读性和安全性。
体现在代码层面,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
从JDK1.5以后,Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>,这表明该List只能保存字符串类型的对象。
JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
1.3 为什么要有泛型
那么为什么要有泛型呢,直接Object不是也可以存储数据吗?
1、解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
2、解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。
在集合中没有泛型时:
public class GenericTest {
//在集合中使用泛型之前的情况:
@Test
public void test1() {
ArrayList list = new ArrayList();
//需求:存放学生的成绩
list.add(78);
list.add(76);
list.add(89);
list.add(88);
//问题一:类型不安全
list.add("Tom");
for (Object score : list) {
//问题二:强转时,可能出现ClassCastException
int stuScore = (Integer) score;
System.out.println(stuScore);
}
}
}
在集合中有泛型时:
public class GenericTest {
//在集合中使用泛型的情况:以ArrayList为例
@Test
public void test2() {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(78);
list.add(87);
list.add(99);
list.add(65);
//编译时,就会进行类型检查,保证数据的安全
// list.add("Tom");
//方式一:
for (Integer score : list) {
//避免了强转操作
int stuScore = score;
System.out.println(stuScore);
}
System.out.println("----------------------------");
//方式二:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int stuScore = iterator.next();
System.out.println(stuScore);
}
}
//在集合中使用泛型的情况:以HashMap为例
@Test
public void test3() {
// Map<String,Integer> map = new HashMap<String,Integer>();
//jdk7新特性:类型推断
Map<String, Integer> map = new HashMap<>();
map.put("Tom", 87);
map.put("Jerry", 87);
map.put("Jack", 67);
// 编译失败
// map.put(123,"ABC");
//泛型的嵌套
Set<Map.Entry<String, Integer>> entry = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> e = iterator.next();
String key = e.getKey();
Integer value = e.getValue();
System.out.println(key + "----" + value);
}
}
}
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮。
二、简单示例解析概念
首先定义一个简单的Box类:
public class Box{
private String data;
public void setdata(String data){
this.data=data;
}
public String getData(){
return data;
}
}
这个盒子类可以装String数据,如何我们又要一个能装Integer数据的盒子呢?以此方式的话需要再写一个新的类,之后又要装Byte、Short、Long、Float、Double、Character、Boolean类型的数据呢?那么我们有要写这么多新的类难道不累吗?
注:当然有的同学说,那还不简单吗,直接使用Object不就行了吗?
public class Box {
private Object data;
public Box() {
}
public Box(Object data) {
super();
this.setData(data);
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
Object虽说可以解决一定的问题,但是在使用具体类型的时候,每次都需要强制类型转换;
为了提高工作效率,这时候泛型类就登场了:
public class Box<T> {
private T data;
public void setdata(T data) {
this.data = data;
}
public T getdata() {
return data;
}
}
这样我们的Box类便可以得到复用,我们可以将T替换成任何我们想要的类型:
Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
我们会发现:
类型后面多了个<T>;
data的类型都是T;
T是什么呢?T表示参数类型,泛型就是类型参数化,处理数据的类型不是固定的,而是可以作为参数传入。
那么如果多个类型呢?
public class Box<K, V> {
private K key;
private V value;
public Box(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
Box<Integer,String> box1=new Box<Integer, String>(1,"sean");
Box<Integer,String> box2=new Box<Integer, String>(1,"sea");
三、类型擦除
下面来说说泛型的本质作用:泛型的约束于表面,只在编译器检查的时候充当一把枷锁,约束代码的书写规范,在编译后泛型会被擦除,用原始类型取而代之:
编译(擦除)前:
public class Box<T>{
private T data;
public void setData(T data){
this.data=data;
}
public T getData(){
return data;
}
}
编译(擦除)后:
public class Box{
private Object data;
public void setData(Object data){
this.data=data;
}
public Object getData(){
return data;
}
}
泛型被擦除后直接被Object类取代,因为根据多态特性它可以指向任何类嘛,泛型对它都是无压力的。
体会:使用泛型的主要优点是能够在编译时而不是在运行时检测错误。
// JDK 1.5 之前
Comparable c = new Date();
System.out.println(c.compareTo("red"));
// JDK 1.5之后
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red")); // 报错
四、自定义泛型结构
泛型有三种使用方式,分别为:
泛型类、泛型接口;
泛型方法;
泛型的声明:
interface 接口名<T> 和 class 类名<K,V>;
其中,T,K,V不代表值,而是表示类型。这里使用任意字母都可以。常用T表示,是Type的缩写。
泛型的实例化:
一定要在类名后面指定类型参数的值(类型)。如:
List<String> strList = new ArrayList<String>();
Iterator<String> iterator = strList.iterator();
T只能是类,不能用基本数据类型填充。但可以使用包装类填充;
把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想;
4.1 泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
自定义泛型类:
// 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
// 在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
private String name;
private int age;
// key这个成员变量的类型为T,T的类型由外部指定
// 类的内部结构就可以使用类的泛型
private T key;
public Generic(){}
public Generic(T key) {
this.key = key;
}
public Generic(String name, int age, T key) {
this.name = name;
this.age = age;
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
public class GenericTest {
public static void main(String[] args) {
// 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
// 传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);
// 传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
System.out.println("泛型测试key is:" + genericInteger.getKey());
System.out.println("泛型测试key is:" + genericString.getKey());
}
}
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。
看一个例子:
public class GenericTest {
@Test
public void test1(){
// 如果定义了泛型类,实例化没有指明类的泛型,则认为此泛型类型为Object类型
Generic generic = new Generic("name1",12,"1234");
Generic generic1 = new Generic("name2",14,123);
Generic generic2 = new Generic("name3",12,5.55);
Generic generic3 = new Generic("name4",12,false);
System.out.println("泛型测试key is " + generic.getKey());
System.out.println("泛型测试key is " + generic1.getKey());
System.out.println("泛型测试key is " + generic2.getKey());
System.out.println("泛型测试key is " + generic3.getKey());
System.out.println("----------------------------");
// 建议:实例化时指明类的泛型
Generic<Integer> generic4 = new Generic<Integer>("name4",12,34);
// 下面语句编译失败
// Generic<Integer> generic5 = new Generic<Integer>("name4",12,34.44);
}
}
测试结果:
泛型测试key is 1234
泛型测试key is 123
泛型测试key is 55.55
泛型测试key is false
注意:泛型的类型参数只能是类类型,不能是简单类型。
4.2 泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
// 定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,未传入泛型实参时:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 即:class FruitGenerator<T>
* implements Generator<T>{ 如果不声明泛型,如:class FruitGenerator implements
* Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T> {
@Override
public T next() {
return null;
}
}
@Test
public void test2() {
// 由于子类在继承带泛型的父类时,未指明了泛型类型。则实例化子类对象时,需要指明泛型。
FruitGenerator<String> fruitGenerator = new FruitGenerator<>();
String next = fruitGenerator.next();
}
当实现泛型接口的类,传入泛型实参时:
import java.util.Random;
/**
* 传入泛型实参时: 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 即:Generator<T>,public T
* next();中的的T都要替换成传入的String类型。
*/
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)];
}
}
@Test
public void test3(){
// 由于子类在继承带泛型的父类时,指明了泛型类型。则实例化子类对象时,不再需要指明泛型。
FruitGenerator generic = new FruitGenerator();
String next = generic.next();
}
4.3 泛型类、泛型接口注意事项
1、泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>;
2、泛型类的构造器如下:public GenericClass(){},而下面是错误的:
public GenericClass<E>(){};
3、实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
4、泛型不同的引用不能相互赋值。
@Test
public void test3() {
Generic<String> generic1 = new Generic<>();
Generic<String> generic2 = new Generic<>();
Generic<Integer> generic3 = new Generic<>();
// 泛型相同的引用可以相互赋值。
generic1 = generic2;
// 泛型不同的引用不能相互赋值。
// generic1 = generic3;
}
5、泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
6、如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
7、jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>(); 类型推断
8、泛型的指定中不能使用基本数据类型,可以使用包装类替换。
9、在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
10、异常类不能是泛型的;
public class Generic<T> {
// ...
// 静态方法中不能使用类的泛型。因为泛型类型是实例化对象的时候传入的,先后顺序问题
/*public static void show(T orderT) {
System.out.println(orderT);
}*/
public void show() {
//编译不通过,异常需要是一个确定的类型
/*try{
}catch(T t){
}*/
}
}
// 异常类不能声明为泛型类
// public class MyException<T> extends Exception{
// }
11、不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity]; 参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
public Generic() {
//编译不通过
//T[] arr = new T[10];
//编译通过
T[] arr = (T[]) new Object[10];
}
12、父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
子类不保留父类的泛型:按需实现;
没有类型 擦除
具体类型
子类保留父类的泛型:泛型子类
全部保留
部分保留
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型;
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
4.4 泛型方法
在Java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称]) 抛出的异常
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> void printArray(T[] arr) {
for (T t : arr) {
System.out.println(t);
}
}
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
4.3.1 泛型方法的基本用法
光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把泛型方法再总结一下。
public class GenericTest {
// 这个类是个泛型类,在上面已经介绍过
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
// 我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
// 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
// 所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey() {
return key;
}
// 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
// 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
/*public E setKey(E key){
this.key = key;
}*/
}
/***
* 这才是一个真正的泛型方法。 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置. 泛型的数量也可以为任意多个 如:public <T,K> K showKeyName(Generic<T>
* container){ ... }
*/
public <T> T showKeyName(Generic<T> container) {
System.out.println("container key :" + container.getKey());
// 当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
// 这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj) {
System.out.println("泛型测试key value is " + obj.getKey());
}
// 这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
// 同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj) {
System.out.println("泛型测试key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
* public <T> T showKeyName(Generic<E> container){ ... }
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。 所以这也不是一个正确的泛型方法声明。 public void
* showkey(T genericObj){
*
* }
*/
public static void main(String[] args) {
GenericTest gt = new GenericTest();
gt.showKeyName(gt.new Generic<String>("Hello"));
}
}
4.3.2 泛型类中的泛型方法
当然这并不是泛型方法的全部,泛型方法可以出现在任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下;
public class Fruit{
@Override
public String toString() {
return "fruit";
}
}
public class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
public class Person{
@Override
public String toString() {
return "Person";
}
}
public class GenerateTest<T>{
private T t;
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public class GenericFruit {
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
// generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
4.3.3 泛型方法与可变参数
再看一个泛型方法和可变参数的例子:
public <T> void printMsg(T... args){
for(T t : args){
System.out.println("泛型测试t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);
4.3.4 静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法。
泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"Cannot make a static reference to the non-static type T"
*/
public static <T> void show(T t){
}
}
4.3.5 泛型方法总结
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。
所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
五、泛型在继承上的体现
虽然类A是类B的父类,但是G<A> 和G<B>二者不具备子父类关系,二者是并列关系。
比如:String是Object的子类,但是List<String >并不是List<Object>的子类。
补充:类A是类B的父类,A<G> 是 B<G> 的父类。
@Test
public void test1() {
List<Object> list1 = null;
List<String> list2 = new ArrayList<String>();
//此时的list1和list2的类型不具有子父类关系
//编译不通过
// list1 = list2;
//反证法:假设list1 = list2;
// list1.add(123);导致混入非String的数据。出错。
show(list1);
show1(list2);
}
public void show1(List<String> list) {
}
public void show(List<Object> list) {
}
@Test
public void test2() {
AbstractList<String> list1 = null;
List<String> list2 = null;
ArrayList<String> list3 = null;
list1 = list3;
list2 = list3;
List<String> list4 = new ArrayList<>();
}
六、通配符的使用
1、使用类型通配符:?
比如:List<?> ,Map<?,?>
List<?>是List<String>、List<Object>等各种泛型List的父类。
2、读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。
3、写入list中的元素时,不行。因为我们不知道c的元素类型,我们不能向其中添加对象。
唯一的例外是null,它是所有类型的成员。
将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。
唯一的例外的是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object。
我们知道Ingeter是Number的一个子类,我们明白Generic<Ingeter>与Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>和Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?
@Test
public void test3() {
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<?> list = null;
list = list1;
list = list2;
//编译通过
print(list1);
print(list2);
List<String> list3 = new ArrayList<>();
list3.add("AA");
list3.add("BB");
list3.add("CC");
list = list3;
//添加(写入):对于List<?>就不能向其内部添加数据。
//除了添加null之外。
// list.add("DD");
// list.add('?');
list.add(null);
//获取(读取):允许读取数据,读取的数据类型为Object。
Object o = list.get(0);
System.out.println(o);
}
public void print(List<?> list) {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
}
有限制的通配符:
<?> 允许所有泛型的引用调用
通配符指定上限:上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
通配符指定下限:下限super:使用时指定的类型不能小于操作的类,即>=
举例:
<? extends Number> (无穷小 , Number] 只允许泛型为Number及Number子类的引用调用
<? extends Number> (无穷小 , Number] 只允许泛型为Number及Number父类的引用调用
<? extends Comparable> 只允许泛型为实现Comparable接口的实现类的引用调用
@Test
public void test4() {
List<? extends Person> list1 = null;
List<? super Person> list2 = null;
List<Student> list3 = new ArrayList<Student>();
List<Person> list4 = new ArrayList<Person>();
List<Object> list5 = new ArrayList<Object>();
list1 = list3;
list1 = list4;
// 编译失败
// list1 = list5;
// 编译失败
// list2 = list3;
list2 = list4;
list2 = list5;
//读取数据:
list1 = list3;
Person p = list1.get(0);
//编译不通过
//Student s = list1.get(0);
list2 = list4;
Object obj = list2.get(0);
编译不通过
// Person obj = list2.get(0);
//写入数据:
//编译不通过
// list1.add(new Student());
// list1.add(new Person());
//编译通过
list2.add(new Person());
list2.add(new Student());
}
七、泛型应用举例
7.1 BaseDAO
public class DAO<T> {//表的共性操作的DAO
//添加一条记录
public void add(T t){
}
//删除一条记录
public boolean remove(int index){
return false;
}
//修改一条记录
public void update(int index,T t){
}
//查询一条记录
public T getIndex(int index){
return null;
}
//查询多条记录
public List<T> getForList(int index){
return null;
}
//泛型方法
//举例:获取表中一共有多少条记录?获取最大的员工入职时间?获取员工的最高薪资?
public <E> E getValue(){
return null;
}
}
public class StudentDAO extends DAO<Student> {//只能操作某一个表的DAO
}
public class CustomerDAO extends DAO<Customer>{//只能操作某一个表的DAO
}
7.2 泛型嵌套
public static void main(String[] args) {
HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>();
ArrayList<Citizen> list = new ArrayList<Citizen>();
list.add(new Citizen("黄磊"));
list.add(new Citizen("孙莉"));
list.add(new Citizen("黄忆慈"));
list.add(new Citizen("黄少艾"));
map.put("黄磊", list);
Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet();
Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Entry<String, ArrayList<Citizen>> entry = iterator.next();
String key = entry.getKey();
ArrayList<Citizen> value = entry.getValue();
System.out.println("户主:" + key);
System.out.println("家庭成员:" + value);
}
}
7.3 实际案例
用户在设计类的时候往往会使用类的关联关系,例如,一个人中可以定义一个信息的属性,但是一个人可能有各种各样的信息(如联系方式、基本信息等),所以此信息属性的类型就可以通过泛型进行声明,然后只要设计相应的信息类即可。
interface Info{ // 只有此接口的子类才是表示人的信息
}
class Contact implements Info{ // 表示联系方式
private String address ; // 联系地址
private String telephone ; // 联系方式
private String zipcode ; // 邮政编码
public Contact(String address,String telephone,String zipcode){
this.address = address;
this.telephone = telephone;
this.zipcode = zipcode;
}
public void setAddress(String address){
this.address = address ;
}
public void setTelephone(String telephone){
this.telephone = telephone ;
}
public void setZipcode(String zipcode){
this.zipcode = zipcode;
}
public String getAddress(){
return this.address ;
}
public String getTelephone(){
return this.telephone ;
}
public String getZipcode(){
return this.zipcode;
}
@Override
public String toString() {
return "Contact [address=" + address + ", telephone=" + telephone
+ ", zipcode=" + zipcode + "]";
}
}
class Introduction implements Info{
private String name ; // 姓名
private String sex ; // 性别
private int age ; // 年龄
public Introduction(String name,String sex,int age){
this.name = name;
this.sex = sex;
this.age = age;
}
public void setName(String name){
this.name = name ;
}
public void setSex(String sex){
this.sex = sex ;
}
public void setAge(int age){
this.age = age ;
}
public String getName(){
return this.name ;
}
public String getSex(){
return this.sex ;
}
public int getAge(){
return this.age ;
}
@Override
public String toString() {
return "Introduction [name=" + name + ", sex=" + sex + ", age=" + age
+ "]";
}
}
class Person<T extends Info>{
private T info ;
public Person(T info){ // 通过构造器设置信息属性内容
this.info = info;
}
public void setInfo(T info){
this.info = info ;
}
public T getInfo(){
return info ;
}
@Override
public String toString() {
return "Person [info=" + info + "]";
}
}
public class GenericPerson{
public static void main(String args[]){
Person<Contact> per = null ; // 声明Person对象
per = new Person<Contact>(new Contact("北京市","01088888888","102206")) ;
System.out.println(per);
Person<Introduction> per2 = null ; // 声明Person对象
per2 = new Person<Introduction>(new Introduction("李雷","男",24));
System.out.println(per2) ;
}
}
八、面试题
1、泛型的理解?
泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参,此时类型也定义成参数形式,然后在使用/调用时传入具体的类型;
参数化类型必须是引用数据类型;
泛型被编译后直接被Object类取代,因为根据多态特性它可以指向任何类,泛型对它都是无压力的;
2、什么是泛型擦除?
泛型的约束于表面,只在编译器检查的时候充当一把枷锁,约束代码的书写规范,在编译后泛型会被擦除,用原始类型取而代之:
3、泛型的使用(***)?
泛型类型用于类的定义中,被称为泛型类。在实例化类的时候指明泛型的具体类型;
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中;
当一个类去实现泛型接口,要么实现接口的时候必须指明泛型类型;要么实现类也必须是泛型类;
泛型方法,是在调用方法的时候指明泛型的具体类型.泛型方法可以出现在任何地方和任何场景中使用
4、泛型类和泛型接口的注意事项?
1、泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>;
2、泛型类的构造器如下:public GenericClass(){},而下面是错误的:
public GenericClass<E>(){};
3、实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
4、泛型不同的引用不能相互赋值。
5、泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
6、如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
7、jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>(); 类型推断
8、泛型的指定中不能使用基本数据类型,可以使用包装类替换。
9、在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
10、异常类不能是泛型的;
11、不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
12、父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
5、在类/接口上声明的泛型,是否可以作用于非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,是否可以作用于静态成员、静态方法?
可以作用于非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型;
不可以作用域静态成员、静态方法;原因在于:泛型的类型是在实例化对象的时候传入的;
6、静态方法可不可以使用泛型?
如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法。