泛型

泛型

一、为什么要存在泛型

泛型特性对Java影响最大的是集合框架的使用,因为Java5增加泛型的支持在很大程度上都是为了集合能够记住其元素的数据类型。

public class demo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("java基础");
        list.add(666);
        list.forEach(a -> System.out.println(((String)a).length()));
    }
}

上面程序先是创建了一个List集合,但是我们一开觉得我们只会存放字符串型的数据对象,突然你加入了一个整型数据,我们在这个地方加入的对象做限制,所以我们可以在add的时候把整型数据加进去,但是我们如果在使用的时候用到了字符串类型的方法,它就会出现强制类型转化错误。

java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

增加了泛型支持的集合,完全可以记住集合中元素的类型,并且可以在编译时检查集合中元素类型是否符合要求,如果我试图想集合中添加不符合我规定的数据类型的时候,编译器就会提示错误。增加泛型后的集合,可以让代码更加简洁,程序更加健壮,我们如果在编译时没有发出警告,运行时候就不会产生ClassCastException异常。

二、使用

从Java5之后,Java引入了参数化类型的概念,就是允许程序在创建集合时指定集合中元素的类型,例如:List<Stirng>,这个意思就是这个List集合中只能存储字符型对象。

在java中参数化的类型被称为泛型(Generic)

public class demo {
    public static void main(String[] args) {
        // 添加泛型,只能保存字符串
        List<String> list = new ArrayList<String>();
        list.add("java基础");
        list.add("4444");
        list.forEach(a -> System.out.println(((String)a).length()));
    }
}

我们在上面创建了一个特殊的List集合,这个集合只能保存字符串,不能保存其他类型。

创建这个特殊集合的方法:在集合接口、类后增加尖括号<>,尖括号里面放一个数据类型,就相当于表明了这个集合接口、集合类只能保存特定的对象。

三、Java7之后增强写法 --- 菱形语法

在Java7之前,如果使用带泛型的接口、类定义变量的时候,那么构造器创建对象的时候后面也必须带上泛型,程序会显得冗余。例如:

List<String> list = new ArrayList<String>();

上面这个我们就可以改变一下,去除尖括号里面的内容。Java7以前的时候尖括号里面的泛型是必须带着的,Java7之后是可以不带着,省略里面的泛型,只留下尖括号。java可以推断出尖括号里面的泛型信息。由于尖括号像菱形,所以我们把这个称作为菱形语法

List<String> list = new ArrayList<>();

从上面对比我们可以看的出来,菱形语法没有对原来的泛型格式做出改变,只是简化了泛型变成时候的代码。

public interface Foo<T> {
    void test(T t);
}
public class AnonymousTest {

    public static void main(String[] args) {
        Demo demo = new Demo();
        Foo<String> f = new Foo<>() {
            @Override
            public void test(String s) {

            }
        };
    }
}

java9再次增强了“菱形”语法,允许创建匿名内部类的时候使用菱形语法,java可以根据上下文推断出匿名内部类中的泛型类型。

四、再次了解泛型

我们查看List集合的源码,他们在里面使用了泛型,

public interface List<E> extends Collection<E> {
    ......
        Iterator<E> iterator();
    ......
        
}

1、定义泛型接口、类

所谓的泛型,就是允许在定义类、接口、方法的时候使用类型形参(也就是泛型),这个类型形参将在声明变量、创建对象、调用方法的时候可以动态的指定(也就是传入实际的参数),Java5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明变量、创建对象的时候传入类型实参,这个类似于List<String>

我们可以想象一下,接口里面的所有E被替换成了String,就类似于产生了一个新类型:List<String>.

public interface List<String> extends List{
    ...
        Iterator<String> iterator();
    ...
        default void sort(Comparator<? super String> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<String> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((String) e);
        }
    }
    ...
}

通过这种方式,虽然我们只定义了一个List<E>接口,但在实际使用时会产生若干个List接口,我们只需要为E传入不同的类型实参就行了,系统就会多出一个List子接口。

需要注意的是LIst<Stirng>不会被替换掉,系统没有进行源代码复制,二进制代码中也没有,磁盘中也没有,内存中也没有

public class Apple<T> {

    // 使用T类型定义实例变量
    private 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> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());

        Apple<Double> a2 = new Apple<>(6.66);
        System.out.println(a2.getInfo());
    }
}

上面程序定义了一个带泛型声明的Apple<T>类(不用理会这个泛型形参是否具有实际的意义),使用Apple<T>类时就可以为T类型传入实际类型,这样就可以生成Apple<String>Apple<Double>.....形式的多个逻辑子类(物理上是不存在的)。

注意:当创建带泛型声明的自定义类时,为该类定义构造器的时候,构造器还是原来的类名,不要增加泛型声明。例如:

Apple<T>类的构造器,构造名还是Apple,而不是Apple<T>,调用时却可以使用Apple<t>的形式,当然应该为T传入对应的实参,顺便我们使用了菱形语法

2、从定义的泛型类中派生出子类

当我们创建带有泛型声明的接口、父类后,实现接口的子类、继承父类的子类,不应该再包含泛型形参,应该指定需要的实参。

下面的代码就是错误的:

public class A1 extends Apple<T> {
.......
}

正确写法:(为T指定对应的实参,比如String)

public class A2 extends Apple<String> {
    public A2(String info) {
        super(info);
    }
}

子类也可以不带泛型声明的实参:

public class A1 extends Apple {
    public A1(Object info) {
        super(info);
    }
}

像上面这种使用Apple类时省略泛型的形式被称为原始类型。

public class A2 extends Apple<String> {
    public A2(String info) {
        super(info);
    }
    
    @Override
    public String getInfo() {
        return "子类: " + super.getInfo();
    }
}

如果子类重写父类的方法,那么在所有T类型的地方都会被替换成String类型,如果不替换,编译不会通过你的代码。

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

l1l2通过new ArrayList<>()产生,我们直观的看,通过getClass()方法来对比,一开始我们会认为输出false,但是实际上输出的是true,因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。对于Java而言,它们依然被当成同一个类来处理,在内存中也只占用一块内存空间。所以我们在静态的方法、静态初始块、静态变量中不允许使用泛型形参。

以下是错误实例:

public class TestGeneric {
    
    //static T info;
    
    //public static void test(T t){}
}

五、类型通配符

当使用一个泛型类时(包括声明变量和创建对象),都应该为这个泛型类传入一个类型实参。

假设我们需要定义一个方法,这个方法里面有集合形参,如果集合形参的元素类型不确定,我们该怎么定义???

public void test (List c) {
    for (int i=0; i<c.size(); i++) {
        System.out.println(c.get(i));
	}
}

上面的程序这么写是没有问题的,这是最简单的List集合的遍历。由于List是一个有泛型声明的接口,我们在这个地方却没有为它传入实际的类型参数,这会引发泛型警告。所以我们考虑传入一个实参----Object

public void test (List<Object> c) {
    for (int i=0; i<c.size(); i++) {
        System.out.println(c.get(i));
	}
}

表面上看起来这个方法是没有问题的,但是在调用的时候就会出现test (java.util.List-sjiava.lang.object>) in Test cannot be appliedto (java.util.List<java.lang.String>也就是无法将String应用到Object。这是由于实际传入的类型参数不是我们所期望的。

public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        test(stringList);
}

上面程序显而易见出现编译错误,这表明List<String>对象不能被当成List<object>来处理,也就是说List<String>不是List<object>的子类。

与数组做对比,我们先定义了一个Integer类型的数组,然后我们整数型数组赋给了Number数组,但是我们在赋值操作时,给了一个Double类型数字,编译器没有报错,但是你在运行时就会抛出Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double,数组存储异常,这就是一种潜在的风险。

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
//        test(stringList);

        Integer[] ia = new Integer[5];
        Number[] na = ia;
        // Storing element of type "java.lang.Double' 
        // to array of java.lang.Integer' elements may produce 'ArrayStoreException
        na[0] = 0.5;
    }

一门优秀的设计语言,除了功能强大,还能提供强大的“错误提示”和“出错警告”,我们才能避免开发者犯错,但是java允许Integer[]赋值给Number[]显然不是一个安全的设计。

所以在泛型设计的时候做出了改进,不允许List<Integer>赋值给List<Number>变量.。

 public static void main(String[] args) {
		// 此处会引发编译错误
        List<Integer> la = new ArrayList<>();
        List<Number> lm = la;
    }

java泛型设计的原则:只要代码在编译时不出现警告,就不会遇到运行时ClassCastExeption异常,也就是类型转换异常。

1、使用类型通配符

类型通配符:?,可以匹配任何类型。

为了表示各种泛型的List的父类,我们可以使用这个?通配符,将问号作为一个类型实参传给List集合,形式:List<?>

那么上面那个List<Object>的那个方法就可以这么改:

    public static void test (List<?> c) {
        for (int i=0; i<c.size(); i++) {
            System.out.println(c.get(i));
        }
    }

现在你是用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,安全,因为不管List的真实类型是什么,它包含都是Object

List<?> c = new ArrayList<>();
// 下面这行代码会引起编译错误,null除外
c.add(new Object());

上面的程序无法确定c集合的元素类型,所以无法像其中添加对象,根据List<E>接口定义处的代码可以发现:add()方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但是在上面的例子中我们不知道E是什么,所以程序无法将任何对象“丢进”这个集合。唯一的例外是null,因为它是所有引用类型的实例。

2、设定类型通配符的上限

当我们使用List<?>这种形式的时候,表明这个List集合是任何泛型List的父类,但是还有一种特殊的情况:设计的程序不希望这个List<?>是任何List的父类,只希望它代表某一大类的List的父类。

先写三个形状类:

public abstract  class Shape {
    public abstract void draw(Canvas canvas);
}

public class Rectangle extends Shape {
    @Override
    public void draw(Canvas canvas) {
        System.out.println("矩形:" + canvas + "上");
    }
}

public class Circle extends Shape {
    @Override
    public void draw(Canvas canvas) {
        System.out.println("圆形: " + canvas + "上");
    }
}

然后在写一个画布类及方法:

public class Canvas {

    public void drawAll (List<Shape> shapes) {
        for (Shape s: shapes) {
            s.draw(this);
        }
    }

    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        List<Circle> circles = new ArrayList<>();
        circles.add(new Circle());
        Canvas c = new Canvas();
		// 此处会引发编译错误,这是由于 List<Circle>不是List<Shape>的子类
        c.drawAll(circles);
    }

}

那么我们可以这么改,设定一个通配符上限:

public void drawAll (List<? extends Shape> shapes) {
        for (Shape s: shapes) {
            s.draw(this);
        }
}

List<? extends Shape>是受限制通配符的例子,此处的问号是代表未知类型,就像前面看的通配符一样,但是此处的未知类型要是Shape的子类型,那么称Shape是通配符的上限。

3、设定类型通配符的下限

<? super 类型>

目的:赋值元素到目的集合中,我们必须保证目的集合比源集合的大才行。

public class Utils {

    public static void main(String[] args) {
        List<Number> ln = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        li.add(666);
        li.add(888);
        // li  ---> ln   copy(ln , li)
        Integer i = copy(ln, li);
        System.out.println(ln);
    }

    public static <T> T copy(List<? super T> dest, List<T> src){
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }
}

4、设置泛型形参的上限

public class Apple<T extends Number> {
    ......
}

六、泛型方法

1、定义泛型方法

在有些特定的情况下,我们在定义类、接口的时候没有使用泛型形参,但是在定义方法的时候想自己定义泛型形参,也是可以的。

需求: 将一个Object数组的所有元素添加到一个Collection集合当中。

static void fromArrayToCollection(Object[] a, Collection<Object> c){
    for (Object o : a) {
        c.add(a);
    }
}

上面方法局限性强,如果传入Stirng类型的集合就不能通过编译。例如:

public static void main(String[] args) {
        Object object = new Object();
        Object[] arr = new Object[4];

        String[] str = {"a", "b"};
        Collection<String> c = new ArrayList<>();
    // 出错
        fromArrayToCollection(str,c);
    }

泛型方法格式:

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

那么我们就可以这么改:

static <T> void fromArrayToCollection(T[] a, Collection<T> c){
        for (T cc : a) {
            c.add(cc);
        }
}

2、泛型方法和类型通配符的区别

在大多数情况下,可以使用类型方法代替类型通配符。

3、菱形语法失效

public class Test {

    public static void main(String[] args) {
        MyClass<String> mc1 = new MyClass<>("rrrr");
        MyClass<String> mc2 = new <String> MyClass<String>("fff");
    }
}

由于你显式的指定了构造器的泛型形参,所以菱形语法失效。

posted @ 2021-01-20 13:01  初之  阅读(43)  评论(0)    收藏  举报