泛型

泛型

目录

1. 泛型入门

public class GenerList {
    public static void main(String[] args) {
        // 创建一个只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();   //(1)
        strList.add("孙悟空");
        strList.add("猪八戒");
        // 下面的代码将会引起编译出错
            strList.add(5);  //(2)
        strList.forEach(str -> System.out.println(str.length()));//(3)
    }
}

(1)出,设定这个List集合中只能有String类型的变量
(2)出,因为经(1)出的声明,这个集合不能有其他类型的变量,如果有将报错。
(3)出,经过(1)出的声明之后这个集合就能记住里面存储元素的类型,不需要强制类型转化。

2. Java7 泛型的“菱形”语法

在Java7之前再调用构造器创建对象时构造器后面也必须带泛型,但在Java7之后只需要写一对尖括号即可。
下面给出的是一个示例:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DiamondTest {
    public static void main(String[] args) {
        // Java自动推断出ArrayList的<>里应该是String
        List<String> books = new ArrayList<>();
        books.add("龙族");
        books.add("临界爵迹");
        // 遍历books集合,集合元素就是String类型
        books.forEach(ele -> {
           System.out.println(ele.length());
        });
        // Java自动推断出HashMap的<>里应该是String, List<String>
        Map<String, List<String>> schoolsInfo = new HashMap<>();
        // Java自动推断出ArrayList的<>里是String
        List<String> schools = new ArrayList<>();
        schools.add("斜月三星洞");
        schools.add("西天取经路");
        schoolsInfo.put("孙悟空", schools);
        schoolsInfo.forEach((key, value) -> System.out.println(key + "----" + value));
    }
}

3. 深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型参数,这个类型参数将在声明变量、创建对象、调用方法时动态的指定。
这种方式不只是可以在集合中可以用,下面的是一个普通的类使用泛型的示例:

public class Apple<T> {
    // 使用T类型形参定义实例变量
    private T info;
    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,所以构造器形参只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getinfo());
        // 由于传给形参的是Double,所以构造器的形参只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getinfo());
    }
}

注意:在定义构造器是可以使用该类型形参,不用写成Apple<T>(),像之前那样定义即可。

4. 从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类创建派生子类,需要指出的是,当使用这些接口、父类是不能再包含类型形参。
如果想从Apple派生一个子类,可以这样:

// 使用Apple类型时为T类型参数传入String类型
public class A extends Apple<String> {}

当然,也可能不传入类型参数。
如果从Apple<String>类的派生子类,则在Apple类中所有的T类型的参数将被替换成String类型,如果子类要重写父类的方法就要注意到这一点。

public class A1 extends Apple<String>{
    // 正确重写父类的方法,返回值
    // 与父类Apple<String>的返回值完全相同
    public String getInfo() {
        return "子类" + super.getInfo();
    }
    // 下面的方法时错误的,重写父类方法时返回值类型不一致
//    public Object getInfo() {
//        return "子类";
//    } 
}

如果使用的Apple类是没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查警告。

5. 并不存在泛型类

List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
// 调用getClass()方法比较l1和l2的类是否相等
System.out.println(l1.getClass() == l2.getClass());

上面的代码输出true说明并不存在泛型类,因为不管泛型的实际类型是什么,他们总在运行时有相同的类(Class)。
因为不管泛型的类型形参传入的是什么类型的实参,对于Java来说,他们依然被当成同一个类处理,在内存中也只占用同一块内存,因此在静态方法、静态初始化块或静态变量的声明中不允许使用类型形参

因为没有泛型类,所以instanceof运算符后不能使用泛型类。例如:

Apple<String> apple = new Apple<>();
if (apple instanceof Apple<String>) {}

6. 类型通配符

当使用一个泛型类时(包括声明和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入就会提出泛型警告。但是如果有一个方法需要传入一个集合形参,集合形参中的元素类型是不能确定的。就像下面这样:

 public void test (List list)  {
     Iterator it = list.iterator();
     while (it.hasNext()) {
         System.out.println(it.next());
     }
 }

此处使用的List没有传入实际的类型参数,这将引起泛型警告。但是又该如何传入呢?考虑下面代码:

public void test(List<Object> list) {
    Iterator it = list.iterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
}

上面的程序看起来没有任何的问题。
如下面的代码调用:

List<String> str = new ArrayList<>();
test(str);

上面的代码将会在编译时出错。

java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<java.lang.Object>

上面的程序出现了错误,说明List<String>不能被当成List<Object>使用,换句话说,List<String>类并不是List<Object>类的子类。

6.1 使用类型通配符

为了表示各种泛型List的父类,可以使用通配符,类型通配符是一个”?”,将List

package 泛型;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test1 {
    public void test (List<?> list)  {
        Iterator it = list.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        List<String> str = new ArrayList<>();
        str.add("孙悟空");
        test1.test(str);

    }
}

上述代码是完整的一个测试代码。
通配符适合于任意的含有泛型类和泛型接口。
但这种带有通配符的List仅仅表示它是各种泛型Lsit的父类,并不能把元素加入其中。例如,如下代码将会引起编译错误。

List<?> c = new ArrayList<>();
c.add(new Object());

List

6.2 设定类型通配符上限

考虑一个问题对于List

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

该抽象类有一个draw的方法,下面有两个它的子类Circle和Rectangle。

public class Circle extends Shape{
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "画圆");
    }
}
public class Rectangle extends Shape{
    public void draw(Canvas c) {
        System.out.println("把一个矩形画在" + c + "上");
    }
}

关于这个Canvas方法可以这样写:

public class Canvas {
    // 同时在画布上画多个图形
    public void drawAll(List<?> shapes) {
        for (Object obj : shapes) {
            Shape s = (Shape)obj;
            s.draw();
        }
    }
}

这样写时需要进行强制类型转化,所以显得有些累赘。
在通配符中也可以使用extends这个关键字。
可以将上面的程序改写成下面的这个样子。

public class Canvas {
    public void drawAll(List<? extends Shape> shapes) {
        for (Shape s : shapes) {
            s.draw(this);
        }
    }
    public String toString() {
        return "A";
    }
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        shapes.add(new Circle());
        shapes.add(new Rectangle());
        shapes.add(new Circle());
        Canvas canvas = new Canvas();
        canvas.drawAll(shapes);
    }
}

List

6.3 设定类型参数的上限

Java不仅可以在使用通配符设定上限,而且可以在定义类型参数时设定上限,用于表示传给该类型形参的实际类型要么是该上限,要么是其子类。下面的程序演示了这种用法:

public class Banana<T extends Number> {
    T age;
    public static void main(String[] args) {
        Banana<Double> a1 = new Banana<>();
        Banana<Integer> a2 = new Banana<>();
        // 下面的代码将引起编译错误
        Banana<String> a3 = new Banana<>();
    }
}

类型参数可以设置多个上限,与类和接口的继承相似,最多可以设置一个父类上限,但可以设置多个接口上限。还有一点,就是和类与接口的继承一样,父类的上限设置必须在接口的上限设置之前。

// 表明T类型必须是Number类或是其子类,并必须实现java.io.Serializable
public class Banana<T extends Number & java.io.Serializable> {

}

7. 泛型方法

考虑如下代码:

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

有前面的内容可知,这明显是错误的。
这个函数想要实现的功能是讲一个数组放进集合中。使用通配符明显不可以,因为Java不允许把对象放进一个未知类型的集合中。
Java5提供了泛型方法,语法格式如下:

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

可以将上面的方法改造成如下:

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类型
        fromArrayToCollection(oa, co);
        String[] sa = new String[100];
        Collection<String> cs = new ArrayList<>();
        // 下面代码中的T代表String类型
        fromArrayToCollection(sa, cs);
        Integer[] ia = new Integer[100];
        Float[] fa = new Float[100];
        Double[] da = new Double[100];
        Number[] na = new Number[100];
        Collection<Number> ca = new ArrayList<>();
        // 下面代码中的T代表Number
        fromArrayToCollection(na, ca);
        // 下面代码中的T代表Number
        fromArrayToCollection(ia, ca);
        // 下面代码中的T代表Number
        fromArrayToCollection(fa, ca);
        // 下面代码中的T代表Number
        fromArrayToCollection(da, ca);
        // 下面代码中的T代表String类型,但na是一个Number类型
        // 所以下面的程序会编译错误
//        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(ao, as);
    }
}

上面的代码将导致系统无法确定其类型。
可以将上面的代码进行修改

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> as = new ArrayList<>();
        List<String> ao = new ArrayList<>();
        // 下面的代码将出现错误
        test(ao, as);
    }
}

上面的代码中test(ao, as);
通过这一行代码可以推断处T为Object,而test的第一个参数是Object 或其子类。

8. Java7中的“菱形”语法与泛型构造器

在构造器中使用泛型时,可以显示的指定,也可以让系统根据参数取推断。
如下:

class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}
public class GenericConstructor {
    public static void main(String[] args) {
        // 泛型构造器中的T参数为String
        new Foo("疯狂Java讲义");
        // 泛型构造器中的T参数为Integer
        new Foo(20);
        // 显示的指定泛型构造器中的T参数为String
        new <String>Foo ("疯狂Android讲义");
        // 显示的指定泛型构造器中T参数为String
        // 但传给Foo构造器的参数是Double对象,下面的代码将出错
//        new <String> Foo(12.3);
    }
}

对于Java7新增的”菱形“语法,如果程序显示的指定了泛型构造器的参数,则不可以使用”菱形“语法。

class MyClass<E> {
    public <T> MyClass(T t) {
        System.out.println("t的参数值为:" + t);
    }
}
public class GenericDiamondTest {
    public static void main(String[] args) {
        // MyClass 中的形参是String类型
        // 泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc1 = new MyClass<>(5);
        // 显示的指定了泛型构造器中的E形参是Integer类型
        MyClass<String> mc2 = new <Integer> MyClass<String>(5);
        // 使用了“菱形”语法,出现了错误
        MyClass<String> mc2 = new <Integer> MyClass<>(5);
    }
}

9. 设定通配符下限

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);
    }
}

10. 擦除和转化

在严格的代码里,带泛型声明的类总应该是带着参数类型。但为了与之前的Java代码保持一致,也允许在使用泛型声明的类时不指定参数类型。如果没有为这个类泛型类指定实际的参数类型,则把该类型参数称作raw type(原始类型),默认是声明该类型参数是指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个不具有泛型信息变量是,所有在尖括号之间的信息都将被扔掉。比如一个List<String>类型装换为List,则该List集合中的类型检查变成了类型参数的上限(即Object)。下面的程序示范了这种擦除。

class Pen<T extends Number> {
    T size;
    public Pen() {}
    public Pen(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) {
        Pen<Integer> a = new Pen<>(6);
        // a的getSize()方法返回Integer对象
        Integer as = a.getSize();
        // 把a对象赋给Pen变量,丢失尖括号里的信息
        Pen b = a;
        // b 只知道size的类型是Number
        Number size1 = b.getSize();
        Integer size2 = b.getSize();
    }
}

就像上面的b,不指定泛型参数类型,就默认是它的上限Number,将a赋值给b后a丢失了尖括号中的内容。
如果讲一个List对象赋给Lsit<String>对象不会引起编译错误,编译器会提示“未经检查的转换”。

class Pen<T extends Number> {
    T size;
    public Pen() {}
    public Pen(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) {
        Pen<Integer> a = new Pen<>(6);
        // a的getSize()方法返回Integer对象
        Integer as = a.getSize();
        // 把a对象赋给Pen变量,丢失尖括号里的信息
        Pen b = a;
        // b 只知道size的类型是Number
        Number size1 = b.getSize();
        Integer size2 = b.getSize();
    }
}

Java允许直接把List对象赋值给List<Type>类型的变量。但是会发出“未经检查的转化”警告。
上面的代码和下面的代码相似:

public class ErasureTest2 {
    public static void main(String[] args) {
        List li  = new ArrayList();
        li.add(2);
        li.add(3);
        System.out.println((String)li.get(0));
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值