Java泛型理解

一、理解篇:

1.1 何谓泛型:

泛型本质上讲是指参数化类型。参数化类型的重要性在于,它们允许创建这样一些类、接口和方法:其所操作的数据类型被指定为一个参数(即类、接口、方法中所使用的变量类型由一些参数所指定)。我们把这样的参数叫做类型参数。(类型参数体现了参数化类型概念)

1.2 泛型好处:

泛型,是个老话题了,在SDK 1.5之后,就加入进来,其好处在于:泛型机制为我们提供了安全的开发环境,使我们不必再为变量的类型安全考虑过多的东西了。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

1.3 SDK1.4与SDK1.5的Map对比:

SDK1.4

public Map getRequestParams(HttpServletRequest request){
    Mapmap=new HashMap();
    String id=request.getParameter("id");
    String type=request.getParameter("type");
    String userName=request.getParameter("userName");
    map.put("id",id);
    map.put("type",type);
    map.put("userName",userName);
}

public void inserData(Map map){
    String id=(String)map.get("id");
    String type=(String)map.get("type");
    String userName=(String)map.get("userName");
}

SDK1.5

public Map<String,String> getRequestParams(HttpServletRequest request){
    Map<String,String> map = new HashMap<String,String>();
    String id = request.getParameter("id");
    String type = request.getParameter("type");
    String userName = request.getParameter("userName");
    map.put("id", id);
    map.put("type", type);
    map.put("userName", userName);             
}

public void inserData(Map<String,String> map){
    String id = map.get("id");
    String type = map.get("type");
    String userName = map.get("userName");
}

通过上面两段代码的比较我们发现了以下的问题:
1.4版本的代码的inserData()方法中,从map中取值时必须进行强制类型转换的操作。如果不进行强制类型转换的话,编译器会报错。可是如果进行了强制类型转换,也通过了编译器的编译。但是我们不完全肯定代码在运行时会通过。比如代码将代码写成int type = ((Integer)map.get("type")).intValue();此时是可以通过编译器编译的。但是实际类型只有向上查阅代码或者在运行时才会发现错误。
1.5版本的代码在使用了泛型方法后,inserData(Map<String,String> map)方法参数类型化了(使用了类型参数),那么这就表示着参数map中元素的key和value的值只能是String类型的。这将是代码具有很好的可读性,并且编译器也很好的利用了这个参数类型化,在调用get方法时,编译器知道返回一定是String类型的值。同时再往map中放入元素时,编译器也会进行类型检查,如果插入元素的key和value的类型不是String时,编译器会报错。

从以上的简单比较我们可以看出泛型的加入使我们在编程时更加安全有效。休息会儿,好好理解,然后进入“进阶篇”。

二、进阶篇:

2.1 学习泛型类

根据前面泛型的描述和代码示例对比,我们对泛型机制有了大概的了解。那么在实际的编程应用中,我们如何来实现和应用泛型呢。这就是我们下面将要学的内容了。

泛型应用的实现方式:泛型类、泛型接口和泛型方法;泛型类:就是声明了一个或多个具有参数化类型参数的类。

看来例子:

public class Gen<T, P> {      ---------(1)
       private T t;           ---------(2)
       private P p;           ---------(2)

       public Gen(T t,P p){   ---------(3)
              this.t = t;
              this.p = p;
       }

       public T getT() {
              return t;
       }
       public void setT(T t) {
              this.t = t;
       }

       public P getP() {
              return p;
       }
       public void setP(P p) {
              this.p = p;
       }

       public void showType(){
              System.out.println("当前实例化对象的两个参数的类型为:" +
                                 "T是"+t.getClass().getName()+"类型;" +
                                 "  P是"+p.getClass().getName()+"类型;");
       }

       public static void main(String[] args) {
              // (4)
              Gen<String,Long> gen = new Gen<String,Long>("aa",new Long(10));

              //也可以这样写,此写法只针对与简单类型所对应的包装器类
                //Gen<String,Long> gen = new Gen<String,Long>("aa",10); 
              System.out.println("当前实例化对象的属性t的值为:"+gen.getT());
              System.out.println("当前实例化对象的属性p的值为:"+gen.getP());
              gen.showType();
       }
}

代码解析:
    (1) 处代码:定义Gen类时引入了T、P两个类型参数,用一对尖括号(<>)括起来放在类名的后面。在创建Gen对象实例时,这两个类型参数所代表的实际类型就会传递给Gen的实际类型占位符(我们可以这样子来想象),在每当需要类型参数时,就会用到T或P所代表的类型对实际值进行类型比较或转换。
    (2) 处代码:这里用来定义类中的私有变量t和p它们的类型分别是T和P,其实这和我们平时定义非泛型类中的私有变量没什么分别,唯一不同的是我们在以前定义类中的私有变量时可能变量的类型都是我们已知的具体类。而在泛型类这里我们用T和P来代替(先占位)未来在实际应用时的私有变量类型。

非泛型类的私有变量定义:

public class Gen {  
    private  Long t;
    private  Person p;
}

而我们在泛型类Gen时,用到的T和P其实相当于类型String和Long,只是我们在定义泛型类时不知道将来具体的Gen实例化对象在构造时实际所传递的参数是什么类型,所以这里先用T和P来表示(预先为将来要使用的参数类型占据一个位置)。
    (3) 处代码:此处是泛型类的构造函数。我们这里的new Gen<String,Long>中的String和Long取代了Gen类内部的T和P。泛型类的构造参数与普通类的构造参数也没有什么区别,只是在编写使用私有变量作为参数的构造函数时,参数的类型要与定义泛型类时声明的类型参数的类型一致。
    (4)处代码:泛型类的实例化。我们在实例化一个泛型类时需要注意以下几点:
        (a) new 类名<类型参数1,….,类型参数n>(构造参数值列表),这里的参数值的类型要与类型参数的类型一致(除简单类型的包装对象)。

    new Gen<String,Long>("aa",new Double(10)); 
    或
     new Gen<String,Long>("aa",10.0);

        是错的。
        (b) 再声明一个泛型类实例时,所传递的形参必须是一个对象,不能传递一个简单类型的参数。这也说明的类型参数所指的类型是类类型,不包括简单类型。

泛型类的一般形式
    由上面的泛型类示例我们可以描述出泛型类的一般语法形式:
    访问限制修饰符 Class 类名<类型参数列表>{…………..}
    申明、引用泛型类的语法形式:
    类名<类型参数列表> 变量名 = new 类名<类型参数列表>(参数值列表);

同一种泛型不同版本的实例对象之间的差别与相互转换
    我们在理解的泛型类之后,是否会有这样的疑问。在定义了一个泛型类后,对实例化的泛型类对象之间如何分辨呢?

  Gen<String,Long> genOne = new Gen<"aa",10>
  和
  Gen<String,Double> genTwo = new Gen<"aa",10.0>

我们如何分辨genOne和genTwo是否是一个类型,genOne=genTwo是否正确。
实际上区别泛型类的实例引用类型很简单,我们通过类型参数所指的实际类型不同,就可以区别。

不同的类型参数版本的泛型类对象所引用的类型是不同的。可以通过改变泛型实例中的类型参数的实际类型而对不同版本的对象进行的转换。但是不建议这样使用,这样就违背了泛型的设计原则,程序也就没有了所谓的安全性。你可以想象,如果你在代码中任意转换泛型类示例所引用的版本类型,那么最后你也不知道这个对象实例中参数类型所表示类型具体是什么类型了。类型参数化也就失去了意义。

public class Gen<T, P> { 
    private T t;
    private P p;

    public Gen(){}

    public Gen(T t,P p){
       this.t = t;
       this.p = p;
    }

    public Gen(T t){
       this.t = t;
    }

    public T getT() {
       return t;
    }
    public void setT(T t) {
       this.t = t;
    }

    public P getP() {
       return p;
    }
    public void setP(P p) {
       this.p = p;
    }

    public void showType(){
       System.out.println("当前实例化对象的两个参数的类型为:" +
                          "T是"+t.getClass().getName()+"类型;" +
                          "  P是"+p.getClass().getName()+"类型;");
    }

    public static void main(String[] args) {      
       //gen默认的是Gen<T,P>版本的泛型版本类型
       Gen gen = new Gen();
       
       //gen1默认的是Gen<String,Long>版本的泛型版本类型
       Gen<String,Long> gen1 = new Gen<String,Long>("aa",new Long(10));
       
       //gen2默认的是Gen<String,String>版本的泛型版本类型
       Gen<String,String> gen2 = new Gen<String,String>("aa","bb");
       
       //gen3默认的是Gen<String,未知类型>版本的泛型版本类型
       Gen gen3 = new Gen("cc");   

       //给gen对象私有变量赋值
       gen.setT("dd");
       gen.setP(1000);

       //现在gen引用的是Gen<String,Long>版本的泛型版本类型
       //gen2=gen;可以通过编译,但是在运行时会抛出如下错误。    
       //java.lang.ClassCastException: 
       //java.lang.Integer cannot be cast to java.lang.String
       //如果Gen<T,P> 如果是一个别人提供的封装类,
       //那么此时我们将不会知道P所代表的类型,
       //这也就失去了参数类型化的意义,程序将变得不再安全。违背了泛型设计原则。
       System.out.println("当前实例化对象的属性t的值为:" + gen.getT());
       System.out.println("当前实例化对象的属性t的值为:" + gen.getP());
       gen.showType();      

       //现在gen引用的是Gen<String,Long>版本的泛型版本类型
       gen = gen1; //正确
       System.out.println("当前实例化对象的属性t的值为:" + gen.getT());
       System.out.println("当前实例化对象的属性t的值为:" + gen.getP());
       gen.showType();

       //现在gen引用的是Gen<String,String>版本的泛型版本类型
       gen = gen2;//正确     
       System.out.println("当前实例化对象的属性t的值为:" + gen.getT());
       System.out.println("当前实例化对象的属性t的值为:" + gen.getP());
       gen.showType();

       //现在gen引用的是Gen<String,未知类型>版本的泛型版本类型
       gen = gen3;//正确     
       System.out.println("当前实例化对象的属性t的值为:" + gen.getT());
       System.out.println("当前实例化对象的属性t的值为:" + gen.getP());//此处报空指针错误。
       gen.showType();
       
       //gen1 = gen2;
       //此处会报错,Type mismatch: 
       //cannot convert from Gen<String,String> to Gen<String,Long> 
    }
}

输出的结果:

当前实例化对象的属性t的值为:dd
当前实例化对象的属性t的值为:1000
当前实例化对象的两个参数的类型为:T是java.lang.String类型;  P是java.lang.Integer类型;
当前实例化对象的属性t的值为:aa
当前实例化对象的属性t的值为:10
当前实例化对象的两个参数的类型为:T是java.lang.String类型;  P是java.lang.Long类型;
当前实例化对象的属性t的值为:aa
当前实例化对象的属性t的值为:bb
当前实例化对象的两个参数的类型为:T是java.lang.String类型;  P是java.lang.String类型;
当前实例化对象的属性t的值为:cc
当前实例化对象的属性t的值为:null
Exception in thread "main" java.lang.NullPointerException
    at com.sn.exercise.one.Gen.showType(Gen.java:38)
    at com.sn.exercise.one.Gen.main(Gen.java:84)

通过以上的代码与分析我们基本上了解了泛型类的定义和使用以及注意事项。

小结
1)泛型的实现方式:泛型类、泛型接口和泛型方法。
2)泛型类的一般定义形式:访问限制修饰符 Class 类名<类型参数列表>{…………..}
      泛型类对象的申明形式:类名<类型参数列表> 变量名 = new 类名<类型参数列表>(参数值列表);
3)最好调用标准的泛型构造函数(以定义类中申明的类型参数列表中的格式定义的构造函数)来创建泛型类对象。
4)创建了标准的泛型类对象后,不要改变类型参数所表示的实际类型(不对类型参数所表示的实际类型进行改变)。

2.2 学习泛型方法

定义泛型方法
定义一个泛型方法很简单,我们知道泛型的本质是参数化类型,所以我们声明一个使用了类型参数的方法就是泛型方法。泛型方法的一般形式看起来是这样的:
访问修饰符  <类型参数列表> 返回值   方法名(){………………………}
如:public <T> getIndexOfValue(T  t)(){………………………}
泛型方法可以声明在普通类中也可以声明在泛型类中,甚至一个普通类的构造函数也可以是泛型的。

调用泛型方法
在调用一个泛型方法时,需要在方法名前加入实际的类型参数。
比如:obj.<Integer> getIndexOfValue(4);
但是在实际应用中我们可以省略类型参数,像普通的方法调用一样。这是实参的类型编译器可以自动识别。比如:obj. getIndexOfValue(4); 这里的4自动转换成Integer类型。

public class GenMethodDemo {
    static<T,V extends T> boolean contain(T t,V[] v){
       for(int i=0;i<v.length;i++){
           if(v[i].equals(t)){
              return true;
           }
       }
       return false;
    }   

    public static void main(String[] args) {
       Integer[] num = new Integer[]{1,2,3,4,5,6,7,8,9};
       GenMethodDemo.contain(2,num);//返回true
       GenMethodDemo.contain("",num);//此处编译出错 提示第一个参数不是第二个参数的同类或子类
    }
}

三、后续:

最近看到一篇讲泛型接口或类的定义的例子,摘要过来,和大家分享下,算是个巩固吧:

public class Fruit{
	
}

public class Apple extends Fruit {
	
}

public class Basket{
	//从泛型化的list中取出Fruit,里面的元素一定是Fruit的子类,
	//所以都可以当作Fruit返回
	public static Fruit getFruit(List<? extends Fruit> list){
		return list.get(0);
	}
	
	//将Apple放入泛型化的list(这个list可以接收的类型是Apple的父类,
	//所以一定可以接收apple或apple的子类)
	public static void addApple(List<? super Apple> list,Apple apple){
		list.add(apple);
	}
}

public class GenericTester extends AbstractTester{
	public void test1(){
		List<Apple> appleList = new ArrayList<Apple>();
		
		//先定义List<Apple>,填入元素
		appleList.add(new Apple());
		
		//List<Apple>类型符合List<? extends Fruit>的要求
		Fruit fruit=Basket.getFruit(appleList);
	}
	
	public void test2(){
		List<Fruit> fruitList=new ArrayList<Fruit>();
		
		//List<Fruit>符合List<? super Apple>的要求
		Basket.addApple(fruitList, new Apple());
	}
}

定义了一个Fruit超类,和Apple子类,并定义了一个存取类Basket。

定义类或接口时,使用"<E extends Fruit>"这种形式,之后就可以在类中对E进行操作。
定义方法所接收的参数时,使用"List<? extends Fruit>"这种形式,就可以接收这个范围的List做参数。
实例化时,不能使用问号这种形式来指定泛型——不能new List<? extends Apple>();
继承或实现时,也不能使用问号这种形式来指定泛型——不能public interface MyList extends List<? extends Apple>

关于extends和super关键字的PECS(producer-extends, consumer-super)原则:
如果参数化类型表示一个T生产者,就使用<? extends T>,因为它只能get,用于将数据从生产者取出(只要生产者可以生产T的子类,那就一定可以生产T);
如果参数化类型表示一个T消费者,就使用<? super T>,因为它只能add,用于将数据add入消费者(只要消费者可以消费T的超类,那就一定可以消费T)。

其实最好的例子,莫过于Java中自带的Collection, List/Map, ArrayList, LinkedList, HashMap, HashTable等,建议大家有空看地源码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值