JAVA中的泛型机制详解

 1.泛型的概念

java泛型是java5引入的一个特性,它允许我们为类,接口,方法指定类型参数,从而提供编译时类型安全检查。泛型的本质是参数化类型,即在声明类,接口或者方法时不指定具体的类型,而是使用一个或者多个类型参数表示

 泛型的好处主要有两个:

  • 类型安全:在编译的时候就能检查到类型的错误,而不是在运行的时候抛出ClassCastException
  • 消除强制类型转换:泛型可以自动进行类型的转换,从而减少代码中显性类型转换的需求,使得代码更加的简洁。

举个例子,泛型究竟是怎么诞生的:

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);for(int i = 0; i< arrayList.size();i++){String item = (String)arrayList.get(i);Log.d("泛型测试","item = " + item);
}

运行结果:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayList可以存放任意类型,例子中添加了一个String类型,又添加了一个Integer类型,例子中我们从Arraylist中获取值用String类型去获取Integer不能自动类型转换,程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。

为了避免在运行时会出现这种情况,我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。

List<String> arrayList = new ArrayList<String>();

这样子,ArrayList里面只能存储String类型的值。

2.泛型方法

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 <E>)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • ? - 表示不确定的 java 类型

泛型标记符在概念上都是相同的,它们都是用来指定泛型列,接口或者方法可以接受的类型。你可以根据自己的喜好或者代码的上下文来随意选择使用哪个标记符。

下面我们来做一个题目来练习一下: 

假定我们有这样一个需求:写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。

public class Main {//泛型方法PrintArraypublic static < E > void printArray(E[] inputArray){//输出数组元素for(E element:inputArray){System.out.printf("%s",element);}System.out.println();}public static void main(String[] args){Integer[] intArray ={1,2,3,4,5};Double[] doubles ={1.1,2.2,3.3,4.4};Character[] characters ={'H','E','L','L','O'};System.out.println("整数数组元素为、:");printArray(intArray);//传递一个整数数组System.out.println("\n双精度型数组元素为:");printArray(doubles);System.out.println("\n字符型数组元素为:");printArray(characters);}}

运行结果为:

整数数组元素为、:
12345双精度型数组元素为:
1.12.23.34.4字符型数组元素为:
HELLO

泛型的基本使用:

在类名之后使用尖括号声明类型参数,声明的类型参数可以像普通类型一样用在类型声明处使用,到使用时再决定其具体类型,然后编译器会帮我们处理一些类型类型转换的细节。

public class Holder<T> {T val;public Holder(T val) {this.val = val;}public T getVal() {return val;}public void setVal(T val) {this.val = val;}public static void main(String[] args) {Holder<String> strHolder = new Holder<String>("abc");String s = h.getVal();}
}

在使用时指定了的 Holder 的类型参数为 String。可以将 getVal() 的返回值直接赋给一个 String 变量,而不用显示的转型。在使用 setVal 时也必须传入 String 类或其子类,若入参不是 String 或其子类那么编译时会报错。

在Java7之前 new 参数化类型时需要指定类型,但在Java7之后 new 操作可以不用显示指定类型,编译器会自动推导出来:

 Holder<String> h = new Holder<>("abc");

2.1泛型方法的使用

泛型类,在创建类的对象的时候确定类型参数的具体类型;
泛型方法,在调用方法的时候再确定类型参数的具体类型。

泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型。
 

举例如下:

public class Demo {  public static void main(String args[]) {  GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象  String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串  int i = d.fun(30);  // 给GenericMethod中的泛型方法传递数字,自动装箱  System.out.println(str); // 输出 汤姆System.out.println(i);  // 输出 30GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin}  
}class GenericMethod {// 普通的泛型方法public <T> T fun(T t) { // 可以接收任意类型的数据  return t;} // 静态的泛型方法public static <E> void show(E one){     System.out.println("静态泛型方法 " + one);}
}  

3.认识泛型的写法

先从一个简单的泛型类开始:

public class Main {class point<T>{//此处可以随便写标识符号,T是type的简称private T var;//var的类型由T指定,即:有外部指定public T getVar(){return var;}public void setVar(T var){//设置的类型也由外部决定this.var=var;}}public void main(String[] args) {point<String> p =new point<String>();//里面的var类型为String类型p.setVar("it");System.out.println(p.getVar().length());}}

多元泛型:

public class Main {static class Notepad<K,V>{private K key;private V value;public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}}public static void main(String[] args) {Notepad<String,Integer> t =null;t=new Notepad<String,Integer>();t.setKey("汤姆");t.setValue(20);System.out.println("姓名"+t.getKey());System.out.println("年龄"+t.getValue());}}

多个类型参数使用逗号分隔:

public class Holder<A, B, C> {public A v1;public B v2;public C v3;public Holder(A v1, B v2, C v3) {this.v1 = v1;this.v2 = v2;this.v3 = v3;}public static void main(String[] args) {Holder<String, Integer, Float> h = new Holder<>("abc", 1, 2.5);}
}

简单的泛型接口:

public class Main {interface Info<T>{        // 在接口上定义泛型public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型}static class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类private T var ;             // 定义属性public InfoImpl(T var){     // 通过构造方法设置属性内容this.setVar(var) ;}public void setVar(T var){this.var = var ;}public T getVar(){return this.var ;}}public static void main(String arsg[]){Info<String> i = null;        // 声明接口对象i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象System.out.println("内容:" + i.getVar()) ;}}

非泛型的类(或者泛型类)中定义泛型方法

class Arraylist<E> {public <T> T[] toArray(T[] a) {return (T[]) Arrays.copyOf(elementData, size, a.getClass());}
}

4.泛型通配符        

泛型通配符是JAVA泛型中的一个概念,它允许你在编写代码的时候对泛型类型参数的类型进行限制。

为什么要使用通配符?

  • 灵活性:通配符提供了一种灵活的方式来处理不同类型的集合,而不需要为每种类型编写特定的代码。
  • 类型安全:尽管通配符允许一定程度的类型不确定性,但它仍然提供了类型安全,防止了类型不匹配的错误。
  • 扩展性:使用通配符可以更容易的扩张代码。

这里的?代表类型是未知的,所以编译器不知道要检查哪种类型,因此不允许你向这样的列表中添加任何的元素。

Object是所有类的超类,因此任何类型的对象都可以安全的转换为Object类型。

无限制通配符:

使用?表示,它不指定任何类型的限制。这种通配符可以代表任何的类型,但使用它的时候,不能对它进行类型转换或者调用实例的方法

List<?> list =new ArrayList<>();

这个例子中,list可以存储任何类型的对象,但你不能调用list.get(0).toString()这样的操作,因为他的类型是未知的。

List<?> list = new ArrayList<String>();
list.add("Hello"); // 编译错误:不能添加具体类型
String s = list.get(0); // 编译错误:不能直接赋值给String
Object obj = list.get(0); // 正确:可以赋值给Object

使用无界通配符的集合可以被用来读取数据,但不能用来插入数据,因为插入需要知道具体的类型。

上界限定通配符:

使用?extend T 表示,它指定了参数类型的上限。意味着参数类型必须是T或者T的子类型。这种通配符常用于方法参数或者返回类型,以便能够处理更加广泛的类型。

   public void printList(List<? extends Number> list){for (Number num:list){System.out.println(num);

在这个例子中,printList方法可以接受任何的Number类型或其子类型的列表。

List<? extends Number> numberList = new ArrayList<Integer>();
numberList.add(5); // 编译错误:不能添加具体类型
Integer i = numberList.get(0); // 正确:可以赋值给Integer
Number num = numberList.get(0); // 正确:可以赋值给Number

在这个例子中,`numberList` 可以存储任何 `Number` 类型或其子类(如 `Integer`、`Double` 等)的对象。但是,你不能向这样的列表添加元素,因为添加操作需要知道具体的类型。

 下界限定通配符:

使用?super T表示,它指定了类型参数的下限,这意味着类型参数必须是T或者T的超类型。这种通配符常用于方法参数,特别是当你需要想集合中添加元素时。

public class Main {public void addToList(List<? super String>list, String item){list.add(item);}}

在这个例子中,addToList方法可以接受任何可以添加String类型元素的列表

List<? super Integer> list = new ArrayList<Number>();
list.add(5); // 编译错误:不能添加Number类型
list.add(new Integer(5)); // 正确:可以添加Integer类型
Number num = list.get(0); // 编译错误:不能赋值给Number
Integer i = list.get(0); // 正确:可以赋值给Integer

下界限定通配符使用 `? super T` 表示,它指定了类型参数的下限。这意味着类型参数必须是 `T` 或 `T` 的超类。

非泛型的类(或者泛型类)中定义泛型方法

class Arraylist<E> {public <T> T[] toArray(T[] a) {return (T[]) Arrays.copyOf(elementData, size, a.getClass());}
}

5.类型擦除

JAVA中的类型擦除是一种语言特性,它允许编译器在编译的时候使用泛型类型,但是在运行的时候将泛型类型擦除,以保持向后兼容性。这意味着在运行的时候,泛型信息不再存在,所有的泛型类型都会被转换为它们的原始类型(即它们的边界类型,如果没有指定边界,则为object)

1.无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T><?>的类型参数都被替换为Object。

2.有限制类型擦除 

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number><? extends Number>的类型参数被替换为Number<? super Number>被替换为Object。

3.擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

如何证明类型擦除呢?

举个例子:


public class Main {static class Box<T>{private T t;public T getT() {return t;}public void setT(T t) {this.t = t;}}public static void main(String[] args) {Box<Integer> integerBox=new Box<>();integerBox.setT(123);Integer value =integerBox.getT();System.out.println("value: "+value);//输出结果为123//尽管Box<Integer>在编译的时候是Integer类型,但是在运行的时候它只是BoxSystem.out.println(integerBox instanceof Box<Integer>);//编译的时候就报错了System.out.println(integerBox instanceof Box);//ture}
}

原始类型相等

public class Test {public static void main(String[] args) {ArrayList<String> list1 = new ArrayList<String>();list1.add("abc");ArrayList<Integer> list2 = new ArrayList<Integer>();list2.add(123);System.out.println(list1.getClass() == list2.getClass()); // true}
}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。

4.如何理解泛型的编译期检查?

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

public static  void main(String[] args) {  ArrayList<String> list = new ArrayList<String>();  list.add("123");  list.add(123);//编译错误  
}

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正涉及类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。

public class Test {  public static void main(String[] args) {  ArrayList<String> list1 = new ArrayList();  list1.add("1"); //编译通过  list1.add(1); //编译错误  String str1 = list1.get(0); //返回类型就是String  ArrayList list2 = new ArrayList<String>();  list2.add("1"); //编译通过  list2.add(1); //编译通过  Object object = list2.get(0); //返回类型就是Object  new ArrayList<String>().add("11"); //编译通过  new ArrayList<String>().add(22); //编译错误  String str2 = new ArrayList<String>().get(0); //返回类型就是String  }  
} 

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象

泛型中参数话类型为什么不考虑继承关系

在Java中,像下面形式的引用传递是不允许的:

ArrayList<String> list1 = new ArrayList<Object>(); //编译错误  
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

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

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

相关文章

openEuler 安装 ROS2 Humble

openEuler 安装 ROS2 Humble 1 介绍2 安装【openEuler 24.03】2.1 Installing ros-humble2.2 Test ros-humble【python 版本冲突&#xff0c;未解决】 2 安装【openEuler 22.03】3 Python 版本问题【pyenv】参考 1 介绍 2 安装【openEuler 24.03】 2.1 Installing ros-humble…

力扣高频SQL 50题(基础版)第十三题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第十三题570. 至少有5名直接下属的经理题目说明思路分析实现过程准备数据实现方式结果截图 力扣高频SQL 50题&#xff08;基础版&#xff09;第十三题 570. 至少有5名直接下属的经理 题目说明 表: Employee ------…

Java面试八股之后Spring、spring mvc和spring boot的区别

Spring、spring mvc和spring boot的区别 Spring, Spring Boot和Spring MVC都是Spring框架家族的一部分&#xff0c;它们各自有其特定的用途和优势。下面是它们之间的主要区别&#xff1a; Spring: Spring 是一个开源的轻量级Java开发框架&#xff0c;最初由Rod Johnson创建&…

Linux之存储桶minio单机安装和使用简介

一、minio简介 MinIO 是一个高性能的分布式对象存储系统&#xff0c;主要用于存储非结构化数据&#xff0c;例如照片、视频、备份和日志文件。它是开源的&#xff0c;基于 Go 语言开发&#xff0c;具有高度可扩展性和高可用性&#xff0c;能够在私有云、公有云和边缘环境中部署…

20240725项目的maven环境报红-重新配置maven

1.在编辑器里面打开项目&#xff0c;导入源码 &#xff08;1&#xff09;找到项目的地址C:\Users\zzz\IdeaProjects\datasys&#xff0c;然后右击用idea编辑器打开。 &#xff08;2&#xff09;idea中上菜单栏打开open&#xff0c;然后输入file&#xff0c;选择源代码文件 2.…

LabVIEW放大器自动测量系统

开发了一个基于LabVIEW平台的多路前置放大器自动测量系统的开发与实施。该系统集成了硬件控制与软件编程&#xff0c;能够实现放大器各项性能指标的快速自动测量&#xff0c;有效提高了测试的精确性和效率。系统设计采用了虚拟仪器技术&#xff0c;结合了先进的测量与控制策略&…

丹东纵横文化传媒:创新引领,广告服务再升级

近日,丹东市振兴区的丹东纵横文化传媒有限公司再次传来振奋人心的消息,这家以互联网和相关服务为主的新兴企业,在广告设计与传播领域实现了重大突破,进一步巩固了其在行业内的领先地位。 【广告服务全面升级,引领市场新风尚】 据丹东纵横文化传媒有限公司最新发布的信息显示,公…

掌握AJAX技术:从基础到实战

文章目录 **引言****1. 什么是AJAX&#xff1f;****2. AJAX的工作原理**AJAX 示例使用 Fetch API 实现 AJAX **3. 如何在项目中使用AJAX****4. 处理AJAX请求的常见问题****5. AJAX与JSON的结合****6. 使用AJAX框架和库****7. 实战&#xff1a;创建一个动态表单****8. AJAX中的事…

spine to unity-2.利用边缘框实现实时碰撞检测

主要讲spine的边缘框&#xff0c;在unity中&#xff0c;实现实时碰撞检测。其中使用的素材&#xff0c;是我为独立游戏ink制作的动画。独立游戏ink的开发日志&#xff0c;在小红薯持续更新中。spine工具包的安装&#xff0c;下载请参考spine to unity-1spine BoundingBoxFollow…

Doris全方位教程+应用实例

Impala性能稍领先于presto,但是presto在数据源支持上非常丰富&#xff0c;包括hive、图数据库、传统关系型数据库、Redis等 缺点&#xff1a;这两种对hbase支持的都不好&#xff0c;presto 不支持&#xff0c;但是对hdfs、hive兼容性很好&#xff0c;其实这也是顺理成章的&…

mac怎样清理photoshop垃圾的方法 ps清理缓存和垃圾 苹果电脑暂存盘已满怎么清理

很多使用过ps&#xff0c;尤其是Adobe全家桶的小伙伴会发现&#xff0c;这些软件占用缓存很多&#xff0c;而且随着使用时间的增长&#xff0c;缓存也会越多&#xff0c;并不会自动清理。那么mac系统怎么清理ps暂存盘呢&#xff1f;mac又该怎么最高效清理磁盘空间呢&#xff1f…

Linux嵌入书学习—数据结构——栈(seqstak)

一、栈&#xff1b; 定义&#xff1a; 是限定仅在表尾&#xff08;栈顶&#xff09;进行插入和删除操作的线性表 栈又称为 后进先出&#xff08;Last In First Out&#xff09; 的线性表&#xff0c;简称 LIFO 结构 栈顶&#xff08;Top&#xff09; 栈顶是栈中允许进行添加&…

RuoYi基于SpringBoot+Vue前后端分离的Java快速开发框架学习_2_登录

文章目录 一、登录1.生成验证码2.验证码作用1.大体流程2.代码层面(我们都是从前端开始看起) 一、登录 1.生成验证码 基本思路&#xff1a; 后端生成一个表达式&#xff0c;例如34?7,显而易见后面是答案截取出来题干和答案把题干11&#xff1f;变成图片&#xff0c;变成流&a…

[C++进阶]多态的概念、定义与实现

多态&#xff0c;顾名思义&#xff0c;即多种形态。具体来说&#xff0c;就是不同对象执行同一行为而产生不同的结果。 一、多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生…

神经网络与注意力机制的权重学习对比:公式探索

神经网络与注意力机制的权重学习对比&#xff1a;公式探索 注意力机制与神经网络权重学习的核心差异 在探讨神经网络与注意力机制的权重学习时&#xff0c;一个核心差异在于它们如何处理输入数据的权重。神经网络通常通过反向传播算法学习权重&#xff0c;而注意力机制则通过学…

LLMs之Llama 3.1:Llama 3.1的简介、安装和使用方法、案例应用之详细攻略

LLMs之Llama 3.1&#xff1a;Llama 3.1的简介、安装和使用方法、案例应用之详细攻略 导读&#xff1a;2024年7月23日&#xff0c;Meta重磅推出Llama 3.1。本篇文章主要提到了Meta推出的Llama 3.1自然语言生成模型。 背景和痛点 >> 过去开源的大型语言模型在能力和性能上一…

OCC 创建方管(拉伸操作)

目录 一、OCC 拉伸操作 二、例子 1、使BRepBuilderAPI_MakeFace 2、使用BRepPrimAPI_MakeRevol 3、垂直路径扫掠 一、OCC 拉伸操作 BRepPrimAPI_MakeSweep Class Reference - Open CASCADE Technology Documentation OCC提供几种图形的构建是由基本图形的旋转,拉伸等方…

基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(二)|| 堆 / 栈

堆和栈 1. 堆 堆就是空闲的一块内存&#xff0c;可以通过malloc申请一小块内存&#xff0c;用完之后使用再free释放回去。管理堆需要用到链表操作。 比如需要分配100字节&#xff0c;实际所占108字节&#xff0c;因为为了方便后期的free&#xff0c;这一小块需要有个头部记录…

电子加密狗的定义与功能

电子加密狗&#xff0c;也称为加密锁、硬件锁或USB密钥&#xff0c;是一种用于软件保护和授权管理的硬件设备。它通常是一个外部设备&#xff0c;插入到计算机的USB接口上&#xff0c;通过加密算法和技术来确保软件的安全性和防止非法复制、盗版以及未经授权的使用。以下是关于…

AR 眼镜之-蓝牙电话-实现方案

目录 &#x1f4c2; 前言 AR 眼镜系统版本 蓝牙电话 来电铃声 1. &#x1f531; 技术方案 1.1 结构框图 1.2 方案介绍 1.3 实现方案 步骤一&#xff1a;屏蔽原生蓝牙电话相关功能 步骤二&#xff1a;自定义蓝牙电话实现 2. &#x1f4a0; 屏蔽原生蓝牙电话相关功能 …