Java基础----泛型
泛型问题的引出
在我之前的博客中介绍了Object类型,Object类型可以接收全部数据类型,实现参数统一。如果对Object类不够熟悉的话可以查看:Java基础—Object类、包装类
虽然使用Object类型可以方便的表示所有类型,但在实际代码运用中可能会存在问题,首先,我们来看一看不同类型的数据使用Object类进行保存的过程。
保存整型:Object int
保存浮点型:Object double
实现存储
import java.util.ArrayList;
import java.util.List;
//定义一个存储数据的Number类
class Number{
//将数据设置为Object类型
private Object X;
private Object Y;
public Object getX() {
return X;
}
public Object getY() {
return Y;
}
public void setX(Object x) {
X = x;
}
public void setY(Object y) {
Y = y;
}
}
public class Test01{
public static void main(String[] args) {
List list = new ArrayList();
Number number = new Number();
number.setX(20);
number.setY(60);
//向下转型并且自动拆箱处理
int x = (Integer)number.getX();
int y = (Integer)number.getY();
list.add(x);
list.add(y);
System.out.println(list);
}
}
运行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=54411:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[20, 60]
Process finished with exit code 0
注:以上代码中向下转型并且自动拆箱等价于:
//向下转型
Integer integer = (Integer)number.getX();
//自动拆箱
int x = integer;
但此时代码也存在着问题,Object描述的范围很广,在set方法中我们可以传输进入任何类型的数据,我们将set方法中传入不同的数据时,修改代码如下:
public class Test01{
public static void main(String[] args) {
List list = new ArrayList();
Number number = new Number();
//传入浮点数:1.6
//传入字符串:张大炮
number.setX(1.6);
number.setY("张大炮");
int x = (Integer)number.getX();
int y = (Integer)number.getY();
list.add(x);
list.add(y);
System.out.println(list);
}
}
运行结果为:
D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=56022:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
Exception in thread "main" java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Integer (java.lang.Double and java.lang.Integer are in module java.base of loader 'bootstrap')
at zhang.da.pao.Test01.main(Test01.java:1624)
Process finished with exit code 1
程序在运行的时候报错了,我们在set输入数据内容的时候输入了错误的数据类型,但在编译中程序是不会报错的,只有在程序执行的时候出错,所以Object在使用的过程中存在隐患;我们需要一种参数类型定义,泛型出现了。
泛型
泛型的定义
泛型是在JDK 5时就引入的新特性,也就是“参数化类型”,类中的属性类型或方法的参数类型使用参数来定义,使用或者调用的时候再传入具体的类型。
泛型的本质是为了参数化类型(在不创建新类型的前提下,通过泛型指定的不同类型来控制形参具体的类型)。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的必要性
未使用泛型时,可以通过Object来实现参数的“任意化”,但这样做的缺点就是需要显式的强制类型转换,这就需要开发者知道实际的类型。而强制类型转换是会出现错误的,比如Object将实际类型为String,强转成Integer。编译期是不会提示错误的,而在运行时就会抛出异常,很明显的安全隐患。Java通过引入泛型机制,将上述的隐患提前到编译期进行检查,开发人员既可明确的知道实际类型,又可以通过编译期的检查提示错误,从而提升代码的安全性和健壮性。
使用泛型
让我们来回忆一下刚刚的实例存在的问题,在上面的实例中我们使用了Object作为属性的类型,在传入参数的过程中出现了类型不匹配的问题,现在我们使用泛型来修改上面的代码,结果如下:
import java.util.ArrayList;
import java.util.List;
class Number<T>{
private T X;
private T Y;
public T getX() {
return X;
}
public T getY() {
return Y;
}
public void setX(T x) {
X = x;
}
public void setY(T y) {
Y = y;
}
}
public class Test01{
public static void main(String[] args) {
List list = new ArrayList();
Number<Integer> number = new Number();
number.setX(1.6);
number.setY("张大炮");
int x = (Integer)number.getX();
int y = (Integer)number.getY();
list.add(x);
list.add(y);
System.out.println(list);
}
}
此时我们在Number类上定义了泛型,在实例化类对象的时候将标记更改为类型,使用泛型后,所有的程序之中具有泛型标记的部分都会更改为相应的类型标记。
而且如截图所示,在实例化对象后,传入的数据类型与规定的类型不符时程序编译报错。
但需要注意的是,泛型类型指派的时候只能使用引用数据类型而不能够使用基本数据类型。
将程序修改为正确的形式,使用泛型可以在实例化对象时修改为任意想要的引用类型。代码如下:
public class Test01{
public static void main(String[] args) {
List list = new ArrayList();
Number<Double> numberB = new Number();
Number<String> numberC = new Number();
numberB.setX(1.6);
numberC.setY("张大炮");
double x = (Double)numberB.getX();
String y = (String)numberC.getY();
list.add(x);
list.add(y);
System.out.println(list);
}
}
泛型通配符
在使用泛型时经常会看到T、E、K、V这些通配符,它们代表着什么含义呢?
本质上它们都是通配符,并没有什么区别,换成A-Z之间的任何字母都可以。不过在开发者之间倒是有些不成文的约定:
·T (type): 表示具体的一个java类型;
·K V (key value) :分别代表java键值中的Key Value;
·E (element) :代表Element;
在使用泛型类时, 我们可以使用一个具体的类型, 例如可以定义一个 List 的对象, 我们的泛型参数就是 Integer; 我们也可以使用通配符 ? 来表示一个未知类型, 例如 List<?> 就表示了泛型参数是某个类型, 只不过我们并不知道它的具体类型时什么。
List<?>所声明的就是所有类型都是可以的, 但需要注意的是, List<?>并不等同于List. 对于 List 来说, 它实际上确定了 List 中包含的是 Object 及其子类, 我们可以使用 Object 类型来接收它的元素. 相对地, List<?> 则表示其中所包含的元素类型是不确定, 其中可能包含的是 String, 也可能是 Integer. 如果它包含了 String 的话, 往里面添加 Integer 类型的元素就是错误的作为对比, 我们可以给一个 List 添加 String 元素, 也可以添加 Integer 类型的元素, 因为它们都是 Object 的子类。
List<?> list = new ArrayList<E>();
list.add(new Object());
//报错,List<?>甚至连 Object都不让放!
正因为类型未知, 我们就不能通过 new ArrayList<?>() 的方法来创建一个新的ArrayList 对象, 因为编译器无法知道具体的类型是什么. 但是对于 List<?> 中的元素, 我们却都可以使用 Object 来接收, 因为虽然类型未知, 但肯定是Object及其子类。这么看来,还可以取出操作,只能执行Object类型元素!!
泛型范围
设置上界:? extends T
? extends T 描述了通配符上界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的子类, 例如:
List<? extends Number> numberArray = new ArrayList<Number>(); // Number 是 Number 类型的
List<? extends Number> numberArray = new ArrayList<Integer>(); // Integer 是 Number 的子类
List<? extends Number> numberArray = new ArrayList<Double>(); // Double 是 Number 的子类
上面三个操作都是合法的, 因为 ? extends Number 规定了泛型通配符的上界, 即我们实际上的泛型必须要是 Number 类型或者是它的子类, 而 Number, Integer, Double 显然都是 Number 的子类(类型相同的也可以, 即这里我们可以认为 Number 是 Number 的子类)。
读取操作:
我们能够从 numberArray 中读取到 Number 对象, 因为 numberArray 中包含的元素是 Number 类型或 Number 的子类型.
我们不能从 numberArray 中读取到 Integer 类型, 因为 numberArray 中可能保存的是 Double 类型.
写入操作:
我们不能添加 Number 到 numberArray 中, 因为 numberArray 有可能是List< Double > 类型
我们不能添加 Integer 到 numberArray 中, 因为 numberArray 有可能是 List< Double > 类型
我们不能添加 Double 到 numberArray 中, 因为 numberArray 有可能是 List< Integer > 类型
设置下界:? super T
? super T 描述了通配符下界, 即具体的泛型参数需要满足条件: 泛型参数必须是 T 类型或它的父类例如:
// 在这里, Integer 可以认为是 Integer 的 “父类”
List<? super Integer> array = new ArrayList();
// Number 是 Integer 的 父类
List<? super Integer> array = new ArrayList();
// Object 是 Integer 的 父类
List<? super Integer> array = new ArrayList();
关于读取:
我们不能保证可以从 array 对象中读取到 Integer 类型的数据, 因为 array 可能是 List 类型的.
我们不能保证可以从 array 对象中读取到 Number 类型的数据, 因为 array 可能是 List 类型的.
唯一能够保证的是, 我们可以从 array 中获取到一个 Object 对象的实例
关于写入:
我们可以添加 Integer 对象到 array 中, 也可以添加 Integer 的子类对象到 array 中.
我们不能添加 Double/Number/Object 等不是 Integer 的子类的对象到 array 中.
通配符总结
List<? super Integer>A = ...
List<? extends Integer> B = ..
? super Integer 和 ? extends Integer 限制的其实是 泛型参数, 即 List<? super Integer> A表示 A的泛型参数 T 必须要满足 T 是 Integer 的父类, 因此诸如 List, List<Number 的对象就可以赋值到A 中. 正因为我们知道了 A中的泛型参数的边界信息, 因此我们就可以向 A 中添加 Integer 对象了
泛型分类
在学习这三种类型的泛型使用场景之前,我们需要明确一个基本准则,那就是泛型的声明通常都是通过< >配合大写字母来定义的,比如,还有我们上述所说的< K , V >, < E >。只不过不同类型,声明的位置不同,使用的方式也有所不同。
泛型类
泛型类的定义如下:
class A<T1, T2, ..., Tn> {
...
}
泛型类的声明在类名后面添加了类型参数声明部分。由<>分隔的类型参数部分跟在类名后面。它指定类型参数T1,T2,…和 Tn。一般将泛型中的类名称为原型,而将<>指定的参数称为类型参数。
泛型接口
泛型接口的定义如下:
interface A<T1, T2, ..., Tn> {
...
}
与泛型类的定义相似,泛型接口也是由<>分隔的类型参数部分跟在类名后面。它指定类型参数T1,T2,…和 Tn。但泛型接口有两种实现方式:子类明确声明泛型类型和子类不明确声明泛型类型。
子类不明确泛型类型
//泛型接口
interface A<T>{
public void send(T content);
}
//明确父类泛型类型
class AImp implements A<String>{
@Override
public void send(String content) {
System.out.println(content);
}
}
public class Test01{
public static void main(String[] args) {
AImp aimp = new AImp();
aimp.send("张大炮");
}
}
子类明确声明泛型类型
//泛型接口
interface A<T>{
public void send(T content);
}
//在子类中继续指定泛型标记
class AImp<T> implements A<T>{
@Override
public void send(T content) {
System.out.println(content);
}
}
public class Test01{
public static void main(String[] args) {
AImp<String> aimp = new AImp<String>();
aimp.send("张大炮");
}
}
在JDK1.8之后出现了一个自动推导泛型的概念,因为每一次都有需要编写泛型类太过于复杂,如果现在像前面定义了泛型类型,后面就不需要重复定义了。如下所示:
public class Test01{
public static void main(String[] args) {
AImp<String> aimp = new AImp<String>();
aimp.send("张大炮");
}
}
泛型方法
泛型方法的声明如下:
public <T> T func(T obj) {
...
}
需要注意的是,泛型方法与类是否是泛型无关。另外,静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
关于泛型方法总结一下就是:如果能使用泛型方法尽量使用泛型方法,这样能将泛型所需到最需要的范围内。如果使用泛型类,则整个类都进行了泛化处理。
总结
通过上面的介绍想必大家已经对泛型有了更深刻的理解,结合上文所说我来对泛型做一个总结,泛型方法就是在使用泛型预定义参数类型,在使用方法时在进行参数的具体类型定义;泛型接口和泛型类,就是在类和接口定义时把了和接口中不确定的、会使用到的参数类型都设置为泛型,在对象实例化的时候在确定具体的参数类型、返回值类型。呼应开头:类中的属性类型或方法的参数类型使用参数来定义,使用或者调用的时候再传入具体的类型。 这样一来是不是觉得非常通透!