1.为什么我们需要泛型?
通过两端代码我们就可以知道为什么需要泛型
1 | public int addInt(int x,int y){ |
以上例子,求两个数的和。现在已经有int类型的求和跟float类型的求和,但是如果要实现一个double类型的和,就需要重新写一个double的add方法。如下:
1 | //double |
其实对于开发者来说,逻辑是一样的,只是参数不同,如果没有泛型,就需要重写类似的方法。
所以泛型的好处:
适用于多种数据类型执行相同的代码。可以简化代码。
2.泛型类,泛型接口,泛型方法
定义一个自己的泛型:
1.泛型类
1 | public class NormalGeneric<K> { |
2.泛型接口
1 | public interface Genertor<T> { |
实现泛型接口的实现类。
实现方式一:
1
2
3
4
5
6public class ImplGenertor<T> implements Genertor<T> {
public T next() {
return null;
}
}实现方式二:
1
2
3
4
5
6public class ImplGenertor2 implements Genertor<String> {
public String next() {
return null;
}
}
可见,如果是泛型类实现泛型接口的话,那么返回值也是泛型。
3.泛型方法
泛型方法是完全独立的
3.1 在普通类中的泛型方法代码如下:
1 | public class GenericMethod { |
3.2 在泛型类里面使用泛型方法:
1 | public class GenericMethod2 { |
上述代码中,
1 | public T getKey(){ |
虽然在方法中使用了泛型,但是这并不是一个泛型方法。
这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
所以在这个方法中才可以继续使用 T 这个泛型。
1 | public <T,K> K showKeyName(Generic<T> container){ |
这才是一个真正的泛型方法。
小结1:
- 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,
- 并且声明了一个泛型T_ 这个T可以出现在这个泛型方法的任意位置._
- 泛型的数量也可以为任意多个
再看个代码示例:
1 | public class GenericMethod3 { |
小结2:
泛型类里面定义泛型方法,参数可以完全不一样。
上面的T影响泛型类的普通方法。
但是对泛型方法没有影响。
3.如何限定类型变量
比如计算两个变量的最小值,最大值。
但是怎么才能保证传入的变量一定有compareTo方法?
为了解决这个问题,就引入了限定类型变量的泛型。
代码如下:
1 | public static <T extends Comparable> T min(T a, T b){ |
T extends Comparable中
T表示应该绑定类型的子类型,后面的Comparable表示绑定类型,
子类型和绑定类型可以是类也可以是接口。
但是如果类跟接口混用的话,规则如下:
- 类只能有一个
- 并且类需要写到接口的前面
- 限定类型的泛型,对泛型类,泛型方法同样适用
代码如下:
1 | public static <T extends ArrayList & Comparable> T min(T a, T b) { |
4.泛型使用中的约束和局限性
假设我们有以下泛型类:
1 | public class Restrict<T> { |
- 不能用基本类型实例化类型参数
1 | //错误代码示例 |
- 在静态域或者方法里面不能引用类型变量
1 | //错误代码示例 |
- 先执行static方法
- 再执行构造方法
- 所以先执行static T,根本不知道T的类型,所以行不通
- 但是如果是静态方法本身是泛型方法是可以的
1 | //正确代码示例 |
- 基础类型不允许做实例化参数的,例如:double不能用在泛型参数里,必须用double的包装类Double
错误的写法:
1 | //错误代码示例 |
- 正确的写法:
1 | //正确代码示例 |
- 泛型里面不允许使用instanceof
- 泛型中可以定义反省数组,但是不能创建参数化类型的数组
- 不能捕获泛型类的实例
但是如果把异常抛出来,是可以,如下:
小结:
- 不能用基本类型实例化类型参数。
- 在静态域或者方法里面不能引用类型变量。
- 基础类型不允许做实例化参数的,例如:double不能用在泛型参数里,必须用double的包装类Double。
- 泛型里面不允许使用instanceof。
- 泛型中可以定义反省数组,但是不能创建参数化类型的数组。
- 不能捕获泛型类的实例。
5.泛型类型能继承吗?
例子如下:
1 | public class Employee { |
1 | Pair<Employee> employeePair2 = new Pair<Worker>(); |
会报错。
但是,泛型类是可以继承或者扩展其他类的!
1 | /*泛型类可以继承或者扩展其他泛型类,比如List和ArrayList*/ |
小结
- 泛型类无法协变的
- 泛型可以继承或者扩展其他的泛型类
6.泛型中的通配符类型
现有如下类的派生关系:
代码如下:
1 | //我们有如下方法和泛型类 |
1 | public class Food { |
但是使用的时候,print(b)
是不允许的。正如之前提到的,Orange
虽然是派生自Fruit
,但是GenericType<Orange>
和GenericType<Fruit>
是没有任何关系的。
1 | public static void use() { |
所以为了解决泛型无法协变的问题的的问题,就引入了泛型通配符的概念。
1.协变指的就是Orange派生自Fruit,那么List也派生自List,但是泛型是不支持的。
2.泛型T是确定的参数类型,一旦传了就定下来了,而通配符非常的灵活是不确定的类型,更多的情况用于扩充参数范围。
3.通配符不是类型参数变量,通配符更像一种规定,规定你只能传哪些类型的参数。
就上述代码,我们想要print(b),可以这么做:
1 | public static void print2(GenericType<? extends Fruit> p) { |
1 | public static void use2() { |
这里的?就是通配符。
6.1 上界通配符<? extends T>
利用 <? extends T>
形式的通配符,可以实现泛型的向上转型。
1 | ? extends Fruit 表示通配符的上界是Fruit |
GenericType<? extends Fruit>
代表一个可以持有Fruit
及其子类(如:Apple,Orange)的实例的GenericTyp对象。
6.1.1 如果?
是Fruit
的父类,会怎样?
6.1.2 上界只能外围取,不能往里放
GenericType<T>
方法中有getData()
和setData()
方法。
1 | public class GenericType<T> { |
代码调用如下:
1 | public static void use2() { |
我们发现往水果里面设置水果类型的方法
setData()
会失效,但是获取某种水果类型的getData()
方法还有效。
原因:? extends T
表示类型的上界,类型参数是T的子类或者他本身,那么可以肯定的说,get方法返回的一定是T,这个编译器是可以确定的。但是set方法只知道传入的是个T,具体是T还是T的哪个子类,编译器不能确定。
Java编译期只知道容器里面存放的是Fruit或者是Fruit的派生类,具体是什么类型,编译器并不知道。当编译器执行到
c.setData(apple)
,GenericType
并没有将值设置成apple,而是标记了一个占位符capture #1
,用来表示
编译器捕获到一个Fruit类或者他的派生类,具体什么类型不知道。所以在setdata()
的时候传入的Apple和Fruit,编译器不确定是否能跟之前标记的capture #1
匹配。getData()
方法,这个可以正常用其实就很好理解了,因为上界通配符只能往容器里面放Fruit类或者他的派生类,所以获取到的类都可以隐式转换为他们的基类(或者Object基类)
6.2 下界通配符<? super T>
下界通配符只能往容器中放T或者T的基类类型的数据。<? super Apple>
代码如下:
1 | public static void printSuper(GenericType<? super Apple> p) { |
6.2.1 下界范围
1 | public static void useSuper() { |
printSuper(hongFuShiGenericType)
和printSuper(orangeGenericType)
就会在编译期报错,因为超过了通配符的下界。
6.2.2 下界不影响往里存,但往外取只能放在Object 对象里
同样GenericType<T>
方法中有getData()
和setData()
方法。
1 | public class GenericType<T> { |
代码调用如下:
1 | public static void useSuper() { |
我们发现setData()
是可以正常调用的,getDat()
方法返回了Object对象。但是为什么setData()
的时候传入了Apple的父类,就会报错呢?以及为什么getData()
只能用object来接收呢?
原因:
? super T
,表示的类型的下界,也就是说表示的是T的基类。所以我们实际上是不知道这个类到底是什么,但是肯定是T的超类或者他本身。
- 因此我们使用
x.setData()
的时候,如果set的类是T的子类或者T,那么他们可以安全的向上转型为T。所以我们x.setData()
可以放T的子类或者T本身。对于T的基类,编译器并不知道这个类对象是否是安全的,所以在使用下界通配符的时候是不能直接添加T的基类。- 我们在使用
x.getData()
的时候,返回的值一定是T的超类或者他本身,但是要转成哪个超类,编译器不知道,唯一可以确定的,Object是所有类的基类,所以只有转成Object才是安全的。这就是我们在使用getData()
的时候必须使用Object来接收原因。
6.3 PECS
PECS:Producer extends Consumer super (生产者使用extends,consumer使用super)
- 上界<? extends T>不能往里存,只能往外取(不能set,只能get),适合频繁往外面读取内容的场景。
- 下界<? super T>不影响往里存,但往外取只能放在Object对象里,适合经常往里面插入数据的场景。
7.虚拟机如何实现泛型?
泛型是JDK 1.5的一项新增特性,它的本质是参数化类型(Parametersized Type)的应用,Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
看下面一个简单的泛型例子:
例子1:
我们有如下类:
1 | public class Apple extends Fruit { |
1 | public class Orange extends Fruit { |
1 | public static void main(String[] args) { |
上述例子中,apples只能存Apple
,orange中只能存Orange
,最好我们通过xxx.getClass()
方法来获取他们的类信息,最后结果发现为true。说明泛型类型Apple
和Orange
都被擦除了,只剩下原始类型。
例子2:
1 | public static void main(String[] args) { |
把这段Java代码编译成Class文件,然后字节码反编译一下,发现所有的泛型都不见了。javac xxxx.java
1 | public static void main(String[] var0) { |
待续。。。