泛型~详解~

背景:没有泛型的话,集合对元素类型就不会限制,容易发生转换异常。

举例:下面举例不检查类型可能发生的异常,

public class TestCircle {
  
  @SuppressWarnings("unchecked")
public static void main(String[] args) {
     @SuppressWarnings({ "rawtypes" })
    List list  =new ArrayList();   //
    list.add("sss");
    list.add("000");
    list.add(5);
    System.out.println(list);    //此次不会出错但是下面将元素取出来,并强制转换时候会出错。就必须引入泛型
    for(int i=0;i<list.size();i++){
        String a =    (String)list.get(i);
     }
  }
}

如何在编译时进行类型检查呢?

问题:手动实现List集合编译时类型检查需要我们定义大量的List子类,这效率不太高。

class strList{
    @SuppressWarnings("rawtypes")
    private List str = new ArrayList();
    @SuppressWarnings("unchecked")
    public boolean add(String s) {
        return str.add(s);
    }
    public String get(int index) {
        return (String)str.get(index);
        
    }
    public int size() {
        // TODO Auto-generated method stub
        return str.size();
    }
    
}
public class TestCircle {
  public static void main(String[] args) {
     @SuppressWarnings({ })
     strList list  =new strList();   //
    list.add("sss");
    list.add("000");
   /* list.add(5); */  //因为重写了add,约束了add为String
    System.out.println(list);     
    for(int i=0;i<list.size();i++){
        @SuppressWarnings("unused")
        String a =    (String)list.get(i);
     }
  }
}

对比可看出引入泛型的好处。

定义:泛型就是允许在定义类和接口时指定类型形参,该类型形参在声明变量和创建对象时传入实际参数。

注意:Set<k>是一种不同于Set的数据类型,可看做是Set的子类。包含了泛型声明的类型可以在定义变量和创建对象时传入一个类型实参,从而动态的生成无数逻辑上的子类,但这种子类实际上是不存在的。

泛型类:

不光是集合可以使用泛型,自定义类也能使用。Apple<T>就是例子(可理解为类中成员变量都只能是一种类型)。例如带泛型声明的Apple<T>类(不要理会这个类型形参是否有实际意义),可生成Apple<Double>,Apple<String>等逻辑子类。JDK在定义List,ArrayList等接口和类时使用了类型形参,所以在使用它们时可为之传入实际的类型参数。

注意:带泛型声明的自定义类,定义构造器时,不要带泛型声明。但在调用该构造器时,可使用泛型声明。

public class Apple<T> {
    public T info;
    public Apple(T info) {
    this.info=info;
   }
    public T getinfo() {
        return info;
    }
    public void setinfo(T info) {
        this.info = info;
    }
    public static void main(String[] args) {
        Apple<String> a =new Apple<String>("苹果");
        System.out.println(a.getinfo());
        Apple<Integer> b =new Apple<Integer>(2);
        System.out.println(b.getinfo());

    }
    
}

从泛型类派生出子类需要注意:

创建了泛型声明的接口,父类,可以为接口创建实现类,也可以从父类派生出子类。但是使用时不能再带类型形参。;例子:public class A extends Apple<T> 是错的,应该改为public class A extends Apple<String>  (传入具体实参)。

与使用方法时必须为形参传入实参不同的是,使用泛型声明类和接口不传入实际参数也是对的。public class A extends Apple。

如果从Apple<String>派生出子类,子类会继承String的方法,子类要重写时,需要特别注意。

父类

public class Apple<T> {
    public T info;
    public Apple(T info) {
    this.info=info;
   }
     
    public static void main(String[] args) {
         

    }
    public String name() {
        return "32";
        
    }
}

子类

public class A1 extends Apple<String>{

    public A1(String info) {
        super(info);
        // TODO Auto-generated constructor stub
    }
    public Object name() {  //重写父类返回类型不能不同
        return null;
    }
}

不存在泛型类 

public class TestCircle {
  public static void main(String[] args) {
    List<String> l1 =new ArrayList<>();
    List<Integer> l2= new ArrayList<>();
    System.out.println(l1.getClass() == l2.getClass());
}
}

上面的结果是true,而不是false,说明泛型类型在实际参数不管是什么,他在运行时总是相同的类。

之前提到了可以把ArrayList<String>看做是ArrayList<T>的子类,实际上,ArrayList<String>也确实是一种特殊的ArrayList类。但是实际上,系统并没为ArrayList<String>生成一个新的Class文件。

泛型对于所有可能的类型参数,都具有相同的行为。所以,可以把不同的类当做相同的类处理。类的静态变量和方法在所有的实例之间共享。所以在静态方法,静态初始化,静态变量的声明和初始化都不允许使用类型形参,下面示范这种错误。

public class TestCircle<T> {
    static T a;  //不能在静态属性声明里使用类型形参
    public static void name(T t) { //不能在静态方法使用类型形参
        
    }
  public static void main(String[] args) {
 
  }
}

因为系统不会生成新的泛型类,所以instanceof都不能跟泛型类。

public class TestCircle<T> {
	 
  public static void main(String[] args) {
   Collection cs =new ArrayList<String>();
		  if(cs instanceof List<String>){  //系统不会产生真正的泛型类,所以instanceof不能跟泛型类
			  
		  }
  }
}

  

下面一段话都是为了引出类型通配符

如果使用泛型类时,应该为它传入一个类型实参。如果没有,会报泛型警告,如果在定义一个方法,该方法里有一个集合形参,集合形参的类型是不确定的,那我们应该如何定义呢?

public class TestCircle<T> {
  public static  void test(List l) {   //泛型警告
    for(int i=0;i<l.size();i++){
        System.out.println(l.get(i));
    }
} 
  public static void main(String[] args) {
    
  }
}
   public class TestCircle<T> {
      public static void test(List<Object> l) {    
        for(int i=0;i<l.size();i++){
            System.out.println(l.get(i));
        }
    } 
      public static void main(String[] args) {
        List<String> l =new ArrayList<String>();
        test(l);  //正确
      }
    }

上面的例子出现编译错误,说明List<String>不能当做List<Object>对象使用,即List<String>不是List<Object>子类。

注意:如果Foo是Goo的子类型(子类或者子接口),G是一个具有泛型声明的类或接口,那么G<Foo>是G<Goo>的子类型不成立

对比下数组,数组中可以把一个Integer[]直接赋值给Number[],但是如果把一个Float对象放入Number[]数组里编译通过,但是运行会报ArrayStoreException

换成是泛型,赋值操作在编译时都是不通过的。

public class TestCircle<T> {
  
  public static void main(String[] args) {
    List<Integer> l =new ArrayList<Integer>();
    List<Number> l2 =l;  //泛型赋值不允许
  }
}

就是告诉我们;使用了泛型,在编译时没出现警告,就不会ClassCastException.

如果确实,集合形参的类型是不确定的,可以用类型通配符  ?作为类型实参传给List集合。Collection<?>(意思是未知元素类型的List)  ?可以匹配任何类型。

   public class TestCircle<T> {
      public static void test(List<?> l) {    
        for(int i=0;i<l.size();i++){
            System.out.println(l.get(i));
        }
    } 
      public static void main(String[] args) {
        List<String> l =new ArrayList<String>();
        test(l);  //正确
      }
    }

但是得注意一点,带通配符的List表示他是各种泛型List的父类,并不能把元素加入其中。因为我们不知道集合的元素类型,所以不能随便往里面增加对象。但是NULL例外。但是可以通过get()方法返回List<?>集合指定索引处的元素。虽然返回值是一个未知的类型,但是一定是Object.

设想下:如果我们不想用List<?>表示任何泛型的父类,只想让他表示是某一种泛型的父类,应该如何表示呢

例:List<circle> 当做;List<? extends Shape>使用。List<? extends Shape>表示所有的Shape泛型List的父类。但是不知道具体受限制的通配符是哪种类型,所以不能把Shape对象和Shape对象子类加入集合。

 下面用一个绘图程序解释这种用法

public class Rectangle extends Shape{

    @Override
    public void draw(Canvas c) {
        // TODO Auto-generated method stub
        System.out.println("在画布上"+c+"画正方形");
    }
    
}
public class Circle extends Shape{

    @Override
    public void draw(Canvas c) {
        // TODO Auto-generated method stub
        System.out.println("在画布上"+c+"画圆");
    }

}
public  abstract class Shape {
    //抽象母类,画图
  public abstract void draw(Canvas c);
}
import java.util.ArrayList;
import java.util.List;


public class Canvas {
    //画布上画所有形状
   public void drawAll(List<? extends Shape> shapes){
       for(Shape s:shapes){
            s.draw(this);
           
       }
   }
   public static void main(String[] args) {
    List<Circle> circles =new ArrayList<Circle>();
    Canvas c =new Canvas();
    for(Circle circle:circles){
        c.drawAll(circles);    // List<Circle>不能当List<Shape>使用
    }
}

Canvas等同与

import java.util.ArrayList;
import java.util.List;


public class Canvas {
    //画布上画所有形状
   public void drawAll(List<?> shapes){
       for(Object s:shapes){
           ((Shape) s).draw(this);
           
       }
   }
   public static void main(String[] args) {
    List<Circle> circles =new ArrayList<Circle>();
    Canvas c =new Canvas();
    for(Circle circle:circles){
        c.drawAll(circles);    // List<Circle>不能当List<Shape>使用
    }
}
}

对比代码之间的差异,可体会出List<? extends Shape>的优势

 public void addRectangle(List<? extends Shape> shapes) {
     shapes.addAll(new Rectangle());   //? extends Shape 是未知类型,所以不能往集合里添加Shape或 Shape的子类
     
}

Java泛型不仅允许在使用通配符形参时使用设定类型上限,还可以在定义 类型形参时设定上限。用来表示传给该类的形参的实际类型必须是这个类型或这个类型的子类。

 

前面说的是 定义类和接口是可以使用类型形参。在该类的该类的方法定义,属性定义和接口的方法定义中,这些类型形参可以被当做普通类型使用。

现在说,另一种情况,定义类和接口时,不用类型形参,在定义方法时使用类型形参。这就是泛型方法。

 定义泛型方法:

如果要实现一个这样的功能把一个Object[]数组放入Collection集合中。

public class Tests {
   public static void fromArrayToCollection(Object[] o,Collection<Object> c) {  //最好不用Collection<Object>,不然只能Object[]复制到Collection<Object>,实现功能有限
    for(Object a:o){
        c.add(a);
    }
}
   public static void main(String[] args) {
    String[] a ={"AA","BB"};
    List<String> l =new ArrayList<>();
/*    fromArrayToCollection(a, l); *///出错,Collection<String>不能当Collection<Object>用
    
}
}

那形参部分能用Collection<?>代替Collection<Object>吗?不能。前面提到了,不能把对象往未知的集合类型里装。泛型方法就能解决这个问题。在声明方法时定义一个或多个类型形参。格式如下:

修饰符  <T,S> 返回值类型 方法名(形参列表){

....

}

与普通方法对比,多了类型形参声明,在返回值类型和修饰符之间。

public class Tests {
   public  static <T>  void fromArrayToCollection(T[] o,Collection<T> c) {   
    for(T a:o){
        c.add(a);
    }
}
   public static void main(String[] args) {
    String[] a ={"AA","BB"};
    List<String> l =new ArrayList<>();
     fromArrayToCollection(a, l);  // 正确
    
}
}

泛型方法里有个T形参,这个T形参可以当做普通方法使用。与接口,类声明使用类型形参不同,它能在整个类和接口里使用。

同时另一个不同就是,调用时不需要显示传入实际参数的类型。例如:

fromArrayToCollection(cs,sa) sa是Collection<String> 类型  cs是Collection<Number>类型。编辑器可以自己推断出T的实际类型。但是不要制造迷惑,制造迷惑就是你错了。
public class Tests {
   public  static <T>  void fromArrayToCollection(Collection<T>  o,Collection<T> c) {   
    for(T a:o){
        c.add(a);
    }
}
   public static void main(String[] args) {
    String[] a ={"AA","BB"};
    List<String> l =new ArrayList<>();
    List<Object> a =new ArrayList<>();
     fromArrayToCollection(a, l);  // 错误,形参类型和实参相同,不能准确推断出实参是谁
    
}
}

 <? super T> 表示泛型的下限

泛型擦除和转换:

把一个带泛型的赋值给不带泛型的数据,泛型信息会被擦除。

public static void main(String[] args) {
List<Integer> a =new ArrayList<Integer>();
a.add(0);
List b = a; //擦除
List<String> c =b; //编译通过
System.out.println(c.get(0)); //但是运行错误
}
泛型和数组(不支持泛型数组)
泛型有个设计原则,如果没有未经检查的警告,则不会有运行时的转换异常。基于这个原因,数据元素不能包含类型变量和类型形参,除非是无上限的类型通配符(?)例子:
List<?>[] a =new ArrayList<?>[10];的写法是没错的。
,但是可以声明这样的数组元素。比如可以声明List<String>[],但是不能创建
new ArrayList<String>[]
下面示范一个错误的例子
    public static void main(String[] args) {
         @SuppressWarnings("unchecked")
        List<String>[] a =new ArrayList[10];   
         Object[] b =a;
         List<Integer> l1 =new ArrayList<>();  
         l1.add(new Integer(9));
         b[0] =l1.get(0);
         String s =(String) a[0].get(0);
         
    }
改过来:
public static void main(String[] args) {
         @SuppressWarnings("unchecked")
         List<String>[] a =new ArrayList[10];   
         Object[] b =a;  //b数组的元素是List<String>
         List<Integer> l1 =new ArrayList<>();  
         l1.add(new Integer(9));
         b[0] =l1;  //把集合赋给元素
         Object d=  (Object) a[0].get(0);
         if(d instanceof String){   //转换前类型检查
         String s =(String) d;
         }
    }
创建元素类型的是类型变量的数组对象也会报错
public <T> T[] setinfo(Collection<T>  info) {
        return new T[info.size()];   //编译错误
    }
因为类型变量实际上是不存在的,所以编译器无法确定运行时类型是是什么

猜你喜欢

转载自www.cnblogs.com/yizhizhangBlog/p/9293801.html