JAVA 学习·泛型(一)

基本概念

引言

  • 泛型(Generic)指可以把类型参数化,这个能力使得我们可以定义带类型参数的泛型类、泛型接口、泛型方法,随后编译器会用唯一的具体类型替换它。
  • 主要优点是在编译时而不是运行时检测出错误。泛型类或方法允许用户指定可以和这些类或方法一起工作的对象类型。如果试图使用一个不相容的对象,编译器就会检测出这个错误。
  • Java 的泛型通过擦除法实现,和 C++ 模板生成多个实例类不同。编译时会用类型实参代替类型形参进行严格的语法检查,然后擦除类型参数、生成所有实例类型共享的唯一原始类型。这样使得泛型代码能兼容老的使用原始类型的遗留代码。

引言中的这三条隐含着很多重要的信息,初看时可能无法深刻理解,后面我们将结合具体实例阐述。

类型参数

  我们先引入一个例子。比如,我们需要实现一个类,这个类中提供了对于各种类型的数据求和的方法(实际上这个类的意义不大,只是为了说明问题而编写)。我们可以这样写:

public class NormalAdder{//二个int相加public static int add(int value1, int value2){ return value1 + value2;}//二个double相加public static double add(double value1, double value2){ return value1 + value2;}//二个float相加public static float add(float value1, float value2){ return value1 + value2;}
}

  类NormalAdder中,我们写了 3 3 3 个重载的add函数实现了 3 3 3 种不同数据类型的加法。在实际使用的时候,编译器会根据我们提供的参数类型选择具体调用哪个add函数。
  但是现在有一个问题:这个类中没有short类型、byte类型、char类型数据的相加方法。按照传统的函数重载子路,我们还需要再编写 3 3 3 个重载的 add 方法。这就显得代码比较冗长。
  而这时我们就可以用上泛型函数

public class GenericAdder {//定义泛型函数,类型参数为T(代表某一种类型),T为类型形参public static <T> T add(T value1, T value2){ return value1 + value2;}
}
//调用泛型函数,需要给出类型实参
GenericAdder.<Integer>add(1,2;//显示地给出类型实参为Integer,传递给形参T。
GenericAdder.add(1,2); //编译器自动可以推断出T为Integer(类型推断)

  从而只需要定义一次add函数。其中,泛型函数的类型参数放在<>里。

这个例子仅仅是用于说明。实际上它不是很恰当,因为 Java 中没有运算符重载,上面 GenericAdder中的add方法在编译时会报错。一旦T不是基本数据类型的包装类,虚拟机就不知道怎么解释+这个运算。

泛型类

  当一个类后面带上形式化参数,这个类就成为泛型类。泛型接口也是这样定义的。形式化类型参数是一个逗号分隔的变量名列表,位于类声明中类名后面的尖括号<>中。下面的代码声明一个泛型类Wrapper,它接受一个形式化类型参数T

public class Wrapper<T>  {// 一些代码……// 需要注意的是,这里面不能出现类似于 new T() 的语句
}

  T是一个类型变量,它可以是 Java 中的任何引用类型。当把一个具体的类型实参传递给类型形参T时,就得到了一系列的参数化类型(Parameterized Types),如Wrapper<String>Wrapper<Integer> ,这些参数化类型是泛型类Wrapper<T>的实例类型:

Wrapper<String>  stringWrapper = new Wrapper<String>();
Wrapper<Circle>  circleWrapper = new Wrapper<Circle>();

强调:类型变量只能是引用类型,不能是int,double,char等值类型。不过可以用这些值类型的包装类。

动机和优点

  泛型的概念是在 JDK 1.5 提出的,它的提出肯定有一定的动机。下面的例子能够说明这个动机。
  当我们想要对两个对象进行比较时,通常会让这个类实现Comparable接口,并重写Comparable中的compareTo函数。在 JDK 1.5 之前,Comparable接口如下:

package java.lang;public interface Comparable{public int compareTo(Object o);
}

  任何一个类A如果实现了Comparable接口,其中的函数compareTo的参数总是Object类型,这意味着我们可以让A类的对象与非A类的对象比较:

Comparable c = new Date();
System.out.println(c.compareTo("red"));

  上面的语句能够通过编译,但是运行时会产生错误,抛出ClassCastException异常。
  显然,程序在编译的时候看不出有什么问题,但是我们一眼就能够发现DateString两个不同类的对象不应该进行比较。泛型的引入解决了这个问题,JDK 1.5 之后的Comparable接口成为了泛型接口:

package java.lang;public interface Comparable<T>{public int compareTo(T o);
}

  引用Comparable对象时,需要传入实际类型参数,完成“泛型实例化”。比如下面就将Comparable<T>泛型接口实例化为了Comparable<Date>的实例接口:

Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));

  此时程序会在编译时报错,这就是引言 2 2 2 条所说的,我们使用的"red"是一个String对象,与cDate对象)不相容。
  泛型引入后,编译时根据传入的泛型参数Date,将Comparable<Date>实例类型中的T全部替换成Date,并检查所有实例方法的调用是否正确,防止编译通过的地方运行时出错问题的发生。一旦编译检查通过,编译器会擦除类型参数,并按照非泛型年代的标准编译程序,这时不会再产生编译通过的地方运行时出错的问题了。(关于擦除类型参数,这个在文末会着重介绍。)
  因此,泛型引入的最大优点就是在编译时找出类型不相容的问题,早发现、早解决。

泛型类、泛型接口、泛型方法的定义

泛型类的定义

  我们可以利用泛型定义一个栈:

import java.util.ArrayList;
public class GenericStack<E> {private ArrayList<E> list = new ArrayList<E>();public boolean isEmpty() {return list.isEmpty();}public int getSize() {return list.size();}public E peek() {return list.get(getSize() - 1);//取值不出栈}public E pop() {E o = list.get(getSize() - 1) ;list.remove(getSize() - 1);return o;}public void push(E o) {list.add(o);} public String toString() {return "stack: " + list.toString();}
}          

  其中的E就是类型参数。其它具体类名在什么地方,它就也几乎可以出现在什么地方;但是有一些例外,将在文章末尾介绍。具体使用这个泛型类时,只需要将实际参数赋给类型参数,即可确定栈的数据类型。例如:

GenericStack<String> stack1 = new GenericStack<String>(); // 后面的 String 可以省略
stack1.push("Londen");
stack1.push("Paris");
stack1.push("New York");GenericStack<Integer> stack2 = new GenericStack<>();
stack1.push(5);// int 类型的 5 被自动打包成 Integer 包装类
stack1.push(10);
stack1.push(15);

泛型接口的定义

  上文中我们已经看到了Comparable<T>,这就是一个泛型接口。非泛型类如果要实现泛型接口,需要给泛型接口传递实际参数类型。上面例子中的Date的函数头就是:

public class Date implements java.io.Serializable, Cloneable, Comparable<Date>{...}

  给Comparable<T>传递了实际参数类型Date。如果我们写了一个类Circle,要实现两个Circle的比较,也可以将Circle的头写成:

public class Circle extends ... implements ...,Comparable<Circle>{...}

  并在Circle内重写compareTo方法。

泛型方法的定义

  前文已经介绍了泛型方法,这里再对泛型方法做一个简单的描述。声明泛型方法,将类型参数<E>置于返回类型之前。方法的类型参数可以作为形参类型,方法返回类型,也可以用在方法体内其他类型可以用的地方;和泛型类一样,类型参数E也存在一些限制。
  而在实际调用泛型方法时,将实际类型放于<>之中方法名之前;也可以不显式指定实际类型,而直接给实参调用,由编译器自动发现实际类型。

public class GenericMethodDemo {public static void main(String[] args) {Integer[] integers = {1,2,3,4,5};String[] strings = {"Londen","Paris","New York","Austin"};		  GenericMethodDemo.<Integer>print(integers);// 显式指定实际类型是 Integer   GenericMethodDemo.print(strings); // 不显示指定实际类型,编译器自己发现是 String}public static <E> void print(E[] list){for(int i = 0 ; i <list.length; i++){System.out.print(list[i]+" ");}}
}

  运行结果:

1 2 3 4 5 Londen Paris New York Austin 

受限的泛型

  可以给形式化参数限定一个范围。考虑下面的一个要求:找到两个对象中较大的那个。
  首先我们不难写出下面的代码:

public class Max{public static <T> T findMax(T o1, T o2){return o1.compareTo(o2)?o1:o2;}
}

  上面的代码通过使用泛型,防止了两个不相容的对象进行比较;但是还存在问题,因为不是所有的类都实现了Comparable接口。也就是说不是所有的对象实例都能够调用compareTo方法。因此,我们需要限定T必须要是实现了Comparable接口的类型。
  改进后的代码如下:

public class Max{public static <T extends Comparable<E>> T findMax(T o1, T o2){return o1.compareTo(o2)?o1:o2;}
}

  <T extends Comparable<E>>规定了传进来的T必须实现了 Comparable<E>接口,否则编译器报错。还可以用<T extends SomeClass>来限定T必须是SomeClass的子类。
  需要注意的是,无论是限定T需要继承某些类,还是限定T要实现某些接口,一律使用关键字extends

泛型擦除和对泛型的限制

泛型擦除

  在引言中的第 3 3 3 条提到过,Java 的泛型通过擦除法实现。泛型的作用就是使得编译器在编译时通过类型参数来检测代码的类型匹配性。当编译通过,意味着代码里的类型都是匹配的。因此,所有的类型参数使命完成而全部被擦除。

一个泛型类的所有实例类型,在擦除类型变量后,共享同一个原始类型。

  比如,下面有一个泛型类:

ArrayList<String> list = new ArrayList<>();
list.add("Genshin");
String content = list.get(0);

  编译器拿到上面的代码,看见了泛型,那么首先根据泛型检查代码。发现没有问题后,会擦除泛型类型,变成下面的代码:

ArrayList list = new ArrayList();
list.add("Genshin");
String content = (String)(list.get(0));

  在擦除后的代码中,由于擦除前已经检查了list.get(0)的类型,所以此时强制类型转换为String是安全的,就不会在运行时出错。不难发现,擦除后的代码中,所有的泛型类都变成了它们的原始类型。这之后,将按照 java 正常的编译流程,将擦除后的代码编译成字节码。

非受限类型参数的擦除

  看完了上面的泛型擦除,你可能会有疑惑。因为有的类型参数T是被用来当做返回类型之类的,你把它擦除了,那究竟返回的是什么类型呢?
  比如上面的GenericStack<E>中,就有一段这样的代码:

public E pop() {E o = list.get(getSize() - 1) ;list.remove(getSize() - 1);return o;
}

  擦除了类型参数E之后,这段代码返回的o是什么?实际上,上面的代码擦除泛型后,会用Object代替E。擦除后的代码如下所示:

public Object pop() {Object o = list.get(getSize() - 1) ;list.remove(getSize() - 1);return o;
}

受限类型参数的擦除

  上面非受限的类型参数E,用Object来代替。如果一个泛型的参数类型是受限的,编译器会用该受限类型来替换它。比如下面的代码:

public static <E extends GeometricObject> boolean equalArea(E object1,E object2){return object1.getArea() == object2.getArea();
}

  擦除类型参数后会变成:

public static boolean equalArea(GeometricObject object1,GeometricObject object2){return object1.getArea() == object2.getArea();
}

对泛型的限制

不能创建泛型对象

  如果有一个类型参数E,那么不能new E(),也不能new E[N]。如果实在想要创建一个对象实例,只能想办法获取E的类型实参的class对象,在通过反射机制,如newInstance()创建一个对象实例。

有关反射机制的内容,在这篇博客中有所介绍。

不能创建泛型数组

  如果有一个类A,以及一个类型参数E,那么不能使用new A<E>[]的方式创建数组。但是要注意,new A<E>()创建对象是可以的。
  因此,下面的语句都是错误的

ArrayList<String>[] list = new ArrayList<String>[10];
ArrayList<String>[] list = new ArrayList<>[10];

  而下面的语句是正确的:

ArrayList<String>[] list = new ArrayList[10];

  该语句不需要强制类型转换,因为同一个类的原始类型和参数化类型是兼容的,一个原始类型可以引用一个参数化类型对象,反之亦然。因为它们擦除了类型参数后就是一样的了。

静态上下文中不允许使用泛型类的类型参数

  下面代码段存在 3 3 3 个非法的地方:

public class Test<E> {public static void m(E o1){} // Illegalpublic static E o1; // Illegalstatic {E o2; // Illegal}
}

  这里需要注意,泛型类Test内的m方法不是泛型方法/泛型函数。静态方法m由于引用了Test类型参数E而非法;但是如果静态方法m引用自己的类型参数,就是合法的。如果将上面的m改成下面的:

public static <T> void m(T o1){}

  它就是合法的。只不过,修改之后的m已经成为一个泛型方法了。

异常类不能是泛型的

  泛型类不能继承java.lang.Throwable。如果你尝试让一个泛型类继承java.lang.Throwable,编译器会报错。比如 IDEA 就会直接告诉你:Generic class may not extend ‘java.lang.Throwable’.
   如果定义了一个:

public class MyException<T> extends Exception {}

  那么在抛出这个异常的时候,就需要一个try...catch块处理它:

try {...
} catch(MyException<T> ex) {...
}

  在我的理解里,这是没有必要的。因为在运行时,T会被擦除,就和普通的非泛型异常是一样的。所以 java 就直接不让定义泛型类异常了。

下一篇文章 JAVA 学习·泛型(二)——通配泛型。介绍了泛型中最重要的通配泛型,以及它的一些性质、应用。

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

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

相关文章

HA-MAc,透明质酸-甲基丙烯酸酯可用于制备具有交联能力的透明质酸基材料

【基本信息】 Hyaluronate Methacrylate&#xff08;甲基丙烯酸酯化透明质酸&#xff0c;简称HA-MAc&#xff09;是一种重要的生物材料 中文名称&#xff1a;甲基丙烯酸酯化透明质酸、透明质酸-甲基丙烯酸酯 英文名称&#xff1a;Hyaluronate Methacrylate、HA-MAc 分子量&…

matlab实现K均值聚类

在MATLAB中实现聚类分析&#xff0c;可以使用MATLAB内置的聚类函数&#xff0c;如kmeans&#xff08;用于K均值聚类&#xff09;&#xff0c;linkage和cluster&#xff08;用于层次聚类&#xff09;&#xff0c;或者使用MATLAB的统计和机器学习工具箱中的其他函数。 以下是一个…

软件设计师考试---访问控制列表、堆,栈和堆栈、防火墙、数据流图、嵌入式操作、绑定方式、uml、模式、传输协议

访问控制列表 访问控制列表&#xff08;Access Control List&#xff0c;ACL&#xff09; 是一种用于控制对资源&#xff08;如文件、目录、网络资源等&#xff09;访问权限的方法。ACL是在计算机安全领域广泛使用的概念&#xff0c;它允许系统管理员定义哪些用户或系统进程有…

男人圣经 10

男人圣经 10 行业基因 你在对行业、客户群体、事情、核心优势上的高感知力 行业基因 你在对行业、客户群体、事情、核心优势上的高感知力 灵性&#xff0c;我感觉是对人、对事情、对行业的感知力&#xff0c;这就是你的天赋程度。 比如情圣&#xff0c;他比女人更懂自己&am…

python代码自动生成器原理 python 生成器原理

python生成器原理剖析 函数的调用满足“后进先出”的原则&#xff0c;也就是说&#xff0c;最后被调用的函数应该第一个返回&#xff0c;函数的递归调用就是一个经典的例子。显然&#xff0c;内存中以“后进先出”"方式处理数据的栈段是最适合用于实现函数调用的载体&…

使用Maven对Scala独立应用程序进行编译打包

一、 安装Maven 1.解压&#xff0c;移动安装包 sudo tar -zxf ~/apache-maven-3.9.6-bin.tar.gz -C /usr/local/ cd /usr/local/ sudo mv apache-maven-3.9.6/ ./maven-3.9.6 sudo chown -R qiangzi ./maven-3.9.6 二、Scala应用程序代码 1.在终端中执行如下命令创建一个文…

【C++】C++11--- lambda表达式

目录 Lambda表达式概述 Lambda表达式语法定义 Lambda表达式参数详解 Lambda捕获列表 捕获列表总结 Lambda参数列表 可变规则mutable lambda表达式原理 Lambda表达式概述 当对自定义类型的数据集合进行排序时&#xff0c;需要根据自定义类型的不同属性去实现不同的排序方…

MySQL变量声明与使用

#MySQL变量声明与使用 变量命名规范 #1 标识符不能以数字作为开头 #2 只能使用_或着$符号 #3 不允许使用系统关键字 set userName 刘德华; select userName:刘青云;#将赋值与查询结合 查询变量/使用变量 匿名的时候建议加上as select userName as 读取到的userName变量值; 整…

百病之源,根在肝脏!4种养肝法,助您对症养肝,越养越健康~

如今生活节奏比较快&#xff0c;人们的身体和精神都承受着巨大的压力&#xff0c;熬夜加班、喝酒应酬、通宵上网等&#xff0c;这些习惯都在悄悄损耗我们的肝脏&#xff0c;使得大家长期处于亚健康的边缘&#xff01; 中医讲&#xff0c;百病之源&#xff0c;根在肝脏。肝不好…

二总线,替代传统485总线通讯,主站设计

二总线通信设计专栏 《二总线&#xff0c;替代传统485总线通讯&#xff0c;选型及应用-CSDN博客》《二总线&#xff0c;替代传统485总线通讯&#xff0c;低成本直流载波方案实现及原理-CSDN博客》《二总线&#xff0c;替代传统485总线通讯&#xff0c;调试避坑指南之最大的电流…

深度学习:基于TensorFlow 和 Keras,使用神经网络回归模型预测 IPL 分数

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

专业习惯:善于写注释,追求极致

写好注释&#xff0c;是一件极其不容易的事情&#xff0c;也被许多人忽略。 写好注释优点如下&#xff1a;1&#xff09;抽象表达自己要干的事情加深自己的印象&#xff1b;2&#xff09;当再次面对自己的代码时&#xff0c;能够让自己和他人尽快熟悉&#xff0c;减少重复理解原…

css类名冲突-css in js

css in js css in js 的核心思想是&#xff1a;用一个JS对象来描述样式&#xff0c;而不是css样式表 例如下面的对象就是一个用于描述样式的对象&#xff1a; const styles {backgroundColor: "#f40",color: "#fff",width: "400px",height: …

关于linux的进阶配置(mysql)你需要知道(1)-认识mysql

1、基本概念 数据库系统(DBS) 数据库管理系统(DBMS) &#xff1a;SQL server ,mysql 数据库管理员(DBA) 2、经典数据模型: 网状模型 层次模型 关系模型 3、主流的数据库: (1)SQL Server(微软公司产品) 面向Windows操作系统 简单、易用 (2)Oracle(甲骨文公司产品) 面向所有主…

Cesium 问题:billboard 加载未出来

文章目录 问题分析问题 接上篇 Cesium 展示——图标的依比例和不依比例缩放,使用加载 billboard 时,怀疑是路径的原因导致未加载成功 分析 原先

怎样把excel表格转换成图片格式?学会这3个Excel小技巧,表格操作不求人,工作效率翻倍

一&#xff0c;前言 excel是办公必备的表格处理软件&#xff0c;每个表格都包含大量的数据和函数逻辑关系&#xff0c;牵一发而动全身。传输excel表格时可以将文件转换成图片或者pdf&#xff0c;这样有利于传输&#xff0c;而且不会改变表格原有的格式。那么怎样才能把excel转…

精心操作MongoDB:删除数据库的关键步骤和重要事项

当你删除 MongoDB 数据库时&#xff0c;必须确保已经备份了数据并且确认删除操作不会对系统产生负面影响。下面是一个更详细的解释&#xff0c;以及示例代码、应用场景和注意事项。 如何删除数据库 使用命令行 在 MongoDB 的命令行界面中&#xff0c;使用 dropDatabase() 函…

设计必备!六款免费平面图设计软件大盘点

平面设计是一种迷人而多样化的艺术形式&#xff0c;它结合了颜色、形状、排版和创造力&#xff0c;通过图像和文本传达信息。市场上有各种各样的平面设计软件&#xff0c;选择合适的设计软件是成为优秀设计师的重要一步。为了降低软件成本&#xff0c;大多数设计师会优先使用免…

Python专题:六、循环语句(2)

for循环语句 列表可以简单的理解为: 顺序保存的若干元素 注释:变量largest,循环语句for,还有二层缩进八个空格 依次取出counts(列表)里的数字,并赋予给x,判断x和largest数值大小。如果x值更大,则赋值给largest 例:1加到100 range对象可遍历的,后面再讲,range(1…

鸿蒙内核源码分析(编译过程篇) | 简单案例窥视编译全过程

一个.c源文件编译的整个过程如图. 编译过程要经过&#xff1a;源文件 --> 预处理 --> 编译(cc1) --> 汇编器(as) --> 链接器(ld) --> 可执行文件(PE/ELF) GCC GCC&#xff08;GNU Compiler Collection&#xff0c;GNU编译器套件&#xff09;&#xff0c;官网:…