suninf 's blog

It’s not what you know, it’s how you think

Java范型

Catalog

Java在5.0开始支持范型,能做到更好的类型安全性可读性,减少容器的元素类型与Object的强制转换。不过,与C++相比,Java的范型相对比较弱,因为范型类的不同类型参数并不会真正实例化出独立的类型,而是通过类型擦除的技术来实现范型。

范型基本用法

范型类

范型类基本形式:class ClassName<T,U>

一个简单的例子:

class MyArray<E> {
    MyArray() {
    }

    public int size() { return size; }

    public boolean isEmpty() {
        return size == 0;
    }

    public void add( E item) {
        grow(size + 1);
        items[size++] = item;
    }

    public E get(int index) {
        if ( index >= size ) {
            throw new IndexOutOfBoundsException("index out of bounds");
        }

        return (E)items[index];
    }

    private void grow(int minCapacity) {
        int oldCapacity = items.length;
        if ( oldCapacity >= minCapacity ) {
            return;
        }

        int newCapacity = oldCapacity * 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }

        items = Arrays.copyOf(items, newCapacity);
    }

    private Object[] items = {};
    private int size = 0;
}


// test
MyArray<String> ma = new MyArray<>();
ma.add("hello");
ma.add("world");

for ( int i=0; i<ma.size(); ++i ) {
    System.out.println( ma.get(i));
}

范型方法

可以定义带有参数类型的方法,类型变量<T>需要放在修饰符(public static)的后面,返回类型的前面

class ArrayAlg {
    public static <T> T getMiddle(T[] a) {
        return a[a.length/2];
    }
}

编译器能自动推断出参数的类型,实际函数调用时,不用传递类型变量,例如:

String[] names = {"hello", "world"};
ArrayAlg.getMiddle(names);

限定符 extends

可以对类型参数T设置依赖:<T extends BoundingType1 & BoundingType2, U>,T和BoundingType可以是类型活着接口,T要求是BoundingType1和BoundingType2的子类型

限定类型用&分隔,逗号用来分隔类型变量

例如:

T 需要实现了Comparable接口,才能调用compareTo方法。

public static <T extends Comparable> T min(T[] a) {
    if ( a==null || a.length == 0 ) {
        return null;
    }

    T smallest = a[0];
    for (int i=0; i<a.length; ++i) {
        if (smallest.compareTo(a[i]) > 0) {
            smallest = a[i];
        }
    }
    return smallest;
}

范型的实现机制(类型擦除)

类型擦除:Java虚拟机没有范型类型对象,所有范型类型的定义都自动提供了一个相应的原始类型(raw type),也就是删除了类型参数后的范型类型名,同时擦除(erased)类型变量,并替换为第一个限定类型(没有限定类型的变量用Object)。

范型类的翻译

class MyClsTest<T extends Comparable, U> {
    T t;
    U u;
}

// 编译器翻译
class MyClsTest {
    Comparable t;
    Object u;
}

范型表达式的翻译

调用范型方法,如果擦除返回类型,编译器会插入强制转换

Pair<Employee> p = new Pair<Employee>();
Employee e = p.getFirst();

// 编译器翻译
Pair p = new Pair();
Employee e = (Employee)p.getFirst();

范型方法的翻译

public static <T extends Comparable> T min(T[] a);

// 编译器翻译
public static Comparable min(Comparable[] a);

范型的约束限制

不能使用基本类型实例化类型参数

因为类型擦除的存在,类型参数只支持对象类型,比如Pair<double>不合法

instanceof类型查询只适用于原始类型

JVM中对象没有范型,instanceof检测类型,查的就是原始类型,比如 if(a instanceof Pair<String>)等价于if(a instanceof Pair)

不支持参数化类型的数组

比如:Pair<String>[] table = new Pair<String>[10]; 不支持

因为数组不像范型容器,它知道自己集合的类型,如果支持范型类型的数组,因为类型擦除的存在,会导致类型错误。

不能直接实例化类型变量,需要借助于 Class<T>

  • 不能使用类似 new T()T.class之类的表达式
  • 类型变量的实例化需要借助于 Class<T>

例如:

class Pair<T> {
    public Pair() {}
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() { return first; }
    public T getSecond() { return second; }

    public void setFirst(T newValue) { first = newValue; }
    public void setSecond(T newValue) { second = newValue; }

    private T first;
    private T second;
}

private static <T> Pair<T> makePair(Class<T> cl) {
    try {
        return new Pair<T>( cl.newInstance(), cl.newInstance() );
    } catch (Exception e) {
        return null;
    }
}

// test
Pair<String> pr = makePair(String.class);
pr.setFirst("hello");

不能实例化类型变量的数组,需要借助于 Array.newInstance

例子:

public static <T extends Comparable> T[] minmax(T[] a) {
    T[] mm = (T[])Array.newInstance( a.getClass().getComponentType(), 2 );
    // ...
}

不能在静态变量或方法中引用范型类的类型变量

class MyClass<T> {
    private static T instance; // ERROR
}

通配符 ?

Java支持使用?通配符类型来支持类型参数变量的继承关系

extends 子类限定

例如:Pair<? extends Employee> 表示参数类型是Employee的子类

public static void print( Pair<? extends Employee> p ) {
    Employee e = p.getFirst();
    // p.setFirst( obj );
}

print方法可以接受Pair<Employee>Pair<Manager>,如果Manager继承自Employee

注意Pair<? extends Employee>的限定对象的特点:

setFirst和getFirst的方法:

? extends Employee getFirst();
void setFirst(? extends Employee);
  • 可以返回值,如getFirst(),因为编译器知道返回对象是继承自Employee的,因此是合法的
  • 不能提供参数,如setFirst,编译器无法明确传入的参数类型,因此拒绝传递任何特定的类型
  • 带有子类限定的通配符,可以从范型对象读取

super 超类限定

? super Manager 的行为与extends相反,表示参数是Manager的超类,可能是Object

注意Pair<? super Manager>的限定对象的特点:

? super Manager getFirst();
void setFirst(? super Manager);
  • 因为getFirst不确定类型,可能是Object,因此只能用Object来接
  • 可以为方法提供参数,如setFirst支持写入Manager及其超类的对象
  • 带有超类限定的通配符,可以向范型对象写入

参考

Comments