泛型:
泛型在java基础中属于重要的一部分,掌握泛型是必要的。接下来以我的理解来解释一下
背景:编译器是先通过检查代码中泛型的类型 再进行类型擦除 再进行编译的
- 什么是泛型,
- 什么时候需要泛型,
- 泛型怎么用,
- 泛型注意的问题。
目前我接触的泛型应用就是集合使用泛型达到 一个集合里面只有一种类型的数据
泛型的好处:
1. 将运行时的异常提前至了编译时。
2. 避免了无谓的强制类型转换 。
泛型在集合中的常见应用:
ArrayList<String> list = new ArrayList<String>(); true 推荐使用。
ArrayList<Object> list = new ArrayList<String>(); false
ArrayList<String> list = new ArrayList<Object>(); false
//以下两种写法主要是为了兼顾新老系统的兼用性问题。
*
ArrayList<String> list = new ArrayList(); true
ArrayList list = new ArrayList<String>(); true
注意: 泛型没有多态的概念,左右两边的数据 类型必须 要一致,或者只是写一边的泛型类型。
推荐使用: 两边都写泛型。
具体集合是上面类型的 是看左边的泛型是什么
举个常见的例子:
集合:
ArrayList list = new ArrayList();
list.add("11");
list.add(3);
这个时候ArrayList集合中存储了多种类型,是不是看起来很不顺眼,日常应用中 我们想要的结果是 一个集合里面存储的类型是同一个。
如:
ArrayList<String> list = new ArrayList<String>();
list.add("11");
list.add(3);
//这个时候 集合list只能存放String类型的数据
//如果存储了其他的如 Integer类型之类的就会报错,是在编译前检查
泛型方法
public <T> T showKeyName(T a){//<T>是声明泛型方法
return a;
}
泛型类:
自定义泛型: 自定义泛型就是一个数据类型的占位符或者是一个数据类型的变量。
class Point< T>{ // 此处可以随便写标识符号,T是type的简称 一般都是T
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
};
注意:在方法上自定义泛型,这个自定义泛型的具体数据类型
是在调用该方法的时候传入实参时确定具体的数据类型的。
泛型类还可以同时定义多个泛型 栗子:
class Point< K,V>{ // 不一样的泛型
private K var ; // var的类型由K指定
private V time ; //time的类型由V指定
}
};
泛型类中要注意一: 静态方法无法访问类上定义的泛型
如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法
(原因我也不知道)
声明泛型一定要写在static后,返回值类型之前
class MyMessage<T>{
public T qu(T a) {
System.out.println("qu");
return a;
}
public static <t> t ha(t a){ //静态函数需要重新泛型
System.out.println("ha");
return a;
}
}
class new1{
public static void main(String[] args) {
System.out.println(MyMessage.ha("aaa"));
}
}
注意 如果是调用静态的泛型函数时不能MyMessage<String>.ha("aaa")
传入具体泛型
泛型类中要注意二:
泛型类与泛型方法共存:
public class Test1<T>{
public void testMethod(T t){
System.out.println(t.getClass().getName());
}
public <T> T testMethod1(T t){
return t;
}
}
上面代码中,Test1<T> 是泛型类,testMethod 是泛型类中的普通方法,
而testMethod1是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数
是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
意思就是调用testMethod1 结果是里面里面的那个决定的 而不是外面的那个类
如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
泛型方法与可变参数
再看一个泛型方法和可变参数的例子:
public <T> void printMsg( T... args){
for(T t : args){
System.out.println(t);
}
}
泛型父类子类继承:
class Son1 extends MyMessage<String>{
//指的是父类里面T是用String来代替了
//如果有重写的话 那么重写的父类类型是String
public String ha(String a) {
return a;
}
}
class Son2<T> extends MyMessage<T>{
public T xx(T a) {
return a;
}
}
泛型接口:
class person<T>{
public T a(T az) {
System.out.println("futher");
return az;
}
}
class son extends person<Integer>{
@Override
public Integer a(Integer az) {
System.out.println("son");
return az;
}
}
注意:不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
List<String> aList=new ArrayList<String>();
aList instanceof List<String>//编译错误
泛型有个叫泛型擦除的 意思就是说 虽然有不同的泛型 但是在编译后计算机只看到Object类型的 举个栗子:
List<String> aList1=new ArrayList<String>();
List<Integer> aList2=new ArrayList<Integer>();
编译后就变成
List aList1=new ArrayList();
List aList2=new ArrayList();
//在编译生成的字节码中不包含泛型中的类型参数,类型参数会在编译时去掉。
//例如:List<String> 和 List<Integer> 在编译后都变成 List。
泛型通配符
先来看一个问题:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>();
Generic<Number> gNumber = new Generic<Number>();
showKeyValue(gInteger);
// showKeyValue这个方法编译器会为我们报错:
我们来解决一下为什么会报错
问: 首先有个疑惑 不是有泛型擦除吗?为什么Generic<Integer>传递给
Generic<Number>的参数会报错 编译后不是变成Generic类型传递给Generic吗?
答:类型检查是在编译前判断的 所以是先判断类型 然后再泛型擦除
先来看一个问题:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>();
Generic<Number> gNumber = new Generic<Number>();
showKeyValue(gInteger);
// showKeyValue这个方法编译器会为我们报错:
我们来解决一下为什么会报错
问: 首先有个疑惑 不是有泛型擦除吗?为什么Generic<Integer>传递给
Generic<Number>的参数会报错 编译后不是变成Generic类型传递给Generic吗?
答:类型检查是在编译前判断的 所以是先判断类型 然后再泛型擦除
问:Integer不是继承Number吗 为什么还无法传递?
答:相同参数类型的泛型类的继承关系取决于泛型类自身的继承结构。
List<Integer>和List<Number>是不同的对象 没有继承关系
前提泛型类自身一定要有继承关系 而泛型继承没有一点卵用 泛型一定要相同才行 (如果有通配符那么两个维度的继承同时满足也可以)
例如 List<String> 是 Collection<String> 的子类
List<Integer> 不是 Collection<Number> 的子类
栗子:
传入的参数是List<Integer>
形参1.public static String print(List<Integer> a) 可以通过
形参2.public static String print(List<Number> a)
不能通过 两个维度没有继承关系
形参3.public static<T> String print(List<T> a) 可以通过
形参4.public static String print(List<?> a) 可以通过
public static void zz(Collection<Integer> a) 可以通过 因为存在继承
public static void zz(Collection<Number> a) 不可以
传入参数List<String>
形参1 public static void xx(List<?> a)可以
形参2 public static void xx(List<? extends Number> a) 不可以 因为限制了范围
当类型声明中使用通配符 ? 时,
其子类型可以在两个维度上扩展。
例如 Collection<Number>
在维度1上扩展:List<? extends Number>
在维度2上扩展:Collection<Integer>
两个维度上同时扩展:List<Integer>
意思就是 Collection<Number>可以接收List<Integer>
总结:
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,
另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>
和List<Object>这样的情况,类型参数String是继承自Object的。
而第二种指的是 List接口继承自Collection接口。
对于这个类型系统,有如下的一些规则:
相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。
即List<String>是Collection<String> 的子类型,
List<String>可以替换Collection<String>。
这种情况也适用于带有上下界的类型声明。
当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开
如对Collection<? extends Number>来说,
其子类型可以在Collection这个维度上展开,
即List<? extends Number>和Set<? extends Number>等;
也可以在Number这个层次上展开,即Collection<Double>和
Collection<Integer>等。如此循环下去,ArrayList<Long>和
如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
通配符写法:
public static void zz(List<?> a)
通配符是实参而且还是根实参 不是形参 所以可接受任何泛型对象
还可以定义上边界和下边界:
上边界:
public static void zz(List<? extends Number> a){
// 只能接收Number及其Number的子类 }
下边界:
public static void zz(List<? super Integer > a){
// 只能接收Integer及其Integer的父类 }
如果类型的变量有限定那么原始类型就用第一个边界的类型变量代替
上界通配符后不能往集合添加元素 取出的元素也是上界父元素 add受限制
下届通配符 可以添加元素 必须是子类或者本身 取出的元素类型都是Object get受限制
获取数据用extend通配符 添加数据用super通配符
两个都想就不别用通配符
只有super有权限添加 其他的只能查看
提一下:
Java泛型无法向上转型
class Info< T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
};
public class GenericsDemo23{
public static void main(String args[]){
Info< String> i1 = new Info< String>() ; // 泛型类型为String
Info< Object> i2 = null ;
i2 = i1 ; //这句会出错 incompatible types 因为两者不是一个对象
}
};
还有:
泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException;和MyException的。对于JVM来说,它们都是
MyException类型的。也就无法执行与异常对应的catch语句。
重点:在泛型中不能使用基本数据类型,如果需要使用基本数据类型,那么就使用基本数据类型对应的包装类型。
桥方法:
看看下面这个类SonPair
class SonPair extends Pair<String>{
public void setFirst(String fir){....}
}
很明显,程序员的本意是想在SonPair类中覆盖父类Pair的setFirst(T fir)这个方法。但事实上,SonPair中的setFirst(String fir)方法根本没有覆盖住Pair中的这个方法。
原因很简单,Pair在编译阶段已经被类型擦除为Pair了,它的setFirst方法变成了setFirst(Object fir)。 那么SonPair中setFirst(String)当然无法覆盖住父类的setFirst(Object)了。
这对于多态来说确实是个不小的麻烦,我们看看编译器是如何解决这个问题的。
编译器 会自动在 SonPair中生成一个桥方法(bridge method ) :
public void setFirst(Object fir)
{
setFirst((String) fir)
}
这样,SonPair的桥方法确实能够覆盖泛型父类的setFirst(Object) 了。而且桥方法内部其实调用的是子类字节setFirst(String)方法。对于多态来说就没问题了。
1.2)问题还没有完,多态中的方法覆盖是可以了,但是桥方法却带来了一个疑问:
现在,假设 我们还想在 SonPair 中覆盖getFirst()方法呢?
class SonPair extends Pair<String>
{
public String getFirst(){....}
}
由于需要桥方法来覆盖父类中的getFirst,编译器会自动在SonPair中生成一个 public Object getFirst()桥方法。 (干货——引入了桥方法,该方法是编译器生成的,不是程序员码出来的)
但是,疑问来了,SonPair中出现了两个方法签名一样的方法(只是返回类型不同):
①String getFirst() // 自己定义的方法
②Object getFirst() // 编译器生成的桥方法
难道,编译器允许出现方法签名相同的多个方法存在于一个类中吗?事实上有一个知识点可能大家都不知道:
① 方法签名 确实只有方法名+参数列表 。这毫无疑问!
② 我们绝对不能编写出方法签名一样的多个方法 。如果这样写程序,编译器是不会放过的。这也毫无疑问!
③ 最重要的一点是: JVM会用参数类型和返回类型来确定一个方法。 一旦编译器通某种方式自己编译出方法签名一样的两个方法 (只能编译器自己来创造这种奇迹,我们程序员却不能人为的编写这种代码)。JVM还是能够分清楚这些方法的,前提是需要返回类型不一样。
结论:
在编译阶段,所有泛型类的类型参数都会被Object或者它们的限定边界来替换。(类型擦除)
在继承泛型类型的时候,桥方法的合成是为了避免类型变量擦除所带来的多态灾难。
最后提一下泛型数组 :
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
泛型数组在java中是不支持的 因为泛型擦除的原因 如果创建了泛型数组
Object[] aa=new ArrayList[]; 加入可以创建
aa[0]=new ArrayList(); 编译器被骗 那么就是不安全的
还有协变……不是很了解