前言
上次我们说了Java常用工具类的集合框架部分,是非常重要的一个Java工具,同时让我很高兴的是上一篇也是上了热榜
如果感兴趣,大家也可以去看看 Java集合框架
言归正传,今天我们来说泛型,泛型在我们写程序的时候的身影那也是无处不见,它的重要性也就不言而喻了。好好学习,次次都能AC!接下来我们正式开始泛型学习。
初识泛型
什么是泛型?
当然我们学习每一个东西,首先要知道的就是要学的是什么东西,所以今天泛型的学习也是一样的,我们先知道什么是泛型。
其实泛型我们已经在前面的学习当中已经接触过了,就是 <> ,不知道你是否还记得这个:
Collection c
1
上次你看到这个可能不太理解,因为这个东西就涉及到了你还没有学习过的泛型。今天学完知道你一定会明白的。
泛型其实就是一个我们常用到的Java工具,对它的定义为:
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
是不是,还是不太理解,有点笼统,接下由我来解释一下。
泛型,其实就是“参数化类型”。
一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
那参数化类型又是什么呢?
原来的参数是一个个具体的参数,在使用泛型之后就是把这些具体的参数类型化。让参数变为一个类型,让参数不在受具体形式的限制。
为什么要学习泛型?
其实泛型在jdk5之后才出来的,在泛型这个工具出来之前,泛型程序设计是使用继承来实现的。
这样的话有两个坏处:
1.需要强制转换
2.可向集合中添加任意类型的对象,存在风险
比如说:
class Text6{ private Object x ; public Text6(Object x){ this.setX(x); } public void setX(Object x){ this.x = x ; } public Object getX(){ return this.x ; }public class Text_6 { public static void main(String[] args){ Text6 text6 = new Text6(new String("abc")); Integer a = (Integer) text6.getX(); } }
这个代码在编译阶段是没有报错的,但是它真的对吗?
当然不对String类怎么能强制转换为Integer呢?
所以当程序运行时候肯定会报错的!如下:
getX()方法返回类型是Object,Object又是所有类的父类,所以这里向下转型在编译时候肯定不会报错的,但是我们传入的是Integer类的数据,它是无法转换为String类的,所以运行时候就会报错。
这种错误在程序篇幅小的时候还是很容易发现的,但是如果程序有上千行那么就会很不容易发现,这对程序员是十分不友好的,所以我们引入了泛型,它既可以帮助我们实现多态又可以帮助我们在编译阶段就帮我们发现这种错误!
初级使用泛型
泛型的使用方式有三种,分别是:泛型类,泛型接口,泛型方法
下面将一一介绍它们的具体实现
泛型类
在使用泛型类时候在类名后面加一个尖括号,括号里是一个大写字母,可以任意大写字母,因为意义是相同。
这个大写字母表示派生自Object类的任何类,比如String,Integer,Double等等(包括自定义类)
至于这个大写字母具体代表什么类型,就得看传入时候是传入什么类型了。
具体怎么用我们看个实例来理解记忆。
对于之前的实例我们使用泛型之后可以变为:
class Text6{ private T x ; public Text6(T x){ this.setX(x); } public void setX(T x){ this.x = x ; } public T getX(){ return this.x ; } } public class Text_6 { public static void main(String[] args){ Text6 text6 = new Text6(new String("abc")); Integer a = (Integer) text6.getX();//报错 } }
先看使用泛型之后的变化:
在类名后面加了
在Text6()方法中原来所有的具体类型全部被大写字母T替换了
在创建对象时候对象引用和实例化处都加了
在实例化时候传入了一个String类型的对象
Integer a = (Integer) text6.getX();在编译阶段报错
1
根据这几个变化我们可以总结一下使用泛型类的用法:
1.想要把一个类变为泛型类只需要在类型后加<大写字母>即可,在变为泛型类之后,我们就可以向这个泛型类里面传入我们需要的类型的参数,这个类型可以用在任何地方如:形参类型,变量类型,返回值类型等等。它就是一个类型(类型由传入处决定)。
2.在使用泛型类时,构建泛型类的对象时候我们需要在引用处和实例化处添加<具体类型>如:
java Text6 text6 = new Text6();
也可以把后面<>中类型省略不写,这样就默认为前面<>中的类型,如:
java Text6 text6 = new Text6<>();
他们是等价的,但是有一个原则就是前后<>中类型必须要一致,否则报错!
3.在使用泛型后编译器在编译阶段就可以检测出来类型转换的错误,如:
在传入参数时也不用进行强制转换,但是传入的参数必须是对象
泛型类也可以又自己的具体类型,比如在这个题中,不是所有的类型都必须T,也可以自己定义一些int 之类的类型。
泛型类与非泛型类之间的区别就是,泛型类可以在使用它是传入一个类型并使用。其他的属性方法都可以和普通类相同。
泛型接口
泛型接口与泛型类的定义及使用基本相同,但是也有所变化。
1.接口未传入泛型实参时,与泛型类的定义相同,如:
public interface Baibai { public T shuai(); }
2.在类实现接口的时候,需将泛型的声明也一起加到类中去,如:
class Abiabi implements Baibai{ public T shuai(){ return null; } }
3.传入泛型实参时,虽然我们只创建了一个泛型类接Baibai ,但是我们可以为T传入无数个实参,形成无数种类型的Baibai接口。
4.在实现类实现泛型接口时,如已经将泛型类型换为实参类型,则所有使用泛型的地方都要替换成换成的实参类型。
class Abiabi implements Baibai{ @Override public String shuai(){ return "白白超牛"; } }
泛型方法
我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,大家非常容易分不清什么是泛型方法,这个特别容易混淆。
泛型类,是在实例化类的时候指明泛型的具体类型。
泛型方法,是在调用方法的时候指明泛型的具体类型 。
我们先来定义一个泛型方法,大家看看定义的格式与方法:
public T Print(T t){ return t; }
1.public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
2.只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3.表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
4.与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
静态方法与泛型
在方法中静态方法比较特殊,当然静态方法使用泛型也就比较特殊了
众所周知静态方法只能访问静态变量,在类中的静态方法使用泛型时:
静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
泛型进阶
学完了泛型类,泛型接口,泛型方法是不是觉得就这?,,就这?
当然不止就这,接下来学习泛型相关的高级货(手动狗头)
泛型通配符
之前说过在<>中只需要是大写字母就可以,虽然是这样的,但是我们一般使用时都有一定的意义和规律。
常用的泛型通配符有T,E,K,V,?,它们一般是这样约定的:
?表示不确定的 java 类型
T (type) 表示具体的一个java类型
K V (key value) 分别代表java键值中的Key Value
E (element) 代表Element
都很容易理解,除了?这个通配符,接下来就来介绍一下吧
不使用泛型时:
import java.util.ArrayList; import java.util.List; public class Test { public static void main (String[] args) throws java.lang.Exception { List list = new ArrayList(); //List listN = new ArrayList(); 编译错误。List 并不是 ArrayList 的父类。 } }
无边界的通配符(Unbounded Wildcards)
采用 的形式,比如 List,无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
使用通配符 ?示例:
import java.util.ArrayList; import java.util.List; class Food {} class Fruit extends Food {} class Apple extends Fruit {} public class Test { public static void main (String[] args) throws java.lang.Exception { List list = new ArrayList(); //list.add(new Food()); 编译错误 //list.add(new Fruit()); 编译错误 //list.add(new Apple()); 编译错误 list.add(null); //Food food = list.get(0); //Fruit fruit = list.get(0); //Apple apple = list.get(0); Object object = list.get(0); } }
固定上边界的通配符(Upper Bounded Wildcards)
使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。要声明使用该类通配符,采用 的形式,这里的 E就是该泛型的上边界。
使用上界通配符 示例:
import java.util.ArrayList; import java.util.List; class Food {} class Fruit extends Food {} class Apple extends Fruit {} public class Test { public static void main (String[] args) throws java.lang.Exception { List list = new ArrayList(); // List listA = new ArrayList(); 编译错误。不能为父类。 List listN = new ArrayList(); listN.add(null); //listN.add(123); 不能add Fruit fruit = listN.get(0); Food food = listN.get(0); //Apple apple = listN.get(0); 编译错误。get获取的值,只能给父类 listN.remove(0); } }
注意:这里虽然用的是 extends 关键字,却不仅限于继承了父类 E 的子类,也可以代指实现了接口 E 的类。
固定下边界的通配符(Lower Bounded Wildcards)
使用固定下边界的通配符的泛型,就能够接受指定类及其父类类型的数据。要声明使用该类通配符,采用 的形式,这里的 E就是该泛型的下边界。
注意:你可以为一个泛型指定上边界或下边界,但是不能同时指定上下边界。
使用下界通配符 示例:
import java.util.ArrayList; import java.util.List; class Food {} class Fruit extends Food {} class Apple extends Fruit {} public class Test { public static void main (String[] args) throws java.lang.Exception { List list = new ArrayList(); List listA = new ArrayList(); //List listN = new ArrayList(); 编译错误,不能为子类 listA.add(new Fruit()); //listA.add(new Food()); 编译错误,不能为父类。 listA.add(new Apple()); Object object = listA.get(0); //Fruit fruit = listA.get(0);编译错误。 //Food food = listA.get(0);编译错误。 //Apple apple = listA.get(0); 编译错误。 } }