欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

Java 泛型,java泛型

来源: javaer 分享于  点击 38821 次 点评:170

Java 泛型,java泛型


参考:

《Java 编程思想》 - 15章
Java总结篇系列:Java泛型:http://www.cnblogs.com/lwbqqyumidi/p/3837629.html
Java心得总结四】Java泛型下——万恶的擦除:(https://www.zhihu.com/question/20400700)
Java 泛型总结(三):通配符的使用:https://segmentfault.com/a/1190000005337789
https://www.zhihu.com/question/20400700


泛型是 Java 中很重要的一部分内容,之前学习 Java 的时候就是大概浏览了一遍。对于里面的内容并没有很好的理解,在实际编程中经常会看到关于泛型的使用。这次好好理清其中的几个重要概念


主要内容


泛型基本概念

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
































示例 1:打印字符串,数字

不用泛型:

public class TypeParameterization {

    private void print(String str) {
        System.out.println(str);
    }

    private void print(double d) {
        System.out.println(d);
    }

    private void print(int i) {
        System.out.println(i);
    }

    public static void main(String[] args) {
     TypeParameterization typeParameterization = new TypeParameterization();

     typeParameterization.print(23);
     typeParameterization.print("Hello World");
     typeParameterization.print(12.31341);
    }
}

使用泛型方法:

public class TypeParameterization {

    private <T> T print(T t) {
        System.out.println(t);

        return t;
    }

    public static void main(String[] args) {
        TypeParameterization typeParameterization = new TypeParameterization();

        typeParameterization.print(23);
        typeParameterization.print("Hello World");
        typeParameterization.print(12.31341);
    }
}

结果:

示例 2:列表输入不同类型值

不用泛型:

public static void main(String[] args) {
    List list = new ArrayList();
    list.add("Hello World");
    list.add("Hi zj");
    list.add(123);

    for (int i = 0; i < list.size(); i++) {
        String name = (String) list.get(i);
        System.out.println("name:" + name);
    }
}

使用泛型:

public static void main(String[] args) {
    List<T> list = new ArrayList();
    list.add("Hello World");
    list.add("Hi zj");
    list.add(123);

    for (int i = 0; i < list.size(); i++) {
        String name = (String) list.get(i);
        System.out.println("name:" + name);
    }
}

  • 泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

  • Java 泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节

  • Java 泛型的局限性:基本类型无法作为类型参数。在类定义时,必须传入对象;当在调用类方法时,传入基本类型,自动打包机制将基本类型的值包装为对应的对象。


泛型类、泛型接口以及泛型方法

泛型类

泛型类即增加了类型参数的类

类型参数:用尖括号括住,放在类名后面,然后在使用这个类的时候,再用实际的类型替换此类型参数

示例:

public class Generic<T> {

    public void Hello(T t) {
        System.out.println("t = " + t);
    }

    public static void main(String[] args) {
        Generic<String> generic = new Generic<>();

        generic.Hello("Hello World");
    }

}

可以继承泛型类,只需在继承时确定类型参数即可:

public class GenericInheritance extends Generic<String> {

    @Override
    public void Hello(String s) {
        super.Hello(s);
    }

    public static void main(String[] args) {
        GenericInheritance genericInheritance = new GenericInheritance();

        genericInheritance.Hello("Hello World");
    }
}

子类也可作为泛型类,也可增加类型参数:

public class GenericInheritance<T, E> extends Generic<T> {

    @Override
    public void Hello(T t) {
        super.Hello(t);
    }

    public void Hi(E e) {
        System.out.println("e = " + e);
    }

    public static void main(String[] args) {
        GenericInheritance<String, Double> genericInheritance = new GenericInheritance<>();

        genericInheritance.Hello("Hello World");

        genericInheritance.Hi(3.14159627);
    }
}

泛型接口

接口使用泛型与类使用泛型没什么区别,同样将类型参数置于接口名后面,用尖括号括住:

public interface GenInterface<T> {

    void print(T t);

}

当有类实现该接口时,用实际类型替换类型参数:

public class Generic implements GenInterface<String> {

    @Override
    public void print(String s) {
        System.out.println(s);
    }

    public static void main(String[] args) {
        Generic generic = new Generic();

        generic.print("Hello World");
    }

}

如果是泛型类继承该接口,可使用泛型类的类型参数来替换接口的类型参数:

public class Generic<E> implements GenInterface<E> {

    @Override
    public void print(E e) {
        System.out.println("e = " + e);
    }

    public static void main(String[] args) {
        Generic<String> generic = new Generic<>();

        generic.print("Hello World");
    }
}

泛型方法

是否拥有泛型方法,与其所在的类是否是泛型没有关系。泛型方法使得该方法能够独立于类而产生变化。

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

public class Generic {

    public <T> T func(T x) {
        if (x.getClass() == String.class) {
            System.out.println("String: x = " + x);
        } else {
            System.out.println("Other: x = " + x);
        }

        return x;
    }

    public static void main(String[] args) {
        Generic generic = new Generic();

        generic.func("array");
        generic.func(12341);
    }
}

泛型方法也可作用于接口,并且独立于接口而变化:

public class Generic implements GenInter {

    @Override
    public <T> void print(T t) {
        System.out.println("t = " + t);
    }

    public static void main(String[] args) {
        Generic generic = new Generic();

        generic.print("Hello World");
        generic.print(1.32351);
    }

}

interface GenInter {

    <T> void print(T t);

}

Note 1:当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会自动找出具体的类型,这称为 类型参数推断( type argument inference

Note 2:类型参数推断不足之处 - 仅对赋值操作有效,其他时候并不起作用。比如,将泛型方法结果作为参数时,传递给另一个方法时,编译器并不执行类型判断。在这种情况下,编译器认为:调用泛型方法后,其返回值被赋给一个 Object 类型的变量

关于 Note 2 的说法,我测试了一下,好像是可以的:

public class LimitsOfInference {

    public <T> T print(T t) {
        return t;
    }

    public <T> void hello(T t) {
        System.out.println("t = " + t);
    }

    public static void main(String[] args) {
        LimitsOfInference limitsOfInference = new LimitsOfInference();

        limitsOfInference.hello(limitsOfInference.print("HJ"));
    }

}

可能是 java 版本改进了吧?

泛型类、接口、方法使用指南

基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。另外,对于一个 static 的方法而言,无法访问泛型类的类型参数,所以,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法


擦除

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

Java 泛型是使用擦除实现的。当你在使用泛型时,任何具体的类型参数信息都被擦除,唯一知道的就是你在使用一个对象:

public class ErasedTypeEquivalence {

    public static void main(String[] args) {
        Class c1 = new ArrayList<Integer>().getClass();
        Class c2 = new ArrayList<String>().getClass();

        System.out.println(c1 == c2);
        System.out.println(c1 == ArrayList.class);
    }
}

结果均为 true,表明类 c1 和类 c2 仅为原生的 ArrayList 类型,其具体类型信息 IntegerString 在编译时被擦除了。

Note:C++Python 等泛型类能够保留具体类型信息,而 Java 不能

由于 Java 擦除了具体类型信息,就是无法在实例化时知道类型参数可以进行哪些具体操作,所以 Java 编译器判定类型参数变量无法调用具体操作。比如:

public class Erased<T> {

    private T obj;

    public Erased(T obj) {
        this.obj = obj;
    }

    private void print(int a, int b) {
        System.out.println("add: " + obj.add(a, b));  // 1 无法编译

        System.out.println("sub: " + obj.subtract(a, b)); // 2 无法编译
    }

    public static void main(String[] args) {
        Erased<Cal> erased = new Erased<>(new Cal());

        erased.print(3, 4);
    }

}

class Cal {
    private int add(int a, int b) {
        return a + b;
    }

    private int subtract(int a, int b) {
        return a - b;
    }
}

边界

解决方法:为泛型类设置边界,告知编译器类型参数能够进行哪些具体操作。

具体操作:定义一个接口,类型参数继承(extends)这个接口

public class Erased<T extends Boundary> {

    private T obj;

    public Erased(T obj) {
        this.obj = obj;
    }

    private void print(int a, int b) {
        System.out.println("add: " + obj.add(a, b));

        System.out.println("sub: " + obj.subtract(a, b));
    }

    public static void main(String[] args) {
        Erased<Cal> erased = new Erased<>(new Cal());

        erased.print(3, 4);
    }

}

interface Boundary {
    int add(int a, int b);

    int subtract(int a, int b);
}

class Cal implements Boundary {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int subtract(int a, int b) {
        return a - b;
    }
}

设置边界起作用的原因是 Java 编译器将类型参数 Cal 的具体信息擦除了,仅剩下边界 Boundary仅擦除到第一个边界),所以如果不将类泛型化,仅使用 Boundary 接口,可实现与上面代码同样的效果,此时泛型类并没有贡献任何好处《Java 编程思想》 上的内容):

public class Erased implements Boundary {

    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int subtract(int a, int b) {
        return a - b;
    }

    private void print(int a, int b) {
        System.out.println("add: " + add(a, b));

        System.out.println("sub: " + subtract(a, b));
    }

    public static void main(String[] args) {
        Erased erased = new Erased();

        erased.print(3, 4);
    }

}

interface Boundary {
    int add(int a, int b);

    int subtract(int a, int b);
}

但是我实践了一下,好像并没有仅擦除到第一个边界,多个继承的方法都可以使用的,可能是版本更新了吧?

Note 1:类型参数可以有多个边界,和继承(inheritance)一样,即可继承单个类以及多个接口

Note 2:当同时需要继承类和接口时,类必须放置在最前面(即第一边界)

示例:

public class MultipleBounds {

    public static void main(String[] args) {
        Gen<Bound> gen = new Gen<>(new Bound());

        gen.print(new Bound());
        gen.hello(new Bound());
        gen.getX();
    }

}

interface BasicInter1 {
    <T> void print(T t);
}

interface BasicInter2 {
    <T> void hello(T t);
}

class BasicClass {
    int x;

    int getX() {
        return x;
    }
}

// 继承类和接口,类必须放置在最前面
class Bound extends BasicClass implements BasicInter1, BasicInter2 {
    @Override
    public <T> void print(T t) {

    }

    @Override
    public <T> void hello(T t) {

    }
}

// 设置多个边界,和继承一样,类必须放置在最前面
class Gen<T extends BasicClass & BasicInter1 & BasicInter2> {
    T item;

    public Gen(T t) {
        this.item = t;
    }

    void print(T t) {
        item.print(t);
    }

    void hello(T t) {
        item.hello(t);
    }

    int getX() {
        return item.getX();
    }
}

何时使用泛型

  • 只有当你希望使用的类型参数比某个具体类型(以及它的所有子类型)更加泛化时 - 也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助

  • 当有返回类型参数的方法存在时,需要使用泛型,因为返回的是具体类型


通配符

通配符的概念和使用比较复杂,《Java 编程思想》 上的内容看了几遍也没理解,里面的有些示例代码也有一些问题(可能是 Java 版本更新的缘故),在网上找了一些博文,加深理解

通配符浅析

通配符:泛型参数表达式中的问号(?

Note:通配符被限制为单一边界

通配符主要有 3 种用法:

上界通配符 - <? extends T>
下界通配符 - <? super T>
无界通配符 - <?>

列表 List<? extends T> 表示具有任何从 T 继承的类型的列表。副作用:无法往列表中加对象,仅能从列表中取出对象

列表 List<? super T> 表示类型为 T 或者被 T 继承的类型的列表。副作用:从列表中取出的对象为 Object 类型,能往列表中加对象

使用 extends、super 来扩展泛型的目的是为了弥补泛型列表仅能存放一种特定类型数据的不足

但是使用 extends、super 能够在实际使用中发挥什么作用,还没有搞懂,等待日后能够理清

Note:PECS(Producer Extends Consumer Super) 原则

频繁往外读取内容的,适合用上界Extends。

经常往里插入的,适合用下界Super。

相关文章

    暂无相关文章
相关栏目:

用户点评