泛型与通配符

目录

1、什么是泛型

2、引出泛型

 2.1泛型的语法

3、泛型类的使用

3.1语法

3.2示例

3.3类型推导(Type Interface)

4.裸类型(Raw Type)

4.1说明

 5、泛型是如何编译的

 5.1擦除机制

5.2为什么不能实例化泛型类型数组

6、泛型的上界 

6.1 语法

 6.2 示例

6.3 复杂示例

7、泛型方法

7.1 定义语法

7.2 示例

7.3 使用示例 - 可以类型推导

7.4 使用示例 - 不使用类型推导

 8、通配符

8.1 通配符能解决什么问题

8.2 通配符上界

8.3 通配符下界


1、什么是泛型

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。------来源《Java编程思想》对泛型的介绍。

泛型是在 JDK1.5 引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。

2、引出泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

思路:

1.我们以前学过的数组,只能存放指定类型的元素,例如:

    int[] nums = new int[10];
    String[] str = new String[10];

int[] 只能存放整形,而 String[] 只能存放字符串

2.所有类的父类,默认为 Object 类。数组是否可以创建为 Object 呢?

代码示例:

class MyArray {
    public Object[] array = new Object[10];

    public void setVal(int pos, Object val){
        this.array[pos] = val;
    }

    public Object getPos(int pos) {
        return this.array[pos];
    }
}

public class Test2 {
    public static void main(String[] args) {

        MyArray myArray = new MyArray();
        myArray.setVal(0, 1);
        myArray.setVal(1, "hello");

        String str = myArray.getPos(1); //编译报错
        System.out.println(str);
    }
}

 可以看到用 Object 类型创建的数组,可以存放整型,也可以存放字符型,它不仅仅可以存放这两种类型,实际上它可以存放任何类型,只要这个类继承了 Object(自定义的类型也可以存放)

问题: 既然任何类型都可以存放,但是为什么会编译报错呢?

因为这样写编译器只会认为你要存放的数据类型是Object,但是当你要取出查看这个元素时,编译器并不知道你要取元素的具体类型,所有这里你要明确指出具体类型,也就是需要强制类型转换,告诉编译器你要取元素的具体类型是什么。

虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,还是希望他只能持有一种数据类型,而不是同时持有很多种类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型作为参数传递,需要什么类型,就传入什么类型。

 2.1泛型的语法

伪代码:

class 泛型类名称<类型形参列表> {
    //这里可以使用类型形参
}

class ClassName<T1,T2,...,Tn> {
}

class 泛型类名称<类型形参列表> extends 继承类/*这里可以使用类型参数*/ {
    //这里可以使用类型形参
}

class ClassName<T1,T2,...,Tn> extends ParentClass<T1> {
    //可以只使用部分类型形参
}

上述伪代码进行改写后:

class MyArray<T> {
    public T[] array = (T[]) new Object[10];// 1

    public void setVal(int pos, T val){
        this.array[pos] = val;
    }

    public T getPos(int pos) {
        return this.array[pos];
    }
}

public class Test2 {
    public static void main(String[] args) {

        MyArray<Integer> myArray = new MyArray<>(); // 2
        myArray.setVal(0, 1);
        myArray.setVal(1, 10);
        int ret = myArray.getPos(1); // 3
        System.out.println(ret);
        myArray.setVal(2,"hello");//  4  编译报错  

    }
}

 代码解释:

        1.类名后的 <T> 代表占位符,表示当前类是一个泛型类

                【规范】类型参数一般使用一个大写字母表示,常用的名称有:

                               E 表示 Element

                               K 表示 Key

                               V 表示 Value

                               N 表示 Number

                               T 表示 Type

        2.注释 1 处,不能使用 new 关键字创建泛型类型的数组

                T[] t = new T[5]; // 这是不对的

        3.注释 2 处,类型后加入 <Integer> 指定当前类型

        4.注释 3 处,不需要进行强制类型转换 

        5. 注释 4 处,代码编译报错,因为之前在注释 2 处指定了类当前的类型,此时编译器会在存放元素的时候帮我们进行类型检查。

3、泛型类的使用

3.1语法

泛型类<泛型实参> 变量名;// 定义一个泛型类引用

new 泛型类<类型实参>(构造方法实参); //实例化一个泛型类对象

3.2示例

MyArray<Integer> array = new MyArray<Integer>();

注意:泛型只能接受类,所有的基本数据类型必须使用包装类!!!

3.3类型推导(Type Interface)

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

MyArray<Integer> array = new MyArray<>();

这里可以推导出实例化需要的类型实参为 Integer ,所以后面的 Integer 可以省略

4.裸类型(Raw Type)

4.1说明

裸类型是一个泛型类但没有带着类型实参,例如 MyArray 就是一个裸类型

MyArray array = new MyArray();

 注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

总结:

       1.泛型是将数据类型参数化,进行传递

       2.使用 <T> 表示当前类是一个泛型类

       3.泛型的优点:数据类型参数化,编译时自动进行类型检查和转换

 5、泛型是如何编译的

 5.1擦除机制

泛型到底是怎么编译的呢?这个问题,也是曾经的一个面试题。泛型本质是一个比较难的语法,所以要理解好还是需要一定的时间的。

下面我们通过反汇编来看一个泛型到底是怎么编译的。

打开终端

通过命令:javap -c    查看字节码文件,所有的 T 都是 Object

在编译的过程中,将所有的泛型类型形参 T 替换为 Object 这种机制,称为:擦除机制

Java的泛型机制是在编译级别实现的。编译器生成的字节码文件在运行期间并不包含泛型的类型信息。

问题:

        1.编译的时候会替换为 Object ,那么为什么 T[] t = new T[10];  是不对的呢?

这不就相当于 Object[] t = new Object[10] 吗?

        2.类型擦除,一定是把 T 变成 Object 吗?

5.2为什么不能实例化泛型类型数组

代码示例:

class MyArray<T> {
    public T[] array = (T[]) new Object[10];

    public void setVal(int pos, T val){
        this.array[pos] = val;
    }

    public T getPos(int pos) {
        return this.array[pos];
    }

    public T[] getArray() {
        return array;
    }
}

public class Test2 {

    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();

        Integer[] num = myArray.getArray();
    }
/*
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    at test.Test2.main(Test2.java:27)
*/
}

可以看到当运行程序时,会报异常

原因:替换后的方法:将 Object[] 分配给 Integer[] 引用,程序报错 

public Object[] getArray() {

    return array;

通俗的讲就是:返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String ,可能是自定义类型,运行的时候,直接传给 Integer 类型的数组,编辑器认为是不安全的。

正确的方式:

     通过反射创建泛型类型数组

import java.lang.reflect.Array;

class MyArray<T> {
    public T[] array;

    public MyArray() {
    }

    public MyArray(Class<T> clazz, int size) {
        array = (T[]) Array.newInstance(clazz,size);
    }

    public T getPos(int pos) {
        return this.array[pos];
    }

    public void setVal(int pos, T val) {
        this.array[pos] = val;
    }

    public T[] getArray() {
        return array;
    }
}
public class Test2 {

    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>(Integer.class, 10);

        Integer[] num = myArray.getArray();
    }
}

6、泛型的上界 

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

6.1 语法

class 泛型类名称<类型形参 extends 类型边界> {

        ......

}

 6.2 示例

public class MyArray<E extends Number> {
    ...
}

只接受 Number 的子类型作为 E 的类型实参 

MyArray<Integer> array1; // 1
MyArray<String> array2; // 2

      java: 类型参数 java.lang.String 不在类型变量T的范围内 

注释 1 处:正常,因为 IntegerNumber 的子类型 

注释 2 处:正常,因为 String 不是 Number 的子类型

6.3 复杂示例

public class MyArray1<E extends Comparable<E>> {
    ...
}

E 必须是实现了 Comparable 接口的 

7、泛型方法

7.1 定义语法

方法限定符<类型形参列表> 返回值类型 方法名称(形参列表){ ... } 

7.2 示例

                        静态的泛型方法需要在 static 后用 <> 声明泛型类型参数 

public class Method {
    public static <E> void swap(E[] a, int i, int j) {
        E temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

        

7.3 使用示例 - 可以类型推导

Integer[] a = { ... };
swap(a, 0, 9);

String[] b = { ... };
swap(b, 0, 9);

7.4 使用示例 - 不使用类型推导

Integer[] a = { ... };
Method.<Integer>swap(a, 0, 9);

String[] b = { ... };
Method.<Integer>swap(b, 0, 9);

 8、通配符

? 用于在泛型的使用,即为通配符

8.1 通配符能解决什么问题

示例:

class Message<T> {
    private T message ;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }

}

public class TestDemo {

    public static void main(String[] args) {

        Message<String> message = new Message<>() ;
        message.setMessage("hello demo");
        fun(message);

    }
    public static void fun(Message<String> temp) {
        System.out.println(temp.getMessage());
    }
}

以上程序会带来新的问题,如果现在泛型的类型设置的不是 String 而是 Integer

public class TestDemo {

    public static void main(String[] args) {

        Message<String> message = new Message<>() ;
        message.setMessage(10); //编译报错,不能接收Integer,只能接收String
        fun(message); 

    }
    public static void fun(Message<String> temp) {
        System.out.println(temp.getMessage());
    }
}

我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改,这种情况就需要使用通配符来处理。

使用通配符示例:

public class TestDemo {

    public static void main(String[] args) {
        Message<Integer> message = new Message<>();
        message.setMessage(10);
        fun(message);
    }

    
    //此时使用通配符 ? 描述它可以接收任何类型,但是由于不确定类型,所以无法修改
    public static void fun(Message<?> meg) {
        // msg.setMessage(10); 无法修改!
        System.out.println(meg.getMessage());
    }
}

 在 通配符 ? 的基础上又产生了两个子通配符

? extends 类 : 设置通配符上界

? super 类 : 设置通配符下界

8.2 通配符上界

语法:

<? extends 上界>

<? extends Fruit> // 可以传入的实参类型:Fruit 或者 Fruit 的子类

代码示例:

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Plate<T> { //设置泛型
    private T plate;

    public T getPlate() {
        return plate;
    }

    public void setPlate(T plate) {
        this.plate= plate;
    }

}

public class TestDemo {

    public static void main(String[] args) {
        Plate<Apple> plate1 = new Plate<>();
        plate1.setPlate(new Apple());
        fun(plate1);

        Plate<Banana> plate2 = new Plate<>();
        plate2 .setPlate(new Banana());
        fun(plate2);
    }


    //此时使用通配符 ? 描述它可以接收任何类型,但是由于不确定类型,所以无法修改
    public static void fun(Plate<? extends Fruit> plate) {
        //plate.setPlate(new Apple()); //无法修改!
        //plate.setPlate(new Banana()); //无法修改!
        System.out.println(plate.getPlate());
    }
}

此时无法再 fun 函数中对 plate 进行添加元素,因为 plate 接收的是 Fruit 和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素

    public static void fun(Plate<? extends Fruit> plate) {
        //plate.setPlate(new Apple()); //无法修改!
        //plate.setPlate(new Banana()); //无法修改!
        Fruit f = plate.getPlate();
        System.out.println(f);
    }

结论:通配符的上界,不能进行写入数据,只能进行读取数据

8.3 通配符下界

语法: 

<? super 下界>

<? super Fruit> // 可以传入的实参类型:Fruit 或者 Fruit 的父类

代码示例: 

class Food {
}
class Fruit extends Food {
}
class Apple extends Fruit {
}
class Banana extends Fruit {
}
class Plate<T> { //设置泛型
    private T plate ;

    public T getPlate() {
        return plate;
    }

    public void setPlate(T plate) {
        this.plate = plate;
    }

}

public class TestDemo {

    public static void main(String[] args) {
        Plate<Fruit> plate1 = new Plate<>();
        plate1.setPlate(new Fruit());
        fun(plate1);

        Plate<Food> plate2 = new Plate<>();
        plate2.setPlate(new Food());
        fun(plate2);
    }

    public static void fun(Plate<? super Fruit> plate) {
        //此时可以修改  添加的是Fruit 或者 Fruit 的子类
        plate.setPlate(new Fruit()); //这个是 Fruit 的本身 
        plate.setPlate(new Banana()); //这个是 Fruit 的子类
        
        //Fruit f = plate.getPlate(); 不能接收,这里无法确定是哪个父类

        System.out.println(plate.getPlate()); // 只能直接输出
    }
}

结论:通配符的下界,不能进行读取数据,只能写入数据 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值