java 泛型 万字详解(通俗易懂)
目录
一、前言
大家好,本篇博文是对java——集合篇章的内容补充,主要是给大家讲讲泛型。
注意 : ①代码中的注释也很重要;②不要眼高手低,自己跟着动手敲一遍代码;③点击文章前面的目录或者侧边栏的目录可以进行跳转。良工不示人以朴,up所有文章都会进行适时改进。感谢阅读!
二、为什么需要泛型?
1.用传统的方法创建集合,并对集合进行添加元素等操作时,无法对加入到集合中的元素的类型进行约束,导致有可能在类型转换时出现“类型转换异常”的情况,不安全。
2.用传统方式遍历集合时,如果集合中的数据量较大,频繁的类型转换会降低程序运行的效率。
eg :
up以Intro类为演示类,我们设法向集合中添加几个苹果类对象,但是又意外地添加了一个梨类对象,我们假装自己不知道这个意外,仍然抱着“集合中都是苹果对象”的心态对集合中苹果对象的name属性进行遍历。代码如下 :
package csdn.knowledge.api_tools.gather.generic;
import java.util.ArrayList;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class Intro {
public static void main(String[] args) {
//创建集合对象
ArrayList arrayList = new ArrayList();
//向集合中添加元素
arrayList.add(new Apple("Green Apple"));
arrayList.add(new Apple("Banana Apple"));
arrayList.add(new Apple("Red Apple"));
//意外发生了
arrayList.add(new Pear());
for (Object o : arrayList) {
Apple apple = (Apple) o;
System.out.println(apple.getName());
}
}
}
class Apple {
private String name;
public Apple(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
class Pear {}
运行结果 :
显然,IDEA不是格格巫,想把一个梨对象凭空转换成苹果对象是不现实的,果断给你报出“ClassCastException”(类型转换异常)。
三、什么是泛型?
1.泛型的定义 :
1° 泛型,又称参数化类型(ParameterizedType),是一种可以“表示其他数据类型”的数据类型。泛型是JDK5.0中出现的新特性,解决数据类型的安全性问题,在类声明或实例化时只要指定好具体的类型即可。
2° java泛型可以保证——如果程序在编译时没有发出警告,运行时就不会产生类型转换异常(ClassCastException),同时使得代码更加简洁和健壮。
2.泛型的作用 :
1° 可以在类声明时通过一个标识来表示类中的某个属性的数据类型;
2° 可以表示类中某个方法的返回值的数据类型;
3° 可以表示某个方法或者构造器的形参的数据类型;
PS : 可以理解为——将来会用指定的类型替换掉源代码中对应的“泛型”。
eg :
up以Intro_2类为演示类,最终要实现——使用String类型“替换掉”Grape类中给出的泛型,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
/**
利用泛型创建对象时,就比如当前情况下,如果直接传入非String类型,会直接报错;
而如果仅对编译类型使用了泛型,构造器没有给出泛型,即写成下面这样子——
Grape<String> grape = new Grape(141);
这时候就会造成——运行期异常—— “ClasCastException”。
*/
public class Intro_2 {
public static void main(String[] args) {
Grape<String> grape = new Grape<String>("grape");
System.out.println("temp = " + grape.getTemp());
System.out.println("temp's Class = " + grape.getTemp().getClass());
}
}
class Grape<E> {
/*
1. E可以表示temp变量的数据类型
该类型在定义Grape类对象时可以指定,即在编译期间确定E是什么类型。
*/
private E temp;
/*
2. E也可以表示形参的数据类型,用法同上。
*/
public Grape(E temp) {
this.temp = temp;
}
/*
3. E也可以表示函数的返回值的数据类型,用法同上。
*/
public E getTemp() {
return temp;
}
}
运行结果 :
其实,当我们创建葡萄类对象给出<String>的泛型时, 达到的效果如下——
class Grape<String> {
private String temp;
public Grape(String temp) {
this.temp = temp;
}
public String getTemp() {
return temp;
}
}
即,在所有泛型E出现的地方,E都被替换为了String。
四、怎么用泛型?
1.泛型的语法 :
1° interface 接口名<T> 或者 interface 接口名<K, V>
2° class 类名<E> 或者 class 类名<K, V>
Δ注意 :
①尖括号<>中可以填写任意字母作为泛型的标识符,一般均为大写,常用T,K和V,分别表示Type,Key和Value。
②字母本身不代表任何值,而代表类型,即程序员手动指定的数据类型。
③在指定泛型时,必须要求最终确定的数据类型为引用类型,不可以是基本数据类型。
④实际传入的类型可以是泛型指定类型的子类型。
⑤若在定义类时使用了泛型,实例化该类时却什么都没有传入,默认使用Object类型。
2. 泛型的使用 :
从JDK7开始,等号后边的泛型可以不用写了,称为“菱形泛型”。
以上文“泛型的作用”中的代码为例,我们可以将其改写为如下的形式——
Grape<String> grape = new Grape<>("grape"); //菱形泛型
在实际开发中,菱形泛型的使用非常广泛,因此,推荐这种写法。
up以Generic_Demo1类为演示类,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
import java.util.ArrayList;
import java.util.Iterator;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class Generic_Demo1 {
public static void main(String[] args) {
//1.创建集合对象(使用泛型)
ArrayList<Integer> arrayList = new ArrayList<>();
//2.向集合中添加元素
arrayList.add(141);
arrayList.add(141);
arrayList.add(5);
arrayList.add(11);
arrayList.add(233);
arrayList.add(233);
//3.遍历集合
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext())
{
System.out.println(iterator.next());
} //不再需要进行类型转换
}
}
运行结果 :
3.自定义泛型类 :
其实,我们在上文中举过的例子———
class Grape<E> {
}
———就是自定义泛型的应用。
1° 基本语法 :
class 类名<T, R, E...> { //可以同时定义多个泛型
//类体
}
2° 使用细节 :
①类的非静态成员可以使用泛型(属性,方法,构造器等)。
②静态成员中不可以使用类的泛型。
原因也很简单,我们在“代码块”一文中讲过,static修饰的成员的初始化——在类加载时就会执行完毕,而泛型最终代表的数据类型是在创建对象时才确定的,所以,jvm在对静态成员初始化时,无法得知它们的实际类型,也就没法儿初始化了。
③在自定义泛型类中,使用了泛型的数组只可以定义,不可以被初始化。
因为jvm无法确定数组的实际类型,也就没法在内存中开辟空间。
④自定义泛型类中,泛型最后代表的数据类型是在创建对象时确定的。
⑤如果在创建泛型类对象时没有给出指定类型,默认以Object替代。
eg :
up以Generic_Demo2类为演示类,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
import java.util.ArrayList;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class Generic_Demo2 {
public static void main(String[] args) {
ArrayList<Phone> phones = new ArrayList<>();
phones.add(new Phone<String, Integer>("Huawei", "mate50", 6000));
phones.add(new Phone<String, Integer>("Huawei", "mate40", 4000));
phones.add(new Phone<String, Integer>("Huawei", "mate30", 3500));
System.out.println("phones = " + phones);
Phone<String, Integer> apple = new Phone<>("Apple", "iphone 14", 6000);
/*
泛型方法中,泛型最终表示的实际的数据类型————即传入的形参的类型。(引用类型)
*/
apple.charge(Integer.valueOf(100));
apple.charge(Long.valueOf(2333));
}
}
class Phone<T, E> {
//使用了泛型的成员变量
private T brand;
private T model;
private E price;
//使用了泛型的构造器
public Phone() {}
public Phone(T brand, T model, E price) {
this.brand = brand;
this.model = model;
this.price = price;
}
//使用了泛型的成员方法
public T getBrand() {
return brand;
}
public void setBrand(T brand) {
this.brand = brand;
}
public T getModel() {
return model;
}
public void setModel(T model) {
this.model = model;
}
public E getPrice() {
return price;
}
public void setPrice(E price) {
this.price = price;
}
//使用了泛型的成员方法的第二种形式
public<M> void charge(M m) {
/*
getClass()方法可以获取当前类的正名(包名 + 类名);
而后面再加上getSimpleName()方法就可以直接获取到类的类名。
*/
System.out.println("传入的引用类型 = " + m.getClass().getSimpleName());
System.out.println("当前的" + getModel() + "手机已经充电了 " + m + " 分钟。");
}
@Override
public String toString() {
return "\nPhone{" +
"brand=" + brand +
", model=" + model +
", price=" + price +
'}';
}
}
运行结果 :
如果我们在静态成员中使用泛型,IDEA会报错,如下图所示 :
4.自定义泛型接口 :
1° 基本语法 :
interface 接口名<T, R, E...> { //可以同时定义多个泛型
//body
}
2° 使用细节 :
①同自定义泛型类一个道理,自定义泛型接口的静态成员也不能使用泛型。
②自定义泛型接口中,泛型最终代表的数据类型是在继承该接口或者实现该接口时确定的。
③若在使用时没有给出具体泛型,默认使用Object类型替代。
PS : 尽管默认以Object类型替换,但是建议——在不指定泛型的情况下,手动添加<Object>泛型标识符。这样在开发中,不管是领导还是你的同事,别人一看你的代码就知道是怎么回事。
eg :
up以Generic_Demo3类为演示类,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class Generic_Demo3 {
/*
PS : 重点不再main方法,请往下看。
*/
public static void main(String[] args) {
Ipad_Air ipad_air = new Ipad_Air();
ipad_air.charge("iPad Air 5", Long.valueOf(100));
Ipad_Pro ipad_pro = new Ipad_Pro();
ipad_pro.charge("iPad Pro 2022", Integer.valueOf(40));
}
}
//定义Usb接口
interface Usb<T, E> {
//在接口的(公有抽象)方法中使用泛型
public abstract void charge(T t, E e);
}
//演示1 : 继承接口时确定泛型
interface IPad extends Usb<String, Long> {
}
class Ipad_Air implements IPad {
/*
注意!
当我们利用快捷键重写Usb接口中的charge方法时,IDEA会自动用String和Long替换掉T和E.
*/
@Override
public void charge(String s, Long aLong) {
System.out.println("给" + s + "充电 " + aLong + " 分钟吧!");
}
}
//演示2 : 实现接口时确定泛型
class Ipad_Pro implements Usb<String, Integer> {
@Override
public void charge(String s, Integer integer) {
System.out.println(s + "设备" + "已充电 " + integer + " 分钟。");
}
}
运行结果 :
5.自定义泛型方法 :
1° 基本语法 :
修饰符<T, R...> 返回值类型 方法名(形参列表) { //形参列表往往会使用定义好的泛型
//body
}
2° 使用细节 :
①自定义泛型方法,既可以定义在普通类中,也可以定义在泛型类中。
②泛型最终调用的数据类型是在调用方法时确定的。
③每次调用泛型方法,都可以指定不同的泛型类型。
④注意区分自定义泛型方法 和 泛型在方法上的应用。
eg :
//以下代码仅作为演示,无实际意义
class<T, U> Watermelon {
public<K> void taste(T t, U u, K k) {
System.out.println("T和U代表泛型在方法上的应用;而K则是自定义泛型方法的使用。");
}
}
五、泛型内容延申
1.关于继承性 :
举个例子,如下图所示 :
up在创建ArrayList对象时使用了泛型,但是没有采用“菱形泛型”的形式,而是在编译类型中给出了<Object>的泛型,在运行类型中给出了<Interger>。
这时,IDEA报错,显示所需的类型和提供的类型不一致。这说明什么?
不会因为Integer类型是Object类型的子类就通过编译,即编译类型的泛型和运行类型的泛型必须统一。泛型本身不存在继承性。
2.关于通配符 :
其实up在之前的“反射”一文中已经提到过通配符。通配符是一个问号,有以下三种使用场景——
1° <?> : 单独使用,表示支持任意泛型类型。
2° <? extends A> : 表示支持A类以及A类的子类,规定了泛型的上限;
3° <? super A> : 表示支持A类以及A类的父类,规定了泛型的下限。
eg :
up以Generic_Demo4类为演示类,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
import java.util.ArrayList;
import java.util.List;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class Generic_Demo4 {
public static void main(String[] args) {
nutrition1(new ArrayList<Object>());
nutrition1(new ArrayList<Fruit>());
nutrition1(new ArrayList<Banana>());
nutrition1(new ArrayList<GreenBanana>());
/*
不报错,因为没有进行类型约束。
*/
System.out.println("=====================================s");
//nutrition2(new ArrayList<Object>());
/*
报错,因为Object不属于“Fruit类及其子类”的范畴。
*/
nutrition2(new ArrayList<Fruit>());
nutrition2(new ArrayList<Banana>());
nutrition2(new ArrayList<GreenBanana>());
System.out.println("=====================================s");
nutrition3(new ArrayList<Object>());
nutrition3(new ArrayList<Fruit>());
//nutrition3(new ArrayList<Banana>());
/*
报错,因为Banana类不属于“Fruit类及其父类”的范畴。
*/
//nutrition3(new ArrayList<GreenBanana>());
/*
同样报错,因为GreenBanana类不属于“Fruit类及其父类”的范畴。
*/
}
//通配符使用情况一 :
public static void nutrition1(List<?> fruit) {
System.out.println("水果营养丰富,富含维生素!");
}
//通配符使用情况二 :
public static void nutrition2(List<? extends Fruit> fruit) {
System.out.println("水果营养丰富,富含维生素!");
}
//通配符使用情况三 :
public static void nutrition3(List<? super Fruit> fruit) {
System.out.println("水果营养丰富,富含维生素!");
}
}
class Fruit {
}
class Banana extends Fruit {
}
class GreenBanana extends Banana {
}
注意看上面代码中的注释,大家也可以把代码复制下来自己去试试。
3.关于JUnit框架 :
①为什么需要JUnit?
平时我们在写程序时,往往一个main方法中会测试很多功能代码(比如说很多方法),所以我们经常会注释掉某部分已经测试完毕的功能代码,以便于测试其他的功能代码。但是,假如一个类中有很多功能代码要测试,我们就不得不频繁地注释与反注释,非常麻烦。
②什么是JUnit?
JUnit是一个java语言提供的单元测试框架,目前多数的java开发环境中,都已集成了JUnit作为单元测试的工具。
③怎么使用JUnit?
up以Generic_Demo5类为演示类,代码如下 :
package csdn.knowledge.api_tools.gather.generic;
public class Generic_Demo5 {
public static void main(String[] args) {
}
public void f1() {
System.out.println("f1方法被调用");
}
public void f2() {
System.out.println("f2方法被调用");
}
public void f3() {
System.out.println("f3方法被调用");
}
}
我们先在要测试的方法前输入@Test,如下图所示 :
这时候IDEA是会报错的,因为我们需要引入相应的组件,使用Alt + Enter快捷键,如下图所示 :
选择导入'JUnit5.8'(一般都使用5.8,而不是4的版本)。进入以后点击OK,首次导入会等待一段时间,如下图所示 :
大概几十秒后,就会显示导入成功了,如下图所示 :
导入成功后,我们会发现——在原先@Test标注的方法上,出现了一个绿色的小箭头的标志,如下图所示 :
点击绿色小箭头我们就可以实现对该方法进行单独地运行或者Debug,如下图所示 :
运行结果 :
并且,当我们已经导入JUnit后,为下一个方法标注“@Test”时就不需要重新导入和等待了,直接就可以标注,如下GIF图演示 :
发现没有,在JUnit框架的帮助下,我们既不是通过设置静态方法来调用,也没有通过创建对象来调用,而是直接指定的可以运行或者Debug某个方法,并且一个方法的调用不会影响其他的方法。这么一来就可以轻松解决我们上文中提出的问题。
六、完结撒❀
🆗,以上就是java——泛型的全部内容了。泛型与集合配合使用的频率非常高,所以up将泛型作为了集合篇章的内容补充。而至此,整个集合篇章的内容已全部讲完。之后,up会单独为集合篇章出一篇总结性质的博文,为大家整理一下集合框架中涉及到的up分享过的知识。感谢阅读!
- 点赞
- 收藏
- 关注作者
评论(0)