Java 泛型,上界(生产者),下界(消费者)总结

Java泛型

1,什么是泛型

Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

2,作用

第一是泛化。可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

第二是类型安全。泛型的一个主要目标就是提高ava程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。

第三是消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

第四是向后兼容。支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。

3,泛型定义及使用

包括类泛型,接口泛型,方法泛型

在讲泛型之前,先要了解一个概念:泛型擦除

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
        
System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。

  • 3.1,泛型类和泛型接口

如果定义的一个类或接口有一个或多个类型变量,则可以使用泛型。泛型类型变量由尖括号界定,放在类或接口名的后面,下面定义尖括号中的T称为类型变量。意味着一个变量将被一个类型替代,替代类型变量的值将被当作参数或返回类型。对于List接口来说,当一个实例被创建以后,T将被当作一个函数的参数。下面分别是泛型类、泛型接口的定义:

  • 3.1.1,泛型类定义
//泛型类定义
class Generic<T>{
    T property;

    Generic(T property){
        this.property = property;
    }

    T getProperty(){
        return property;
    }
}

class Generic1<T,A,P>{
    T name;
    A age;
    P price;

    Generic1(T name ,A age,P price){
        this.name = name;
        this.age =age;
        this.price = price;
    }

    public A getAge() {
        return age;
    }

    public T getName() {
        return name;
    }

    public P getPrice() {
        return price;
    }
}
  • 3.1.2,泛型接口定义
//泛型接口定义
interface TInterface <T>{
    void printInfo(T info);
}

class ShowInfo<T> implements TInterface<T>{

    @Override
    public void printInfo(T info) {
        System.out.println("info = " + info);
    }
}
  • 3.1.3,泛型方法定义
//泛型方法定义
public static <T> T getGenericProperties(T pro){
        return new Generic<T>(pro).getProperty();
}

//泛型可变参数
public static <T> void printArgs(T... args){
    int i=0;
    for(T arg:args){
        System.out.println("arg"+ i++ +" = " + arg);
    }
}

3.2,泛型使用

  • 3.2.1,泛型类调用
public static void main(String args[]) {
    
    //泛型的类型实参,只能是类类型的,不能是基础类型
    //如果按下下面这种方式写的话会报错,举个例子:
    //int a= 8888;
    //Generic<Int> aInt = new Generic(a);
    //上面这种写法,编译器会报错
    Generic<String> gStr = new Generic<String>("name");
    Generic<Integer> gInt = new Generic<Integer>(888);
    Generic<Float> gFloat = new Generic<Float>(88.88f);
    
    System.out.println("gstr  = "+gStr.getProperty()+",gInt = "+gInt.getProperty()+",gFloat = "+gFloat.getProperty());
}

运行结果如下:

gstr  = name,gInt = 888,gFloat = 88.88

定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

参考如下写法:

public static void main(String args[]) {
    Generic gStr = new Generic("name");
    Generic gInt = new Generic(888);
    Generic gFloat = new Generic(88.88);
    System.out.println("gstr  = "+gStr.getProperty()+",gInt = "+gInt.getProperty()+",gFloat = "+gFloat.getProperty());

}

运行结果如下:

gstr  = name,gInt = 888,gFloat = 88.88

由此结果可知,使用泛型,能够方便的使用不同类型的数据,可以很好的提高针对不同类型数据处理时,代码的复用性。

这里说一个原始类型:

缺少实际类型变量的泛型就是一个原始类型,举个例子:

Generic gStr = new Generic("name");

这里Generic就是Generic<String>的原始类型

注意:

  1. 泛型实参不能使用基础类型,必须是类类型的
  2. 不能对确切的泛型类型使用instanceof操作,如下面编写会报错
if(gInt instanceof Generic<Integer>){
            
}

//只能写为如下方式:
if(gInt instanceof Generic){
            
}
  • 3.2.2,泛型接口调用
public static void main(String args[]) {
    ShowInfo info = new ShowInfo();
    info.printInfo(8);
    info.printInfo("xxxxxxx");
}

运行结果如下:

info = 8
info = xxxxxxx

3.2.3,泛型方法调用

public static void main(String args[]) {
    System.out.println("getGenericProperties = " + getGenericProperties(8)+","+getGenericProperties("x8x8x8"));
    printArgs(888,888.888,"dddsg");
}

运行结果如下:

getGenericProperties = 8,x8x8x8
arg0 = 888
arg1 = 888.888
arg2 = dddsg

4,泛型上界(生产者Producer),下界(消费者Consumer)

  1. 上界,通配符表示为<? extends T>,只能读取,不能写入(生产者只能读取);使用固定上边界的通配符的泛型可以接收T及其所有子类类型的数据,这里的T可以是类也可以是接口
  2. 下界,通配符表示为<? super T>,只能写入,不能读取(消费者只能写入);所有固定下边界的通配符的泛型可以接收T及其所有超类类型的数据。

这里有个小技巧记忆,我们将上面两种通配符表示为:PECS

PECS:Producer extends Consumer super

  • 4.1,泛型上界的基础用法

举个例子,我们都知道Integer是Number的子类,但是List<Integer>并不是List<Number>的子类,编译器没法知道他们的继承关系,上代码:

public static void main(String args[]) {
    List<Number> nums = new ArrayList<>();
    List<Integer> integers = new ArrayList<>();
    //Integer是Number的子类,但是List<Integer>并不是List<Number>的子类,故不能直接赋值
    //nums = integers; //此处编译器会报错
    //但是通过通配符?List<? extends Number>就可以赋值了
    List<? extends Number> numbers = new ArrayList<>();
    //? extends Number定义上界为Number,所以变量numbers可以接受Number和Number的所有子类赋值
    numbers = integers;
    List<Float> floats = new ArrayList<>();
    numbers = floats;

    //在集合中使用泛型通配符,要记得PECS规则,生产者只能读取,不能写入(除了null)
    numbers.add(null); //编译器不会报错
    numbers.add(999); //编译器会报错
    //上界通配符无法写入,但是可以正常读取
    Number num = numbers.get(0);
    //如果要获取的类型是Number的子类,则必须使用强制类型转换
    int i = (Integer)numbers.get(0);
    
}

1,通过上界通配符<? extends T>,就可以接受T以及T子类类型的数据赋值,上述例子中,Integer和Float都是Number的子类;

2,上界(生产者)不能写入,此处注意null是所有引用类型都有的元素,可以add成功,其他类型元素均不行。这是因为在给numbers调用add方法的时候,编译器无法确定具体添加的是List<Integer>、List<Long>、还是List<Float>等其他类型,这样会导致类型不一致的问题,这样就和泛型的初衷相悖了,故Java为了保证类型的一致性,禁止这样做。

换个说法可能好理解一些:子类可以赋值给父类对象,但是父类对象是无法直接赋值给子类对象的(需要强制转换);

原因也很好理解,因为子类继承自父类,自然拥有父类的所有属性,所以子类对象可以赋值给父类;但是父类对象无法直接赋值给子类对象,原因是子类对象可能会添加新的属性、方法,那么父类对象中是没有这些子类新添加的属性的,会造成数据丢失。

故上界可以应用于一些只读的场景,如果需要既能读,又能写,那么只能用确定的泛型类型,比如List<Integer>这样List就只能接受Integer类型的数据,同时也可以进行读写操作;既能读,又能写的场景就无法使用?通配符了。

3,另外考虑一个问题,假设生产者(Producer)可以写入的话,会产生以下问题:

List<? extends Object> eObject= new ArrayList<>();

如果此时有如下语句:

eObject.add(8.8); //给eObject写入一个Float类型

接着:

String aaa = (String )eObject.get(0);

先前我们放入的是Floater类型,现在读取的时候,将其强制转换为String,照样会引起ClassCastException,这样的话,泛型将毫无意义,违背了创建泛型的初衷。

同样用之前我们自己写的Generic类做个测试,代码如下:

public static void main(String args[]) {
    Generic<String> gStr = new Generic<String>("name");
    Generic<Integer> gInt = new Generic<Integer>(888);
    Generic<Float> gFloat = new Generic<Float>(88.88f);
    
    Generic<Number> gNum = new Generic<Number>(999);
    gNum = gInt; //此处编译器会报错,Generic<Number>和Generic<Integer>没有任何继承关系
    gNum = gFloat; //此处编译器会报错,Generic<Number>和Generic<Float>没有任何继承关系
    //我们可以用上界通配符,让gNum可以接受Number以及Number的子类
    Generic<? extends Number> gNum1 = new Generic<Number>(999);
    gNum1 = gInt; //可以正常赋值
    gNum1 = gFloat;//可以正常赋值
}

4.2 ,泛型下界的基础用法

下界通配符,可以接受T类以及T的所有父类的赋值,在集合中使用时,可以add所有T类以及T子类的数据,看代码:

class Base {
    String name;
    int age;

    void printProPerties(){
        System.out.println("name = "+name+",age = "+age);
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class Person extends Base {

    String addr;

    Person(){

    }

    Person(String addr,String name,int age){
        this.addr = addr;
        this.name = name;
        this.age = age;
    }

    @Override
    void printProPerties() {
        //super.printProPerties();
        System.out.println("addr = "+addr);
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    @Override
    public void setAge(int age) {
        super.setAge(age);
        System.out.println("Persion setAge");
    }

    @Override
    public void setName(String name) {
        super.setName(name);
        System.out.println("Persion setAge");
    }
}

public static void main(String args[]) {
    ArrayList<? super Person> list2 = new ArrayList<>();
    //用add方法添加的时候,只能接受Person类以及Person子类类型
    //注意在集合中使用时,只能接受下界和下界的子类型,比如调用集合的add方法,那么只能接受Person和Person的子类
    Person person = new Person();
    Base base = new Base();
    list2.add(person);
    list2.add(base);//此处会报错
    //如果要添加,只能通过强制转换,但要小心数据丢失
    list2.add((Person) base);//此处不会报错

    //消费者只能写入,不能读取
    Base base1 = list2.get(0);//此处会报错,因为消费者只能写入,不能读取
    //如果一定要读取,那么需要通过显式的强制转换,告诉编译器,具体要接收那种类型的数据
    Base base2 = (Base)list2.get(0);//此处不会报错    
    Person person1= (Person)list2.get(0);//此处不会报错

    List<Base> list3 = new ArrayList<>();
    list2 = list3;//此处会报错,要知道List<Base>并不是List<Person>的父类,注意文章开头提到的类型擦除

    //换成我们之前的Generic类
    Generic<Base> gb = new Generic(base);
    Generic<Person> gp1 = new Generic<Person>(person);
    gp1 = gb;//此处会报错,Generic<Base> 和Generic<Person>没有继承关系
    Generic<? super Person> gp2 = new Generic(person);
    gp2 = gb;//使用下界通配符,可以正常编译,不会报错,因为在此处Base是Person的父类,而下界通配符就是可以接受下界(Person)以及下界(Person)的超类赋值的。
    
}

关于下界通配符:

下界通配符可以接受下界以及任何下界的超类的赋值

但是在List中使用时候,list添加的时候只能接受下界以及下界的子类,这个跟下界在其他泛型类中的使用是不同的,这个需要注意。

总结:

  1. java中,Array是不能使用泛型的
  2. List<Object>和List<String>,Generic<Base>和Generic<Person>并没有继承关系,他们是两种完全不同的类类型,所以以下两种赋值是会编译报错的:

    ArrayList<String> arrayList1=new ArrayList<Object>();

    ArrayList<Object> arrayList1=new ArrayList<String>();

  3. List<Object>和List的区别:原始类型List和带参数类型List<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该类对象可以接受任何类型的对象,比如String或Integer。这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,比如List list = new List<String>();是可以编译通过的,但却不能把List<String>传递给List<Object>类型的对象,会产生编译错误,因为List<Object>和List<String>没有继承关系。

  4. List<?>和List<Object>有什么不同:List<?> 是一个未知类型的List,而List<Object>其实是任意类型的List。你可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给List<Object>

  5. List<String>和List之间的区别:该题类似于“原始类型和带参数类型之间有什么区别”。带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List却不是类型安全的。你不能把String之外的任何其它类型的对象存入String类型的List中,而你可以把任何类型的对象存入原始List中。使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。

    List listOfRawTypes = new ArrayList();

    listOfRawTypes.add("abc");

    listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常

    String item = (String) listOfRawTypes.get(0); //需要显式的类型转换

    item = (String) listOfRawTypes.get(1); //抛ClassCastException,因为Integer不能被转换为String

         

    List<String> listOfString = new ArrayList();

    listOfString.add("abcd");

    listOfString.add(1234); //编译错误,比在运行时抛异常要好

    item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换

  6. Java中泛型是什么?使用泛型有什么好处?

    泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。

    泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException

  7. Java的泛型是如何工作的 ? 什么是类型擦除 ?

    泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。

    编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。伪泛型,虚拟机不支持

  8. 什么是泛型中的限定通配符和非限定通配符 ?

    限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。

    另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。

  9. 泛型类型变量不能是基本的数据类型,如:int,float等。

  10. 泛型类型不能用在instanceof语句中做判断。

  11. 泛型在静态方法和静态类中的问题

    public class Test2<T> {   

        public static T one;  //编译报错

        public static  T show(T one){  //编译报错

            return null;   

        }   

     

      public static <T >T show1(T one){//可以正常编译

            return null;   

        }  

    } ,泛型变量不能声明为静态变量,泛型变量不能在普通静态方法中使用,因为静态变量和静态方法是属于Class的,所有该类型的对象都可以直接使用,如果此时声明Test2<String> tStr = new Test2<String>();Test2<Integer> tInt = new Test2<Integer>();由于方法和变量是静态的,在还没有声明这两个变量之前,就可以直接使用Test2.show调用,此时编译器并不知道show方法的参数到底是String还是Integer会导致运行错误;但是可以在静态的泛型方法中使用,因为show1是被声明为泛型方法的,此处的T和类Test2<T>的T是没有任何关系的,在调用show1方法的时候就必须指明具体的调用类型,如Test2.show1(888);那么JVM就会自动推断出此时的show1方法中的泛型类型T为Integer类型。

到此,Java泛型基本就结束了。

转载请注明出处。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值