Java 泛型的介绍

1.学习目标

1.以能阅读 java 集合源码为目标学习泛型
2.了解泛型
3.了解通配符

2.什么是泛型


先来看下面的代码,如果要给方法传参,传递的参数必须是符合参数类型的。

public class Test {
    
    

    public static void func (int a) {
    
    

    }

    public static void main(String[] args) {
    
    
        func(10);
    }
}


因为上述代码中的 func 方法的参数是 int 类型,此时调用方法时传递的参数就必须是 int 类型的。

如果是其他类型的这里就会报错。



通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化;简单来说就是在给方法传参时,可以将一些不同类型的参数一起传递过去。

3.引入泛型


解决下面的问题:

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

此时如果将这个数据定义为具体的某一个类型,那么它就只能传递具体的某一个参数类型。
比方说,如果是一个 int 的数组,它就只能传递 int 类型的参数;如果是一个 char 类型的参数,那么它就只能传递 char 类型的参数…

解决办法就是,将这个数组定义为 Object 数组 即可,这是因为 Object 是所有类的父类,也就是所有都继承这个类,如下代码片段所示。

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

    // 给数组添加元素
    public void setVal (int pos, Object val) {
    
    
        objects[pos] = val;
    }

    // 返回数组中某个下标的值
    public Object getVal (int pos) {
    
    
        return objects[pos];
    }
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        MyArray myArray = new MyArray();
        // 调用方法给数组的 0 下标添加一个int类型的 10 元素
        myArray.setVal(0, 10);
        // 给数组 1 下标添加一个 String 类型的 “hello” 元素
        myArray.setVal(1, "hello");
    }
}


按照上述代码中调用 setVal 方法就可以给数组添加元素。


在调用 getVal 方法返回某个下标的的值时代码报错了,鉴于上述情况可以得出结论:

1、任何类型的数据都可以存放。
2、1号下标本身就是字符串,但是取数据的时候编译却报错了。此时必须进行强制类型转换



可以看到强转后就不报错了,按照上述的情况,必须要看看下标元素是什么类型后再取出,
这就比较不合适了。

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


接下来修改代码使用 泛型 来解决这个问题。

class MyArrays<T> {
    
    
    public T[] objects = (T[])new Object[10];

    // 给数组添加元素
    public void setVal (int pos, T val) {
    
    
        objects[pos] = val;
    }

    // 返回数组中某个下标的值
    public T getVal (int pos) {
    
    
        return objects[pos];
    }
}

public class Work {
    
    

    public static void main(String[] args) {
    
    
        // 此时指定的是 Integer 类型,只能传递整数
        MyArrays<Integer> myArrays = new MyArrays<Integer>();
        myArrays.setVal(0, 10);
        myArrays.setVal(1, 20);
    }
}


< T > : 相当于是一个占位符,表示当前类是一个泛型类

括号里的字母不一定是 T,也可以别的,但是我们习惯括号里的字母可以将我们想要表达的意思表示出来,T 和 E 一般用来表示类型。

main 函数里的 < Intrger>表示当前只能传递 Integer 类型的参数,也就是只能传递整数。

如果此时要传递 Integer 类型以外的参数。


可以看到代码此时会报错。

如果要取出指定下标元素的值定义一个变量接收方法返回的值即可。

 int ret = myArrays.getVal(1);



根据输出的结果可以判断出的确得到了下标为 1 的元素的值。


如果想要传递其他类型的参数,更改 <> 中的类型即可。

public static void main(String[] args) {
    
    
    MyArrays<String> myArrays1 = new MyArrays<String>();
    myArrays1.setVal(2, "hello");
    myArrays1.setVal(3, "world");
    String retString = myArrays1.getVal(2);
    System.out.println(retString);
}


可以看到得到的结果也是正确的。

这就是泛型!!!


泛型存在的两个最大的意义:

1、存放元素的时候,会进行类型的检查。

2、取出元素的时候,会自动进行类型转换,不在需要类型的强转了。

泛型只要是编译时期的一种机制,这种机制叫做 擦除机制,而运行的时候是没有泛型概念的。

有关泛型擦除机制的文章介绍:擦除机制的文章介绍

语法

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

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

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

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


一个泛型类的参数列表可以指定多个类型,也就是 <> 里可以有多个字母,

如下图所示:

4.泛型类的使用

语法

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

示例

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


注意:泛型只能接受类,所有的基本数据类型必须使用包装类!,也就是<> 里只能是 类类型,不能是基本类型。

6.泛型的上界


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

语法

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

示例

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


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

MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型


实现一个泛型类,求一个数组中的最大值

class MaxVal<T extends Comparable<T>> {
    
    
    public T findMax (T[] array) {
    
    
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
    
    
            if(max.compareTo(array[i]) < 0 ) {
    
    
                max = array[i];
            }
        }
        return max;
    }
}

public class MaxValue {
    
    

    public static void main(String[] args) {
    
    
        MaxVal<Integer> maxVal = new MaxVal<>();
        Integer[] array = {
    
    1,2,3,4,5};
        Integer ret = maxVal.findMax(array);
        System.out.println(ret);
    }
}



可以输出结果正确。

7.泛型的方法

定义语法

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

示例

public class Util {
    
    
    //静态的泛型方法 需要在static后用<>声明泛型类型参数
    public static <E> void swap(E[] array, int i, int j) {
    
    
        E t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
}

8.通配符


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

通配符解决什么问题


通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student 是 Person 的子类,那么 List 也应该是 List 的子类,但是泛型是不支持这样的父子类关系的。

泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。

class Message<T> {
    
    
    private T message ;
    public T getMessage() {
    
    
        return message;
    }
    public void setMessage(T message) {
    
    
        this.message = message;
    }
}

public class Test3 {
    
    
    public static void main(String[] args) {
    
    
        Message<String> message = new Message() ;
        message.setMessage("中土世界欢迎您");
        fun(message);
    }
    public static void fun(Message<String> temp){
    
    
        System.out.println(temp.getMessage());
    }
}



以上是程序的输出结果。


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


可以看到程序会报错,因为 fun 方法被要求是 String 类型的。

解决办法就是引入 通配符 的概念,直接在代码中写入 ? 即可。

class Message<T> {
    
    
    private T message ;
    public T getMessage() {
    
    
        return message;
    }
    public void setMessage(T message) {
    
    
        this.message = message;
    }
}
public class Test3 {
    
    

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


可以看到改写好的代码,就没有报错了。

此时的问号表示的含义是程序也不知道此时的类型是什么类型。


此时就得到了正确的结果。

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

? extends 类:设置泛型上限
? super 类:设置泛型下限

通配符上界

语法

<? extends 上界>
<? extends Number>//可以传入的实参类型是Number或者Number的子类


示例

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) {
    
    
        // 可以看到p1 与 p2 调用 fun 方法后都是正确的
        Plate<Apple> plate1 = new Plate<>();
        plate1.setPlate(new Apple());
        fun(plate1);

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

    // 此时继承的是 Fruit 类,表名 fun 方法可以调用 Fruit、Apple or Banana
    public static void fun(Plate<? extends Fruit> temp){
    
    

    }
}


根据上面代码可以看到,plate1 设置为只能放 Apple,而 plate2 设置为了只能放 Banana。
实际上 fun 继承了 Fruit 类,在调用方法的时候就决定了可以是 Fruit、Apple、Banana其中一个。
因为 Apple 与 Banana 是 Fruit 的子类。

此时通过 fun 方法是不能往里面放元素的。


站在 fun 方法的角度考虑,此时他有可能接收的 Apple 也有可能接收的是 Banana,因此也就不可以往里面放元素。

取元素的时候是没问题的,但是放元素的时候是有问题的。因为取出的元素一定是 Fruit 或者它的子类,但是放进去的是什么就不能确定了。就好像可以说苹果是水果,但是不能说水果是苹果一样。

 public static void fun(Plate<? extends Fruit> temp){
    
    
     Fruit fruit = temp.getPlate();
 }


需要注意的是在这里的代码只能用 Fruit 来引用,还是因为具体是什么类型是不确定的。


可以看到是错的,因此通配符的上界,不能进行写入数据,只能进行读取数据

通配符下界

语法

<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型

示例

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 fun(Plate<? super Fruit> temp){
    
    
        temp.setPlate(new Apple());
        temp.setPlate(new Banana());
        temp.setPlate(new Fruit());
    }
    
    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);   
    }
}


下界和上界正好是相反的,可以放元素但是不可以取元素。因为不能确定这里存放的是什么。


可以看到取出的时候报错了。

下界这里方法调用的时候只能传递 Fruit 或者 它的父类作为参数。


可以看到传递 Fruit 的子类是出错了。

9.包装类


在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

基本数据类型和对应的包装类



需要注意的是除了圈出的两个,其他的包装类都是首字母大写。

装箱和拆箱

int i = 10;

// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);

// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();

自动装箱和自动拆箱

// 自动拆箱和装箱
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱
int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱

猜你喜欢

转载自blog.csdn.net/m0_63033419/article/details/130916247