目录
1、为什么引入泛型?
Java是一种强类型语言,也就是每一种数据类型都有其内存模型进行约束,不同的数据类型的内存模型是一样的,因此对不同数据类型的操作行为也是不一样的。
但是Java存在继承和多态特性,子类往往都会拥有父类部分的内存模型,因此父类引用往往可以直接地引用其子类对象,这就是自然地向上转型。所以Object类可以引用任意类型的对象而不报错。
但是想要使用子类数据类型特有的内存模型部分,就需要手动地向下转型,而就是向下转型这部分最容易报错。
因为Object可以随意引用各种数据类型,但是如果一个容器里面存放了很多被Object引用的数据类型,如何不报错的进行向下转型呢?除非提前知道容器里面每一个元素的数据类型。
public void test() { ArrayList list = new ArrayList(); // 使用Object引用各种数据类型而不会报错 list.add("aaa");//引用字符串 list.add("bbb");//引用字符串 list.add("ccc");//引用字符串 list.add(111);//引用Integer for (int i = 0; i < list.size(); i++) { /* 如果某个接盘侠不知道list里面还有Integer的话,向下强转为String直接报错, 且里面有几万个元素,排除问题也麻烦*/ System.out.println((String)list.get(i)); } }
但是这种方法对开发效率来说非常低下。
最好的方式如下:
一个容器里面都存相同的数据类型,然后使用Object进行引用,向下转型时就不用关心数据类型不同带来的麻烦。
或者对数据进行二次封装时,就指定这个类只能处理某种类型的数据类型,这样也不用担心类型转换报错了。
public void test() { ArrayList<String> list = new ArrayList<>();//提前声明这个容器只能放String类型 list.add("aaa"); /* 按照要求储存一个字符串"aaa",但是这个字符串还得被Object进行引用,只有用到 这个字符串的时候才进行向下强转为字符串,然后就可以使用字符串这种数据类型 的方法和属性,比如大小写转换*/ list.add("bbb"); list.add("ccc"); list.add(111); // 在编译阶段,编译器会报错,提示你不要放其他数据类型进去, //要不然后面的方法无法操作你这种数据类型,毕竟整数111可没有大小写转换 for (int i = 0; i < list.size(); i++) { // 直接强转,再也不用担心Cast问题了 System.out.println((String)list.get(i)); } }
总结:
- 泛型技术的出现解决了开发过程当中的难点,避免了类型转换错误。
- 泛型就是提前告知容器、类、方法等代码封装体,你们里面的逻辑只能处理我告诉你们这些数据类型。
- 这种限制操纵数据类型的事是编译器或者IDE进行提示的,也就是泛型只发生在你写代码的时候。
2、使用现成的泛型类
public void test() { /*ArrayList就是一个系统提供现成泛型类*/ //写代码时直接告诉ArrayList你这里面只能存放String就OK了 //在写代码时,你想存放不是String的数据就提示你不能放 ArrayList<String> list = new ArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); for (int i = 0; i < list.size(); i++) { System.out.println((String)list.get(i)); } }
使用泛型类时(具体指生成泛型类实例对象),需要手动指定这个泛型类型所管理的数据类型,使用<String>进行传递。
请注意:ArrayList<String>从语法上来说就是一种数据类型,list就是ArrayList<String>这种数据类型的引用。
3、自定义一个泛型类
// 类型写在类名后面的<>里面 class F1<T> { T age; } // F2可以同时封装对3种数据类型的操作 class F2<T,E> { T age;//泛型参数修饰成员属性 void show(E e){};//泛型参数修饰局部变量 E show(E e){ return e //泛型参数做方法返回值 }; }
泛型类就是指定你这个类的逻辑能处理哪些数据类型而不会发生类型转换的错误。请注意:泛型不构成重载。而且,凡是指定数据类型的语法地方都可以换成泛型。
4、使用自定义的泛型类
// F2可以同时封装对3种数据类型的操作 class F2<T,E> { } F2<String,Integer> ins = new F2<>() ;
临时生成一个F2<String,Integer>数据类型,在创建类的对象的时候确定类型参数的具体类型,然后临时生成这个类对象。
5、泛型接口
同泛型类一模一样。
6、泛型类继承
泛型类继承时需要明确指定父类的泛型类型,不管不给传的话,默认缺省值是Object。
7、泛型类实现泛型接口
同泛型类继承,需要指定实现泛型接口能够处理的类型,而且这些类型只能从当前类里面选。
8、泛型接口继承泛型接口
同泛型类实现泛型接口。
9、泛型方法
class FX<T> { T age; T m1(T k) { T s; s = null; return s; } // 在方法返回值前边限制其能够处理的数据类型, //然后在该方法里面使用数据类型的地方都可以换成泛型 static <T, E> E m3(E e) { return e; } }
注意点:
- 泛型方法的泛型参数可以和类泛型参数重复且覆盖。
- 泛型方法可以用static修饰。
- 泛型方法在调用的时候才会临时生成这个方法对象,因为指定的泛型类型不一样生成的函数对象也不一样。
- 基本数据类型不能充当泛型类型形参,因为他们不是封装数据类型,没有内存模型。
- 类泛型或者接口泛型中的泛型类型数据都无法使用static修饰。
interface I1<T> { T t=null;//报错,因为t是static的,不是使用T修饰 }
10、泛型通配符
每一种类都对应一种数据类型,可以使用Object来统一进行引用。
但是Object本身就是一种类型,就是指定只能处理Object这种数据类型,String、Integer啥的都无法处理。
这时候就可以使用万能的通配符了,也就是<?>,它可以接收任意数据类型,本质是还是使用Object引用一切。
此外:这玩意一般放在方法的形参部分。
void print(ArrayList<?> ins){ }
11、通配符上限
说白了就是:我不想使用Object进行统一引用了,换一个父类进行统一引用。
List<? extends Number> // 把父亲从Object直接换成Number
12、通配符下限
说白了就是:还是使用Object进行统一引用,但是多层继承太多的类就不需要引用了。
List<? super Integer> list
13、类型擦除
泛型只是在编码阶段进行,编译的时候会自动向上转型为Object或者上限通配符的父类。
14、类型擦除的桥接模式
当一个类实现泛型接口时,泛型接口由于类型擦除,向上转型为Object引用,而实现的泛型类已经指定了类型。所以不满足多态的条件之一(泛型接口的类型为Object,泛型类的是String),所以编译器会在泛型类里面实现一个Object的多态方法,从而实现重写。
15、泛型与数组
数组可以看做一种基本数据类型,但是里面不能放泛型类的数据对象,原因就是数组底层没有实现泛型语法。
所以只能声明泛型数组的引用,不能放里面放东西。也就是声明一个数组,这个数组里面将要存放泛型类的实例对象,但是不能真实的开辟内存空间。
但是可以使用reflect.Array()进行空间开辟。