泛型学习笔记


一、泛型的定义

泛型也叫做参数化类型,在定义类(接口)的时候不会设置其属性或方法中参数的具体类型,而是在使用时进行定义。若在使用时不指定具体的类型,则默认按照Object类型来处理,会产生警告但是不会报错。


二、泛型的使用

泛型共有3种使用方式,分别是泛型类、泛型方法和泛型接口

2.1 泛型类

1、定义

public class Test<T> {
    T field;
}

<> 中的 T 就是类型参数,它可以用于指代任意类。
除了 T,我们还可以使用其它任意字符串来代替,但是出于规范性的考虑,一般都使用大写字母。
2、使用

Test<String> test01 = new Test<>();
Test<Integer> test02 = new Test<>();

在使用泛型类来创建对象时,直接在 <> 中放入指定的类型即可。

2.2 泛型接口

泛型接口和泛型类其实是类似的

1、定义

public interface Test<T> {
    void add(T object);
}

2、使用

// 实现 Test 接口的时候,指定一个 类型参数 String
public class TestImpl implements Test<String> {
    @Override
    public void add(String object) {
        List list = new ArrayList<>();
        list.add(object);
    }
}
//----------------------------------------------------
TestImpl test = new TestImpl();
test.add("QY");
test.add(1);// 编译器报错

因为是接口泛型 ,所以需要编写一个类来实现这个接口,并在 <> 中指定一个类型参数。
由于指定了类型参数 String,所以当我们想用 add 方法传入其它类型的值时,编译器就会报错。

2.3 泛型方法

1、定义

public class Test01 {
    public <T> T methodTest(T t) {
        return t;
    }
}

在泛型方法中,类型参数需要写在返回值类型的前面。
类型参数也可以当作方法的返回值类型。在上面的代码中, <T> 后面的 T 就是返回值类型。
2、使用

Test01 test01 = new Test01();
String name="QY";
String name2 = test02.methodTest(name);

2.4 泛型类和泛型方法共存的情况

泛型类中也可以出现泛型方法,如下代码所示

// 这是一个泛型类,类型参数为 T
class Test01<T> {
    
    // 这是泛型类中的一个普通方法
    public void methodTest01(T t) {
        System.out.println(t);
    }

    // 这是一个泛型方法,类型参数为 E
    public <E> void methodTest02(E e) {
        System.out.println(e);
    }
}

但是,当 泛型方法的类型参数 和 泛型类的类型参数 相同时,泛型方法中的类型参数以下面哪个为准呢?
1、 泛型类 的类型参数
2、 泛型方法 的类型参数

// 这是一个泛型类,类型参数为 T
class Test01<T> {

    // 这是泛型类中的一个普通方法
    public void methodTest01(T t) {
        System.out.println(t);
    }

    // 这是一个泛型方法,类型参数为 T(和泛型类的类型参数相同)
    public <T> void methodTest02(T t) {
        System.out.println(t);
    }
}

我们编写代码进行测试

Test01<String> test = new Test01<>();
test.methodTest01("QY");
test.methodTest02(new Integer(999));

结果如下所示:

QY
999

我们可以发现,泛型类的类型参数和泛型方法的类型参数没有任何关系。泛型方法始终以自己定义的类型参数为准。

一般来说,当泛型类中存在泛型方法时,最好使用不同的字符来表示,使代码具有更强的可读性。


三、类型通配符

通配符多用于类库的开发,在Java源码中可以看到很多地方都使用到了通配符。
通配符用于指定泛型中的类型范围,通配符都是使用"?"来代替具体的类型参数。
通配符有3种形式:

  • <?>:无限定的通配符
  • <? extends T>:有上限的通配符,表示T和T的子类
  • <? super T>:有下限的通配符,表示T和T的父类

3.1 我们为什么需要通配符?

我们来看一个例子

class Father {}
class Son extends Father {}

public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        Father father = son;
    }
}

因为 Son 是 Father 的子类,它们是继承关系,所以子类实例可以给父类引用赋值。
但当我们编写如下代码时,编译器便会报错

List<Son> sons = new ArrayList<>();
List<Father> fathers = sons;

因为 Son 和 Father 是继承关系,但是 List 和 List 并不是继承关系
但是,在现实中有时我们希望泛型可以处理某一范围内的数据类型,例如某个类和它的子类。这时候通配符就可以帮助到我们了。

3.2 无限定通配符<?>

一般和容器类配合使用,?代表类型参数是未知类型,且当涉及到?类型参数的操作时,一定和具体类型无关。

public void test01(List<?> list) {
    list.add("QY");	// 编译报错	
    list.add(111);	// 编译报错

    list.size();
    list.isEmpty();
    Object o = list.get(0);
}

方法中的参数类型List被 <?> 所修饰,表示List中的具体类型是未知的,所以我们只能调用与 List 中与类型无关的方法。
在上面的代码中,使用 add 方法涉及到具体的类型,所以编译器报错;但是与类型无关的方法 size、isEmpty、get 可以正常使用。

可以认为, <?> 提供了只读的功能,只保留了和具体类型无关的功能

3.3 有上限通配符<? extends T>

<? extends T> 表示T和T的子类,它可以帮助我们缩小 <?> 所表示的范围

public class Father {}
public class Son extends Father {}

//-------------------------------------------

public void test01(List<? extends Father> list) {
    list.add(new Son());	//编译报错

    list.get(0);
    list.get(1);
}

方法 test01 中的参数 list 只接受 Father 和 Father 的子类。
它没有写操作的能力。

3.4 有下限通配符<? super T>

<? super T> 表示T和T的父类,它可以帮助我们缩小 <?> 所表示的范围

public class Father {}
public class Son extends Father {}

//--------------------------------------

public void test01(List<? super Son> list) {
    list.add(new Son());    // 不报错
    list.add(new Father()); // 报错
}

从代码中我们可以看出,<? super T> 有一定的写操作能力。

3.5 注意

1、将通配符换成类型参数后,原本无法进行写操作的地方可以进行写操作了,但是需要进行类型强制转换

public class Father {}
public class Son extends Father {}

//-------------------------------------------

public void test01(List<T> list) {
    list.add((T)"QY");
    list.add((T)new Integer(23));
}

2、类型参数也适用于参数之间的类别依赖

// 类
class Test01<K, V extends K> {
    K key;
    V value;
}
// 方法
public <K, V extends K> void test(K k, V v) {}

3、通配符可以和类型参数一起使用

public <T> void test02(List<? extends T> list) {}

四、类型擦除

4.1 定义

泛型信息只存在于源代码阶段,在编译器编译后,与泛型相关的信息都会被擦除。

4.2 验证

我们执行以下代码

List<String> list01 = new ArrayList<>();
List<Integer> list02 = new ArrayList<>();

System.out.println(list01.getClass() == list02.getClass());
System.out.println("list01 : " + list01.getClass().getName());
System.out.println("list02 : " + list02.getClass().getName());

输出结果如下

true
list01 : java.util.ArrayList
list02 : java.util.ArrayList

可以看到,两者都是 ArrayList 类型,为什么呢?

在代码编写阶段,正如我们所看到的,还存在泛型信息

List<String> list01 = new ArrayList<>();
List<Integer> list02 = new ArrayList<>();

但是编译后呢?我们可以通过编译后的 Test.class 文件来查看信息
1、先进入 Java 文件所在目录下
2、使用 Shift+鼠标右键,打开 PowerShell 命令窗口使用命令 javac -encoding UTF-8 Test.java 编译 Java 文件(encoding UTF-8 指定编码格式为 UTF-8)
3、编译后可以在目录下看到一个 Test.class 文件,将其使用 Java 反编译程序打开,如下所示在这里插入图片描述4、我们可以看到,泛型信息都被擦除了!
所以,可以验证结论:泛型信息只存在于源代码阶段,在编译器编译后,与泛型相关的信息都会被擦除掉。

4.3 泛型信息转译

类型擦除后,泛型信息都被擦除,但是类中用到类型参数的地方该怎么办呢?
答案是“转译”,编译时会将类型参数转译为相应的类型。

4.3.1 情况一:类型参数T

class Gen01<T> {
    T object;

    public Gen01(T object) {
        this.object = object;
    }
    
    public void add(T obj){}
}
Gen01<Integer> gen01 = new Gen01<>(100);
Field[] fields = gen01.getClass().getDeclaredFields();
for (Field field : fields) {
    System.out.println("Name:" + field.getName() + " ; Type:" + field.getType().getName());
}

输出结果

Name:object ; Type:java.lang.Object

4.3.2 情况二:有上限的类型参数T

class Gen02<T extends Integer> {
    T object;

    public Gen02(T object) {
        this.object = object;
    }
    
    public void add(T obj){}
}
Gen02<Integer> gen = new Gen02<Integer>(new Integer(100));
Field[] fields = gen.getClass().getDeclaredFields();
for (Field field : fields) {
    System.out.println("Name:" + field.getName() + " ; Type:" + field.getType().getName());
}

执行结果

Name:object ; Type:java.lang.Integer

4.3.3 总结

当类型擦除时,泛型类中的类型参数 :

  • 若没有指定上限,如 <T> ,类型参数会被转译成 Object `;
  • 若指定了上限,如 <T extends Integer> ,类型参数会被转译成 上限类 Integer。

4.3.4 注意事项

因为泛型擦除时会对类型参数进行转译,所以我们在使用反射操作某些方法时要注意方法的参数类型。

  • 对于 Gen01<Integer> gen01 = new Gen01<>(100); ,其 add 方法中的类型参数转译后为 Object,所以我们使用反射时应调用 getDeclaredMethod("add", Object.class)
  • 对于 Gen02<Integer> gen = new Gen02<Integer>(new Integer(100)); ,其 add 方法的类型参数为 Integer,所以我们使用反射时应调用 getDeclaredMethod("add", Integer.class)

五、如何在List中添加其它类型的数据?

使用泛型,会对我们的一些操作进行限制。

List<String> list=new ArrayList<>();
list.add("QY");
list.add(111);  // 编译报错

如上代码所示,我们在创建List对象的时候,添加了泛型参数 String,所以我们无法向其中添加其它类型的值。
但是,这只是在源代码阶段的限制,从泛型擦除的定义我们可以知道,在编译之后,泛型信息都被清除了,所以我们可以使用反射,在运行时阶段对其进行操作,从而绕开泛型的限制。

public class Test02 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("QY");

        // 使用反射,向 list 中填其其它类型的值
        try {
            // 获取 add 方法
            Method method = list.getClass().getDeclaredMethod("add", Object.class);
            method.invoke(list, 111);	// 添加 int 类型的值
            method.invoke(list, new BigDecimal("123.653"));	// 添加 BigDecimal 类型的值
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        // 打印输出 list 中的数据
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

执行结果:

QY
111
123.653

我们可以看出,利用反射在运行时阶段对对象进行操作,成功绕开了泛型的限制。


六、泛型中注意的地方

1、不能 new 泛型类型的对象

T t = new T();	// 错误

2、不能 new 泛型对象的数组

List[] lists = new ArrayList<Integer>[10];	// 错误

3、不能 new 泛型类型的数组

T[] ts = new T[10];	// 错误

4、泛型擦除不是替换,若为 <T> ,则擦除为 Object ;若为 <T extends 上限类> ,则擦除为 上限类 。即向超类的方向擦除。

5、八种基本数据类型无法作为类型参数,而要使用它们的包装类。因为基本数据类型没有基类,无法进行擦除操作。

6、泛型有上限,没有下限,即 有<T extends 上限>不存在<T extends 下限>

7、不能在 static 方法中使用泛型的类型参数,这与线程安全有关。
静态方法在 JVM 的类加载阶段被写到方法区中,静态方法和静态方法的参数都是只有一份,且都在多线程之间共享。如果使用了泛型,当多个对象调用静态方法并先后注入 String、Integer 这两个类型参数,那么该静态方法的类型参数最后保存的是 Integer 类型 ,此时先前调用静态方法的对象将会因为类型不匹配,而出现运行时异常。所以,为了杜绝这种情况,一开始就会提示不能在静态方法中使用泛型。


参考文章

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值