一、上栗子

啥都不说先上例子:定义一个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
}

很顺利,我们已经完成了一个完整的泛型方法代码的编写。总结下:

  1. 泛型(是什么)–>本质是***参数化类型***–[类型变量]。
  2. 泛型(为什么用)–>A.提高代码重用率;B.安全:编译时检查类型安全;C 更加关注算法实现本身
  3. 泛型(如何定义) – >简单说:先用尖括号声明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,最后生成字节码。这一过程就叫做“擦拭”

  1. 编译器可以对实际类型进行推导,注意不是原生数据类型而是对类类型进行推导
  2. 删除类型变量参数替换为真正的类型,在实际运行时已经没有不确定的类型变量了。
  1. 类型推导,只介绍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。

  1. 泛型类中的类型推导。
    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中接口的作用:提供统一标准.

  1. 定义一个接口,提供统一标准:需要实现 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());
}