java - 泛型

java - 泛型_第1张图片

java - 泛型

java - 泛型

java SE 5 加入了泛型,泛型实现了 参数化类型 的概念,使代码可以应用于多种类型。

创造容器类是促成泛型出现的原因之一,泛型可以用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

1. 简单泛型

public class Holder {

    private E ele;

    public Holder(E e) {
        this.ele = e;
    }

    public E get() {
        return ele;
    }

}

Holder 类现在可以持有任何类型的对象。如:

    public static void main(String[] args) {
        Holder h1 = new Holder("string");
        Holder h2 = new Holder<>(new Exception());
        Exception e = h2.get(); // 取出时自动就是目标类型了
    }

2. 泛型接口

定义接口 Generator —— 生成器,专门用于创建对象的类。

public interface Generator {
    T next();
}

编写辅助类:Fruit 及其子类 苹果,西瓜,梨。

public class Fruit {
    private static long counter = 0;
    private final long id = counter++; // counter 是属于类的,每次创建对象都会自增

    @Override
    public String toString() {
        String c = getClass().getSimpleName();
        return String.format("%-15s%-10d", c, id); // 格式化字符串
    }

    static class Apple extends Fruit {
    }

    static class Watermelon extends Fruit {
    }

    static class Pear extends Fruit {
    }
}

现在可以编写类实现 Generator< Fruit >接口,用于产生水果:

public class FruitGenerator implements 
    Generator, 
    Iterable  { // 使可以在 foreacher 语句中迭代

    private Random random = new Random(47);
    private Class[] types = {Fruit.Apple.class, Fruit.Watermelon.class, Fruit.Pear.class};
    private int size; // 边界值,产生器的生产量边界

    public FruitGenerator(int size) {
        this.size = size;
    }

    @Override
    public Iterator iterator() {
        return new FruitIterator();
    }

    @Override
    public Fruit next() {
        Class type = types[random.nextInt(types.length)];
        Fruit fruit = null;
        try {
            fruit = (Fruit) type.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return fruit;
    }

    class FruitIterator implements Iterator { // 迭代器
        int count = size;

        @Override
        public boolean hasNext() {
            return count > 0;
        }

        @Override
        public Fruit next() {
            count--;
            return FruitGenerator.this.next(); // 随机产生[水果]
        }
    }

    public static void main(String[] args) {
        for (Fruit f : new FruitGenerator(10)) {
            System.out.println(f);
        }
    }
}

利用反射使随机产生水果。
输出:

Pear           0         
Pear           1         
Watermelon     2         
Pear           3         
Watermelon     4         
Pear           5         
Watermelon     6         
Pear           7         
Apple          8         
Watermelon     9         

泛型的一个局限性:基本类型无法作为类型参数,不过,java SE 5 具备了自动打包和拆包功能,可以很方便的在基本类型与其包装类之间转换。

3. 泛型方法

  1. 是否拥有泛型方法与其所在类是否为泛型类没有关系。
  2. 使用泛型类或泛型方法的原则:无论何时,只要你做得到,就应该尽量使用泛型方法。
  3. 对于一个 static 方法,其无法访问所在泛型类的类型参数,所以,静态方法使用泛型的唯一方式是使其自己成为泛型方法。(类似于类的静态成员和非静态成员的关系,静态成员是属于类的,静态成员无法访问非静态成员,同理,类型参数是属于对象的,静态成员无法访问)。

定义泛型方法,只需将泛型参数列表置于返回值之前:

public class GenericMethods {

    public static  void printClassName(E e) {
        System.out.printf(e.getClass().getSimpleName());
    }

    public static  void printSameMethods(E e, T t, F f) {
        Method[] ems = e.getClass().getMethods();
        Method[] tms = t.getClass().getMethods();
        Method[] fms = f.getClass().getMethods();

        Set set = new HashSet<>(); // Set 集合元素不能重复
        for (Method m : ems) {
            set.add(m.getName());
        }
        for (Method m : tms) {
            set.add(m.getName());
        }
        for (Method m : fms) {
            set.add(m.getName());
        }

        for (String s : set) {
            System.out.printf(s + "\n");
        }

    }

    public static void main(String[] args) {
        // 不必指明参数类型
        GenericMethods.printClassName(AbstractCollection.class);
        System.out.printf("\n");
        GenericMethods.printSameMethods(HashSet.class, ArrayList.class, TreeMap.class);
    }

}

注意:当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这称为 类型参数推断

泛型有时需要向程序中加入更多的代码

创建一个持有 List 的 Map:
在 JDK 1.6 之前,你需要通过如下方式声明:
Map> map = new HashMap>();
JDK 1.6 之后得到了简化:
Map> map = new HashMap<>();

显示的类型说明

在泛型方法中,可以显示的指明类型,但这种语法很少使用,要显示的指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。

public class Test {


    public static void main(String[] args) {

        Test test = new Test();
        test.f(test.map());
    }

    public void f(Map map) {
    }

    public  Map map() {
        return new HashMap<>();
    }

}

4 可变参数与泛型

可变参数与泛型可以很好的共存。

public class Test {


    public static void main(String[] args) {
        Test t = new Test();
        for (Integer i : t.list(1, 2, 3, 4, 5)) {
            System.out.printf(i + " ");
        }

    }

    public  List list(T... ts) {
        return Arrays.asList(ts);
    }

}

输出

1 2 3 4 5 

5 擦除的神秘之处

    public static void main(String[] args) {
        Class c1 = new ArrayList().getClass();
        Class c2 = new ArrayList().getClass();
        System.out.printf(c1.equals(c2) ? "true" : "false");
    }

输出结果为 true,ArrayListArrayList很容易被认为是不同的类型,不同的类型在行为方面肯定不同,但事实是他们是同样的类型。

在泛型代码内部,无法获得任何有关泛型参数类型的信息

java 是使用擦除来实现的,这意味着当你使用泛型时,任何具体的类型信息都被擦除了。

public class Test {

    public static void main(String[] args) {
        Test t = new Test();
        t.test(t.new Te());
    }

    private  void test(E e) {
        e.f(); // 编译无法通过
    }

    class T {
        void f() {
        }
    }

    class Te extends T {
    }

}

上面的例子在泛型方法 test 中调用了 e 对象的 f 方法,对于 C++ ,这是允许的,C++ 编译器将对此进行检查,它看到 e 对象调用了 f 方法,而此时 e 映射到的类型 Te 拥有 f 方法,编译就通过,否则编译出错,这样就保证了类型安全。 java 则不同,由于有了擦除,编译器无法将 test 方法通过参数 e 调用 f 方法这一需求映射到 e 对象拥有 f 方法这一事实上。我们必须协助泛型类,给定泛型的边界,以此告知编译器只能接受遵循这个边界的类型。这样就能通过编译:

public class Test {


    public static void main(String[] args) {
        Test t = new Test();
        t.test(t.new Te());
    }

    private  void test(E e) {
        e.f(); // 编译通过
    }

    class T {
        void f() {
        }
    }

    class Te extends T {
    }

}

泛型类型擦除会擦除到他的第一个边界,因而 e 对象可以调用 T 类型的所有可访问方法。

6 迁移兼容性

擦除是 java 泛型实现的一个折中,因为泛型不是 java 语言出现时就有的组成部分,所以这种折中是必须的。

泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为他们的非泛型上界,例如,诸如 List这样的类型擦除时将被替换为 List ,而普通的类型变量在未指定边界的情况下将被擦除为 Object

擦除的核心动机是使得泛化的客户端可以使用非泛化的类库,反之亦然,这经常被称为 迁移兼容性。java 泛型必须支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持其之前的定义,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。

7 擦除的问题

擦除的主要正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入 java 语言。

擦除的代价时显著的,泛型不能用于显示的引用运行时类型的操作中,例如转型、instanceof 和 new 操作,因为所有有关类型的信息都丢失了。

public class Foo{
    T var;
}
Foo f = new Foo();

在泛型类 Foo 中,看似类型 T 都在被替换为 String ,但事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个 Object 。”

@SuppressWarnings("unchecked")

禁止【未检查类型】警告,该注解在 java SE 5 之前不支持。

这个注解被放置在产生这类警告的方法之上,而不是整个类上,当要关闭警告时,最好是尽量的“聚焦”,这样就不会因为过于宽泛的关闭警告,而导致意外的遮蔽掉正在的问题。

public class Test {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        TC tc = new TC(); 
        tc.put(""); //未指明类型参数类型,因而在赋值时编译器无法进行类型安全检查

    }

    static class TC {
        T t;

        void put(T t) {
            this.t = t;
        }

        T get() {
            return t;
        }
    }

}

8 边界处的动作

public class Test {

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        TC tc = new TC();
        List list = tc.create(1, 2, 3, 4, 5); // 未检查警告
        System.out.println(list);
    }

    static class TC {
        List create(T... ts) {
            List result = new ArrayList();
            for (T t : ts) {
                result.add(t);
            }
            return result;
        }
    }
}

即使编译器无法知道有关 create 方法中的 T 的任何信息,但它仍旧可以确保你放置到 result 中的对象具有 T 类型,使其适合 ArrayList,因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以在方法或类中使用类型的 内部一致性

因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期间执行类型检查并插入转型代码的地点。

为上述 TC 类添加如下方法:
void set(T t) { ele = t; }
当外部调用 set 方法时,编译器会进行类型检查,当然,前提是指定 TC 类的类型参数,即修改上述代码中 tc 的定义如下:
TC tc = new TC<>();
这样,当向 set 方法传入错误的类型,编译器会提醒你(编译无法通过),若不指定类型参数,则只会得到未检查类型 警告。

添加如下方法:
T get(){ return (T)"str";} // 此时会得到未检查类型转换异常
对比如下实现:
T get(){ return ele;} // 合法
外部调用:
Integer num = tc.get(); // 指定类型参数为 Integer
由于擦除,这里 get 方法中的 T 在运行时就是 Object 。但外部调用 get 方法时却无需进行显示的类型转换,然而这里的类型转换是必须的。事实是,编译器完成了这个动作,编译器会在编译期插入对传递出去值的转型。

修改后的代码:

public class Test {

//    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        TC tc = new TC<>();
//        tc.set(""); // 编译无法通过
        List list = tc.create(1, 2, 3, 4, 5);
        System.out.println(list);

        tc.set(13);
        Integer num = tc.get();
        System.out.println("\n" + num);
    }

    static class TC {
        T ele;

        List create(T... ts) { // 编译期类型检查
            List result = new ArrayList();
            for (T t : ts) {
                result.add(t);
            }
            return result;
        }

        // 边界就是发生动作的地方
        void set(T t) { // 编译期类型检查
            ele = t;
        }

        T get() {
            return ele; // 编译期转型代码插入发生的地方
        }
    }
}

9 擦除的补偿

擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型的操作都无法工作。

要想在运行时获得类型信息只能通过类型判断,当然,这就违背了 java 泛型的主旨:参数化类型

public class Test {

    public static void main(String[] args) {
        TC tc = new TC<>();
        System.out.println(Person.class.getSimpleName());
        tc.check(new Person("tom", 12));
    }

    static class TC {
        void check(T t) {
            if (t.getClass().equals(Person.class)) {
                Person p = (Person) t;
                System.out.println(p.getName());
            }
        }
    }

    static class Person {
        String name;
        int id;

        public Person(String name, int id) {
            this.name = name;
            this.id = id;
        }

        public String getName() {
            return name;
        }

        int getId() {
            return id;
        }
    }
}

需要知道确切类型的操作都无法工作。

    static class TC {
        void test() {
//            T[] ts = new T[1]; // 错误
//            T t = new T(); // 错误
//            T[] ts1 = (T[]) new Object[1]; // 未检查类型转换
        }
    }
擦除补偿:类型标签

需要显示的传递类型的 Class 对象,以便可以在类型表达式中使用。

public class Test {

    public static void main(String[] args) {
        TC tc = new TC<>(A.class);
        System.out.println(tc.check(""));
        System.out.println(tc.check(new A()));
    }

    static class TC {
        Class kind;

        public TC(Class kind) {
            this.kind = kind;
        }

        boolean check(Object obj) {
            return kind.isInstance(obj);
        }
    }

    static class A {
    }
}

输出:

false
true

isInstance 方法将检查 obj 的类型是否为 kind。


文章大部分摘抄自《Java 编程思想》 第 15 章 泛型,加上部分自己的理解,若有错误,欢迎指正。


你可能感兴趣的:(java - 泛型)