Java 泛型入门

1 篇文章 0 订阅

参考资料

[1]. Java泛型深入理解,
http://blog.csdn.net/sunxianghuang/article/details/51982979
[2]. 疯狂Java讲义第三版,李刚
[3]. Java总结篇系列:Java泛型,
https://www.cnblogs.com/lwbqqyumidi/p/3837629.html

泛型入门

编译时不检查类型的异常

下面是编译时不检查类型所导致的异常。

List strList = new ArrayList();
strList.add("a");
strList.add("aa");
// 添加了一个Integer类型的元素
// 这将导致java.lang.ClassCastException异常
strList.add(3);
strList.forEach(str -> System.out.println(((String)str).length()));

使用泛型

使用泛型在编译阶段就可以发现错误

List<String> strList = new ArrayList<String>();
strList.add("a");
strList.add("aa");
// 此处会报错,因为类型与泛型规定的不同
strList.add(3);
// 此处无需进行强制类型转换,因为strList可以“记住”它的所有集合元素都是String类型。
strList.forEach(str -> System.out.println((str).length()));

Java 7泛型的“菱形”语法

在Java 7以前,如果使用带泛型的接口、类定义变量的使用方法

List<String> strList = new ArrayList<String>();
Map<String, Integer>scores = new HashMap<String, Integer>();

在Java 7以后的省略用法,因为可以从带泛型的接口、类定义变量推断类型,所以后面可以直接使用<>

List<String> strList = new ArrayList<>();
Map<String, Integer>scores = new HashMap<>();

使用示范

List<String> books = new ArrayList<>();
books.add("a");
books.add("aa");
books.add("aaa");
books.forEach(str-> System.out.println(str.length()));

Map<String, List<String>> schoolsInfo = new HashMap<>();
List<String> schools = new ArrayList<>();
schools.add("a");
schools.add("aa");
schools.add("aaa");
schoolsInfo.put("a",schools);
schoolsInfo.forEach((k,v) -> System.out.println(k + "--" +v));

深入泛型

定义泛型类

下面是Java 5改写后List接口的代码片段

// 定义接口时指定了一个类型形参,该形参名为E
// 只要是继承自Collection类即可
public interface List<E> extends Collection<E> {
    boolean add(E e);
    Iterator<E> iterator();
    ...
}

下面是Java 5改写后Iterator接口

// 定义接口时指定了一个类型形参,该形参名为E
public interface Iterator<E> {
    // 在该接口里E完全可以作为类型使用
    E next();
    boolean hasNext();
    ...
}

下面是Java 5改写后Map接口

// 定义该接口时指定了两个类型形参,其形参名为K、V
public interface Map<K,V> {
    // 在该接口里K、V完全可以作为类型使用
    Set<K> keySet();
    V put(K key, V value);
    ...
}

例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型:List<String>类型,可以把List<String> 想象成E被全部替换成String的特殊List子接口。

// List<String>等同于如下接口
public interface ListString extends List
{
    // 原来的E形参全部变成String类型实参
    void add(String x);
    Iterator<String> iterator();
    ...
}

下面是一个示范的Apple类

// 定义Apple类时使用了泛型声明
public class Apple<T>
{
    // 使用T类型形参定义实例变量
    private T info;
    public Apple(){}
    // 下面方法中使用T类型形参来定义构造器
    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)
    {
        // 由于传给T形参的是String,所以构造器参数只能是String
        Apple<String> a1 = new Apple<>("apple");
        System.out.println(a1.getInfo());
        // 由于传给T形参的是Double,所以构造器参数只能是Double
        Apple<Double> a2 = new Apple<>(3.1415);
        System.out.println(a2.getInfo());
    }
}

从泛型类派生子类

为上面的Apple类派生派生子类
错误用法示例:

// 定义类A继承Apple类,Apple类不能跟类型形参
public class A extends Apple<T>{}

可以不限制类型

public class A extends Apple{}

或者限制为某个类型

public class A extends Apple<String>{}

重写父类方法时,会继承父类的类型

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

并不存在泛型类

它们运行的类是一个类,不会为List生成新的class文件

List<String> a1 = new ArrayList<>();
List<Integer> a2 = new ArrayList<>();
// 输出为true
System.out.println(a1.getClass() == a2.getClass());

不管为泛型的类型形参传入哪一种类型形参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参,因为系统不会真正生成泛型类。

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

由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类,下面是错误示范:

Collection<String> cs = new ArrayList<>();
    // 并不存在ArrayList<String>类
    if (cs instanceof ArrayList<String>)
    {
        // ...
    }

类型通配符

当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出警告。
假设集合形参的元素类型不确定,当List c是一个有泛型声明的接口,此处使用List接口时没有传入实际类型参数,这将会引起警告。
考虑下面的代码:

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

因为List c集合里的元素类型是不确定的,将上面方法改为如下形式:

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

上面的泛型信息是固定的Object,所以当我们使用其他泛型信息进行调用会发生编译错误

// 这表明List<String>对象不能当成<Object对象使用>,也就是说List<String>类并不是List<Object>类的子类
List<String> strlist = new ArrayList<>();
test(strlist);

看一下数组在这方面存在的问题

// 定义一个Integer数组
Integer[] ia= new Integer[5];
// 可以把一个Integer[]数组赋给Number变量
Number[] na = ia;
// 下面代码编译正常,但运行时会引发ArrayStoreException异常
// 因为0.5并不是Integer
na[0] = 0.5;

可以发现数组可以通过编译,但在运行时会出现ArrayStoreException异常。
Java在此方面进行了改进,不允许跨对象赋值。

List<Integer> ilist = new ArrayList<>();
// 此处会报错
// 在泛型出现以后,Java不再允许把List<Integer>对象赋值给List<Number>变量
List<Number> nlist = ilist;

使用类型通配符

使用通配符(?),它的元素类型可以匹配任何类型,即传入的参数可以为任意类型:

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

这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中,下面的代码会引起错误:

List<?> c = new ArrayList<String>();
// 下面的程序会引起错误
c.add(new Object);

设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类,但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。

现在编写一个抽象的形状(Shape)类,然后再编写两个继承了它的实现子类圆(Circle)类和长方形(Rectangle)类,最后用一个画类
Shape抽象类,抽象了draw方法

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

Circle实现类,实现了draw类

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

Rectangle实现类,实现了draw类

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

画画类Canvas

public class Canvas {
    // 同时在画布上绘制多个形状
    public void drawAll(List<Shape> shapes)
    {
        for (Shape s : shapes)
        {
            s.draw(this);
        }
    }
}

调用

// 定义一个装Circle类的List
List<Circle> circleList = new ArrayList<>();
Canvas c = new Canvas();
// 不能把List<Circle>当成List<Shape>使用,所以会引起编译错误
c.drawAll(circleList);

现在修改Canvas类

public class Canvas {
    // 同时在画布上绘制多个形状
    public void drawAll(List<?> shapes)
    {
        for (Shape s : shapes)
        {
            // 因为使用了通配符来表示所有的类型,所以需要强制转换
            Shape s = (Shape)obj;
            s.draw(this);
        }
    }
}

如果使用被限制的泛型通配符,代码如下:

public class Canvas {
    // 同时在画布上绘制多个形状
    public void drawAll(List<? extends Shape> shapes)
    {
        for (Shape s : shapes)
        {
            // 因为使用了限制,所以不需要强制转换
            s.draw(this);
        }
    }
}
将Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends Shape>使用。即List<? extends Shape>可以表示List<Circle>、List<Rectangle>的父类--只要List后尖括号里的类型是Shape的子类型即可。

设定类型形参上限

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

泛型方法

定义泛型方法

现在需要将一个Object数组的所有元素添加到一个Collection集合中

public class GenericMethodTest {
    // 这个方法只能放入Object类型,其他不可以
    static void fromArrayToCollection(Object[] a, Collection<Object> c)
    {
        for (Object o : a)
        {
            c.add(o);
        }
    }

    public static void main(String[] args)
    {
        String[] strArr = {"a", "b"};
        List<String> strList = new ArrayList<>();
        // Collection<String>对象不能当成Collection<Object>使用,下面会出现编译错误
        fromArrayToCollection(strArr, strList);
    }
}

使用泛型方法后,编译器根据实参推断类型形参的值。
因为是使用第二个参数来add添加集合的,所以第二个参数是什么类型,那么第一个类型就是什么类型,前提是第二个参数可以兼容第一个参数,比如Number和Integer。


public class GenericMethodTest {
    // 使用<T>,声明一个泛型方法,该泛型方法中带一个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);
    }
}

错误的使用案例

public class ErrorTest {
    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> ao = new ArrayList<>();
        List<String> as = new ArrayList<>();
        // 下面代码将产生错误
        // 该方法中的两个形参from、to的类型都是Collection<T>
        // 这要求调用该方法时的两个集合实参中的泛型类型相同
        // 否则编译器无法准确推断出泛型方法中类型形参的类型。
        test(as, ao);
    }
}

改进代码


public class ErrorTest {
    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<>();
        // 下面代码可以正常运行
        // 只要前一个Collection集合里的元素类型是后一个
        // Collection集合里的元素类型的子类即可。
        test(as, ao);
    }
}

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

大多数时候都可以使用泛型方法来代替类型通配符,例如对于Java的Collection接口中的两个方法定义:

public interface Collection<E> extends Iterable<E> 
{
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    ...
}

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示:

public interface Collection<E> extends Iterable<E> 
{
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
    ...
}

Java的Collections的copy方法,使用了类型通配符

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

改为使用泛型方法

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

类型通配符与泛型方法(在方法签名中显式声明类型形参)还有一个显著的区别,类型通配符即可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参只能在对应方法中显式声明。

Java 7的“菱形”语法与泛型构造器

public class Foo {
    public <T> Foo(T t)
    {
        System.out.println(t);
    }

    public static void main(String[] args)
    {
        // 泛型构造器中的T参数为String
        new Foo("a" );
        // 泛型构造器中的T参数为Integer
        new Foo(200);
        // 显示指定泛型构造器中的T参数为String
        // 传给Foo构造器的实参也是String对象,完全正确
        new <String> Foo("a");
        //new <String> Foo(12.3);

    }
}

Java 7新增的“菱形”语法允许调用构造器时在构造器后使用一对尖括号来代表泛型信息,但如果程序显式的指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。如下所示:


public class MyClass <E>
{
    public <T> MyClass(T t)
    {
        System.out.println("t参数的值为:" + t);
    }

    public static void main(String[] args)
    {
        // MyClass类声明中的E形参是String类型
        // 泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc1 = new MyClass<String>(5);
        // 显示指定泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc2 = new <Integer> MyClass<String>(5);
        // MyClass类声明中的E形参是String类型
        // 如果显示指定泛型构造器中声明的T形参是Integer类型
        // 此时就不能使用“菱形”语法,下面的代码是错误的
        MyClass<String> mc3 = new <Integer> MyClass<>(5);
    }
}

设定通配符的下限

简单来讲就是可以设定传入的参数是某个参数的子类
现在实现一个工具:将src集合里的元素复制到dest集合里,代码如下:

public static <T> void copy(Collection<T> dest, Collection<? extends T> src)
{
    for (T ele : src)
    {
        dest.add(ele);
    }
}

假设上面的方法需要一个返回值,返回最后一个被复制的元素,代码如下:

// 返回的其实是Number类,但是实际返回的却是Integer,最后一个元素
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;
}

调用上面的代码:

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
// 下面代码会引起编译错误
// 因为最后返回的是Number类型
Integer last = copy(ln, li);

为了解决这个问题,可以使用表示类型下限的表达式super,使用这种语句,就可以保证程序最后返回的值的类型是第一个参数的子类,而不是笼统的Number类型,代码如下:

public class MyUtils {
    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);
        li.add(6);
        Integer last = copy(ln, li);
        System.out.println(last);

    }
}

TreeSet类的实现
TreeSet类也是使用了设置通配符下限的方式

 public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。

public class TreeSetTest {
    public static void main(String[] args)
    {
        TreeSet<String> ts1 = new TreeSet<>(
                new Comparator<Object>()
                {
                    @Override
                    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>()
                {
                    @Override
                    public int compare(String fst, String snd) {
                        return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 :0;
                    }
                }
        );

        ts1.add("Hello");
        ts1.add("wa");
    }
}

泛型方法与方法重载

下面的两个方法虽然写法不一样,但在表达上面是一样的,所以不会通过编译。

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

public static <T> T copy(Collection<? super T> dest, Collection<T> src)
{
    ...
}

Java 8改进的类型推断

public class MyUtil <E>{
    public static <Z> MyUtil <Z> nil()
    {
        return null;
    }

    public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail)
    {
        return null;
    }

    E head()
    {
        return null;
    }

    public static void main(String[] args)
    {
        // 可以通过方法赋值的目标参数(MyUtil<String> ls)来推断类型参数为String
        MyUtil<String> ls = MyUtil.nil();
        // 无需使用下面语句在调用nil()方法时指定类型参数的类型,<String>
        MyUtil<String> mu = MyUtil.<String>nil();
        // 可调用cons()方法所需的参数类型来推断类型参数为Integer
        MyUtil.cons(42, MyUtil.nil());
        // 无须使用下面语句在调用nil()方法时指定类型参数的类型
        MyUtil.cons(42, MyUtil.<Integer>nil());
        // 无法推断,必须显示指定类型参数
        // String s1 = MyUtil.nil().head();
        // 改为下面的形式
        String s2 = MyUtil.<String>nil().head();
    }
}

擦除与转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数,如果没有为这个泛型类型指定实际的类型参数,则该类型参数被称作raw type(原始类型),默认是声明该类型参数时指定的第一个上限类型。
下面程序定义了一个带泛型声明的Apple类,其类型形参的上限是Number,这个类型形参用来定义Apple类的size变量

class Apple<T extends Number>
{
    T size;
    public Apple(){}
    public Apple(T size)
    {
        this.size = size;
    }

    public void setSize(T info)
    {
        this.size = size;
    }

    public T getSize()
    {
        return this.size;
    }

    public static void main(String[] args)
    {
        Apple<Integer> a = new Apple<>(6);
        System.out.println(a.getSize());
        // 把a对象赋给Apple变量,将丢失尖括号里的类型信息
        Apple b = a;
        // b只知道size的类型是Number
        // 因为类型是默认声明该类型参数时指定的第一个上限类型Number
        Number size1 = b.getSize();
        // 下面代码引起编译错误
        //Integer size2 = b.getSize();
    }
}
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
// 丢失泛型信息,这是典型的擦除
List list = li;
// 下面代码引起“未经检查的转换”警告,编译、运行时完全正常
List<String> ls = list;
// 但只要访问ls里的元素,就会引起运行时异常
// ls实际上引用的是List<Integer>集合
// 所以,当试图把该集合里的元素当成String类型的对象取出来的时候
// 将引发异常
System.out.println(ls.get(0));

下面的代码与上面的类似

List li = new ArrayList<>();
li.add(6);
li.add(9);
// 当试图通过强制类型转换把它转换成一个String,将引发运行时异常
// 因为实际引用的是List<Integer>
System.out.println((String)li.get(0));

泛型与数组

数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符,但可以声明元素类型包含类型变量或类型形参的数组。

// 错误用法
List<String>[] las1 = new ArrayList<String>[10];
// 正确用法,采用无上限的类型通配符
List<?>[] las2 = new ArrayList<?>[10];

下面的用法会引起“未经检查的转换”的警告,即编译器并不能保证这段代码是类型安全的。

List<String>[] las3 = new ArrayList[10];

Java允许创建无上限的通配符泛型数组,例如new ArrayList

List<?>[] lsa = new ArrayList<?>[10];
// 在这里同步两个变量
Object[] oa = lsa;
// 创建一个List集合
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(4));
// 将List集合放到Object[] oa里面
oa[1] = li;
// oa在这里是Object类型
System.out.println(oa[1].toString().getClass());

// 在这里是Integer类型
// 貌似泛型自动将同步进去的变量转换为Integer类型了
System.out.println(lsa[1].get(0).getClass());
// 所以转换将会引发编译异常
// java.lang.Integer cannot be cast to java.lang.String
// String s = (String)lsa[1].get(0);

// 为了保证转换成功,在此使用instanceof
Object target = lsa[1].get(0);
if (target instanceof String)
{
    System.out.println("可以转换");
    String sa = (String) target;
}

创建元素类型是类型变量的数组对象也将导致编译错误,如下

<T> T[] makeArray(Collection<T> coll)
{
    // 下面代码导致编译错误
    return new T[coll.size()]
}

由于类型变量只在编译时存在,在运行时不存在,而编译器无法确定实际类型是什么,因此编译器报错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值