Java 泛型 超详细解答分析

目录

1. 什么是泛型

2. 引出泛型

 2.1 泛型的语法

3. 泛型类的使用

3.1 泛型类的语法和示例

3.2类型推导

4. 擦除机制(编译)

5. 泛型的上界

5.1 语法-示例

 6. 泛型方法

6.1 泛型的静态方法

6.2泛型的非静态方法

6.3 应用---比较

6.4 小结

7. 通配符

7.1 通配符解决问题

7.2 通配符上界

7.3 通配符下界

8. 包装类

8.1 基本数据类型和对应的包装类 


1. 什么是泛型

   一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的 代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。
  泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化    

2. 引出泛型

以前的学习中,我们都是通过指定的数据类型来存储数据的,比如数组,只能存放指定类型的元素。通过学习,我们知道所有类的父类,默认为Object类。数组就可以创建为Object,来实现接受各种弄个类型。

所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

所以我们就可以理解为泛型,实际上就是将数据类型参数化,进行传递。

 2.1 泛型的语法

1.首先,我们先用object类型类替代我们使用的数据类型。

 从代码运行结果来看,我们发现定义object类的话,我们可以在object数组里同时存放多种数据类型,并且可以正常输出,但是要进行强制类型转化!

 2.这个时候,我们在源代码的基础上进行改写。

(除了注释1处,将object都改为T。)

(main方法中添加类型<>)

class MyArray<T>{
    public T[] obj = (T[]) new Object[3];            //注释1
    public void setObj(int a,T val){
        obj[a] = val;
    }
    public T getObj(int a){

        return obj[a];
    }
}
public class Test {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();    //注释2
        myArray.setObj(0,123);
        myArray.setObj(1,3);
        myArray.setObj(2,55);
        System.out.println( myArray.getObj(0));        //注释3
        System.out.println( myArray.getObj(1));
        System.out.println( myArray.getObj(2));
    }
}

1.类名后的 <T> 代表占位符,表示当前类是一个泛型类。

了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型                暂时理解即可
2.注释1处,不可以new泛型类型的数组!!!
T [] ts = new T [ 3 ];        //这样是错误的!!!
3. 注释 2 处,类型后加入 <Integer> 指定当前类型。
3.注释3处,不再需要强制类型转化。

3.在2的基础上修改一下代码如下。 

这里就代码编译错误了,这是因为在注释2处指定类当前的类型,此时编译器会在存放元素的时候帮助我们进行类型检查。

需要提示的是: 在这里使用如下代码并不是足够好的方式,这个后期会进行讲解。

T[] array = (T[])new Object[3];

3. 泛型类的使用

3.1 泛型类的语法和示例

  new 泛型类<类型实参>(构造方法实参);                 // 实例化一个泛型类对象

  MyArray < Integer > list = new MyArray < Integer > ();     
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!!!

3.2类型推导

当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写。

MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String

4. 擦除机制(编译)

编译的过程当中,将所有的 T替换为Object 这种机制,我们称为:擦除机制 

编译之后的字节码文件当中,都变成了Object。

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。

所以要注意:运行的时候就没有泛型这个概念了,只有object了!!! 

5. 泛型的上界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。 

5.1 语法-示例

class 泛型类名称 < 类型形参 extends 类型边界 > {
...
}
public class MyArray < E extends Number > {
...
}

此时,只会接受Number或者Number的子类类型作为 E 的类型实参。 

当我们运行MyArray<String>时 ,就会发生编译错误,因为String不是Number的子类型。

当我们没有指定类型边界的时候,就可以视为:E extends Object

稍微复杂一点的示例:

public class MyArray < E extends Comparable < E >> {
...
}

这个是时候E必须是实现了Comparable接口的!!! 

例如如下代码中Person类是自定义的,要使用泛型类则需要实现Comparable接口。

 6. 泛型方法

6.1 泛型的静态方法

语法格式:static<T>        

public static<T> T findMax (T[] array)

在main方法中使用时:Integer 变量名 = 类名.<Integer>方法名(参数)        //<Integer>可省略 

示例:

 注意:static静态方法可直接通过类名来调用!!!

 6.2泛型的非静态方法

与静态方法相类似。

6.3 应用---比较

 要注意:引用类型是不可以进行比较的,因为不确定类型。所以要实现comparable接口。

class Alg<T extends Comparable<T>>

 同时注意:T必须是实现了Comparable接口的!!!

6.4 小结

1. 泛型是将数据类型数据化,进行传递。

2. 使用<T>表示当前类是一个泛型类。

3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转化。

4. <>中的类型必须是类类型,而不能是基本类型!!!

7. 通配符

7.1 通配符解决问题

? 用于在泛型的使用,即为通配符。
泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围。

 如下代码:

class Man<T>{
    private T man;
    public T getMan(){
        return this.man;
    }
    public void setMan(T man){
        this.man = man;
    }
}
public class Test5 {
    public static void main(String[] args) {
        Man<String > man = new Man<>();
        man.setMan("person");
        func(man);

        Man<Integer> man1 = new Man<>();
        man1.setMan(111);
        func1(man1);
    }
    public static void func(Man<String > man){
        System.out.println(man.getMan());
    }
    public static void func1(Man<Integer> man){
        System.out.println(man.getMan());
    }
}

我们可以看出我们因为func和func1的参数类型不同,定义了两个函数,此时我们就可以用通配符来进行优化。

class Man<T>{
    private T man;
    public T getMan(){
        return this.man;
    }
    public void setMan(T man){
        this.man = man;
    }
}
public class Test5 {
    public static void main(String[] args) {
        Man<String > man = new Man<>();
        man.setMan("person");
        func(man);

        Man<Integer> man1 = new Man<>();
        man1.setMan(111);
        func(man1);
    }
    public static void func(Man<?> man){
        System.out.println(man.getMan());
    }


}

易错点 :使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改

 当按如下使用的时候,就会出错!!!

注意点:在编译过程中,他们的类型都是一样的。

验证一下:

 我们可以看出,输出的类型是一致的。其中<String><Integer>是不参与类型的组成的。

所以如下代码就会发生报错,同时也需注意到的是,这个跟重载是不一样的概念!!!

 

7.2 通配符上界

? extends 类:设置泛型上限

语法: 

<? extends 上界 >
<? extends Number > // 可以传入的实参类型是 Number 或者 Number 的子类

注意点: 

class Food{
}
class Fruit extends Food{
}
class Apple extends Fruit{
}
class Banana extends Fruit{
}
class Plate <T>{
    public T plate;
    public void setPlate(T plate){
        this.plate = plate;
    }
    public T getPlate(){
        return plate;
    }
}
public class Test {
    public static void main(String[] args) {
        Plate<Food> plate1 = new Plate<>();
        plate1.setPlate(new Food());
        func(plate1);
    }
    public static void func(Plate <? extends Food> temp){
        //temp.setPlate(new Food());            //错误的!!!
        System.out.println(temp.getPlate());    //注释1
        Food food = temp.getPlate();            //注释2
    }
}

从上诉代码中,可以知道能传入的实参类型是Food或者Food的子类(Fruit,Apple,Banana) 

需要注意的是:

1. 图中错误点表明了通配符的上界,不能进行写入数据(添加元素),因为temp接受的是Food或者Food的子类,这就导致了容易发生向下转型,这很显然就是错误的。

例如:实参是Fruit,然后temp.setPlate( new Apple() ) 这很显然就是错误的!!!

2. 图中注释1和注释2也说明了通配符的上界是可以进行读取数据(获取元素)的!!!

 总结:通配符的上界,不能进行写入数据,只能进行读取数据。

7.3 通配符下界

? super 类:设置泛型下限

 语法:

<? super 下界 >
<? super Integer > // 代表 可以传入的实参的类型是 Integer 或者 Integer 的父类类型

注意点: 

class Food{
}
class Fruit extends Food{
}
class Apple extends Fruit{
}
class Banana extends Fruit{
}
class Plate <T>{
    public T plate;
    public void setPlate(T plate){
        this.plate = plate;
    }
    public T getPlate(){
        return plate;
    }
}
public class Test {
    public static void main(String[] args) {
        Plate<Food> plate1 = new Plate<>();
        plate1.setPlate(new Food());
        func(plate1);
    }
    public static void func(Plate <? super Fruit> temp){
        System.out.println(temp.getPlate());            //只能直接输出
        //Fruit fruit = temp.getPlate();                //注释1,错误点,不能接收
        temp.setPlate(new Apple());                     //注释3
        temp.setPlate(new Banana());                    //注释4
        //temp.setPlate(new Food());                    //注释2,错误点
    }
}

从上诉代码中,可以知道能传入的实参类型是Fruit或者Fruit的父类(Food)

需要注意的是:

1. 从上诉代码中的错误点注释1,我们可以看出通配符的下界不能用来接收(读取数据),因为Fruit是下界,可能会导致向下转型。但是可以用来输出

2. 从错误点注释2和注释3和注释4,我们可以看出可以进行修改,可以添加形参(Fruit)本身或者它的子类(写入数据),但是不可以添加形参的父类!!!

总结: 通配符的下界,不能进行读取数据,只能写入数据。

8. 包装类

Java 中,由于基本类型不是继承自 Object ,为了在泛型代码中可以支持基本类型, Java 给每个基本类型都对应了 一个包装类型。

8.1 基本数据类型和对应的包装类 

 其中需要特殊记忆的就是Integer和Character。

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PlLI-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值