一、上栗子
啥都不说先上例子:定义一个Show方法,实现一个“弱智”的功能,输出传入的参数值。
先来考虑传入的是一个int
类型参数,秒写下面代码:
public static void Show(int param){//为了能在main方法中调用,定义为static,知识点:静态方法调用
System.out.println(param);
}
//main方法如下
public static void main(String[] args) {
Show(123); //调用static方法,输入123;
}
有一天这个"弱智"系统升级了需要调用Show
方法输入浮点数。So easy~ 立马想到学过的多态–方法重载**(overload)**,添加一个相同名字的方法:
public static void Show(int param){ //为什么这个方法要定义为 static?
System.out.println(param);
}
public static void Show(double param){//知识点:方法重载【同一个类中】
System.out.println(param);
}
//测试方法--main方法
public static void main(String[] args) {
Show(126); //调用static方法,输入123;
Show(25.88);
}
灾难来了。。。 甲方爸爸想要一个万能的输入,可以输入 float 类型,String类型,自定义的类 class 对象(来算算会写多少个 show方法?emm~ 我们一起来数星星…)
怎么办?================
- 思考:方法中的形式参数(形参)
param
是一个变量可以指代同种类型的任意值。 两个show方法中唯一的不同就是param
的数据类型不同。 - 关键点:形参中的数据类型是否也能像变量一样定义方法的时候先不确定,当真正使用的时候再确定 是什么数据类型。
- 解决:我们也可以定义一个类型变量,用任意个字母从形式上来代替一个数据类型,如下用字母T表示某种数据类型。行话叫做**“将数据类型参数化”**,代码如下:
public static void Show(T param){// T 就是任意的字母来代表某个数据类型,目前不知道具体是什么类型
System.out.println(param);
}
问题又来了~ java 编译器 不认识 我随意定义的字母***T*** ,怎么办-- 告诉它 不就完啦。因此我们有了 <T>
尖括号告诉编译器这个字母 ***T***是有特殊含义的,它指代某个具体的数据类型。
问题又又来了~ 放哪里 ? 肯定要放在 使用***T***的前面。括号中的形参会使用 , 不要忘记方法返回值类型 也是要指定数据类型的(这个例子中是void
表示没有返回值),因此 <T>
显然要放到返回值(这里是 void) 前面 所以完整代码如下:
public static <T> void Show(T param){// <T> 告诉编译器 T这个字母已经赋予了神圣的使命
System.out.println(param);
}
//测试方法--main方法
public static void main(String[] args) {
Show(126); Show(25.88);
Show("输出字符串也没问题"); Show(new Date());//输出Date 对象也ok
}
很顺利,我们已经完成了一个完整的泛型方法代码的编写。总结下:
- 泛型(是什么)–>本质是***参数化类型***–[类型变量]。
- 泛型(为什么用)–>A.提高代码重用率;B.安全:编译时检查类型安全;C 更加关注算法实现本身
- 泛型(如何定义) – >简单说:先用尖括号声明T是一个类型变量,然后像变量一样使用这个T! [
啰嗦的说:]任给一个字母表示数据类型,然后用一对尖括号<>告诉编译器。编程习惯常用大写字母T、E、K、V 等来表示。同时 <> 的位置一定是放在所有类型变量使用之前。
二、泛型类与泛型接口
继续思考,Java的核心思想是什么? 万物皆对象!显然,很多时候希望这个类型变量:T ,能在整个类范围中使用,而不是仅仅局限于方法。因此有了泛型类 – 很简单<T>
这个类型变量的声明放**类名
**后面就好了。如下
public class MyGenerics<T>{// <T>声明一个类型变量T;告诉编译器T在整个class范围内都代表某个数据类型
public static void Show(T param){ //不需要在方法中声明 <T>
System.out.println(param);
}
}
//测试类(主类)中的 main方法写测试代码
public static void main(String[] args) {
MyGenerics myGen=new MyGenerics();
myGen.Show(126); myGen.Show(25.88);
myGen.Show("输出字符串也没问题"); myGen.Show(new Date());//输出Date 对象也ok
}
同理,为了**统一标准往往需要使用 接口
,将类型变量<T>
放入接口定义则有了泛型接口;
public interface InterfaceGen<T> {
public void Show(T param);
}
public class MyGenerics implements InterfaceGen{//重新类MyGenerics实现接口InterfaceGen
@Override //问:这是啥?
public void Show(Object obj){ //思考:A这个方法哪里来的? B参数为什么是Object?
System.out.println(obj);
}
}
问题叒来了,接口中定义的 param
类型是 T 为什么在实现中 变为了 Object
.
三、Java泛型实现-擦拭法
在理解**“擦拭法”前,先回顾下前面泛型的使用。尽管我们定义了泛型public static <T> void Show(T param)
但是我们在调用该方法的时候Show(126);
并没有像变量赋值一样给*类型变量T*指定具体的数据类型。同样在使用泛型类的时候也没有指定具体类型。这是为什么?很简单,在编译阶段编译器根据实际输入值进行了类型推导。然后用推导出的类型来替换类型变量T,最后生成字节码。这一过程就叫做“擦拭”
- 编译器可以对实际类型进行推导,注意不是原生数据类型而是对类类型进行推导
- 删除类型变量参数替换为真正的类型,在实际运行时已经没有不确定的类型变量了。
- 类型推导,只介绍Java 8版本的泛型的类型推导,【2018年Java 10版本的局部类型推导请自行百度】。
public static <T> T add(T a1,T a2){
return a1;
}
//对该方法的调用
Integer x=add(12,22) //两个参数都是整形,这里的类型是Integer类型。所以返回值也为 Integer。
Number x=add(12,22.58)//取Integer和Double的最小公倍数,可以理解为他们共同的父类返回为Number。
重点是理解为什么 Integer 和 Double一起返回的类型是Number。
- 泛型类中的类型推导。
MyGenerics myGen=new MyGenerics();
用泛型类MyGenerics
实例化对象的时候没有指明具体数据类型,编译器也没办法通过对象实例化来确定***T*** 的数据类型。怎么办?很简单所有类类型的最小公倍数不就是根类Object,因此默认情况下,所有的泛型类类型都会使用Object类型替换。这就回答了上一节为什么param
类型是 T 在实现中 变为了Object
。
显然我们也可以指定类型. MyGenerics<Number> myGen=new MyGenerics<Number>()
这样实际对象myGen
中类型变量T将都替换为Number类型。实际使用中myGen.Show(x)
x变量的类型可以为Number
以及Number
的***子类***
来思考下,如果指定的不是Number
这样的抽象类,而是接口会怎么样?例如 MyGenerics<Comparable> myGen=new MyGenerics<Comparable>()
答案:myGen.Show(x)
x对象变量的类型一定是实现了接口Comparable的类。
四、泛型类型上界
现在有需求定义一个泛型方法对输入的2个变量比较大小。我们很容易写出下面代码
public static <T> void Cmp(T eA ,T eB){
//遇到问题了 怎么比较 eA和eB 的大小
}
问题:怎么比较传入的两个变量的大小?
分析:T的类型是什么?目前在没用使用该方法时候(定义方法),T的类型默认为Object ,那么eA 和 eB要比较大小就必须采用 Object中比较大小的方法,可惜Object中没有比较大小的方法。怎么办?
Java中提供了一个接口Comparable
代码改下如下
public static <T extends Comparable> int Cmp(T eA ,T eB){
//可以试试 eA和eB 具有比较大小的方法 compareTo.
return eA.compareTo(eB);
}
T extends Comparable
是什么? 就是规定了泛型类型的上界,也就说当使用该方法时,传入的类类型必须是实现了Comparable接口的类,相当于做了限制约束。
同理,有上界也存在下界 T super xxx 。 注:超纲了,感兴趣的可以自行百度 😃…
五、以一个案例作为结束
同时用这个案例来说明JAVA中接口的作用:提供统一标准.
- 定义一个接口,提供统一标准:需要实现 Min 和 Max两个方法
public interface MinMax<T extends Comparable<T>>{
public T Min();
public T Max();
}
2.定义泛型类,约束类型变量上界,实现接口
class MyClass<T extends Comparable<T>> implements MinMax<T> {
T[] vals;
MyClass(T[] ob) {
vals = ob;
}
@Override
public T Min(T a) {
T val = vals[0];
for (int i = 1; i < vals.length; ++i) {
if (vals[i].compareTo(val) < 0) {
val = vals[i];
}
}
return val;
}
@Override
public T Max(T a) {
T val = vals[0];
for (int i = 1; i < vals.length; ++i) {
if (vals[i].compareTo(val) > 0) {
val = vals[i];
}
}
return val;
}
}
//测试类中的主方法
public static void main(String args[]) {
Integer inums[] = {56, 47, 23, 45, 85, 12, 55};
Character chs[] = {'x', 'w', 'z', 'y', 'b', 'o', 'p'};
MyClass<Integer> iob = new MyClass<Integer>(inums);
MyClass<Character> cob = new MyClass<Character>(chs);
System.out.println( 'Max value in inums: '+iob.max());
System.out.println('Min value in inums: '+iob.min());
System.out.println('Max value in chs: '+cob.max());
System.out.println('Min value in chs: '+cob.min());
}