目录
一、前言
大家好,本篇博文是对java——集合篇章的内容补充,主要是给大家讲讲泛型。
注意 : ①代码中的注释也很重要;②不要眼高手低,自己跟着动手敲一遍代码;③点击文章前面的目录或者侧边栏的目录可以进行跳转。良工不示人以朴,up所有文章都会进行适时改进。感谢阅读!
二、为什么需要泛型?
1.用传统的方法创建集合,并对集合进行添加元素等操作时,无法对加入到集合中的元素的类型进行约束,导致有可能在类型转换时出现“类型转换异常”的情况,不安全。
2.用传统方式遍历集合时,如果集合中的数据量较大,频繁的类型转换会降低程序运行的效率。
eg :
up以Intro类为演示类,我们设法向集合中添加几个苹果类对象,但是又意外地添加了一个梨类对象,我们假装自己不知道这个意外,仍然抱着“集合中都是苹果对象”的心态对集合中苹果对象的name属性进行遍历。代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; importjava.util.ArrayList; /*** @author : Cyan_RA9* @version : 21.0*/publicclassIntro { publicstaticvoidmain(String[] args) { //创建集合对象ArrayListarrayList=newArrayList(); //向集合中添加元素arrayList.add(newApple("Green Apple")); arrayList.add(newApple("Banana Apple")); arrayList.add(newApple("Red Apple")); //意外发生了arrayList.add(newPear()); for (Objecto : arrayList) { Appleapple= (Apple) o; System.out.println(apple.getName()); } } } classApple { privateStringname; publicApple(Stringname) { this.name=name; } publicStringgetName() { returnthis.name; } } classPear {}
运行结果 :
显然,IDEA不是格格巫,想把一个梨对象凭空转换成苹果对象是不现实的,果断给你报出“ClassCastException”(类型转换异常)。
三、什么是泛型?
1.泛型的定义 :
1° 泛型,又称参数化类型(ParameterizedType),是一种可以“表示其他数据类型”的数据类型。泛型是JDK5.0中出现的新特性,解决数据类型的安全性问题,在类声明或实例化时只要指定好具体的类型即可。
2° java泛型可以保证——如果程序在编译时没有发出警告,运行时就不会产生类型转换异常(ClassCastException),同时使得代码更加简洁和健壮。
2.泛型的作用 :
1° 可以在类声明时通过一个标识来表示类中的某个属性的数据类型;
2° 可以表示类中某个方法的返回值的数据类型;
3° 可以表示某个方法或者构造器的形参的数据类型;
PS : 可以理解为——将来会用指定的类型替换掉源代码中对应的“泛型”。
eg :
up以Intro_2类为演示类,最终要实现——使用String类型“替换掉”Grape类中给出的泛型,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; /**利用泛型创建对象时,就比如当前情况下,如果直接传入非String类型,会直接报错;而如果仅对编译类型使用了泛型,构造器没有给出泛型,即写成下面这样子——Grape<String> grape = new Grape(141);这时候就会造成——运行期异常—— “ClasCastException”。*/publicclassIntro_2 { publicstaticvoidmain(String[] args) { Grape<String>grape=newGrape<String>("grape"); System.out.println("temp = "+grape.getTemp()); System.out.println("temp's Class = "+grape.getTemp().getClass()); } } classGrape<E> { /*1. E可以表示temp变量的数据类型该类型在定义Grape类对象时可以指定,即在编译期间确定E是什么类型。*/privateEtemp; /*2. E也可以表示形参的数据类型,用法同上。*/publicGrape(Etemp) { this.temp=temp; } /*3. E也可以表示函数的返回值的数据类型,用法同上。*/publicEgetTemp() { returntemp; } }
运行结果 :
其实,当我们创建葡萄类对象给出<String>的泛型时, 达到的效果如下——
classGrape<String> { privateStringtemp; publicGrape(Stringtemp) { this.temp=temp; } publicStringgetTemp() { returntemp; } }
即,在所有泛型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类为演示类,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; importjava.util.ArrayList; importjava.util.Iterator; /*** @author : Cyan_RA9* @version : 21.0*/publicclassGeneric_Demo1 { publicstaticvoidmain(String[] args) { //1.创建集合对象(使用泛型)ArrayList<Integer>arrayList=newArrayList<>(); //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类为演示类,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; importjava.util.ArrayList; /*** @author : Cyan_RA9* @version : 21.0*/publicclassGeneric_Demo2 { publicstaticvoidmain(String[] args) { ArrayList<Phone>phones=newArrayList<>(); phones.add(newPhone<String, Integer>("Huawei", "mate50", 6000)); phones.add(newPhone<String, Integer>("Huawei", "mate40", 4000)); phones.add(newPhone<String, Integer>("Huawei", "mate30", 3500)); System.out.println("phones = "+phones); Phone<String, Integer>apple=newPhone<>("Apple", "iphone 14", 6000); /*泛型方法中,泛型最终表示的实际的数据类型————即传入的形参的类型。(引用类型)*/apple.charge(Integer.valueOf(100)); apple.charge(Long.valueOf(2333)); } } classPhone<T, E> { //使用了泛型的成员变量privateTbrand; privateTmodel; privateEprice; //使用了泛型的构造器publicPhone() {} publicPhone(Tbrand, Tmodel, Eprice) { this.brand=brand; this.model=model; this.price=price; } //使用了泛型的成员方法publicTgetBrand() { returnbrand; } publicvoidsetBrand(Tbrand) { this.brand=brand; } publicTgetModel() { returnmodel; } publicvoidsetModel(Tmodel) { this.model=model; } publicEgetPrice() { returnprice; } publicvoidsetPrice(Eprice) { this.price=price; } //使用了泛型的成员方法的第二种形式public<M>voidcharge(Mm) { /*getClass()方法可以获取当前类的正名(包名 + 类名);而后面再加上getSimpleName()方法就可以直接获取到类的类名。*/System.out.println("传入的引用类型 = "+m.getClass().getSimpleName()); System.out.println("当前的"+getModel() +"手机已经充电了 "+m+" 分钟。"); } publicStringtoString() { 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类为演示类,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; /*** @author : Cyan_RA9* @version : 21.0*/publicclassGeneric_Demo3 { /*PS : 重点不再main方法,请往下看。*/publicstaticvoidmain(String[] args) { Ipad_Airipad_air=newIpad_Air(); ipad_air.charge("iPad Air 5", Long.valueOf(100)); Ipad_Proipad_pro=newIpad_Pro(); ipad_pro.charge("iPad Pro 2022", Integer.valueOf(40)); } } //定义Usb接口interfaceUsb<T, E> { //在接口的(公有抽象)方法中使用泛型publicabstractvoidcharge(Tt, Ee); } //演示1 : 继承接口时确定泛型interfaceIPadextendsUsb<String, Long> { } classIpad_AirimplementsIPad { /*注意!当我们利用快捷键重写Usb接口中的charge方法时,IDEA会自动用String和Long替换掉T和E.*/publicvoidcharge(Strings, LongaLong) { System.out.println("给"+s+"充电 "+aLong+" 分钟吧!"); } } //演示2 : 实现接口时确定泛型classIpad_ProimplementsUsb<String, Integer> { publicvoidcharge(Strings, Integerinteger) { System.out.println(s+"设备"+"已充电 "+integer+" 分钟。"); } }
运行结果 :
5.自定义泛型方法 :
1° 基本语法 :
修饰符<T, R...> 返回值类型 方法名(形参列表) { //形参列表往往会使用定义好的泛型
//body
}
2° 使用细节 :
①自定义泛型方法,既可以定义在普通类中,也可以定义在泛型类中。
②泛型最终调用的数据类型是在调用方法时确定的。
③每次调用泛型方法,都可以指定不同的泛型类型。
④注意区分自定义泛型方法 和 泛型在方法上的应用。
eg :
//以下代码仅作为演示,无实际意义class<T, U>Watermelon { public<K>voidtaste(Tt, Uu, Kk) { 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类为演示类,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; importjava.util.ArrayList; importjava.util.List; /*** @author : Cyan_RA9* @version : 21.0*/publicclassGeneric_Demo4 { publicstaticvoidmain(String[] args) { nutrition1(newArrayList<Object>()); nutrition1(newArrayList<Fruit>()); nutrition1(newArrayList<Banana>()); nutrition1(newArrayList<GreenBanana>()); /*不报错,因为没有进行类型约束。*/System.out.println("=====================================s"); //nutrition2(new ArrayList<Object>());/*报错,因为Object不属于“Fruit类及其子类”的范畴。*/nutrition2(newArrayList<Fruit>()); nutrition2(newArrayList<Banana>()); nutrition2(newArrayList<GreenBanana>()); System.out.println("=====================================s"); nutrition3(newArrayList<Object>()); nutrition3(newArrayList<Fruit>()); //nutrition3(new ArrayList<Banana>());/*报错,因为Banana类不属于“Fruit类及其父类”的范畴。*///nutrition3(new ArrayList<GreenBanana>());/*同样报错,因为GreenBanana类不属于“Fruit类及其父类”的范畴。*/ } //通配符使用情况一 :publicstaticvoidnutrition1(List<?>fruit) { System.out.println("水果营养丰富,富含维生素!"); } //通配符使用情况二 :publicstaticvoidnutrition2(List<?extendsFruit>fruit) { System.out.println("水果营养丰富,富含维生素!"); } //通配符使用情况三 :publicstaticvoidnutrition3(List<?superFruit>fruit) { System.out.println("水果营养丰富,富含维生素!"); } } classFruit { } classBananaextendsFruit { } classGreenBananaextendsBanana { }
注意看上面代码中的注释,大家也可以把代码复制下来自己去试试。
3.关于JUnit框架 :
①为什么需要JUnit?
平时我们在写程序时,往往一个main方法中会测试很多功能代码(比如说很多方法),所以我们经常会注释掉某部分已经测试完毕的功能代码,以便于测试其他的功能代码。但是,假如一个类中有很多功能代码要测试,我们就不得不频繁地注释与反注释,非常麻烦。
②什么是JUnit?
JUnit是一个java语言提供的单元测试框架,目前多数的java开发环境中,都已集成了JUnit作为单元测试的工具。
③怎么使用JUnit?
up以Generic_Demo5类为演示类,代码如下 :
packagecsdn.knowledge.api_tools.gather.generic; publicclassGeneric_Demo5 { publicstaticvoidmain(String[] args) { } publicvoidf1() { System.out.println("f1方法被调用"); } publicvoidf2() { System.out.println("f2方法被调用"); } publicvoidf3() { 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分享过的知识。感谢阅读!