JAVA-泛型

一、泛型是什么

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

二、泛型的使用场景

我们需要要以下代码来代入我们理解泛型:

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值。
我们知道Object是所有类的父类,那我们就可以使用Object来实现。
public class MyArray {
    private Object[] array = new Object[10];

    public void set(int pos, Object val) {
        array[pos] = val;
    }

    public Object get(int pos) {
        return array[pos];
    }
}


class TestDemo {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.set(0,1);//给0小标Integer类型
        myArray.set(1,2);//给1小标Integer类型
        myArray.set(2,"hallo");//给2小标String类型

        System.out.println(myArray.get(1));
        String s = myArray.get(2);//报错
        System.out.println(s);
    }
}

在获取2小标也就是String类型的时候报错了,强制类型转换为String类型后,不再报错。

结果:

 

但是我们发现有一些问题:

1. 任何类型数据都可以存放
2. 2 号下标本身就是字符串,但是确编译报错。必须进行强制类型转换
也就是说什么东西都可以往里面放,各种类型乱七八糟的,而且我要用某个小标的值还需要提前知道里面是什么类型的值。
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象。让编译 器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

 三、泛型的基本使用

简单介绍一下,方便我们理解类型擦除机制,后面再详细介绍

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}

那么我们将刚刚的MyArray给改成泛型类:

public class MyArray<T>{
    private Object[] array = new Object[10];//为什么Object不行为T

    public void set(int pos, T val) {
        array[pos] = val;
    }

    public T get(int pos) {
        return (T)array[pos];
    }
}

可能细心的朋友就要问:为什么数组类型不改为T仍然是Object

无论是哪一边改为T都不行,编译都编译不了:

最多可以这样,但是这样写并不算好,为什么一会再说:

最好还是写为:

private Object[] array = new Object[10];

 类名后的 <T> 代表占位符,表示当前类是一个泛型类,实际上就是个名字写成什么都可以,但是行业有自己的一些行业规范:

【规范】类型形参一般使用一个大写字母表示,常用的名称有:

E 表示 Element 数组元素
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型

泛型类的使用:

更推荐第二种方法

public static void main(String[] args) {
            
     //传入类型                           这里可以不传
     MyArray<Integer> myArray1 = new MyArray<Integer>();

     //推荐                       自动推导出类型
     MyArray<Integer> myArray = new MyArray<>();
        
}

 四、类型擦除机制

我们来到MyArray生成的字节码文件路径下,在文件路径输出cmd,再输入 javap -c 文件名 

可以看到所有关于T的类型全部被替换为了Object。

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

在 Java 中,泛型的擦除机制是在编译阶段发生的,编译后的字节码文件中泛型类型参数会被擦除,对于没有指定上界的泛型类型参数,会被替换为 Object 类型;

如果指定了上界(例如 class MyClass<T extends Number> ),则会被替换为所指定的上界类型(这里是 Number )

现在还不知道上界,不要紧后面看就懂了

可能到这里就有朋友有疑问了,反正泛型都会被替换为Object那为什么不能用泛型创建数组呢?

先讲直接用泛型创建的数组:

private T[] array = new T[10];
  • 我现在根本不知道它是什么类型,但是数组在创建的时候需要确定类型

用Object创建再强转的情况:

 private T[] array = (T[])new Object[10];

我们用一个代码来解释:

public class MyArray<T>{
    private Object[] array = new Object[10];

    public void set(int pos, T val) {
        array[pos] = val;
    }

    public T get(int pos) {
        return (T)array[pos];
    }

    public T[] getArray() {
        return (T[])array;
    }
}


class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> myArray = new MyArray<>();
        Integer[] ret = myArray.getArray();
    }
}

报了一个这样的异常:类型异常

Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer; ([Ljava.lang.Object; and [Ljava.lang.Integer; are in module java.base of loader 'bootstrap')
	at demo1.TestDemo.main(MyArray.java:23)
原因:替换后的方法为:将 Object[] 分配给 Integer[] 引用,程序报错
正确的方法:(了解)
class MyArray<T> {
    public T[] array;
    public MyArray() {
    }
    /**
     * 通过反射创建,指定类型的数组
     * @param clazz
     * @param capacity
     */
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }
    public T getPos(int pos) {
        return this.array[pos];
    }
    public void setVal(int pos,T val) {
        this.array[pos] = val;
    }
    public T[] getArray() {
        return array;
    }

    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);
        Integer[] integers = myArray1.getArray();
    }
}

五、泛型类

1.泛型类的定义

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
public class Test<T> {
    public T key;

    public void set(T key) {
        this.key = key;
    }

    public T get() {
        return this.key;
    }
}

在泛型类中,类型参数定义的位置有三处,分别为:

  • 非静态的成员变量类型
  • 非静态的常用方法形参
  • 非静态的成员方法返回类型

其中静态的成员方法,可以用泛型方法的方式使用泛型,后面泛型方法讲解

2.泛型类的使用

public static void main(String[] args) {
        //推荐
        //在<>内传入你想使用的类型,后面的<>内可以不写会自动推导
        Test<Integer> test = new Test<>();
        
        //显示定义
        Test<Integer> test1 = new Test<Integer>();
}
  • 当我们传入Integer类型之后就不能传入其他类型了,如果传入其他类型编译器会直接报错,所以使用泛型是有安全检查机制,能帮我们更好的管理数据。
  • 值得注意的是,泛型类,类型的确定是在实例化对象的时候,在此之前是Object

3.泛型被替换后

public class Test<Integer>{
    public Integer key;

    public void set(Integer key) {
        this.key = key;
    }

    public Integer get() {
        return this.key;
    }
} 

4.裸类型(Raw Type) (了解)

如果我们在实例化泛型类时,没有指定类型:

Test test = new Test();

那么里面的泛型类都会变为Object 

public class Test {
    public Object key;

    public void set(Object key) {
        this.key = key;
    }

    public Object get() {
        return this.key;
    }
} 

注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制
下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。
小结:
1. 泛型是将数据类型参数化,进行传递
2. 使用 <T> 表示当前类是一个泛型类。
3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换

六、泛型接口 

1.泛型接口的定义

泛型接口和泛型类差别不大:

public interface 接口名<类型形参列表> {
    ...
}
public interface IUsb<T> {
//    public T key;  报错,接口中的成员是静态的: public static final

    public T get(T key); //抽象方法可以使用: public abstract

    //默认的方法也可以使用
    default T Func(T key) {
        return key;
    }
}

2.泛型接口的使用

泛型类实现泛型接口

class AA<T> implements IUsb<T> {

    @Override
    public T get(T key) {
        return null;
    }
}

普通类实现泛型接口:

就需要指定类型了

class BB implements IUsb<Integer> {

    @Override
    public Integer get(Integer key) {
        return null;
    }
}

接口情况:

泛型接口继承泛型接口:

interface AC<T> extends IUsb<T> {
    //……
}
class CC implements AC<String> {

    @Override
    public String get(String key) {
        return null;
    }
}

都是泛型类:

interface AC<T> extends IUsb<T> {
    //……
}
class CC<T> implements AC<T> {

    @Override
    public T get(T key) {
        return null;
    }
}

总结:

  • 如果是泛型类实现或继承某个接口或类,在实例化对象的时候才确定具体类型。
  • 如果不是泛型类或接口实现或继承某个接口或类,就需要在定义接口或类的时候指定类型
  • 且接口中的成员是静态的,不能使用泛型

 

七、泛型方法

1.泛型方法的定义

要区分好使用泛型类泛型的方法,和本身就是泛型方法的区别:

public <类型参数> 返回类型 方法名(类型参数 变量名) {
    ...
}

只有在方法返回值类型前面写了<?>的时候才是泛型方法

public class Test<T> {
    public T key;

    public void set(T key) {
        this.key = key;
    }

    public T get() {
        return this.key;
    }
    //这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
    public <T> T test(T t) {
        return t;
    }
}

 2.泛型方法的使用

public static void main(String[] args) {
        Test<Integer> test = new Test<>();
        //传入一个不一样的类型参数,证明它们不一样
        System.out.println(test.test("hallo"));

        //或者也可以显示定义类型,但一般没有这么写。不写也会根据你给的值确定类型
        System.out.println(test.<String>test("bit"));
}

结果:

3.静态泛型方法的定义

 //这里的泛型虽然和类同名,但是在此方法里面,用的是方法的泛型
    public static <T> T test(T t) {
        return t;
    }

 

4.静态泛型方法的使用

public static void main(String[] args) {
        //通过类名直接调用
        Test.test("hello");

        //或者
        Test.<String>test("word");
}

5.为什么静态方法可以使用泛型

不知道大家有没有这样的问题,为什么泛型类里面的静态方法不能使用泛型,但是静态方法却可以?

我们知道,泛型类里面的静态方法不能使用是因为,泛型类的类型参数是在实例化对象时才确定的,而静态方法在类加载时就存在,此时类的泛型类型参数还未确定,

静态泛型方法有自己独立声明的泛型类型参数,这个类型参数是在方法调用时才确定具体类型的,与泛型类的实例化无关。

 

八、泛型的上界

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

意思是:你传入的参数类型(E)必须是Number 的子类或者它自己 

 更复杂一点的:

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

 你传入的参数类型 E 必须是实现了Comparable,且类型是E

泛型的下届不能直接像泛型的上界一样使用,代码会报错:

但是通配符是可以使用下届的

九、通配符

 ? 用于在泛型的使用,即为通配符

1.通配符的作用 

有这样一段程序

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>() ;
        message.setMessage("比特就业课欢迎您");
        fun(message);
    }
    //                   如果我们传入的不是Stirng类型
    public static void fun(Message<String> temp){
        System.out.println(temp.getMessage());
    }
}

如果我们不想只传入String类型,还想传入各种类型,并且我不希望里面的值被更改

此时就可以使用通配符了:

class Message<T> {
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}
class TestDemo {
    public static void main(String[] args) {
        Message<String> message = new Message<>() ;
        message.setMessage("比特就业课欢迎您");
        fun(message);
    }
    public static void fun(Message<?> temp){
        temp.setMessage("hello");//无法修改
        System.out.println(temp.getMessage());
    }
}

 那我们想传入各种类型,但是允许更改数据我们继续用普通的泛型就可以了

public static <T> void fun(Message<T> temp){
        temp.setMessage((T)"hello");//此时就可以做到更改了
        System.out.println(temp.getMessage());
}

那我们希望可以指定一下传入数据的范围,就可以使用通配符的上界和下届:

? extends 类:设置通配符上限
? super 类:设置通配符下限

2.通配符的上界

语法:

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

}

class Dog extends Animal{

}

class Cat extends Animal{

}

class Message<T> { // 设置泛型
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}

public class Test {
    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);

    }

    // 此时?可以接收Animal的子类,但是由于不确定类型,所以无法修改
    public static void fun(Message<? extends Animal> temp){
//        temp.setMessage(new Dog()); //仍然无法修改!
//        temp.setMessage(new Animal()); //仍然无法修改!
//        temp.setMessage(new Cat()); //仍然无法修改!
        Animal animal = temp.getMessage();//但是可以接收
        System.out.println(animal);
    }
}

因为我们无法确定到底传入的是哪一个子类,所有通配符的上界是无法对内容进行更改的。

因此:通配符的上界,不能进行写入数据,只能进行读取数据

那么普通的泛型上界,可以写入读取数据吗:

    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);
    }

    public static <T extends Animal> void fun(Message<T> tmp) {
        tmp.setMessage((T) new Dog());
        tmp.setMessage((T) new Cat());
        Animal animal = tmp.getMessage();
        System.out.println(animal);
    }

最终输出了Cat对象的地址,答案是可以做到的。

3.通配符的下界

语法

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

        

class Animal{

}

class Dog extends Animal{

}

class DDD extends Dog {
    
}
class Cat extends Animal{

}

class Message<T> { // 设置泛型
    private T message ;
    public T getMessage() {
        return message;
    }
    public void setMessage(T message) {
        this.message = message;
    }
}

public class Test {
    public static void main(String[] args) {
        Message<Dog> message = new Message<>();
        message.setMessage(new Dog());
        fun(message);
    }



    public static void fun(Message<? super Dog> temp){
    // 此时可以修改!!添加的是Dog 或者 Dog的子类
        temp.setMessage(new DDD());//这个是Dog的子类
        temp.setMessage(new Dog());//这个是Dog的本身
//        Dog dog = temp.getMessage(); 不能接收,这里无法确定是哪个父类
        System.out.println(temp.getMessage());//只能直接输出
    }
}
通配符的下界,不能进行获取数据,只能写入数据
这里可能有点绕我画个图解释一下:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值