泛型的使用

本文详细介绍了Java泛型的各个方面,包括定义、泛型接口、泛型类、子类派生、类型通配符的上限和下限、泛型方法、泛型构造器以及类型擦除等核心概念。通过实例解析了泛型在实际编程中的应用,帮助开发者更好地理解和使用Java泛型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、泛型(Generic)的定义

  允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型 )将在声明变量、创建对象、调用方法时动态地指定。

  Java泛型的设计原则是,只要代码在编译时没有出现“[unchecked] 未经检查的转换”的警告,就不会遇到运行时ClassCastException异常。

二、定义泛型接口

定义:

interface Foo<T>{
    public void test(T t);
}

使用:

public void test01(){
        //Java9编译器可以推断出匿名内部类内的泛型类型,可以写成 Foo<String> foo=new Foo<>(){...}
        Foo<String> foo=new Foo<String>() {
            @Override
            public void test(String s) {
                System.out.println(s);
            }
        };
        foo.test("泛型类型为String");
    }

三、定义泛型类

定义:

class Apple<T>{
    private T info;
    
    public Apple(){}
    
    public Apple(T info){
        this.info=info;
    }
    
    public T getInfo(){
        return info;
    }
    
    public void setInfo(T info){
        this.info=info;
    }
}

使用:

  public void test02(){
        //使用有参构造
        Apple<String> a = new Apple<>("苹果 ");
        System.out.println(a.getInfo());
        //使用无参构造,必须在调用构造器时指定类型
        Apple<Double> b=new Apple<Double>();
        b.setInfo(2.4);
        System.out.println(b.getInfo());
    }

四、从泛型类派生子类

  定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。
错误写法:

public class A extends Apple<T>{...}

正确写法:

public class A extends Apple<String>{...}
//此种省略泛型的写法会使用原始类型,即Object
public class B extends Apple{...}

五、并不存在泛型类

可以看到以下情况:

ArrayList<String> list1 = new ArrayList<>();
ArrayList<Double> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass());//true

  不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,类信息在内存中也只占用一块内存空间。
因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参
由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类
错误用法:

public class Apple<T>{
  static T name;
  static void Test(T a){
    System.out.println(a);
  }
}

后文介绍如何在静态方法中使用泛型形参。

六、类型通配符

1、通配符的简单应用
  问号(?)被称为通配符,表示未知类型,它的元素类型可以匹配任何类型。
使用:

 public void test02(){
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        testList(list);
    }
    public void testList(List<?> list){
    	//list.add("2")  编译错误
        System.out.println(list);
    }

以下用法将引起编译错误:

 public void test02(){
        List<?> list=new ArrayList<String>();
        //编译错误,不能添加元素
        list.add("1");
    }

因为程序无法确定list集合中元素的类型,所以不能向其中添加对象。根据List接口定义的代码可以发现:add()方法有类型 参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。使用get()方法获取list任意位置上的实例,其返回值类型为Object。

2、设定类型通配符的上限

<? extends Object>表示匹配所有Object的子类,此时的“?”必须是Object的子类型
设定:
abstract class Shape
class Circle extends Shape
class Rectangle extends Shape
public void drawAll(List<Shape> shapes){...}//方法1
public void draw_All(List<? extends Shape> shapes){...}//方法2
则 :
List<Circle>List<Rectangle>不是List<Shape>的子类型,因此不能使用方法1
List<Circle>List<Rectangle>List<? extends Shape>的子类型,因此可以使用方法2

类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。但此时get()方法的返回值类型是Shape类型。
协变:
  比如Foo_1是Bar的子类,这样A<Foo_1>就相当于A<? extends Bar>的 子类,可以将A<Foo_1>赋值给A<? extends Bar>类型的变量,这种型变方式被称为协变。

  对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。

3、设定通配符的下限
  可以使用<? super Class_>的方式设定通配符的下限,其作用于通配符的上限相反。此时“?”表示的是Class_的任意父类。
逆变:
  指定通配符的下限就是为了支持类型型变。比如Foo_是Bar_的子 类,当程序需要一个A<? super Foo_>变量时,程序可以将A<Bar_>、A<Object_>赋值给A<? super Foo>类型的变量,这种型变方式被称为逆变。(此处用Object_表示Object)

  对于逆变的泛型而言,它只能调用泛型类型作为参数的方法;而不能调用泛型类型作为返回值类型的方法。

3、设定泛型形参的上限

public class Apple<T extends Number>{
  T col;
  public static void main (String[] args){
  Apple<Integer> ai = new Apple<>();
  Apple<Double> ad = new Apple<>();
  //下面代码将引发编译异常,下面代码试图把String类型传给T形参
  //但String不是Number的子类型,所以引起编译错误
  Apple<String> as = new Apple<>();
 }
}

  上面程序定义了一个Apple泛型类,该Apple类的泛型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。
  在一种更极端的情况下,程序需要为泛型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行,并且实现多个上限接口。

//表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable>{
...
}

  与类同时继承父类、实现接口类似的是,为泛型形参指定多个上限时,所有的接口上限必须位于类上限之后。

T,K,V,E,?通配符含义:https://juejin.cn/post/6844903917835419661

七、泛型方法

1、定义泛型方法
泛型方法,可以在声明方法时定义一个或多个泛型形参。泛型方法声明格式:

修饰符<T,S> 返回值类型 方法名(参数列表){
   ...
}

不难发现泛型方法的方法签名比普通方法的方法签名多了泛型形参声明,泛型形参声明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,所有的泛型形参声明放在方法修饰符和方法返回值类型之间。泛型形参声明必须显式声明,不能使用通配符。
例:

   public static<S,T> void test03(S a,T b){
        System.out.println(a);
        System.out.println(b);
    }

与接口、类声明 中定义的泛型不同的是,方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型则可以在整个接口、类中使用。
情况1:

  public void test02(){
        test03("a",4);
    }
    static<T> void test03(T a,T b){
        System.out.println(a.getClass());//class java.lang.String
        System.out.println(b.getClass());//class java.lang.Integer
    }

以上程序中参数a和b虽然都使用了泛型参数T,但可以接受不同类型的值,因为编译器将T认定为Object类型。但要注意以下情况:

    public void test02(){
       String[] s=new String[2];
       test03(s,1);
    }
    static<T> void test03(T[] a,T b){
        a[0]=b;
        System.out.println(a[0]);
 //将引发java.lang.ArrayStoreException: java.lang.Integer异常
 //只有当a的类型与b的类型相同或是b类型的父类型才可以
    }

情况2:

  public void test02(){
       String[] s=new String[2];
       Collection<Integer> nc=new ArrayList<>();
       Collection<String> sc=new ArrayList<>();
       Collection<Object> oc=new ArrayList<>();
       //以下,T为Object类型
       test03(s,oc);
       //以下,T为String类型
       test03(s,sc);
       //以下,将出现编译错误,编译器无法推断出T代表的类型
       test03(s,nc);
       //以下,将出现编译错误,编译器无法推断出T代表的类型,要求传入的必须是同类型 
       //Collection<String>不是Collection<Object>的子类型
       test04(sc,oc);
       //以下,T为Object类型
       test05(sc,oc);
    }

    static<T> void test03(T[] a, Collection<T> b){
    }
    static<T> void test04(Collection<T> a, Collection<T> b){
    }
    static<T> void test05(Collection<extends T> a, Collection<T> b){
    }

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参 数。编译器根据实参推断出泛型所代表的类型,它通常推断出最直接的类型。

2、泛型构造器
  正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。
  一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。如下所示 :

class Foo{
   public<T> Foo(T t){
   System.out.println(t);
   }
}
public class Test{
	public static void main(String[] args){
		new Foo("String");//T为String类型
		new <Integer>Foo(2);//T为Integer类型		
	}
}

  Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛 型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。如下程序所示:

class MyClass<E>{
	public <T> MyClass(T t){
	System.out.println("t参数的值为: " + t);
	}
}
public class GenericDiamondTest{
	public static void main(String[] args){
	// MyClass 类声明中的E形参是String类型
	//泛型构造器中声明的T形参是Integer类型
	MyClass<String> mcl = new MyClass<>(5);
	//显式指定泛型构造器中声明的T形参是Integer类型
	MyClass<String> mc2 = new <Integer> MyClass<String> (5);
	// MyClass 类声明中的E形参是String类型
	//如果显式指定泛型构造器中声明的T形参是Integer类型
	//此时就不能使用“菱形”语法,下面代码是错的
	MyClass<String> mc3 = new <Integer> MyClass<> (5);
	}
}

3、泛型方法与方法重载
  因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义:

public class MyUtils{

public static <T> void copy (Collection<T> dest, Collection<? extends T> src){...}

public static <T> T copy (Collection<? super T> dest, Collection<T> src){..}
}

如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
MyUtils.copy(ln,li);//编译错误

4、类型擦除
  在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。
  当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。
  Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。
详见:https://www.cnblogs.com/wuqinglong/p/9456193.html

5、泛型与数组
  Java泛型有一个很重要的设计原则:如果一段代码在编译时没有提出“未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List<String_> []形式的数组,但不能创建ArrayList<String_>[10]这样的数组对象。见以下三段代码:

//下面代码实际上是不允许的
List<String>[] 1sa = new ArrayList<String>[10]; 
//将lsa向上转型为0bject[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
//将List<Integer>对象作为oa的第二个元素
//下面代码没有任何警告
oa[1] = li;
//下面代码也不会有任何警告,但将引发ClassCastException异常
String s = lsa[1].get(0); 
//下面代码编译时有“[unchecked]未经检查的转换”警告
List<String>[]  lsa = new ArrayList[10];
//将lsa向上转型为Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
//下面代码引起ClassCastException异常
String s = lsa[1].get(0);
List<?>[] lsa = new ArrayList<?>[10];
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);
oa[1] = li;
Object target = lsa[1].get(0);
if (target instanceof String)
//下面代码安全了
String S=(String) target;

与此类似的是,创建元素类型是泛型类型的数组对象也将导致编译错误。如下代码所示。

public<T> T[] makeArray (Collection<T> coll){
//下面代码导致编译错误
return new T[coll.size()];
}

由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器会报错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值