Java - 泛型

news2024/11/25 5:47:55

一、什么是泛型?

一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的
代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。
泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化

这一节比较难,主要是以看得懂源码为主。

二、引出泛型

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

在这里插入图片描述
所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

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];

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

    public T get(int pos){
        return array[pos];
    }
}

public class TestDome {
    public static void main(String[] args) {
        MyArray<String> myArray = new MyArray<String>();
        myArray.set(0,"asde");
        myArray.set(1,"asd");
        String ret1 = myArray.get(1);
    }
}

代码解释:

  1. 类名后的 代表占位符,表示当前类是一个泛型类
    了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:
    E 表示 Element
    K 表示 Key
    V 表示 Value
    N 表示 Number
    T 表示 Type
    S, U, V 等等 - 第二、第三、第四个类型

  2. 不能new泛型类型的数组:例如,public T[] array = new T[10];
    T[] array = (T[])new Object[10];是否就足够好,答案是未必的。这块问题一会儿介绍

三、泛型如何编译的

3.1、擦除机制

那么,泛型到底是怎么编译的?这个问题,也是曾经的一个面试问题。泛型本质是一个非常难的语法,要理解好他还是需要一定的时间打磨。

找到字节码文件,按住shift,鼠标右键,打开powerShell窗口
通过命令:javap -c 查看字节码文件,所有的T都是Object。
在这里插入图片描述

在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制。

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。
有关泛型擦除机制的文章截介绍:https://zhuanlan.zhihu.com/p/51452375

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

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

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

    public T get(int pos){
        return array[pos];
    }

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

public class TestDome {
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>();
        Integer[] strings = myArray1.getArray();
    }
}    

在这里插入图片描述
替换后的方法为:将Object[]分配给Integer[]引用,程序报错

数组和泛型之间的一个重要区别是它们如何强制执行类型检查。具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。
通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Intefer类型的数组,编译器认为是不安全的。

正确的方式:【了解即可】
通过反射创建,指定类型的数组

class MyArray<T> {
    public T[] array;
    
    public MyArray() {
    } 
    /**
     * 通过反射创建,指定类型的数组
     * @param clazz
     * @param capacity
     */
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }
    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;
    }
}

四、泛型的上界

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

4.1、语法

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

4.2、示例

class MyArrayList<T extends Number>{
	
}

说明:< T extends Number > 此时的这个泛型的参数可以使Number或者Number的子类
只接受 Number 的子类型作为 T 的类型实参
在这里插入图片描述
**了解: 如果没有指定类型边界 T,可以视为 T extends Object
泛型没有下界
**

4.3、泛型方法

示例:写一个泛型类,找出数组中最大的数
在这里插入图片描述

在这里插入图片描述

class Alg<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 Test {
    public static void main(String[] args) {
        Alg<Integer> alg = new Alg<>();
        Integer[] array = {10,2,8,36};
        System.out.println(alg.FindMax(array));
    }
}

上面实现的泛型方法在调用的时候都要new对象,不然级就不能调用这个泛型方法,此时我们在这个方法上加上static,就成为了静态方法,直接使用类名来调用,但是在写法上面有一点区别
在这里插入图片描述

class Alg2{
    public static <T extends Comparable<T>> 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 Test {
    public static void main(String[] args) {
        Integer[] array = {10,2,8,36};
        System.out.println(Alg2.FindMax(array));
    }
}

五、泛型中的父子类关系

public class MyArrayList<E> { ... }
// MyArrayList<Object> 不是 MyArrayList<Number> 的父类型
// MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型

在这里插入图片描述

六、通配符

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

6.1、通配符解决什么问题

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

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

2、或者我们可以这样理解:泛型T就像是个变量,等着你将来传一个具体的类型,而通配符则是一种规定,规定你能传哪些参数

在这里插入图片描述

class Agl{
    public static <T> void print1(ArrayList<T> list){
        for (T x: list) {
            System.out.println(x);
        }
    }

    public static void print2(ArrayList<?> list){
        for (Object x: list) {
            System.out.println(x);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList();
        list.add(1);
        list.add(3);
        Agl.print1(list);

        Agl.print2(list);
    }
}

print2中使用了通配符,和print1相比,此时传入list的,具体是什么数据类型,我们是不清楚的。这就是通配符。

6.2、通配符上界

6.2.1、语法

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

6.2.2、示例

// 可以传入类型实参是 Number 子类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? extends Number> list) {
	...
} 
// 以下调用都是正确的
	printAll(new MyArrayList<Integer>());
	printAll(new MyArrayList<Double>());
	printAll(new MyArrayList<Number>());
// 以下调用是编译错误的
	printAll(new MyArrayList<String>());
	printAll(new MyArrayList<Object>());

示例2:假设有如下关系

Animal
Cat extends Animal
Dog extends Animal

根据以上的关系,写一个方法,打印一个存储了Animal或者Animal子类的list。
代码1public static void print(List<Animal> list) {

}
这样不可以解决问题,因为print的参数类型是 List<Animal> list ,就不能接收 List<Cat> list 。

代码2public static <T extends Animal> void print2(List<T> list) {
	for (T animal : list) {
		System.out.println(animal);
	}
}
此时T类型是Animal的子类或者自己。该方法可以实现

代码3:通配符实现
public static void print3(List<? extends Animal> list) {
	for (Animal ani : list) {
		System.out.println(ani);//调用谁的toString 方法?
	}
}

区别是什么?
1、对于泛型实现的print2方法, 对T进行了限制,只能是Animal的子类
比如:传入Cat,那么类型就是Cat
2、对于通配符实现的print3方法,首先不用再static后使用尖括号,其次相当于对Animal进行了规定,允许
你传入Animal 的子类。具体哪个子类,此时并不清楚
比如:传入了Cat,实际上声明的类型是Animal,使用多态才能调用Cat的toString方法

6.3、通配符的上界-父子类关系

// 需要使用通配符来确定父子类型
	MyArrayList<? extends Number>MyArrayList <Integer>或者 MyArrayList<Double>的父类类型
	MyArrayList<?>MyArrayList<? extends Number> 的父类型

6.4、通配符的上界-特点

对于这个代码,我们思考:是否可以对这个List进行写入?

ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<Double> arrayList2 = new ArrayList<>();
List<? extends Number> list = arrayList1;
//list.add(1,1);//报错,此时list的引用的子类对象有很多,再添加的时候,任何子类型都可以,为了安全,java不让这样进
行添加操作。
Number a = list.get(0);//可以通过
Integer i = list.get(0);//编译错误,只能确定是Number子类

在这里插入图片描述

答案不可以!
因为,list可以引用arrayList1,或者arrayList2。所以:

  1. 如果对list中添加数据的时候,报错!愿意很简单,list中存储的可能是Number也可能是Number的子类。此时添加任何类型的数据都不可以,无法确定到底是哪种类型。
  2. Number a = list.get(0);可以通过,此时获取的元素肯定是Number的 子类
  3. 但是不能这么写:Integer i = list.get(0);你怎么知道,获取的就是Integer呢?

通配符的上节不适合写入数据

6.5、通配符下界

6.5.1、语法:

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

6.5.2、示例

// 可以传入类型实参是 Integer 父类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? super Integer> list) {
...
} /
/ 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Number>());
printAll(new MyArrayList<Object>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Double>());

6.5.3、通配符下界-父子类关系

MyArrayList<? super Integer>MyArrayList<Integer>的父类类型
MyArrayList<?>MyArrayList<? super Integer>的父类类型

6.5.4、通配符下界-特点

对于这个代码,我们思考:是否可以对这个List进行读取?

ArrayList<? super Person> list = new ArrayList<Person>();
//ArrayList<? super Person> list2 = new ArrayList<Student>();//编译报错,list2只能引用Person或者Person父类类型的list

list.add(new Person());//添加元素时,只要添加的元素的类型是Person或者Person的子类就可以
list.add(new Student());

Student s = list.get(0);//error
Object s = list.get(0);//可以

答案不可以!
因为添加元素的时候,我们知道list引用的对象肯定是Person或者Person的父类的集合,我们能够确定此时存储的
元素的最小粒度比Person小的都可以。但是,你读取的时候,你知道是读取到的是哪个子类吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

初次使用yolov8遇到的问题

记录第一次使用yolo8跑自己的数据&#xff1b; 首先将官方文档看一下&#xff0c;大概捉摸了2个小时&#xff0c;地址&#xff1a;GitHub - ultralytics/ultralytics: NEW - YOLOv8 &#x1f680; in PyTorch > ONNX > CoreML > TFLite 获得了基本的一些了解&#x…

$\Beta$分布推导与可视化

$\Gamma$函数 $\Gamma$函数(Gamma函数)是阶乘函数在实数和复数域的扩展。对于正整数$n$&#xff0c;阶乘函数表示为$n! 1 \times 2 \times ... \times n$。然而&#xff0c;这个定义仅适用于正整数。Gamma函数的目的是将阶乘扩展到实数和复数域&#xff0c;从而计算实数和复数…

代码随想录【链表】--->删除倒数第N个节点、链表相交、环形链表

⭐️代码随想录⭐️ 数组篇: 二分查找 移除数组 有序数组的平方 长度最小的数组 螺旋矩阵 链表篇&#xff1a; 链表移除 设计链表 反转链表 交换链表中的节点 文章目录19. 删除链表的倒数第 N 个结点思路代码面试题 02.07. 链表相交思路代码142. 环形链表 II思路判断链表有环确…

ARM 制作简易的根文件系统

一、根文件系统概述 1、为什么需要根文件系统 (1) init 进程这个应用程序&#xff0c;在根文件系统上。 (2) 根文件系统提供了根目录&#xff1a;/。 (3) 内核启动后的应用层配置(/etc 目录)&#xff0c;在根文件系统上。几乎可以认为&#xff1a;发行版 内核 rootfs。 …

Arduino开发之如何连接压力传感器模块?

文章目录0.引言1.压力传感器模块说明2.代码编写3.功能演示0.引言 在利用Arduino开发过程中&#xff0c;若需知道设备能感知到受到外部按压&#xff0c;设备可以通过压力传感器模块来感知周围环境。本文在【Arduino如何进行开发&#xff1f;】基础上&#xff0c;借鉴现有网络资料…

数字营销新宠:探究2023年YouTube网红营销的核心趋势和商业价值

2023年&#xff0c;YouTube网红营销已经成为全球广告营销行业的重要组成部分。YouTube作为全球最大的视频分享平台&#xff0c;每月活跃用户数达到了20亿&#xff0c;其中有超过100万的YouTube网红在平台上分享自己的创意和想法。在这篇文章中&#xff0c;Nox聚星将和大家探讨2…

Visual Studio如何使用Qt开发桌面软件?

文章目录0.引言1.开发环境配置2.编写第一个Qt程序0.引言 笔者熟悉的第一门编程语言是C#&#xff0c;当初本科毕业设计需要进行Qgis的二次开发&#xff0c;本想利用C#编程&#xff0c;但网上资料较少&#xff0c;多是利用Qt进行Qgis的二次开发&#xff0c;Qt是利用C编程&#xf…

如何将aac转化为mp3,4种常用方法

aac是高级音频编码格式之一&#xff0c;支持多声道、解析度高&#xff0c;与mp3相比&#xff0c;aac格式的音质更佳&#xff0c;文件更小。在手机上录音时&#xff0c;保存下来的录音文件通常是AAC格式的。虽然aac格式有很多优点&#xff0c;但是&#xff0c;在不同的设备上&am…

POST请求与GET请求

get和post是HTTP协议中的两种发送请求的方法 HTTP是基于TCP/IP的关于数据如何在万维网中通信的协议 一、get请求与querystring get请求即客户端向server服务端请求数据&#xff0c;如&#xff1a;获取文章列表的分页码等 通过queryString来获取数据&#xff0c;如&#xff1…

设计模式 -- 模板方法模式

前言 月是一轮明镜,晶莹剔透,代表着一张白纸(啥也不懂) 央是一片海洋,海乃百川,代表着一块海绵(吸纳万物) 泽是一柄利剑,千锤百炼,代表着千百锤炼(输入输出) 月央泽,学习的一种过程,从白纸->吸收各种知识->不断输入输出变成自己的内容 希望大家一起坚持这个过程,也同…

java生成随机字符串的方法

今天我们来学习下随机数的生成&#xff0c;随机数就是不确定的数&#xff0c;它可以是任意一个整数或者字符串。下面就让我们一起来学习下随机字符串的生成吧。 首先&#xff0c;我们需要先定义一个名为 str的数组&#xff0c;并将它作为输入文件。 1、使用 Java中的 str作为输…

传播的最大能量场来自私域的裂变——“春生百味”品牌营销裂变活动复盘后记

关于大宗家电等耐用品品牌想要精准触达潜在用户&#xff0c;如何实施传播策略&#xff1f; 刚刚与方太品牌方复盘完3月份“春生百味”关于营销裂变直播活动的整体效果&#xff0c;我们借此次整体案例实施过程的梳理&#xff0c;以分享几个裂变过程中的关键点。 传播的最大能量…

React styled-components(二)—— props、attrs属性

styled-components props、attrs属性propsprops 穿透添加 attrs 属性获取 state 中的样式变量控制样式通过 props 控制样式通过 css 控制样式props props 穿透 styled-components 可以 props 穿透&#xff0c;把属性穿透到元素中。 通常&#xff0c;用 css 的 input 组件实现…

【Python】【进阶篇】十七、Python爬虫实现实时翻译

目录十七、Python爬虫实现实时翻译17.1 JS代码slat与sign17.2 Python代码表示参数17.3 完整程序实现十七、Python爬虫实现实时翻译 YD翻译是以异步方式实现数据加载的&#xff0c;要实现数据抓取&#xff0c;其过程极其繁琐。 上一节《Python爬虫的浏览器实现抓包》&#xff…

使用LeafLet叠加Geoserver wms图层到已有底图的方法

背景 随着现代城市交通建设的飞速发展&#xff0c;各个城市的地铁路线和地铁站点也是越来越多。地铁极大的方便了广大人民的交通出行。作为Giser&#xff0c;经常会遇到需要将一份shp数据在地图上展示&#xff0c;甚至需要在网页端进行浏览的需要。把shp这种空间矢量数据进行we…

数据库笔记Ch04----概念数据库的设计(1)

前三章我们学习了如何使用DBMS我们学会了增删改查&#xff0c;插入数据库&#xff0c;创建视图... 这一章是我们的数据库刚刚建立&#xff0c;只有一个需求&#xff0c;需要根据用户的需求来创建数据库&#xff0c;每个表有哪些属性&#xff0c;参照关系是什么&#xff0c;主键…

一.Jetpack全套

Jetpack全套一.Jetpack介绍1.特性&#xff1a;2.分类&#xff1a;二.应用架构三.LifeCycle:1.简介2.简单使用3.实战&#xff1a;Dialog内存泄漏四.VideModel1.介绍2.简单使用3.AndroidViewModel使用4.使用viewmodel实现fragment直接数据共享五.LiveData1.介绍2.常用方法3.使用场…

车载 OTA技术概念

1 OTA技术概念 随着高级辅助驾驶的发展和自动驾驶的引入&#xff0c;汽车变得越来越智能&#xff0c;这些智能汽车被软件控制&#xff0c;装有巨量的软件程序&#xff0c;当出现一个软件程序问题或者更新时&#xff0c;如果按照传统的解决方式&#xff0c;那都将是一项很繁重的…

【深度学习】基于Hough变化的答题卡识别(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。⛳座右铭&#…

云开发--实现发送邮件+短信+链接跳转小程序功能

目录 1、小程序实现发送邮件 准备一个qq邮箱&#xff0c;并启动SMTP服务 确定小程序云开发环境&#xff0c;并新建云函数 2、小程序实现发送短信 确定应用 确定签名 确定模板 编写云函数-发送短信 3、链接跳转小程序 H5 配置 生成 URL Link 学习记录&#xff1a; …