泛型

来源:互联网 发布:域名和服务器的关系 编辑:程序博客网 时间:2024/05/17 20:34

1泛型入门

1为什么要设计泛型,有下面两个简单的理由。

集合对元素类型没有任何限制,这样可能引发一些问题,例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。

由于把对象"丢进"集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换,这种强制类型转换即增加了编程的复杂度,也可能引发ClassCastException异常。

2使用泛型可以在编译时期检查其容器的类型,防止一些不符合类型存储到容器中。

代码例子:

不使用泛型:

public class ListErr{public static void main(String[] args) {// 创建一个只想保存字符串的List集合List strList = new ArrayList();strList.add("beyondboy");strList.add("scau");strList.add("java");// "不小心"把一个Integer对象"丢进"了集合,这里不会引起编译错误strList.add(5);     // ①for (int i = 0; i < strList.size() ; i++ ){// 因为List里取出的全部是Object,所以必须强制类型转换// 最后一个元素将出现ClassCastException异常String str = (String)strList.get(i);   // ②}}}


使用泛型:

public class GenericList{public static void main(String[] args) {// 创建一个只想保存字符串的List集合List<String> strList = new ArrayList<String>();  // ①strList.add("beyondboy");strList.add("scau");strList.add("java");// 下面代码将引起编译错误strList.add(5);    // ②for (int i = 0; i < strList.size() ; i++ ){// 下面代码无须强制类型转换String str = strList.get(i);    // ③}}}
2深入泛型

1定义一个自定义的泛型类其格式是类名<T>或接口名<T>,注意是为类定义构造器时,构造器还是原来的类名,不要增加泛型声明(如 构造器名<T>这种写法的是错误的,应直接写构造器名。)

代码例子:

//定义Apple类时使用了泛型声明public class Apple<T>{// 使用T类型形参定义实例变量private T info;//这里会编译错误//public Apple<T>(){}//正确写法public Apple(){}// 下面方法中使用T类型形参来定义构造器public Apple(T info){this.info = info;}public void setInfo(T info){this.info = info;}public T getInfo(){return this.info;}public static void main(String[] args){// 因为传给T形参的是String实际类型,// 所以构造器的参数只能是StringApple<String> a1 = new Apple<>("苹果");System.out.println(a1.getInfo());// 因为传给T形参的是Double实际类型,// 所以构造器的参数只能是Double或者doubleApple<Double> a2 = new Apple<>(5.67);System.out.println(a2.getInfo());}}


2当创建了带泛型声明的接口,父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些接口,父类时不能再包含类型形参(除非实现类也是泛型类)。

错误写法:

 //定义类A继承上面的Apple类,Apple类不能跟类型形参

public class A extends Apple<T>{}

这样写是正确的:

public class A<T> extends Apple<T>{}

3当子类继承一个泛型的父类(子类不是泛型类时),一种是需要为父类传入实际类型的泛型参数,另一种是不为父类泛型参数传入实际类型(这样父类默认的泛型参数就是Object),不过编译器会产生使用了未经检查或不安全的操作的警告.

代码例子:

public class A1 extends Apple<String>{// 正确重写了父类的方法,返回值// 与父类Apple<String>的返回值完全相同public String getInfo(){return "子类" + super.getInfo();}/*// 下面会编译错误的,重写父类方法时返回值类型比父类还大public Object getInfo(){return "子类";}*/}public class A2 extends Apple{// 重写父类的方法public String getInfo(){// super.getInfo()方法返回值是Object类型,// 所以加toString()才返回String类型return super.getInfo().toString();}}
4当我们使用同一个泛型类来传入不同的类型形参,系统并不会产生不同的新类,而是把他们当做同一种类。

代码例子:

//分别创建List<String>对象和List<Integer>对象List<String> list1=new ArrayList<String>();List<Integer> list2=new ArrayList<Integer>();//调用getClass()方法来比较list1和list2的类是否相同,下面输出trueSystem.out.println(list1.getClass()==list2.getClass());Collection<String> collection=new ArrayList<String>();//会出现编译错误if(collection instanceof Collection<String>){}//编译通过if(collection instanceof Collection){}


5静态方法,静态初始化块或者静态变量的声明和初始化中不允许使用泛型类型形参(但在这些泛型方法中,是可以的,下面的例子将会介绍)。

代码例子:

public class R<T>{// 下面代码错误,不能在静态Field声明中使用类型形参//static T info;T age;public void foo(T msg){}// 下面代码错误,不能在静态方法声明中使用类型形参//public static void bar(T msg){}}
3类型通配符

1如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型 ,而对于数组泛型来说Foo[]依然是Bar[]的子类型(不过如果储存的元素不是Foo类型时,运行时会抛出异常)。

代码例子:

public class Test{static void test(List<Object> c){System.out.println("通过");}public static void main(String[] args){List<String> list1=new ArrayList<String>();//编译错误test(list1);Integer[] integers=new Integer[5];//编译通过Number[] numbers=integers;//运行时不会抛出ArrayStoreException异常numbers[0]=1;////运行时会抛出ArrayStoreException异常numbers[1]=0.5;}}
2为了表示各种泛型某类的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给容器集合,如泛型List的父类可以写作:List<?>,但是需注意的是,不能通过add()方法把元素加入到其中(因为add方法有类型参数E,故需要具体类型参数,而?没有传入具体类型,故也不知道E是什么类型,唯一的可以添加是null),调用get()方法将返回的是Object类型,因为任何类都是Object的子类,如果要得到具体类型,一般需要强传,

代码例子:

public class Test{static void test(List<?> c){System.out.println("通过");}public static void main(String[] args){List<String> list1=new ArrayList<String>();//编译通过test(list1);List<?> list2=new ArrayList<String>();//编译不能通过list2.add("beyondboy");//编译通过list2.add(null);//编译通过Object object=list2.get(0);//编译不能通过String object1=list2.get(0);}}
3当需要表示某一类泛型的父类时,通常可以用该类的泛型定义成<? extends 父类>,如:Shape是一个抽象父类,该抽象父类有两个子类,Circle和Rectangle,那么List<? extends Shape>(Shape为上限,即?只接受Shape类或其子类)就可以表示List<Circle>,List<Rectangle>的父类,不过List <? extends Shape>还是不能通过add添加元素(因为? extends Shape的?不具体),从容器取出的元素的类型是Shape。

代码例子:

// 定义一个抽象类Shapepublic abstract class Shape{ public abstract void draw(Canvas c); }// 定义Shape的子类Circlepublic class Circle extends Shape{// 实现画图方法,以打印字符串来模拟画图方法实现public void draw(Canvas c){System.out.println("在画布" + c + "上画一个圆");}}// 定义Shape的子类Rectanglepublic class Rectangle extends Shape {// 实现画图方法,以打印字符串来模拟画图方法实现public void draw(Canvas c) { System.out.println("把一个矩形画在画布" + c + "上");} }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> circleList = new ArrayList<Circle>();List<Rectangle> rectangles=new ArrayList<Rectangle>();List<? extends Shape> shapes=new ArrayList<Shape>();circleList.add(new Circle());rectangles.add(new Rectangle());Canvas c = new Canvas();//编译通过c.drawAll(circleList);//编译通过c.drawAll(rectangles);//编译不通过shapes.add(new Circle());}}
4当程序需要为类型形参设定多个上限(之多有一个父类上限,可以有多个个接口上限),表明该类型形参必须是其父类的子类或其本身,并且实现多个上限接口,语法例子如:public class Apple<T extends Number&java.io.Serializable>.

代码例子:

public class Apple<T extends Number>{T col;public static void main(String[] args){Apple<Integer> ai = new Apple<>();Apple<Double> ad = new Apple<>();// 下面代码将引起编译异常,下面代码试图把String类型传给T形参// 但String不是Number的子类型,所以引发编译错误Apple<String> as = new Apple<>();//①}}

4泛型方法

1泛型方法是在声明方法时定义一个或多个类型形参,语法:修饰符  <T,S>返回值类型 方法名 (形参列表){方法体...},方法中的类型形参与接口,类声明定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口,类声明中定义的类型形参则可以在整个接口,类中使用,调用该方法前无须传入String,Object等具体类型,编译器会根据实参推断类型形参的值,通常推断出最直接的类型参数。

代码例子:

public class GenericMethodTest{// 声明一个泛型方法,该泛型方法中带一个T类型形参,static <T> void fromArrayToCollection(T[] a, Collection<T> c){for (T o : a){c.add(o);}}public static void main(String[] args) {Object[] oa = new Object[100];Collection<Object> co = new ArrayList<>();// 下面代码中T代表Object类型<Object> fromArrayToCollection(oa, co);String[] sa = new String[100];Collection<String> cs = new ArrayList<>();// 下面代码中T代表String类型fromArrayToCollection(sa, cs);// 下面代码中T代表Object类型fromArrayToCollection(sa, co);Integer[] ia = new Integer[100];Float[] fa = new Float[100];Number[] na = new Number[100];Collection<Number> cn = new ArrayList<>(); // 下面代码中T代表Number类型fromArrayToCollection(ia, cn);// 下面代码中T代表Number类型fromArrayToCollection(fa, cn); // 下面代码中T代表Number类型fromArrayToCollection(na, cn);// 下面代码中T代表Object类型fromArrayToCollection(na, co);// 下面代码中T代表String类型,但na是一个Number数组,// 因为Number既不是String类型,// 也不是它的子类,所以出现编译错误//fromArrayToCollection(na, cs);}}
会让编译器混淆的一种写法。

代码例子:

public class ErrorTest{// 声明一个泛型方法,该泛型方法中带一个T类型形参static <T> void test(Collection<T> from, Collection<T> to){for (T ele : from){to.add(ele);}}public static void main(String[] args) {List<Object> as = new ArrayList<>();List<String> ao = new ArrayList<>();// 下面代码将产生编译错误test(as , ao);}}
修改其为正确写法:

public class RightTest{// 声明一个泛型方法,该泛型方法中带一个T形参static <T> void test(Collection<? extends T> from , Collection<T> to){for (T ele : from){to.add(ele);}}public static void main(String[] args) {List<Object> ao = new ArrayList<>();List<String> as = new ArrayList<>();// 下面代码完全正常test(as , ao);}}
2在泛型方法和类型通配符之间如何选择呢?因为这两者之间通常可以替代,如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符,因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a) 的类型。

代码例子:

       //使用类型通配符public interface Collection<E>{boolean containsAll(Collection<?> c);boolean addAll(Collection<? extends E> c);}
可以被替换成:

//使用泛型方法public interface Collection<E>{boolean <T> containsAll(Collection<T> c);boolean <T extends E> addAll(Collection<T> c);}
对比一下两种代码:

 //第一种src通配符

public class Collections
{

           public static <T> void copy(List<T> dest,List<? extends T>src){....}

}

//第二种src泛型方法

public class Collections
{

           public static <T,S extends T> void copy(List<T> dest,List<S>src){....}

}

上面copy方法的dest和src存在明显的依赖关系,丛源List中复制出来的元素,必须可以'丢进"目标List中,所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身,JDK定义是src形参类型时使用的是类型通配符,而不是泛型方法,是由于该方法无须向src集合中添加元素,也无须修改stc集合的元素,所以可以使用类型通配符。第二种上面的类型形参S,它仅使用了一次,没有其他参数的类型,方法返回值的类型依赖它,那类型形参S就没有存在的必要,因此用通配符来代替的话,会让程序客户端人员更了解其用法。

3Java也允许定义泛型构造器,其语法:修饰符 <T> 构造器名(T t){},它也可以根据实参的类型来推断出其类型,不过注意的是,允许调用构造器时在构造器后使用一对尖括号来代表泛型信息,但如果程序显示指定了泛型构造器中声明的类型形参的实际类型,则不可以使用"菱形"语法。

代码例子:

public class Test{public <T> Test(T t){System.out.println(t);}public static void main(String[] args){//根据实参推断出T为Stringnew Test("beyondboy");//编译通过new <Integer> Test(5);//编译不通过,因为T是String类型,而这里传的是Integer类型new <String> Test(7);}}public class Test<E>{    public <T> Test(T t)    {        System.out.println(t);    }    public static void main(String[] args)    {        Test<String> test1=new Test<>(5);        //尽管T的形参已经显示指定类型是Integer,        //但后面的尖括号也指定了E为String类型,故编译通过        Test<String> test2=new <Integer> Test<String>(5);        //T的形参已经显示指定类型是Integer,因此这里就不能使用菱形语法        //故这里会编译错误        Test<String> test3=new <Integer> Test<>(5);    }} 
4设定通配符下限,其语法:容器名字或类名<? super T>(?的类型只能是T的父类或其本身),我们看看一个例子,通配符下限是有何用之处?

代码例子:

第一种不用通配符下限:

public class MyUtils{// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类public static <T> T copy(Collection<T> dest , Collection<? extends T> src){T last = null;for (T ele  : src){last = ele;dest.add(ele);}return last;}public static void main(String[] args) {List<Number> ln = new ArrayList<>();List<Integer> li = new ArrayList<>();li.add(5);//copy根据形参推断出T的类型是Number类型,而不是Integer类型                //故下面会编译错误                Integer last = copy(ln , li);   System.out.println(ln);}}
第二种使用通配符下限:

public class MyUtils{// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类public static <T> T copy(Collection<? super T> dest , Collection<T> src){T last = null;for (T ele  : src){last = ele;dest.add(ele);}return last;}public static void main(String[] args) {List<Number> ln = new ArrayList<>();List<Integer> li = new ArrayList<>();li.add(5);// 此处可准确的知道最后一个被复制的元素是Integer类型// 与src集合元素的类型相同Integer last = copy(ln , li);    // ①System.out.println(ln);}}
Java集合框架中的TreeSet<E>中有一个构造器也用到了这种设定通配符下限的语法:

TreeSet(ComparaTor<? super E> c)

代码例子:

public class TreeSetTest{public static void main(String[] args) {// Comparator的实际类型是TreeSet里实际类型的父类,满足要求TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>(){public int compare(Object fst, Object snd){return hashCode() > snd.hashCode() ? 1: hashCode() < snd.hashCode() ? -1 : 0;}});ts1.add("hello");ts1.add("wa");TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>(){public int compare(String first, String second){return first.length() > second.length() ? -1: first.length() < second.length() ? 1 : 0;}});ts2.add("hello");ts2.add("wa");System.out.println(ts1);System.out.println(ts2);}}
5因为泛型方法可以重载,但其重载规则的泛型参数跟常规参数有点不同。
代码例子:

第一种情况public static <T> void copy(Collection<T> dest,Collection<? extends T> src){}//会发生编译错误,无法重载public static <T> void copy(Collection<? super T> dest,Collection<T> src){}第二种情况<pre name="code" class="java">public static <T> void copy(Collection<T> dest,Collection<? extends T> src){}//会发生编译错误,无法重载public static <T> void copy(Collection<? extends T> dest,Collection<T> src){}

5檫除和转换

1檫除和转换:为了与老的Java代码保持一致,Java允许在使用带泛型声明的类时不指定实际的类型参数,如果没有为这个泛型类指定实际的类型参数,则该类型被称作原始类型(raw type),默认是声明该参数时指定的第一个上限类型。如:当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都被扔掉,比如一个List<String>类型被转换成List,则该List对集合元素的类型检查编程了类型变量的上限(即Object).

代码例子:

class Apple<T extends Number>{T size;public Apple(){}public Apple(T size){this.size = size;}public void setSize(T size){this.size = size;}public T getSize(){return this.size;}}public class ErasureTest{public static void main(String[] args){Apple<Integer> a = new Apple<>(6);    //①// a的getSize方法返回Integer对象Integer as = a.getSize();// 把a对象赋给Apple变量,丢失尖括号里的类型Integer信息转换成了Number类型,这就是典型的檫除和转换Apple b = a;      //②// b只知道size的类型是NumberNumber size1 = b.getSize();// 下面代码引起编译错误Integer size2 = b.getSize();  //③}}
2如果直接把一个不带类型形参或其带上限类型形参的容器赋给一个带具体类型或者子类型的容器,编译器不会引起编译错误,不过会产生"未经检查的转换"的警告,但在访问时,一运行容易抛出异常。

代码例子:

public class ErasureTest2{public static void main(String[] args) {List<Integer> li = new ArrayList<>();li.add(6);li.add(9);List list = li;// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常List<String> ls = list;     // ①                //由于list实际上引用的是List<Ingeter>集合,存储的是Ingeter类型的元素                //而ls的具体类型是String,存储是String类型的元素                //故只要访问ls里的元素,如下面代码将引起运行时ClassCastException异常。System.out.println(ls.get(0));}}
6泛型与数组

1数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符,但可以声明元素类型包含类型变量或类型形参的数组。如:可以声明List<Strng>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象,创建元素类型是类型变量的数组对象也会出现编译错误。

代码例子:

public class Test{public static void main(String[] args){//编译错误,不能创建new ArrayList<String>[10]这样的数组对象//List<String>[] list=new ArrayList<String>[10];//编译通过,但会产生"未经检查的转换"警告//List<String>[] list=new ArrayList[10];//因使用了通配符,编译通过List<?>[] list=new ArrayList<?>[10];Object[] objects=(Object[]) list;List<Integer> list2=new ArrayList<Integer>();list2.add(new Integer(2));objects[1]=list2;//运行时,会抛出ClassCastException异常String s=(String)list[1].get(0);}<T> T[] makeArray(Collection<T> collection){//编译错误,这里T类型根本就不存在,编译无法知道该实际类型是什么//故这里不能new T[];return new T[collection.size()];}}
篇博客参考资料:

Java疯狂讲义2





0 0
原创粉丝点击