请选择 进入手机版 | 继续访问电脑版
查看: 115|回复: 0

这条目录1.初识泛型

[复制链接]

132

主题

134

帖子

713

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
713
发表于 2022-10-26 02:59:03 | 显示全部楼层 |阅读模式
当你觉得路难走时,那一定是上坡路



目录

1. 泛型的初识 1.1 什么是泛型

《Java 编程思想》一书中对泛型的介绍中有一段话:通用类和方法只能使用特定类型:要么是基本类型,要么是自定义类。如果你想写出适用于多种类型代码的代码,这种严格的限制可能对代码有很大的限制

以下代码:

<p><pre>    <code class="language-java">class Array {
    public int[] arr = new int[10];
    public void setArr(int val,int pos) {
        this.arr[pos] = val;
    }
    public int getArr(int pos) {
        return this.arr[pos];
    }
}
public class Test {
    public static void main(String[] args) {
        Array array = new Array();
        array.setArr(1,0);
        int ret = array.getArr(0);
        System.out.println(ret);
    }
}</code></pre></p>
一般在方法的形参和返回值中使用特定的类型,限制了代码。例如:我们实例化一个 Array 类型的对象。当我们使用这个对象调用setArr方法来设置数组的值时,因为形参是整数,所以只能传入整数的实参。当我们使用这个对象调用getArr方法来获取数组中某个位置的值,因为返回值是一个整数,所以只能返回一个整数值,那么这种刚性限制对代码的限制会很大。

JDK 1.5 中引入了泛型来防止上述限制。

Generic 是一种参数化类型。说到参数,大家都会想到方法的形参。当我们要调用一个方法时,我们需要传入实际参数。什么是参数化类型?顾名思义,形参的类型是根据实参的类型来确定的。

那么我们现在需要在上面的代码数组中存储任意类型的数据,那么我们需要如何修改代码呢?

思路:所有类的父类,默认是Object类。所以类型有包装类,那么它们的包装类都继承了Object类,那么我们可以将数组设置为Object类。所以所有子类都可以被父类接收,那么我们将数组设置为Object类型后就可以接收任意类型的值了

<p><pre>    <code class="language-java">class Array {
    public Object[] arr = new Object[10];
    public void setArr(Object val,int pos) {
        this.arr[pos] = val;
    }
    public Object getArr(int pos) {
        return this.arr[pos];
    }
}
public class Test {
    public static void main(String[] args) {
        Array array = new Array();
        array.setArr(1,0);
        array.setArr("obj",1);
        //int ret = (int)array.getArr(0);
    }
}</code></pre></p>
将数组设置为Object类型后,就可以将不同类型的值放在数组的不同位置,但是当我们获取到数组中指定位置的值时,就需要对类型进行强制转换。在大多数情况下,我们想要的是它只能保存一种数据类型,而不是同时保存这么多类型。泛型的主要目的是指定当前容器,保存什么类型的对象,并让编译器检查。此时,您需要将类型作为参数传递。需要什么类型,传入什么类型即可。

1.2 泛型类语法 1.2.1 泛型类定义

<p><pre>    <code class="language-java">class 泛型类名称<类型形参列表> {
}</code></pre></p>
<p><pre>    <code class="language-java">class Array<T> {


  
}</code></pre></p>
注意:一个泛型类型参数列表可以指定多种类型

类型参数一般用大写字母表示,类型参数命名为:

1.2.2 泛型类使用语法

<p><pre>    <code class="language-java">泛型类<类型实参> 变量名 = new 泛型类<类型实参>(构造方法实参); </code></pre></p>
泛型类变量名:定义泛型类引用

新的泛型类(构造函数参数):实例化一个泛型类对象

<p><pre>    <code class="language-java">Array<Integer> array = new Array<Integer>();</code></pre></p>
类型扣除:

<p><pre>    <code class="language-java">Array<Integer> array = new Array<>();</code></pre></p>
当编译器可以根据上下文推导出类型实参时,类型实参的填充可以省略

1.2.3 泛型类的使用

<p><pre>    <code class="language-java">class Array<T> {
    public T[] arr = (T[])(new Object[10]);
    public void setArr(T val,int pos) {
        this.arr[pos] = val;
    }
    public T getArr(int pos) {
        return this.arr[pos];
    }
}
public class Test {
    public static void main(String[] args) {
        Array<Integer> array = new Array<Integer>();
        array.setArr(1,0);
        array.setArr(1,1);
        Integer ret = array.getArr(0);
        System.out.println(ret);
    }
}</code></pre></p>
泛型存在的意义:

1.2.4 裸类型

裸类型:定义和实例化泛型类时不提供类型参数

<p><pre>    <code class="language-java">泛型类 变量名 = new 泛型类(构造方法实参); </code></pre></p>
<p><pre>    <code class="language-java">Array array = new Array();</code></pre></p>
裸类型存在的意义:是为了兼容老版本API保存的机制

2.泛型是如何编译的 2.1 擦除机制

<p><pre>    <code class="language-java">class Array<T> {
    public T[] arr = (T[])(new Object[10]);
    public void setArr(T val,int pos) {
        this.arr[pos] = val;
    }
    public T getArr(int pos) {
        return this.arr[pos];
    }
}</code></pre></p>
接下来我们通过DOS窗口查看上述泛型类的字节码文件:



我们使用命令 javap -c 在 Dos 窗口中查看字节码文件。此时,所有的 T 都是 Object。那么在编译过程中,将所有T替换为Object的机制称为擦除机制。

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

问题:为什么我们不能新建一个泛型类型数组?

答:T[] arr = new T[],编译过程中新的T不会被擦除到Object。Java规定擦除机制只针对变量的类型和返回值的返回类型,不会擦除实例化时的泛型类型除外,所以当创建新的T数组时,不会擦除T入Object,则编译器不识别T,编译时编译器会报错。

3.泛型的上限

泛型的上限:定义泛型时,有时需要对传入的类型做一个约束。在这种情况下,可以使用泛型的上限对其进行约束。例如:我们要传递的类型是继承School类,那么我们可以使用泛型的上界对其进行约束。如果传入的类型没有继承School类,会报错

3.1 语法

<p><pre>    <code class="language-java">class 泛型类名称<类型形参 extends 类型上界> {
   
}</code></pre></p>
3.2 演示

<p><pre>    <code class="language-java">class Student<T extends School> {
}</code></pre></p>
此时只接受 School 的子类型作为 T 的类型参数,在编译时擦除时会作为其父类擦除,即作为 School 擦除

注意:如果没有指定泛型的上界,则默认为T extends Object

<p><pre>    <code class="language-java">public class MyArray<E extends Comparable<E>> {
}</code></pre></p>
此时传递的实际参数类型必须实现Comparable接口

4. 泛型方法 4.1 语法

<p><pre>    <code class="language-java">方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) {
        
}</code></pre></p>
4.2 例子

<p><pre>    <code class="language-java">public class Test {
    public static <T> void swap(T[] array,int i,int j) {
        T t = array;
        array = array[j];
        array[j] = t;
    }
}</code></pre></p>
4.2.1 使用示例——可以类型推演

<p><pre>    <code class="language-java">public static void main(String[] args) {
        Integer[] arr = {1,2,3,4,5,6};
        swap(arr,0,1);
    }</code></pre></p>
当编译器可以根据上下文推导出类型实参时,类型实参的填充可以省略

4.2.2 使用示例——不带类型推导

<p><pre>    <code class="language-java">public static void main(String[] args) {
        Integer[] arr = {1,2,3,4,5,6};
        Test.<Integer>swap(arr,0,1);
    }</code></pre></p>
手动将类型参数传递给类型参数

注意:将类型传递给泛型时,必须传递类,不能传递基本数据类型,但可以传递它们的包装类

5. 通配符

通配符:?用于泛型,这个?是通配符

5.1 通配符的作用

<p><pre>    <code class="language-java">class Array<T> {
    public T[] arr = (T[])(new Object[10]);
    public void setArr(T val,int pos) {
        this.arr[pos] = val;
    }
    public T getArr(int pos) {
        return this.arr[pos];
    }
}
public class Test {
    public static void main(String[] args) {
        Array<Integer> array = new Array<Integer>();
        fun(array);
    }
    public static void fun(Array<Integer> arr) {
        arr.setArr(1,0);
        System.out.println(arr.getArr(0));
    }
}</code></pre></p>
在main函数中实例化了一个Array泛型类,传入的类型是Integer。在 fun 方法的形参中只能接收 Array 类型的 Integer 类型的对象。调用 fun 方法时,可以使用类型推导。如果传入不是Array类型的Integer类型的对象,会报错。Array 是一个泛型类。实例化时可以传入不同类型的参数,所以 fun 有一定的局限性。

然后我们可以将fun方法的形参中Array中的类型设置为通配符,这样就可以接收Array类的任何类型的对象。

<p><pre>    <code class="language-java">class Array<T> {
    public T[] arr = (T[])(new Object[10]);
    public void setArr(T val,int pos) {
        this.arr[pos] = val;
    }
    public T getArr(int pos) {
        return this.arr[pos];
    }
}
public class Test {
    public static void main(String[] args) {
        Array<Integer> array = new Array<Integer>();
        array.setArr(1,0);
        fun(array);
    }
    public static void fun(Array<?> arr) {
        System.out.println(arr.getArr(0));
    }
}</code></pre></p>
使用通配符“?” 表示可以接收任意类型的Array对象,但由于类型不确定,不能修改

5.2 子通配符

在通配符的基础上,生成两个子通配符:

5.2.1 通配符上限

① 语法

<p><pre>    <code class="language-java"><? extends 上界></code></pre></p>
<p><pre>    <code class="language-java"><? extends Number></code></pre></p>
传入的参数类型必须是 Number 本身或 Number 的子类

②示例

<p><pre>    <code class="language-java">class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
    private T message;
    public void setMessage(T message) {
        this.message = message;
    }
    public T getMessage() {
        return message;
    }
}
public class Main {
    public static void main(String[] args) {
        Message<Apple> message = new Message<>();
        message.setMessage(new Apple());
        fun(message);
    }
    public static void fun(Message<? extends Fruits> tmp) {
        //tmp.setMessage(new Apple());
        Fruits fruits = tmp.getMessage();
    }
}</code></pre></p>
fun的形参类型传递的实参类型必须是Fruits类型或者Fruits的子类。如果我们使用通配符来接收 fun 中的类型,那么我们不知道具体的类型。我们只知道是 Fruits 类型或者 Fruits 子类类型,那么就不能调用 setMessage 来设置消息的值,因为子类类型不能存储父类对象。我们知道main函数传递给fun方法的对象类型要么是Fruits类型,要么是Fruits子类类型,那么我们可以调用getMessage获取消息的值,并用Fruits接收。

通配符的上限,不能写数据,只能读数据

5.2.2 通配符的下界

① 语法

<p><pre>    <code class="language-java"><? super 下界></code></pre></p>
<p><pre>    <code class="language-java"><? super Integer></code></pre></p>
传入的类型必须是Integer自己的类或者Integer的父类

②示例

<p><pre>    <code class="language-java">class Fruits {
}
class Apple extends Fruits {
}
class Message<T> {
    private T message;
    public void setMessage(T message) {
        this.message = message;
    }
    public T getMessage() {
        return message;
    }
}
public class Main {
    public static void main(String[] args) {
        Message<Fruits> message = new Message<>();
        message.setMessage(new Fruits());
        fun(message);
    }
    public static void fun(Message<? super Fruits> tmp) {
        tmp.setMessage(new Apple());
        //Fruits fruits = tmp.getMessage();
    }
}</code></pre></p>
fun 参数类型传递的实际参数类型必须是 Fruits 类型或者 Fruits 类型的父类,那么当我们通过 fun 方法中的 setMessage 设置消息的值时,我们就知道 main 传递的对象类型fun方法的功能是最低的,一定是Fruits方法。通过setMessage来设置消息值,我们可以将其设置为Fruits子类对象,因为Fruits子类肯定也是Fruits父类的子类,所以可以设置消息的值。但是我们无法通过getMessge获取message的值。原因是我们只知道main函数传递给fun的实际参数类型是Fruits类型或者Fruits类型的父类。如果传递的实参类型是 Fruits 父类泛目录解析

通配符的下界,不能读数据,只能写数据

6.包装类

在 Java 中,基本类型不继承 Object 类。为了支持泛型代码中的基本类型,Java 为每个基本类型对应一个包装类型。

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

字节

字节





整数

整数





漂浮

漂浮

双倍的

双倍的

字符

特点

布尔值

布尔值

6.2 装箱和拆箱 6.2.1 装箱

<p><pre>    <code class="language-java">int a = 10;
Integer i = Integer.valueOf(a);</code></pre></p>


Integer.valueof:主要将一个基本数据类型的值存入Integer类的对象中

上述代码装箱操作新建了一个Integer类型的对象泛目录解析,并将i的值放入对象的一个​​属性中

在Integer类的valueOf方法中,如果你存储的值在-128(IntegerCache.low: -128)到127(IntegerCache: 127)的范围内,则存储在数组i+(-整数缓存.low)。如果超过这个范围,则创建一个新的 Integer 对象,并通过构造方法存储在 value 属性中

6.2.2 开箱

<p><pre>    <code class="language-java">int a = 10;
Integer i = Integer.valueOf(a);//装箱
int b = i.intValue();//拆箱</code></pre></p>
拆箱操作从 Integer 对象中取值并将其放入基本数据类型

6.2.3 自动装箱和自动拆箱

从上面的手动打包和解包可以看出,手动打包和解包给开发者带来了很多代码。为了减轻开发者的负担,Java提供了自动打包和解包机制。

<p><pre>    <code class="language-java">public class Demo {
    public static void main(String[] args) {
        int i = 10;
        Integer ii = i; // 自动装箱
        Integer ij = (Integer)i; // 自动装箱
        int j = ii; // 自动拆箱
        int k = (int)ii; // 自动拆箱
    }
}</code></pre></p>


从字节码文件可以看出,装箱和拆箱会在编译时自动进行,恢复为装箱和拆箱

豪侠泛目录站群程序,专业泛目录,站群,二级目录,泛站群程序!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表